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

# 0001 rate domain redesign

# ADR-0001: Rate Domain 재설계

* **Status**: Proposed
* **Date**: 2026-05-15
* **Authors**: 정다운 ([daun@vendit.co.kr](mailto:daun@vendit.co.kr))
* **Affected modules**: `apps/core-svc/src/domains/ari-rate`, `ari-period`, `ari-package`, `ari-inclusion`
* **Branch**: `refactor/rate-domain-redesign`

***

## 1. Context

현재 `ari-rate`, `ari-period`, `ari-package`, `ari-inclusion` 4개 도메인으로 구성된 요금 모델은 운영을 시작하지 않은 시점에 한 번 정리해야 한다. 글로벌 PMS 표준(Opera, Mews, Apaleo, Cloudbeds)과 OTA 분배 표준(HTNG, OpenTravel)에 비춰볼 때 다음 결함이 식별되었다.

### 1.1 구조적 결함

| #   | 결함                                                                                                               | 영향                                                             |
| --- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| C1  | **Daily Rate 매트릭스 부재** — `(ratePlanId, roomTypeId, date) → amount` 테이블 없음                                        | OTA 채널 매니저 일별 push 불가, Yield Engine 연동 불가, 일별 캘린더 조회 매번 런타임 계산 |
| C2  | **Restriction(CTA/CTD/StopSell/MinLOS) 부재**                                                                      | PMS 인증 자체가 불가능. 채널 매니저 연동의 전제 조건 미충족                           |
| C3  | **Occupancy 기반 가격 부재** — `baseOccupancy` 필드 자체가 스키마에 없음 (mdx 문서엔 존재)                                             | 다인실 요금 책정 불가, 패밀리룸·스위트룸 표현 불가                                  |
| C4  | **Package 의미 역전** — Package가 RatePlan을 담는 컨테이너                                                                   | 글로벌 표준 정반대. OTA·GDS 어디서도 쓰지 않는 구조                              |
| C5  | **Adjustment 폴리모픽 과잉** — 단일 테이블이 channel/period/base 세 컨텍스트 동시 표현                                                | 의미 불명확, 디버깅 난이도 ↑, 계산 공식이 코드에만 존재                              |
| C6  | **CurrencySymbol 중복 저장** (5개 테이블)                                                                                | 데이터 무결성 리스크. 변경 시 누락 위험                                        |
| C7  | **checkInAt/checkOutAt 타입 불일치** — VARCHAR(8) vs VARCHAR(5)                                                       | 원본/히스토리 포맷 다름. 타임존·파싱 일관성 ↓                                    |
| C8  | **Period 표현 잡종** — `daysOfWeek/daysOfMonth/monthsOfYear` JSON + 명시적 `startDate/endDate` + `AriPeriodRange` 동시 존재 | 우선순위·결합 규칙 스키마로 표현 불가                                          |
| C9  | **이벤트 소싱(Saga) 패턴 과적용** — CRUD 단순 케이스(AriPackage)에 saga 적용                                                       | 응답 지연, 코드 복잡도 ↑, 학습 비용 ↑                                       |
| C10 | **History의 정규화/JSON 혼재**                                                                                         | Immutability 깨짐. snapshot 의미 모호                                |

### 1.2 글로벌 PMS 공통 패턴

| 시스템             | Rate 모델 핵심 구조                                                            |
| --------------- | ------------------------------------------------------------------------ |
| Oracle Opera    | `RATE_HEADER` → `RATE_DETAIL`(per date×room class) → `RATE_RESTRICTIONS` |
| Mews            | `RateGroup` → `Rate` → `RateValue`(per date) + `Restriction`             |
| Apaleo          | `RatePlan` → `Rate`(per date) + `Restriction`, `Promotion`(derived)      |
| Cloudbeds       | `Rate`(plan) → `RoomTypeRate`(per date×roomType)                         |
| HTNG/OpenTravel | `RatePlan` + `BookingRule` + `Restriction` + `Offer`                     |

**공통 패턴 5가지**:

1. RatePlan(설정 메타데이터) ↔ DailyRate(날짜별 가격) 명확 분리
2. Master-Derived Rate 계층
3. Restriction을 별도 도메인으로 격리
4. Occupancy 가격 매트릭스
5. Package는 RatePlan의 inclusion bundle 속성

***

## 2. Decision

### 2.1 전체 도메인 재설계 (Clean Slate)

운영 데이터가 없으므로 **legacy 4개 도메인을 즉시 폐기**하고 신규 5개 도메인으로 재구성한다.

**제거**:

* `AriRate`, `AriRateAdjustment`, `AriRateAdjustmentMapping`
* `AriPackage` (컨테이너 역할로서)
* `AriPeriod`의 `daysOfWeek/daysOfMonth/monthsOfYear` JSON 필드
* 모든 테이블의 `currencySymbol` 컬럼

**신규** (core-svc 내부):

* `RatePlan` — 요금제 설정 (메타데이터, 정책, 기본 가격)
* `DailyRate` — 일별 가격 매트릭스 (PMS의 심장)
* `RatePlanRestriction` — 날짜별 판매 통제
* `RatePlanOccupancyPrice` — 점유 인원별 가격
* `Inclusion`, `RatePlanInclusion` — 부가 서비스 (override 지원)
* `RateSeason` — 시즌/기간 (iCal RRULE 활용)

**Audit 처리**: core-svc 내 AuditLog 테이블을 두지 않는다. 모든 사용자/시스템 조작은 `@vpms-cluster/audit` 라이브러리를 통해 **audit-svc 로 통합 발행**한다. (§2.8 참고)

### 2.2 Storage Model: **Hybrid**

| 항목              | 결정                                                                                              |
| --------------- | ----------------------------------------------------------------------------------------------- |
| 기본값 위치          | `RatePlan.defaultAmount`                                                                        |
| 일별 영속화 위치       | `DailyRate` row                                                                                 |
| `DailyRate` 의미  | 특정 `stayDate`의 **최종 판매가 snapshot**                                                              |
| Resolution 우선순위 | `DailyRate row 존재 → 사용` → `parent ratePlan + derivationRule` → `defaultAmount + occupancyPrice` |

**근거**:

* Dense(전체 펼치기)는 가격 변동 적은 시설(펜션·모텔)에 storage 낭비
* Sparse(변경분만)는 OTA push 시 매번 resolve 부담
* Hybrid는 운영 시점에 정책 전환 가능 (yield engine 도입 시 dense화)

#### 2.2.1 Price Resolution Contract

`DailyRate`는 단순 override가 아니라, 특정 날짜에 예약 엔진과 OTA에 노출되는 **resolved final amount의 영속 snapshot** 으로 정의한다.

계산 순서는 다음과 같다:

1. `DailyRate(ratePlanId, roomTypeId, stayDate)` row가 존재하면 그 값을 최종값으로 사용한다.
2. row가 없고 `parentRatePlanId`가 있으면 부모 `RatePlan`의 동일 날짜 가격을 기준으로 `derivationRule`을 적용한다.
3. 부모가 없으면 `RatePlan.defaultAmount`를 기준으로 사용한다.
4. `RoomType.defaultSleeps` 초과 인원이 있으면 `RatePlanOccupancyPrice`를 적용한다.
   * 점유 기준(base/max occupancy)은 `RatePlan` 이 아니라 `RoomType` 의 `defaultSleeps`/`maxSleeps` 가 source of truth 다. 같은 RatePlan 이 정원이 다른 여러 RoomType 에 매핑될 때의 모순을 피하기 위함.
   * `RatePlanOccupancyPrice` 는 sparse 하게 입력한다 (필요한 (roomType, occupancy) 슬롯만). 단순한 1인당 추가요금은 `RatePlanGuestTypeAdjustment(adult, appliesAfterBaseOccupancy=true)` 로 표현하는 것이 권장 패턴.
5. 금액 계산 결과는 통화 반올림 규칙(예: KRW 절사, 소수 통화는 scale 유지)에 따라 normalize 한다.

#### 2.2.2 Materialization Rules

* `DailyRate` row는 다음 경우에 생성 또는 갱신한다.
  * 사용자가 특정 날짜 가격을 직접 수정한 경우
  * 시즌/프로모션/일괄 인상 등으로 특정 날짜 범위 가격을 확정 적용한 경우
  * horizon extension job이 미래 기간 기본값을 선계산하도록 선택한 경우
* `RatePlan.defaultAmount`, `derivationRule`, `occupancyPrice` 변경 시 이미 존재하는 미래 `DailyRate` row는 **자동 overwrite 하지 않는다**.
* 대신 영향 범위를 계산해 비동기 재산정 job을 enqueue 하고, 사용자가 승인한 경우에만 미래 구간을 재물질화한다.
* 이 규칙으로 "설정 변경"과 "이미 확정된 일별 판매가"를 분리해 가격 drift와 의도치 않은 대량 변경을 방지한다.

#### 2.2.3 Derived Rate Guardrails

* `RatePlan`은 선택적으로 `parentRatePlanId`를 가질 수 있다.
* parent chain 최대 깊이는 5로 제한한다.
* 순환 참조는 저장 시점에 금지한다.
* `derivationRule`은 1차 범위에서 `fixed_amount`, `fixed_delta`, `percentage_delta` 세 종류만 허용한다.
* 복수 파생 규칙 조합, 채널별 파생 규칙, 조건부 파생 규칙은 이번 ADR 범위에서 제외한다.

### 2.3 Restriction Model

`RatePlanRestriction`은 가격과 분리된 독립 판매 통제 도메인으로 정의한다.

#### 2.3.1 Restriction Scope

* PK/unique 기준: `(ratePlanId, roomTypeId, stayDate)`
* 1차 지원 필드:
  * `closedToArrival` (CTA)
  * `closedToDeparture` (CTD)
  * `stopSell`
  * `minLengthOfStay`
  * `maxLengthOfStay`
* restriction은 숙박일(`stayDate`) 기준으로 평가한다.
* 체크인 가능 여부는 입실일의 `CTA`, 체크아웃 가능 여부는 퇴실 전일의 `CTD`, 판매 가능 여부는 숙박 구간 전체의 `stopSell`과 LOS를 함께 평가한다.

#### 2.3.2 Restriction Resolution Rules

1. 동일 날짜에 `stopSell=true`가 존재하면 다른 restriction보다 항상 우선한다.
2. `minLengthOfStay`, `maxLengthOfStay`는 더 보수적인 값이 우선한다.
3. 부모 `RatePlan`과 자식 `RatePlan`이 동시에 restriction을 가지는 경우, 자식 row가 있으면 자식 값을 사용한다.
4. 자식 row가 없으면 부모 restriction을 상속한다.
5. 채널별 restriction override는 이번 ADR 범위에서 제외한다.

#### 2.3.3 Restriction Persistence Rule

* restriction도 가격과 동일하게 날짜별 snapshot row를 기본 단위로 저장한다.
* RRULE/시즌 적용은 입력 편의 기능일 뿐 source of truth는 날짜별 row이다.

### 2.4 Forward Horizon: **24개월**

| 근거             | 내용                                                                               |
| -------------- | -------------------------------------------------------------------------------- |
| Booking.com 요구 | 최소 16개월 forward availability                                                     |
| 마진             | 장기 예약·MICE 대응 위해 8개월 추가                                                          |
| Storage 예측     | 1,000 시설 × 10 roomType × 5 ratePlan × sparse 30% × 730일 ≈ **16M rows** (\~2.4GB) |

Daily cron으로 매일 자정 horizon을 24개월로 롤링 유지한다. 다만 이는 **운영 기본값**이며, Phase 1 acceptance criteria는 아니다.

### 2.5 History Retention

| 계층           | 기간   | 저장 위치                                         |
| ------------ | ---- | --------------------------------------------- |
| Hot history  | 13개월 | 동일 테이블 (PostgreSQL native partitioning)       |
| Cold archive | 7년   | 별도 archive 테이블 (TimescaleDB 압축 또는 S3 Parquet) |

월별 cron으로 13개월 이전 partition을 archive로 이관한다. archive 파이프라인은 Phase 4에서 도입한다.

### 2.6 PostgreSQL Partitioning

```sql theme={null}
CREATE TABLE daily_rates (
  -- columns ...
) PARTITION BY RANGE (stay_date);

-- 월별 자동 파티션 (pg_partman 또는 manual cron)
```

* 파티션 키: `stayDate` (월 단위)
* 인덱스: `(ratePlanId, roomTypeId, stayDate)` unique + `(stayDate)` + `(roomTypeId, stayDate)`
* TimescaleDB 확장은 **Phase 2 종료 시점에 도입 여부 재평가** (지금은 native partitioning으로 시작)
* 단, 초기 row volume이 낮은 동안은 단일 테이블로 시작하고 Phase 4 이전에 partitioning을 적용해도 된다.

### 2.7 Cache Layer: **Redis (Hot Cache, NOT Source of Truth)**

| 항목   | 결정                                                          |
| ---- | ----------------------------------------------------------- |
| 키 패턴 | `rate:{accommodationId}:{ratePlanId}:{roomTypeId}:{YYYYMM}` |
| 자료형  | Hash (field=day 01\~31, value=amount)                       |
| TTL  | 1시간                                                         |
| 무효화  | DailyRate write 시 pub/sub로 즉시 invalidate                    |

**Redis 단독 저장 거부 이유**:

* Durability(ACID) 보장 부족 → 가격 데이터 손실 위험
* Audit trail 불가
* BI·OLAP 연동 불가
* 메모리 비용 (1B rows × 100B = 100GB RAM → \~\$5,000/월)

PostgreSQL이 source of truth, Redis는 hot path 가속 전용이다. Redis 미가용 시 resolver는 직접 DB 조회로 fallback 해야 하며, 캐시 미스는 오류가 아니라 성능 저하로 취급한다.

### 2.8 Event Sourcing(Saga) 폐기 + audit-svc 통합 발행

| 변경     | 내용                                                                                                    |
| ------ | ----------------------------------------------------------------------------------------------------- |
| 폐기     | AriPackage CRUD의 saga 패턴 제거. 일반 transactional write로 단순화                                              |
| 유지 검토  | DailyRate 대량 변경(시즌 일괄 적용 등)은 BullMQ job으로 비동기 처리                                                      |
| **신규** | core-svc 내에 AuditLog 테이블을 두지 않는다. 사용자/시스템 조작은 모두 **`@vpms-cluster/audit` 라이브러리를 통해 audit-svc 로 발행**한다 |

#### Audit 책임 분리 원칙

| 책임                                                  | 위치                                                              |
| --------------------------------------------------- | --------------------------------------------------------------- |
| Audit log source of truth (저장, 조회, 이상 탐지)           | `apps/audit-svc`                                                |
| Audit 클라이언트 (publish API, GraphQL directive, 타입 정의) | `libs/audit` (`@vpms-cluster/audit`)                            |
| Audit 발행 (rate domain mutation 시점)                  | `apps/core-svc/src/domains/rate-plan/**` 및 관련 도메인 service layer |

core-svc 는 audit-svc 의 데이터 모델에 의존하지 않고 `@vpms-cluster/audit` 의 `publishAuditLog` / `publishAuditLogBatch` API 와 `CreateAuditLogData` 타입에만 의존한다.

#### audit-svc 측 데이터 모델 요지

audit-svc 의 schema(`apps/audit-svc/prisma/schema.prisma`) 는 다음을 제공한다:

* `AuditLog` — entityType / entityId / actionType(CREATE/UPDATE/DELETE/EVENT) / status / oldData / newData / highlightFields / userId / userAgent / ipAddress / accommodationId / contextId / createdAt
* `AuditContext` — 동일 요청/트랜잭션 내 여러 audit log 를 한 묶음으로 연결하는 컨테이너
* 익명 탐지(`isAnomaly`, `anomalyReason`) 후크
* 시설(accommodationId) 단위 인덱스 다수

본 ADR 의 rate domain 변경은 이 모델을 그대로 활용하며 audit-svc schema 를 수정하지 않는다.

#### Audit 발행 전략

| 변경 유형                                                | Audit 발행                                           | 방법                                                            |
| ---------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------- |
| 사용자가 GraphQL mutation 호출                             | ✅ 필수                                               | `@audit` GraphQL directive 또는 resolver 내 `publishAuditLog` 호출 |
| Cron job (horizon extension, archive)                | ✅ 기록 (actionType=EVENT)                            | service layer 에서 명시적 `publishAuditLog`                        |
| Channel manager pull-in                              | ✅ 기록 (actionType=EVENT, eventName='CHANNEL\_PULL') | 통합 어댑터에서 명시적 호출                                               |
| Derivation cascade (parent ratePlan 변경 → 자식 cascade) | ✅ 묶음 기록                                            | 단일 `contextId` 로 묶어 `publishAuditLogBatch`                    |
| 재물질화 일괄 적용 (ADR-0002 §2.4)                           | ✅ 묶음 기록                                            | 동일 `contextId` + 영향 cell 수 요약을 `newData` 에 포함                 |

#### Audit 호출 위치

* **GraphQL Resolver 레벨**: `@audit(entityType: "RatePlan", action: UPDATE)` directive 를 우선 사용. resolver 가 entity 식별자와 변경 전후 데이터를 가공해 directive 가 자동 발행.
* **Service 레벨**: directive 적용이 어려운 비-mutation 흐름(cascade, batch job 등)은 service 함수에서 직접 `publishAuditLog` 호출.
* **트랜잭션 외부 발행**: audit 발행은 도메인 트랜잭션 커밋 이후 실행한다. audit 실패가 도메인 트랜잭션을 롤백하지 않도록 격리한다.

#### 엔티티 레벨 보조 필드

audit-svc 로 모든 변경 이력은 발행되지만, "누가 마지막으로 만졌나" 를 빠르게 보여주기 위해 변경 가능 엔티티(`RatePlan`, `DailyRate`, `RatePlanRestriction`, `Inclusion` 등)에 보조 필드를 둔다:

```prisma theme={null}
lastModifiedBy     Bytes?   @database.ByteA   // FK to User
updatedAt          DateTime @updatedAt
lastModifiedReason String?  @database.VarChar(255)
```

이는 **캐시/최적화 목적이지 audit source of truth 가 아니다**. 전체 변경 이력은 항상 audit-svc 에서 조회한다.

### 2.9 ID 타입 통일

모든 도메인 엔티티 PK를 **ULID (Bytes)** 로 통일:

* 분산 환경에서 충돌 없는 생성 가능
* 시간 기반 정렬 가능
* 외부 API에 안전하게 노출 가능

예외: 다대다 매핑 테이블(`RatePlanRoomType` 등)은 composite PK 유지.

### 2.10 Currency 처리

* `currency`: ISO 4217 enum (KRW, USD, JPY 등) 그대로 유지
* `currencySymbol`: **모든 테이블에서 제거**
* 심볼은 프론트엔드 `Intl.NumberFormat(locale, { style: 'currency', currency })`로 도출

### 2.11 Time 표현 통일

`checkInTime`, `checkOutTime`을 **Int (minutes from midnight)** 로 통일:

* `14:00` → `840`
* `11:30` → `690`
* 타임존 영향 없음, 비교·정렬 자명, 파싱 불필요

***

## 3. Out of Scope (이번 ADR 범위 외)

다음은 별도 ADR로 분리한다:

* ADR-0002: Cancellation / Payment Policy 1st-class 도메인화
* ADR-0003: Tax / Fee 분리 (gross/net, 다국가 세금 표현)
* ADR-0004: Yield Management 외부 엔진 연동 인터페이스
* ADR-0005: Channel Manager(OTA) 분배 표준화 (HTNG/OpenTravel 매핑)

이번 ADR은 **기반 구조(Rate + DailyRate + Restriction + Inclusion + Audit)** 까지만 다룬다.

추가로 다음 운영 최적화는 **채택 방향은 유지하되 Phase-gated implementation** 으로 취급한다:

* Redis hot cache
* PostgreSQL partitioning 자동화
* Horizon extension cron
* Cold archive 파이프라인

즉, Phase 1의 완료 기준은 "신규 도메인 모델 + 가격/resolution contract + restriction contract + audit contract" 이며, 운영 최적화는 후속 Phase에서 순차 도입한다.

***

## 4. Consequences

### 4.1 Positive

* ✅ Daily Rate 매트릭스 확보 → OTA·Yield Engine 연동 가능
* ✅ Restriction 도메인 확보 → PMS 인증 요건 충족
* ✅ Occupancy 가격 → 다인실 요금 표현 가능
* ✅ Audit 체계화 → 회계 감사 대응 가능, "누가 가격 바꿨나" 추적 가능
* ✅ Saga 제거 → API 응답 속도 ↑, 코드 단순화
* ✅ Hybrid storage → 시설 규모에 따라 운영 모드 전환 가능
* ✅ 설정 변경과 확정된 일별 판매가를 분리 → 의도치 않은 미래 가격 덮어쓰기 방지
* ✅ Redis 캐시 → 일별 캘린더 조회 ms 단위 응답

### 4.2 Negative

* ❌ **Breaking change**: 기존 GraphQL API(`getAriPackageList` 등) 전부 deprecated. 프론트엔드 동시 변경 필수
* ❌ **Cron 운영 추가**: Horizon extension, Archive job 모니터링 필요
* ❌ **Redis 의존성 추가**: 단일 장애점 → fallback 경로(직접 DB 조회) 필수
* ❌ **AuditLog 용량**: 활성 시설 1,000개 × 일 평균 100 mutation = 100K rows/day = 36M/year. 별도 archive 정책 필요 (Phase 4에 반영)
* ❌ **마이그레이션 비용**: 운영 미시작이라 데이터 마이그레이션은 없지만, 코드 베이스 전체 재작성 = 2주 이상 소요
* ❌ **재물질화 정책 필요**: 기본 가격/파생 규칙 변경 시 어떤 미래 날짜를 다시 계산할지 운영 규칙이 필요

### 4.3 Risks & Mitigations

| Risk                             | Mitigation                                |
| -------------------------------- | ----------------------------------------- |
| 파티션 누락 → 미래 날짜 insert 실패         | `pg_partman` 자동 생성 + alert                |
| Redis cache poisoning            | TTL 1h 강제 + write 직후 invalidate           |
| AuditLog 무제한 증가                  | 1년 후 cold archive 이관 정책 (별도 ADR로 분리)      |
| Derivation cascade infinite loop | parent 체인 최대 깊이 5 제한 (RatePlan 자체 검증)     |
| 기본값 변경 후 미래 가격 불일치               | 영향 범위 미리보기 + 비동기 재산정 job + 명시적 사용자 승인     |
| Restriction 충돌 규칙 모호             | `stopSell` 최우선, LOS는 보수적 값 우선 규칙을 ADR에 명시 |
| 다국가 진출 시 timezone                | 모든 시간은 시설 로컬 타임존 기준. 표시는 사용자 타임존으로 변환     |

***

## 5. Alternatives Considered

### A1. Legacy 유지하며 DailyRate만 추가

* ❌ 거부 — Adjustment 폴리모픽 문제, currencySymbol 등 다른 결함 그대로 잔존. 두 모델 공존 = 영구 기술 부채.

### A2. Dense Storage (모든 날짜 row 강제 생성)

* ❌ 거부 — 가격 변동 적은 시설에 storage 낭비. Hybrid가 운영 시점 결정권을 보존.

### A3. Sparse Only (DailyRate row는 override만)

* ❌ 거부 — Yield Engine 도입 시 "현재 가격이 무엇인가" 모호. dense화 옵션 필요.

### A4. Redis를 Source of Truth로

* ❌ 거부 — Durability·Audit·BI 연동 불가. 메모리 비용 폭증.

### A5. Event Sourcing 전면 적용 (전체 도메인 saga)

* ❌ 거부 — RatePlan CRUD는 단순 케이스. saga 오버헤드가 이득보다 큼. DailyRate 대량 변경만 비동기 job으로 처리.

### A6. NoSQL(MongoDB) 도입

* ❌ 거부 — 트랜잭션·JOIN·BI 도구 호환성에서 PostgreSQL이 우월. 시계열 특화는 TimescaleDB로 충분.

***

## 6. Implementation Roadmap

| Phase     | 작업                                                                         | 예상        |
| --------- | -------------------------------------------------------------------------- | --------- |
| 1         | Prisma 스키마 작성 (legacy drop + 신규 7개 모델, resolution/restriction contract 반영) | 3일        |
| 2         | RateResolverService + AuditInterceptor                                     | 3일        |
| 3         | GraphQL schema + resolver 재구현                                              | 3일        |
| 4         | Redis 캐시 + horizon extension + archive/partition 운영 작업                     | 2일        |
| **Total** |                                                                            | **\~11일** |

각 Phase는 별도 PR로 분리. Phase 1 PR이 base가 되어 후속 PR이 stack.

***

## 7. References

* [HTNG Specifications](https://www.htng.org/specifications)
* [OpenTravel Alliance Schemas](https://opentravel.org/specifications/)
* [Apaleo API Documentation](https://api.apaleo.com)
* [Mews Open API](https://mews-systems.gitbook.io/connector-api/)
* [Booking.com Connectivity Requirements](https://connect.booking.com)
* [PostgreSQL Native Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html)
* [TimescaleDB Hypertable](https://docs.timescale.com/use-timescale/latest/hypertables/)
* [iCalendar RRULE (RFC 5545)](https://datatracker.ietf.org/doc/html/rfc5545)
