specs/trips.md

行程與梯次

一句話:trips 是行程產品定義(目錄/行銷/MDX 內容),departures 是該行程的可賣梯次(日期區間 + 席次容量 = 旅遊團的售賣單位)。本檔是行程實體的單一入口。欄位以 code 為準(packages/core/src/schema/business.ts、packages/core/src/catalog/*),畫面以 wireframe 為準(§6)。

定位:domain/core spec。功能切片(定價 / 內容 / 團號 / 配套住宿 / 分房 / 登山難度)在各自 spec,§5 索引串接。訂房不走此模型(旅宿走房型每晚庫存,見 lodging-inventory)。狀態見 roadmap。

1. 實體模型

  • trips(行程定義):slug(unique)、title、summary、content_mdx、cover_image_url、price_from_twd(起價,展示用)、duration_days、destination、status(draft…)、模組旗標 is_climbing / is_overseas(行程層 gate,見 modules)、三條硬規則 config(見 §3.1)、created_at/updated_at。
  • trip_images:trip_id(cascade)、url、caption、display_order。
  • departures(可賣梯次 = 售賣單位):trip_id(cascade)、departure_date、return_date、price_twd(梯次價)、capacity、booked_count、status(selling…)、notes。CHECK:capacity > 0、booked_count <= capacity。

與訂單的接點:order_lines → tour_booking_lines.departure_id → departures.id。一張 tour 訂單的 party_size 由 tour line 承載(見 orders)。

2. 狀態機

  • trips.status:draft → 上架(published)等。
  • departures.status:selling → 關閉/額滿/取消等(updateDepartureStatus)。

    確切值集以 code 為準(catalog/trips-repo.ts);本檔描述語意,不鎖列舉。

2.1 三條硬規則 config(行程層可設定,D5 / item 5)

GS 國內高山團的三條硬規則扶正為 trips 上的可設定欄(已落地 packages/core/src/schema/business.ts)。本批僅落「設定欄」;實際 gating / 抽籤 / 候補遞補的 enforcement 邏輯隨 lottery-integration / climbing-screening epic。

欄位型別語意
confirm_group_multipleinteger nullable成團倍數(GS 國內高山團 = 7 的倍數)。NULL = 不限倍數。成團 / 派車 / 人力(每 8 人一組)算法的基準。
restricted_quota_enabledboolean default false人頭使用次數限制開關(奇萊南華 / 嘉明湖 / 北大武等以身分證計次的國家公園 permit 配額)。
quota_per_person_limitinteger nullable每人(每身分證)使用次數上限。NULL = 不限。
quota_windowtext nullable計次窗口(如 'annual')。NULL = 不限窗口。
waitlist_enabledboolean default false候補開關(玉山搶候補 / 候補遞補)。本批僅旗標,遞補邏輯隨 lottery epic。

三者皆 nullable 或有預設,不破既有列。人頭使用次數限制屬登山專屬規則(permit 配額),enforcement 檢查層會比對旅客身分證的歷史使用次數(見 lottery-integration 的人頭限制檢查層註記)。

3. 不變式

  • race-safe 席次(核心):訂位用 conditional UPDATE departures SET booked_count = booked_count + N WHERE booked_count + N <= capacity + DB CHECK booked_count <= capacity,不靠讀後寫(catalog/seat-reservation.ts,由 orders createOrderWithReservation 呼叫)。
  • 兩層模組 gate:登山/海外功能 = 平台層授權(control_db.tenant.module_*)且 行程層旗標(trips.is_climbing / is_overseas)皆開才生效(modules)。
  • 價格解析單一來源:有效售價走唯一的 resolveDeparturePrice(catalog/pricing.ts,fallback 鏈 promo→梯次價→行程起價),歸 trip-pricing,不另寫第二份。
  • slug 唯一、cascade(trip 刪→images/departures 連帶)。
  • 訂房不適用:旅宿不建 departures,走每晚共用庫存(lodging-inventory)。

4. 存取層契約

packages/core/src/catalog/trips-repo.ts(first arg db: ScopedDb):

  • 前台:listPublishedTrips(目錄卡)、getTripBySlug(行程頁)。
  • 後台:listAdminTrips、getTripById、createTrip、updateTrip、createDeparture、updateDepartureStatus。
  • 席次原語:catalog/seat-reservation.ts。價格:catalog/pricing.ts(resolveDeparturePrice,trip-pricing)。

5. 擴充行程的功能 spec(索引)

Spec擴充什麼
trip-pricing直客/同業價欄、訂金/尾款、resolveDeparturePrice、包團(charter)
content-authoringMDX 行程內容 / 圖片 / journal 撰寫
group-code梯次團號(region/airline code + 產生/搜尋)
climbing-screening行程難度欄(grade_score 等)+ is_climbing,下單資格審核
lodging-tour-packagedeparture_lodging 配套住宿(扣旅宿庫存、派生 departure_rooms)
room-assignmentdeparture_rooms 分房配置
modulesis_climbing / is_overseas 兩層 gate
departure-prep出團 deadline 警戒 / 許可證文件
lottery-integration成團倍數 / 人頭使用次數限制 / 候補(§2.1 config 的 enforcement)

6. 畫面(UI = wireframe)

  • 後台行程列表 / 編輯(含梯次/價格/配套)../apps/wireframes/app/admin/trips/