> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.vpms.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 요금제 관리 (Rate Plan)

> RatePlan + DailyRate snapshot 기반의 신규 요금 도메인 API

## 개요

기존 ARI(Rate / Period / Package / Inclusion) 4개 도메인을 폐기하고, 글로벌 PMS 표준에 맞춰
**RatePlan(설정) + DailyRate(일별 판매가 snapshot) + Restriction(판매 통제)** 구조로 재설계했습니다.

설계 결정은 [ADR-0001](../../adr/0001-rate-domain-redesign.md), [ADR-0002](../../adr/0002-pricing-rules-and-ui-patterns.md) 참고.

**핵심 개념:**

* **RatePlan**: 요금제 설정. `defaultAmount` + `pricingRules`(요일/시즌 가산치) 가 "레시피".
* **DailyRate**: 특정 `(ratePlanId, roomTypeId, stayDate)` 의 **확정 최종 판매가 snapshot**. 단순 override 가 아니라 예약 엔진·OTA 에 노출되는 영속 값.
* **Master-Derived**: `parentRatePlanId` + `derivationRule` 로 파생 요금제.
* **Restriction**: 날짜별 CTA/CTD/StopSell/LOS 통제.
* **RematerializationProposal**: 규칙 변경 시 기존 snapshot 을 자동 덮어쓰지 않고 사용자 승인 후 재물질화.

## Queries

### getRatePlanList

특정 숙박 시설의 요금제 목록 조회.

```graphql theme={null}
query GetRatePlans($accommodationId: ID!) {
  getRatePlanList(accommodationId: $accommodationId, isActive: true) {
    id
    code
    name
    type
    baseOccupancy
    defaultAmount
    currency
    pricingRules
    parentRatePlanId
  }
}
```

### getRatePlan

단일 요금제 상세 (occupancyPrices / inclusions / channelMappings / roomTypes 포함).

```graphql theme={null}
query GetRatePlan($id: ID!) {
  getRatePlan(id: $id) {
    id
    code
    name
    defaultAmount
    pricingRules
    occupancyPrices { occupancy amount pricingMode }
    inclusions { inclusion { name } quantity chargeOverride }
    channelMappings { channel { name } externalCode adjustmentOp adjustmentValue }
    roomTypes { id name }
  }
}
```

### getDailyRateCalendar

캘린더 그리드 조회. 각 셀은 snapshot 존재 여부(`hasSnapshot`)와 해석 경로(`resolutionSource`)를 반환.

```graphql theme={null}
query Calendar($ratePlanId: ID!, $roomTypeId: ID!, $start: Date!, $end: Date!) {
  getDailyRateCalendar(
    ratePlanId: $ratePlanId
    roomTypeId: $roomTypeId
    startDate: $start
    endDate: $end
  ) {
    stayDate
    amount
    currency
    resolutionSource   # daily_rate_snapshot | derived_from_parent | default_with_rules
    hasSnapshot
    ratePlanChain
  }
}
```

### resolveDailyRate

단일 날짜·점유 인원 기준 해석 가격을 반환. occupancy 변경 시뮬레이션에 사용.

### getRatePlanRestrictions

날짜 범위의 restriction 목록 (CTA/CTD/StopSell/MinLOS/MaxLOS).

### getInclusionList

시설 별 inclusion(부가서비스) 카탈로그.

### getRematerializationProposal

규칙 변경 후 생성된 영향 범위 분석 결과 조회. 사용자에게 "재물질화 진행/폐기" 선택 UI 에 사용.

## Mutations

### createRatePlan / updateRatePlan / deleteRatePlan

요금제 CRUD. update 의 `defaultAmount / pricingRules / derivationRule` 변경 시
**기존 미래 DailyRate snapshot 을 자동 변경하지 않는다** (ADR-0001 §2.2.2). 대신
RematerializationProposal 이 자동 생성되며 사용자가 별도로 승인해야 적용된다.

### DailyRate set / clear

* `setDailyRate(input)`: 단일 `(roomTypeId, stayDate)` 셀 가격 확정.
* `setDailyRateBulk(input)`: 범위 × roomType\[] 일괄 설정. `amount` / `amountDelta` /
  `amountPercentDelta` 택 1. `overrideExisting: false` 면 빈 날짜만 채움.
* `clearDailyRate(...)`: 단일 snapshot 삭제 → 규칙 계산값 복귀.
* `clearDailyRateBulk(...)`: 범위 일괄 삭제. 시즌 reset 등 사용.

### Restriction set / clear

* `setRatePlanRestriction(input)`: 단일 날짜.
* `setRatePlanRestrictionBulk(input)`: 범위 × roomType\[] 일괄 설정.
* `clearRatePlanRestriction(...)` / `clearRatePlanRestrictionBulk(...)`.

### RatePlan 하위 엔티티 전량 교체 (wizard 단계 지원)

* `setRatePlanOccupancyPrices(ratePlanId, prices[])` — wizard step 3.
* `setRatePlanInclusions(ratePlanId, inclusions[])` — wizard step 5.
* `setRatePlanChannelMappings(ratePlanId, mappings[])`.
  모두 기존 항목 전량 제거 후 입력으로 교체하는 idempotent 작업.

### Inclusion CRUD

`createInclusion` / `updateInclusion` / `deleteInclusion`. `postingType` 은
`per_stay` / `per_night` / `per_person` / `per_person_per_night`.

### RateSeason CRUD

명명된 시즌 엔티티. 여러 RatePlan 이 공유 가능. `startDate`/`endDate` 명시 또는
iCalendar `rrule` (RFC 5545) 으로 반복 정의.
`createRateSeason` / `updateRateSeason` / `deleteRateSeason`.

### Rematerialization Proposal

* `applyRematerializationProposal(id)`: 영향 범위 cell 을 새 규칙 기준 재계산해 갱신.
  `source = rematerialized`, 단일 `contextId` 로 audit batch.
* `discardRematerializationProposal(id)`: 폐기.

## Audit

모든 사용자 mutation 은 `@vpms-cluster/audit` 라이브러리를 통해 **audit-svc** 로 발행된다.
core-svc 에는 audit 테이블이 없으며 audit-svc 가 source of truth.

추가로 변경 가능 엔티티에 `lastModifiedBy` / `updatedAt` / `lastModifiedReason` 보조 필드를 두어
"마지막으로 누가 만졌나" 만 빠르게 조회한다 (전체 이력은 audit-svc 에서 조회).

## Resolution 우선순위

`resolveDailyRate` / `getDailyRateCalendar` 의 가격 해석 순서 (ADR-0001 §2.2.1):

1. `DailyRate(ratePlanId, roomTypeId, stayDate)` row 존재 → 그 값.
2. `parentRatePlanId` + `derivationRule` 적용. 부모 가격 × 규칙.
3. `defaultAmount` + `pricingRules` 계산. 요일/시즌 multiplier 결합.
4. `occupancy != baseOccupancy` → `RatePlanOccupancyPrice` 적용.
5. 통화 반올림 normalize (KRW/JPY 등은 정수 절사).

## 캐시 정책

* Redis Hash: `rate:{ratePlanId}:{roomTypeId}:{YYYYMM}` (field=day, value=amount).
* TTL: 1h. 쓰기 직후 자동 invalidate.
* Redis 미가용 시 캐시 미스로 취급 (오류 아님). DB 직조회로 fallback.

## 관련 ADR

* [ADR-0001: Rate Domain 재설계](/adr/0001-rate-domain-redesign)
* [ADR-0002: Pricing Rules & UI Patterns](/adr/0002-pricing-rules-and-ui-patterns)
