using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Domain.Entities.Store.ValueObject; namespace Domain.Entities.Store; /// /// 통합 환불 이력 (취소/반품/교환/환불). Order N:N (한 주문에 여러 환불 가능). /// 흐름: Requested -> Approved (관리자) -> Completed (실제 처리, Wallet/재고/쿠폰 환원). /// public class OrderRefund { [ForeignKey(nameof(OrderID))] public virtual Order? Order { get; private set; } /// /// 환불 단위 — 쿠폰 별 후처리(회수/만료) 정책. OrderRefund 1건에 N개의 OrderRefundItem. /// 자동 환불(Cancel)이나 옛 환불은 비어있을 수 있음 (Amount 만 사용). /// public virtual ICollection Items { get; private set; } = new List(); [Key] public int ID { get; private set; } public int OrderID { get; private set; } public RefundType Type { get; private set; } /// 사유 유형. RefundType 과 직교한 표준화 카테고리. 통계·필터링에 사용. public RefundReasonType ReasonType { get; private set; } = RefundReasonType.Other; public RefundStatus Status { get; private set; } = RefundStatus.Requested; public int Amount { get; private set; } /// 사용자가 직접 작성한 상세 사유. ReasonType 외 보조 설명. ReasonType=Other 인 경우 호출자가 필수 처리. public string Reason { get; private set; } = default!; public string? AdminMemo { get; private set; } /// 처리 관리자 (ASP.NET Identity ApplicationUser.Id). 자동 환불(Cancel)은 null. public string? AdminUserId { get; private set; } /// 처리 관리자 Email 스냅샷 (표시용). Member FK 없이 감사 표시 유지. public string? AdminEmail { get; private set; } public DateTime RequestedAt { get; private set; } = DateTime.UtcNow; public DateTime? ResolvedAt { get; private set; } private OrderRefund() { } public static OrderRefund Create(int orderID, RefundType type, RefundReasonType reasonType, int amount, string reason) { if (orderID <= 0) { throw new ArgumentOutOfRangeException(nameof(orderID)); } if (amount <= 0) { throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be positive."); } if (string.IsNullOrWhiteSpace(reason)) { throw new ArgumentException("Reason is required.", nameof(reason)); } if (reason.Length > 500) { throw new ArgumentOutOfRangeException(nameof(reason)); } return new OrderRefund { OrderID = orderID, Type = type, ReasonType = reasonType, Amount = amount, Reason = reason }; } public void Approve(string adminUserId, string? adminEmail, string? adminMemo = null) { if (Status != RefundStatus.Requested) { throw new InvalidOperationException($"Cannot approve from status {Status}."); } if (string.IsNullOrWhiteSpace(adminUserId)) { throw new ArgumentException("AdminUserId is required.", nameof(adminUserId)); } Status = RefundStatus.Approved; AdminUserId = adminUserId; AdminEmail = adminEmail; AdminMemo = adminMemo; } public void Reject(string adminUserId, string? adminEmail, string adminMemo) { if (Status != RefundStatus.Requested) { throw new InvalidOperationException($"Cannot reject from status {Status}."); } if (string.IsNullOrWhiteSpace(adminUserId)) { throw new ArgumentException("AdminUserId is required.", nameof(adminUserId)); } if (string.IsNullOrWhiteSpace(adminMemo)) { throw new ArgumentException("Rejection requires admin memo.", nameof(adminMemo)); } Status = RefundStatus.Rejected; AdminUserId = adminUserId; AdminEmail = adminEmail; AdminMemo = adminMemo; ResolvedAt = DateTime.UtcNow; } public void Complete() { if (Status != RefundStatus.Approved) { throw new InvalidOperationException($"Cannot complete from status {Status}."); } Status = RefundStatus.Completed; ResolvedAt = DateTime.UtcNow; } }