using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Domain.Entities.Store.ValueObject;
namespace Domain.Entities.Store;
///
/// 상품별 판매 정책 (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).
///
public class ProductLimitConfig
{
[ForeignKey(nameof(ProductID))]
public virtual Product? Product { get; private set; }
/// PK = Product.ID (1:1).
[Key]
public int ProductID { get; private set; }
/// 수량 제한 사용 여부.
public bool UseQuantityLimit { get; private set; }
/// 회원당 누적 최대 구매 수량 (0 = 무제한, UseQuantityLimit=true 일 때 의미 있음).
public int MaxQuantity { get; private set; }
/// 기간 제한 사용 여부. true 면 PeriodType 으로 윈도우 결정. false 면 all-time 누적.
public bool UsePeriodLimit { get; private set; }
/// 누적 윈도우 단위. None/Day/Week/Month/Custom.
public LimitPeriodType PeriodType { get; private set; } = LimitPeriodType.None;
/// PeriodType=Custom 일 때 사용. 절대 윈도우 시작 시각 (UTC).
public DateTime? PeriodStartAt { get; private set; }
/// PeriodType=Custom 일 때 사용. 절대 윈도우 종료 시각 (UTC). 반드시 PeriodStartAt 보다 커야 함.
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
};
}
///
/// Admin 측에서 정책 전체 업서트. 모든 필드를 한번에 갱신.
/// useQuantityLimit=false 면 maxQuantity 는 0 으로 정규화.
/// usePeriodLimit=false 면 periodType=None, PeriodStartAt/EndAt 도 null 로 정규화.
/// Custom 이 아니면 PeriodStartAt/EndAt 은 null 로 정규화.
/// Custom 인 경우 두 값 모두 not null + end > start 검증.
///
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;
}
///
/// 현재 정책에서 누적 윈도우 시작 시각을 반환. UTC 기준.
/// UsePeriodLimit=false 또는 PeriodType=None 이면 null (=all-time).
/// Custom 은 PeriodStartAt 절대값 반환.
///
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
};
}
///
/// 현재 정책에서 누적 윈도우 종료 시각을 반환. UTC 기준.
/// rolling 유형(Day/Week/Month) 은 항상 현재 시각이 종료.
/// Custom 은 PeriodEndAt 절대값 반환.
/// 윈도우 미적용(All-time) 이면 null.
///
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
};
}
}