using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Domain.Entities.Members.ValueObject;
using Domain.Entities.Store;
using Domain.Entities.Store.ValueObject;
namespace Domain.Entities.Members;
///
/// 계정 귀속 아이템 (쿠폰 코드와 별개). 상점에서 ItemKind 상품 구매 시 발급된다. 양도 불가(MemberID 직접 귀속).
/// • 지위 아이템(Badge=2/NicknameEffect=3/CommentEffect=4): 장착형 — Equip/Unequip. 슬롯당 1개.
/// • 소모품(ChatBanLift=5 …): 사용(소모) 시 효과 적용 — MarkUsed/Revoke.
/// 선물이면 GiftFromMemberID 에 발신자, 지속일(DurationDays) 스냅샷으로 ExpiresAt 를 계산한다(null=영구).
///
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; }
/// 선물이면 발신자 회원 ID. 본인 구매면 null.
public int? GiftFromMemberID { get; private set; }
/// 아이템 종류 (구매 시점 Product.ItemKind 스냅샷).
public ItemKind Kind { get; private set; }
/// 지위 아이템 장착 슬롯 (Badge=1/NicknameEffect=2/CommentEffect=3). 소모품은 null.
public EquipSlot? EquipSlot { get; private set; }
/// 지위 아이템 장착 여부. 슬롯당 1개만 true (필터 유니크 인덱스로 강제).
public bool IsEquipped { get; private set; }
/// 효과 지속일 스냅샷 (ExpBooster 활성 창 등). 0 = 즉시 소모형/영구.
public int DurationDays { get; private set; }
public DateTime AcquiredAt { get; private set; } = DateTime.UtcNow;
/// 만료 일시 (DurationDays 스냅샷). null = 영구. 조회 시 만료분 필터.
public DateTime? ExpiresAt { get; private set; }
/// 소모/활성 시각 — NULL 이면 미사용(사용 가능). 지위 아이템은 사용하지 않음(항상 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, int? giftFromMemberID = null)
{
if (memberID <= 0)
{
throw new ArgumentOutOfRangeException(nameof(memberID));
}
if (productID <= 0)
{
throw new ArgumentOutOfRangeException(nameof(productID));
}
if (orderItemID <= 0)
{
throw new ArgumentOutOfRangeException(nameof(orderItemID));
}
if (giftFromMemberID is <= 0)
{
throw new ArgumentOutOfRangeException(nameof(giftFromMemberID));
}
var days = durationDays < 0 ? 0 : durationDays;
var now = DateTime.UtcNow;
return new MemberItem
{
MemberID = memberID,
ProductID = productID,
OrderItemID = orderItemID,
GiftFromMemberID = giftFromMemberID,
Kind = kind,
EquipSlot = kind.ToEquipSlot(),
DurationDays = days,
AcquiredAt = now,
// 지위 아이템: DurationDays>0 이면 획득 시점 기준 만료 스냅샷(null=영구). 소모품은 사용 후 창(IsBoostActiveAt)이라 미적용.
ExpiresAt = kind.IsStatusItem() && days > 0 ? now.AddDays(days) : null
};
}
/// 지위 아이템 여부 — 장착 슬롯 보유.
public bool IsStatusItem => EquipSlot is not null;
/// 지정 시각 기준 만료 여부 (ExpiresAt 존재 && 경과).
public bool IsExpiredAt(DateTime nowUtc) => ExpiresAt is not null && ExpiresAt.Value <= nowUtc;
/// 사용 가능 여부 — 미사용 && 미회수. (소모품 기준)
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 Equip(DateTime nowUtc)
{
if (EquipSlot is null)
{
throw new InvalidOperationException("장착할 수 없는 아이템입니다.");
}
if (RevokedAt is not null)
{
throw new InvalidOperationException("회수된 아이템입니다.");
}
if (IsExpiredAt(nowUtc))
{
throw new InvalidOperationException("만료된 아이템입니다.");
}
IsEquipped = true;
}
/// 지위 아이템 장착 해제. 이미 미장착이면 무해(idempotent).
public void Unequip()
{
IsEquipped = false;
}
/// 소모/활성 처리. 이미 사용/회수 시 예외.
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;
}
IsEquipped = false;
RevokedAt = nowUtc;
return true;
}
}