一句話:旅客名單建檔 / 編輯時,逐人檢查資料完整與正確(身分證 checksum、生日合理性、緊急聯絡人邏輯、金額一致),把錯誤即時標出;規則還原自現行 GS「內表」的單一巨型驗證公式(見 legacy-gs-gap §2.3),驗證為讀時計算、不落 DB。
畫面(UI = wireframe,本功能為共用畫面上的 delta):
order-detail(旅客卡完整度 + 金額檢查)·control(名單資料狀態)
order_charge_lines / payment_schedules)· 行程定價 / 內容(價格政策)· 團控分房(travelers.gender 由其新增)· 證件效期提醒(同為 traveler 欄位品質)旅客 PII(姓名 / 身分證 / 護照 / 生日 / 緊急聯絡人)目前在 ERP travelers 表純自由輸入,無任何驗證。現行 GS「內表」靠一條巨型公式逐人把關,吐出十多種錯誤(身分證錯誤、生日未來人、緊急聯絡人是隊員、手機與緊連重複、收款差…)。痛點:
解法:把 GS 內表的驗證規則落成 ERP 的 traveler 驗證模組,建檔 / 編輯即時顯示問題,團控名單用「資料完整度」欄一眼看出哪幾位要補。非阻擋式(沿用 GS:只標記、不擋存,讓 OP 能分次補資料),但出團前置檢核會把未解決者標紅。
validateTraveler(traveler, context) → 回問題清單(每項:欄位 + 等級 + 訊息)。SUM(payment_schedules 各節點目標額) == SUM(current order_charge_lines)(收款計畫總額 == 當前應收總額)。應收真相在 order_charge_lines、收款節點在 payment_schedules(見 金流模型)——不再讀已從 orders 外殼移除的 total_twd / deposit_due_twd / balance_due_twd。travelers.address(入山名冊需要;現 schema 無)。validation_exempt(對齊 GS 的 ECPTLIST 免驗名單)。id_number 有值且看似台灣格式時套用;有護照無身分證者跳過 ID 檢查。travelers.gender;本 spec 驗「性別空 / 性別值非法」,不負責建欄(依賴關係見 §3.1)。對齊現行慣例:穩定欄位直接進
packages/core/src/schema/business.ts。
travelers 補欄位(drizzle schema)// packages/core/src/schema/business.ts — travelers 表 append
/** 通訊地址(入山人員名冊需要)。nullable。 */
address: text("address"),
/** 免驗標記:領隊 / 嚮導 / 佔位列等不需通過旅客驗證(對齊 GS ECPTLIST)。 */
validationExempt: boolean("validation_exempt").notNull().default(false),
依賴:
travelers.gender由 團控分房 擁有。本 spec 只定義驗證規則,不重複定義分房資料模型。
不新增驗證結果欄:驗證是讀時計算(view-time derived),不落 DB(對齊證件效期「不依賴排程、查詢時算」的精神)。要看哪些單有問題 → 查詢時跑
validateTraveler。
放 packages/core/src/travelers/validation.ts,不碰 db。
export type IssueLevel = 'error' | 'warning';
export interface TravelerIssue {
field: string; // 'id_number' | 'birth_date' | 'emergency_contact_phone' | ...
level: IssueLevel;
message: string; // 「身分證檢查碼錯誤」「緊急聯絡人為同團隊員」
}
export function validateTraveler(
t: TravelerInput,
ctx: {
partyTravelers: Array<{ fullName: string; phone: string | null }>; // 同訂單其他旅客(緊連=隊員檢查)
genderEnabled: boolean; // travelers.gender 是否已上線
},
): TravelerIssue[];
| 規則 | 等級 | 條件 | GS 來源 |
|---|---|---|---|
| 姓名空 | error | fullName 空 | 姓名空 |
| 性別空 / 非法 | error | (genderEnabled)gender 空 或 ∉ {male, female, other} | 性別空 / 性別錯誤 |
| 生日空 | warning | birthDate 空 | 生日空 |
| 生日未來 | error | birthDate > today | 生日錯誤(未來人?) |
| 生日過老 | warning | 年齡 > 100 | 生日錯誤(超級老) |
| 身分證空 | warning | idNumber 與 passportNo 皆空 | 身分證空 |
| 身分證檢查碼錯 | error | idNumber 看似台灣格式但 checksum 不過(§4.2) | 身分證錯誤 |
| 電話空 | warning | phone 空 | 電話空 |
| 信箱空 | warning | email 空 | 信箱空 |
| 地址空 | warning | address 空 | 地址空 |
| 緊急聯絡人姓名空 | warning | emergencyContactName 空 | 緊急聯絡人姓名空 |
| 緊急聯絡人電話空 | warning | emergencyContactPhone 空 | 緊急聯絡人電話空 |
| 緊急聯絡人為隊員 | error | emergencyContactName 命中同訂單其他 traveler 姓名 | 緊急聯絡人為隊員 |
| 手機與緊連重複 | error | 正規化後 phone == emergencyContactPhone | 手機與緊連人重複 |
validationExempt=true → 直接回 [](免驗)。-、空白後比對(同 GS REGEXREPLACE(...,"[- ]"))。/** 台灣身分證驗證:1 英文 + 9 數字 + 檢查碼。還原 GS 的字母轉碼 + 權重 mod 10。 */
export function isValidTwId(id: string): boolean {
// 字母轉雙位數(A=10 … 對照表 "ABCDEFGHJKLMNPQRSTUVXYWZIO" 之序 + 10),
// 拆兩位 → 套權重 [1,9,8,7,6,5,4,3,2,1,1] → SUM mod 10 == 0。
}
對「看似台灣身分證」(
/^[A-Z][0-9]{9}$/)才驗;純護照客(無 id_number)不套此規則。
/** 收款計畫總額 == 當前應收總額。不一致 → error「收款金額不符」。
* 金額來自金流模型:應收 = SUM(current order_charge_lines.amount_twd)、收款節點 = SUM(payment_schedules)。 */
export function validateOrderAmounts(o: {
receivableTwd: number; // SUM(current order_charge_lines.amount_twd)
scheduledTwd: number; // SUM(payment_schedules 各節點目標額)
}): TravelerIssue | null;
對齊 GS「收款差 = 集體應付 −(已排定收款)」。本檢查掛 order,不掛單一 traveler;金額來源為金流模型的
order_charge_lines(應收)與payment_schedules(收款計畫),非已從 orders 外殼移除的total_twd/deposit_due_twd/balance_due_twd(見 金流模型)。
/** 民國 / 西元混填 → 西元 YYYY/M/D。'114.5.30'、'民國114年5月30日'、'2025-05-30' 皆可。 */
export function normalizeBirthDate(raw: string): string | null;
還原 GS「正確資料」分頁的民國 / 西元轉換。前端輸入框 onBlur 呼叫,轉好填回,由使用者確認再存(不靜默改)。
validateTraveler,即時在欄位下顯示 issue(error 紅、warning 橘)。normalizeBirthDate 掛生日欄輸入輔助。存檔不擋(warning 與 error 都只提示;error 可在送出時二次確認「仍有 N 項錯誤,確定儲存?」)。/admin/control:每位旅客一個「資料」狀態:✓(無 issue)/ 橘(僅 warning)/ 紅(有 error)+ tooltip 列出問題。梯次標頭彙總「N 位資料待補」。/admin/control 進)。放 packages/core/src/travelers/validation.ts(純函式)+ 既有 traveler repo 擴充查詢帶出彙總。
// 團控 / order 用:一批 traveler + 同訂單上下文 → 每人 issues + 訂單金額 issue
getTravelerValidationForOrder(db, orderId): Promise<{
travelers: Array<{ travelerId: string; issues: TravelerIssue[] }>;
amountIssue: TravelerIssue | null;
}>
getDepartureValidationSummary(db, departureId): Promise<
Array<{ orderId: string; errorCount: number; warningCount: number }>
>
order.update 權限),不新增 permission;驗證在 server action 內附帶回傳 issues(前端顯示)。不新增 permission。沿用:
| 視圖 | gate |
|---|---|
| 編輯 traveler(含驗證提示) | order.update(sales / op / admin) |
| 團控資料完整度欄 | departure.read(admin / op) |
| order-detail 完整度 | order.read(依既有訂單可見範圍) |
vitest(純函式為主,對齊既有 tests/*.test.ts):
isValidTwId:正確 ID 通過;錯檢查碼擋;非台灣格式(護照)跳過。validateTraveler:各規則觸發(空值、未來生日、>100 歲、緊連=隊員、手機=緊連);validationExempt → 空清單;genderEnabled=false 時不檢性別。normalizeBirthDate:民國 114.5.30、民國114年、西元 2025-05-30、亂格式 → null。validateOrderAmounts:訂金+尾款=應收 → null;不符 → error。getTravelerValidationForOrder / getDepartureValidationSummary:彙總 error / warning 數正確;exempt 不計。(UI 即時驗證可選一條 Playwright:建錯資料 traveler → 團控顯示紅標 + tooltip。)
| 類別 | 數量 | 細節 |
|---|---|---|
travelers 新欄 | 2 | address、validation_exempt(drizzle;與 room-assignment 的 gender 同 PR 建議) |
| 新純函式模組 | 1 | packages/core/src/travelers/validation.ts(規則 + checksum + 日期轉換) |
| repo 擴充 | 1 | traveler / 團控查詢帶出 issue 彙總 |
| 改既有 page | 2-3 | traveler 編輯表單 · /admin/control · order-detail |
| Permission | 0 | 沿用 order.update / departure.read |
| 測試 | unit 為主 | ~12 vitest(純函式好測) |
不影響:schema 大改(只加 2 欄);不擋存檔;不依賴排程。
| 項目 | 預設 | 待釐清 |
| ----------------------------------------- | ------------------------------------------------------------ | ---------------------------------- | ------------ |
| error 是否在出團前轉「必須補齊」硬閘 | 本 spec 不擋;出團前置 呈現「未補齊」 | 出團前要不要擋出名冊 |
| 身分證以外的證號驗證(居留證 / 護照格式) | 只驗台灣身分證 checksum | 居留證 checksum 是否要做 |
| gender 值域 | 'M' | 'F'(依 room-assignment) | 是否需第三值 |
| 金額一致性掛點 | order 層,金流模型 order_charge_lines / payment_schedules | 金流模型已落地,規則可直接套用(不再依賴已移除的 orders 外殼欄) |
| 民國年起算 | +1911 | 邊界(民國 100 以下 2 碼)解析規則 |
travelers.gender)、行程定價(金額欄位)、證件效期 / 護照 OCR(護照欄位品質,分屬各 spec)travelers 表、traveler 編輯 action(order.update)、/admin/control 名單| 版本 | 日期 | 變更 | Commit |
|---|---|---|---|
| 1 | 2026-05-30 | 初版:GS 內表驗證規則落 ERP(checksum / 緊連邏輯 / 金額一致 / 民國轉換) | — |
| 2 | 2026-06-20 | 金額一致性對齊金流模型 v2:改以 order_charge_lines(應收)+ payment_schedules(收款計畫),移除對已從 orders 外殼移除的 total_twd / deposit_due_twd / balance_due_twd 引用 | — |
| 3 | 2026-07-01 | Code-is-truth:§4.1 gender 值域由 {M,F} 更正為 male | female | other(對齊 packages/core/src/travelers/validation.ts 的 VALID_GENDERS 與 schema CHECK)。 | — |