一句話:行程(滑雪團)設計時固定指定自家旅宿的房型與入住晚數,出團對那幾晚整塊預留團體床位(扣 旅宿庫存 同一份),旅館細節資料顯示在前台行程頁;受 模組化 的「訂房」+「海外」開關 gate。
departure_rooms 從 block 產生)· 行程定價(房費 / 升等)· 模組化長野雪場旅館不單獨賣,而是綁在滑雪行程裡:報滑雪團就含這間旅館的住宿。需要:
現況 departure_rooms 是 OP 手 key 每梯次房間(房名 / 床數 / 房費)—— 與旅宿主檔脫節、重複輸入、也不扣任何共用庫存。
departure_lodging:梯次 → 指定 property + room_type + 入住區間 + 團體 block 大小。reserveNights(block units);取消整梯 → 釋回。properties / room_types)。departure_rooms 對齊:block 預留到的房 / 床產生 departure_rooms,與外部山屋 / 民宿的手 key 房並存(分房在其上運作)。departure_lodging」即可涵蓋多晚多房型;跨旅宿複雜排程留 future。departure_lodging(梯次配套住宿)export const departureLodging = pgTable(
'departure_lodging',
{
id: text('id').primaryKey(), // 'dl_<ulid>'
departureId: text('departure_id')
.notNull()
.references(() => departures.id, { onDelete: 'cascade' }),
propertyId: text('property_id')
.notNull()
.references(() => properties.id),
roomTypeId: text('room_type_id')
.notNull()
.references(() => roomTypes.id),
checkIn: date('check_in').notNull(),
checkOut: date('check_out').notNull(), // 不含當晚
/** 團體預留單元數(間 / 床)。預設依梯次容量推算,可 OP 覆寫。 */
blockUnits: integer('block_units').notNull(),
/** 是否已對庫存預留(避免重複扣 / 重複釋放)。 */
reserved: boolean('reserved').notNull().default(false),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('departure_lodging_departure_idx').on(t.departureId),
index('departure_lodging_roomtype_idx').on(t.roomTypeId, t.checkIn),
check('departure_lodging_dates', sql`${t.checkOut} > ${t.checkIn}`),
],
);
一梯次可有多筆(多晚 / 多房型 / 含多間旅宿)。
departure_rooms(分房)的對齊 —— 跨 spec不重造。
departure_rooms(room-assignment)保留為「團員 → 床位 + 房費成本」的分房層;其資料從departure_lodging預留到的 block 產生,與手 key 房並存:
- 預留成功後,依 block(property/room_type/間數/床數)自動生成對應
departure_rooms列(accommodation_name= property.name、room_type= room_type.name、bed_count= room_type.capacity_per_unit、base_cost_twd= 0)。- 分房(
travelers.room_id)照舊在這些列上運作。- 房費成本:派生房
base_cost_twd = 0(自家旅宿無對外供應商成本,與手 key 山屋 / 民宿房不同);房升補款另循order_charge_lines(type='room_upgrade')(分房時依床位分攤),不從 room_type 帶房費成本。已落地:派生
departure_rooms與外部山屋 / 民宿手 key 房並存(非淘汰)—— 派生房帶departure_lodging_id標記,releaseDepartureLodging只刪派生房、手 key 房留存(見 Changelog v2 / v3)。
departure_lodging 的 block 與 直接訂房 的 lodging order lines 扣同一份 room_night_inventory.booked_count(inventory §3.3)。現況長野只配套(無散客),但共用模型天生支援未來混用。
reserveNights(roomTypeId, checkIn, checkOut, blockUnits);成功 → reserved=true + 生成 departure_rooms。庫存不足 → 擋下並提示(改房型 / 改旅宿 / 調 block)。kind='tour');住宿由 block 涵蓋,報名不再各自扣旅宿庫存(block 已預留整段)。departure_lodging → properties/room_types 顯示旅館細節(設施 / 房型 / 照片)。releaseNights)+ 清 reserved。packages/core/src/lodging/package-repo.ts:
setDepartureLodgingAction(input: {
departureId; propertyId; roomTypeId; checkIn; checkOut; blockUnits;
}): Promise<{ ok: true } | { ok: false; reason: 'NO_AVAILABILITY' | 'CLOSED_NIGHT'; night?: string }>
// reserveNights(block) → upsert departure_lodging(reserved=true) → 生成/對齊 departure_rooms → audit
releaseDepartureLodgingAction(departureLodgingId): Promise<{ ok: true }>
// releaseNights + reserved=false + 處理對應 departure_rooms
getDepartureLodging(db, departureId) // 團控 / 行程頁顯示
| 動作 | permission | 角色 |
|---|---|---|
| 設定 / 改梯次配套旅宿 | lodging.manage + trip.update | op、admin |
| 前台看旅館細節 | 公開(行程頁) | — |
受訂房模組(+ 海外旅宿時海外模組)授權前提。
trip-edit.html)。/tour/[slug]:加「住宿」區塊讀 departure_lodging → 旅館細節(沿用 MDX / 結構化資料)。setDepartureLodgingAction:足量 → block 預留各晚 +blockUnits、生成 departure_rooms、reserved=true;不足 → 擋、不留半成品。releaseDepartureLodgingAction:釋回、reserved=false。| 類別 | 內容 |
|---|---|
| 新表 | departure_lodging(drizzle) |
| 改既有 | departure_rooms 新增 block 派生來源(departure_lodging_id 標記;與手 key 房並存) |
| 新 repo | packages/core/src/lodging/package-repo.ts |
| UI | trip-edit 配套住宿區 · 前台行程頁住宿區塊 · 團控顯示 |
| 模組 | 訂房模組(+ 海外旅宿時海外模組) |
| 項目 | 預設 | 待釐清 |
|---|---|---|
| block 大小推算 | 預設 = 梯次容量 / room_type.capacity_per_unit;OP 可覆寫 | 是否要精準到實際報名數動態調 |
| 客人選房型升等 | 不做(OP 固定) | 未來接 trip-pricing room upgrade |
| 多旅宿 / 跨夜不同房型 | 一梯次多筆 departure_lodging 涵蓋 | 複雜排程 future |
| 房費成本來源 | 派生房 base_cost_twd=0(自家無供應商成本);房升走 order_charge_lines | 對外供應旅宿(非自家)配套時的成本記法 |
| 版本 | 日期 | 變更 | Commit |
|---|---|---|---|
| 1 | 2026-05-30 | 初版草案:行程配套住宿(梯次固定指定旅宿 + 團體整塊預留 + departure_rooms 派生) | — |
| 2 | 2026-05-31 | backend 已落地:departure_lodging 表 + departure_rooms.departure_lodging_id 並存標記欄(partial unique index WHERE reserved=true 擋並發重複 block)+ packages/core/src/lodging/package-repo.ts(setDepartureLodging 單一 tx:共用庫存 reserveNightsInTx 整塊預留 + 派生 departure_rooms(bedCount=capacity_per_unit、baseCostTwd=0 自家無供應商成本)+ audit;releaseDepartureLodging 釋回 + 只刪派生房(手 key 房留存)+ idempotent;getDepartureLodging)。決議:派生與外部山屋/民宿手 key 並存(非淘汰)。模組 gate / trip-edit·前台·團控 UI 延後(UI 切片)。plan docs/superpowers/plans/2026-05-31-lodging-tour-package-backend.md | (本批) |
| 3 | 2026-05-31 | UI 已落地:getTripDepartureLodging(撈整行程跨梯次 block)+ getTripLodgingForDisplay(前台顯示 distinct reserved 旅宿)+ trip-edit「梯次」tab 內「配套住宿」設定區(per-departure set/release,setDepartureLodgingAction/releaseDepartureLodgingAction gate trip.read,旅宿選單 usage_hint ∈ {package,both},gated on lodging 模組)+ 前台 /tour/[slug] 住宿配套 Section(純資訊:旅宿名/地點/晚數/簡述,無價無下單鈕)+ 團控分房 room-card 派生房「配套」badge(RoomRow.departureLodgingId 區分手 key 房)。plan docs/superpowers/plans/2026-05-31-lodging-tour-package-ui.md、design docs/superpowers/specs/2026-05-31-lodging-tour-package-ui-design.md。 | (本批) |
| 4 | 2026-06-20 | 修正 §3.2 房費成本描述:派生房 base_cost_twd = 0(自家旅宿無供應商成本,對齊 code package-repo.ts:101 與 Changelog v2),移除「base_cost_twd 從 room_type 帶」「block 預留成本餵 base_cost」的相反敘述;§10 未決同步收斂。 | — |