PaymentOrder.cs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. using System.ComponentModel.DataAnnotations;
  2. using System.ComponentModel.DataAnnotations.Schema;
  3. using Domain.Entities.Members;
  4. using Domain.Entities.Payments.ValueObject;
  5. namespace Domain.Entities.Payments;
  6. public class PaymentOrder
  7. {
  8. [ForeignKey(nameof(MemberID))]
  9. public virtual Member? Member { get; private set; }
  10. [Key]
  11. public int ID { get; private set; }
  12. public int MemberID { get; private set; }
  13. /// <summary>가맹점 주문번호 (unique, 중복 불가)</summary>
  14. public string OrderID { get; private set; } = default!;
  15. /// <summary>다날 CPID (계약 완료 후 발급)</summary>
  16. public string MerchantID { get; private set; } = default!;
  17. /// <summary>상품명</summary>
  18. public string OrderName { get; private set; } = "포인트 충전";
  19. /// <summary>다날 거래번호 (승인 후 저장)</summary>
  20. public string? TransactionID { get; private set; }
  21. /// <summary>PG 결제 총액 (= PointAmount + VatAmount, 부가세 별도 가산)</summary>
  22. public int Amount { get; private set; }
  23. /// <summary>충전될 포인트 (사용자 입력값, VAT 제외)</summary>
  24. public int PointAmount { get; private set; }
  25. /// <summary>부가세 (= PointAmount × 10%)</summary>
  26. public int VatAmount { get; private set; }
  27. public PaymentMethod PaymentMethod { get; private set; }
  28. public PaymentStatus Status { get; private set; }
  29. public DateTime? PaidAt { get; private set; }
  30. public DateTime? CancelledAt { get; private set; }
  31. public string? FailReason { get; private set; }
  32. /// <summary>가상계좌: 계좌번호</summary>
  33. public string? VirtualAccountNumber { get; private set; }
  34. /// <summary>가상계좌: 은행명</summary>
  35. public string? VirtualAccountBank { get; private set; }
  36. /// <summary>가상계좌: 예금주</summary>
  37. public string? VirtualAccountHolder { get; private set; }
  38. /// <summary>가상계좌: 입금 기한</summary>
  39. public DateTime? VirtualAccountExpireAt { get; private set; }
  40. public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
  41. private PaymentOrder() { }
  42. /// <summary>
  43. /// 주문 생성. <paramref name="pointAmount"/>는 사용자가 충전하려는 포인트(VAT 제외).
  44. /// VAT 10%는 별도 가산되며 PG 결제 총액 = pointAmount + vat.
  45. /// </summary>
  46. public static PaymentOrder Create(
  47. int memberID,
  48. string orderID,
  49. string merchantID,
  50. int pointAmount,
  51. PaymentMethod method,
  52. string orderName = "포인트 충전"
  53. ) {
  54. ArgumentOutOfRangeException.ThrowIfNegativeOrZero(memberID);
  55. ArgumentOutOfRangeException.ThrowIfNegativeOrZero(pointAmount);
  56. if (string.IsNullOrWhiteSpace(orderID)) {
  57. throw new ArgumentException("orderID required", nameof(orderID));
  58. }
  59. if (string.IsNullOrWhiteSpace(merchantID)) {
  60. throw new ArgumentException("merchantID required", nameof(merchantID));
  61. }
  62. // VAT 10% 별도 가산. PG 결제 총액 = pointAmount + vat.
  63. var vat = (int)Math.Round(pointAmount * 0.1);
  64. return new PaymentOrder
  65. {
  66. MemberID = memberID,
  67. OrderID = orderID,
  68. MerchantID = merchantID,
  69. OrderName = orderName,
  70. Amount = pointAmount + vat,
  71. PointAmount = pointAmount,
  72. VatAmount = vat,
  73. PaymentMethod = method,
  74. Status = PaymentStatus.Pending
  75. };
  76. }
  77. public void MarkPaid(string transactionID)
  78. {
  79. // NeedsReconciliation 재승인(재확인) 성공 시에도 Paid 로 전이 가능해야 한다. (Phase 1.5)
  80. if (Status is not PaymentStatus.Pending and not PaymentStatus.WaitingDeposit and not PaymentStatus.NeedsReconciliation)
  81. {
  82. throw new InvalidOperationException($"Cannot mark as paid from status {Status}");
  83. }
  84. TransactionID = transactionID;
  85. Status = PaymentStatus.Paid;
  86. PaidAt = DateTime.UtcNow;
  87. }
  88. public void MarkFailed(string reason)
  89. {
  90. Status = PaymentStatus.Failed;
  91. FailReason = reason;
  92. }
  93. /// <summary>
  94. /// PG 승인 응답이 불명(통신 타임아웃 등)일 때 호출. 다날이 실제 승인했을 수 있어 Failed 로 단정하지 않고
  95. /// 대사 대상으로 표시한다. Pending/WaitingDeposit(첫 시도) 또는 NeedsReconciliation(재확인도 다시 불명)에서만 전이. (Phase 1.5)
  96. /// </summary>
  97. public void MarkNeedsReconciliation(string reason)
  98. {
  99. if (Status is not PaymentStatus.Pending and not PaymentStatus.WaitingDeposit and not PaymentStatus.NeedsReconciliation)
  100. {
  101. throw new InvalidOperationException($"Cannot mark as needs-reconciliation from status {Status}");
  102. }
  103. Status = PaymentStatus.NeedsReconciliation;
  104. FailReason = reason;
  105. }
  106. public void MarkCancelled()
  107. {
  108. if (Status != PaymentStatus.Paid)
  109. {
  110. throw new InvalidOperationException($"Cannot cancel from status {Status}");
  111. }
  112. Status = PaymentStatus.Cancelled;
  113. CancelledAt = DateTime.UtcNow;
  114. }
  115. public void MarkWaitingDeposit(string accountNumber, string bank, string holder, DateTime expireAt)
  116. {
  117. Status = PaymentStatus.WaitingDeposit;
  118. VirtualAccountNumber = accountNumber;
  119. VirtualAccountBank = bank;
  120. VirtualAccountHolder = holder;
  121. VirtualAccountExpireAt = expireAt;
  122. }
  123. }