[BE-01] LimitedDrop 도메인 모델·상태·Repository·오버셀 이벤트 계약

작업 내용 (설계 의도)

변경 사항

goods 도메인에 한정판 회차 Aggregate Root를 신설한다(근거 TDD: ../TDD.md “도메인 바운디드 컨텍스트 경계 판단”, ADR-002). 후행 티켓(BE-03 도메인 서비스, BE-05 영속화, BE-06 DTO)이 import하는 공통 도메인 계약을 한 wave에서 확립한다.

  • LimitedDrop(Entity, Rich Domain Model): private 생성자 + create()/reconstitute() 팩토리. 필드 private var. 대상 productId·openAt·closeAt·limitedQuantity·perUserLimit·status(ZonedDateTime 사용).
  • validatePurchasable(): now<openAt이면 TooEarly 예외(openAt 포함), now≥closeAt이면 Closed 예외, status가 SOLD_OUT이면 SoldOut 예외. 시각은 메서드 내부 ZonedDateTime.now()로 해결(시간 인자 금지).
  • open()·markSoldOut()·close() 상태 전이 — LimitedDropStatus.canTransitTo() 캡슐화.
  • LimitedDropStatus enum: SCHEDULED/OPEN/SOLD_OUT/CLOSED + canTransitTo(next).
  • LimitedDropRepository interface(domain): save·findById·findOpenByProductId.
  • LimitedDropOversoldEvent(domain event): dropId·productId·감지 수량. source=oversell·severity=critical 태그(⑥ 연계). AggregateRoot domainEvents에 적재.
  • 예외: LimitedDropTooEarlyException(openAt 보유), LimitedDropSoldOutException, LimitedDropClosedException, LimitedDropNotFoundException.

의존

  • 없음 (선행 병목)

다이어그램

클래스 의존

flowchart LR
    LimitedDrop --> LimitedDropStatus
    LimitedDrop --> LimitedDropOversoldEvent
    LimitedDropRepository -.->|반환| LimitedDrop
    LimitedDrop --> AggregateRoot["common.AggregateRoot"]

테스트 케이스

  • openAt 이전 구매 검증 시 TooEarly 예외가 openAt을 담아 던져진다
  • openAt 이후·closeAt 이전·재고 있음이면 validatePurchasable이 통과한다
  • SOLD_OUT 상태에서 validatePurchasable은 SoldOut 예외를 던진다
  • closeAt 이후 validatePurchasable은 Closed 예외를 던진다
  • SCHEDULED→OPEN→SOLD_OUT→CLOSED 전이는 허용되고 CLOSED→OPEN은 canTransitTo가 거부한다
  • create는 openAt<closeAt·limitedQuantity>0·perUserLimit>0을 검증하고 위반 시 예외를 던진다
  • reconstitute는 검증 없이 필드를 복원한다