Wallet.cs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. using Domain.Entities.Common.ValueObject;
  2. using Domain.Entities.Members;
  3. using Domain.Entities.Wallets.Policy;
  4. using Domain.Entities.Wallets.ValueObject;
  5. namespace Domain.Entities.Wallets
  6. {
  7. public class Wallet
  8. {
  9. public virtual Member Member { get; private set; } = null!;
  10. private readonly List<WalletBalance> _balances = [];
  11. public IReadOnlyCollection<WalletBalance> Balances => _balances;
  12. private readonly List<WalletTransaction> _transactions = [];
  13. public IReadOnlyCollection<WalletTransaction> Transactions => _transactions;
  14. public int ID { get; private set; }
  15. public Guid WalletKey { get; private set; } = Guid.NewGuid();
  16. public int MemberID { get; private set; }
  17. public DateTime? UpdatedAt { get; private set; }
  18. public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
  19. private Wallet() { }
  20. private Wallet(int memberID)
  21. {
  22. if (memberID <= 0)
  23. {
  24. throw new ArgumentOutOfRangeException(nameof(memberID));
  25. }
  26. MemberID = memberID;
  27. // 지갑 생성 시 구분별 잔액 기본 생성
  28. EnsureBalance(WalletBalanceType.PgCharged);
  29. EnsureBalance(WalletBalanceType.Deposit);
  30. EnsureBalance(WalletBalanceType.Donation);
  31. EnsureBalance(WalletBalanceType.Reward);
  32. EnsureBalance(WalletBalanceType.Airdrop);
  33. EnsureBalance(WalletBalanceType.Locked);
  34. EnsureBalance(WalletBalanceType.Adjusted);
  35. }
  36. public static Wallet Create(int memberID) => new(memberID);
  37. public Money GetBalance(WalletBalanceType type) => GetBalanceEntity(type).Amount;
  38. public Money GetTotalAvailable()
  39. {
  40. var total = Money.KRW(0);
  41. foreach (var b in Balances.Where(x => x.Type != WalletBalanceType.Locked))
  42. total += b.Amount;
  43. return total;
  44. }
  45. // ---- Credit ----
  46. public void CreditPgCharge(Money amount, string reason = "PG_CHARGE", string? refID = null)
  47. => Credit(WalletBalanceType.PgCharged, WalletTransactionType.Charge, amount, reason, refID);
  48. public void CreditDonationIn(Money amount, string reason, string? refID = null)
  49. => Credit(WalletBalanceType.Donation, WalletTransactionType.DonationIn, amount, reason, refID);
  50. public void CreditReward(Money amount, string reason = "REWARD", string? refID = null)
  51. => Credit(WalletBalanceType.Reward, WalletTransactionType.RewardEarned, amount, reason, refID);
  52. private void Credit(
  53. WalletBalanceType balanceType,
  54. WalletTransactionType txType,
  55. Money amount,
  56. string reason,
  57. string? refID
  58. ) {
  59. EnsureMoney(amount);
  60. var balance = EnsureBalance(balanceType);
  61. balance.Increase(amount);
  62. _transactions.Add(WalletTransaction.Create(
  63. walletKey: WalletKey,
  64. balanceType: balanceType,
  65. txType: txType,
  66. amount: amount,
  67. balanceAfter: balance.Amount,
  68. reason: reason,
  69. refID: refID
  70. ));
  71. }
  72. // ---- Debit ----
  73. public void DebitDonationOut(Money amount, string reason, string? refID = null)
  74. => DebitSingle(WalletBalanceType.Donation, WalletTransactionType.DonationOut, amount, reason, refID);
  75. // ---- Spend (정책 순서대로 분할 차감) ----
  76. public void Spend(Money amount, string reason = "SPEND", string? refID = null)
  77. {
  78. EnsureMoney(amount);
  79. var remaining = amount;
  80. foreach (var type in SpendPolicy.DefaultSpendOrder)
  81. {
  82. if (remaining.IsZero) break;
  83. var balance = EnsureBalance(type);
  84. if (balance.Amount.IsZero) continue;
  85. var takeValue = Math.Min(balance.Amount.Value, remaining.Value);
  86. if (takeValue <= 0) continue;
  87. var take = Money.KRW(takeValue);
  88. balance.Decrease(take);
  89. remaining = remaining - take;
  90. _transactions.Add(WalletTransaction.Create(
  91. walletKey: WalletKey,
  92. balanceType: type,
  93. txType: WalletTransactionType.Spend,
  94. amount: take,
  95. balanceAfter: balance.Amount,
  96. reason: reason,
  97. refID: refID
  98. ));
  99. }
  100. if (!remaining.IsZero)
  101. {
  102. throw new InvalidOperationException("Insufficient balance for spending.");
  103. }
  104. }
  105. // ---- Lock/Unlock ----
  106. public void LockForWithdrawal(Money amount, string reason = "WITHDRAW_REQUEST", string? refID = null)
  107. {
  108. EnsureMoney(amount);
  109. var from = EnsureBalance(WalletBalanceType.Donation);
  110. var locked = EnsureBalance(WalletBalanceType.Locked);
  111. from.Decrease(amount);
  112. _transactions.Add(WalletTransaction.Create(WalletKey, WalletBalanceType.Donation, WalletTransactionType.Lock, amount, from.Amount, reason, refID));
  113. locked.Increase(amount);
  114. _transactions.Add(WalletTransaction.Create(WalletKey, WalletBalanceType.Locked, WalletTransactionType.Lock, amount, locked.Amount, reason, refID));
  115. }
  116. public void UnlockWithdrawal(Money amount, string reason = "WITHDRAW_CANCEL", string? refID = null)
  117. {
  118. EnsureMoney(amount);
  119. var locked = EnsureBalance(WalletBalanceType.Locked);
  120. var donation = EnsureBalance(WalletBalanceType.Donation);
  121. locked.Decrease(amount);
  122. _transactions.Add(WalletTransaction.Create(WalletKey, WalletBalanceType.Locked, WalletTransactionType.Unlock, amount, locked.Amount, reason, refID));
  123. donation.Increase(amount);
  124. _transactions.Add(WalletTransaction.Create(WalletKey, WalletBalanceType.Donation, WalletTransactionType.Unlock, amount, donation.Amount, reason, refID));
  125. }
  126. // ---- Adjust ----
  127. public void AdjustIncrease(Money amount, string reason, string? refID = null, string? memo = null)
  128. {
  129. EnsureMoney(amount);
  130. if (string.IsNullOrWhiteSpace(reason))
  131. {
  132. throw new ArgumentException("Adjustment reason is required.", nameof(reason));
  133. }
  134. var balance = EnsureBalance(WalletBalanceType.Adjusted);
  135. balance.Increase(amount);
  136. _transactions.Add(WalletTransaction.Create(
  137. walletKey: WalletKey,
  138. balanceType: WalletBalanceType.Adjusted,
  139. txType: WalletTransactionType.Adjusted,
  140. amount: amount,
  141. balanceAfter: balance.Amount,
  142. reason: $"ADJUST_IN:{reason}",
  143. refID: refID,
  144. memo: memo
  145. ));
  146. }
  147. public void AdjustDecrease(Money amount, string reason, string? refID = null, string? memo = null)
  148. {
  149. EnsureMoney(amount);
  150. if (string.IsNullOrWhiteSpace(reason))
  151. {
  152. throw new ArgumentException("Adjustment reason is required.", nameof(reason));
  153. }
  154. var balance = EnsureBalance(WalletBalanceType.Adjusted);
  155. balance.Decrease(amount);
  156. _transactions.Add(WalletTransaction.Create(
  157. walletKey: WalletKey,
  158. balanceType: WalletBalanceType.Adjusted,
  159. txType: WalletTransactionType.Adjusted,
  160. amount: amount,
  161. balanceAfter: balance.Amount,
  162. reason: $"ADJUST_OUT:{reason}",
  163. refID: refID,
  164. memo: memo
  165. ));
  166. }
  167. // ---- Internal ----
  168. private void DebitSingle(WalletBalanceType balanceType, WalletTransactionType txType, Money amount, string reason, string? refID)
  169. {
  170. EnsureMoney(amount);
  171. var balance = EnsureBalance(balanceType);
  172. balance.Decrease(amount);
  173. _transactions.Add(WalletTransaction.Create(
  174. walletKey: WalletKey,
  175. balanceType: balanceType,
  176. txType: txType,
  177. amount: amount,
  178. balanceAfter: balance.Amount,
  179. reason: reason,
  180. refID: refID
  181. ));
  182. }
  183. private WalletBalance GetBalanceEntity(WalletBalanceType type)
  184. {
  185. var found = _balances.SingleOrDefault(x => x.Type == type);
  186. if (found is null)
  187. {
  188. throw new InvalidOperationException($"Balance type '{type}' not initialized.");
  189. }
  190. return found;
  191. }
  192. private WalletBalance EnsureBalance(WalletBalanceType type)
  193. {
  194. var found = _balances.SingleOrDefault(x => x.Type == type);
  195. if (found != null)
  196. {
  197. return found;
  198. }
  199. var created = WalletBalance.Create(WalletKey, type);
  200. _balances.Add(created);
  201. return created;
  202. }
  203. private static void EnsureMoney(Money amount)
  204. {
  205. if (amount.IsZero || amount.Value <= 0)
  206. {
  207. throw new ArgumentException("Amount must be positive.", nameof(amount));
  208. }
  209. }
  210. }
  211. }