一句話:旅客報名抽籤梯次時自選一串有序備案(同行程其他梯次,可混先到先得/抽籤);未中籤時 ERP 沿清單鏈式 cascade 前進——遇先到先得有位即當下成立、遇抽籤梯次則重新應募等下一輪;每跳取消舊單、在目標梯次成立新單、以應收 credit 法把訂金一路結轉、寄信通知補尾款。清單走完 / 全滿 → 待人工安置(不自動退款),由線控 force-place 到同 trip 任意梯 或 取消退款。後台亦可代客(K 單)維護備案清單。由 抽籤整合 的
lost訊號觸發(現為線控手動裁決)。
抽籤制熱門團,旅客常在報名時就先選好「抽不到就改去哪一梯」的備案。0617 會議決議:未中籤後自動完成「取消主單 → 成立備案單 → 催尾款」,不要營運逐筆手搬(過去在 GS 是純人工)。
範圍:主→備的鏈式訂單轉移、訂金結轉、自動催尾款、走完退款。
不做:抽籤本身(抽籤整合);無可落腳備案時的退款細節走 金流模型 refunds,本檔負責「沿清單找落腳並轉」。
| 欄位語意 | 說明 |
|---|---|
departures.allocation_mode | 'fcfs'(先到先得,預設)/ 'lottery'(抽籤制)。 |
orders.lottery_state | null(fcfs)/ 'applied'→'won'/'lost';線控手動裁決寫入。 |
order_backup_choices | 有序多備案清單(取代單欄 backup_departure_id):id / order_id(FK orders, cascade) / departure_id(FK departures, restrict) / rank(1-based 偏好序) / created_at;unique(order_id, rank) + unique(order_id, departure_id)。 |
orders.failover_from_order_id | 備案新單回指被未中籤取消的主單(UNIQUE 冪等最後防線;譜系 / audit)。 |
orders.cancellation_reason | 新增(nullable,CHECK enum 含 lottery_lost)。報表篩選真相,區分抽籤取消與一般取消。 |
同行程同價約束(checkout 強制):
order_backup_choices的每個departure_id必須是主單 同trip的另一個departure,且departures.price_twd與主梯相同。同 trip 不保證同價(promo / 梯次 override 可造成異價),故「同價」是 checkout 強制約束、非假設:公開站備案選擇器只顯示同價候選(backup-picker過濾priceTwd === 主梯 priceTwd);送出 server action 以 server 為權威重讀候選再驗證、丟棄非同 trip / 異價 / 重複 / 主梯本身的 id(沿用靜默丟棄慣例)。如此 cascade 跨單結轉的成交價恆一致,消滅主備價差,無需處理差額。可混先到先得 / 抽籤梯次,上限為該行程剩餘梯次數(無人為上限)。不新增金流表,訂金帶過走應收 credit 法(見 §3.2)。
packages/core/src/lottery/failover.ts(db: ScopedDb),advanceToNextBackup 函式,全程同一 tx(db.transaction)。沿主單 order_backup_choices(rank 升序)找第一個可落腳目標:
SELECT … FOR UPDATE 鎖主單 + 冪等檢查(非抽籤單→not_lottery;已 cancelled 或已有 failover 子單→already_resolved)。departure_id / party_size / unit_price_twd_snapshot / price_channel_snapshot)。讀 order_backup_choices(rank 升序);空清單 → 整 tx rollback、回 exhausted(呼叫端走無備案退款路徑)。createOrderWithReservationInTx(reservation primitive,skipDefaultFinancials)扣席建單(沿用 lottery 席次模型「應募即扣席」;候選梯次此刻才佔):
sold_out/closed/departed)→ 前進下一 rank。targetOrderId,記 hitRank + 候選 allocation_mode,break。exhausted。transferMainToTarget(per-hop 轉單帳務,見 §3.2),落點依候選 allocation_mode 參數化:
booking_state='confirmed' + lottery_state='won'(終態,鏈結束),outcome placed。booking_state='pending_payment' + lottery_state='applied'(等那一輪抽籤),outcome reapplied;且把 rank > hitRank 的剩餘備案 carry forward 複製到新單的 order_backup_choices(重排 rank 從 1),鏈在此暫停(等下一次裁決對新單觸發)。assertOrderFinancialInvariants + 寫主 / 備兩單 audit + 譜系。
resolveLotteryOrder的lost出口呼叫本函式:placed/reapplied皆回backupOrderId(涵蓋兩種落點);exhausted→ 走無備案取消 + 退款 draft(見 §3.3)。
旅客已付訂金不退回再收,每跳以應收 credit 法帶到目標單。不在 payment_transactions 建任何 transfer 列——現金分類帳保持純銀行真相。訂金現金實體只留在鏈首;下游每跳新單的訂金以負 failover_deposit_credit charge line 形式結轉(非現金)。
主單 / 上一跳單(被取消):
gross_receivable 沖至 0(逐筆 reversal charge line),所有 current schedules supersede(reconcileSchedulesToReceivable,H3 ✓)。payment_transactions 不動)→ payment_state 派生 paid(receivable=0 且 collected>0,與全退單仍 paid 一致);booking_state=cancelled、lottery_state='lost',營收認列用 gross=0(不計營收)。目標單(新成立):
appendChargeLine(base_fare = 成交價)(價格用主單 tour_booking_lines.unit_price_twd_snapshot × party_size,不裸用 departures.price_twd,保快照穩定性)。appendChargeLine(type='failover_deposit_credit', amount = −cap),以 failover_from_order_id 串回上一跳稽核。其中 cap = min(carriedDeposit, 成交價),carriedDeposit = customerCashApplied(上一跳) + |上一跳已結轉的 failover_deposit_credit|——鏈長 ≥ 2 時上一跳已無現金(訂金以 credit 存在),必須把結轉 credit 加回,否則訂金會在第二跳起蒸發。gross_receivable = 成交價 − cap = 尾款(同 trip 同價下 cap = 訂金,尾款為剩餘應收)。createSchedule(purpose='full', target=尾款) + createIntentForSchedule(SUM(current target) == gross_receivable,H3 ✓)。不建 satisfied 訂金列——訂金以 credit 內含於較低 gross。目標 payment_state 派生 unpaid(gross=尾款、cash_applied=0)。客人付尾款後 → paid;全程付款總額 = 訂金(鏈首)+ 尾款(落腳單)= 原價。
exhausted)→ 待人工安置(不自動退款):空清單、未選備案、或所有備案皆滿 / 不可訂 → cascade 整 tx rollback、主單保持原狀;呼叫端(resolveLotteryOrder)僅設 lottery_state='lost'(FOR UPDATE 防並發),保留 booking_state、保留主梯席次、不取消、不退款,回傳 awaitingPlacement。主單進「待人工安置」清單,由線控顯式裁決(§3.4 force-place 或取消退款)。抽籤梯此時已不可報名,被未中籤單佔著的席次不影響任何人;force-place 或取消才釋主梯席,席次帳全程一致。(舊版「exhausted 即自動取消 + 退訂金 draft」已退場,改由線控顯式觸發 cancelLotteryLostOrder。)applied)+ carry forward 剩餘清單,不再禁止鏈式自動轉(鏈由客人有限排序的 order_backup_choices 界定,非無限自動)。failover_from_order_id UNIQUE 為最後防線(一新單只來自一上一跳);FOR UPDATE 鎖主單防並發;主單已 cancelled 或已有 failover 子單 → 回 already_resolved。consent_signatures 以 append-only 複製原簽署(沿用 signedAt);旅客以 PII 重加密(AAD 綁新 rowId)複製,不搬移原列。待安置主單(或線控主動介入 applied 單,客人來電指定梯)由線控顯式裁決,兩條出口建構於同一套 cascade 帳務引擎:
forcePlaceToTarget(db, mainOrderId, targetDepartureId, actor)(packages/core/src/lottery/failover.ts):線控指定同 trip 任意梯單一目標安置(不限 order_backup_choices)。單一 tx:FOR UPDATE 鎖主單 + 冪等(lottery_state null→not_lottery;已 cancelled 或已有 failover 子單→already_resolved;允許主單 applied/lost)→ 讀成交 snapshot + 算 cap(同 §3.2)→ 驗目標同 trip(否則 not_same_trip)、非主梯本身 → createOrderWithReservationInTx 扣目標席(sold_out/closed/departed→回 target_full,不 rollback 成 exhausted,讓線控改選)→ 重用 transferMainToTarget(resultLotteryState='won' / resultBookingState='confirmed',即使目標為抽籤梯也直接成立,線控已裁量)。force-place 為終態,不 carry forward 備案。沿用 credit-law、base_fare 用主單 snapshot ⇒ 同 trip 同價零價差。cancelLotteryLostOrder(db, mainOrderId, actor):線控顯式「取消退款」。內容沿用原 exhausted 邏輯(cancellation_reason='lottery_lost' → cancelOrder 釋席 / 殺 rails → 沿 failover_from_order_id 回溯鏈首 → 依鏈首 money state 選 receivable_reduction/overpayment_return 建 draft 退款;無訂金→null)。setOrderBackupChoices(db, orderId, departureIds[], actor)(整批 replace 語意):後台代客(電話下單 K 單)設定 / 調整 order_backup_choices。FOR UPDATE 鎖訂單、驗為抽籤單(lottery_state 非 null)且未終結 → 對 departureIds 逐一 server 重讀候選,剔除非同 trip / 異價(price_twd ≠ 主梯)/ 重複 / 主梯本身 / 不存在(沿用 checkout 靜默丟棄慣例、保輸入順序去重)→ DELETE 既有 + 依序重寫 rank。與公開站 checkout 備案寫入同一套驗證規則,只是 actor 換成後台。allocation_mode='lottery')的結帳流程(公開站 design system apps/web/src/components/ui/*)顯示抽籤說明文案 + 備案選擇器(backup-picker.tsx)讓旅客從同行程其他在售梯次中自選並排序多個備案;送出時 server action 設 lottery_state='applied' + 依序寫 order_backup_choices(同 trip 驗證、去重、保留排序)。團控不再提供「設定備案梯次」action(備案改客人自助)。lottery_state badge。applied 抽籤單另顯示備案管理區(setOrderBackupChoices):有序清單 + 新增(同 trip 同價在售候選 Combobox)/ 移除 / 排序,供後台代客(電話 K 單)維護備案;非 applied 只唯讀顯示譜系。/admin/control):抽籤梯次顯示應募訂單列 + 各筆 lottery_state + 已選備案筆數 + 逐單「中籤 / 未中籤」action(線控手動裁決);cascade 結果(落腳 / 重應募)可視。待人工安置列(exhausted)顯示 [手動安置](開 dialog 選同 trip 任意梯 force-place)+ [取消退款](cancelLotteryLostOrder);applied/lost 單亦可主動「手動轉團」(客人來電指定梯)→ 同一 force-place dialog。trip.manage / order.update),不新增 resource;所有 write action 受 module_lottery gate fail-closed。trip 任意梯(沿用 snapshot 同價、零價差)。真正換產品(跨行程、不同登山分級 / 證件需求、價格基礎不同)走既有「取消 + 退款 + 重新建單」,不硬塞進 cascade 引擎。| 版本 | 日期 | 變更 | Commit |
|---|---|---|---|
| 1 | 2026-06-20 | 初版(草案):未中籤自動轉備案梯次(取消主單 + 扣席成立備案單 + 訂金結轉 + 催尾款)、失敗 rollback 不靜默、訂金結轉帳務表達待與金流模型定。 | — |
| 2 | 2026-06-20 | 對齊定案席次模型:主單下單時已扣主梯席、未中籤取消即釋回;備案梯次不預扣,failover 當下才以 createOrderWithReservation 扣席。 | — |
| 3 | 2026-06-30 | 落地後更新(13-task SDD):§2 加 allocation_mode/lottery_state/cancellation_reason 欄 + 同行程同價約束(消滅價差開放議題);§3.1 轉單流程改寫為 inline-cancel + reservation primitive 保原子性;§3.2 訂金結轉改寫為應收 credit 法(failover_deposit_credit charge line,不建 payment_transactions transfer 列,保現金真相);§3.3 失敗 rollback 補 anomaly 另 tx + consent append-copy;§4 UI 更新為線控手動裁決(/admin/control)+ module gate fail-closed;§5 移除已解決的價差 / 帳務表達開放議題。 | — |
| 4 | 2026-06-30 | 多備案 + 鏈式 cascade 落地(客人側):單欄 backup_departure_id → order_backup_choices 有序多備案清單(同 trip、可混 FCFS/抽籤、無人為上限);§3.1 failoverToBackup(單目標)→ advanceToNextBackup(沿 rank cascade:FCFS 落腳 placed / FCFS 滿跳過 / 抽籤重應募 reapplied + 剩餘清單 carry forward / 走完 exhausted)+ 抽出 per-hop transferMainToTarget;§3.2 cap 改用 carriedDeposit(現金 + 結轉 credit) 防多跳訂金蒸發;§3.3 解除「禁止鏈式自動轉」、exhaust 退款回鏈首依 money state 選 receivable_reduction/overpayment_return;§4 UI 改公開站客人自選多備案(backup-picker,移除團控 setBackupDepartureAction);§5 開放議題更新(線控手動轉團 / K 單代選 / 原地改梯 → future)。supersede 單一備案模型。 | — |
| 5 | 2026-06-30 | 備案同價改為 checkout 強制約束:§2 同行程同價約束改寫——同 trip 不保證同價(promo / override 可異價),故「同價」由假設升級為 checkout 強制:公開站 backup-picker 只顯示同價候選(過濾 priceTwd),送出 server action 以 server 為權威重讀候選、剔除異價梯次,杜絕 cascade 跨單成交價漂移。 | — |
| 6 | 2026-06-30 | 後台收尾(A 線控手動轉團 + B K 單代選備案):§3.3 exhausted 行為改寫——不再自動取消 + 退款,改設 lottery_state='lost' 進「待人工安置」(保留主梯席次);新增 §3.4 forcePlaceToTarget(同 trip 任意梯 force-place,重用 transferMainToTarget,落點一律 won/confirmed,目標滿回 target_full 不破壞主單)+ cancelLotteryLostOrder(抽出原 exhausted 取消退款邏輯,改顯式觸發);新增 §3.5 setOrderBackupChoices(後台代客整批 replace,同 trip 同價 server 權威過濾);§4 UI 補待安置列 force-place / 取消退款 + 訂單詳情備案管理區 + 獨立手動轉團入口;§5 移除已解決 open issue(線控手動轉團 / K 單代選備案),保留跨 trip override / 原地改梯 / 反悔窗。無 schema migration。 | — |