Shipment.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. using System.ComponentModel.DataAnnotations;
  2. using System.ComponentModel.DataAnnotations.Schema;
  3. using Domain.Entities.Store.ValueObject;
  4. namespace Domain.Entities.Store;
  5. /// <summary>
  6. /// 배송 정보. Order 1:1 (Physical 상품 포함 주문 시점에 생성).
  7. /// 주문 시점의 회원 주소 스냅샷을 보존 (회원이 추후 주소 변경해도 영향 없음). 암호화는 MemberAddress 와 동일.
  8. /// </summary>
  9. public class Shipment
  10. {
  11. [ForeignKey(nameof(OrderID))]
  12. public virtual Order? Order { get; private set; }
  13. [Key]
  14. public int ID { get; private set; }
  15. public int OrderID { get; private set; }
  16. // 주소 스냅샷 (암호화)
  17. public string RecipientName_Encrypted { get; private set; } = default!;
  18. public string Phone_Encrypted { get; private set; } = default!;
  19. public string ZipCode { get; private set; } = default!;
  20. public string Address1_Encrypted { get; private set; } = default!;
  21. public string Address2_Encrypted { get; private set; } = default!;
  22. public int KeyVersion { get; private set; }
  23. public ShipmentStatus Status { get; private set; } = ShipmentStatus.Preparing;
  24. public string? Carrier { get; private set; }
  25. public string? TrackingNumber { get; private set; }
  26. /// <summary>배송비 (원) — 관리자 수동 입력. 기본 0.</summary>
  27. public int ShippingFee { get; private set; }
  28. /// <summary>관리자 메모 (운영용 내부 메모, 최대 500자)</summary>
  29. public string? AdminMemo { get; private set; }
  30. public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
  31. public DateTime? ShippedAt { get; private set; }
  32. public DateTime? DeliveredAt { get; private set; }
  33. private Shipment() { }
  34. public static Shipment Create(
  35. int orderID,
  36. string recipientNameEncrypted,
  37. string phoneEncrypted,
  38. string zipCode,
  39. string address1Encrypted,
  40. string address2Encrypted,
  41. int keyVersion
  42. ) {
  43. if (orderID <= 0)
  44. {
  45. throw new ArgumentOutOfRangeException(nameof(orderID));
  46. }
  47. if (string.IsNullOrWhiteSpace(recipientNameEncrypted))
  48. {
  49. throw new ArgumentException("RecipientName_Encrypted is required.", nameof(recipientNameEncrypted));
  50. }
  51. if (string.IsNullOrWhiteSpace(phoneEncrypted))
  52. {
  53. throw new ArgumentException("Phone_Encrypted is required.", nameof(phoneEncrypted));
  54. }
  55. if (string.IsNullOrWhiteSpace(zipCode))
  56. {
  57. throw new ArgumentException("ZipCode is required.", nameof(zipCode));
  58. }
  59. if (zipCode.Length > 10)
  60. {
  61. throw new ArgumentOutOfRangeException(nameof(zipCode));
  62. }
  63. if (string.IsNullOrWhiteSpace(address1Encrypted))
  64. {
  65. throw new ArgumentException("Address1_Encrypted is required.", nameof(address1Encrypted));
  66. }
  67. if (string.IsNullOrWhiteSpace(address2Encrypted))
  68. {
  69. throw new ArgumentException("Address2_Encrypted is required.", nameof(address2Encrypted));
  70. }
  71. if (keyVersion <= 0)
  72. {
  73. throw new ArgumentOutOfRangeException(nameof(keyVersion));
  74. }
  75. return new Shipment
  76. {
  77. OrderID = orderID,
  78. RecipientName_Encrypted = recipientNameEncrypted,
  79. Phone_Encrypted = phoneEncrypted,
  80. ZipCode = zipCode,
  81. Address1_Encrypted = address1Encrypted,
  82. Address2_Encrypted = address2Encrypted,
  83. KeyVersion = keyVersion
  84. };
  85. }
  86. public void RegisterTracking(string carrier, string trackingNumber)
  87. {
  88. if (string.IsNullOrWhiteSpace(carrier))
  89. {
  90. throw new ArgumentException("Carrier is required.", nameof(carrier));
  91. }
  92. if (carrier.Length > 50)
  93. {
  94. throw new ArgumentOutOfRangeException(nameof(carrier));
  95. }
  96. if (string.IsNullOrWhiteSpace(trackingNumber))
  97. {
  98. throw new ArgumentException("TrackingNumber is required.", nameof(trackingNumber));
  99. }
  100. if (trackingNumber.Length > 50)
  101. {
  102. throw new ArgumentOutOfRangeException(nameof(trackingNumber));
  103. }
  104. Carrier = carrier;
  105. TrackingNumber = trackingNumber;
  106. if (Status == ShipmentStatus.Preparing)
  107. {
  108. Status = ShipmentStatus.Pickup;
  109. }
  110. }
  111. public void MarkInTransit()
  112. {
  113. if (Status is not ShipmentStatus.Pickup and not ShipmentStatus.Preparing)
  114. {
  115. throw new InvalidOperationException($"Cannot mark in transit from status {Status}.");
  116. }
  117. Status = ShipmentStatus.InTransit;
  118. ShippedAt = DateTime.UtcNow;
  119. }
  120. public void MarkDelivered()
  121. {
  122. if (Status != ShipmentStatus.InTransit)
  123. {
  124. throw new InvalidOperationException($"Cannot mark delivered from status {Status}.");
  125. }
  126. Status = ShipmentStatus.Delivered;
  127. DeliveredAt = DateTime.UtcNow;
  128. }
  129. public void MarkFailed()
  130. {
  131. Status = ShipmentStatus.Failed;
  132. }
  133. /// <summary>배송비 + 관리자 메모 동시 갱신 (배송 상세 form 에서 호출)</summary>
  134. public void UpdateBillingInfo(int shippingFee, string? adminMemo)
  135. {
  136. if (shippingFee < 0)
  137. {
  138. throw new ArgumentOutOfRangeException(nameof(shippingFee), "ShippingFee must be >= 0.");
  139. }
  140. if (adminMemo is { Length: > 500 })
  141. {
  142. throw new ArgumentOutOfRangeException(nameof(adminMemo), "AdminMemo max length is 500.");
  143. }
  144. ShippingFee = shippingFee;
  145. AdminMemo = string.IsNullOrWhiteSpace(adminMemo) ? null : adminMemo.Trim();
  146. }
  147. }