using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Domain.Entities.Store.ValueObject; namespace Domain.Entities.Store; /// /// 게임사 월별 정산 큐. 매월 1일 새벽 HostedService 가 전월 Order 들의 GameRevenueAmount 를 GameID 별로 집계. /// UNIQUE (GameID, Year, Month). Admin 이 다운로드 후 수동 송금하고 Settled 마킹. /// public class GameSettlement { [ForeignKey(nameof(GameID))] public virtual Game? Game { get; private set; } [Key] public int ID { get; private set; } public int GameID { get; private set; } public int Year { get; private set; } public int Month { get; private set; } /// 해당 월 누적 게임사 몫 (= Σ Order.GameRevenueAmount). public int TotalRevenueAmount { get; private set; } /// 해당 월 주문 건수. public int OrderCount { get; private set; } public GameSettlementStatus Status { get; private set; } = GameSettlementStatus.Pending; public DateTime CreatedAt { get; private set; } = DateTime.UtcNow; public DateTime? SettledAt { get; private set; } /// 처리 관리자 (ASP.NET Identity ApplicationUser.Id). public string? SettledByAdminUserId { get; private set; } /// 처리 관리자 Email 스냅샷 (표시용). public string? SettledByAdminEmail { get; private set; } public string? AdminMemo { get; private set; } private GameSettlement() { } public static GameSettlement Create(int gameID, int year, int month) { if (gameID <= 0) { throw new ArgumentOutOfRangeException(nameof(gameID)); } if (year < 2000 || year > 9999) { throw new ArgumentOutOfRangeException(nameof(year)); } if (month < 1 || month > 12) { throw new ArgumentOutOfRangeException(nameof(month)); } return new GameSettlement { GameID = gameID, Year = year, Month = month }; } /// 월간 집계 시 row 가 이미 있으면 누적. HostedService 에서 멱등 처리용. public void AddRevenue(int revenueAmount, int orderCount = 1) { if (revenueAmount < 0) { throw new ArgumentOutOfRangeException(nameof(revenueAmount)); } if (orderCount < 0) { throw new ArgumentOutOfRangeException(nameof(orderCount)); } if (Status != GameSettlementStatus.Pending) { throw new InvalidOperationException("이미 정산된 row 에는 추가 적재 불가."); } TotalRevenueAmount += revenueAmount; OrderCount += orderCount; } /// 환불 시 비례 차감. public void SubtractRevenue(int revenueAmount) { if (revenueAmount <= 0) { throw new ArgumentOutOfRangeException(nameof(revenueAmount)); } if (Status != GameSettlementStatus.Pending) { throw new InvalidOperationException("이미 정산된 row 에는 환불 차감 불가. 별도 정정 정산 row 필요."); } TotalRevenueAmount = Math.Max(0, TotalRevenueAmount - revenueAmount); } public void MarkSettled(string adminUserId, string? adminEmail, string? adminMemo = null) { if (Status != GameSettlementStatus.Pending) { throw new InvalidOperationException($"Cannot mark settled from status {Status}."); } if (string.IsNullOrWhiteSpace(adminUserId)) { throw new ArgumentException("AdminUserId is required.", nameof(adminUserId)); } Status = GameSettlementStatus.Settled; SettledByAdminUserId = adminUserId; SettledByAdminEmail = adminEmail; SettledAt = DateTime.UtcNow; AdminMemo = adminMemo; } }