using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Domain.Entities.Members;
using Domain.Entities.Store.ValueObject;
namespace Domain.Entities.Store;
///
/// 상점 주문 헤더. 단일 결제 = 단일 Order.
/// 결제 시점에 수익 배분 (PlatformFee/GameRevenue/ChannelReward) 동결.
/// 채널 미선택 주문은 ChannelRewardAmount=0, ChannelID=null. ChannelReward 만큼 PlatformFee 가 증가 (antooza 100%).
///
public class Order
{
[ForeignKey(nameof(MemberID))]
public virtual Member? Member { get; private set; }
[ForeignKey(nameof(ChannelID))]
public virtual Channel? Channel { get; private set; }
private readonly List _items = [];
public IReadOnlyCollection Items => _items;
private readonly List _refunds = [];
public IReadOnlyCollection Refunds => _refunds;
[Key]
public int ID { get; private set; }
/// 주문 번호 (예: "20260522-A1B2C3"). UNIQUE. 사용자 표시용.
public string OrderNumber { get; private set; } = default!;
public int MemberID { get; private set; }
/// 선물 수신자 회원 ID (선택). null 이면 본인 구매. 값이 있으면 발급 대상은 수신자, 결제자는 MemberID.
public int? GiftToMemberID { get; private set; }
/// 선물 메시지 (선택, 최대 200자).
public string? GiftMessage { get; private set; }
/// 후원 채널 (선택). null 이면 antooza 100% 수익.
public int? ChannelID { get; private set; }
public int TotalAmount { get; private set; }
/// antooza 몫 (= TotalAmount - GameRevenueAmount - ChannelRewardAmount).
public int PlatformFeeAmount { get; private set; }
/// 게임사 정산 큐 적재 금액 (= TotalAmount × Game.CommissionRate / 100).
public int GameRevenueAmount { get; private set; }
/// [비활성] 채널 보상 입금 금액 — 채널 보상 적립 제거됨(2026-07-03), 과거 주문 호환용 유지. 신규 주문은 항상 0.
public int ChannelRewardAmount { get; private set; }
public OrderStatus Status { get; private set; } = OrderStatus.Pending;
public string? CancelReason { get; private set; }
public string? RefundReason { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? PaidAt { get; private set; }
[Timestamp]
public byte[] RowVersion { get; private set; } = default!;
private Order() { }
public static Order Create(
string orderNumber,
int memberID,
int totalAmount,
int? channelID,
int platformFeeAmount,
int gameRevenueAmount,
int channelRewardAmount,
int? giftToMemberID = null,
string? giftMessage = null
) {
if (string.IsNullOrWhiteSpace(orderNumber))
{
throw new ArgumentException("OrderNumber is required.", nameof(orderNumber));
}
if (orderNumber.Length > 20)
{
throw new ArgumentOutOfRangeException(nameof(orderNumber));
}
if (memberID <= 0)
{
throw new ArgumentOutOfRangeException(nameof(memberID));
}
if (giftToMemberID is <= 0)
{
throw new ArgumentOutOfRangeException(nameof(giftToMemberID));
}
if (giftToMemberID == memberID)
{
throw new ArgumentException("본인에게는 선물할 수 없습니다.", nameof(giftToMemberID));
}
var trimmedGiftMessage = string.IsNullOrWhiteSpace(giftMessage) ? null : giftMessage.Trim();
if (trimmedGiftMessage is { Length: > 200 })
{
throw new ArgumentOutOfRangeException(nameof(giftMessage), "선물 메시지는 200자 이하여야 합니다.");
}
if (totalAmount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(totalAmount));
}
if (platformFeeAmount < 0 || gameRevenueAmount < 0 || channelRewardAmount < 0)
{
throw new ArgumentOutOfRangeException(null, "수익 배분 금액은 음수 불가.");
}
if (platformFeeAmount + gameRevenueAmount + channelRewardAmount != totalAmount)
{
throw new ArgumentException("PlatformFee + GameRevenue + ChannelReward 의 합이 TotalAmount 와 일치해야 합니다.");
}
if (channelID is null && channelRewardAmount > 0)
{
throw new ArgumentException("채널 미선택 주문은 ChannelRewardAmount=0 이어야 합니다.");
}
return new Order
{
OrderNumber = orderNumber,
MemberID = memberID,
GiftToMemberID = giftToMemberID,
GiftMessage = trimmedGiftMessage,
ChannelID = channelID,
TotalAmount = totalAmount,
PlatformFeeAmount = platformFeeAmount,
GameRevenueAmount = gameRevenueAmount,
ChannelRewardAmount = channelRewardAmount
};
}
/// 선물 주문 여부.
public bool IsGift => GiftToMemberID is not null;
/// 아이템 발급 대상 회원 ID (선물이면 수신자, 아니면 구매자).
public int RecipientMemberID => GiftToMemberID ?? MemberID;
public void AddItem(OrderItem item)
{
ArgumentNullException.ThrowIfNull(item);
_items.Add(item);
}
public void MarkPaid()
{
if (Status != OrderStatus.Pending)
{
throw new InvalidOperationException($"Cannot mark paid from status {Status}.");
}
Status = OrderStatus.Paid;
PaidAt = DateTime.UtcNow;
}
/// Digital 상품 단독 주문 시 Paid → Delivered 점프.
public void MarkDelivered()
{
if (Status is not OrderStatus.Paid and not OrderStatus.Shipped)
{
throw new InvalidOperationException($"Cannot mark delivered from status {Status}.");
}
Status = OrderStatus.Delivered;
}
public void MarkPreparing()
{
if (Status != OrderStatus.Paid)
{
throw new InvalidOperationException($"Cannot mark preparing from status {Status}.");
}
Status = OrderStatus.Preparing;
}
public void MarkShipped()
{
if (Status is not OrderStatus.Paid and not OrderStatus.Preparing)
{
throw new InvalidOperationException($"Cannot mark shipped from status {Status}.");
}
Status = OrderStatus.Shipped;
}
public void Cancel(string reason)
{
if (Status is not OrderStatus.Pending and not OrderStatus.Paid)
{
throw new InvalidOperationException($"Cannot cancel from status {Status}.");
}
if (string.IsNullOrWhiteSpace(reason))
{
throw new ArgumentException("Cancel reason is required.", nameof(reason));
}
Status = OrderStatus.Cancelled;
CancelReason = reason;
}
public void MarkRefunded(string reason)
{
if (string.IsNullOrWhiteSpace(reason))
{
throw new ArgumentException("Refund reason is required.", nameof(reason));
}
Status = OrderStatus.Refunded;
RefundReason = reason;
}
public void MarkExchanged()
{
Status = OrderStatus.Exchanged;
}
}