using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Domain.Entities.Store; using Domain.Entities.Store.ValueObject; namespace Domain.Entities.Members; /// /// 계정 귀속 소모품 아이템 (쿠폰 코드와 별개). 상점에서 ItemKind 상품 구매 시 발급되고, /// 사용 시 해당 효과(밴 해제·글 초기화·쪽지권·경험치부스터·프로필 변경권 등)를 적용한다. /// 양도 불가 (MemberID 직접 귀속). /// public class MemberItem { [ForeignKey(nameof(MemberID))] public virtual Member Member { get; private set; } = null!; [ForeignKey(nameof(ProductID))] public virtual Product? Product { get; private set; } [Key] public int ID { get; private set; } public int MemberID { get; private set; } public int ProductID { get; private set; } /// 발급 근거 주문 항목 (환불 시 회수 연동). public int OrderItemID { get; private set; } /// 아이템 종류 (구매 시점 Product.ItemKind 스냅샷). public ItemKind Kind { get; private set; } /// 효과 지속일 스냅샷 (ExpBooster 활성 창 등). 0 = 즉시 소모형. public int DurationDays { get; private set; } public DateTime AcquiredAt { get; private set; } = DateTime.UtcNow; /// 소모/활성 시각 — NULL 이면 미사용(사용 가능). public DateTime? UsedAt { get; private set; } /// 환불/회수 시각 — NULL 이 아니면 무효. public DateTime? RevokedAt { get; private set; } [Timestamp] public byte[] RowVersion { get; private set; } = default!; private MemberItem() { } public static MemberItem Create(int memberID, int productID, int orderItemID, ItemKind kind, int durationDays) { if (memberID <= 0) { throw new ArgumentOutOfRangeException(nameof(memberID)); } if (productID <= 0) { throw new ArgumentOutOfRangeException(nameof(productID)); } if (orderItemID <= 0) { throw new ArgumentOutOfRangeException(nameof(orderItemID)); } return new MemberItem { MemberID = memberID, ProductID = productID, OrderItemID = orderItemID, Kind = kind, DurationDays = durationDays < 0 ? 0 : durationDays }; } /// 사용 가능 여부 — 미사용 && 미회수. public bool IsAvailable => UsedAt is null && RevokedAt is null; /// 경험치 부스터 활성 여부 (사용 후 DurationDays 창 이내). public bool IsBoostActiveAt(DateTime nowUtc) { if (Kind != ItemKind.ExpBooster || RevokedAt is not null || UsedAt is null) { return false; } return UsedAt.Value.AddDays(DurationDays) > nowUtc; } /// 소모/활성 처리. 이미 사용/회수 시 예외. public void MarkUsed(DateTime nowUtc) { if (RevokedAt is not null) { throw new InvalidOperationException("회수된 아이템입니다."); } if (UsedAt is not null) { throw new InvalidOperationException("이미 사용한 아이템입니다."); } UsedAt = nowUtc; } /// 환불/회수 — 미사용 아이템만 회수 가능. 성공 시 true. public bool Revoke(DateTime nowUtc) { if (RevokedAt is not null || UsedAt is not null) { return false; } RevokedAt = nowUtc; return true; } }