specs/settings.md

租戶設定

一句話:租戶層級設定子系統的 source of truth;新功能若要動設定(例如 團號 的代碼表、訂單派發 的派發模式)應 extend 本檔,而非各自描述。

畫面(UI = wireframe):../apps/wireframes/app/admin/settings/page.tsx

1. 範圍與定位

設定是 租戶層級(tenant-scoped):每個租戶 DB 一列 singleton(tenant_settings.id = 'singleton'),整個租戶共用。

重要:本系統沒有 per-user 個人設定表。 使用者個人層級只有:

  • better-auth 帳號(email / 密碼 / 未來 2FA,存各租戶 DB 自己的 user 表,與商業表同庫;無共享 auth_db)
  • 會員資料(member_profiles,見 會員)

凡是「整個旅行社的設定」一律進 tenant_settings;凡是「某個人的偏好」目前無對應載體(YAGNI,真需要時再開 user_preferences)。

2. 資料模型

tenant_settings 單例表(全欄位在 packages/core/src/schema/business.ts;singleton 有 DB CHECK 硬約束 id = 'singleton',插不進第二列)。現行欄位(以 code 為準):

欄位語意說明
id主鍵,恆 'singleton'(default + CHECK)
company_name / company_phone / tax_id / company_address / logo_url公司抬頭(顯示於發票、請款單、Email 頁尾)
site_tagline / seo_default_description / og_image_url站台級 SEO 預設(無頁面覆寫時 fallback)
email_reply_to回覆地址;無 email_from 欄——寄件人 = sender_local_part@sending_domain 運行時組合(emails)
sending_domain / resend_domain_id / sending_domain_status / sender_local_part自有寄件 domain 快照 + Resend 資源 id + 驗證狀態(CHECK enum)+ local-part(空值視為 info)
assignment_mode(CHECK:manual_review | auto_round_robin | auto_customer_choice | auto_by_destination)訂單派發模式
default_assignee_user_id(FK user)/ allow_sales_claim派發兜底業務 / sales 自派搶單開關
group_code_pattern / group_code_seq_style(CHECK:alpha | alpha_no_io | numeric)/ group_code_seq_width團號租戶自訂 pattern 與序號樣式
ecpay_merchant_id / ecpay_secrets_enc(jsonb EncryptedField)ECPay per-tenant 憑證(write-only、KMS envelope)
travelinvoice_merchant_id / travelinvoice_secrets_enc(jsonb EncryptedField)代收轉付憑證(同 pattern,travelinvoice-integration)
ecpay_fee_bps / fongshou_fee_bps / profit_tax_bps內外帳淨利試算費率(bps 整數)
sales_commission_bps / monthly_top_bonus_bps排行榜佣金試算費率
passport_expiry_warning_days(integer default 180)證件效期 threshold(海外模組生效時才用到)
updated_at最後更新

模組開通不在這裡:登山 / 海外模組「有沒有開通」是平台方決策,存 control_db.tenant(非 tenant_settings),由 provisioning CLI 管。租戶端只能唯讀看自己方案含哪些模組。見 模組化 §3.1。

派發兩欄(assignment_mode / default_assignee_user_id)已扶正進 drizzle schema(穩定業務欄位一律直接進 schema/business.ts,見 系統總綱);setup-business-extras.ts 現只 idempotent 建立 singleton 列、不再 ALTER 這兩欄。getTenantSettings 仍以顯式欄位清單的 raw SQL 讀取(避免 schema diff 意外遮蔽)。

3. 存取層

packages/core/src/settings/admin-repo.ts:

  • getTenantSettings(db): TenantSettingsRow — 讀 singleton,不存在則 lazy insert
  • updateTenantSettings(db, input) — 公司 / Email / 站台 SEO 欄位 partial update(只 patch 表單實際帶到的欄位,缺欄不動 → 各 tab 互不洗資料);SEO 三欄(site_tagline / seo_default_description / og_image_url)由 seo tab 的 updateSeoSettingsAction 呼叫
  • updateAssignmentSettings(db, input) — 派發模式子集(單獨 action / form path)

第一個參數一律 db: ScopedDb(branded type,見 系統總綱)。

4. UI 與權限

/admin/settings(src/app/(admin)/admin/settings/page.tsx),17 個 tab(順序同 code):

Tab(value)可寫?後端
公司資料(company)✏️ 可編輯updateTenantSettingsAction
訂單派發(assignment)✏️ 可編輯updateAssignmentSettingsAction(訂單派發)
付款設定(payments)🔒 唯讀 + ✏️ 可編輯provider 顯示唯讀(PAYMENT_PROVIDER env-backed,改需重啟);ECPay 憑證(特店 ID / HashKey / HashIV)為 per-tenant 可寫表單(updateEcpayCredentialsAction / clearEcpayCredentialsAction),三欄一次填齊、write-only 加密存租戶 DB,HashKey/HashIV 永不回讀(僅末 4 碼遮罩供顯示),無租戶憑證時僅 staging(ECPAY_IS_STAGING=true)退回平台 env 過渡,prod 即 fail-closed(見下方註腳)
收款帳戶(accounts)✏️ 可編輯收款帳戶目錄 + 費用類型路由維護(SettingsAccountsPanel:receiving_accounts CRUD 新增 / 改名 / 改性質 / 設幣別 / 停用 + fee_category_routes 費用路由設定,receiving_account.manage)。租戶自管、product-agnostic(不綁產品線);外幣帳戶=外幣池。從 多帳戶對帳 / 子系統 B 移入(對帳頁專注對帳、右上 link 過來)。唯一對 accountant(非 admin)也可見的 tab。
費用類別(expense-categories)✏️ 可編輯公司營業費用科目目錄 + 月損益分組(SettingsExpenseCategoriesPanel:expense_categories 新增 / 改名 / 停用;operating_expense.read 可見、operating_expense.manage 可寫)。admin + accountant 可見。見 公司營業費用 §2.1
損益分區(pnl-regions)✏️ 可編輯月損益分區維護(SettingsPnlRegionsPanel)。進階會計模組 tab。見 公司營業費用
月固定費用(recurring-expenses)✏️ 可編輯月固定費用範本維護(SettingsRecurringTemplatesPanel,每月生成 operating_expenses 列)。進階會計模組 tab,admin + accountant 可見。見 公司營業費用
內外帳費率(ledger-fees)✏️ 可編輯內外帳「淨利試算」預估費率(updateLedgerFeeRatesAction 寫 ecpay_fee_bps / fongshou_fee_bps / profit_tax_bps,basis points 整數,0–10000)。見 內外帳 §2.3
郵件設定(email)✏️ 可編輯不顯示任何 provider 憑證(Resend key 平台級 env、無 BYOK 欄位,決策見 emails §1.1);回覆 email_reply_to 與寄件 local-part(sender_local_part,預設 info)可編輯(updateTenantSettingsAction);自有網域寄件另配 SendingDomainActionForm。寄件 domain = 綁定站台 domain 派生,見 emails §2/§4
API / Webhook(api)🔒 唯讀顯示 ECPay notify / return callback URL 與 App Base URL,供外部設定
方案模組(modules)🔒 唯讀顯示平台授權的登山 / 海外 / 訂房模組(getTenantModules,來源 control_db.tenant.module_* 經 proxy header 注入);開通由平台 CLI 管、租戶不可自改 —— 只讓租戶管理者知道方案範圍。見 模組化 §3.1
PDPA / 同意書(consent)🔗 外連非就地編輯;以 <Link href="/admin/consent"> 導向同意書範本管理頁(consent_templates 版本化由該頁維護)
團號格式(group-code)✏️ 可編輯租戶自訂團號 pattern 與代碼表。見 團號系統
SEO / 站台(seo)✏️ 可編輯updateSeoSettingsAction 寫 site_tagline / seo_default_description / og_image_url(站台級 SEO 預設,無頁面覆寫時 fallback;見 SEO/JSON-LD)
外觀(appearance)✏️ 可編輯後台介面色彩模式(ThemeModeControl,next-themes attribute="data-theme";自動跟隨系統 / 固定淺色 / 深色)· 純前端偏好,不寫 tenant_settings
危險區域(danger)⚠️ 操作purgeTestData:TRUNCATE CASCADE 訂單 / 付款 / 旅客 / 同意書簽署 / 稽核紀錄(保留 trips / departures / consent_templates);繞過 append-only trigger(TRUNCATE 不 fire row-level trigger)
店面(storefront)✏️ 可編輯前台店面模板選擇(tenant_settings.storefront_template,儲存前可預覽 —— role.manage 鑄短效 signed token、前台 host 兌換後 draftMode + 簽名 cookie(split-host 下 cookie/session 不跨 host,token handoff 是唯一通路),預覽不進公開 cache)+內容槽編輯(storefront_contents 只存覆蓋值,預設住模板定義;圖片欄位走圖片選擇器)。admin-only,write action 掛 role.manage。設計 SSOT 見 店面模板

進階會計模組閘(accounting-module §5/§6):收款帳戶(accounts,含費用路由)/ 費用類別(expense-categories)/ 內外帳費率(ledger-fees)三個 tab 屬 module_accounting。accountingActive(modules) 為 false 時三個 tab(trigger + content)server 端不渲染、目錄資料不抓,其 write action 亦掛 requireAccountingModule() fail-closed。accountant 於 module OFF 無任何可管 tab → 顯示「尚未開通進階會計模組」空狀態;admin 仍看得到其餘非會計 tab。

權限:進站閘 = requirePermission('receiving_account.manage')(admin 走 wildcard、accountant 通過;op / sales 被擋在整頁外)。不再是舊的整頁 role.manage —— F1 把收款帳戶管理從對帳頁移入本頁後,若仍用 role.manage(admin only)會把持有 receiving_account.manage 的 accountant 一併擋在整頁外。進站後再用 isAdmin(hasPermissionForRole(..., 'role', 'manage'))二次守門敏感 tab:admin 看全部 tab;accountant 只看得到「收款帳戶」+「費用類別」等會計 tab(後者再 gate operating_expense.read),其餘 admin-only tab 不渲染、props 不外漏(defaultValue 對 accountant 落在 accounts)。寫入 action 各自 requirePermission(敏感 tab 對應 role.manage,帳戶 / 費用類別對應 receiving_account.manage / operating_expense.manage)。每次寫入記 audit_log(tenant_settings.update / tenant_settings.assignment_mode.change 等)。

付款憑證已 per-tenant(KMS envelope 加密存租戶 DB,/admin/settings 付款 tab 自填 write-only)。郵件憑證決議維持平台級(寄信是平台代履行的服務職責,與「錢進租戶帳戶」的 ECPay 本質不同)—— per-tenant 的是寄件身分(email_from / email_reply_to / 自有寄件 domain),見 emails。

5. 擴充點(未來設定相關功能掛這)

  • 團號:region / airline 代碼表、團號規則 → 新增設定區塊 + 表
  • 訂單派發:已落地的派發模式(第四種「依目的地分配」UI 已留 disabled placeholder)
  • 模組化:登山 / 海外模組開通由平台方控制(control_db,provisioning CLI),settings 只唯讀顯示方案範圍 —— 不是租戶可自改的設定
  • feature flags(未來多租戶規模化):tenant_settings.feature_* 欄,透過 CLI 改(見 memory wildtw-phase2-multitenant-strategy)
  • 開發者 tab(API 金鑰自助申請 + CLI session 檢視 / 撤銷):設計定案於 tenant-cli §3.1(@better-auth/api-key、PAT 式綁使用者、單一 Authorization: Bearer header 以 toko_sk_ 前綴分派、api_key.manage);MVP 建立限 admin(settings 既有進站閘不動;開放 sales / op 自建連同 UI 入口列 tenant-cli §11 開放議題)。wireframe 已有(settings 線稿「開發者」tab),code 未實作。資料不進 tenant_settings(key 存 plugin 的 apikey 表、CLI session 即 better-auth session 表)