> ## 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.

# 0003 rate domain migration runbook

# ADR-0003: Rate Domain 마이그레이션 런북

* **Status**: Accepted
* **Date**: 2026-05-15
* **Branch**: `refactor/rate-domain-redesign`
* **Related**: [ADR-0001](./0001-rate-domain-redesign.md), [ADR-0002](./0002-pricing-rules-and-ui-patterns.md)

본 문서는 본 브랜치 머지 후 운영 환경에서 수행해야 할 절차를 정리한다.

## 1. Phase 별 산출물 요약

| Phase    | 산출물                                                                                                       |
| -------- | --------------------------------------------------------------------------------------------------------- |
| Phase 1  | Prisma 스키마 재설계. legacy ari-\* 4개 도메인 제거. 신규 rate-plan / inclusion 도메인 + AuditLog는 audit-svc로 위임.          |
| Phase 2  | `pricing-rules.service` / `rate-resolver.service` / `rate-audit.publisher` / `rematerialization.service`. |
| Phase 3a | folio / reservation / bill / block 의 legacy ari 참조 정리 (rename or stub).                                   |
| Phase 3b | GraphQL `rate-plan.gql` + resolver. Registry 등록.                                                          |
| Phase 4  | Redis hot cache + `horizon-extension.scheduler` + `rate-archive.scheduler` + `restriction.service`.       |
| Tests    | `pricing-rules.service.test` + `restriction.service.test`.                                                |
| Docs     | `docs/api-reference/core-svc/rate-plan.mdx` + 본 런북.                                                       |

## 2. 머지 전 확인

1. `pnpm prisma generate` 재실행 — generated client 가 새 모델/필드 반영하는지 확인.
2. `pnpm test` — node:test 가 신규 unit test 를 인식하는지 확인.
3. `pnpm nx run core-svc:build` — TS 컴파일 통과 확인.
4. `pnpm nx run core-svc:lint` — biome 위반 없는지.

## 3. DB 마이그레이션

운영 데이터가 없는 시점이므로 `prisma migrate reset` 후 신규 스키마로 마이그레이션 적용.

```bash theme={null}
cd apps/core-svc
pnpm prisma migrate reset --force
pnpm prisma migrate dev --name rate-domain-redesign
```

마이그레이션 SQL 에는 다음이 포함되어야 한다:

* legacy 테이블 DROP: `ariRates`, `ariRateAdjustments`, `ariRateAdjustmentMappings`, `ariRatePlans`, `ariRateHistories`, `ariRatePlanHistories`, `ariRatePlanChannelAdjustments`, `ariRatePlanInclusions`, `ariInclusions`, `ariInclusionHistories`, `ariPeriods`, `ariPeriodRanges`, `ariPackages`, `ariPackageHistories`, `ariFolioRateSnapshot`, `ariFolioInclusionSnapshot`.
* 신규 테이블 CREATE: `ratePlans`, `dailyRates`, `ratePlanRestrictions`, `ratePlanOccupancyPrices`, `ratePlanRoomTypes`, `ratePlanChannelMappings`, `ratePlanInclusions`, `ratePlanHistories`, `rateSeasons`, `rematerializationProposals`, `folioRateSnapshots`, `folioInclusionSnapshots`, `inclusions`, `inclusionHistories`.
* 변경: `prices.ariPackageId` → `prices.ratePlanId`.

## 4. BullMQ cron 등록

worker bootstrap 시 다음 작업을 등록한다.

```typescript theme={null}
import { horizonExtensionScheduler } from '@/domains/rate-plan/schedulers/horizon-extension.scheduler.ts';
import { rateArchiveScheduler } from '@/domains/rate-plan/schedulers/rate-archive.scheduler.ts';

// Daily 00:00 — 24개월 horizon 유지
queue.add('horizon-extension', {}, { repeat: { pattern: '0 0 * * *' } });

// Monthly 1st 03:00 — archive + proposal 만료 정리
queue.add('rate-archive', {}, { repeat: { pattern: '0 3 1 * *' } });
```

processor:

```typescript theme={null}
new Worker('horizon-extension', async () => horizonExtensionScheduler.run());
new Worker('rate-archive', async () => rateArchiveScheduler.run());
```

## 5. Phase 5 booking integration 진행 상태

Phase 3a 에서 NotImplemented stub 으로 비워둔 booking 흐름을 신규 RatePlan / DailyRate
도메인 기반으로 재구현하는 작업. 본 브랜치에서 다음까지 완료되었다:

### ✅ 완료

* `snapshot-creation.service`: `prepareSnapshotData` / `commitPreparedSnapshots` /
  `createSnapshots` 가 `RateResolverService.resolve` 로 가격을 해석하고
  `FolioRateSnapshot` / `FolioInclusionSnapshot` 을 생성한다.
  `ratePlanSnapshot` / `inclusionSnapshot` JSON 으로 immutability 를 보장.
* `folio-amount-calculator.service`: 신규 snapshot 의
  `rateAmount/inclusionAmount/totalAmount` 를 그대로 활용. history join 불필요.
* `bill-creation.service`: `FolioRateSnapshot` 단위로 Bill 발행.
  `BillBriefKey.CHARGE_RESERVATION_DAILY` / `CHARGE_INCLUSION` 적용.
* `FolioRatePlanInput / RatePlanInput / RatePlanUpdateInput` 입력 shape 을 신규
  `ratePlanId(ID) + roomTypeId(ID) + occupancy + amountOverride` 로 마이그레이션.
* `reservation-modify.helpers.deriveRatePlanInclusionInputs`: `RatePlanInclusion`
  직접 조회로 default inclusion 목록 구성.
* `reservation.service.createReservationForFolio`: 예약 확정 전
  `RestrictionService.assertBookable` 호출로 CTA/CTD/StopSell/LOS 위반 차단.
* `block.service`: FolioRatePlanInput 호출부 신규 shape 으로 마이그레이션.

### ✅ schema 마이그레이션 (추가)

* `ReservationNight.ratePlanId`: `Int` → `Bytes(ULID)`.
  `rateId`: `Int` → `BigInt?` (DailyRate.id 참조, sparse 허용).
* `BlockComposition.ratePlanId/rateId`: 동일 패턴으로 `Bytes? / BigInt?`.
* `reservation-night.helpers.createReservationNights` 시그니처를 `string / bigint?`
  로 갱신, 호출 흐름 복원.
* `ReservationNight.ratePlan/rate` 및 `BlockComposition.ratePlan/rate` resolver 가
  신규 RatePlan / DailyRate row 를 직접 findUnique 한다.
* GraphQL `ReservationNight.ratePlanId/rateId` / `BlockComposition.ratePlanId/rateId`
  타입을 `ID!`/`String` 으로 정정 (BigInt 직렬화).

### ✅ snapshot JSON shape 정리 (추가)

* `BlockCompositionRateSnapshot` shape: `ratePlanId` → `string(ULID)`, `rateId` →
  `string|null(BigInt 직렬화)`. `currencySymbol` 폐기.
  `ratePlanVersion / rateIdVersion` 은 deprecated 표시 후 legacy archive 호환용으로만
  유지 (신규 row 는 안 채우는 것이 권장).
* block.gql 의 `BlockCompositionRateSnapshot` type / `BlockCompositionRateSnapshotInput`
  도 동일하게 갱신.

### ✅ BigInt id 직렬화 (추가)

DailyRate 와 RatePlanRestriction 의 PK 가 `BigInt` 인데 GraphQL `ID!` 는 `string` 이라
resolver 경계에서 직렬화 필요. `serializeBigIntId` / `serializeBigIntIds` 헬퍼를
rate-plan.resolver 에 추가하고 `setDailyRate` / `getRatePlanRestrictions` /
`setRatePlanRestriction` / `block.resolver.rate` / `reservation.resolver.rate` /
`folio.resolver.appliedRate` 에 적용. 모든 BigInt 반환 경로가 string 으로 일관 직렬화.

### ⏭ Out of scope (별도 PR)

* Bill domain 의 `Payment.currencySymbol` 컬럼 — ADR 원래 범위 외. 통화 코드만 저장하는
  표준에 맞춰 별도 마이그레이션 PR 권장.
* 신규 `appliedPackage` resolver 가 현재 RatePlan 을 반환 — package 개념이 inclusion
  bundle 로 흡수된 만큼 GraphQL surface 에서 제거할지 별도 논의.
* E2E 통합 테스트 (예약 → folio snapshot → bill 발행 happy path).

## 6. PostgreSQL partitioning

ADR-0001 §2.6 정책에 따라 `daily_rates` 테이블을 `stay_date` 월별로 파티셔닝한다.
초기 row 수가 적은 동안은 단일 테이블로 운영 가능. 다음 조건 중 하나 충족 시 도입:

* row 수 1천만 초과
* 시설 1천 개 초과
* 캘린더 쿼리 P95 > 200ms

도입 시 `pg_partman` 자동 파티션 생성 + monthly cron 으로 13개월 cutoff partition 을
archive 테이블로 DETACH → COPY → DROP 한다.

## 7. 롤백 정책

운영 데이터 마이그레이션이 시작된 후에는 롤백이 어렵다. 본 변경은 운영 데이터가 없는
시점에 적용되므로 다음 안전망만 유지:

* 머지 전 main 브랜치 백업 tag 생성 (`pre-rate-redesign`).
* 머지 후 24시간 핫스탠바이 운영자 배치.
* 외부 통합(채널 매니저 등)은 별도 ramp-up 일정으로 분리.
