Wallet.cs 9.5 KB

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