MemberItem.cs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. using System.ComponentModel.DataAnnotations;
  2. using System.ComponentModel.DataAnnotations.Schema;
  3. using Domain.Entities.Store;
  4. using Domain.Entities.Store.ValueObject;
  5. namespace Domain.Entities.Members;
  6. /// <summary>
  7. /// 계정 귀속 소모품 아이템 (쿠폰 코드와 별개). 상점에서 ItemKind 상품 구매 시 발급되고,
  8. /// 사용 시 해당 효과(밴 해제·글 초기화·쪽지권·경험치부스터·프로필 변경권 등)를 적용한다.
  9. /// 양도 불가 (MemberID 직접 귀속).
  10. /// </summary>
  11. public class MemberItem
  12. {
  13. [ForeignKey(nameof(MemberID))]
  14. public virtual Member Member { get; private set; } = null!;
  15. [ForeignKey(nameof(ProductID))]
  16. public virtual Product? Product { get; private set; }
  17. [Key]
  18. public int ID { get; private set; }
  19. public int MemberID { get; private set; }
  20. public int ProductID { get; private set; }
  21. /// <summary>발급 근거 주문 항목 (환불 시 회수 연동).</summary>
  22. public int OrderItemID { get; private set; }
  23. /// <summary>아이템 종류 (구매 시점 Product.ItemKind 스냅샷).</summary>
  24. public ItemKind Kind { get; private set; }
  25. /// <summary>효과 지속일 스냅샷 (ExpBooster 활성 창 등). 0 = 즉시 소모형.</summary>
  26. public int DurationDays { get; private set; }
  27. public DateTime AcquiredAt { get; private set; } = DateTime.UtcNow;
  28. /// <summary>소모/활성 시각 — NULL 이면 미사용(사용 가능).</summary>
  29. public DateTime? UsedAt { get; private set; }
  30. /// <summary>환불/회수 시각 — NULL 이 아니면 무효.</summary>
  31. public DateTime? RevokedAt { get; private set; }
  32. [Timestamp]
  33. public byte[] RowVersion { get; private set; } = default!;
  34. private MemberItem() { }
  35. public static MemberItem Create(int memberID, int productID, int orderItemID, ItemKind kind, int durationDays)
  36. {
  37. if (memberID <= 0)
  38. {
  39. throw new ArgumentOutOfRangeException(nameof(memberID));
  40. }
  41. if (productID <= 0)
  42. {
  43. throw new ArgumentOutOfRangeException(nameof(productID));
  44. }
  45. if (orderItemID <= 0)
  46. {
  47. throw new ArgumentOutOfRangeException(nameof(orderItemID));
  48. }
  49. return new MemberItem
  50. {
  51. MemberID = memberID,
  52. ProductID = productID,
  53. OrderItemID = orderItemID,
  54. Kind = kind,
  55. DurationDays = durationDays < 0 ? 0 : durationDays
  56. };
  57. }
  58. /// <summary>사용 가능 여부 — 미사용 && 미회수.</summary>
  59. public bool IsAvailable => UsedAt is null && RevokedAt is null;
  60. /// <summary>경험치 부스터 활성 여부 (사용 후 DurationDays 창 이내).</summary>
  61. public bool IsBoostActiveAt(DateTime nowUtc)
  62. {
  63. if (Kind != ItemKind.ExpBooster || RevokedAt is not null || UsedAt is null)
  64. {
  65. return false;
  66. }
  67. return UsedAt.Value.AddDays(DurationDays) > nowUtc;
  68. }
  69. /// <summary>소모/활성 처리. 이미 사용/회수 시 예외.</summary>
  70. public void MarkUsed(DateTime nowUtc)
  71. {
  72. if (RevokedAt is not null)
  73. {
  74. throw new InvalidOperationException("회수된 아이템입니다.");
  75. }
  76. if (UsedAt is not null)
  77. {
  78. throw new InvalidOperationException("이미 사용한 아이템입니다.");
  79. }
  80. UsedAt = nowUtc;
  81. }
  82. /// <summary>환불/회수 — 미사용 아이템만 회수 가능. 성공 시 true.</summary>
  83. public bool Revoke(DateTime nowUtc)
  84. {
  85. if (RevokedAt is not null || UsedAt is not null)
  86. {
  87. return false;
  88. }
  89. RevokedAt = nowUtc;
  90. return true;
  91. }
  92. }