Order.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. using System.ComponentModel.DataAnnotations;
  2. using System.ComponentModel.DataAnnotations.Schema;
  3. using Domain.Entities.Members;
  4. using Domain.Entities.Store.ValueObject;
  5. namespace Domain.Entities.Store;
  6. /// <summary>
  7. /// 상점 주문 헤더. 단일 결제 = 단일 Order.
  8. /// 결제 시점에 수익 배분 (PlatformFee/GameRevenue/ChannelReward) 동결.
  9. /// 채널 미선택 주문은 ChannelRewardAmount=0, ChannelID=null. ChannelReward 만큼 PlatformFee 가 증가 (antooza 100%).
  10. /// </summary>
  11. public class Order
  12. {
  13. [ForeignKey(nameof(MemberID))]
  14. public virtual Member? Member { get; private set; }
  15. [ForeignKey(nameof(ChannelID))]
  16. public virtual Channel? Channel { get; private set; }
  17. private readonly List<OrderItem> _items = [];
  18. public IReadOnlyCollection<OrderItem> Items => _items;
  19. private readonly List<OrderRefund> _refunds = [];
  20. public IReadOnlyCollection<OrderRefund> Refunds => _refunds;
  21. [Key]
  22. public int ID { get; private set; }
  23. /// <summary>주문 번호 (예: "20260522-A1B2C3"). UNIQUE. 사용자 표시용.</summary>
  24. public string OrderNumber { get; private set; } = default!;
  25. public int MemberID { get; private set; }
  26. /// <summary>후원 채널 (선택). null 이면 antooza 100% 수익.</summary>
  27. public int? ChannelID { get; private set; }
  28. public int TotalAmount { get; private set; }
  29. /// <summary>antooza 몫 (= TotalAmount - GameRevenueAmount - ChannelRewardAmount).</summary>
  30. public int PlatformFeeAmount { get; private set; }
  31. /// <summary>게임사 정산 큐 적재 금액 (= TotalAmount × Game.CommissionRate / 100).</summary>
  32. public int GameRevenueAmount { get; private set; }
  33. /// <summary>[비활성] 채널 보상 입금 금액 — 채널 보상 적립 제거됨(2026-07-03), 과거 주문 호환용 유지. 신규 주문은 항상 0.</summary>
  34. public int ChannelRewardAmount { get; private set; }
  35. public OrderStatus Status { get; private set; } = OrderStatus.Pending;
  36. public string? CancelReason { get; private set; }
  37. public string? RefundReason { get; private set; }
  38. public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
  39. public DateTime? PaidAt { get; private set; }
  40. [Timestamp]
  41. public byte[] RowVersion { get; private set; } = default!;
  42. private Order() { }
  43. public static Order Create(
  44. string orderNumber,
  45. int memberID,
  46. int totalAmount,
  47. int? channelID,
  48. int platformFeeAmount,
  49. int gameRevenueAmount,
  50. int channelRewardAmount
  51. ) {
  52. if (string.IsNullOrWhiteSpace(orderNumber))
  53. {
  54. throw new ArgumentException("OrderNumber is required.", nameof(orderNumber));
  55. }
  56. if (orderNumber.Length > 20)
  57. {
  58. throw new ArgumentOutOfRangeException(nameof(orderNumber));
  59. }
  60. if (memberID <= 0)
  61. {
  62. throw new ArgumentOutOfRangeException(nameof(memberID));
  63. }
  64. if (totalAmount <= 0)
  65. {
  66. throw new ArgumentOutOfRangeException(nameof(totalAmount));
  67. }
  68. if (platformFeeAmount < 0 || gameRevenueAmount < 0 || channelRewardAmount < 0)
  69. {
  70. throw new ArgumentOutOfRangeException(null, "수익 배분 금액은 음수 불가.");
  71. }
  72. if (platformFeeAmount + gameRevenueAmount + channelRewardAmount != totalAmount)
  73. {
  74. throw new ArgumentException("PlatformFee + GameRevenue + ChannelReward 의 합이 TotalAmount 와 일치해야 합니다.");
  75. }
  76. if (channelID is null && channelRewardAmount > 0)
  77. {
  78. throw new ArgumentException("채널 미선택 주문은 ChannelRewardAmount=0 이어야 합니다.");
  79. }
  80. return new Order
  81. {
  82. OrderNumber = orderNumber,
  83. MemberID = memberID,
  84. ChannelID = channelID,
  85. TotalAmount = totalAmount,
  86. PlatformFeeAmount = platformFeeAmount,
  87. GameRevenueAmount = gameRevenueAmount,
  88. ChannelRewardAmount = channelRewardAmount
  89. };
  90. }
  91. public void AddItem(OrderItem item)
  92. {
  93. ArgumentNullException.ThrowIfNull(item);
  94. _items.Add(item);
  95. }
  96. public void MarkPaid()
  97. {
  98. if (Status != OrderStatus.Pending)
  99. {
  100. throw new InvalidOperationException($"Cannot mark paid from status {Status}.");
  101. }
  102. Status = OrderStatus.Paid;
  103. PaidAt = DateTime.UtcNow;
  104. }
  105. /// <summary>Digital 상품 단독 주문 시 Paid → Delivered 점프.</summary>
  106. public void MarkDelivered()
  107. {
  108. if (Status is not OrderStatus.Paid and not OrderStatus.Shipped)
  109. {
  110. throw new InvalidOperationException($"Cannot mark delivered from status {Status}.");
  111. }
  112. Status = OrderStatus.Delivered;
  113. }
  114. public void MarkPreparing()
  115. {
  116. if (Status != OrderStatus.Paid)
  117. {
  118. throw new InvalidOperationException($"Cannot mark preparing from status {Status}.");
  119. }
  120. Status = OrderStatus.Preparing;
  121. }
  122. public void MarkShipped()
  123. {
  124. if (Status is not OrderStatus.Paid and not OrderStatus.Preparing)
  125. {
  126. throw new InvalidOperationException($"Cannot mark shipped from status {Status}.");
  127. }
  128. Status = OrderStatus.Shipped;
  129. }
  130. public void Cancel(string reason)
  131. {
  132. if (Status is not OrderStatus.Pending and not OrderStatus.Paid)
  133. {
  134. throw new InvalidOperationException($"Cannot cancel from status {Status}.");
  135. }
  136. if (string.IsNullOrWhiteSpace(reason))
  137. {
  138. throw new ArgumentException("Cancel reason is required.", nameof(reason));
  139. }
  140. Status = OrderStatus.Cancelled;
  141. CancelReason = reason;
  142. }
  143. public void MarkRefunded(string reason)
  144. {
  145. if (string.IsNullOrWhiteSpace(reason))
  146. {
  147. throw new ArgumentException("Refund reason is required.", nameof(reason));
  148. }
  149. Status = OrderStatus.Refunded;
  150. RefundReason = reason;
  151. }
  152. public void MarkExchanged()
  153. {
  154. Status = OrderStatus.Exchanged;
  155. }
  156. }