using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Domain.Entities.Members; using Domain.Entities.Payments.ValueObject; namespace Domain.Entities.Payments; public class PaymentOrder { [ForeignKey(nameof(MemberID))] public virtual Member? Member { get; private set; } [Key] public int ID { get; private set; } public int MemberID { get; private set; } /// 가맹점 주문번호 (unique, 중복 불가) public string OrderID { get; private set; } = default!; /// PG 가맹점 식별자 public string MerchantID { get; private set; } = default!; /// 상품명 public string OrderName { get; private set; } = "캐시 충전"; /// PG 거래 키 (승인 후 저장 — Toss paymentKey) public string? TransactionID { get; private set; } /// PG 결제 총액 (= PointAmount + VatAmount, 부가세 별도 가산) public int Amount { get; private set; } /// 충전될 포인트 (사용자 입력값, VAT 제외) public int PointAmount { get; private set; } /// 부가세 (= PointAmount × 10%) public int VatAmount { get; private set; } public PaymentMethod PaymentMethod { get; private set; } public PaymentStatus Status { get; private set; } public DateTime? PaidAt { get; private set; } public DateTime? CancelledAt { get; private set; } public string? FailReason { get; private set; } /// 가상계좌: 계좌번호 public string? VirtualAccountNumber { get; private set; } /// 가상계좌: 은행명 public string? VirtualAccountBank { get; private set; } /// 가상계좌: 예금주 public string? VirtualAccountHolder { get; private set; } /// 가상계좌: 입금 기한 public DateTime? VirtualAccountExpireAt { get; private set; } public DateTime CreatedAt { get; private set; } = DateTime.UtcNow; private PaymentOrder() { } /// /// 주문 생성. 는 사용자가 충전하려는 포인트(VAT 제외). /// VAT 10%는 별도 가산되며 PG 결제 총액 = pointAmount + vat. /// public static PaymentOrder Create( int memberID, string orderID, string merchantID, int pointAmount, PaymentMethod method, string orderName = "캐시 충전" ) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(memberID); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(pointAmount); if (string.IsNullOrWhiteSpace(orderID)) { throw new ArgumentException("orderID required", nameof(orderID)); } if (string.IsNullOrWhiteSpace(merchantID)) { throw new ArgumentException("merchantID required", nameof(merchantID)); } // VAT 10% 별도 가산. PG 결제 총액 = pointAmount + vat. var vat = (int)Math.Round(pointAmount * 0.1); return new PaymentOrder { MemberID = memberID, OrderID = orderID, MerchantID = merchantID, OrderName = orderName, Amount = pointAmount + vat, PointAmount = pointAmount, VatAmount = vat, PaymentMethod = method, Status = PaymentStatus.Pending }; } public void MarkPaid(string transactionID) { // NeedsReconciliation 재승인(재확인) 성공 시에도 Paid 로 전이 가능해야 한다. (Phase 1.5) if (Status is not PaymentStatus.Pending and not PaymentStatus.WaitingDeposit and not PaymentStatus.NeedsReconciliation) { throw new InvalidOperationException($"Cannot mark as paid from status {Status}"); } TransactionID = transactionID; Status = PaymentStatus.Paid; PaidAt = DateTime.UtcNow; } public void MarkFailed(string reason) { Status = PaymentStatus.Failed; FailReason = reason; } /// /// PG 승인 응답이 불명(통신 타임아웃 등)일 때 호출. 다날이 실제 승인했을 수 있어 Failed 로 단정하지 않고 /// 대사 대상으로 표시한다. Pending/WaitingDeposit(첫 시도) 또는 NeedsReconciliation(재확인도 다시 불명)에서만 전이. (Phase 1.5) /// public void MarkNeedsReconciliation(string reason) { if (Status is not PaymentStatus.Pending and not PaymentStatus.WaitingDeposit and not PaymentStatus.NeedsReconciliation) { throw new InvalidOperationException($"Cannot mark as needs-reconciliation from status {Status}"); } Status = PaymentStatus.NeedsReconciliation; FailReason = reason; } public void MarkCancelled() { if (Status != PaymentStatus.Paid) { throw new InvalidOperationException($"Cannot cancel from status {Status}"); } Status = PaymentStatus.Cancelled; CancelledAt = DateTime.UtcNow; } public void MarkWaitingDeposit(string accountNumber, string bank, string holder, DateTime expireAt) { Status = PaymentStatus.WaitingDeposit; VirtualAccountNumber = accountNumber; VirtualAccountBank = bank; VirtualAccountHolder = holder; VirtualAccountExpireAt = expireAt; } }