| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- using System.ComponentModel.DataAnnotations;
- using System.ComponentModel.DataAnnotations.Schema;
- using Domain.Entities.Store.ValueObject;
- namespace Domain.Entities.Store;
- /// <summary>
- /// 상품별 판매 정책 (1:1 with Product). fdmbox `tb_box_limit_config` 의 antooza 차용.
- /// 미설정 상품은 row 없음 (정책 미적용).
- ///
- /// 정책 동작:
- /// - UseQuantityLimit + MaxQuantity > 0 : 회원당 누적 구매 수량 제한.
- /// - UsePeriodLimit + PeriodType : 누적 윈도우 범위. 미사용 시 all-time 누적.
- /// - None/Day/Week/Month : 현재 시각 기준 rolling 윈도우.
- /// - Custom : PeriodStartAt ~ PeriodEndAt 의 절대 datetime 범위 (UTC).
- /// </summary>
- public class ProductLimitConfig
- {
- [ForeignKey(nameof(ProductID))]
- public virtual Product? Product { get; private set; }
- /// <summary>PK = Product.ID (1:1).</summary>
- [Key]
- public int ProductID { get; private set; }
- /// <summary>수량 제한 사용 여부.</summary>
- public bool UseQuantityLimit { get; private set; }
- /// <summary>회원당 누적 최대 구매 수량 (0 = 무제한, UseQuantityLimit=true 일 때 의미 있음).</summary>
- public int MaxQuantity { get; private set; }
- /// <summary>기간 제한 사용 여부. true 면 PeriodType 으로 윈도우 결정. false 면 all-time 누적.</summary>
- public bool UsePeriodLimit { get; private set; }
- /// <summary>누적 윈도우 단위. None/Day/Week/Month/Custom.</summary>
- public LimitPeriodType PeriodType { get; private set; } = LimitPeriodType.None;
- /// <summary>PeriodType=Custom 일 때 사용. 절대 윈도우 시작 시각 (UTC).</summary>
- public DateTime? PeriodStartAt { get; private set; }
- /// <summary>PeriodType=Custom 일 때 사용. 절대 윈도우 종료 시각 (UTC). 반드시 PeriodStartAt 보다 커야 함.</summary>
- public DateTime? PeriodEndAt { get; private set; }
- public DateTime? UpdatedAt { get; private set; }
- public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
- private ProductLimitConfig() { }
- public static ProductLimitConfig Create(int productID)
- {
- if (productID <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(productID));
- }
- return new ProductLimitConfig
- {
- ProductID = productID
- };
- }
- /// <summary>
- /// Admin 측에서 정책 전체 업서트. 모든 필드를 한번에 갱신.
- /// useQuantityLimit=false 면 maxQuantity 는 0 으로 정규화.
- /// usePeriodLimit=false 면 periodType=None, PeriodStartAt/EndAt 도 null 로 정규화.
- /// Custom 이 아니면 PeriodStartAt/EndAt 은 null 로 정규화.
- /// Custom 인 경우 두 값 모두 not null + end > start 검증.
- /// </summary>
- public void Update(
- bool useQuantityLimit,
- int maxQuantity,
- bool usePeriodLimit,
- LimitPeriodType periodType,
- DateTime? periodStartAt,
- DateTime? periodEndAt
- ) {
- if (maxQuantity < 0 || maxQuantity > 9999)
- {
- throw new ArgumentOutOfRangeException(nameof(maxQuantity), "MaxQuantity 는 0~9999 범위.");
- }
- var isCustom = usePeriodLimit && periodType == LimitPeriodType.Custom;
- if (isCustom)
- {
- if (!periodStartAt.HasValue || !periodEndAt.HasValue)
- {
- throw new ArgumentException("Custom 기간은 시작/종료 시각 모두 지정해야 합니다.", nameof(periodStartAt));
- }
- if (periodEndAt.Value <= periodStartAt.Value)
- {
- throw new ArgumentException("종료 시각은 시작 시각보다 이후여야 합니다.", nameof(periodEndAt));
- }
- }
- UseQuantityLimit = useQuantityLimit;
- MaxQuantity = useQuantityLimit ? maxQuantity : 0;
- UsePeriodLimit = usePeriodLimit;
- PeriodType = usePeriodLimit ? periodType : LimitPeriodType.None;
- PeriodStartAt = isCustom ? periodStartAt : null;
- PeriodEndAt = isCustom ? periodEndAt : null;
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>
- /// 현재 정책에서 누적 윈도우 시작 시각을 반환. UTC 기준.
- /// UsePeriodLimit=false 또는 PeriodType=None 이면 null (=all-time).
- /// Custom 은 PeriodStartAt 절대값 반환.
- /// </summary>
- public DateTime? GetWindowStart(DateTime now)
- {
- if (!UsePeriodLimit || PeriodType == LimitPeriodType.None)
- {
- return null;
- }
- return PeriodType switch
- {
- LimitPeriodType.Day => now.AddDays(-1),
- LimitPeriodType.Week => now.AddDays(-7),
- LimitPeriodType.Month => now.AddDays(-30),
- LimitPeriodType.Custom => PeriodStartAt,
- _ => null
- };
- }
- /// <summary>
- /// 현재 정책에서 누적 윈도우 종료 시각을 반환. UTC 기준.
- /// rolling 유형(Day/Week/Month) 은 항상 현재 시각이 종료.
- /// Custom 은 PeriodEndAt 절대값 반환.
- /// 윈도우 미적용(All-time) 이면 null.
- /// </summary>
- public DateTime? GetWindowEnd(DateTime now)
- {
- if (!UsePeriodLimit || PeriodType == LimitPeriodType.None)
- {
- return null;
- }
- return PeriodType switch
- {
- LimitPeriodType.Day => now,
- LimitPeriodType.Week => now,
- LimitPeriodType.Month => now,
- LimitPeriodType.Custom => PeriodEndAt,
- _ => null
- };
- }
- }
|