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;
}
}