SPay4 實作入口
狀態:實作文件規劃|196 項業務需求已決策;此入口只呈現實作所需的最新基線、已決策事項與執行 gate。
一眼看懂
- 現在要做: 審核 14 Core + 34 Domain = 48 relations 的初始化 SQL、依 Wave 實作、建立測試與 cutover evidence。
- 不再討論: SPay3 遷移設計、舊 table 數量、舊 API 方法估算、歷史替代方案。
- 唯一資料邊界: 新 SPay4 schema;
supply_account 為 Supply principal;不讀取或回退 Legacy authority。
實作流程
DDL 審核 → W0 Profile → W1 Foundation → W2 Onboarding/Auth → W3 Report → W4 Compatibility → W5 Cutover
主要審核請由 SPay4 HTML portal 進入;逐表 evidence 請見 表格完整性審核,完整程式碼路徑、Wave 與驗證則保留於 SPay4 實作規劃。
已決策(簡表)
| 分類 | 結論 |
|---|
| Schema | Greenfield CREATE TABLE、14 Core + 34 Domain = 48 relations、無 DB foreign key、table/column COMMENT、service transaction 維護關聯。 |
| Security | supply_account + fixed Supply profile;menu 是 projection,不是 authorization。 |
| Secret | 僅保存 non-secret credential reference;禁止 Legacy secret import/read/fallback/dual-store。 |
| Report | immutable fact/correction 與 shared hourly aggregate;Commission query-time 計算。 |
| Release | 無 dual-write;production 單次切換;SPay3 至少 30 天唯讀觀察。 |
新問題/執行 gate
無未決業務規則。執行前仍需完成 target/operator approval、authorization seed expected-state、import/reconciliation、credential reconnect、wire regression 與 cutover evidence。
審核入口
圖表
SPay4 文件 Reader 與 HTML portal 現在可發現全部 27 份 concepts:Current / Accepted 是實作 authority;規劃與實作、驗證矩陣/Runbook、歷史/參考文件均保留明確狀態,後者不作 current authority。
SPay4 實作規劃
1. 實作結論
- SPay4 以獨立 MySQL schema 平行建立;不重用 SPay3 的 table、DAO、session、routing 或 credential authority。
- 196 項業務需求均已決策;本階段只處理可追溯的實作切片、DDL artifact 與驗證,不再重開歷史設計討論。
- 初始 DDL 由 @@SPAY4TOKEN0@@ 所定義的 @@SPAY4TOKEN1@@ 組成,包含 14 Core relations 與 37 Domain relations,完整 inventory 為 51;獨立的 @@SPAY4TOKEN2@@ seed 經 SPay3 dev 唯讀查詢確認的 14 筆標準 Currency、34 筆啟用的 THB Thailand Bank,以及兩筆已凍結的 HOUSE Card Global daily limit。三份 artifact 均為待人工審核,不得直接對任何資料庫執行;Backoffice Menu Catalog Manifest 的完整 7 group/23 method metadata 可由獨立
menu-catalog.sql 在 ddl.sql schema read-back 後另案審核、seed 與 read-back,且不寫入 grant。
2. 已決策基線
| 範圍 | 已決策結果 |
|---|
| 資料邊界 | Greenfield schema、無 DB foreign key、所有 table/column 有 COMMENT,由 service transaction 維護關聯。 |
| 身分與授權 | supply_account 是唯一 Supply principal;Supplier/Gateway 用 fixed profile,BO 授權維持 versioned seed。 |
| 憑證與稽核 | Agent OGP/LINE 以 metadata+獨立 AES-GCM secret 保存;user 與 supply_account 各自直接保存 own password、TOTP、challenge 與 session state;每個成功提交且實際異動 SPay4 資料的 BO_USER command 與恰一筆 append-only audit 同 transaction;詳見 BO Domain Audit Contract。 |
| 報表 | immutable fact/correction → queue → shared hourly aggregate;Commission 於 query-time 計算,不保存 payment/result state。 |
| 切換 | 無 dual-write;先完成 import/reconciliation/reconnect evidence,production 僅切換一次並保留 SPay3 唯讀觀察。 |
詳細契約僅在 reviewer 需要追溯時閱讀: Rebuild Spec、Schema Contract、Authorization Contract、Onboarding Contract。
3. 程式碼實作路徑
| Wave | 程式路徑 | 工作內容 | 最小驗證 |
|---|
| W0 | settings.gradle、spay-bo/src/main/resources/application-spay4.yml | 建立隔離 profile、datasource 與 migration execution policy;不改動 Legacy datasource。 | profile 啟動時 Hibernate schema validation 通過。 |
| W1 | spay-bo/src/main/java/com/sit/spay2/bo/spay4/model/db/、dao/ | 實作 Agent integration credential metadata/secret、直接 credential-state 的 BO User/Supply Account、Supplier、Gateway、PT、direction config、allocation、獨立 SPay4 audit entity/repository、Spay4DomainAuditAction、write command、MANDATORY insert-only recorder 與 sanitizer;不得重用 Legacy DomainAuditLog。 | mapping/unique/index integration tests;audit shape/secret boundary test。 |
| W2 | spay-bo/src/main/java/com/sit/spay2/bo/spay4/service/、controller/;必要 contract 放 spay-common/.../spay4/ | Supplier atomic onboarding、owner scope、fixed profile、credential one-time delivery;每個 BO route 建立唯一 READ_ONLY 或 MUTATION(actionType) catalog entry,mutation service 在 commit 前寫一筆 audit。 | rollback、scope-negative、endpoint-catalog、success-only、secret-safe audit tests。 |
| W3 | spay-bo/src/main/java/com/sit/spay2/bo/spay4/report/、task/ | eligible fact、correction、projection queue/failure attempt/watermark/hourly aggregate 與 query-time Commission。 | per-fact atomicity、idempotency、retry/park/requeue、aggregate exactness tests。 |
| W4 | spay-bo/src/main/java/com/sit/spay2/bo/spay4/adapter/、spay-cardholder/... | OGP、LINE、Cardholder compatibility adapter;僅以 target service 作 authority。 | wire contract、HMAC、session、callback regression matrix。 |
| W5 | spay-bo/src/main/java/com/sit/spay2/bo/spay4/cutover/、docs/spay4/concepts/ | allowlist import、read-back、rehearsal、enable 與 30-day observation evidence。 | cutover gate matrix 全數通過。 |
spay-bo 仍是唯一可直接連 MySQL 的 module;不得建立新的獨立 Supply service 或讓 spay-cardholder/spay-scheduler 直接存取資料庫。
4. SQL artifact 與審核順序
DDL review → 人工核准 exact SQL → Operator 確認 target → 手動執行 → read-back → profile validate → integration test
- DBA 先逐表審核
ddl.sql 的 14 張 Core/master/auth/configuration/credential relation 與 37 張 Domain relation、data.sql 的 14 筆 Currency、34 筆 TH Bank 與兩筆 HOUSE Card Global daily limit;確認 14 + 37 = 51 的 inventory、欄位、unique key、query index、COMMENT 與無 foreign key 約束。 - 授權 reviewer 分別核准三份 exact artifact hash;獨立
menu-catalog.sql 可在 ddl schema read-back 後 materialize Backoffice Menu Catalog Manifest 的完整 metadata,但不建立 grant,也不越過 B/F/R runtime readiness gate。snapshot import 與 data repair 必須另案,不能附帶在初始化 SQL 或最小 Currency/TH Bank seed。 - Operator 在獨立 target schema 中手動選取資料庫,依
ddl → read-back → data → Currency/TH Bank/HOUSE Card limit read-back 嚴格順序執行;失敗立即停止,禁止修正後繼續執行。 - 以 schema read-back、表格完整性審核、Hibernate
ddl-auto: validate 與對應 integration test 驗證。
5. 新問題/執行 gate
以下是執行前必須產出的 evidence,不是未決業務需求:
- target database identity 與 operator approval;
- BO authorization versioned expected-state seed 的個別審核;
- Legacy allowlist import 的 mapping、reject、reconciliation 與 read-back;
- OGP/LINE integration credential rotation、direct-principal TOTP 與 session boundary proof;
- credential provider reissue/reconnect proof;
- OGP、LINE、Cardholder wire compatibility regression;
- cutover rehearsal、final sign-off 與 30-day observation record。
6. 完成定義
每個 Wave 必須同時交付:程式碼、對應 schema artifact、正反向測試、audit/security evidence 及文件連結。任何 SQL、import、credential 或 cutover evidence 缺失時,該 Wave 保持未完成;不得以 menu、API 可呼叫或單一 happy-path test 宣告完成。
SPay4 實作就緒路線圖
1. 結論與 authority
本文件是 SPay4 W0–W3 的 implementation-readiness authority:它將已接受的 target schema、authorization 與 onboarding contract 拆成可驗證的實作波次;不授權 Java、DDL、SQL、target database 操作、credential provider deployment、外部 adapter 或 cutover。
sql/V4/ddl.sql 與 sql/V4/data.sql 都是待人工審核 artifact,不是已執行 migration,也不是 runtime 已就緒的證明。所有 target relation 均為 logical relation;service transaction 與 guard 維護完整性,絕不建立 database foreign key。
Current target authority 只有本文件、Target Schema Foundation Contract、Authorization and Menu Contract、Backoffice Menu Catalog Manifest、Prefix and Supplier Onboarding Contract、Target ER 審核 與 表格完整性審核。current diagram 是上述 authority 的導覽視覺化,不新增或取代 DDL contract。D1/W1 開始時,Deposit/Withdrawal 工作台另以 Order Workspace Contract 凍結 owner scope、projection、CSV、Action、時間與 Legacy report boundary。
2. 就緒狀態與開始 gate
| 狀態 | 現況 | 不可據此宣稱完成 | 開始 gate |
|---|
| 已存在、未執行 DDL | Core/Domain schema 與 Currency seed 草稿已在版本控制。 | schema 已建立、seed 已核准、target 可用或 Hibernate 可 validate。 | DBA 分別審核 exact artifact、Operator 確認隔離 target、手動執行與 read-back。 |
| 尚未存在 runtime | spay-bo 尚無 spay4 package、隔離 profile、entity/repository、realm guard、onboarding 或 projector。 | 任何 W0–W3 API、policy、queue、報表或 credential lifecycle 已可使用。 | 依本文件 W0 → W3 前置與完成 gate 實作、測試。 |
| 外部/人工 gate | target identity、SQL/seed approval、credential provider、release manifest、cutover evidence 尚未取得。 | automation、文件或單一 happy path 已替代人工 approval。 | 對應 owner 提供具 target identity 的 read-back 或 evidence,缺一即停。 |
3. Target relation matrix
此 matrix 以 51 張 target relation 為 inventory;同一列中的多個 target key 是該 relation 的完整 logical target set,而不是 database FK。currency 被各 target relation 的 currency_id 邏輯引用;realm 只能分類 principal,不能取代任何 owner key。D1/W1 的 order money、reservation、counter 與 house_card_transaction 固定 DECIMAL(30,10);PT/reporting relation 維持 DECIMAL(38,10)。
| 來源 table/key | 目標 table/key | Cardinality | 關係性質 | Service guard |
|---|
currency.id | bank.currency_id | 1:N | Core Currency reference | 僅 active Currency 可被 Bank mutation 引用。 |
bank.id | agent_bank.bank_id | 1:N | Agent–Bank availability 的一端 | 驗證 Bank active;不以此決定 Supply owner。 |
agent.id | agent_bank.agent_id | 1:N | Agent–Bank availability 的另一端 | 驗證 Agent active 與 pair uniqueness。 |
agent.id | currency.id(agent.currency_id) | N:1 | Agent immutable Currency 與 business_utc_offset | 僅 active Currency;offset Create 明確提供、無 default、建立後 immutable,是 House 日界與 effective-binding time authority;不推導 Gateway Currency。 |
system_group.id | system_method.system_group_id | 1:N | BO resource group → method | release manifest expected state 與 active lifecycle 必須一致。 |
system_method.id | role_method.system_method_id | 1:N | method → approved Role grant | manifest 是 resource/action 唯一 authority;DB 僅 materialization。 |
role.id | role_method.role_id | 1:N | Role → approved method grants | Role 與 grant 均 active,且 grant mutation 與 epoch revoke 同 transaction。 |
role.id | user.role_id | 1:N | BO User 授權 Role | User、Role active,並檢查 owner matching。 |
agent.id | role.agent_id | 1:N/system scope | Prefix Role owner;null 是 system scope | Role/User 的 agent_id 必須相同,或合法 system scope。 |
agent.id | user.agent_id | 1:N/system scope | Prefix BO User owner;null 是 system scope | 禁止 cross-owner Role rebind 與 client scope escalation。 |
system_config.id | agent_system_config.system_config_id | 1:N | global default → Agent override | 只允許 active global key 的 Agent override。 |
agent.id | agent_system_config.agent_id | 1:N | Agent-scoped configuration | Agent scope 只覆寫自身;不得成為 Supply/Legacy fallback。 |
agent.id | credential_reference.agent_id | 1:N | Agent OGP/LINE integration credential metadata | OGP scope 固定 DEFAULT;LINE scope 必須匹配 active notification_provider_source,不得綁定 Supply account。 |
notification_provider_source.id | credential_reference.notification_provider_source_id | 1:N/LINE only | LINE webhook provider registry | 僅 LINE_WEBHOOK 可設定 active source;source type/vendor/parser profile 由 service 驗證。 |
credential_reference.id | credential_secret.credential_reference_id | 1:1 current secret | Agent integration metadata → current AES-GCM secret | 僅 current reference 可有 current secret;rotation 以 optimistic version 原子覆寫,不保留 request/callback 分離 slot。 |
credential_reference.id | external_api_replay_nonce.credential_reference_id | 1:N、5 分鐘窗口 | External Order HMAC replay guard | nonce 僅保存 canonical UUID 與 UTC expiry;claim 使用獨立短 transaction,cleanup 固定批次刪除過期 row。 |
supplier.id | supply_gateway.supplier_id | 1:N | Supplier 是 Gateway 唯一 owner | Supplier active,Gateway immutable owner;allocation 不改 owner chain。 |
currency.id | supply_gateway.currency_id | 1:N | Gateway immutable Currency | onboarding 僅接受 active Currency;Supplier 不具自身 Currency。 |
supplier.id/supply_gateway.id | supply_account.owner_type + owner_id | 各 1:N | Supplier 或 Gateway login principal | fixed SupplyProfile 必須匹配 immutable owner type 與 active owner chain。 |
| committed actor/target scope | domain_audit_log.actor_id、target 與 owner snapshot keys | 1:N evidence | append-only committed mutation evidence | audit 與 mutation 同 transaction;不可作 credential、permission 或 owner authority。 |
| Supplier/Gateway/Cardholder owner | supply_pt_policy.owner_type + owner_id | 各 1:1 | owner-level PT aggregate | owner identity unique;保存 aggregate version 與共享 immediate-change entitlement。 |
supply_pt_policy.id | supply_pt_revision.policy_id | 1:N | current/pending/immutable history revision | unique active slot 只允許一筆 current 與 pending;history 永久保留。 |
supply_pt_revision.id | supply_pt_revision_line.revision_id | 1:2 | atomic DEPOSIT/WITHDRAWAL desired state | unique revision/direction;service transaction 驗證完整 pair,Cardholder line 才使用 base fee/threshold。 |
supply_gateway.id | supply_gateway_direction_config.supply_gateway_id | 1:N(每 direction 一筆) | Gateway per-direction order limits | unique Gateway/direction;不保存 enable 或 daily limit。 |
supply_gateway.id | supply_gateway_agent_allocation.supply_gateway_id | 1:N | Gateway availability side | Gateway active;allocation 僅表示可用性、不保存 Supplier key。 |
agent.id | supply_gateway_agent_allocation.agent_id | 1:N | Agent availability side | Agent active;unique Gateway/Agent pair,絕不改變 owner chain。Agent-first read 經 Gateway→Supplier chain 驗證 active lifecycle、Currency 與 offset equality;符合者可跨 Supplier 列出。 |
| accepted source identity | supply_eligible_fact 的 owner/currency_id/direction snapshot | 1:1 idempotent fact | immutable canonical financial evidence | 內部 fact-acceptance 驗證 source identity、active owner chain 與 canonical UTC hour/month。 |
supply_eligible_fact.id | supply_eligible_fact_correction.original_fact_id | 1:N | immutable correction 回投原 canonical bucket | correction identity unique;僅可寫 signed delta 至 original fact hour/month。 |
| fact/correction work identity | supply_eligible_fact_projection_queue.work_type + work_identity | 1:1 work | durable unfinished projection state | acceptance/correction 與 queue 建立原子化;queue 不是 financial authority。 |
supply_eligible_fact_projection_queue.id | supply_eligible_fact_projection_failure_attempt.projection_queue_id、supply_aggregate_projection_watermark.logical_source + partition_key、supply_financial_hourly_aggregate canonical grain | 1:N attempts;projection | operation evidence → read model | claim/apply exactly once;failure immutable;watermark 不作 completeness gate;aggregate 以 UTC hour、Commission month、owner keys、Currency、direction 唯一 grain 更新。 |
| supply_gateway.id | supply_cardholder.supply_gateway_id | 1:N | immutable CH owner | 建立時驗證 Supplier redundancy;禁止 reassignment、Agent fallback 或 Prefix owner。 | | supply_cardholder.id | supply_cardholder_session.supply_cardholder_id | 1:N | App session | session epoch、active lifecycle 與 expiry 必須同時驗證。 | | supply_cardholder_session.id | supply_cardholder_device_token.supply_cardholder_session_id | 1:N | device generation | 僅使用 current active registration;不保存原始 token。 | | supply_cardholder.id | supply_cardholder_notification.supply_cardholder_id | 1:N | notification history | read/delivery transition 採 CAS,payload/audit 不含 secret。 | | supply_cardholder.id | payment_account_application.supply_cardholder_id | 1:N | account application | 檢查 Cardholder owner、Bank/Currency 與 review authority。 | | supply_cardholder.id | payment_account.supply_cardholder_id | 1:N | immutable Payment Account owner | Gateway/Supplier redundancy 必須與 Cardholder chain 一致。 | | payment_account.id | payment_account_balance_log.payment_account_id | 1:N | append-only balance ledger | before/delta/after 與 account balance 在同一 transaction 驗證。 | | agent.id | house_card.agent_id | 1:N | Prefix-owned HOUSE | HOUSE 沒有 Gateway/Cardholder owner,且只作 Deposit。 | | payment_account.id | payment_account_daily_counter.payment_account_id | 1:N/day/direction | account quota | lock (account,date,direction) 後 reserve/release/consume,防止並行重複占用。 | | supply_cardholder.id | supply_cardholder_daily_assignment_counter.supply_cardholder_id | 1:N/day | fairness counter | 不保存 Agent;Supplier/Gateway redundancy 由 immutable owner chain 驗證。 | | agent.id | agent_api_order_claim.agent_id | 1:N | Agent API idempotency claim | UQ (agent_id, order_type, agent_order_id);不讀寫 Legacy agent_api_order_log。 | | agent_api_order_claim.id | supply_deposit_order.claim_id/supply_withdrawal_order.claim_id | 1:1 by direction | target formal order | 同一 claim 只連一筆方向相符 formal order;request fingerprint retry 不重複寫入。 | | formal order | supply_order_assignment_attempt.order_type + order_id | 1:N | immutable assignment history | Order current pointer/projection 與 attempt/reservation 同 transaction 更新。Deposit 的 payer_account 在 create 正規化後 immutable。 | | supply_deposit_order.id/agent.id/credential_reference.id | agent_callback_outbox.supply_deposit_order_id/agent_id/credential_reference_id | 各 N:1;order/event unique | Deposit terminal callback intent | 僅保存 immutable validated URL snapshot、safe v2 payload 與 signing credential reference;idempotency、dispatch CAS/lease/retry 必須由 runtime guard 驗證,schema 存在不代表 dispatcher 已啟用。 | | payment_account.id | payment_account_reservation.payment_account_id | 1:N | capacity/balance reservation | UQ order/attempt;review-held 是 projection,不是 reservation status。 | | assignment attempt | supply_order_proof.assignment_attempt_id | 1:N | proof metadata | content hash unique;原始 proof 不進 DDL/Audit。 | | verified LINE source/derived Agent scope | house_card_transaction.notification_provider_source_id/agent_id | 各 N:1 | immutable HOUSE inbound Deposit evidence | provider identity unique;canonical replay idempotent、payload conflict fail closed;raw notification 僅保存 ciphertext。 | | withdrawal order | supply_withdrawal_recovery_case.withdrawal_order_id | 1:N | duplicate/late recovery | PENDING_RECOVERY 與 case/evidence/resolution/verdict/actor/time/Audit 同 transaction;System Admin 可全域結案,已驗證 SUPPLIER 僅在 active immutable own-Supplier chain、既有 emergency authority 與 non-empty reason guard 全數通過時可結案,Gateway 一律拒絕。 | | house_card.id | house_card_daily_counter.house_card_id | 1:N/day | HOUSE Deposit quota | UQ HOUSE ID/date;跨日與不同 HOUSE ID 不搬移或合併。 | | Deposit assignment attempt | house_card_deposit_reservation.assignment_attempt_id | 1:1 | HOUSE Deposit quota reservation | requested/confirmed actual adjustment、release 和 counter 在同 transaction;不得用於 Withdrawal。 | | workspace scope | list/detail/helper/CSV/action projection | 1 policy | authorization boundary | 同一 server-derived OrderWorkspaceScope;跨 scope detail/mutation 不洩漏存在性。 |
4. W0–W3 implementation route
W0 — 隔離 runtime foundation
| 項目 | 內容 |
|---|
| 輸入 | 已審核且 read-back 的 target schema、隔離 target identity、DBA/Operator approval。 |
| 輸出 | application-spay4.yml、target datasource 與 migration execution policy、Hibernate ddl-auto: validate profile。 |
| 禁止事項 | 不讀取或改寫 Legacy datasource;不自動執行 DDL、seed、import 或 repair。 |
| 依賴 | DDL 與 data seed 均已依 ddl → data 順序人工執行與 read-back。 |
| 最小驗證 | profile 僅對已確認 target schema 進行 Hibernate validate;連線/schema identity mismatch 必須 fail closed。 |
| 完成 gate | 人工批准與 read-back 可追溯、profile validate 通過,且無 Legacy datasource fallback。 |
W1 — BO auth/config 與 active principal
| 項目 | 內容 |
|---|
| 輸入 | W0 profile、49 relation mapping、同 release 的 policy-catalog manifest。 |
| 輸出 | Core/Domain entity mapping 與 repository、Agent OGP/LINE credential metadata/secret、共用 TOTP credential、Supply password provider reference、expected-state seed/preflight、BO_USER 與 SUPPLY_ACTOR realm resolver、session epoch/active lifecycle guard、resource-action guard、owner-scope resolver、Redis derived projection cache。 |
| 禁止事項 | 不新增 Supply role table;不以 DB discovery 改寫 manifest;不以 Redis、本機 stale cache、menu、JWT decoded claim 或 client owner ID 作 authority。 |
| 依賴 | versioned manifest、每份 exact seed 的獨立人工 SQL approval;preflight 僅 read-only。 |
| 最小驗證 | realm mismatch、inactive account/credential/Role、epoch mismatch、cross-owner、client scope escalation、seed mismatch 與 Redis failure 皆 fail closed。 |
| 完成 gate | readiness preflight、grant revoke atomicity、cache safety 與 owner-scope negative matrix 全數通過。 |
W2 — Supplier/DEFAULT Gateway onboarding
| 項目 | 內容 |
|---|
| 輸入 | W1 Superadmin guard、active Currency lookup、credential provider abstraction 與 transaction/audit mapping。 |
| 輸出 | POST /bo/v2/suppliers application service:單一 transaction 建立 Supplier、direct-credential Supplier account、兩組 PT、DEFAULT Gateway、兩筆 direction config、direct-credential Gateway account 與 success-only non-secret audit。 |
| 禁止事項 | Prefix Create 建立任何 Supply aggregate、allocation 或 Legacy channel;after-commit 補建、REQUIRES_NEW audit、credential read-back、secret audit/log/telemetry 與 Supplier-level Currency。 |
| 依賴 | credential material 僅可在 transaction commit 成功後一次性 response delivery。 |
| 最小驗證 | canonical identity collision、inactive Currency、任一 persistence/credential/audit failure 都無 partial state。 |
| 完成 gate | rollback、Prefix negative boundary、secret-safe audit 與 one-time credential exposure tests 全數通過。 |
W3 — Fact、queue 與 hourly aggregate
| 項目 | 內容 |
|---|
| 輸入 | W1 owner-scope/realm guard、W2 owner chain、fact/queue/aggregate mappings。 |
| 輸出 | 僅 BO 內部 idempotent fact-acceptance service 與 test fixture;correction、queue claim/apply、immutable failure attempt、UTC-minute bounded recovery、operator requeue、watermark 與 aggregate read/query-time Commission。它不建立 Deposit/Withdrawal summary;工作台查詢與 R1 Commission Report 維持分離。 |
| 禁止事項 | OGP、LINE、Cardholder 或任何 external adapter 呼叫尚未定義的 endpoint;讀取 SPay3 persistence、Legacy fallback、dual-write、persist Commission result 或將 watermark 當 report completeness gate。 |
| 依賴 | W4 才能讓 OGP/LINE/Cardholder adapter 呼叫 internal fact-acceptance service,且仍只以 target service 為 authority。 |
| 最小驗證 | per-fact atomicity、exactly-once、original-bucket correction、1/5/15/60 retry、第五次 park、50-item/30-second recovery、available-facts report semantics 與 owner-scope negative tests。 |
| 完成 gate | queue/aggregate exactness、immutable failure evidence、operator requeue 與所有負向 scope/security test 均通過。 |
5. 明確排除與後續 handoff
W4 compatibility 與 W5 cutover 不屬本輪實作:W4 才處理 OGP/LINE/Cardholder target-service adapter 與 wire regression;W5 才處理 allowlist import、reconciliation、rehearsal、single cutover 與 read-only observation。
本輪不新增 database FK、Supply role relation、Legacy credential/config fallback、SPay3 persistence read、dual-write、外部 fact adapter 或 target database 操作。任何 DDL、seed、import、credential provider deployment 或 cutover action 都需要各自的人工批准與證據,不能由本文件替代。
6. 驗證與 review checklist
- 執行
node docs/spay4/tools/validate-docs.mjs:UTF-8 without BOM、portal artifact inventory、local links、49 relation inventory、D1/W1 money precision、HOUSE transaction evidence/matching key、credential boundary 與 SQL safety checks 必須通過。 - 人工核對本 matrix 的 table 名稱、owner key、unique aggregate grain 與資料流對應
sql/V4/ddl.sql 及三份 accepted contract。 - 僅檢視現有 V4 SQL workspace 修改;不修改、執行或宣稱已核准任何 SQL artifact。
SPay4 Target Schema Foundation Contract
Scope and exclusions
本contract描述 SPay4 target relation 的 identity、owner/snapshot authority、required field group 與 query-shaped index intent,供#197審閱。完整候選 inventory 為 ddl.sql 的 14 Core 加 37 Domain,合計 51 relations;逐表結果與文件/HTML finding 請見 表格完整性審核,完整 nullable/cardinality 請見 Target ER 審核。每張表的 DDL 必須有 table/column COMMENT;關聯由 service transaction 驗證,不建立 DB foreign key。本文件不是DDL、seed、migration、database connection或runtime implementation授權。
SPay3 table、superseded Supply actor relation、Legacy credential/session/routing/financial/reporting persistence都不是target authority。realm不能取代agent_id、supplier_id或supply_gateway_id。
Target relation matrix
| Relation | Identity/required field group | Authority與owner scope | Query-shaped index intent |
|---|
agent | id;immutable Currency、Prefix 與 business_utc_offset CHAR(6) NOT NULL | Agent 固定 ±HH:MM offset 是 House 日界與 Agent–Gateway effective-binding time authority;Create 明確提供、無 default、建立後 immutable | unique prefix;(currency_id, status, id) |
notification_provider_source | id;source type、implementation vendor、display name、parser profile | LINE webhook provider registry;不保存 credential material | unique (source_type, implementation_vendor);active source lookup |
credential_reference | id;agent_id、kind、scope、LINE provider source、lifecycle/version | Agent OGP/LINE integration 的 non-secret metadata;OGP scope 固定 DEFAULT | unique (agent_id, kind, scope);Agent lifecycle 與 LINE provider lookup |
credential_secret | id;credential reference、API key/fingerprint、AES-GCM cipher、key version、rotation/version | credential_reference 的一對一 current OGP/LINE secret;唯一可保存 integration ciphertext | unique reference、API key、fingerprint |
supplier | id;immutable code=login_id;可更新 name、business UTC offset、operational/lifecycle/version | Supply root;Supplier是Gateway唯一owner | unique code、unique login_id;owner/detail by id |
supply_gateway | id;supplier_id、immutable currency/code;name、operational status、version | Gateway Currency與operational authority;不保存Agent owner或offset | unique (supplier_id, code);(supplier_id, currency, operational status, id) |
supply_account | id;immutable owner_type/owner_id;global unique account;BCrypt password hash、TOTP AES-GCM cipher、single pending password-change challenge、session epoch、fixed profile、account lifecycle、version | Supplier或Gateway登入principal;直接保存 own credential/session state,不保存 Agent integration key | unique account;(owner_type, owner_id, account lifecycle, id) |
domain_audit_log | append-only id;actor、stable action_type、aggregate-root target、owner keys、non-secret before/after/effect-object array、risk、request/trace | committed successful BO mutation evidence;audit write與mutation同transaction | (supplier_id, create time)、(supply_gateway_id, create time)、(agent_id, create time)、target lookup、(action_type, create time) |
supply_pt_policy | id;immutable owner_type/owner_id;共享 immediate-change state、aggregate version | Supplier/Gateway/Cardholder owner-level PT aggregate | unique (owner_type, owner_id) |
supply_pt_revision | id;policy、CURRENT/PENDING/HISTORICAL、effective month、active slot、version | 原子化兩方向 desired-state revision;history 永久唯讀 | unique (policy_id, active slot);policy/status/month rollover lookup |
supply_pt_revision_line | id;revision/direction、rate、base fee、threshold | revision 的唯一 Deposit/Withdrawal line;Cardholder 才使用 fee/threshold | unique (revision_id, direction) |
supply_gateway_direction_config | id;supply_gateway_id/direction;min/max per-order limit、version | Gateway direction current config;不保存direction enable或daily limit | unique (supply_gateway_id, direction) |
supply_gateway_agent_allocation | id;supply_gateway_id/agent_id;enabled、priority、version | Agent與Gateway N:M availability,不改變 Gateway owner、也不保存 Supplier key;Supplier 必由 Gateway owner chain 解析 | unique (supply_gateway_id, agent_id);(agent_id, enabled, priority, supply_gateway_id) 支援 Agent-first enabled-binding read |
supply_eligible_fact | immutable id;canonical source identity、occurrence instant、owner keys、currency、direction、amount、offset snapshot | canonical accepted financial evidence | unique canonical source identity;canonical-hour/owner query |
supply_eligible_fact_correction | immutable id;original fact reference、correction identity、signed delta | correction永遠屬原fact canonical hour/month | unique correction identity;original fact lookup |
supply_eligible_fact_projection_queue | id;fact/correction work identity、current retry state、next eligibility | durable unfinished projection work;不是financial authority | unique work identity;(retry state, next eligibility, id) |
supply_eligible_fact_projection_failure_attempt | immutable id;queue identity、committed failure sequence、classification/sanitized diagnostic | operations evidence;不表示投影完成 | unique (queue identity, failure sequence) |
supply_aggregate_projection_watermark | id;logical source/partition、contiguous source cursor | projector progress only;不是report completeness gate | unique (logical source, partition) |
supply_financial_hourly_aggregate | id;UTC hour、Commission Accounting Month、owner keys、currency、direction、eligible amount/fact count、published revision | Report/Commission唯一共用read model;Commission為query-time derived | unique aggregate grain;owner+hour/month report queries |
Onboarding, report and seed boundaries
Supplier onboarding同一transaction建立Supplier、直接保存 credential state 的 Supplier initial supply_account、Supplier PT policy 與 current zero-value revision pair、DEFAULT Gateway、直接保存 credential state 的 Gateway initial supply_account、Gateway PT policy 與 current zero-value revision pair、兩筆Gateway direction config與一筆 required CREATE_SUPPLIER aggregate audit;Cardholder onboarding 同樣建立其 PT policy 與 current zero-value revision pair。任一失敗都rollback。audit target 為 Supplier,effect-object array 列出全部子資源的 non-secret committed effects。user 與 supply_account 分別保存 own TOTP/session state;Agent OGP/LINE integration credential 不屬於 onboarding aggregate。
Report以per-fact projection更新aggregate,並保留每分鐘recovery。Hourly Report Reconciliation只隔離重建已結束UTC hour,完整驗證後原子發布;可重跑但不得double count,不持久化Commission result。
本Issue不提供可執行seed。system_method/role_method的versioned idempotent BO_USER seed由Authorization child擁有;每份future seed或DDL package都須先對本contract取得獨立人工批准,再取得Operator/target approval。
| Relation | Future table COMMENT | Required column COMMENT intent |
|---|
agent | Agent/Prefix master | business_utc_offset 是建立時明確設定、immutable、無 default 的 canonical ±HH:MM time authority |
supplier | Supply owner root | code/login_id canonical identity;business_utc_offset current owner time authority |
supply_gateway | Supplier-owned Supply Gateway | supplier_id immutable owner;currency_id immutable Gateway currency;operational status current state |
notification_provider_source | LINE webhook provider registry | source type/vendor 是 provider identity;parser profile 是穩定 routing metadata |
credential_reference | Agent integration credential metadata | agent_id 是 owner;OGP DEFAULT scope、LINE provider source/scope 與 lifecycle 都是 non-secret metadata |
credential_secret | Current integration ciphertext | reference 一對一;僅 api_secret_cipher 保存 OGP/LINE AES-GCM ciphertext |
supply_account | Supply actor login principal | owner_type/owner_id immutable owner binding;account global login identity;password/TOTP/challenge/session epoch 與 profile/lifecycle 都是 current principal state |
domain_audit_log | Append-only committed successful BO mutation audit | actor/action_type/aggregate-root target/owner keys are scope snapshots; before/after/effect-object array must be non-secret |
supply_pt_policy | Supply owner PT aggregate | immutable owner binding, shared immediate-change entitlement and aggregate version |
supply_pt_revision | Supply PT revision header | active slot permits one current/pending revision while immutable history remains append-only |
supply_pt_revision_line | Supply PT direction line | revision/direction is unique; service always writes a complete Deposit/Withdrawal pair |
supply_gateway_direction_config | Gateway per-direction current limit | Gateway reference and direction form unique grain; min/max are per-order current limits |
supply_gateway_agent_allocation | Agent-to-Gateway availability mapping | both relation IDs define mapping; enabled/priority are current availability, not ownership;Supplier scope is resolved only through Gateway owner chain |
supply_eligible_fact | Immutable accepted financial evidence | source identity is idempotency authority; owner keys/offset are immutable snapshots |
supply_eligible_fact_correction | Immutable delta for original eligible fact | original fact reference and correction identity are immutable; signed delta stays in original bucket |
supply_eligible_fact_projection_queue | Current unfinished projection work | work identity is unique; retry state/next eligibility are operational current state only |
supply_eligible_fact_projection_failure_attempt | Immutable projection failure evidence | queue identity plus committed sequence is unique; diagnostic must be sanitized |
supply_aggregate_projection_watermark | Per-source partition projection progress | source/partition identity is unique; cursor is contiguous progress, not report completeness |
supply_financial_hourly_aggregate | Shared hourly Report and Commission read model | UTC hour/Commission month/owner keys form aggregate grain; amount and fact count are latest published values |
Naming and owner-key trace
| Contract surface | Required name/authority |
|---|
| schema/relation | supply_account only; never superseded Supply actor relation |
| API DTO/JWT principal | future principal identity is supplyAccountId; fixed profile and owner scope resolve server-side from the same supply_account identity |
| credential mapping/audit target | credential_reference.agent_id 僅綁定 Agent OGP/LINE integration;user 與 supply_account 各自直接保存 own password/TOTP/challenge/session state;audit actor/action_type/aggregate-root target 只保存非 secret snapshot,effect 必為安全 object array |
| owner chain | Supplier → Supply Gateway → Supply Cardholder → Payment Account; Agent allocation is availability only,且可跨 Supplier,不保存或推導 Agent-owned Supplier scope |
| report scope | applicable agent_id、supplier_id、supply_gateway_id are indexed keys; realm only classifies authenticated principal and cannot supply owner scope |
Legacy-authority exclusion review
- target schema不讀取、匯入、fallback或dual-store Legacy raw secret、ciphertext、credential reference或configuration。
credential_secret 僅保存 Agent integration AES-GCM ciphertext;user 與 supply_account 各自直接保存 own TOTP AES-GCM ciphertext。metadata、audit、log、telemetry、seed 與 Frontend persistent state 均不得保存 secret material。- OGP callback URL 設定 authority 只可取
agent.callback_url;Deposit terminal event 的 agent_callback_outbox.callback_url_snapshot 僅保存由該 authority 驗證後凍結的 immutable destination,不是第二個設定來源。credential relation、Legacy pending URL 或 request payload 都不得提供 callback URL;outbox 不保存 signing material。 - 不以SPay3 table、DAO、entity、session、routing、financial或reporting persistence作target authority;
superseded Supply actor relation與Supply role table不建立。 - queue、failure attempt與watermark只承載projection operations state;只有accepted fact、correction與published aggregate形成financial read authority。
- 本Issue沒有可執行seed或DDL。未來每份seed/DDL package都必須先取得本contract的獨立人工審核,再取得Operator/target approval。
Review evidence
本 relation matrix、table/column comment matrix、naming/owner-key trace、Legacy-authority exclusion review 與 表格完整性審核 共同覆蓋#197的 documentary acceptance criteria。表格完整性審核不採納任何 DDL 修正;nullable system-scope user uniqueness 維持為需獨立決策的項目。
BO Frontend Login/Menu/Account 接線手冊(歷史 Stage 1)
V2 precedence — 2026-09-24: 本文件保留 Stage 1 接線追溯,並非現行 menu authority。SUPPLY_GATEWAY_ALLOCATION 不再是獨立 menu/grant/Prefix route,PAYMENT_ACCOUNT_APPLICATION 也不再是獨立 menu。請以 V2 菜單 API 參考、V2 catalog 與 Authorization contract 為唯一 current contract;其中任何舊 allocation route、role constant 或 menu 表格都不得實作或 enable。
Why It Matters
這份文件回答四個前端問題:
- Login Type選定後要顯示及提交哪些欄位。
- Login成功後如何判斷正式登入或首次改密碼。
- 哪些菜單能顯示,以及Frontend可不可以自行判斷權限。
- Supplier/Gateway Account List及現行Agent-bound Supply Gateway read要接哪些API。
Stage 1的Login、首次改密碼、Menu、Supplier Account List/Create與Gateway Account List/Create已有backend candidate;仍不是已部署API。啟動candidate前須由operator手動套用sql/V2/v2.sql。完整23-code menu、API maturity與release readiness請以BO Menu API 前端參考為準;不得以舊/supplyGatewayAllocation/**作新SPay4接線基準。
Focused contract readiness與implementation gates見BO V2 Contract Spec。
第一個可交付Frontend的切片見BO Frontend Stage 1 Integration Spec。Stage 1只包含Login、首次改密碼、Menu bootstrap、Account List及Create;其他actions明確Deferred。
Stage 2 已選範圍
2026-08-24確認下一階段為Account Lifecycle & Recovery:Supplier/Gateway Account Enable/Disable、Password Reset與GA Reset。三類mutation必須共同處理owner scope、optimistic concurrency、audit、formal-session revoke及pending password-change token revoke。
G1 Account status、G2-P Password Reset、G2-GA Reset與Stage 2 action authority mapping均已凍結:Supplier Account三項action只允許Superadmin;Gateway Account三項action允許Superadmin或owning Supplier。List使用VIEW、Create使用INSERT,三個lifecycle actions共同使用Account List的UPDATE,不新增menu或per-action permission。這表示contract可進入OpenAPI review,但不構成coding authority。Gateway Allocation、Account Settings與完整Supplier/Gateway fixed menu inventory繼續Deferred。
先看這張判斷表
| 時機 | Frontend判斷依據 | 要做的事 | 不可做的事 |
|---|
| Login前 | 使用者選取的loginType | 顯示對應欄位並組request | 用dropdown推導登入後permission或owner scope |
| Login response | data.token或data.nextStep | 進系統或進首次改密碼 | 把passwordChangeToken當正式JWT |
| 正式登入後 | GET /bo/v2/menu回傳method code | 建立側邊導航 | 依principalType硬編整份Supplier/Gateway menu |
| Gateway Account List | menu是否含GATEWAY_ACCOUNT_LIST | 顯示menu與頁面 | 只靠Frontend filter限制Supplier scope |
| Account lifecycle按鈕 | Account List menu+authenticated principalType | Superadmin顯示Supplier/Gateway actions;Supplier只顯示Gateway actions | 假設按鈕可見就代表Backend一定允許 |
| Supply Gateway Allocation | menu回傳SUPPLY_GATEWAY_ALLOCATION且為active Agent-bound Prefix principal | 只接GET /bo/v2/agent/supplyGateways read contract | 傳送Agent/Supplier/Gateway selector,或使用舊modal mutation API |
| API mutation | Backend success/error/conflict | 只提交或回復該次operation | 以Frontend權限判斷取代Backend authorization |
所有非voidresponse仍遵循專案ResponseVo<T> envelope:
{
"code": 2000,
"message": "",
"data": {}
}
下列response範例若未特別說明,均只展示data內容。
Login Type 與欄位
| Login Type | 顯示欄位 | 不顯示欄位 |
|---|
PREFIX | Prefix Code、Account、Password、Google Authenticator | Supplier Login ID |
SUPPLIER | Supplier Login ID、Account、Password、Google Authenticator | Prefix Code |
GATEWAY | Account、Password、Google Authenticator | Prefix Code、Supplier Login ID |
三種Login Type都呼叫:
POST /public/v2/login
Prefix/Superadmin request
{
"loginType": "PREFIX",
"prefixCode": "PREFIX01",
"account": "operator",
"password": "使用者輸入值",
"gaCode": "012345"
}
Superadmin仍選PREFIX,固定提交:
{
"loginType": "PREFIX",
"prefixCode": "SUPERADMIN",
"account": "superadmin-account",
"password": "使用者輸入值",
"gaCode": "012345"
}
Supplier request
{
"loginType": "SUPPLIER",
"supplierLoginId": "supplier-login-id",
"account": "operator",
"password": "使用者輸入值",
"gaCode": "012345"
}
Gateway request
{
"loginType": "GATEWAY",
"account": "gateway-account",
"password": "使用者輸入值",
"gaCode": "012345"
}
Frontend可以trim prefixCode、supplierLoginId及account後送出;Backend仍會重新normalize。Frontend不得trim或轉換password、gaCode。gaCode使用string保存前導零。
Login response 怎麼分流
正式登入成功
{
"token": "formal-jwt",
"principalType": "SUPERADMIN",
"account": "superadmin-account"
}
Frontend處理順序:
- 保存formal token。
- 保存
principalType作畫面識別,不從JWT解析permission。 - 呼叫
GET /bo/v2/menu。 - 依menu method code建立導航。
Supplier/Gateway 首次改密碼
{
"nextStep": "PASSWORD_CHANGE_REQUIRED",
"passwordChangeToken": "single-use-token",
"expiresAt": "2026-08-22T05:00:00Z"
}
此branch沒有formal token。Frontend切換至設定新密碼畫面,呼叫:
POST /public/v2/password/change
{
"passwordChangeToken": "single-use-token",
"newPassword": "使用者輸入的新密碼"
}
成功response與正式登入成功相同,之後才保存formal token並查menu。Token過期、重放或被reset撤銷時,清除本地token並回登入頁。
新密碼規則:8–20字元,至少一個英文字母及一個數字,只允許英數字與!@#$%&*_?-,不得包含完整login account且不得與temporary password相同。Frontend可做相同pre-check,但Backend結果才是authoritative。
所有authentication failure顯示同一個登入失敗訊息;不要根據HTTP body猜測帳號、密碼、GA、owner或status哪一項錯誤。
Menu 怎麼接
GET /bo/v2/menu
Authorization: Bearer {formal-jwt}
data沿用現有group/method shape:
[
{
"id": 1,
"name": "Admin Management",
"sort": 1,
"methods": [
{
"id": 101,
"code": "GATEWAY_ACCOUNT_LIST",
"name": "Gateway Account List",
"sort": 3
}
]
}
]
Frontend route registry以method.code為stable key;id與name不可作permission identity。Backend沒回傳的code就不顯示入口;直接輸入URL仍可能發生,頁面API必須正確處理403。
已沿用的 frozen method identity:
| Code | Superadmin | Prefix | Supplier | Gateway |
|---|
SUPPLIER_ACCOUNT_LIST | 顯示 | 隱藏 | 隱藏 | 隱藏 |
GATEWAY_ACCOUNT_LIST | 顯示 | 隱藏 | 顯示 | 隱藏 |
SUPPLY_GATEWAY | 顯示 | 隱藏 | 顯示 | 顯示 |
Gateway帳號沒有GATEWAY_ADMIN或GAA role;Gateway Account List移除ROLE欄位。完整 23-code inventory、英文 group/sort、principal visibility、scope、API family、negative case 與 B/F/R gate 已由 Backoffice Menu Catalog Manifest 凍結。catalog不等於runtime發布:只有Backend API/policy、Frontend route及current release enablement三者皆ready的code才由Menu API回傳。Future code只存在contract,不回傳disabled placeholder;Frontend仍只渲染實際response。
Spay4 Payment Management只以SUPPLY_GATEWAY顯示Supply Gateway Setting。Legacy PAYMENT_CHANNEL不屬 SPay4 catalog、seed 或 runtime menu,也不得透過 Spay4 route/API client fallback 或重建;若未來需要該 Legacy surface,必須先完成獨立 catalog/API/scope/negative-case contract。SUPPLY_GATEWAY的可見actor共用同一page component,但畫面action依Backend授權結果呈現:Superadmin可管理全部及create/soft delete;Supplier限旗下Gateway並可修改name、global status與direction limits;Gateway限自身且只可修改name,status與limits皆read-only。Prefix不顯示此menu,直接輸入route時Backend仍須執行相同scope與action檢查。
Supply Gateway Setting不可只寫「依權限顯示」,Frontend依下列actor × action matrix接線;OPEN項目不得先顯示操作:
| Action | Superadmin | Supplier | Gateway | Prefix |
|---|
| View page/detail | 全部non-deleted Gateways | 只限owned Gateways | 只限self Gateway | 不可存取 |
| Create Gateway | 可 | 不可 | 不可 | 不可 |
| Edit Gateway Name | 可,全部non-deleted Gateways | 可,只限owned Gateways | 可,只限self Gateway | 不可 |
| Edit Operational Config | 可,全部Gateways | 可,只限owned Gateways | 不可;status/limits read-only | 不可 |
| Soft Delete Gateway | 可,全部Gateways;仍受version與live blockers限制 | 不可 | 不可 | 不可 |
View Used By (Prefix) | 可,enabled mappings only | 可,owned Gateway enabled mappings only | 可,self Gateway enabled mappings only | 不適用 |
| Read Agent-bound Supply Gateway Allocation | 不可;此read contract只接受active Agent-bound Prefix principal | 可,僅own Agent effective binding | 不可 | 不可 |
這張matrix只定義Frontend affordance與已確認scope;Backend仍是每個action的最終authorization boundary。Delete不預先顯示eligibility flag,實際command可能回SUPPLY_GATEWAY_DELETE_BLOCKED(40050)。
三種可編輯actor共用同一個Gateway Name client method;Frontend不得另建Gateway self alias或呼叫generic Gateway update:
PUT /bo/v2/supplyGateways/{gatewayId}/name
{
"gatewayName": "Gateway A",
"expectedVersion": 12
}
Superadmin可帶任一non-deleted Gateway ID,Supplier只能帶owned Gateway ID,Gateway actor只能帶self Gateway ID;Gateway actor仍須在path明確送出自己的gatewayId,Backend不得忽略或改寫。Stale version回HTTP 409與RESOURCE_VERSION_CONFLICT(40051),Frontend重新讀取authoritative data且不得自動retry。成功data固定為最小name result:
{
"gatewayId": 101,
"gatewayName": "Gateway A",
"version": 13
}
三個欄位皆required。Frontend先以它更新open detail中的gatewayName與root version,再依既定規則reload current page;不得期待Supplier、code、status、limits、Currency/Business Zone、usedByAgents或action flags,也不得把此result自行拼成SupplyGatewayPageRowVo或視為完整detail。
Gateway Name輸入先移除前後空白,再以canonical value驗證required與1..100。大小寫、Unicode及內部空白照原樣保留,不合併連續空白;例如" Bangkok Core "送出/保存為"Bangkok Core"。Frontend必須用canonical value顯示字數與validation,trim後為空或超過100時不送出;Backend仍會再次正規化並以FIELD_VALIDATE_FAILED(40005)拒絕invalid request。成功後一律採response的canonical gatewayName,不得用raw input覆寫。
Gateway Name允許在同一Supplier及跨Supplier重複。Frontend不得以Name作React key、action target或deduplication key;一律使用gatewayId,API/audit識別另可顯示immutable gatewayCode。SupplyGatewayPageRowVo required identity固定包含gatewayId、gatewayCode、gatewayName;list/detail/selector只要可能同時出現多個Gateway,就必須讓使用者同時看見Gateway Code,不得只顯示Name後自行合併同名records。Name filter需呈現Backend回傳的所有matches。
使用current expectedVersion送出與current persisted name相同的canonical value時,Backend回成功current { gatewayId, gatewayName, version },version不變且不產生audit/modify metadata。Frontend不需要判斷stateChanged,仍以response更新open detail並reload current page。Stale version即使canonical name相同仍回HTTP 409,Frontend重新GET且不得自動retry。
Gateway page row直接回required usedByAgents,Frontend用它呈現USED BY (PREFIX):
"usedByAgents": [
{
"agentId": 1001,
"agentPrefix": "OLEV"
}
]
Array只含enabled mappings,固定依agentPrefix ASC, agentId ASC排序;空集合[]顯示Not in use。agentPrefix只作顯示,Frontend不得以它定位Agent、逐列呼叫mapping API或另查Agent master,也不得把此field送回任何Gateway mutation。
三種可見actor共用GET /bo/v2/supplyGateways?pageNumber=0&pageSize=20。Frontend不傳scope selector;Backend依principal讓Superadmin看全部非deleted Gateways、Supplier只看旗下Gateway、Gateway只看自己,Prefix拒絕。Response固定為zero-based PageVo<SupplyGatewayPageRowVo>,pageSize使用BO default且限制1..500。
Page filter固定為optional status+gatewayName。status未傳表示All,只接受ACTIVE/INACTIVE並精確比對Gateway operational status;gatewayName使用contains搜尋。Frontend不提供generic keyword、Gateway Code搜尋、Supplier、Currency或Used By Prefix進階filter,也不得在已載入records上自行模擬server-side filter。
Page固定依gatewayName ASC, gatewayId ASC顯示,gatewayId只作同名Gateway的穩定tie-breaker。Frontend不傳sort參數、不自行重排records;Gateway Name修改成功後reload current page,避免local row仍留在舊的跨頁位置。此順序不代表routing或allocation priority。
SupplyGatewayPageRowVo固定為下列exact summary row:
// Canonical fixed UTC offset,例如 +07:00;不接受 IANA region ID。
type BusinessUtcOffset = string;
interface SupplyGatewayPageRowVo {
gatewayId: number;
gatewayCode: string;
gatewayName: string;
supplierId: number;
supplierCode: string;
supplierName: string;
currencyCode: string;
currencyName: string;
businessZone: BusinessUtcOffset;
supplyGatewayStatus: 'ACTIVE' | 'INACTIVE';
version: number;
usedByAgents: Array<{ agentId: number; agentPrefix: string }>;
}
所有欄位required;usedByAgents無mapping時回[]並依agentPrefix/agentId排序。Row可直接呈現Gateway、Supplier、Currency、Business Zone、Status與Used By,不需要另查master API。Row不包含Deposit/Withdrawal四個per-order limits、BaseEntity status/timestamps、direction row ID/version、daily limits或action flags。
開啟Edit/Detail固定呼叫:
GET /bo/v2/supplyGateways/{gatewayId}
三種可見actor共用同一client method;Superadmin可讀全部non-deleted、Supplier只讀owned、Gateway只讀self,Prefix拒絕。Frontend從row帶入gatewayId,不傳scope selector,也不建立/self或/detail client alias。Row的version只是summary snapshot,不能用來跳過detail GET。成功data為下列flat完整projection:
// Plain decimal string;不得轉成JavaScript number後再送出。
type GatewayAmount = string;
interface SupplyGatewayVo {
gatewayId: number;
gatewayCode: string;
gatewayName: string;
supplierId: number;
supplierCode: string;
supplierName: string;
currencyCode: string;
currencyName: string;
businessZone: BusinessUtcOffset;
supplyGatewayStatus: 'ACTIVE' | 'INACTIVE';
depositMinAmount: GatewayAmount;
depositMaxAmount: GatewayAmount;
withdrawalMinAmount: GatewayAmount;
withdrawalMaxAmount: GatewayAmount;
version: number;
usedByAgents: Array<{ agentId: number; agentPrefix: string }>;
}
所有欄位required;businessZone固定為canonical ±HH:MM(例如+07:00),不得回Asia/Bangkok等IANA region ID;usedByAgents無mapping時回[]並依agentPrefix/agentId排序。Detail GET與Operational Config success共用此VO;不回BaseEntity status、timestamps、direction row ID/version、direction enable、daily limits或action flags。Gateway actor可看status/limits,但controls仍read-only。
GatewayAmount request只接受^(?:0|[1-9]\d{0,19})(?:\.\d{1,10})?$,禁止JSON number、sign、scientific notation、comma、空白及超過20位整數/10位小數。Response是canonical plain string:移除不必要zeros,zero固定"0"。Frontend input state維持string,使用任意精度decimal library或等價精確比較;不得經Number/parseFloatround-trip。
Supply Gateway API 使用時機
| UI時機 | API | Success data | Frontend處理 |
|---|
| 進入頁面、filter、pagination或mutation後reload | GET /bo/v2/supplyGateways | PageVo<SupplyGatewayPageRowVo> | 整體替換records與page metadata |
| 開啟View/Edit或直接進Detail route | GET /bo/v2/supplyGateways/{gatewayId} | flat完整SupplyGatewayVo | 初始化authoritative detail與current version |
| Save Gateway Name | PUT /bo/v2/supplyGateways/{gatewayId}/name | { gatewayId, gatewayName, version } | 更新detail name/version後reload page |
| Save Operational Config | PUT /bo/v2/supplyGateways/{gatewayId}/operationalConfig | flat完整SupplyGatewayVo | 更新完整detail後reload page |
Page row不回allowedActions或canEditName/canEditOperationalConfig/canDelete等booleans。Frontend依authenticated actor、fixed profile與Menu API resource/action permission顯示基本操作,但不得把button visibility當成授權;Backend command仍驗證actor、scope、version與live blockers。Delete是否可執行不在page row預測,實際失敗依SUPPLY_GATEWAY_DELETE_BLOCKED(40050)呈現。
Gateway擁有required immutable Currency,Supplier擁有可由Superadmin更新的canonical businessUtcOffset。Gateway list/detail顯示Gateway currency及Supplier解析出的current offset;Frontend原樣顯示±HH:MM,不可轉成IANA region或提供Gateway offset override。Gateway Create必須明確提供currencyId,建立後不可改Currency;Supplier offset mutation依#142 note47090只屬System Admin。Per-order Deposit/Withdrawal limits仍屬Gateway direction config。
Supplier Setting 與 Supplier Create
Spay4 Supplier Create固定呼叫:
POST /bo/v2/suppliers
Supplier management沿用既有Payment Management group中的SUPPLIER_SETTING/Supplier Setting及目前route registry,不新增SUPPLY_SUPPLIER_SETTING或第二個navigation item。Menu API回傳SUPPLIER_SETTING時才註冊route;進頁與所有操作仍需要對應 policy action。沿用menu只代表同一Supplier master與navigation,不可取代頁內API authorization。
Create只提供給formal V2 principalType=SUPERADMIN。SUPPLIER_SETTING 已完成 B/F/R,Frontend可依 menu response 與 INSERT affordance 顯示 Create button;Prefix、Supplier與Gateway不顯示disabled placeholder。即使button被竄改或menu錯誤曝光,Backend仍拒絕非Superadmin direct call。
Request使用專用SupplierCreateRequestVo,不得重用Legacy SupplierVo,也不得fallback到舊POST /supplier。依#142 note47089/47090,Supplier Create最小request有三個required fields:
{
"name": "Kola Pay",
"loginId": "KOLA",
"businessUtcOffset": "+07:00"
}
Supplier Create不再接受Supplier-wide currencyId或預設THB;Currency改由每個Gateway Create required提交。businessUtcOffset required,必須明確提交canonical ±HH:MM且不得預填或fallback;Supplier可由System Admin修改offset,commit後影響新transaction而不重算歷史。Supplier表單使用Name、Login ID與Business UTC Offset。不得提交code、account、status或Legacy url、secretKey、level、channels;Backend仍衍生identity/initial Account、固定ACTIVE並原子建立第一個Supplier Account。
Supplier Name在blur或submit時移除前後空白,trim後required且長度1..50。保留大小寫、Unicode與內部空白,例如Kola Pay與中文品牌名稱都可使用;Name允許重複,不做duplicate warning或自動改名。Backend會重做canonicalization,success後Frontend必須用data.supplier.name覆蓋raw input。List/detail若有同名Supplier,全部保留並以Supplier ID/Code/Login ID區分,不以Name作row key。
| UI時機 | API | 可操作內容 | Frontend處理 |
|---|
| Menu bootstrap | GET /bo/v2/menu | 無表單操作 | 只有response含SUPPLIER_SETTING+VIEW時註冊既有Supplier Setting route |
| 進入Supplier Setting/重新整理List | GET /bo/v2/suppliers | 分頁查看Supplier | code/loginId exact、supplierName contains、supplierStatus filter;固定id ASC,只使用V2 secret-safe response,不得fallback到Legacy GET /supplier/page。 |
| 開啟Supplier Create form | 無Supplier Currency選擇 | 輸入Name、Login ID、Business UTC Offset | Offset保持未設定且不預填;幣別由Gateway Create指定 |
| 開啟 Supplier detail | GET /bo/v2/suppliers/{supplierId} | 查看不可變 identity、可編輯 basic data、status 與 version | deleted/不存在視為 not-found,回 list 並提示資料已不存在。 |
| 儲存 basic data | PUT /bo/v2/suppliers/{supplierId} | 提交supplierName、businessUtcOffset、expectedVersion | 同值不更新version;409 reload 後由使用者重確認,禁止自動retry。 |
| 儲存 operational status | PUT /bo/v2/suppliers/{supplierId}/status | 提交ACTIVE或INACTIVE與expectedVersion | INACTIVE 不阻擋帳號登入,但不可參與新的Deposit/Withdrawal分配;重啟後依其他條件恢復。 |
| 刪除 Supplier | DELETE /bo/v2/suppliers/{supplierId} | 提交expectedVersion | 未刪除Gateway時顯示conflict;成功後不可restore、保留帳號/audit但撤銷Supplier Account session。 |
| 點擊Create | POST /bo/v2/suppliers | 提交name、loginId、businessUtcOffset | Submit期間防重複送出;validation failure留在form |
| Create success | 同一POST response | 無第二段建立操作 | 直接讀data.supplier/account並顯示一次性credentials;不再呼叫detail或Supplier Account Create |
| Create failure | 同一POST error | 修正原表單後由使用者再次送出 | 不fallback到Legacy POST /supplier,不假設root已建立 |
Supplier Login ID與Account是兩個不同欄位:
| Frontend欄位 | Backend source | 用途 |
|---|
| Supplier Login ID | Supplier.loginId/suppliers.login_id | Required、globally unique、immutable;先定位Supplier owner,例如KOLA |
| Account | SupplyActorAccount.account | 定位該Supplier底下的實際操作帳號,例如kola_supplier_admin |
Supplier login固定同時提交兩個不同值。administrator不是Supplier Login ID;Frontend也不得把Account label改成Supplier Login ID、只保留其中一欄,或把KOLA同時送成Supplier Login ID與Account。KOLA/kola_supplier_admin是已確認的identity shape。
Supplier Login ID輸入先移除前後空白並轉為大寫,合法格式固定^[A-Z0-9][A-Z0-9_-]{0,49}$。Frontend應在輸入/blur時顯示canonical uppercase value、說明允許字元並於invalid時阻擋submit;Backend仍會重做canonicalization與validation。Success後必須用response的canonical loginId覆蓋form/detail,不保留raw input。
Supplier Create form不提供另一個Supplier Code欄位,request也不傳code。Backend將同一canonical Login ID同時保存為Supplier.code與Supplier.loginId;若success projection同時回兩欄,Frontend可assert兩者相同,但登入永遠使用loginId,其他Legacy display可繼續使用code。兩欄都不可編輯。
Supplier Create form不顯示或提交Status。Backend固定建立ACTIVE Supplier、ACTIVE initial Account與ACTIVE Authenticator credential;success account row應為loginEnabled=true、blockedBy=null、mustChangePassword=true、gaReady=true。Frontend收到success即可顯示credential modal並引導首次登入,不需另呼叫activation API;這不代表Frontend可略過首次改密碼。
Supplier Create不顯示可編輯Account欄位,也不在request傳account/initialAccount。Backend固定以validated canonical Supplier Login ID的Locale.ROOT lowercase加_supplier_admin產生第一個Account,例如raw kola 先成為KOLA,再產生kola_supplier_admin。Frontend可在Supplier Login ID欄位下顯示Initial Account: kola_supplier_admin的read-only preview;preview只協助確認,不是request authority,完成後須以success response的account值呈現credential modal。不得在Frontend截短、加數字、改suffix或允許override。
現有Currency options API仍可用:
GET /currency/list
此API要求authenticated BO session,回傳所有ACTIVE Currency,並由標準ResponseVo包裝:
{
"code": 2000,
"message": "",
"data": [
{
"id": 1,
"code": "THB",
"name": "Thai Baht"
}
]
}
Supplier Create不使用Currency options或THB default;#142 note47089已把Currency選擇放在Gateway Create。上述Currency response只作既有API shape示例,不表示目前環境只支援THB,也不授權沿用ID 1。Gateway Currency options與submit-time validation須由#149依Gateway單一Currency contract凍結,不能依options筆數自動改變產品政策。
Supplier Create必須顯示required Business UTC Offset並提交businessUtcOffset;格式為canonical ±HH:MM,例如+07:00/+08:00,不接受IANA region或其他別名。Frontend初始保持未設定,missing/blank/invalid值fail closed且無fallback。Supplier offset後續僅由System Admin以PUT /bo/v2/suppliers/{supplierId}及expectedVersion修改,commit後影響新transaction且歷史不重算;409 必須reload後由使用者重確認,禁止自動retry。
此前Supplier onboarding使用currency_id=1(THB)default的設計已被Gateway fixed single Currency取代:Supplier Create不決定Currency,Gateway Create required提交currencyId且建立後immutable。Supplier Create required提交canonical businessUtcOffset;不保存國家或IANA zone。Historical migration維持最後階段,backfill值必須依source data preflight與read-back,不在Frontend或DB DEFAULT硬編。
Supplier Create不是「先建Supplier,再由Frontend補建帳號」的兩段流程。單一request成功時,Backend已在同一transaction建立Supplier root、恰好一個初始Supplier Account、Authenticator credential及Supplier owner-level Deposit/Withdrawal current PT rules(兩方向rate皆為0);任一步失敗時全部未建立。PT初始化不需要Frontend額外呼叫API,也不加入一次性credential response。Frontend不得在Supplier Create success後自動呼叫POST /bo/v2/supplierAccounts補第一個帳號,也不需設計root/account/PT部分成功的補償UI。
此流程只在atomic onboarding上類似Prefix Create。Supplier不建立Legacy BO User/Role,也不使用由Supplier Code可預測的固定密碼;Backend沿用Supplier Account的V2 fixed profile與credential policy。初始帳號建立後為ACTIVE、mustChangePassword=true、gaReady=true。
Supplier Create success固定由ResponseVo<SupplierCreateResponseVo>包裝,data可直接依下列exact shape解析:
{
"supplier": {
"supplierId": 10,
"name": "Kola Pay",
"code": "KOLA",
"loginId": "KOLA",
"businessUtcOffset": "+07:00",
"status": "ACTIVE"
},
"account": {
"accountId": 20,
"account": "kola_supplier_admin",
"accountStatus": "ACTIVE",
"loginEnabled": true,
"blockedBy": null,
"gaReady": true,
"mustChangePassword": true,
"lastLoginTime": null,
"version": 0,
"supplierId": 10,
"supplierLoginId": "KOLA",
"supplierName": "Kola Pay"
},
"temporaryPassword": "<one-time-generated-password>",
"totpSecret": "<one-time-generated-base32-secret>"
}
supplier是專用完整SupplierCreatedVo,account沿用完整SupplierAccountVo。Frontend以response呈現canonical identity、effective THB/+07:00及ACTIVE狀態,不再呼叫Supplier detail。Response不會回Legacy url、secretKey、level、channels、audit metadata、password hash或encrypted secret。
兩個secret都只揭露一次;Frontend只在成功後開啟一次性credential modal,不得寫入console、analytics、URL、localStorage、IndexedDB或可重播state。Modal關閉或資料遺失後不能重新GET,只能使用既有Password Reset/GA Reset流程旋轉credential。
既有POST /bo/v2/supplierAccounts保留給已存在Supplier建立第二個及後續額外帳號;它不是Supplier Create的fallback。只有第一個Account套用lowercase Supplier Login ID+_supplier_admin;後續Account仍由Superadmin明確輸入,不自動產生default name,也不得建立、複製或重設Supplier owner的PT設定。Gateway Account Create同樣只新增登入principal,不得改動Gateway owner PT。
PT Manage呈現與操作邊界
Supplier/Gateway的PT設定屬於owner,Frontend不得把PT綁到Account List row或在Account Create/Reset後重新初始化。Owner onboarding完成時,Deposit/Withdrawal current rate已由Backend初始化為0;Frontend第一次進PT Manage只需query current狀態,不需執行Create。
初始化後Deposit與Withdrawal共用一次owner-level立即Save資格,Frontend使用一個PT form/Save,不提供兩個方向各自的立即Save。第一次任一方向有value change並成功後,畫面顯示新的current值並將共享資格標示為已使用;即使只改Deposit,之後修改Withdrawal也要進next-month pending。第二次及後續Save不會立即覆蓋current,而是顯示next-month pending值與生效月份;current與pending必須同時可辨識。
兩方向desired values都未變時Frontend應停用Save;即使仍送出,Backend也成功回目前projection且不消耗資格、不改version/audit/pending。Backend先檢查expectedVersion,因此stale同值Save仍進conflict reload流程。
每次Save都必須提交depositRate、withdrawalRate及expectedVersion完整desired state;不使用partial patch,也不能省略未修改方向或傳null表示unchanged。Frontend必須先以authoritative query projection初始化兩個欄位;只改Deposit時仍回傳目前可編輯target的Withdrawal值,反之亦然。Exact query/mutation paths、permission與rate wire contract仍待凍結,Frontend目前不得先接猜測API。
Supplier/Gateway/Cardholder的rate input一律以0..100百分點顯示,最多4位小數,欄位旁顯示%;例如輸入12.5就是12.5%,不得在送出前除以100。Negative、超過100或超過4位小數時Frontend可先提示,但Backend仍會field-specific拒絕。Cardholder base fee/threshold不是百分比,不套此range。
Rate request/response全程使用plain decimal string,例如"12.5"與"0";不得送JSON number或scientific notation,也不得轉成JavaScript number後再序列化。Response會canonicalize trailing zeros,因此form成功後以response值覆蓋raw input;no-op比較應使用decimal-string/arbitrary-precision library的numeric semantics。
Cardholder PT form固定有depositBaseFee、depositThreshold、withdrawalBaseFee及withdrawalThreshold四個required money inputs;皆使用非負plain decimal string、最多10位小數,允許"0"且不顯示任意maximum。Frontend不得提供「Withdrawal沿用Deposit」checkbox或隱藏任一方向欄位,亦不得因目前是THB就限制成2位小數。
Cardholder PT表單每個方向都必須各自提供rate、baseFee及threshold;不得只顯示一組Base Fee/Threshold後讓Withdrawal沿用Deposit值。Cardholder Create成功時Backend已在同一transaction建立Deposit與Withdrawal兩份current rules,每份三個值皆為"0";Frontend不另外呼叫PT Create,也不得在Account/credential/session建立後重設defaults。
Cardholder Deposit與Withdrawal分別按Commission Accounting Month累計eligible completed amount。沒有eligible Order或累計低於自身threshold時commission為0;至少有一筆eligible Order且累計等於或高於threshold時,baseFee + (monthlyEligibleAmount - threshold) × rate / 100。等於threshold時顯示完整base fee;Frontend不得把公式改成逐筆Order計算,也不得合併兩方向volume。Commission Report query先加總target interval內目前已接受的authoritative Hourly Financial Aggregate eligible amount,再對每個owner+currency+direction target套公式與round一次;不得把hourly Commission相加。資料缺漏、尚未抵達或coverage未知只是不計入,Backend不因缺漏阻擋Report,也不回傳partial/complete狀態或合成zero fact;later fact/correction完成投影後由後續query反映,Commission不需要activation boundary。已投影eligible fact的amount correction保留原fact,追加source-linked immutable delta並修正原canonical UTC hour/Commission month;Frontend不得把correction arrival time當成新period,也不顯示可直接覆寫原fact的操作。Per-source/partition cursor只供projector progress/resume/operations,不是Report contract。Controlled rebuild只在替換已上線bounded scope時先隔離產生scope並於validation後atomic發布;失敗時Frontend仍看到完整舊live scope。Normal historical projection則逐步供Report使用目前已接受facts,不等待all-source full-scope coverage。Commission result不由Scheduler materialize,也不保存Current Result;每次Report query都顯示當下latest derived final。Business response不提供freshness timestamp、dataAsOf、completeness/partial flag或projector progress,Frontend也不得自行衍生或顯示這些狀態。各target可使用其實際DB statement執行時的latest committed aggregate,同一request不保證單一point-in-time snapshot;Frontend不得因row amounts來自不同committed時點就標示partial/stale,Backend也不需刻意拆成N+1 query。後台不提供已付、未付、outstanding、recovery、waive/write-off或disposition欄位與操作,Frontend不得從latest amount推導payment status。Exact計算維持DECIMAL(38,16) representability,final維持DECIMAL(38,10) bound並使用Currency.decimalNumber/HALF_UP;decimalNumber只接受0..10且首次被Spay4引用後immutable。只有invalid Currency metadata、numeric overflow或missing applicable PT rule等真正計算錯誤時Backend才fail closed,Frontend不得clamp、fallback,或把failed result顯示成commission 0/previous value。任何requested target有這類錯誤時,整份Report不顯示任何Commission amounts、partial rows、summaries或totals;Currency cutover UI仍待後續contract。
Cardholder建立後,Deposit/Withdrawal六個PT fields共用一次Cardholder-level立即修改資格;任一欄第一次實際變更成功便消耗,之後兩方向任何變更都顯示為next-month pending。Frontend不得替兩個方向各顯示一個立即資格,也不得從目前六欄是否全0推測資格尚未使用。Exact Save payload、no-op/conflict與pending projection仍待後續contract,現階段不得先接猜測API。
Cardholder PT Save固定一次提交depositRate、depositBaseFee、depositThreshold、withdrawalRate、withdrawalBaseFee、withdrawalThreshold及root expectedVersion。七個fields皆required;Frontend必須先從authoritative editable target初始化完整表單,只改一欄時仍送出另外五個current/pending target values。不得建立Deposit/Withdrawal分開Save按鈕、傳null表示unchanged或只送dirty fields。Backend會在同一transaction把完整desired state寫入current或next-month pending;exact endpoint、response、no-op/conflict仍待後續contract。
Backend先驗證root expectedVersion再比較六欄。Version正確且六欄numeric同值時成功no-op,不消耗立即資格、不改version/audit/pending;Frontend可停用Save但不能依賴UI guard。Stale version即使六欄碰巧與最新值相同仍回conflict,Frontend必須reload且不得自動retry。Equality不能用raw string,"12.5"與"12.5000"視為同值;exact conflict code與success response仍待凍結。
Cardholder PT不提供Cancel Pending action。已有pending時,Frontend以該pending六欄作authoritative editable target;operator再次Save完整六欄即可覆寫,只有等於pending才是no-op。送出與current相同但與pending不同的值仍代表覆寫,不是取消。月界即使pending與current同值也會升為新revision;沒有pending則current持續有效,Frontend不得期待Backend每月產生copy row。
Supplier/Gateway/Cardholder Commission Accounting Month依Agent與Gateway owner Supplier已匹配的configured businessZone換算,沒有全域+08:00fallback;該month只用於rule生效、next-month pending、各方向eligible monthly volume與calculation snapshot。Report固定UTC +00:00 hourly half-open window且不拆Prefix/Supplier/Superadmin三張physical tables;Report與Monthly Commission都讀取Backend的Hourly Financial Aggregate,但只有Monthly Commission會按month彙總eligible amount後套公式。Frontend不得依歷史三視角文件先建三套畫面、把local zone當Report bucket,或用UTC hour/current businessZone/viewer timezone自行重算Commission。
V2 Create可用且Frontend完成切換後,Legacy POST /supplier停止Create能力並標記deprecated。觀察期內mapping仍存在,但固定回HTTP 410 Gone與code=40052(LEGACY_API_RETIRED),不會建立Supplier。Frontend此後不得呼叫、重試、把Legacy payload轉換後送往V2,或把它當成V2 fallback;若舊版Frontend收到此錯誤,應停止流程並提示重新整理/升級到已接V2的版本,不依message或data內容判斷。Supplier page/update/status等其他Legacy APIs不因本決策停用。Operator確認觀察期沒有410/40052 hits後,Backend才在後續release移除mapping。
同一SUPPLIER_SETTING頁的List已固定切至GET /bo/v2/suppliers,新Frontend不得呼叫或fallback到Legacy GET /supplier/page;但V2 List的exact filters、sort及row fields仍待凍結,Frontend不得先猜schema或混用Create response當page row contract。Detail/Update/Status/Delete要沿用Legacy API或切換V2仍須逐項確認。
Supply Gateway的Status filter與edit control只允許ACTIVE/INACTIVE,並同時作用於Deposit/Withdrawal。不得把Legacy Gateway的FROZEN或Deposit/Withdrawal enable switch帶入此頁;direction config只編輯limits,soft-deleted row不出現在一般列表。
Supplier/Superadmin把Gateway改為INACTIVE前,Frontend必須確認提示:「將停止Deposit/Withdrawal新routing、登出此Gateway全部BO users,既有訂單改由Supplier/Superadmin處理」。成功後不得保留Gateway actor頁面session;allocation、limits與歷史列表不因停用而刪除。重新啟用後要求重新登入,不復用舊token。
Gateway create預設Deposit與Withdrawal的per-order min/max皆為0/0。Frontend顯示No limit,但送出decimal 0,不得轉成null;只有missing/null資料才顯示Range required。任一bound大於0時顯示實際值,兩者皆大於0時必須先驗證min不大於max。
Gateway Setting不顯示或提交daily amount/count limits;Gateway list/detail/export也不期待這些欄位。每日總額與筆數限制只由Supplier-level設定畫面與API提供。
Supplier/Superadmin在Edit Gateway按Save時,Frontend以單一request提交global status、Deposit Min/Max、Withdrawal Min/Max及detail response的root expectedVersion。Frontend不得把status與兩個direction拆成多個mutation,也不得把Gateway name或Agent–Supply Gateway allocation塞進此request;只有完整transaction成功後才用response的完整operational projection與新version覆蓋detail,失敗時不得假設任何欄位已生效。
Change reason維持optional,不顯示required marker;未填或只輸入空白都可Save。Frontend可省略欄位或送trim後文字,Backend將空白正規化為null;此欄位只補充audit note,不影響authority、validation、routing或session revoke。
使用current expectedVersion送出完全相同的status與四個limits時,Backend回成功current projection但version不變;Frontend照常以response覆蓋畫面,不顯示「已異動」audit語意。Stale version即使payload剛好等於current state仍是conflict,Frontend必須reload且不得自動retry;填寫reason但沒有改五個operational values也仍是no-op。
Operational Save固定呼叫:
PUT /bo/v2/supplyGateways/{gatewayId}/operationalConfig
{
"supplyGatewayStatus": "ACTIVE",
"depositMinAmount": "0",
"depositMaxAmount": "0",
"withdrawalMinAmount": "0",
"withdrawalMaxAmount": "0",
"changeReason": null,
"expectedVersion": 12
}
成功的data固定為完整SupplyGatewayVo,Frontend先更新open detail snapshot,再使用原本的pageNumber、pageSize、status及gatewayName重新GET current page並整體替換records/pagination metadata。Status change、limits-only與成功no-op都走相同reload,不local replace/remove/insert row或自行修補total。第一次reload後以lastPageNumber = max(ceil(total / pageSize) - 1, 0)判斷;total=0顯示page 0 empty state且不再GET,原page超過last page且total大於0時保留filters/pageSize再GET最後有效頁。Request不得混入name、Supplier、code、Currency/Timezone、direction enable、daily limits或allocation;Gateway Name Save固定使用PUT /bo/v2/supplyGateways/{gatewayId}/name的另一個API client method。
Account List 權限與畫面
| 頁面 | Superadmin | Supplier | Prefix | Gateway |
|---|
| Supplier Account List | 全部管理 | 不可 | 不可 | 不可 |
| Gateway Account List | 全部管理 | 僅旗下Gateway accounts | 不可 | 不可 |
| Account Settings | 自己 | 自己 | 既有行為 | 自己 |
同一Supplier或Gateway可以有多個登入帳號。List row至少需要:owner display、account、accountStatus、loginEnabled、blockedBy、gaReady、lastLoginTime及version。帳號不另設displayName,Frontend直接顯示immutable account。Gateway Account List 的blockedBy只會是ACCOUNT_INACTIVE或null,不得回GATEWAY_INACTIVE;其餘exact response VO仍需OpenAPI freeze。
TOTP欄只使用Frontend projection:
gaReady | 顯示 | 語意 |
|---|
true | BOUND | Backend已有可驗證GA secret;不代表能證明使用者完成手動設定 |
false | RESET REQUIRED | 目前沒有可供正式login驗證的GA secret |
Account List不得回internal credential status、failed attempts、lock threshold或lockedUntil;暫時鎖定也不改變gaReady。
共同row fields:
{
"accountId": 101,
"account": "gateway_operator",
"accountStatus": "ACTIVE",
"loginEnabled": true,
"blockedBy": null,
"gaReady": true,
"mustChangePassword": false,
"lastLoginTime": "2026-08-22T04:30:00Z",
"version": 3
}
Supplier Account row另回supplierId、supplierLoginId、supplierName。Gateway Account row另回Supplier三欄及gatewayId、gatewayCode;不回gatewayName,因現有SupplyGateway沒有此 authoritative field。lastLoginTime使用ISO-8601 UTC;尚未登入時為null。不回ROLE或owner mutation fields。
Gateway Account List支援安全 detail、New Account、Enable/Disable、Reset Password、Reset GA 與 soft delete。GET /bo/v2/gatewayAccounts/{accountId}只回既有 safe row projection;deleted、missing 與 cross-scope target 一律 not-found。Gateway account 可刪除至該 Gateway 沒有任何登入帳號,沒有「最後帳號」blocker;delete 會撤銷 session 並釋放 Supplier-scope canonical identity。
不提供displayName、rename、role選擇、owner轉移或Login ID修改。Supplier Account List另提供 soft delete;Superadmin 可跨 Supplier 操作,Supplier Admin 僅可操作 own Supplier 的 SUPPLIER_USER,Supplier User 不可管理帳號。
Action authority與按鈕顯示
Stage 2不新增action permission API,也不新增STATUS_UPDATE、PASSWORD_RESET或GA_RESET menu code。Frontend採以下固定mapping:
| Account List resource | Page/read | Create | Enable/Disable、Reset Password、Reset GA | 顯示對象 |
|---|
SUPPLIER_ACCOUNT_LIST | VIEW | INSERT | status/credential recovery 使用UPDATE;soft delete 使用DELETE | Superadmin;Supplier Admin 僅 own SUPPLIER_USER |
GATEWAY_ACCOUNT_LIST | VIEW | INSERT | UPDATE/GATEWAY_ACCOUNT_LIST_UPDATE | Superadmin、Supplier |
Frontend顯示規則:
principalType=SUPERADMIN且menu含SUPPLIER_ACCOUNT_LIST:顯示Supplier Account的Create、status、delete及兩個credential recovery actions。principalType=SUPPLIER、supplyRole=SUPPLIER_ADMIN且menu含SUPPLIER_ACCOUNT_LIST:僅對 own Supplier 的 SUPPLIER_USER 顯示相同操作;SUPPLIER_ADMIN target 不顯示這些按鈕。principalType=SUPERADMIN或SUPPLIER,且menu含GATEWAY_ACCOUNT_LIST:顯示Gateway Account的Create及三個lifecycle actions。PREFIX與GATEWAY不顯示Account List或這些actions。- 三個lifecycle buttons是同一組visibility,不需要逐一等待permission payload。Backend仍會逐次驗證principal type與owner scope;403時Frontend顯示權限已變更並reload,不得改用另一個owner id重送。
Backend實作對照為:Supplier Account controller由 SUPPLIER_ACCOUNT_LIST 的 action 與 server-derived Supplier scope 保護;Supplier Admin 只能管理 SUPPLIER_USER。Gateway lifecycle controller接受ADMIN或GATEWAY_ACCOUNT_LIST_UPDATE。Gateway與Prefix不會取得 Supplier Account List authority。這些是Backend enforcement細節;Frontend不得自行產生或提交authority字串。
Lifecycle audit(Backend/QA handoff)
Stage 2採A2並沿用既有domain_audit_log,但新增nullable supplier_id、gateway_id與對應time index。每次成功的實際lifecycle mutation以account id/immutable account作target:Supplier Account事件填supplier_id;Gateway Account事件同時填事件發生時的supplier_id與gateway_id,並在metadata保存當下login id、code與name snapshot。
Audit使用四個可辨識action:Activate、Deactivate、Password Reset與GA Reset;before/after只保存accountStatus、mustChangePassword、gaReady、version及session epoch。effects另外標記password/GA rotation、replay/lock clear、session revoke與pending challenge clear。任何temporary password、hash、TOTP secret/code、encrypted secret或token都不得進入audit。
這項決策不改變Account lifecycle mutation request/response。既有Domain Audit page新增optional supplierId/gatewayId filters:只選Supplier時包含該Supplier Account與旗下全部Gateway Account audits;再選Gateway時縮小到該Gateway。兩者不相符時回empty page。
Domain Audit list/detail對Supply Actor event回nullable supplierId、supplierLoginId、supplierName、gatewayId、gatewayCode與gatewayName,Frontend直接顯示snapshot,不另查master API。這不代表Supplier/Gateway actor取得Audit menu或API;Stage 2維持既有Audit authority,Backend仍是最終visible-scope gate。
Lifecycle Domain Audit採success-only:只有成功且實際改變資料的status action、Password Reset或GA Reset建立row。Same-status no-op、409、403、missing/cross-scope target、validation或rollback failure都不建立Domain Audit;Frontend仍依原API error處理,Backend security/application log以request/trace id保留診斷關聯。Audit UI因此可把每筆Lifecycle Audit解讀為真正完成的mutation,不需要再以result排除未發生的操作。
Audit與account/credential mutation使用同一transaction。Audit insert失敗時,account version、credential、session epoch及pending challenge全部rollback;Password Reset/GA Reset產生但未commit的temporary password/TOTP secret不會出現在response。Frontend只有收到HTTP success與code=2000時才能顯示一次性credential;其他錯誤一律不得猜測reset已成功,應reload current page後等待使用者重新操作。
Audit risk固定為:Activate/Deactivate=MEDIUM,Password Reset/GA Reset=HIGH。Domain Audit畫面可直接依既有risk欄位顯示/篩選;Frontend不得把credential reset當成預設LOW,也不需要自行推導或覆寫Backend回傳值。
Create Additional Account contract
替既有Supplier建立第二個及後續額外Supplier Account:
{
"supplierId": 10,
"account": "supplier_operator"
}
建立Gateway Account:
{
"gatewayId": 20,
"account": "gateway_operator"
}
兩支Create API的data使用相同response envelope:
{
"account": {
"accountId": 101,
"account": "gateway_operator",
"accountStatus": "ACTIVE",
"loginEnabled": true,
"blockedBy": null,
"gaReady": true,
"mustChangePassword": true,
"lastLoginTime": null,
"version": 0
},
"temporaryPassword": "一次性顯示的臨時密碼",
"totpSecret": "一次性顯示的Base32 secret"
}
account實際使用對應Supplier/Gateway Account page row VO,因此也包含該類型已凍結的owner display fields。Create request不傳displayName、password、TOTP secret、status、role或principal type;Backend建立後預設ACTIVE、mustChangePassword=true及gaReady=true。Gateway Account 的loginEnabled只依自身accountStatus計算。
Response不回gaIssuer、gaAccountName或credentialsDisclosedAt:
gaIssuer與gaAccountName只影響Authenticator App顯示名稱,不參與六位TOTP驗證。- 使用者可直接在Authenticator App自行命名;Frontend不需要在Create Account表單增加這兩欄,也不需要傳回Backend。
- 若Frontend提供QR,可自行用固定產品名稱與
account組顯示label;驗證仍只依Backend產生的totpSecret及系統固定TOTP參數。 credentialsDisclosedAt只由Backend audit保存,不提供Frontend。
Account administration API inventory
| Method/path | 用途 | Frontend readiness |
|---|
GET /bo/v2/supplierAccounts | Supplier Account分頁 | Superadmin 可選 Supplier;Supplier Admin 固定 own Supplier scope |
GET /bo/v2/supplierAccounts/{accountId} | Supplier Account detail | 同 list scope;deleted 或 cross-scope target 視為 not-found |
POST /bo/v2/supplierAccounts | 建立額外 Supplier User | 一次性 credential;不作為 Supplier onboarding 後補第一個帳號 |
PUT /bo/v2/supplierAccounts/{accountId}/status | Enable/Disable | Superadmin;Supplier Admin 僅 own SUPPLIER_USER |
DELETE /bo/v2/supplierAccounts/{accountId} | Soft delete | Superadmin;Supplier Admin 僅 own SUPPLIER_USER;需 expectedVersion |
POST /bo/v2/supplierAccounts/{accountId}/password/reset | 同步產生一次性 temporary password 與 TOTP setup secret | Superadmin;Supplier Admin 僅 own SUPPLIER_USER |
GET /bo/v2/gatewayAccounts | Gateway Account分頁 | Path、filters與row VO已凍結 |
POST /bo/v2/gatewayAccounts | 建立Gateway Account | Request、response、scope與一次性secret行為已凍結 |
PUT /bo/v2/gatewayAccounts/{accountId}/status | Enable/Disable | G1 contract已凍結;Superadmin/owning Supplier |
POST /bo/v2/gatewayAccounts/{accountId}/password/reset | 產生一次性temporary password | G2-P contract已凍結;Superadmin/owning Supplier |
POST /bo/v2/gatewayAccounts/{accountId}/ga/reset | rotate並一次性回GA secret | G2-GA contract已凍結;Superadmin/owning Supplier |
所有page API使用pageNumber/pageSize,pageNumber為zero-based,pageSize必須為1..500。Response使用PageVo的records、total、size、number。
Account status mutation
{
"accountStatus": "INACTIVE",
"expectedVersion": 3
}
成功後Frontend直接以response內updated SupplierAccountVo/GatewayAccountVo替換該列。Same-version same-status是成功no-op且version不變;stale version包含stale same-status都回HTTP 409與RESOURCE_VERSION_CONFLICT(code 40051),Frontend必須重新載入該列或current page,不可用舊version重送覆蓋。
實際Disable立即使formal sessions及pending password-change token失效;Enable不重設password、GA或mustChangePassword。Gateway operational status 不會改變既有 Gateway Account 的loginEnabled或blockedBy。
Password Reset:expectedVersion來源與完整Frontend範例
expectedVersion不是Frontend自行產生;page row與新的 safe detail API 都包含version。Frontend在使用者開啟action時保存該投影 snapshot:
GET /bo/v2/gatewayAccounts?pageNumber=0&pageSize=20&account=gateway.ops
{
"code": 2000,
"message": "",
"data": {
"records": [
{
"accountId": 912,
"account": "gateway.ops",
"accountStatus": "ACTIVE",
"loginEnabled": true,
"blockedBy": null,
"gaReady": true,
"mustChangePassword": false,
"lastLoginTime": "2026-08-24T01:12:30Z",
"version": 3,
"supplierId": 17,
"supplierLoginId": "SUP-017",
"supplierName": "Example Supplier",
"gatewayId": 81,
"gatewayCode": "GW-081"
}
],
"total": 1,
"size": 20,
"number": 0
}
}
按下Reset Password時,將該列的version=3送成expectedVersion:
POST /bo/v2/gatewayAccounts/912/password/reset
Content-Type: application/json
Authorization: Bearer {formal-token}
{
"expectedVersion": 3
}
成功後account version增加;Frontend先以updated row覆蓋local row,再顯示只能查看一次的temporary password:
{
"code": 2000,
"message": "",
"data": {
"account": {
"accountId": 912,
"account": "gateway.ops",
"accountStatus": "ACTIVE",
"loginEnabled": true,
"blockedBy": null,
"gaReady": true,
"mustChangePassword": true,
"lastLoginTime": "2026-08-24T01:12:30Z",
"version": 4,
"supplierId": 17,
"supplierLoginId": "SUP-017",
"supplierName": "Example Supplier",
"gatewayId": 81,
"gatewayCode": "GW-081"
},
"temporaryPassword": "<one-time temporary password>"
}
}
Frontend handling示意:
const row = page.records.find((item) => item.accountId === 912);
if (!row) throw new Error("Account row is no longer available");
try {
const result = await resetGatewayPassword(row.accountId, {
expectedVersion: row.version,
});
replaceRow(result.account); // version 3 -> 4
showOneTimePasswordModal(result.temporaryPassword);
} catch (error) {
if (error.httpStatus === 409 && error.code === 40051) {
await reloadCurrentPage();
showConflictMessage();
return; // credential mutation不得自動retry
}
throw error;
}
競爭情境:管理者A與B都載入version 3;A先Reset成功並取得version 4,B之後送expectedVersion=3時不會再次旋轉password,而是收到:
HTTP/1.1 409 Conflict
{
"code": 40051,
"message": "resource version conflict (40051)",
"data": []
}
Frontend只依HTTP 409與code=40051分支,不依message文字或data內容。重新GET current page後必須讓使用者重新確認;不可自動重送reset。Supplier Account流程完全相同,改用Supplier path及SupplierAccountVo。
GA Reset:完整Frontend範例
GA Reset沿用相同List-row concurrency。假設Password Reset後local row已更新為version 4:
POST /bo/v2/gatewayAccounts/912/ga/reset
Content-Type: application/json
Authorization: Bearer {formal-token}
{
"expectedVersion": 4
}
成功response只揭露新的Base32 secret,不回temporary password或QR metadata:
{
"code": 2000,
"message": "",
"data": {
"account": {
"accountId": 912,
"account": "gateway.ops",
"accountStatus": "ACTIVE",
"loginEnabled": true,
"blockedBy": null,
"gaReady": true,
"mustChangePassword": true,
"lastLoginTime": "2026-08-24T01:12:30Z",
"version": 5,
"supplierId": 17,
"supplierLoginId": "SUP-017",
"supplierName": "Example Supplier",
"gatewayId": 81,
"gatewayCode": "GW-081"
},
"totpSecret": "<one-time Base32 TOTP secret>"
}
}
Frontend先以updated row覆蓋local row,再顯示一次性GA modal:
const row = page.records.find((item) => item.accountId === 912);
if (!row) throw new Error("Account row is no longer available");
try {
const result = await resetGatewayGa(row.accountId, {
expectedVersion: row.version,
});
replaceRow(result.account); // version 4 -> 5
showOneTimeGaSecretModal(result.totpSecret);
} catch (error) {
if (error.httpStatus === 409 && error.code === 40051) {
await reloadCurrentPage();
showConflictMessage();
return; // GA rotation不得自動retry
}
throw error;
}
Reset成功後舊GA secret立即失效;Backend清除舊secret的replay step、failed attempts與lock。gaReady=true只代表Backend已有可用secret,不表示使用者已完成Authenticator App設定。
Frontend不得將totpSecret寫入console、analytics、URL、localStorage、IndexedDB或可重播state。使用者關閉modal後不能重新GET該secret;若尚未保存,只能以updated version=5再次reset,此時version再增加且本次secret立即失效。409 response與Password Reset完全相同:reload current page、要求使用者重新確認,不自動重送。Supplier Account流程改用Supplier path及SupplierAccountVo。
Gateway Account List query固定為:
GET /bo/v2/gatewayAccounts
?pageNumber=0
&pageSize=20
&supplierId=
&gatewayId=
&status=
&account=
- Superadmin可傳全部filters。
- Supplier不傳
supplierId;Backend強制使用authenticated Supplier owner scope。 - Supplier傳入的
gatewayId只能命中旗下Gateway,跨scope一律拒絕。 account採case-insensitive contains search。status只接受ACTIVE/INACTIVE,且只比對SupplyAccount.accountStatus。- Gateway 的
status與operationalStatus只影響新的存款/提款分配候選,不影響既有 Gateway account 的列表可見性、accountStatus、loginEnabled、blockedBy或登入;Gateway owner 關聯僅用於資料範圍與gatewayId filter。
Supplier Account List query固定為:
GET /bo/v2/supplierAccounts
?pageNumber=0
&pageSize=20
&supplierId=
&status=
&account=
- 只有Superadmin可呼叫。
supplierId由Supplier dropdown選取,不使用supplierLoginId文字作為page filter。account採case-insensitive contains search。status只接受ACTIVE/INACTIVE。
Create/Password Reset/GA Reset成功response包含只顯示一次的secret。Frontend不得寫入console、telemetry、URL、localStorage或可重播的state persistence;使用者離開畫面後只能重新reset,不能再次GET。
Agent-bound Supply Gateway Allocation(現行基準)
SUPPLY_GATEWAY_ALLOCATION 是獨立 menu identity。正式登入後,只有GET /bo/v2/menu實際回傳此 code 時才可註冊 route;Prefix Admin 的現行 read contract 是GET /bo/v2/agent/supplyGateways?pageNumber&pageSize。它只接受active、Agent-bound principal,並由Backend從 security context 推導 own agentId;System Admin、Supply actor、未綁定 Agent 的 BO_USER 及password-change challenge都拒絕,request不得帶agentId、supplierId、Gateway或其他owner selector。
回應是依priority ASC, allocationId ASC排序的PageVo<AgentBoundGatewayVo>,只含allocation、Gateway、Supplier、Currency與兩端offset的identity/display;cross-Supplier binding符合全部lifecycle、Currency與offset gate時可見。它不回secret、limit、PT、financial/report data。現行contract只有read;allocation mutation雖維持System Admin authority,但exact endpoint/OpenAPI尚未凍結,Frontend不得建立mutation button或production client。完整接線矩陣見BO Menu API 前端參考。
歷史modal API(僅追溯,不得作新接線)
GET /supplyGatewayAllocation/agent/{agentId}?pageNumber=0&pageSize=20
成功data可直接分頁:
{
"allocationVersion": 12,
"enabledCount": 3,
"records": [
{
"gatewayId": 101,
"gatewayName": "Bangkok Core Gateway",
"gatewayCode": "BKK-CORE",
"supplierId": 10,
"supplierCode": "KOLA",
"supplierName": "KolaPay",
"priority": 1,
"enabled": true
}
],
"total": 4,
"size": 20,
"number": 0
}
每列直接提供Gateway、Code、Supplier、Priority及Enabled所需的id/code/name fields;Frontend不另查Supplier或Gateway master。agentId是path中的technical identity,畫面從既有Agent master顯示其agentPrefix;allocationVersion是整個Agent allocation resource的mutation version,不是row version。pageNumber為zero-based、pageSize為1..500。enabledCount與total都涵蓋完整eligible scope,不是current page;Frontend直接顯示{enabledCount} of {total} gateways enabled。API只回Currency與Business Zone皆和Agent相同且Supplier/Gateway ACTIVE的candidate;不回ineligible rows或reason。灰色只代表該row的allocation enabled=false,不表示Gateway/Supplier inactive或Currency/Business Zone mismatch。priority永遠是正整數:missing mapping為1,existing disabled mapping保留原值。Disabled UI顯示—並停用priority arrows,但保留numeric value供off toggle重新Enable;不使用null、dash字串或NOT_MAPPED。Backend固定以priority ASC, gatewayName ASC, gatewayId ASC排序;較小值先顯示,但這只是畫面順序且不影響Deposit/Withdrawal routing。Frontend照records順序顯示,不自行重排。
單列修改
PUT /supplyGatewayAllocation/agent/{agentId}/gateway/{gatewayId}
{
"enabled": true,
"priority": 3,
"expectedVersion": 12
}
一列一次呼叫。成功後採Backend回傳狀態/version;失敗只回復該列,不回復其他已成功列。
成功data:
{
"allocationVersion": 13,
"affectedCount": 1,
"enabledCount": 3,
"total": 4,
"row": {
"gatewayId": 101,
"gatewayName": "Bangkok Core Gateway",
"gatewayCode": "BKK-CORE",
"supplierId": 10,
"supplierCode": "KOLA",
"supplierName": "KolaPay",
"priority": 1,
"enabled": true
}
}
Frontend用row.gatewayId更新local row,更新footer的enabledCount/total並保存新allocationVersion。Enable/Disable可原位替換;Priority成功因可能跨頁移動,必須再GET current page,不可只對目前records做local sort。No-op時affectedCount=0、version不變但仍回current row與summary;error/conflict不使用success projection,Frontend回復該次optimistic UI。
Enable All/Disable All
POST /supplyGatewayAllocation/agent/{agentId}/enableAll
POST /supplyGatewayAllocation/agent/{agentId}/disableAll
Request帶目前expectedVersion。Bulk作用於全部定義範圍,不限current page。成功data不回records:
{
"allocationVersion": 14,
"affectedCount": 3,
"enabledCount": 4,
"total": 4
}
Frontend先保存allocationVersion、更新footer summary,再重新載入current page。affectedCount是實際變更的mapping數;enabledCount/total只計完整eligible scope,所以Disable All處理到ineligible existing mappings時,affected count可能大於total。Reload失敗時顯示資料重新載入失敗,不得把已成功的bulk呈現為rollback。Single/Bulk stale version固定回HTTP 409與RESOURCE_VERSION_CONFLICT(40051);Frontend重新GET,不可用舊version自動重送覆蓋。
Prefix modal 必改項目
Save Agent只保存Prefix master,不包含allocation。- 移除
Allocation change reason。 - 保留priority輸入;只接受正整數,可重複,default 1,數值越大語意上越優先。Priority是weight而非rank,畫面列順序不代表Priority,修改後不因weight自動移動row。
- Page固定依
priority ASC, gatewayName ASC, gatewayId ASC顯示;較小值先顯示但不代表實際routing priority,Frontend不提供本期未定義的sort切換。 - 明確顯示「Gateway–Prefix Priority目前不影響Smart Routing」;這是UI label,technical relation仍是Agent–Supply Gateway Allocation。
- Agent inactive時允許query及Disable All;禁止enable、首次建立mapping與修改priority。
API 接線總表
| 順序 | API | 何時呼叫 |
|---|
| 1 | POST /public/v2/login | Login form submit |
| 2A | POST /public/v2/password/change | nextStep=PASSWORD_CHANGE_REQUIRED |
| 2B | GET /bo/v2/menu | 拿到formal token後;只按實際method code註冊route |
| 3 | Supplier/Gateway account page APIs | menu包含對應account list code後 |
| 4 | Account create/status/reset actions | 對應頁面操作;Backend重新驗證scope |
| 5 | GET /bo/v2/agent/supplyGateways | 僅menu回SUPPLY_GATEWAY_ALLOCATION的active Agent-bound Prefix principal進入read route後 |
| 6 | Allocation mutation | 尚未有現行凍結endpoint;不建立production client |
尚未凍結,Frontend暫時不要猜
- Account Settings self-service exact API paths與normal password-change request。
- Supplier/Gateway完整fixed menu code inventory。
- Gateway action matrix、name-only endpoint、success response、validation、non-unique及same-name no-op語意已凍結:成功
data為{ gatewayId, gatewayName, version };current same-name成功但version/audit不變,stale same-name仍409。 - Formal JWT expiry、storage policy與logout/revoke endpoint。
這些項目不影響Login Type畫面與已凍結path的前端切分,但在OpenAPI contract完成前不能把示意值寫成production integration。
Source Drift
| 舊來源 | 衝突 | Current interpretation |
|---|
| ADR-0100 | /public/login與/supply/auth/login分流 | 由ADR-0104改為單一POST /public/v2/login+loginType |
| ADR-0101 | 允許MFA OFF略過GA | 新V2三種Login Type全部強制GA |
| ADR-0102 | realm-specific response | 新V2使用formal success或PASSWORD_CHANGE_REQUIRED兩個branch |
| ADR-0034/ADR-0035 | Gateway accounts只由System Admin管理 | owning Supplier可透過獨立Gateway Account List管理旗下accounts |
| ADR-0036 | password change重傳current password | 新V2只傳passwordChangeToken+newPassword |
| ADR-0096/ADR-0097/ADR-0105 | 舊Prefix Allocation menu/Superadmin modal與/supplyGatewayAllocation/**流程 | 新catalog將SUPPLY_GATEWAY_ALLOCATION定為獨立menu;現行只採authenticated Agent推導scope的GET /bo/v2/agent/supplyGateways read contract。舊modal API僅供歷史追溯,不作新接線基準。 |
| Backoffice Capability Map/Foundation Schema | 仍引用split login、split menu及舊account/allocation grants | Login與Account仍以本handoff為準;完整23-code menu、API maturity、release readiness與現行Agent-bound allocation read以BO Menu API 前端參考為準;schema exact change仍待implementation contract |
附件中的prototype與RD討論稿只用來驗證畫面意圖;若與已接受ADR衝突,以較新的ADR-0104/0105為準。
Source Links
- 統一Login/Menu/Account ADR
- BO Menu API 前端參考
- Prefix內嵌Allocation ADR(歷史追溯)
- 現有legacy login controller
- 現有role-based menu controller
- 現有menu service
- 現有JWT只載入BO User的filter
- 共用PageVo contract
SPay4 Prefix and Supplier Onboarding Contract
本文件是 GitLab #199 的 implementation-input contract。它固定 Prefix 的 no-implicit-Supply boundary 與 Supplier/DEFAULT Gateway 的 atomic onboarding;不授權 Java、DDL、SQL、資料庫操作、credential provider 整合、deployment 或 Tracker publication。
1. Authority and replacement
本文件以 SPay4 Rebuild Consolidated Spec §2、§3.2 為 authority,並使用 #197 的 target schema contract 與 #200 的 authorization contract。任何較早文件中「Supplier 有單一 Currency」、「Supplier Create 不建立 DEFAULT Gateway」、SupplyActorAccount/AuthenticatorCredential 作為 target canonical 名稱,或以 Legacy POST /supplier 寫入的敘述,都不得作為 SPay4 target implementation authority。
Supplier 是 Supply owner root,沒有自身單一 Currency 語意;Currency 屬於 immutable Supply Gateway。supply_account 是 Supplier 或 Gateway 登入 principal 的唯一 canonical aggregate,直接保存 own password、TOTP、challenge 與 session state。Agent OGP/LINE integration 的 credential_reference/credential_secret 不屬於 Supply onboarding。
2. Prefix Create boundary
Prefix 是既有 Agent 的 BO 顯示名稱,不是獨立 technical entity。Prefix Create 只建立 Agent/Prefix 與必要 BO_USER onboarding,且必須明確提供 immutable businessUtcOffset;其值只接受 canonical ±HH:MM、範圍 -14:00..+14:00,不得使用 IANA timezone、Z、省略格式或 runtime/DB default。建立後不得修改,亦不得保留 legacy businessZone 作 parallel authority。
禁止在 Prefix Create transaction 或其 after-commit flow 中隱式建立或修改:
- Supplier、Supply Gateway、
supply_account、其 direct credential state、Supply PT rule 或 Gateway direction config; - Agent
credential_reference、credential_secret、notification_provider_source 或 OGP callback URL; - Agent–Supply Gateway allocation;
- Legacy Payment Channel 或任何 Legacy Payment Channel allocation。
未來若需要 Prefix 使用 Supply Gateway,必須由獨立且已授權的 Agent–Supply Gateway allocation mutation 建立;Prefix Create 不能用 default、background job 或補償 API 形成該關係。
3. Supplier Create request and identity
Supplier Create 固定為 POST /bo/v2/suppliers。request 只接受下列 required fields:
| Field | Canonical behavior |
|---|
name | 移除前後空白後 required、長度 1..50;保留大小寫、Unicode 與內部空白,可重複。 |
loginId | 移除前後空白、以 Locale.ROOT 轉大寫後驗證 ^[A-Z0-9][A-Z0-9_-]{0,49}$;canonical value 必須全域唯一。 |
businessUtcOffset | required canonical ±HH:MM fixed UTC offset;不接受 IANA region、空白或 runtime/persistent default。 |
defaultGatewayCurrencyId | required 的 active Currency identity;只決定本次 DEFAULT Supply Gateway 的 immutable Currency,不賦予 Supplier 自身 Currency。 |
request 不得接受 code、account 名稱、Supplier/Gateway status、Gateway code/name、PT rates、direction limits、allocation、owner/realm selector、credential material 或 audit field。Supplier Create 只允許通過 #200 request guard 的 Superadmin;menu visibility 或 client payload 不構成 authority。
Backend 將 canonical loginId 同時保存為 immutable Supplier.code 與 Supplier.loginId。第一個 Supplier account 必須衍生為 {canonicalLoginId lowercase}_supplier_admin,第一個 Gateway account 必須衍生為 {canonicalLoginId lowercase}_gateway_admin;兩者都不接受 Frontend override、truncate、collision suffix 或同值 fallback。
4. Atomic onboarding aggregate
所有下列步驟必須在單一 application-service transaction 完成,且只能在 transaction 成功 commit 後交付 credential material:
- 建立 ACTIVE Supplier,保存 canonical equal
code/loginId 與 businessUtcOffset。 - 建立 ACTIVE Supplier initial
supply_account,owner 為該 Supplier、fixed SupplyProfile.SUPPLIER、mustChangePassword=true,並直接保存其 credential state。 - 建立 Supplier owner-level Deposit/Withdrawal CURRENT PT rules,兩筆 rate 都為
0。 - 建立 ACTIVE DEFAULT Supply Gateway,固定
code=DEFAULT、name={Supplier Name} Default Gateway,Currency 為 defaultGatewayCurrencyId。 - 建立兩筆 DEFAULT Gateway Deposit/Withdrawal direction config,兩個 direction 都是有效的
0/0 unlimited per-order range。 - 建立 ACTIVE Gateway initial
supply_account,owner 為該 DEFAULT Gateway、fixed SupplyProfile.GATEWAY、mustChangePassword=true,並直接保存其 credential state。 - 建立 Gateway owner-level Deposit/Withdrawal CURRENT PT rules,兩筆 rate 都為
0。 - 寫入一筆 required、non-secret
CREATE_SUPPLIER aggregate audit:target 是 Supplier;semantic_effects 列出 Supplier、兩個 account、四筆 PT rule、DEFAULT Gateway 與兩筆 direction config 的 committed non-secret effect。
任一 request validation、identity uniqueness、Currency validation、password credential generation、direct credential-state persistence、PT/direction config persistence、audit sanitization/serialization 或 audit 寫入失敗,都必須 rollback 整個 aggregate。不得留下 Supplier root、DEFAULT Gateway、account、credential state、PT rule、direction config 或 audit 的 partial state;不得使用 after-commit 補建、HTTP self-call、Frontend 兩段補償或 REQUIRES_NEW audit。
5. Credential delivery and audit boundary
成功 response 必須在同一 response 內按 Supplier account 與 Gateway account 分組交付各自的一次性 credential material,並回傳所建立的 non-secret identity/lifecycle projection。關閉、遺失或傳輸失敗後不得以 GET、detail、list、audit 或另一個 read API 重新取得;後續 recovery 必須使用獨立且已授權的 credential reset flow。
credential material、password hash、encrypted secret、provider raw secret 與 Legacy secret reference 都不得出現在:
- target persistent model(
credential_secret 的 Agent OGP/LINE AES-GCM ciphertext 與 supply_account 的 own TOTP AES-GCM ciphertext 除外)、credential_reference 或 audit before/after/effects; - application/access log、exception message、telemetry、trace、metric label、URL 或 analytics;
- Frontend localStorage、sessionStorage、IndexedDB、persistent state、route state 或可重播 cache。
required audit 只記錄已 commit 的 authenticated BO_USER actor、server-derived owner scope、Supplier aggregate root target、stable CREATE_SUPPLIER action type、non-secret effect-object array 與必要 before/after state。credential material 必須在 commit 成功後才回傳,audit failure 仍使整個 onboarding rollback;完整 shape 與 endpoint catalog 見 BO Domain Audit Contract。
6. Explicit exclusions and verification assumptions
本 contract 不建立 Prefix-to-Gateway allocation、additional Gateway onboarding、Supplier-level Currency、Supply role table、Legacy Payment Channel、Legacy credential fallback 或 credential read-back API。OGP callback runtime URL 只可由 agent.callback_url 取得,且 onboarding 不得讀寫它或任何 Agent integration credential relation。它不定義 DDL、seed、credential provider protocol、exact response field names、runtime exception mapping 或 Frontend component implementation。
後續 implementation 必須以 Prefix and Supplier Onboarding Verification Matrix 證明 Prefix negative boundary、request/identity semantics、完整 transaction rollback、one-time credential exposure 與 secret-safe audit/log/telemetry/Frontend handling。
SPay4 Prefix and Supplier Onboarding Verification Matrix
本文件是 GitLab #199 的 documentary verification evidence。它定義未來 implementation 必須提供的 Prefix/Supplier request-response、transaction rollback、credential exposure 與 audit safety proof;不是 runtime test result、SQL、資料庫操作或 credential provider 執行紀錄。
1. Prefix no-implicit-Supply matrix
| Scenario | Required result | Negative proof |
|---|
| Prefix Create 成功 | 只建立 Agent/Prefix 與必要 BO_USER onboarding | 不建立 Supplier、Supply Gateway、supply_account、其 direct credential state、PT rule、direction config、allocation、Agent integration credential relation 或 Legacy Payment Channel |
| Prefix Create 後的 background/after-commit processing | 沒有 Supply aggregate write | 不得使用 default、job、listener、retry 或補償 API 自動建立 Supply resource |
| Prefix Create 失敗 | Agent/Prefix 與 BO_USER onboarding 依既有 transaction contract rollback | 不得留下任何上述 Supply 或 Legacy Payment Channel artifact |
| 後續需要使用 Supply Gateway | 必須使用獨立已授權 allocation mutation | Prefix Create 不得建立、enable 或猜測 Agent–Supply Gateway allocation |
2. Supplier request and identity matrix
| Input / condition | Required result | Failure or security proof |
|---|
valid name、loginId、businessUtcOffset、defaultGatewayCurrencyId | 進入 atomic onboarding | request 不接受額外 owner、status、account、Gateway、PT、allocation、credential 或 audit field |
name edge whitespace | 保存 trim 後的 1..50 display value;保留大小寫、Unicode 與內部空白 | blank/過長失敗,無任何 aggregate write |
loginId kola | canonical KOLA、保存 code=loginId=KOLA | raw/mixed-case value 不得成為另一个 identity |
invalid loginId、canonical duplicate 或 code/loginId drift | fail closed | 不建立 Supplier、Gateway、account、credential、PT、config 或 audit |
missing/invalid businessUtcOffset | fail closed | 不使用 IANA zone、server time、DB default 或 fallback offset |
missing/inactive defaultGatewayCurrencyId | fail closed | Supplier 不取得 Currency,亦不得以 THB/first active Currency 或任何 default 取代 |
| account derivation | kola_supplier_admin 與 kola_gateway_admin | Frontend 不得 override;不得 truncate、加 suffix 或以 loginId 本身 fallback |
| non-Superadmin/menu-only/forged owner selector | request guard 拒絕 | 不得建立 aggregate,且不可由 client 選擇 Supplier/Gateway owner scope |
3. Atomic onboarding and rollback matrix
| Fault injection / rejection point | Required committed state |
|---|
| request、identity 或 Currency validation failure | 沒有 Supplier、Gateway、account、credential state、PT rule、direction config 或 audit |
| Supplier initial account or password credential generation failure | 沒有 Supplier root、DEFAULT Gateway 或任何 partial child |
| Supplier PT rule persistence failure | 沒有 Supplier、Gateway、account、credential state、PT rule、direction config 或 audit |
| DEFAULT Gateway/direction config persistence failure | 沒有 Supplier、Gateway、account、credential state、PT rule、direction config 或 audit |
| Gateway initial account、password credential or PT persistence failure | 沒有 Supplier、Gateway、account、credential state、PT rule、direction config 或 audit |
| required audit sanitization、serialization or persistence failure | 整個 onboarding rollback;不得使用 REQUIRES_NEW、after-commit 或非同步補寫 |
| complete success | ACTIVE Supplier、DEFAULT Gateway、兩個直接保存 credential state 的 supply_account、兩個 owner 各自 Deposit/Withdrawal rate 0 PT rules、兩筆 Gateway 0/0 config 與一筆 CREATE_SUPPLIER audit 一同 commit |
成功 aggregate 額外檢查:DEFAULT Gateway 固定 code=DEFAULT、name={Supplier Name} Default Gateway,Currency 等於 request 的 defaultGatewayCurrencyId;Supplier 不保存 single-Currency authority,且沒有 Agent allocation 或 Legacy Payment Channel。
4. Credential exposure and persistence matrix
| Surface | Required result | Negative proof |
|---|
| successful Create response | 以 Supplier account 與 Gateway account 分組,各自只交付一次 credential material | response 不包含 Legacy secret/configuration、password hash、encrypted secret 或 audit payload |
| failed/rolled-back request | 不交付 credential material | 即使已在 memory 產生 credential,失敗 response、log、trace 與 telemetry 都不得含 secret |
| follow-up GET/list/detail/audit | 不提供 credential material 或可重播 token | 關閉/遺失後只能走獨立 credential reset flow |
| audit | 只保存 committed authenticated BO_USER actor、server-derived scope、Supplier aggregate-root target、CREATE_SUPPLIER、before/after 與含全部子資源 ID 的 non-secret effect-object array | 不記錄 password、temporary password、TOTP secret/code、OGP/LINE API secret、provider raw secret、hash、ciphertext、token、authorization header 或可還原衍生資料 |
| Supply principal credential state | supply_account 直接保存 own password、TOTP、challenge 與 session state | 不可當作 Agent integration credential,且不進 audit、log、telemetry 或 response read-back |
| Agent integration/MFA boundary | OGP/LINE 僅用 Agent credential_reference+credential_secret;TOTP 由 user 與 supply_account 各自直接保存 | onboarding 不建立、共用或讀取 Agent integration credential;TOTP 不可作 API key |
| server observability | log、exception、trace、telemetry、metric label 與 URL 都不含 secret | failure diagnostics 必須 secret-safe 且不回顯 raw request credential data |
| endpoint catalog | 每個已實作 SPay4 BO handler 恰有一個 READ_ONLY 或 MUTATION(actionType) 分類;POST /bo/v2/suppliers 對應 CREATE_SUPPLIER | 未登錄、重複登錄、mutation 缺少 action type,或 read-only/export/external-only handler 寫入 audit 都必須使測試失敗 |
| Frontend | 只在成功一次性 credential presentation 使用 | 不寫入 console、analytics、URL、localStorage、sessionStorage、IndexedDB、persistent state、route state 或 replayable cache |
5. #199 acceptance coverage
| Issue acceptance criterion | Documentary evidence |
|---|
| Prefix negative test 證明無隱式 Supply/allocation/Legacy Payment Channel | §1 Prefix no-implicit-Supply matrix |
Supplier request、canonicalization、code=loginId、initial account derivation 與 defaultGatewayCurrencyId semantics 完整定義 | §2 request and identity matrix;onboarding contract §3 |
| Supplier 沒有單一 Currency;DEFAULT Gateway 使用 immutable request Currency | §3 complete-success checks;onboarding contract §1、§4 |
| validation、credential、persistence、audit failure 全部 rollback | §3 atomic onboarding and rollback matrix |
| credentials 僅成功一次揭露,不能 read-back 或持久化 | §4 credential exposure and persistence matrix |
SPay4 Deposit/Withdrawal 流程 Contract
結論
本文件是 D1/W1 的唯一流程 authority。HOUSE Card 只可參與 Deposit 收款:withdrawal_enabled 是保留設定值,無論其值為何,HOUSE 都不得進入 Withdrawal 候選、指派、重派、分單、回收或 recovery。house_card_transaction 是唯一的 LINE HOUSE inbound Deposit evidence,先持久化、後比對,且不承載 Legacy transaction、Cardholder、Payment Account 或 Withdrawal。所有 relation 都是 logical relation,沒有 database foreign key;所有 mutation、reservation、BO remark append 與 Audit 必須在 committed transaction 邊界內完成。
1. 共同建立、候選與 idempotency
- Agent API 保留四條既有 path、HMAC 與 request idempotency 語意;新模型只寫 target-native
agent_api_order_claim,不得 ALTER、讀取、fallback 或 dual-write agent_api_order_log。 - claim 以
agent_id + order_type + agent_order_id 唯一;同一 key 重送只回同一 formal order/結果,不能建立第二個 attempt、reservation、callback 或 Audit。 - 建單先驗證方向對應 bank pair 至少有一個非空白;bank 是 request snapshot,不是候選 Payment Account/HOUSE Bank 的相等條件。
- 候選一律先通過 owner chain、Gateway-Prefix mapping、active/online/可承接狀態、direction、Currency、單筆 min/max、日限額、Freeze、風控與併發 gate,再依既有排序規則排序。
- 每次選中、略過、保留失敗、釋放、consume、改派、review/recovery 與 callback 均寫 non-secret committed Audit;Audit 不得含完整帳號、憑證內容、token 或 secret。
2. Deposit
2.1 候選與原子指派
Deposit 是單一扁平池:該 Prefix 的合格 HOUSE Card 加上已 mapping、Supplier/Gateway 可用、且 Payment Account 合格的 CH 帳戶。HOUSE 額外要求 deposit_enabled=true、自身 deposit_min/deposit_max 完整且合格;CH 使用其 Gateway 的 DEPOSIT config。候選不足或全部 reservation 失敗時,Order 進 PENDING_REVIEW,不靜默丟單。
同一交易必須完成:鎖定候選 counter/可用容量、建立 reservation 與 immutable assignment attempt、更新 Order current pointer/projection、寫 route/reservation Audit。任一步失敗全部 rollback。HOUSE Deposit 同時建立 house_card_deposit_reservation;其 quota identity 固定為 house_card_id + local business_date,不與 CH Payment Account counter 混用。
2.2 狀態、證據與結算
PENDING_ROUTING → ASSIGNED → SETTLED
├→ EXPIRED(未證明逾時)
└→ PENDING_REVIEW(金額不符或證據不足)
- CH/Gateway/Supplier 可在自身 scope 提交資料;Supplier 只能走 emergency authority 並填 reason。Deposit 對 Supplier/Gateway 工作台永遠唯讀,任何 Supply Deposit mutation 一律後端拒絕。
- 只有 requested amount 與已確認 actual amount 相等、證據完整且既有規則通過時才可自動
SETTLED。金額不符或證據不足不自動成功,也不自動拒絕。 - HOUSE 成功時,在同一 transaction 將原 assignment date 的 quota 由 requested amount 調整為 confirmed actual;跨日不搬移。已證明
NOT_RECEIVED 且 assignment 正式終止才可原子釋放 reservation/quota。 - callback 使用 formal order 的一次性 callback projection/idempotency key;callback dispatch 失敗不得回滾已提交的 financial state。
2.3 HOUSE LINE transaction ingress 與 settlement
- 只有已驗證
notification_provider_source/credential 的 LINE ingress adapter 可建立 house_card_transaction;adapter 從 verified source 導出單一 agent_id。BO CRUD endpoint 不得收 provider payload,非 inbound Deposit 一律拒絕且不建立 HOUSE transaction。 - ingress 同一 transaction 寫入正規化 snapshot、canonical payload hash 與 versioned AES-GCM raw ciphertext,並以
(notification_provider_source_id, provider_transaction_id) 取得或建立 row。相同 identity/hash 是 idempotent replay;identity 相同但 payload 不同必須拒絕、不得覆寫既有 evidence,僅留下不含 raw payload、ciphertext、帳號或姓名的安全 conflict evidence。 - service 只以同 Agent 的 receiver Bank + canonical receiver account 精確解析 active HOUSE Card。沒有或有多張 HOUSE Card 時仍保存 row 為
PENDING_REVIEW,不得猜測或改綁 owner。 - matcher 僅掃描同 Agent、
ASSIGNED、current attempt 仍綁定相同 HOUSE 的 Deposit order。HOUSE、amount 及 order immutable payer_account 三項均相同且 candidate 唯一,才可 auto settlement;其他情境寫入固定安全 match_remark 並保留 UNMATCHED、PENDING_REVIEW 或 MATCH_FAILED。 - auto 與 manual settlement 共用同一 locked transaction:鎖定 transaction、order、current attempt、HOUSE reservation 與 daily counter,重新驗證未占用、Deposit direction、Agent/HOUSE scope、order state 與必要欄位,接著原子寫入 match association、actual amount、terminal state、consume 與 callback outbox intent。callback 僅 commit 後送出。
- 僅在沒有 candidate 時於 commit 後依序 1、3、5 分鐘重試。多 candidate、HOUSE 無法解析、終態 order、金額或 payer account 不符不會自動結案;第三次後保留歷史,不增加 dismiss/reject action。
- 人工 command 只接受 transaction ID、Deposit order ID 和例外原因;登入 principal 導出 Agent scope。HOUSE/Agent/direction/order state/未占用條件任一不符即拒絕;只有 amount 或 payer account 不符可在 non-empty reason 下例外,成功標記
MANUAL 並記錄安全 Audit。 - page/detail 只回正規化且必要時遮罩的資料。raw-content read 先驗證 own-Agent 或 System Admin scope,service 解密後以
Cache-Control: no-store 回應;raw plaintext/ciphertext 不得進 Audit、ChangeLog、log、telemetry、CSV 或一般 response。
3. Withdrawal
3.1 候選與原子指派
Withdrawal 的單一扁平池只有已 mapping 且合格的 CH Payment Account。HOUSE 即使 withdrawal_enabled=true 也必須在查詢、排序、指派、改派、分單及回收前被排除。
Payment Account 指派在同一 transaction 建立 account balance/capacity reservation、immutable assignment attempt、Order current pointer/projection 與 route Audit。一般候選 reservation 競爭失敗可依排序嘗試下一個候選;Withdrawal 餘額 reservation 失敗時整筆進 ROUTING_FAILED/人工處理,不得 fallback 或改派下一候選。
3.2 狀態、證據、逾時與 recovery
PENDING_ROUTING → ASSIGNED → ACCEPTED → PROOF_SUBMITTED → SETTLED
├→ PENDING_REVIEW(逾時、金額不符或證據不足)
├→ REJECTED_NO_TRANSFER(已確認 NOT_TRANSFERRED)
└→ PENDING_RECOVERY(疑似或確認 duplicate payout)
SETTLED 僅限 actual=requested、證據完整且規則通過;其他結果保留人工義務。Withdrawal PENDING_RECOVERY/supply_withdrawal_recovery_case 的人工終態由 System Admin 全域裁決,或由已驗證 SUPPLIER principal 在自身 immutable Supplier scope 內、同時通過 active owner chain、既有 emergency authority 與 non-empty reason guard 後直接裁決;Gateway 不得裁決。這不新增自動結案或新的終態。- assignment deadline 到期不等於未轉出;reservation 留在 review-held projection,直到人工確認 disposition。
- 只有已確認
NOT_TRANSFERRED 且舊 assignment 正式結束後可以改派。先原子取得新候選 reservation,再在同一 transaction切換 Order pointer、結束舊 attempt 並處理舊 reservation。新 reservation 失敗時舊 pointer、assignment、reservation 與 quota 均不變。 - 疑似或確認重複出款必須寫入
supply_withdrawal_recovery_case,Order 進 PENDING_RECOVERY;recovery case 保存 evidence、resolution/verdict、操作者與時間。裁決與既有 evidence、Order disposition、non-secret Audit 必須在同一 committed transaction 寫入,不得靠 callback 或 CH 提交直接結案。
4. 權限、投影與不可洩漏拒絕
所有 list、detail、helper、CSV 與 action 使用同一個 server-derived OrderWorkspaceScope。client realm、Login Type、owner selector 或猜測 ID 都不得擴權;不在 scope 的 detail/helper/mutation 回相同 not-found representation,不洩漏存在性。
- Prefix 僅看 own Agent 訂單;對 Supply source 遮罩 owner identity,混有 Supply 的 balance-impact 區塊整塊隱藏,CSV 與畫面完全同投影。
- Supplier/Gateway 只看可靠 immutable owner snapshot 可證明的 own Supply order;排除 HOUSE、未指派池與其他 owner chain。
- Gateway 有 own scope 的 Withdrawal 日常 action,但不得裁決 review/recovery。Supplier 只有以已驗證
SUPPLIER principal 通過 active owner chain、case/order immutable own-Supplier scope、既有 emergency authority 與 non-empty reason 後,才可直接裁決 PENDING_RECOVERY recovery 終態;任一失敗 fail closed 並回相同 not-found representation。 - 每次 CSV export、代理 CH mutation、route decision、reservation transition、review/recovery、callback 與 BO remark append 都需要 committed non-secret Audit。
- BO remark append 是獨立 command:在既有
OrderWorkspaceScope 驗證後,以 optimistic version/CAS 原子追加 trim 後文字;單次最多 500 字元、累積最多 4,000 字元,空白或超長一律拒絕且不得截斷。既有文字不為空時只可用精確 ; 連接,不能覆寫、清除或刪除。 - remark append 不改變 routing、reservation、quota、settlement、callback 或任何 order state;同一 transaction 的
BO_ORDER_REMARK_APPENDED Audit 只保存 actor、時間、append action、前後是否有備註、總長度與新增字元數,不保存原文。
5. Transaction boundary checklist
| Command | 同一 transaction 必須提交的內容 | idempotency/不可變事實 |
|---|
| claim/create | claim、formal order、初始狀態、Audit | claim key、Agent/order snapshot |
| assign/reassign | candidate lock、counter/reservation、attempt、Order pointer/projection、Audit | attempt no、owner/Currency/time snapshot |
| proof/actual | proof metadata、Order/attempt projection、Audit | content hash;不建立通用 Supply transaction evidence |
| HOUSE LINE ingress | immutable house_card_transaction、canonical dedupe/conflict guard、加密 raw evidence | provider source + transaction identity;canonical payload hash |
| HOUSE auto/manual settlement | transaction/order/attempt/reservation/counter lock、match association、actual、terminal state、outbox intent、Audit | matched Deposit order unique;AUTO/MANUAL mode;callback after commit |
| settle/review/recovery | formal state、reservation consume/release/review-held projection、case、resolution/verdict、actor/time、Audit | approved disposition/evidence/operator/time;Supplier recovery 裁決另驗證 immutable own scope、emergency authority、non-empty reason,並保存原 Gateway/CH |
| callback | callback projection/outbox intent、Audit | callback idempotency key;dispatch 不回滾已提交終態 |
| BO remark append | CAS 保護的 bo_remark 追加、derived has_bo_remark 與 BO_ORDER_REMARK_APPENDED Audit | append-only、500/4,000 字元上限;不改變 routing、reservation、quota、settlement、callback 或 state |
6. 驗收重點
- 並行 assignment 不可重複占用同一容量;Deposit 候選耗盡進人工處理。
- Withdrawal balance reservation 失敗不 fallback;HOUSE 不會出現在任何 Withdrawal trace。
NOT_TRANSFERRED 前不得重派;新 reservation 失敗時舊 assignment 不變。- amount mismatch、late proof、證據不足與 duplicate payout 都不得自動成功;前者 review、後者 recovery。
PENDING_RECOVERY 只可由 System Admin 全域裁決,或由通過 own-Supplier scope、emergency authority 與 non-empty reason guard 的 Supplier 裁決;Gateway 一律拒絕。 - HOUSE LINE auto settlement 僅接受唯一、同 HOUSE、同金額及同 payer account candidate;無 candidate 只重試 1、3、5 分鐘,其他不收斂情境轉人工。
- HOUSE quota 成功調整原 assignment date,跨日不搬移;未證明逾時不得當作可釋放證據。
- BO remark 只可追加且與 Audit 同 transaction;不改變任何 lifecycle、routing、reservation、quota、callback 或 settlement 結果。
Deposit 圖 · Withdrawal 圖 · Order ER · State 圖
SPay4 存取款工作台與角色資料範圍 Contract
本文件是 D1(Deposit)與 W1(Withdrawal)的 implementation-input contract。它定義四種已驗證 BO principal 在訂單工作台的資料範圍、投影、匯出、操作、時間與稽核邊界;不授權 Java、DDL、SQL、target database 操作、Legacy migration 或 routing/settlement 實作。
1. 結論與非目標
SPay4 不建立獨立的存取款彙總報表中心、dashboard、跨角色 polymorphic report API 或 actor-specific report table。營運查詢只由 Deposit List、Withdrawal List、其單筆明細、同投影 CSV 匯出與 Withdrawal Action 工作台承載。
Supplier 與 Gateway 不取得 Channel Analytics、Channel Deposit Report 或任何 Legacy daily/channel report authority。PT/Commission Report 是獨立 R1 scope,不混入存取款工作台,也不得以工作台資料臨時組成日/時 aggregation 或新的 report fact。
現行 Legacy DailyReportController、payment_channel_daily_report 與 payment_channel_card_daily_report 只保留 Legacy 相容行為。SPay4 Supply actor 不得透過它們取得訂單、Channel 或供應側資料;它們不是 Supply order projection、scope resolver 或 API 的實作基礎。
2. Principal、owner 與可見工作台
Agent 是 SPay4 technical owner identity;Prefix 只是 Agent 的 BO 顯示名稱。每個 request 必須先由已驗證的 formal principal 重新推導 realm、principal type 與 owner chain,不能相信 Login Type、menu、route、JWT decoded owner claim 或 client request 的 owner selector。
| 已驗證 principal | server-derived 可見範圍 | Deposit 工作台 | Withdrawal 工作台 |
|---|
SUPERADMIN | 全域;可用 Agent、Supplier、Gateway filter 縮小 | list、detail、CSV;沿用既有人工處理 authority | list、detail、CSV;沿用既有人工處理 authority |
PREFIX(Agent) | 僅 authenticated agentId 的訂單;HOUSE 與供應側採不同投影 | list、detail、CSV;可保留既有代表 Agent 建單與自身 HOUSE Deposit flow | list、detail、CSV;不取得供應側 CH Action 或任何 HOUSE outflow flow |
SUPPLIER | authenticated Supplier 與其 active/historical owner chain 可判定的 Gateway、CH、Payment Account 訂單;不含 HOUSE、其他 Supplier、未指派池 | list、detail、CSV,唯讀 | list、detail、CSV;僅緊急介入 Action |
GATEWAY | authenticated Gateway 與直屬 CH/Payment Account 的訂單;不含 HOUSE、同 Supplier 其他 Gateway、未指派池 | list、detail、CSV,唯讀 | list、detail、CSV;既有日常 CH Action |
訂單只有在 target Supply order 保存或可從 immutable historical snapshot 驗證 supplierId、supplyGatewayId 與相關 CH/Payment Account owner chain 時,才可列入 Supplier 或 Gateway 範圍。不得以 Agent allocation、目前 mapping、Legacy channel、名稱、Prefix 或 fallback 欄位猜測 Supply owner。
3. 單一 server-derived scope resolver
D1/W1 必須提供一個由 authenticated principal 取得的 OrderWorkspaceScope resolver;每一個 list、detail、tab/helper query、CSV export 與 mutation 都必須使用其結果。Controller、service、DAO 各自重建 scope、或先查詢再在前端過濾,都不符合本 contract。
resolver 至少輸出:principal realm/type、actor identity、可見 Agent/Supplier/Gateway predicate、可見 HOUSE/Supply source 類型、viewer businessUtcOffset、projection profile 與可用 Action policy。資料庫 query 必須以這些 server-derived owner predicates 限制;不能以 realm string 當作 fact/order ownership key。
3.1 Filter 規則
- System Admin 可提交
agentId、supplierId、gatewayId 作為縮小全域 scope 的 filter。 - 所有 principal 都可提交可選精確布林
hasBoRemark=true|false;未帶時不加 predicate,帶入時只對 generated has_bo_remark 作等值篩選,不支援備註全文搜尋。 - Agent 只可提交自己的
agentId 作為冗餘縮小 filter;不同值一律拒絕。 - Supplier、Gateway request 不接受任何 Agent、Supplier、Gateway owner selector;API binding 必須拒絕而非忽略欄位。
- System Admin/Agent 帶入不存在或不在其 server-derived scope 的 owner filter 時,必須在查詢前回 endpoint 對應的
DATA_NOT_EXISTS/not-found representation;不得回空 page、移除 filter 後查自己的資料,或揭露 target 是否存在。 - 以猜測 order id 存取不在 scope 的 detail、helper 或 mutation,也一律回相同 not-found representation。
3.2 來源與操作共通限制
Supplier/Gateway scope 一律排除 HOUSE、其他 Supplier/Gateway、全域未指派池及 owner 無法可靠判定的歷史訂單。Deposit Action 必須為空,且後端同樣拒絕任何供應側 Deposit mutation;不可只以前端隱藏按鈕。
除 Withdrawal PENDING_RECOVERY/supply_withdrawal_recovery_case 外,所有金額不一致、證據不足或需要人工裁決的 terminal result,只能走既有 System Admin 人工審核 authority。該 recovery 終態由 System Admin 全域裁決,或由已驗證 SUPPLIER principal 在自身 immutable Supplier scope 內、同時通過 active owner chain、既有 emergency authority 與 non-empty reason guard 後裁決;Gateway 不得因 owner scope 取得裁決權。
4. 角色投影與遮罩
同一 scope resolver 必須選擇 list、detail、helper 與 CSV 的 projection profile。任何 response 都不能用 client 的 reportView、realm、Supplier/Gateway scope 或 Login Type 切換投影。所有在既有 scope 內可讀 order 的 SUPERADMIN、PREFIX、SUPPLIER、GATEWAY 都可取得 hasBoRemark 與完整 boRemark;備註不授與額外 order visibility,也不對 Agent API、Cardholder API 或 Legacy endpoint 外露。
4.1 Agent/Prefix projection
Agent 對自身 HOUSE 訂單可取得完成工作所需的完整 HOUSE 資料。若訂單來源是 CH Payment Account,或某個 detail 區塊混合 HOUSE 與 Supply source,則必須按下列規則遮罩;無法辨識來源時視為 Supply source。
| 類別 | Agent 不得取得的 Supply 資料 |
|---|
| identity 與 navigation | Supplier/Gateway/CH identity、Payment Account 類型標籤與代號、CH Card Activity link、依 CH id 的排序、候選池與 allocation/routing priority |
| sensitive account state | 未遮罩帳號、Supply Account 餘額、before/after/delta;任何混合 HOUSE 與 Supply balance-impact 區塊整體隱藏 |
| CH workflow information | CH-... 拒絕人改回中性值、CH 拒絕原因與說明、Split Group 子單的 Supply source/CH id/CH 拒絕資訊與 history |
| actions | 代 CH confirm/reject、reclaim、指派/重新指派、候選卡查詢與任何其他供應側 workflow Action |
不涉及 CH identity 的交易事實(例如實際金額、憑證、reference 與確認時間)可依產品 contract 顯示,但欄位名稱及值不得夾帶 CH/Cardholder 字樣或識別碼。
4.2 Supplier/Gateway projection
Supplier 與 Gateway 只取得完成自身 Supply task、追蹤 assignment、實際金額與憑證所必要的 Supply fields。它們不取得 HOUSE、未指派池、其他 owner chain 或全域 routing/report 資料。Gateway 的 projection 不得因同 Supplier 關係顯示其他 Gateway 的資料。
4.3 HOUSE Card transaction projection 與操作
/houseCardTransaction/** 是獨立於 Deposit/Withdrawal workspace 的 HOUSE inbound evidence surface:只提供 page、detail、受控 raw-content read 與 manual-match command;不接受 provider payload、CSV export、dismiss/reject 或 Legacy fallback。
| principal | scope | 可讀/可操作範圍 |
|---|
SUPERADMIN | 全域,可用 Agent filter 縮小 | 可 page/detail、讀 raw、對任一 Agent transaction manual match。 |
PREFIX(Agent) | authenticated agentId | 僅 own transaction 的 page/detail、raw read 與 manual match;body/query 的 Agent selector 不得擴權。 |
SUPPLIER/GATEWAY | 無 | 不取得 HOUSE transaction list、detail、raw 或 action。 |
page/detail 只回完成匹配所需的正規化 projection;payer_account、receiver account、payer name 與 description 依 viewer profile 遮罩。raw endpoint 在 server-side scope check 後才解密,回應強制 Cache-Control: no-store,不得轉交 ChangeLog、Audit、telemetry、log、CSV 或一般 detail。manual match 從 principal 推導 Agent scope,只接受 transaction ID、Deposit order ID 與 exception reason;amount/payer account 不符時 reason required,但 Agent、HOUSE、Deposit direction、可匹配 order state 與 unmatched-order guard 永遠不可例外。
5. Withdrawal Action workspace
供應側操作只存在於 Withdrawal List 的 Action 工作台及已另行定義的 Cardholder App flow;Deposit List 不提供供應側 Action entry point。
| actor | 可執行 scope | 允許動作 | 必要 guard |
|---|
| System Admin | 全域、既有人工處理 scope | 既有人工處理與終態裁決 | 對越過一般責任鏈的動作寫入 Audit |
| Agent | own Agent | 僅既有 Agent 流程;HOUSE 僅可作 Deposit | Supply CH workflow 與所有 HOUSE outflow 一律拒絕 |
| Gateway | own Gateway、直屬 CH/Payment Account | 日常指派/重新指派、代 CH accept/reject、實際金額/憑證提交,以及 CH reject/timeout/failure 後既有 recovery | 目標與候選都必須落在 Gateway scope;不得裁決爭議終態 |
| Supplier | own Supplier 的 child Gateway/CH/Payment Account | 與 Gateway 同型、但只限緊急介入;可直接裁決 own-scope PENDING_RECOVERY recovery 終態 | 必須是已驗證 SUPPLIER principal,通過 active owner chain、case/order immutable Supplier scope、獨立 emergency authority 與 non-empty reason,並保留原 Gateway/CH 與 before/after |
Supplier 不是一般日常操作者。缺少 emergency authority、reason、可信 owner chain、case/order immutable own-Supplier scope 或候選 scope 的 command 必須 fail closed,且不得在錯誤中揭露外部訂單存在性。Supplier recovery 終態裁決必須與既有 evidence、resolution/verdict、actor、時間及 non-secret Audit 在同一 committed transaction 寫入;Audit 額外保留原 Gateway/CH 與 reason。
6. 時間、排序與日期篩選
訂單 occurrence 的 canonical storage 是 UTC,排序與歷史事實不因 viewer 而改寫。工作台只轉換 display 與 query interval:
- Supplier 與 Gateway 使用所屬 Supplier 的 immutable
businessUtcOffset;Gateway 不可自行選擇 offset。 - Agent 使用自身 immutable
businessUtcOffset。 - 本地日期 filter 先以 viewer offset 轉為 UTC half-open interval
[local-start, next-local-start),再作 indexed occurrence predicate;不得在 occurrence column 套 timezone function。 - list 的排序仍以 canonical occurrence 與穩定 tiebreaker 執行;display conversion 不得改變排序。
本 contract 不建立日/時 aggregation bucket,也不重分既有 report facts。日限額、Commission accounting month、PT Snapshot 與 R1 UTC hourly aggregate 各自維持其既有獨立 time contract。
7. CSV export 與 Audit
CSV export 必須使用與畫面同一個 scope resolver、query predicate、projection profile、遮罩與排序;因此每列與畫面同時提供 hasBoRemark,並在已授權投影中提供完整 boRemark。匯出不得因為欄位較多、序列化方式不同或直接讀 entity 而取得畫面不可見的 identity、帳號、餘額、候選池或 CH 拒絕資訊。
每一筆 CSV export 與下列 mutation 都必須與該 committed operation 寫入 non-secret Audit:System Admin 人工處理與 recovery 終態裁決、Gateway 日常 CH Action、Supplier emergency Action 與 own-scope recovery 終態裁決,以及 BO remark append(BO_ORDER_REMARK_APPENDED)。remark append Audit 最少保存 authenticated actor realm/account identity、server-derived owner scope、target order、action、前後 hasBoRemark、前後總長度、新增字元數、request id、trace id 與 committed time;不得複製完整備註。其他 Audit 仍最少保存 authenticated actor realm/account identity、server-derived owner scope、target order、action、適用時 reason、before/after safe summary、request id、trace id 與 committed time。Supplier emergency Audit 另外保存原 Gateway/CH。不得記錄 password、token、secret、完整敏感帳號、原始憑證內容或 BO remark 原文。
export Audit 只證明已交付的已遮罩 projection;不得把完整 CSV payload、secret 或未遮罩欄位另存到 Audit。
8. Legacy 與 R1 邊界
- SPay4 Supply order endpoint 不得 join、fallback 或 dual-write 至 Legacy
DepositOrder、WithdrawalOrder、PaymentChannel、PaymentChannelCard、agent_api_order_log 或 Legacy report tables。 - Legacy Deposit/Withdrawal/Daily Report endpoints 也不得改接 SPay4 Supply persistence。
SUPPLY_ACTOR 直接呼叫 Legacy daily/channel report endpoint 必須於 realm/authority guard 拒絕;不能依 Legacy RoleSet absence、menu omission 或空結果作為唯一保護。- R1 只承接 PT/Commission report;本工作台不創造 summary、trend、reconciliation 或跨角色 report API。未來若需要這些能力,必須另立 contract,先凍結 aggregation grain、owner scope、projection、masking、export 與 authority。
9. D1/W1 acceptance matrix
| 驗證情境 | 必要結果 |
|---|
| realm/owner 隔離 | Agent、Supplier、Gateway 只能取得自己的 list、detail、helper、CSV 與可用 action;猜測 order id、跨 owner filter、前端 Login Type 修改與直呼 mutation 均不洩漏存在性地拒絕。 |
| HOUSE 與 Agent projection | Agent 完整看到自己的 HOUSE 必要資料;同一 Supply order 不回 Supplier/Gateway/CH identity、未遮罩帳號、餘額 snapshot、候選池或 CH 拒絕內容。 |
| Supply source 排除 | Supplier/Gateway 均查不到 HOUSE、其他 Gateway、其他 Supplier、未指派或 owner 不可判定訂單;Deposit Action 與後端 Supply Deposit mutation 都拒絕。 |
| Withdrawal actions | Gateway 可完成自身日常 action但不可裁決爭議終態;Supplier 只有已驗證 SUPPLIER principal 通過 active owner chain、case/order immutable own-Supplier scope、emergency authority 與 non-empty reason 時,才可裁決 PENDING_RECOVERY recovery 終態並寫入完整 Audit(含原 Gateway/CH、reason、resolution/verdict、actor、時間);其餘情況不洩漏存在性地拒絕。 |
| viewer offset | 同一 UTC order 在 Agent 與 Supplier/Gateway 各依其 configured offset 顯示;各自以本地日期查詢時正確轉成 UTC half-open interval,底層 occurrence 不變。 |
| export parity | 畫面與 CSV 的資料列、欄位、遮罩與排序完全相同;hasBoRemark 與已授權的 boRemark 也完全同投影;每次 export 有 secret-safe Audit。 |
| HOUSE transaction scope | Agent 僅讀取、raw read 與人工匹配 own agent_id 的 HOUSE transaction;System Admin 才可跨 scope,Supplier/Gateway 不可存取。一般 response 遮罩帳號,raw response no-store 且完全不進 CSV/Audit/log。 |
| BO remark | 四種 BO principal 只能在既有 scope 讀取訂單備註;可用 hasBoRemark 作精確布林篩選,不能全文搜尋,Agent API、Cardholder API 與 Legacy endpoint 不新增欄位或可見性。 |
| Legacy report rejection | Supply profile 沒有 Legacy daily/channel report menu 或 endpoint authority,直接呼叫同樣拒絕。 |
10. Implementation dependencies
實作必須依 SPay4 實作就緒路線圖 完成 W0、W1、W2、B3、B4 與 B5 的 owner/account/House prerequisites 後才開始 D1/W1。現有 Legacy controllers 與目前 repository 中的 historical Stage code 都不是本 contract 的 runtime foundation;在 target order persistence、realm guard、owner chain 與 D1/W1 settlement contract 尚未完成前,不得以修改 Legacy endpoint 宣稱交付本工作台。
SPay4 BO Domain Audit Contract
本文件固定 SPay4 BO 成功資料異動的 audit 寫入契約;不授權 DDL execution、Java runtime、Audit 查詢 API、BO UI、Legacy schema migration 或既有 Legacy BO API 的回補。
1. 適用範圍與結果語意
每一個由已認證 principal 發起、成功提交且實際異動 SPay4 業務資料的 command,必須在同一 application-service transaction 寫入恰一筆 append-only domain_audit_log fact。actor、owner scope、target、request/trace identity 都必須由已認證 principal、server-side owner guard 與實際 committed aggregate 推導;client 不得提交或覆寫這些可信欄位。
下列操作不寫 audit fact:
- validation、authorization、owner-scope guard 或 persistence 失敗;rollback、optimistic-lock conflict 與沒有實際 state change 的 no-op;
- list、detail、helper、validation、export 或其他純讀取;
- 僅造成外部效果但沒有異動 SPay4 業務資料的 command;
- runtime migration、seed、snapshot import、cutover operator artifact 與其他非 BO_USER application command。
失敗操作可使用既有 security/application log 以 request ID、trace ID 關聯,但不得偽造未提交的 audit fact。
2. Fact shape 與安全邊界
每筆 fact 必須有穩定大寫 VERB_RESOURCE 格式的 action_type、BO_USER actor、target、適用 owner scope、risk_level、request ID、trace ID 與提交時間。target_type/target_id 指向本 command 的 aggregate root。
before_summary 與 after_summary 只保存可安全揭露的摘要。semantic_effects 必為非空 JSON array;每個 object 固定包含下列欄位:
{
"resourceType": "SUPPLIER",
"resourceId": 123,
"effect": "CREATED",
"summary": { "status": "ACTIVE" }
}
resourceType、resourceId 與 effect 描述實際 committed resource;summary 是可選的非 secret object。複合 aggregate command 仍只寫一筆 audit:root 是 target,全部子資源及其 ID 必須列入 semantic_effects。
任何 before/after/effect/metadata(若未來新增)均不得含 password、temporary password、password hash、TOTP seed/code、secret、credential material、ciphertext、token、authorization header、provider raw secret,或可還原上述內容的衍生資料。sanitization 或 JSON serialization 無法安全完成時必須拋出錯誤;不得省略、吞掉、改為 best-effort 或以未清理 payload 寫入。
3. Transaction 與 persistence 規則
spay4-bo 的 Spay4DomainAuditAction、Spay4DomainAuditWriteCommand、payload sanitizer 與只提供 insert 的 Spay4DomainAuditRecorder 是 runtime 的唯一權威。recorder 使用 MANDATORY propagation 加入 caller transaction;不得提供 update/delete、REQUIRES_NEW、after-commit、async outbox 或補寫流程。action、target、resource、effect 與 risk 都必須由對應 enum 輸出,禁止自由字串 append。
mutation service 在所有 domain persistence 與 server-side guard 成功後、transaction commit 前組裝 audit command。recorder persistence、sanitization 或 serialization 任一失敗時,主 aggregate 與 audit fact 必須共同 rollback。
4. BO endpoint catalog
Spay4DomainAuditEndpointCatalog 的受管範圍固定為所有 /bo/v2/** handler,以及已明定的 public auth flow。Config 與 Domain Audit query surface 均使用 /bo/v2 namespace;後者列為 READ_ONLY,不產生 domain mutation audit。每條受管 route 必須在 catalog 中有且只有一個分類:READ_ONLY、MUTATION(allowedActions) 或 EXCLUDED_SECURITY。MUTATION(actionType) 表示實際提交的 action 必須是該 route allowedActions 內的 enum 值,且 action 集不得為空。未登錄、重複登錄或無 action 的 mutation 都是測試失敗;coverage test 必須自 com.sit.spay4 的 @RestController annotation 掃描受管 route,再與 catalog 一對一比對。external HMAC API、LINE ingress、scheduler、seed 與 migration 不假裝是 BO audit command。
目前已凍結的 SPay4 BO route 如下;尚未有 Java controller 的 route 不得被假定為已實作:
| Method | Route | Classification | action type | Aggregate target |
|---|
POST | /bo/v2/suppliers | MUTATION | CREATE_SUPPLIER | SUPPLIER |
PUT | /bo/v2/auth/password | MUTATION | CHANGE_OWN_PASSWORD | authenticated USER_PRINCIPAL or SUPPLY_ACCOUNT |
POST | /bo/v2/auth/users/{userId}/password/reset | MUTATION | RESET_USER_PRINCIPAL_PASSWORD | USER_PRINCIPAL |
POST | /bo/v2/supplierAccounts/{accountId}/password/reset | MUTATION | RESET_SUPPLIER_ACCOUNT_PASSWORD | SUPPLY_ACCOUNT |
POST | /bo/v2/gatewayAccounts/{accountId}/password/reset | MUTATION | RESET_GATEWAY_ACCOUNT_PASSWORD | SUPPLY_ACCOUNT |
POST | /public/v2/password/change | MUTATION | COMPLETE_TEMPORARY_PASSWORD_CHANGE | authenticated SUPPLY_ACCOUNT |
GET | /bo/v2/access/roles | READ_ONLY | — | — |
GET | /bo/v2/access/roles/{roleId} | READ_ONLY | — | — |
GET | /bo/v2/access/permission-resources | READ_ONLY | — | — |
POST | /bo/v2/access/roles | MUTATION | CREATE_ACCESS_ROLE | ROLE |
PUT | /bo/v2/access/roles/{roleId} | MUTATION | UPDATE_ACCESS_ROLE | ROLE |
PUT | /bo/v2/access/roles/{roleId}/status | MUTATION | UPDATE_ACCESS_ROLE_STATUS | ROLE |
DELETE | /bo/v2/access/roles/{roleId} | MUTATION | DELETE_ACCESS_ROLE | ROLE |
POST | /bo/v2/access/users | MUTATION | CREATE_ACCESS_USER | USER_PRINCIPAL |
PUT | /bo/v2/access/users/{userId} | MUTATION | UPDATE_ACCESS_USER | USER_PRINCIPAL |
PUT | /bo/v2/access/users/{userId}/status | MUTATION | UPDATE_ACCESS_USER_STATUS | USER_PRINCIPAL |
DELETE | /bo/v2/access/users/{userId} | MUTATION | DELETE_ACCESS_USER | USER_PRINCIPAL |
POST | /bo/v2/supplyCardholders | MUTATION | CREATE_SUPPLY_CARDHOLDER | SUPPLY_CARDHOLDER |
PUT | /bo/v2/supplyCardholders/{cardholderId} | MUTATION | UPDATE_SUPPLY_CARDHOLDER_PROFILE | SUPPLY_CARDHOLDER |
PUT | /bo/v2/supplyCardholders/{cardholderId}/status | MUTATION | UPDATE_SUPPLY_CARDHOLDER_STATUS | SUPPLY_CARDHOLDER |
POST | /bo/v2/houseCard | MUTATION | CREATE_HOUSE_CARD | HOUSE_CARD |
PUT | /bo/v2/houseCard/{houseCardId} | MUTATION | UPDATE_HOUSE_CARD | HOUSE_CARD |
PUT | /bo/v2/houseCard/{houseCardId}/status | MUTATION | UPDATE_HOUSE_CARD_STATUS | HOUSE_CARD |
DELETE | /bo/v2/houseCard/{houseCardId} | MUTATION | DELETE_HOUSE_CARD | HOUSE_CARD |
POST | /bo/v2/paymentAccounts | MUTATION | CREATE_PAYMENT_ACCOUNT | PAYMENT_ACCOUNT |
GET | /bo/v2/config/definitions | READ_ONLY | — | — |
GET | /bo/v2/domainAudit/page | READ_ONLY | — | — |
GET | /bo/v2/domainAudit/{id} | READ_ONLY | — | — |
GET | /bo/v2/domainAudit/actions | READ_ONLY | — | — |
UPDATE_ACCESS_ROLE 的 semantic_effects[0].effect 以 UPDATED 或 PERMISSIONS_UPDATED 區分名稱與完整 grant set 異動。建立的 after_summary 保存 Role 與初始 active grants;更新保存完整 before/after snapshot 與 name、grant 的新增/移除/actions 差異;status 切換保存 status before/after。DELETE_ACCESS_ROLE 保存包含全部 active grant 的 before/after Role snapshot,after status 為 DELETED,並記錄單一 ROLE/DELETED effect 與 revokedUserCount: 0。所有 grant 只可保存 server-resolved system_method id、code 與 action,不得記錄 secret。
COMPLETE_TEMPORARY_PASSWORD_CHANGE 的 actor、Supplier/Gateway scope 與 target account id 必須由已驗證的 Supply principal 與 aggregate 推導,risk 固定為 HIGH。before/after summary 僅可包含 mustChangePassword、version 與 session epoch;semantic effect 必須標示 challenge/lock 已清除、舊 formal session epoch 已撤銷與新 formal session 已簽發,且不得包含任何 password、hash、challenge hash、TOTP、token 或 JWT。
POST /public/v2/login 是唯一 EXCLUDED_SECURITY:它沒有 authenticated principal,屬 authentication/session security flow,不能建立 domain audit fact。PUT 與 DELETE /bo/v2/config/{key} 分別依 server-derived caller scope 允許有限的 global 或 Agent override action 集合。Access User 與 Supply Cardholder route 沿用既有的一筆 audit;同值、拒絕、rollback 與 stale version 不寫第二筆。HOUSE Card target scope 只保存 aggregate 推導的 agent_id,其 safe summary 只可包含顯示名稱、Bank/Currency identity、方向啟用、card status、daily limits、version 與 soft-delete state,所有實際變更的 risk 都是 HIGH。Payment Account create target scope 由鎖定 Cardholder immutable owner chain 推導為 supplier_id、supply_gateway_id(agent_id=null);summary/effect 只含 Cardholder、Bank、Currency identity 與 account/direction status,risk 固定 HIGH。HOUSE Card 與 Payment Account 的任何 summary/effect 都不得含完整帳號、帳戶名稱、ciphertext 或 fingerprint。新增受管 route 時,必須同時新增 catalog entry、service audit、success/rollback/payload-safety test,以及 read-only 或 security exclusion proof。
5. HOUSE Card transaction 特例
已驗證 LINE ingress 寫入 house_card_transaction 時,Audit、ChangeLog、log 與 telemetry 一律不得含 raw notification plaintext/ciphertext、canonical payload、payer/receiver account、payer name 或 description。相同 provider identity 但 canonical payload hash 不同時,系統只可產生不含敏感資料的安全 conflict reason,且不得覆寫既有 transaction、將原文帶入 error 或建立看似成功的 match audit。
成功的 auto/manual settlement 必須與 transaction、Deposit order、current assignment attempt、HOUSE reservation/counter 的同一 transaction 寫入一筆 non-secret Audit。manual action type 為 MANUAL_MATCH_HOUSE_CARD_TRANSACTION,至少保存 authenticated actor、server-derived Agent scope、transaction/order/attempt/HOUSE IDs、MANUAL mode、是否有 amount/payer-account exception 與 reason presence;不得保存 exception 原文以外的敏感 payload 或任一完整帳號。auto settlement 由 internal actor 寫入相同安全摘要的 AUTO_MATCH_HOUSE_CARD_TRANSACTION。callback/outbox 僅在 transaction commit 後執行,dispatch 結果不得改寫 match evidence。
raw-content read、一般 page/detail、idempotent replay、無 candidate retry 與 payload conflict rejection 不屬 committed business mutation,不建立完整 payload audit;raw read 的 access evidence 如產品日後要求,必須另立非 payload、最小化的 security-event contract。
6. Supplier onboarding 特例
Supplier onboarding 成功時只寫入一筆 CREATE_SUPPLIER audit,target 為新 Supplier。其 semantic_effects 必須涵蓋 Supplier、Supplier account、兩筆 Supplier PT rule、DEFAULT Gateway、Gateway account、兩筆 Gateway PT rule、兩筆 direction config 與兩筆 password credential reference 的非 secret resource IDs/摘要。不得記錄任何一次性 credential material 或 provider secret。
6. Required verification
未來 W1/W2 implementation 至少必須提供:
- endpoint catalog coverage,證明每個 SPay4 BO handler 恰有一個分類,且 mutation 有有效 action type;
- 每個 mutation 的 integration/service proof:成功只產生一筆 action 相符的 fact,actor、scope、target、摘要與 effects 皆為 committed state;
- recorder 或 sanitizer fault injection,證明主 aggregate 與 audit row 都 rollback;
- payload safety test,掃描所有 JSON audit 欄位拒絕 password、TOTP、secret、ciphertext、token 與 one-time credential material;
- HOUSE auto/manual settlement 的 single safe audit、rollback 與 callback-after-commit proof,以及 raw-content/replay/conflict 的 zero-payload-audit proof;
- read-only、export、validation 與 external-only handler 的 zero-audit proof。
SPay4 Target ER Model 與 Schema 審核
結論與審核邊界
目前候選 target schema 為 14 張 Core 加 37 張 Domain,完整 inventory 51 relations。本文件僅審核文件與候選 DDL;不執行 SQL、不連線 target database,也不代表任何人工 approval。所有關係都是 logical relation,沒有 database foreign key;service transaction、owner guard、immutable snapshot、CAS/idempotency 和 committed non-secret Audit 是完整性邊界。
D1/W1 只使用 target-native agent_api_order_claim,禁止接觸 agent_api_order_log。HOUSE 是獨立 root,僅作 Deposit:Withdrawal relation、attempt、reservation、current projection 與 recovery 都不能 reference HOUSE。
Cross-relation findings
| Finding | Guard |
|---|
| polymorphic/denormalized owner snapshot | writer 先鎖定 authoritative owner chain,於同 transaction 驗證並保存 immutable redundancy。 |
| money/ledger/quota | D1/W1 使用 DECIMAL(30,10);PT/reporting 才使用 DECIMAL(38,10);不可使用浮點數或跨日搬移 quota。 |
| current pointer/history | formal Order current projection 與 attempt/reservation 同 transaction 更新;attempt 是歷史 authority。 |
| scope/projection | OrderWorkspaceScope 唯一 server-derived;list/detail/helper/CSV/action 不得各自重建或 client 選擇 owner。 |
| BO remark | 僅 formal order 保存 append-only bo_remark;has_bo_remark 是 trim 後非空的 stored generated query flag,僅在既有 scope 的 list/detail/CSV 投影可見。 |
| secret/privacy | credential、完整帳號、raw proof、token 和 BO remark 原文不進 Audit、DDL comment payload、HTML 或未授權 CSV。 |
Relation catalog
下列每一條均需有對應 service test、owner scope、nullable 及 query-shaped index 審核;「評定」不是 SQL 執行結果。
currency
- 功能與寫入責任: Currency master,由核准 master writer 寫入。
- 欄位/nullable 審核: code/status 是必要 lifecycle input;rate 為 reporting metadata。
- logical inbound/outbound relation 與 cardinality: 1:N Bank、Agent、Gateway 與 immutable Currency snapshots。
- 評定: 需以 service invariant 保證。
bank
- 功能與寫入責任: Bank master,由核准 master writer 寫入。
- 欄位/nullable 審核: Currency、code 與 active lifecycle 必須可驗證。
- logical inbound/outbound relation 與 cardinality: N:1 Currency;1:N Account/HOUSE/Agent-Bank logical reference。
- 評定: 需以 service invariant 保證。
agent
- 功能與寫入責任: Prefix/Agent master,由 BO master writer 寫入。
- 欄位/nullable 審核: immutable Currency 與 UTC offset;current callback URL authority 僅在 Agent,outbox 只能保存 terminal event 的 immutable validated URL snapshot。
- logical inbound/outbound relation 與 cardinality: 1:N allocation、HOUSE、claim、order snapshot。
- 評定: 需以 service invariant 保證。
notification_provider_source
- 功能與寫入責任: notification provider registry。
- 欄位/nullable 審核: provider type/vendor/parser profile 受 lifecycle guard。
- logical inbound/outbound relation 與 cardinality: 1:N Agent credential metadata。
- 評定: 需以 service invariant 保證。
credential_reference
- 功能與寫入責任: Agent integration non-secret metadata。
- 欄位/nullable 審核: 不能保存 ciphertext、password 或 Supply account locator。
- logical inbound/outbound relation 與 cardinality: Agent 1:N reference、reference 1:1 current secret。
- 評定: 需以 service invariant 保證。
credential_secret
- 功能與寫入責任: 專用 encrypted integration secret writer。
- 欄位/nullable 審核: ciphertext/key version 必須隔離,永不進 Audit。
- logical inbound/outbound relation 與 cardinality: N:1 credential reference。
- 評定: 需以 service invariant 保證。
external_api_replay_nonce
- 功能與寫入責任: credential-scoped external HMAC nonce replay ledger。
- 欄位/nullable 審核: 僅保存 canonical UUID v4 nonce、UTC create/expiry time;不保存 API key、secret、signature 或 request body。
- logical inbound/outbound relation 與 cardinality: credential reference 1:N、每組 credential/nonce 唯一。
- 評定: claim 使用獨立短 transaction;每分鐘固定批次清理過期 row。
agent_bank
- 功能與寫入責任: Agent-Bank availability join。
- 欄位/nullable 審核: composite identity 無額外 owner payload。
- logical inbound/outbound relation 與 cardinality: Agent N:M Bank。
- 評定: 需以 service invariant 保證。
system_group
- 功能與寫入責任: BO menu group expected-state materialization。
- 欄位/nullable 審核: name/sort/status 由 release manifest 驗證。
- logical inbound/outbound relation 與 cardinality: 1:N system method。
- 評定: 需以 service invariant 保證。
system_method
- 功能與寫入責任: BO resource/action catalog materialization。
- 欄位/nullable 審核: code/status 必須與 manifest 一致。
- logical inbound/outbound relation 與 cardinality: N:1 group,1:N role grant。
- 評定: 需以 service invariant 保證。
role
- 功能與寫入責任: BO role master。
- 欄位/nullable 審核: system scope nullable owner collision 要 fail closed。
- logical inbound/outbound relation 與 cardinality: 1:N user、role method。
- 評定: 需以 service invariant 保證。
role_method
- 功能與寫入責任: BO grant materialization。
- 欄位/nullable 審核: role/method/action/status 必須同 transaction 驗證。
- logical inbound/outbound relation 與 cardinality: N:1 role、N:1 system method。
- 評定: 需以 service invariant 保證。
user
- 功能與寫入責任: BO principal。
- 欄位/nullable 審核: scoped account uniqueness、BCrypt password hash、AES-GCM TOTP ciphertext、lock 與 session epoch 必須含 system scope guard。
- logical inbound/outbound relation 與 cardinality: N:1 role/Agent;credential state 直接由 root 持有。
- 評定: 需以 service invariant 保證。
system_config
- 功能與寫入責任: global configuration writer。
- 欄位/nullable 審核: key unique;value nullability 由 key policy 決定。
- logical inbound/outbound relation 與 cardinality: 1:N Agent override。
- 評定: 需以 service invariant 保證。
agent_system_config
- 功能與寫入責任: Agent configuration override writer。
- 欄位/nullable 審核:
(agent,key) unique 且 key 必須對應 active global key。 - logical inbound/outbound relation 與 cardinality: N:1 Agent/system config。
- 評定: 需以 service invariant 保證。
supplier
- 功能與寫入責任: Supply owner root。
- 欄位/nullable 審核: immutable login identity/offset 與 lifecycle 均 required。
- logical inbound/outbound relation 與 cardinality: 1:N Gateway、account、PT、scope snapshot。
- 評定: 需以 service invariant 保證。
supply_gateway
- 功能與寫入責任: Supplier-owned Gateway master。
- 欄位/nullable 審核: Supplier/Currency immutable,operational lifecycle required。
- logical inbound/outbound relation 與 cardinality: 1:N Cardholder、config、allocation、account。
- 評定: 需以 service invariant 保證。
supply_account
- 功能與寫入責任: Supply authenticated principal。
- 欄位/nullable 審核: owner discriminator/profile/active lifecycle、BCrypt password hash、AES-GCM TOTP ciphertext、一次性 challenge hash 與 session epoch 必須一致。
- logical inbound/outbound relation 與 cardinality: owner 1:N account;credential state 直接由 root 持有。
- 評定: 需以 service invariant 保證。
domain_audit_log
- 功能與寫入責任: committed append-only non-secret audit。
- 欄位/nullable 審核: actor/target/scope/effects required;summary 不含 secret/raw proof/full account。
- logical inbound/outbound relation 與 cardinality: N:1 committed mutation snapshot,不反向授權。
- 評定: 需以 service invariant 保證。
supply_pt_policy
- 功能與寫入責任: owner aggregate 保存 shared immediate-change entitlement。
- 欄位/nullable 審核: owner identity immutable,version 與 immediate-change entitlement 必須同一 aggregate transition。
- logical inbound/outbound relation 與 cardinality: Supplier、Gateway、Cardholder 各 1:1 policy;policy 1:N revision。
- 評定: DB unique grain 配合 service transaction、owner-chain scope 與 parent-rate invariant 保證。
supply_pt_revision
- 功能與寫入責任: 保存 policy 的 current、pending 與 immutable history revision header。
- 欄位/nullable 審核: current/pending 使用 unique active slot,history slot 為 null。
- logical inbound/outbound relation 與 cardinality: N:1 policy;revision 1:2 direction line。
- 評定: service transaction 必須完整建立兩個 direction line,並在 rollover 前驗證 lifecycle。
supply_pt_revision_line
- 功能與寫入責任: 保存原子 Deposit/Withdrawal desired state direction line。
- 欄位/nullable 審核: revision/direction unique;Cardholder 才可設定 fee 與 threshold。
- logical inbound/outbound relation 與 cardinality: N:1 revision;每筆 revision 固定 1:2 Deposit/Withdrawal lines。
- 評定: DB unique grain 與 service transaction 保證完整 direction pair。
supply_gateway_direction_config
- 功能與寫入責任: Gateway Deposit/Withdrawal amount config。
- 欄位/nullable 審核: 兩方向皆 required;
max > 0 且 min <= max。 - logical inbound/outbound relation 與 cardinality: Gateway 1:N direction config;不得跨方向 fallback。
- 評定: 需以 service invariant 保證。
supply_gateway_agent_allocation
- 功能與寫入責任: Gateway-Prefix availability mapping。
- 欄位/nullable 審核: enabled/priority 是 routing eligibility,非 owner。
- logical inbound/outbound relation 與 cardinality: Agent N:M Gateway。
- 評定: 需以 service invariant 保證。
supply_eligible_fact
- 功能與寫入責任: immutable reporting financial evidence。
- 欄位/nullable 審核: amount/owner/Currency/time snapshots required。
- logical inbound/outbound relation 與 cardinality: 1:N correction,1:1 queue work。
- 評定: 需以 service invariant 保證。
supply_eligible_fact_correction
- 功能與寫入責任: immutable reporting delta。
- 欄位/nullable 審核: signed delta/idempotency/original fact required。
- logical inbound/outbound relation 與 cardinality: N:1 fact,投影回原 bucket。
- 評定: 需以 service invariant 保證。
supply_eligible_fact_projection_queue
- 功能與寫入責任: reporting projection work state。
- 欄位/nullable 審核: work identity/CAS/retry projection 必須非 financial authority。
- logical inbound/outbound relation 與 cardinality: 1:N failure attempt、watermark/aggregate projection。
- 評定: 需以 service invariant 保證。
supply_eligible_fact_projection_failure_attempt
- 功能與寫入責任: immutable projection failure evidence。
- 欄位/nullable 審核: sanitized diagnostic;queue-local sequence unique。
- logical inbound/outbound relation 與 cardinality: N:1 projection queue。
- 評定: 需以 service invariant 保證。
supply_aggregate_projection_watermark
- 功能與寫入責任: reporting projection progress。
- 欄位/nullable 審核: cursor 是 operations metadata,不是 completeness authority。
- logical inbound/outbound relation 與 cardinality: N:1 logical source partition。
- 評定: 需以 service invariant 保證。
supply_financial_hourly_aggregate
- 功能與寫入責任: shared reporting read model。
- 欄位/nullable 審核:
DECIMAL(38,10) eligible amount 與 aggregate grain required。 - logical inbound/outbound relation 與 cardinality: N:1 fact/correction projection bucket。
- 評定: 需以 service invariant 保證。
supply_cardholder
- 功能與寫入責任: Gateway-owned CH master。
- 欄位/nullable 審核: immutable Gateway/Supplier redundancy;online/drain status 不可由 client 宣告。
- logical inbound/outbound relation 與 cardinality: Gateway 1:N CH;CH 1:N session/account/order snapshot。
- 評定: 需以 service invariant 保證。
supply_cardholder_session
- 功能與寫入責任: CH App session lifecycle。
- 欄位/nullable 審核: session epoch/status/expiry required;無 token plaintext。
- logical inbound/outbound relation 與 cardinality: CH 1:N session;session 1:N device token generation。
- 評定: 需以 service invariant 保證。
supply_cardholder_device_token
- 功能與寫入責任: CH device registration fingerprint。
- 欄位/nullable 審核: fingerprint/status/times required;token 原值不保存。
- logical inbound/outbound relation 與 cardinality: N:1 session,notification delivery 選 current generation。
- 評定: 需以 service invariant 保證。
supply_cardholder_notification
- 功能與寫入責任: channel-specific CH
IN_APP history 或 FCM delivery/audit outbox;Legacy notification pipeline 不寫入此 relation。 - 欄位/nullable 審核: immutable Cardholder/Supplier/Gateway snapshot、catalog-derived category/type 與 content/source/idempotency snapshot required;
read_time 僅 IN_APP 可用,FCM 必為 null;retry 不可覆寫 immutable history。 - logical inbound/outbound relation 與 cardinality: N:1 CH;Supplier/Gateway snapshot 在建立時由 CH owner chain 驗證,只作 history query scope,不取代 authorization authority。
- 評定: 需以 service invariant 與 channel-scoped idempotency 保證;schema 不建立 constraint。
payment_account_application
- 功能與寫入責任: CH account application/review。
- 欄位/nullable 審核: encrypted account/fingerprint;review actor/time 可空直到裁決。
- logical inbound/outbound relation 與 cardinality: N:1 CH,approved application 可建立 account。
- 評定: 需以 service invariant 保證。
payment_account
- 功能與寫入責任: CH Payment Account root。
- 欄位/nullable 審核: immutable CH/Gateway/Supplier redundancy、encrypted identity、balance/reserved amount required。
- logical inbound/outbound relation 與 cardinality: 1:N ledger/counter/reservation;Deposit/Withdrawal candidate。
- 評定: 需以 service invariant 保證。
payment_account_balance_log
- 功能與寫入責任: append-only Payment Account ledger。
- 欄位/nullable 審核: before/delta/after、idempotency required;assignment attempt 可空,沒有 HOUSE transaction pointer。
- logical inbound/outbound relation 與 cardinality: N:1 Payment Account,必要時 N:1 assignment attempt;不得連至 HOUSE transaction。
- 評定: 需以 service invariant 保證。
house_card
- 功能與寫入責任: Prefix-owned HOUSE root。
- 欄位/nullable 審核: Agent/Currency/Bank/account identity immutable;Deposit min/max required;
withdrawal_enabled 不授權 Withdrawal。 - logical inbound/outbound relation 與 cardinality: 1:N Deposit counter/reservation/Deposit attempt;無 Withdrawal outbound relation。
- 評定: 需以 service invariant 保證。
payment_account_daily_counter
- 功能與寫入責任: Payment Account daily direction quota。
- 欄位/nullable 審核: date/direction/count/amount/version required。
- logical inbound/outbound relation 與 cardinality: N:1 Payment Account;counter lock 對 reservation 1:N。
- 評定: 需以 service invariant 保證。
supply_cardholder_daily_assignment_counter
- 功能與寫入責任: CH fairness assignment counter。
- 欄位/nullable 審核: immutable owner redundancy/date/count required;無 Agent key。
- logical inbound/outbound relation 與 cardinality: N:1 CH/Gateway/Supplier.
- 評定: 需以 service invariant 保證。
agent_api_order_claim
- 功能與寫入責任: Agent API target-native idempotency/association。
- 欄位/nullable 審核: Agent/direction/order/fingerprint required;兩個 formal-order pointer 互斥。
- logical inbound/outbound relation 與 cardinality: claim 1:1 Deposit 或 Withdrawal formal order;無 Legacy fallback。
- 評定: 需以 service invariant 保證。
supply_deposit_order
- 功能與寫入責任: Deposit formal aggregate;BO remark 只可由已授權、既有 scope 內的 append command 寫入。
- 欄位/nullable 審核: requested
DECIMAL(30,10) 與建立時正規化的 immutable payer_account required;actual/current projection 可空;bo_remark nullable、單次/累積上限為 500/4,000 字元且僅可用 ; 追加;has_bo_remark 是不可直接寫入的 stored generated flag;HOUSE 僅此 direction 合法。 - logical inbound/outbound relation 與 cardinality: claim 1:1 order;order 1:N attempt/proof/reservation,並至多 1:1 被
house_card_transaction.matched_deposit_order_id 結案;remark 不建立 transaction 或其他 evidence relation。 - 評定: 需以 scope、CAS、append-only 與同 transaction Audit service invariant 保證。
supply_withdrawal_order
- 功能與寫入責任: Withdrawal formal aggregate;BO remark 只可由已授權、既有 scope 內的 append command 寫入。
- 欄位/nullable 審核: review/disposition fields 可空至人工裁決;
bo_remark nullable、單次/累積上限為 500/4,000 字元且僅可用 ; 追加;has_bo_remark 是不可直接寫入的 stored generated flag;HOUSE pointer 不存在。 - logical inbound/outbound relation 與 cardinality: claim 1:1 order;order 1:N attempt/proof/recovery;HOUSE transaction 沒有 Withdrawal relation,remark 不建立 evidence relation。
- 評定: 需以 scope、CAS、append-only 與同 transaction Audit service invariant 保證。
supply_order_assignment_attempt
- 功能與寫入責任: immutable assignment history。
- 欄位/nullable 審核: owner/time/deadline/idempotency required;Payment Account/HOUSE references mutually exclusive,Withdrawal HOUSE rejected。
- logical inbound/outbound relation 與 cardinality: N:1 formal order;1:N reservation/proof/transaction.
- 評定: 需以 service invariant 保證。
agent_callback_outbox
- 功能與寫入責任: Deposit terminal event 的 durable Agent callback intent;entity/DAO/DDL 已存在,不據此宣稱 producer 或 dispatcher 已啟用。
- 欄位/nullable 審核: Deposit order、Agent、event、validated URL snapshot、credential reference、safe v2 payload、idempotency 與 dispatch state required;retry/lease/provider result/terminal time 依 dispatch state nullable,不保存 signing material、payer、proof 或 raw content。
- logical inbound/outbound relation 與 cardinality: N:1 Deposit order、N:1 Agent、N:1 credential reference;order/event 與 idempotency key 各自 unique,沒有 Withdrawal 或 HOUSE owner relation。
- 評定: 需以 terminal intent 原子性、immutable snapshot、CAS/lease、retry 與 sanitized provider evidence 的 runtime test 驗證;本文件僅盤點既有 schema,不核准 dispatch。
payment_account_reservation
- 功能與寫入責任: Payment Account capacity/balance reservation。
- 欄位/nullable 審核: amount/status/reserved time required;released/consumed time nullable by state。
- logical inbound/outbound relation 與 cardinality: N:1 Account/order/attempt;review-held 是 projection 非 status。
- 評定: 需以 service invariant 保證。
supply_order_proof
- 功能與寫入責任: proof metadata writer。
- 欄位/nullable 審核: content hash/path/uploader/status required;raw content 不保存。
- logical inbound/outbound relation 與 cardinality: N:1 formal order/attempt。
- 評定: 需以 service invariant 保證。
house_card_transaction
- 功能與寫入責任: 已驗證 LINE source 接收的 immutable HOUSE Card inbound Deposit evidence;必須先持久化後才執行 matching。
- 欄位/nullable 審核: Agent/source/provider identity、canonical payload hash、正規化 payer/receiver snapshot、amount
DECIMAL(30,10)、time、direction token 與 AES-GCM raw ciphertext required;Bank/HOUSE/matched order/attempt 與 match actor 僅在已解析或結案時可有值。 - logical inbound/outbound relation 與 cardinality: N:1 Agent、N:1 provider source、N:1 HOUSE、至多 1:1 Deposit order/attempt;provider identity UQ 供 replay,matched order UQ 防止雙重結案;沒有 Cardholder、Payment Account、Withdrawal 或 Legacy transaction relation。
- 評定: 需以 source-to-Agent scope、exact HOUSE resolution、payload-hash conflict rejection、1/3/5 分鐘 retry、鎖定 settlement、raw read no-store 與 scope/Audit service invariant 保證。
supply_withdrawal_recovery_case
- 功能與寫入責任: System Admin 全域或 Supplier own-scope emergency duplicate/late payout recovery。
- 欄位/nullable 審核: evidence/owner/outflow required;resolution fields nullable until human close。
- logical inbound/outbound relation 與 cardinality: N:1 Withdrawal order;attempt snapshots link original/current work.
- 評定: 需以 service invariant 重驗已驗證
SUPPLIER principal、active immutable owner chain、case/order own-Supplier scope、既有 emergency authority 與 non-empty reason;任一失敗不洩漏存在性地拒絕,Gateway 不得裁決,裁決與 evidence/resolution/verdict/actor/time/non-secret Audit 同 transaction。
house_card_daily_counter
- 功能與寫入責任: HOUSE Deposit daily quota counter。
- 欄位/nullable 審核: HOUSE ID/date/count/amount/version required;no Withdrawal columns。
- logical inbound/outbound relation 與 cardinality: N:1 HOUSE; 1:N Deposit quota reservation。
- 評定: 需以 service invariant 保證。
house_card_deposit_reservation
- 功能與寫入責任: HOUSE Deposit quota reservation evidence。
- 欄位/nullable 審核: requested amount required; confirmed actual/release/consume time nullable by lifecycle。
- logical inbound/outbound relation 與 cardinality: N:1 HOUSE/Deposit order; 1:1 Deposit assignment attempt。
- 評定: 需以 service invariant 保證。
Required verification
- README、DDL、ER/portal、roadmap relation inventory 都是 51(14 Core + 37 Domain)。
- 每張 DDL table 都有上述寫入 owner、nullable semantics、unique/index、Audit/test responsibility。
- 無 FK、無 seed/Legacy SQL、無
agent_api_order_log 或 supply_transaction interaction;所有 order money 與 HOUSE transaction 皆 DECIMAL(30,10),PT/reporting 為 DECIMAL(38,10)。 - Deposit ER 顯示 HOUSE quota;Withdrawal ER 只顯示 Payment Account、review-held、
NOT_TRANSFERRED 改派及 duplicate recovery。 - 兩種 formal order 各有 nullable append-only
bo_remark 與 derived has_bo_remark;僅既有 OrderWorkspaceScope 內的 BO list/detail/CSV 可見,Audit 不保存原文,且備註不建立 transaction evidence relation。 house_card_transaction 只由已驗證 LINE adapter 寫入;raw notification 僅為 ciphertext,Agent 只讀 own scope,System Admin 可跨 scope,且一般 response/Audit/log/CSV 一律不含 raw content。
SPay4 表格完整性審核
結論與 boundary
current contract 的盤點基線是 14 Core relation 與 37 Domain relation,合計 51 relations。user 與 supply_account 現直接保存 credential state,候選 DDL、README 與 current contract inventory 一致;審核不執行或核准 target DDL、seed、runtime 或 database。
本次無待處理 DDL/contract 漂移(本次僅記錄);portal、Target ER 與 relation catalog 都已改為 direct-principal 51-relation target。user 的 nullable system scope unique grain 是保留的後續 DDL/contract 決策。
判定定義
- 符合:候選 DDL relation、identity/unique grain、owner 或 snapshot、必要欄位群、index intent 與 current flow 相容。
- 文件/HTML 過期:current source 以外的 portal、diagram 或 reader 曾使用舊 count、舊 relation 或 HOUSE outflow 內容;本輪只修文件產物。
- DDL/contract 漂移(本次僅記錄):candidate DDL 與已接受 contract 不一致時使用;目前沒有已確認項目。
- 需後續決策:需要獨立 contract/DDL/Operator 審核,不能由本文件採納。
全域發現
| ID | 判定 | 證據與影響 | 後續處置 |
|---|
| DOC-01 | 文件/HTML 過期 | 舊 portal/SQL 初始化頁曾顯示已淘汰的 relation inventory。 | 由 portal generator 統一輸出 current 14 Core + 37 Domain = 51。 |
| DOC-02 | 文件/HTML 過期 | 舊 Master/auth ER 使用 shared/provider credential relation。 | 所有 current ER 只使用 direct user/supply_account credential state。 |
| DOC-03 | 文件/HTML 過期 | 舊 Withdrawal/architecture 圖將 HOUSE 描述為 Withdrawal candidate、balance 或 reservation gate。 | Withdrawal 只保留 Payment Account reservation;HOUSE 在候選前排除。 |
| DEC-01 | 需後續決策 | user 的 uk_user_agent_account(agent_id, account) 無法在 MySQL 的 nullable agent_id 下保證 system-scope account 唯一。 | 另案決定 DDL unique grain 或等價 fail-closed enforcement;本輪不改 DDL。 |
Contract 51-relation inventory
下表逐列比對 contract relation 是否存在於 current DDL、identity/unique grain、owner/snapshot、必要欄位群、index 意圖與流程使用點。欄位及 key 的逐項證據以 SQL 為準;完整 nullable、cardinality 與 service guard 請見 Target ER 審核。
| Layer | Relation | Identity/unique grain | Owner/snapshot | 必要欄位群與 index 意圖 | 流程使用點 | 判定 |
|---|
| Core | currency | id、code unique | master | code/status/rate;status lookup | master、Gateway Currency | 符合 |
| Core | bank | id、code unique | Currency reference | currency/status;Currency-status lookup | bank pair、Account、HOUSE | 符合 |
| Core | agent | id、prefix unique | immutable Currency/offset | currency、UTC offset、status;Currency-status lookup | Prefix、claim、HOUSE scope | 符合 |
| Core | notification_provider_source | id、type+vendor unique | provider registry | parser profile/status;active lookup | LINE ingress | 符合 |
| Core | credential_reference | id、Agent+kind+scope unique | Agent integration | kind/source/scope/lifecycle;Agent/provider lookup | OGP/LINE credential | 符合 |
| Core | credential_secret | id、reference/API key/fingerprint unique | reference current secret | cipher/key version/rotation;unique current lookup | Agent integration secret | 符合 |
| Core | agent_bank | Agent+Bank composite PK | availability pair | two logical IDs;Bank reverse index | bank-pair eligibility | 符合 |
| Core | system_group | id | BO catalog | name/sort/status;status-sort lookup | BO menu | 符合 |
| Core | system_method | id、code unique | system group | group/code/status;group-status-sort lookup | resource/action policy | 符合 |
| Core | role | id | Agent or system scope | nullable agent/status;Agent-status lookup | BO authorization | 符合 |
| Core | role_method | id、role+method unique | role grant | action/status;method-status reverse lookup | BO authorization | 符合 |
| Core | user | id、Agent+account candidate grain | BO principal | role/account/status;role-status lookup | BO login | 需後續決策(DEC-01) |
| Core | system_config | id、key unique | global config | key/value/status;status lookup | configuration default | 符合 |
| Core | agent_system_config | id、Agent+key unique | Agent override | config ref/key/value;default-status lookup | Agent configuration | 符合 |
| Domain | external_api_replay_nonce | id、credential+nonce unique | validated credential reference | canonical nonce/UTC expiry;expiry index | External Order HMAC replay guard | 符合 |
| Domain | supplier | id、code/login ID unique | Supply root | immutable login/offset/lifecycle;status lookup | onboarding, owner chain | 符合 |
| Domain | supply_gateway | id、Supplier+code unique | Supplier/Currency | owner/Currency/status;Supplier-Currency lookup | onboarding, routing | 符合 |
| Domain | supply_account | id、account unique | Supplier or Gateway owner | owner type/id/profile/lifecycle;owner-lifecycle lookup | Supply login | 符合 |
| Domain | domain_audit_log | append-only id | actor/target/scope snapshots | effect object/risk/time;scope/target/action indexes | committed mutation evidence | 符合 |
| Domain | supply_pt_policy | id、owner unique | Supplier/Gateway/Cardholder | immediate entitlement/version;owner-status lookup | onboarding, PT mutation | 符合 |
| Domain | supply_pt_revision | id、policy+active slot unique | PT policy | status/effective month/version;rollover lookup | current/pending/history lifecycle | 符合 |
| Domain | supply_pt_revision_line | id、revision+direction unique | PT revision | rate/base fee/threshold;revision lookup | atomic Deposit/Withdrawal desired state | 符合 |
| Domain | supply_gateway_direction_config | id、Gateway+direction unique | Gateway | min/max/version | routing limits | 符合 |
| Domain | supply_gateway_agent_allocation | id、Gateway+Agent unique | availability only | enabled/priority;Agent-first available lookup | candidate Gateway read | 符合 |
| Domain | supply_eligible_fact | id、source identity unique | immutable owner/Currency/time snapshots | amount/UTC/month/direction;aggregate and Gateway-time indexes | report fact acceptance | 符合 |
| Domain | supply_eligible_fact_correction | id、correction identity unique | original fact reference | signed delta/reason;original-fact lookup | report correction | 符合 |
| Domain | supply_eligible_fact_projection_queue | id、work type+identity unique | projection state | retry/CAS/time;candidate queue index | projector recovery | 符合 |
| Domain | supply_eligible_fact_projection_failure_attempt | id、queue+sequence unique | immutable failure evidence | classification/diagnostic/time;queue-time lookup | projector failure | 符合 |
| Domain | supply_aggregate_projection_watermark | id、source+partition unique | progress only | cursor/version | projector progress | 符合 |
| Domain | supply_financial_hourly_aggregate | id、UTC/month/owners/Currency/direction unique | published read model | amount/count/revision;owner report indexes | report/Commission query | 符合 |
| Domain | supply_cardholder | id、username unique | immutable Gateway/Supplier | lifecycle/online state;Gateway routing index | Cardholder routing | 符合 |
| Domain | supply_cardholder_session | id | Cardholder | epoch/status/expiry;active-session lookup | Cardholder session | 符合 |
| Domain | supply_cardholder_device_token | id、fingerprint unique | session generation | fingerprint/lifecycle;session-status lookup | device delivery | 符合 |
| Domain | supply_cardholder_notification | id、idempotency key unique | Cardholder/Supplier/Gateway snapshots | channel/dispatch tuple;history/FCM indexes | notification history/delivery | 符合 |
| Domain | payment_account_application | id | Cardholder request | encrypted identity/review;owner/status and fingerprint indexes | Account application | 符合 |
| Domain | payment_account | id、owner+Bank+Currency+fingerprint unique | Cardholder/Gateway/Supplier | balance/reserved/lifecycle;route/scope indexes | Deposit/Withdrawal candidate | 符合 |
| Domain | payment_account_balance_log | id、idempotency unique | Payment Account | before/delta/after;account/attempt indexes | Account ledger | 符合 |
| Domain | house_card | id、active identity unique | immutable Agent/Currency/Bank | Deposit bounds/enabled;Deposit route index | Deposit only | 符合 |
| Domain | payment_account_daily_counter | id、Account+date+direction unique | Payment Account quota | used/reserved amount/count;route index | Payment Account reservation | 符合 |
| Domain | supply_cardholder_daily_assignment_counter | id、Cardholder+date unique | immutable Supplier/Gateway snapshots | count/version;Supplier/Gateway fairness indexes | Cardholder fairness | 符合 |
| Domain | agent_api_order_claim | id、Agent+direction+order unique | immutable Agent API owner | fingerprint/formal-order pointer;direction pointers | target-native claim | 符合 |
| Domain | supply_deposit_order | id、claim unique | Agent/owner/current projection snapshots | payer/amount/status/remark;Agent/Supplier/Gateway workspace indexes | Deposit formal order | 符合 |
| Domain | supply_withdrawal_order | id、claim unique | Agent/owner/current projection snapshots | amount/status/review/remark;Agent/Supplier/Gateway workspace indexes | Withdrawal formal order | 符合 |
| Domain | supply_order_assignment_attempt | id、order+attempt and idempotency unique | immutable assignment snapshots | direction/owner/date/fulfilment; deadline/owner indexes | routing/reassign history | 符合 |
| Domain | agent_callback_outbox | id、order+event 與 idempotency key unique | immutable Deposit/Agent/credential reference、URL snapshot | safe v2 payload、dispatch CAS/lease/retry;pending/lease/Agent indexes | Deposit terminal callback intent | 符合(schema;runtime 待驗證) |
| Domain | payment_account_reservation | id、order+attempt unique | immutable Account/Supply/Agent snapshots | amount/state/times;Account/Gateway indexes | Payment Account reserve/release/consume | 符合 |
| Domain | supply_order_proof | id、order+content hash unique | order/attempt | path/hash/uploader/status;attempt lookup | proof metadata | 符合 |
| Domain | house_card_transaction | id、provider transaction and matched Deposit order unique | derived Agent/HOUSE/Deposit snapshots | inbound ciphertext/match; Agent/HOUSE matching indexes | LINE HOUSE Deposit evidence | 符合 |
| Domain | supply_withdrawal_recovery_case | id | Withdrawal order/attempt snapshots | evidence/disposition/actor; order/status lookup | Withdrawal duplicate/late recovery | 符合 |
| Domain | house_card_daily_counter | id、HOUSE+date unique | HOUSE Deposit quota | used/reserved amount/count;date route index | HOUSE Deposit quota | 符合 |
| Domain | house_card_deposit_reservation | id、attempt unique | HOUSE/Deposit/attempt snapshots | requested/confirmed/lifecycle;HOUSE/order indexes | HOUSE Deposit reserve | 符合 |
不把文件發現當 schema 修正
本表的「符合」不代表 schema 已執行或 runtime 已完成;它只表示該列目前沒有已確認的 relation 漂移。任何採納 DEC-01 或日後 DDL/contract 漂移,必須另立審核,逐一取得 contract、exact SQL、DBA、Operator 與 target identity approval,再做 read-back。不得以本文件、portal、diagram 或 validator 取代該程序。
驗證
ddl.sql 的 CREATE TABLE inventory 必須為 14 Core + 37 Domain,聯集為 51 且無重複。- current portal、SQL 初始化頁、Target ER 與本 audit 都必須顯示 14 + 33 = 47。
- current HTML/source 不得出現
superseded Supply actor relation、29/14 schema count、或 HOUSE outflow candidate/reservation path。 - Deposit/Withdrawal Contract 與 Withdrawal diagram 必須明確維持「Withdrawal only Payment Account」;HOUSE 僅在 Deposit、LINE inbound evidence 與 HOUSE Deposit quota/reservation 出現。
API, Table and Function Impact Map
Configuration Reference Constraint Impact — ADR-0302
system_config需Global UQ (key),agent_system_config需Agent UQ (agent_id, key);全部keys適用。這是SPAY4 target schema與concurrent-write驗證責任,不改House fallback、40053或既定error data。writer API 固定為 caller-conditional 的 /bo/v2/config:System Admin 寫 global,Agent caller 只寫 own override;collision 回 UNIQUE_KEY_EXISTS。每個成功且實際變更的設定 row 與一筆 non-secret domain_audit_log 在同一 transaction 提交,action/target/scope 由 server 推導;同值 no-op、拒絕與 rollback 不寫 audit,config value 也不保存至 audit。00-core-schema.sql schema read-back 必須確認兩個 Config unique key 均已存在。
Why It Matters
新架構不是單純增加一批BO CRUD。Agent API path、HMAC與既有response shape維持,但Deposit/Withdrawal Create的bank fields與routing eligibility會調整;Cardholder App要保留40條external paths,同時把目前只認Legacy id的JWT、facade client與BO internal mapping改成dual-model。若未先固定這些seam,直接用舊spec的83條新API或29張table估工,會低估parser/contract、adapter、House balance authority與regression範圍。
Impact Legend
| Level | Meaning | Implementation gate |
|---|
| Critical | external path穩定,但request contract、identity、routing或money state會改變 | contract regression、transaction與negative authorization tests必須先定義 |
| High | 新BO domain API與新table直接對應,會形成後續routing依賴 | B0凍結route/field/role/error matrix後才實作 |
| Medium | read projection、background worker或共用master需增加Supply branch | 必須證明Legacy branch不受影響 |
| Deferred | 此階段只保留contract與table設計,不進第一個backend UAT cut | 由D1/W1/R1/A1另行開工 |
Executive Impact Matrix
| API surface | Current inventory | Contract action | Primary tables | Function/component seam | Feature impact | Level/slice |
|---|
| Agent API v1 Order | 4 | path、HMAC、response保持;Create驗證 direction-specific bank pair;target-native claim 固定 idempotency,HOUSE 僅可參與 Deposit,絕不讀取或回退 Legacy order log | target-native agent_api_order_claim;D1/W1 Order/attempt/reservation/counter/recovery tables | AgentApiOrderController、DepositOrderService.createV1/findV1、WithdrawalOrderService.createV1/findV1、AgentApiV1OrderSupport | Deposit/Withdrawal create、query、idempotency、bank validation、no-candidate、callback | Critical;D1/W1 |
| Cardholder external | 40 | 全部path維持且都需security/session regression;bank master維持共用read,其餘依subject model dispatch | B3/B4、D1/W1、R1 tables;existing bank/currency | controllers、CardholderJwtService、CardholderJwtAuthenticationFilter、CardholderSecurityContext、11個RestCardholder*Client | login、cards、orders、notifications、reports、proof | Critical;A1+各domain slice |
| BO Cardholder internal | 41 actual | 保留/internal/cardholder/**給Legacy;新增versioned subject contract後再接Supply adapter | 同Cardholder external | 14個internal controllers(含notification package)、domain services、session validation | external 40條的BO facade backing+session validation | Critical;A1 |
| Supply actor/master BO | exact count未凍結 | 新namespace,不共用Legacy permissions;Agent-bound Gateway read 固定為 GET /bo/v2/agent/supplyGateways | B1/B2共6張新table+agent.business_utc_offset/supplier.business_utc_offset;gateway.currency_id屬新Gateway master | planned Supply auth/policy/menu、Gateway/allocation/config services | Supply login、MFA、menu、Gateway與 Agent-first cross-Supplier enabled binding read | High;B1/B2 |
| Supply Cardholder/Payment Account BO | exact count未凍結 | 新namespace與Gateway-owner-scoped authorization | B3/B4共7張planned table;不含membership table | planned Cardholder owner/session、application/review/account/balance services | 第一階段主要後台操作 | High;B3/B4 |
| House Card BO | exact count未凍結 | 只做master;不得提供本地假balance、manual Withdrawal route或override action | house_card | planned House master service | Prefix House account管理、Deposit readiness;W1另接外部balance authority | High;B5 |
| HOUSE LINE transaction BO | 4 planned operations | /houseCardTransaction/** 僅 page、detail、受控 raw read、manual match;LINE payload 僅由 ingress adapter 接收 | house_card_transaction、Deposit order/attempt/HOUSE reservation/counter | planned LINE ingress adapter、House matcher、同一 settlement service、scope resolver | verified evidence、idempotent replay/conflict、1/3/5 retry、Agent own scope/System Admin cross scope | Critical;D2 |
| Deposit/Withdrawal BO | deferred route freeze | 新 Supply Order endpoint 只提供 Deposit/Withdrawal list、detail、同投影 CSV 與 Withdrawal Action workspace;不得新增 summary/dashboard/跨角色 report API。Legacy /paymentChannel/**與 Order persistence不接Supply,Supply actor 也沒有 Legacy daily/channel report authority | D1/W1共11張 order/routing relation+target-native claim;HOUSE Deposit quota 另有兩張 relation | planned router、assignment、proof、settlement、recovery、OrderWorkspaceScope resolver | 派單、accept/reject、proof、expiry、manual recovery、owner-scoped inquiry/export | Critical;D1/W1;詳見 Order Workspace Contract |
| Commission/Reports | Report UTC hourly boundary、shared aggregate topology、numeric、late-fact、existing fact immutable correction、projector cursor、controlled rebuild、per-fact atomic application、after-commit targeted attempt+Scheduler bounded recovery、same-transaction claim/apply、separate immutable failure attempts+queue current state、queue-local committed failure sequence、每分鐘UTC整分recovery cadence、logical eligibility time+queue identity candidate order、每fire最多50 candidate IDs、Java-only 5-total-attempt retry schedule、explicit deterministic early-park classification、retry exhaustion park+audited operator requeue、available-facts query-time latest Commission、amounts-only response、per-target latest committed reads與真正計算錯誤的all-or-nothing Report failure已定 | Supplier/Gateway/Cardholder兩方向Commission依Agent/Supplier matched configured zone分月;target interval內目前已接受facts直接參與計算,資料缺漏或coverage未知只是不計入,不阻擋Report、不標partial,也不需要activation boundary;各target可讀實際DB statement執行時的latest committed aggregate,同一request不保證point-in-time snapshot;一般Report與Commission Report共同讀取Hourly Financial Aggregate,不拆三張actor tables | 保留rule、Eligible Fact Correction、1張shared hourly aggregate candidate與1張separate immutable failure-attempt candidate;ADR-0209排除current result、scheduled result attempt與所有paid/unpaid/settlement/recovery/disposition persistence;projection failure attempt只屬operations/retry evidence;ADR-0214也不新增snapshot table/token;ADR-0220排除retry config/seed persistence;ADR-0221不新增classification config;ADR-0222排除queue JSON history與latest-failure-only overwrite;ADR-0223固定queue-local committed failure sequence,ADR-0224固定每分鐘UTC整分recovery cadence,ADR-0225固定logical eligibility time+queue identity candidate order,ADR-0226固定每fire最多50 IDs且no-op/loser不退回slot;ADR-0227固定每fire 30秒soft execution budget,從第一次candidate query前以monotonic elapsed time起算;exact technical primary key/columns/index仍待決;2張Reconciliation candidates仍待決 | planned hourly projector、query-time Commission calculator與report components;每筆work在同一per-fact transaction取得ownership/status guard並共同commit identity/delta/applied evidence/queue completion;source commit後targeted attempt與Scheduler recovery共用projector,recovery每1分鐘於UTC整分鐘第0秒觸發並使用每fire最多50 IDs/30秒soft execution budget;budget從第一次candidate query前以monotonic elapsed time起算,active per-fact/failure-record transaction不hard-cancel;nextRetryTime是最早eligibility,HTTP delivery retry不增加projector attempt count;automatic-selectable due rows依eligibility time ASC、queue identity ASC admit,queue identity只作tie-break且order不保證application/completion順序;每個logical fire最多admit 50 IDs,no-op/loser不退回slot、same-fire delivery retry不取得新budget且targeted不消耗cap;只有guard winner且實際進入projector才消耗attempt,targeted/recovery loser與fetch-only candidate不計數;application rollback後以獨立短transaction append immutable failure attempt並更新queue current state,兩者共同commit且不得推進cursor;Scheduler candidate query不join history。Failure attempt以queue identity + committed failure sequence作queue-local unique ordinal,sequence在queue guard下由committed count加1取得,未commit不占用durable ordinal。5 total attempts包含initial execution,failures 1/2/3/4後分別延遲1/5/15/60分鐘,failure 5直接park且沒有180-minute fallback;retry values固定於projector-owned Java policy,不讀取Global/tenant/Spring property/environment override;三個explicit domain categories可在attempt 1至4直接park,deadlock/timeout/temporary infrastructure/unknown/classifier failure仍retry;operator requeue不重設history,只提供一次額外執行機會且不修改attempt rows;exact signal、lock/attempt technical primary key/columns/index、source inventory/cursor/correction/rebuild schema、overlap/non-reentry未定前不得實作 | Commission Report query加總目前已接受的latest hourly eligible amount、套完整target公式並round一次;later fact在後續query反映。Business response不回freshness/completeness/partial/projector progress。只有invalid Currency metadata、numeric overflow或missing applicable PT rule等真正計算錯誤才使整份Report不回amounts;mixed committed read times不是failure;獨立signed deltas可不依queue順序套用但cursor不得skip gap;NEEDS_ATTENTION不自動選取且不讓cursor skip,operator requeue保留exhausted history、只增加一次執行機會,失敗立即append新attempt並re-park,SQL只作break-glass;failure-record transaction失敗時attempt與queue update都rollback且queue仍可重試;bounded fetch不是batch commit,SKIP LOCKED/claim-token/lease只由G6 evidence gate;classification不得依exception message且diagnostic必須secret-safe;existing fact correction回投原canonical bucket,normal history不等full-scope coverage,controlled rebuild只原子替換已上線scope;不由Scheduler materialize,不處理會計付款與對帳 | High;R1 |
| Scheduler/provider ingress | trigger paths盡量不變 | trigger仍只叫BO;已驗證 LINE ingress 建立 immutable HOUSE evidence,commit 後才呼叫 matcher | Order/house_card_transaction/report tables;existing outbox/task tables | BO worker、LINE adapter/matcher、callback projector、report trigger | matching、1/3/5 retry、callback、expiry、daily/monthly jobs | Medium;D1/W1/D2/R1 |
Existing Agent API — Four Paths
| Method/path | Current call | Supply change | Writes/reads after cutover | Affected behavior |
|---|
POST /v1/agentApi/depositOrder/assign | DepositOrderService.createV1(...) | payerBankCode/payerBankName至少一個;payerAccount 正規化後 immutable;Deposit flat pool 為合格 HOUSE+Payment Account,request bank 不要求與候選 bank 相同 | agent_api_order_claim、supply_deposit_order、attempt/reservation/counter/HOUSE quota;HOUSE LINE settlement 才寫 house_card_transaction/outbox | bank validation、no-candidate review、idempotency、Deposit fulfillment policy |
GET /v1/agentApi/depositOrder/{agentOrderId} | DepositOrderService.findV1(...) | 依 target-native claim/formal order 投影 canonical response | agent_api_order_claim+supply_deposit_order | query parity、status mapping、callback snapshot |
POST /v1/agentApi/withdrawalOrder | WithdrawalOrderService.createV1(...) | receiverBankCode/receiverBankName至少一個;候選僅限合格 Payment Account;餘額 reserve 失敗不 fallback | agent_api_order_claim、supply_withdrawal_order、attempt/reservation/counter/balance log/outbox | bank validation、HOUSE hard exclusion、reserve、expiry、review-held、duplicate-risk recovery |
GET /v1/agentApi/withdrawalOrder/{agentOrderId} | WithdrawalOrderService.findV1(...) | 依 target-native claim 投影 Supply Withdrawal 狀態與 review/recovery 結果 | agent_api_order_claim+supply_withdrawal_order | status mapping、settlement disposition、callback snapshot |
AgentApiV1OrderSupport目前直接驗證request並選取/lock Legacy PaymentChannel與PaymentChannelCard。實作時這個selection seam必須被包在routing-model boundary後方;不能在既有Legacy query中加入Supply table JOIN或UNION。建議的router/adapter class name只是B0設計項,不代表source已存在。
Bank contract需要同步調整strict allowed-fields parser、request VO、OpenAPI、idempotency fingerprint與negative tests。提供code時無效code必須拒絕,不能降級成name-only;提供code與name時code是identity與House gate依據,name只作snapshot/display。
Cardholder External API — 40 Stable Paths
| Family | Count | Current external paths | Supply table/feature impact |
|---|
| Auth | 6 | POST /cardholder/auth/{login,refresh,changePassword,logout,scheduleLogout,cancelScheduledLogout} | supply_cardholder、supply_cardholder_session;JWT subject discriminator仍待A1決策 |
| Bank | 1 | GET /cardholder/banks | existing bank read;無Supply persistence branch |
| Cards/Payment Account | 8 | GET /cardholder/cards、POST /cardholder/cards/applications、GET/DELETE /cardholder/cards/{id}、POST .../{id}/{pause,resume,receiving}、POST .../receiving/batch | Gateway ownership、application、payment_account、balance log |
| Card rows | 1 | GET /cardholder/cardRows | owner Gateway+Payment Account read projection |
| Deposits | 4 | pending、detail、confirm、reject under /cardholder/deposits/** | Supply Deposit Order、attempt、proof、reservation、balance log;不連至 house_card_transaction |
| Withdrawals | 5 | pending、detail、accept、reject、submitProof under /cardholder/withdrawals/** | Supply Withdrawal Order、attempt、proof、reservation、balance log、recovery case |
| Withdrawal proof | 1 | GET /cardholder/withdrawalProofFiles/{proofFileId}/content | supply_order_proof metadata/content ownership guard |
| Notifications | 3 | page、read、readAll under /cardholder/notifications/** | supply_cardholder_notification是單一history/read+FCM delivery outbox;每次claim/retry選current ACTIVE session最新ACTIVE FCM generation,target只代表current attempt、可隨後續Login registration改綁且不fan-out;committed claim是Login race cutoff,舊generation in-flight send可完成,後續attempt才改選新token;找不到eligible current-session registration時為非終態WAITING_FOR_DEVICE且不耗attempt,registration commit後targeted wake-up並由Scheduler recovery;FCM eligibility固定為create time起24小時且不被Login/registration/retry延長,deadline只阻止新claim;未claim delivery到期使用獨立EXPIRED/DELIVERY_WINDOW_EXPIRED/deadline tuple,清target/retry但保留history及attempt evidence;deadline前read/readAll把未claim FCM原子轉SKIPPED/ALREADY_READ/read time,不耗attempt且不覆寫provider diagnostics,已claim PROCESSING仍可finalize;operationNow到達deadline時expiry優先,read照常保存但delivery不改成skip,且結果不依Scheduler/lock先後;window內stale PROCESSING自動回RETRY且recovery不重複計attempt,清target後new claim重選current-session token,舊result no-op並接受transport duplicate風險;lease與deadline皆到達的claimed row轉DEAD/DELIVERY_OUTCOME_UNKNOWN,end time取較晚boundary、保留最後target、不改token且late result no-op;不重用Legacy table或建立sibling row |
| Device token | 3 | register、unregister、testSend under /cardholder/deviceTokens/** | supply_cardholder_device_token;token不是routing eligibility |
| Profile | 1 | GET /cardholder/me | supply_cardholder+immutable owner Gateway projection;不回固定Agent binding |
| Activity/reports | 7 | activity、dashboard summary、commission daily breakdown、summary、transactions、detail、CSV export | Order/Payment Account balance log;R1 report/PT tables後接;不讀 house_card_transaction |
| Total | 40 | external contract保持 | 40條都經identity/session guard;bank以外39條另需adapter或Supply domain projection |
Required facade seam
目前CardholderJwtService只簽發cardholderUserId與cardholderSessionId,CardholderJwtAuthenticationFilter也以這兩個Legacy id驗證session。RestCardholder*Client把相同id與agentId放入query/body後呼叫/internal/cardholder/**。因此A1不是只加login endpoint,而是同時影響:
- token subject contract與
CardholderIdentity; - JWT issue/extract/session validation;
- controller取得authenticated subject的方法;
- 11個REST client的internal route與payload;
- BO internal controller的subject authorization及Legacy/Supply adapter dispatch;
- 40條external contract regression。
BO Internal Cardholder API — 41 Actual Paths
| Family | Actual count | Internal paths/notes |
|---|
| Auth | 6 | /internal/cardholder/auth/** |
| Bank | 1 | /internal/cardholder/banks |
| Cards/Card rows | 9 | 8條/cards/**+1條/cardRows |
| Deposits/Withdrawals/proof | 10 | Deposit 4+Withdrawal 5+proof metadata 1 |
| Device/profile/session | 5 | device 3+profile 1+POST /sessions/validate 1 |
| Activity/reports | 7 | report 5+dashboard 1+commission 1 |
| Notifications | 3 | controller位於com.sit.spay2.notification.controller,不是bo.controller |
| Total | 41 | source盤點結果;不能再使用舊spec的38 |
新版本應以/internal/v2/cardholder/**或等價versioned contract承載model-neutral subject context;舊/internal/cardholder/**保留作Legacy rollback path。是否採用此namespace與subject欄位仍須在B0/A1凍結,不能直接把Supply id塞進cardholderUserId。
Backend-first BO API Families
第一階段先凍結「操作集合」,不沿用舊spec的83 methods總數。只有route/HTTP method/request/response/role/error全部完成B0 review後,才可形成新的method count與PD估算。
| Slice | Proposed namespace | Minimum operation set | Tables | Downstream dependency |
|---|
| B1 | /public/login、/supply/auth/login、/supply/auth/password/change、/supply/menu | 四種後台身分使用相同 username/password/nullable otpCode shape;MFA OFF忽略 OTP、ON完整驗證;Supplier/Gateway共用 Supply endpoint但回 realm-specific SupplyActorLoginResponseVo;不新增 verifyMfa endpoint。password、TOTP、challenge 與 session epoch 分別直接保存在 user 或 supply_account。 | user、supply_account | Prefix/System與所有Supply BO操作的authentication/authorization;OFF→ON會撤銷bootstrap sessions;既有LoginResponseVo及Agent/Device token rotation API不變 |
| B2 | /bo/v2/suppliers、Legacy POST /supplier retirement、/bo/v2/supplyGateways/**、/supplyGatewayAllocation/**、/supplierDirectionConfig/**、GET /bo/v2/agent/supplyGateways | Agent/Supplier Create required提交immutable/current canonical Business UTC Offset;Gateway Create required提交immutable Currency。Supplier/Gateway owner onboarding原子建立Deposit/Withdrawal current PT rate=0,額外Account不重設;Gateway page/detail/create/name-only update/soft delete;PUT .../{gatewayId}/operationalConfig原子提交status與limits;Agent allocation query/mutation/routing重新驗證Agent與Gateway Currency相同、Agent與Gateway owner Supplier offset相同。Agent read 從 authenticated Agent 推導 scope,以 Agent-first predicate 列 enabled、未刪除且所有 lifecycle gate active 的 binding;跨 Supplier 可見但不接受 Agent/Supplier selector,也不回 credential、limit、financial/report data | Agent/Supplier Business UTC Offset、Gateway Currency/allocation/兩層direction config+owner PT bootstrap | Cardholder immutable owner與scope redundancy、account eligibility、routing scope、Commission offset snapshot、R1 PT lifecycle |
| B3 | /supplyCardholder/** | page/detail/create/update/status、credential、session/device admin;Create required指定一個Gateway且建立後不得reassign,不接受agentId/prefixId;Cardholder Create原子建立Deposit/Withdrawal兩份current PT rules且各自rate/base fee/threshold全0 | Cardholder/session/device/notification+Cardholder PT bootstrap;無membership table | B4 owner、D1/W1 candidate eligibility、A1 login、R1 PT lifecycle |
| B4 | /paymentAccountApplication/**、/paymentAccount/** | application、review、status、direction availability、priority、balance adjustment、ledger read | 獨立Payment Account application/account/balance-log tables、entities與repositories;不與House共用mutable root | D1/W1 reserve與settlement、reports |
| B5 | /houseCard/** | page/detail/create/update、typed PUT /houseCard/{houseCardId}/status、delete、authorized full canonical account read;Create固定單一POST /houseCard與HouseCardCreateRequestVo,exact flat fields只含actor-conditional agentId、required displayName/bankId/accountNumber/accountName/depositEnabled/withdrawalEnabled及required-present nullable dailyAmountLimit/dailyTxLimit;兩個limits各自以null表示fallback、non-null表示House override,omitted拒絕且不得建立House或audit;不建立nested account/availability/limit object且不接受response-only effective/source、derived/audit/alias fields;missing/empty body、top-level null/nonobject、malformed syntax、known-field wrong JSON type及unknown top-level field固定fail closed為HTTP 400/40005/exact ["request", "invalid request"],不得反射unknown field/parser path/raw input且只作House-local strict parsing;duplicate top-level property在binding前拒絕,即使same-value也不採first/last wins並回同一generic response;完整九欄semantic validation固定HTTP 400+40005+exact [field, reason],依既有七欄後接dailyAmountLimit、dailyTxLimit順序fail-fast且在lookup/mutation前完成,兩個limits都錯時先回Amount;Create limits exact mapping固定為omitted required/null valid/non-number request invalid request/zero-negative must be positive/positive representation failure invalid format;reason只用六個exact lowercase phrases並依ADR-0257固定existing field-condition mapping及same-field precedence,Prefix non-null永遠must be omitted、false booleans有效、non-empty invalid Account Number回invalid format,不得由Bean Validation回alternate shape或在reason洩漏raw input;Create成功固定HTTP 200、無Location,Controller直接回完整HouseCardVo並由ApiAdvice單次包成ResponseVo<HouseCardVo>;displayName與accountName皆先拒絕raw Unicode control characters,再作Unicode edge trim及1..100 code-point validation,Create/Update各自共用field validator且不normalize/collapse內容;System Admin body agentId required positive,Prefix只可omitted/null且任何non-null值拒絕,不得silent overwrite;不提供Agent path/query或admin alias,兩者都不提交Currency/status;status body只含required cardStatus/expectedVersion,Prefix限own Agent且使用UPDATE permission,System Admin使用ADMIN,成功回ResponseVo<HouseCardVo>;authorized target lock後先驗證version,stale same-state回409/RESOURCE_VERSION_CONFLICT (40051)且不回target data,current-version same-state回200 full VO且zero-write/zero-audit,real transition才升版一次;不提供activate/inactivate/query-param/path-segment/generic-update status或restore;Create由Backend明確建立ACTIVE並驗證ACTIVE Agent/Currency Bank scope及canonical account;相同ACTIVE identity回HTTP 409+UNIQUE_KEY_EXISTS (40007)且不回existing ID,相同INACTIVE/DELETED不阻擋一般Create;Update禁止identity、允許audited displayName/accountName/direction flags,並固定authenticated route/role→strict JSON/allow-list→seven-field request validation→scope-protected target lookup/authorization→version→domain/mutation precedence;invalid request優先且不得查target或透露target data;Activate只修改target,其他INACTIVE不動,ACTIVE collision時target/version/audit不變並回同一error;delete required expectedVersion、INACTIVE且無current/unsettled financial blockers;fallback config有效時page/detail/create/update/status success共用exact 24-field HouseCardVo;current page/detail任一required resolution failure都使整份response失敗且不回partial rows/total/VO,mutation endpoints在寫入前解析post-command state並於failure保持zero-side-effect;上述五種resolution failure固定回HTTP 500+HOUSE_LIMIT_CONFIGURATION_INVALID (40053);House non-null override不讀未使用defaults;上述success projection,包含完整accountNumber、Agent/Currency/Bank display、current status/directions、configured/effective limits、per-field sources、derived Bank eligibility與version,不含BaseEntity status、audit metadata或masked alias;只有configured limits、missing Bank時bankCode/bankName及eligible時reason可為null;不以Bank失效改寫card status | 獨立house_card table/entity/repository;required no-default card_status+required mutable non-unique display_name+required no-default deposit_enabled/withdrawal_enabled+immutable owner/Currency/Bank+canonical account_no+mutable canonical account_name VARCHAR(100) NOT NULL;generated nullable active_identity_key的UQ只限制ACTIVE identity並允許多筆INACTIVE/DELETED rows;不得建立status+identity composite UQ;BaseEntity soft delete保留history;不建立generic card persistence | INACTIVE與Direction flags都只控制新Attempt;targeted activation不得auto-inactivate/delete/merge sibling;切換status保留flags且不影響in-flight pointer/reservation/LINE/settlement/reconciliation;soft delete不cascade且terminal history不阻擋;舊Order/Attempt保留舊ID,新routing只使用新ID,LINE不得跨generation rebind;reactivation重驗全部gates及ACTIVE uniqueness;Display與Account Name不參與identity/matching/routing;new routing直接套全部Bank current predicates,不讀reason enum;完整account不得進入URL/log/analytics/audit/error;不提供manual routing或local balance authority |
B5 House limit addendum — ADR-0258/0259/0260/0261/0262/0263/0264/0265/0266/0267/0268/0269/0270/0271/0272/0273/0274/0275/0276/0277/0278/0279/0280/0281/0282/0283/0284/0285/0286/0287/0288/0289/0290/0291/0292/0293/0294/0295/0296/0297/0298/0299/0300/0301: house_card新增daily_amount_limit DECIMAL(30,10) NULL(Java BigDecimal、Deposit amount only)與daily_tx_limit INT NULL(Java Integer、Deposit+Withdrawal shared count);非null值必須為目標type可無損表示的正值,超出precision/scale/range時拒絕且不得round/truncate/clamp。General PUT /houseCard/{houseCardId}採full replacement,exact allow-list為displayName、accountName、兩個direction flags、兩個limits及expectedVersion;Create exact flat allow-list由ADR-0250的七欄基線擴充為九欄,新增同名兩個limits。Create與Update的兩個limit properties都required-present且nullable,explicit null表示該欄fallback,omitted拒絕且不得mutation,non-null才作numeric validation;Create omitted不得建立House或audit,non-null override與其他Create資料原子保存。Create semantic fail-fast保留既有七欄順序,最後依序驗證Amount與Tx,全部在lookup/mutation前完成;兩個limits都錯時先回Amount。Create與Update對兩欄共用exact mapping:omitted回[field, "required"]、present null合法、非number token回["request", "invalid request"]、zero/negative回[field, "must be positive"]、positive但Amount precision/scale或Tx fractional/range無法表示回[field, "invalid format"]。Update limit validation failure不得產生row mutation、version/modify metadata變更或audit。Update fixed precedence為authenticated route/role gate→House-local strict JSON與allow-list→displayName、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit、expectedVersion完整field validation→scope-protected target lookup/authorization→version comparison→target-dependent domain guards/mutation;invalid request優先且不觸發target lookup。每欄各自依House non-null value→owning Agent override→Global system_config解析,mixed state有效;House-specific Global initial persisted defaults固定為spay.house.daily.amount.limit = 500000與spay.house.daily.tx.limit = 100000,不預建Agent rows。Agent row僅不存在時fallback;present-invalid及Global absent/invalid一律configuration-error fail closed,zero不是unlimited。Amount error只排除new Deposit,Tx error排除new Deposit+Withdrawal。BO current page/detail採all-or-nothing resolution;只解析實際需要的fallback,House non-null override不讀未使用defaults。任何required resolution failure不回PageVo、partial rows/total/VO或null/zero/stale/cached/hard-coded effective value/source。Create/Update/status在mutation與audit前解析post-command state,failure不得insert/update、改status/directions/limits、升版或寫audit,status same-state亦不得回success。上述BO page/detail/create/update/status resolution failure固定回HTTP 500+HOUSE_LIMIT_CONFIGURATION_INVALID (40053),不得重用40005/40044/40048/5000。Page/detail/create/update/status success共用exact 24-field VO,在既有18 fields上新增nullable configured dailyAmountLimit/dailyTxLimit、required read-only effectiveDailyAmountLimit/effectiveDailyTxLimit與required per-field dailyAmountLimitSource/dailyTxLimitSource: HouseLimitSource { HOUSE, AGENT, GLOBAL };effective/source不持久化且不得進入request,mixed sources有效。ADR-0275另固定此configuration error的ResponseVo.data為required空陣列[];不得省略、回null/object/非空array,單筆或多筆、多dimension failure皆相同,且data不帶field/source/reason或config value。ADR-0276另固定:Frontend依code=40053選擇自己的i18n提示;backend message僅供人類診斷,不解析且不凍結exact wording,不作畫面提示的文案authority。本版House limit configuration error依ADR-0277採secret-safe server log/metric供維運診斷,不建立persistent BO alert/incident;失敗command仍不得寫入任何DB row,包含獨立transaction的alert/outbox/audit。ADR-0278固定診斷主體為實際失敗的config scope+key,Agent設定再以owning Agent identity定位;Global根因不因受影響Agent/House而改變,Agent row absent仍是正常fallback。此主體不決定event筆數或去重。ADR-0279固定本次House failure metric labels只使用固定、有限分類值,不帶Agent/House/request/trace等識別值;owning Agent透過診斷log定位,剩餘exact log/metric技術細節後由ADR-0282移交owning implementation slice。ADR-0280將三種numeric failure合併為一類;ADR-0281固定exact diagnostic reason為MISSING(必要Global row不存在)、BLANK(Agent/Global row存在但值為null/blank)、INVALID_NUMERIC(無法解析、非正值或無法無損表示)。Agent row absent仍是正常fallback;reason不放入ResponseVo.data,既有validation不變,ADR-0283固定House config讀取時先用Java 21 String.strip()去除首尾whitespace,present null或strip後empty為BLANK;其餘兩欄共用BigDecimal(String)數值語法,再依Amount正DECIMAL(30,10)與Tx正INT作無損檢查,Tx的100.0/1e2接受為100,fraction/overflow仍拒絕。不改寫DB原字串,也不改JSON request contract。ADR-0282將剩餘House telemetry技術契約交由owning implementation slice在既有約束下定案、記錄,並以測試與operations文件驗證,不再逐項阻擋#142設計收斂;此移交不包含config parser或quota lifecycle產品決策,也不授權立即實作。ADR-0284固定Daily Amount Limit與shared Daily Tx Limit都依House owning Agent的immutable fixed Business Zone切日,每日區間為[當地00:00,次日00:00)。此決策只固定日界與時區authority;初次占用時點後由ADR-0285固定。ADR-0285固定House新assignment成功與所需daily quota占用原子成立:Deposit占用Amount+shared Tx,Withdrawal占用shared Tx;in-flight也減少剩餘capacity,assignment失敗/rollback不留下占用。ADR-0286固定House Deposit初次Daily Amount quota按該筆已驗證Order requested amount全額預留,不扣fee、不預估actual amount;requested為1000時初次預留1000。成功完成的Amount計入基礎後由ADR-0290固定,與成功terminal原子生效後由ADR-0293固定。ADR-0287固定House初次quota以該次成功assignment所記錄的同一事件時間,換算owning Agent local date歸日;Deposit的Amount與shared Tx共用該時間,不各自讀現在時間。當地9/8 23:59建單、9/9 00:01成功assignment時,初次占用9/9 quota;assignment事件時間不要求等於DB commit時鐘。ADR-0288固定新日admission不計入前日仍in-flight的quota;舊占用保留原日,local midnight本身不觸發釋放,也不搬移或重複占用新日quota。Daily Amount與shared Daily Tx採相同原則。每日Amount上限1000、前日600仍進行中且新日尚無其他占用時,新日可再承接1000,跨日進行中總額可達1600;daily quota本身不限制全部跨日in-flight總量,也不保證實際銀行入帳日總量上限。ADR-0289固定已確認沒有該次資金交易且House assignment已正式終止時,完整釋放該次原占用的quota:Deposit釋放Amount+shared Tx,Withdrawal釋放shared Tx。僅到期、未收到通知、待人工判定或已收/付款但未完成,都不符合此釋放前提。反覆assignment後以無交易結案可重複使用額度,shared Tx不能兼作派發次數上限;讓無資金確認與正式終止兩條件俱備的轉換,與原quota完整釋放對admission原子生效,後由ADR-0299固定;確認權限、證據與各狀態mapping依ADR-0300交由#145 grill,定案前不得實作相關未決行為。ADR-0290固定House Deposit已依settlement規則合法成功完成且已有確認actual amount時,最終Daily Amount quota以該actual amount為計入基礎;例如requested為1000、actual為900且已合法確認成功,最終基礎為900。此決策不允許partial payment自動成功,也不直接採raw LINE notification作terminal actual;Actual缺失/無效與fee定義依ADR-0300交由#145 grill,不能由實作自行假設。ADR-0291固定同一次House assignment合法成功完成後,Amount與shared Tx quota仍歸該次initial quota date,不轉到完成日;Withdrawal的shared Tx同理。9/8 assignment預留600、9/9合法成功且actual也是600時,600仍計入9/8;9/9自身已承接900仍為900,不因前日成功結果增加為1500。此決策只選同一次assignment成功歸日;合法重派的新占用與歸日後由ADR-0296固定,重派許可與舊新銜接依ADR-0300交由#145 grill。ADR-0292固定House Deposit已確認真實入款且其餘settlement成功條件均成立時,actual差額造成原日Daily Amount超額也不阻擋成功,完整actual仍計入原initial quota date。例limit始終1000、原預留600、原日其他占用300,確認actual=800後合計1100、超額100;不得把actual裁為700或只為quota不足暫停成功。此例不選overage診斷的limit比較版本;telemetry與實作仍未決。ADR-0293固定House Deposit成功terminal與原日Amount quota由requested轉為確認actual,作為同一次原子狀態轉換對後續admission生效。原占用1000、合法成功actual=900時,成功轉換前不得先釋放100;轉換生效後該筆貢獻為900,其他gates通過才可使用剩餘100。增額與ADR-0292允許的overage同時反映,不得成功後仍留下較小requested造成虛假capacity;跨日只調整原日,不增加今日額度。Exact counter/transaction/lock實作仍未凍結,不要求與外部資金系統形成分散式transaction。ADR-0294固定同一次House assignment合法成功完成後,shared Daily Tx持續保留原占用1筆;Deposit與Withdrawal相同,成功不再加1,也不釋放這1筆。原日總數9筆中一筆由in-flight變成功,仍為9,不變10或8;對admission的貢獻持續為1,不出現中間0或2,跨日仍歸initial quota date。Reserved/completed是否分欄與更新方式交實作;多次重派總數、已發生資金但失敗、人工更正與partial settlement規則仍未決。ADR-0295固定House effective limit合法變更後,新assignment立即採當下新上限,既有每日quota事實保留,不延用當日舊上限,也不因改limit重設/回收占用或取消既有assignment。當日占用800、上限由1000合法降至600,占用仍800且無可用Amount;再合法提高至1200、其他條件不變時可用400。Amount與shared Tx相同;此決策以變更已合法成立為前提;House general Update低於當日占用的儲存規則後由ADR-0298固定,Agent/Global config writer政策與overage診斷比較版本仍未決。ADR-0296固定合法re-dispatch產生的新House assignment,在目標House通過所有gates的前提下,依當下effective limit原子取得自己的quota:Deposit按已驗證requested amount全額占用Amount+shared Tx 1,Withdrawal占用shared Tx 1;新quota date由新assignment記錄的同一事件時間換算目標House owning Agent local date,不沿用舊assignment的占用資格或日期。舊assignment歸9/8、新assignment記錄於當地9/9時,新占用歸9/9;舊占用依原lifecycle處理,不推定已釋放。本決策不授權re-dispatch或選定目標允許範圍;原assignment尚未終止時的失敗效果後由ADR-0297固定,成功替換的舊新占用銜接與相關起始狀態處置依ADR-0300由#145完成產品決策。ADR-0297固定原House assignment尚未終止、且其他規則已允許替換時,若re-dispatch無法成功建立新assignment,這次失敗command保留原assignment/current association、quota與deadline,不因失敗先終止或釋放舊assignment;新目標不留下成功assignment或新占用。例A仍承接Deposit並占用Amount 600+shared Tx 1,合法改派B因容量不足失敗,在沒有其他獨立狀態轉換時,A維持原關係、600+1與原deadline。本規則只限制該失敗command,不阻止獨立expiry/settlement,不復活已終止assignment,也不撤銷先前已提交的結案/release;成功替換的舊新占用銜接依ADR-0300由#145完成產品決策。ADR-0298固定House general Update在其他驗證、權限、version與required config resolution均通過時,允許post-command effective limit低於owning Agent當日已占用quota,不把低於占用本身設為儲存失敗條件;Amount與shared Tx相同。占用800、原上限1000,Update改600可成功;explicit null清除override後繼承有效default 600亦相同。既有占用仍800,新admission依ADR-0295暫無該項capacity,不重設占用或取消既有assignment。Positive/lossless numeric與其他既有guards維持;本決策只涵蓋House general Update,Agent/Global config writer政策仍未決。ADR-0299固定讓House assignment首次同時滿足已確認無資金交易+正式終止的狀態轉換,與適用原quota的完整釋放,作為同一次原子結果對後續admission生效。Deposit釋放原Amount+shared Tx,Withdrawal釋放原shared Tx;不得讓兩條件俱備的結果已生效卻仍等待背景工作交還quota。兩條件可先後成立,只規範使第二個條件成立的轉換;release仍歸原quota date,前日釋放不增加今日額度。確認權限/證據/狀態mapping、重派舊新銜接、外部balance reservation與具體SQL/lock實作不由本決策選定。ADR-0300固定#142保留已接受House quota約束並繼續收斂核心Ownership/Master Schema;剩餘Order/settlement產品決策明確交由#145 grill,包括actual amount確認/有效性/fee定義、無資金確認權限/證據/狀態mapping,以及合法重派與成功替換的舊新銜接。#145定案前不得實作相關未決行為,也不得從quota結果反推partial成功、確認權限或重派許可。#144協作ledger/reservation/reconciliation,#146協作routing selection/fallback;Agent/Global config writer政策不由本次移交定案。這是設計責任移交,不代表#142完成、不變更既有SQL交付順序,也不授權Java/SQL/DB或GitLab操作。ADR-0301固定House Daily Amount與shared Daily Tx的quota identity為houseCardId;即使Agent/Currency/Bank/canonical account相同,不同House IDs也不合併或繼承彼此當日占用。A今日已成功計入800,合法停用A後建立並啟用新ID的B,兩張上限均1000且B無其他占用時,B可承接完整1000,同帳戶當日跨ID合計可達1800。舊quota與Order/Attempt歷史保留原ID,不搬移或重寫;單純同ID停用/啟用不是新identity,不因此重設其占用。新承接仍須通過全部gates,ACTIVE-only uniqueness維持;本決策不新增重派、settlement或SQL規則。其他未決事項須依其owning scope繼續收斂,不得將責任移交當作產品規則已定案。Exact migration collision policy、presence tracking、其他釋放與重派lifecycle仍未決。
D2 — HOUSE LINE transaction
| Slice | Proposed namespace | Minimum operation set | Tables | Downstream dependency |
|---|
| D2 | /houseCardTransaction/** | page/detail、受控 raw-content read、manual match;不提供 ingress CRUD、CSV 或 dismiss/reject | house_card_transaction、Deposit order/attempt、HOUSE reservation/counter、existing callback outbox | ingress 在 verified source 解析 Agent,raw 僅 AES-GCM ciphertext;唯一完整 candidate 才 AUTO,沒有 candidate 才在 1/3/5 分鐘重試,manual reason 僅容許 amount/payer-account exception |
Table-to-Feature Blast Radius
| Table group | Count | Direct features | API consumers | If wrong |
|---|
| Access/master | 6 + 1 ALTER | actor login、MFA、menu、Gateway、allocation、direction config、Supplier zone | B1/B2 BO、later routing/reporting | cross-realm privilege leak、routing scope錯誤 |
| Cardholder/account/House | 8 | Cardholder lifecycle/immutable Gateway owner、session、notification、application、account、balance adjustment、House master | B3/B4/B5 BO、40 external paths after A1 | identity collision、owner reassignment、money ledger不平、House假balance |
| Order/routing | 9 + 1 ALTER | Agent claim、route、assignment、reservation、proof、settlement、recovery、HOUSE LINE evidence/matching | 4 Agent APIs、HOUSE transaction BO、Cardholder order actions、workers | duplicate order/outflow、reservation leak、callback錯單 |
| Commission/Reports | 1 shared hourly aggregate candidate;#25/#26依ADR-0209無table;2 Reconciliation candidates待決 | Commission rule與Hourly Financial Aggregate供一般Report及Commission Report共用;result與payment state不持久化 | hourly projector、query-time Commission calculator、report consumer | Zone snapshot、rounding漂移、逐hour誤算Commission、query failure顯示0/stale value、誤把latest Report解讀成backend payment state |
| Existing shared masters | 4+ | Prefix、Supplier、bank、currency、audit/outbox | all slices | Supply污染Legacy或顯示scope錯誤 |
Function and Module Change Set
| Module/package | Existing source seam | Planned change | Must remain unchanged |
|---|
spay-bo.bo.controller | Agent API與Internal Cardholder controllers | 增加routing-model/subject-model dispatch;新增Supply BO controllers | external Agent paths;Legacy internal rollback behavior |
spay-bo.bo.service | DepositOrderService、WithdrawalOrderService、Cardholder domain services | Supply adapter/router/projection;不要把兩套persistence揉進單一DAO query | Legacy PaymentChannel state machine |
spay-bo.notification | internal notification API、LINE/provider dispatch | 增加Supply notification與matching branch | notification failure不得rollback assignment |
spay-bo.task | existing internal trigger/worker pattern | 新增Supply expiry、callback、report worker或既有worker branch | scheduler保持stateless trigger-only |
spay-cardholder.config | JWT filter、security context | model-neutral subject及session validation | /cardholder/** protection與opaque token |
spay-cardholder.service.impl | 11個RestCardholder*Client | versioned internal routes、subject context、response compatibility | external 40 paths與no-DB boundary |
spay-common | cross-module VO/DTO/enum | 只放model-neutral contract | 不放entity、DAO或schema implementation |
Functional Estimate Before Coding
這裡先估blast radius,不假裝提供精準PD:
| Workstream | Radius | Reason |
|---|
| B1 actor auth/MFA | High | security、credential encryption、replay/lockout與two-realm isolation |
| B2 Gateway/allocation/config | Medium–High | 4 tables+Supplier zone,且是所有routing的eligibility前置 |
| B3 Cardholder management | High | immutable single-Gateway owner+no-direct-Agent invariant+session/credential authority |
| B4 Payment Account | High | application state、concurrency、balance invariant與append-only ledger |
| B5 House master | Medium | CRUD本身有限,但必須防止local假balance或manual Withdrawal override外洩 |
| D1/W1 Agent cutover | Critical | 4 external paths、idempotency、money state、callback與recovery |
| A1 App integration | Critical | 40 external+41 internal、JWT identity與11個client |
| R1 Commission | High | 依matched configured zone分月、方向分開,shared Hourly Financial Aggregate以DECIMAL(38,10) exact addition保存eligible amount且不做hourly rounding;late-arriving new fact恰好一次更新原hour row,duplicate no-op;existing fact correction保留原fact並以source-linked immutable delta恰好一次修正原canonical bucket。每筆work在同一per-fact transaction取得ownership/status guard並共同commit fact/correction identity、aggregate delta、applied identity與queue PROCESSED;互相獨立的signed deltas不保證application order,per-required-source/partition contiguous cursor不得skip gap且只描述projector progress/resume/operations,不作Report gate。Source commit後best-effort targeted attempt與Scheduler bounded recovery共用同一projector;recovery使用每fire最多50 IDs與30秒soft execution budget但仍逐筆commit;budget從第一次candidate query前以monotonic elapsed time起算,active per-fact/failure-record transaction不hard-cancel。只有ownership/status guard winner且實際進入projector才消耗attempt;targeted/recovery loser與fetch-only candidate不消耗。Application failure完整rollback後另以短transaction在separate immutable attempt table append failure evidence並更新queue current status/count/next time/last error/version;兩者共同commit或rollback,Scheduler不join history。Failure attempt以queue identity + committed failure sequence作queue-local unique ordinal,未commit不占用durable ordinal。該metadata不得標記applied、完成queue或推進cursor,failure-record transaction失敗時queue仍可重試。Initial execution包含在5 total attempts內;failures 1/2/3/4 commit後分別延遲1/5/15/60分鐘再執行,failure 5直接park為NEEDS_ATTENTION並停止automatic selection,沒有180-minute fallback。Retry values固定為projector-owned Java domain policy;不得新增Global/Agent/Prefix SystemConfig、Spring property、environment override、seed row或BO設定入口。只有SOURCE_CONTRACT_INVALID、IMMUTABLE_IDENTITY_CONFLICT與NUMERIC_REPRESENTATION_INVALID可在attempt 1至4記錄evidence後直接park且不排next time;deadlock/timeout/temporary DB/network/infrastructure、mutable reference暫時缺少、unexpected runtime exception、unknown code與classifier failure仍retry。Classification不得解析exception/SQL message,diagnostic必須secret-safe。Audited BO operator command把同一row設為RETRYING/due now且等待Scheduler,保留exhausted retry history、不得修改既有attempt rows且只提供一次額外執行機會;再失敗append新attempt並立即re-park,direct SQL只作break-glass。Normal historical projection逐步提供目前已接受facts;controlled rebuild只在替換既有live bounded scope時由immutable originals加accepted corrections產生隔離scope並atomic發布。Commission Report query依month+owner+currency+direction加總target interval內目前已接受的latest eligible amount,再套完整公式與round一次;資料缺漏或coverage未知只是不計入,later fact在後續query反映,不需要activation boundary。Business response只回amounts或真正calculation failure,不回freshness/completeness/partial/projector progress。各target可讀實際query時的latest committed aggregate,同一request不保證point-in-time snapshot;mixed committed times不算failure,也不要求N+1 query。Exact/final維持既有representability bounds,final scale/mode使用Currency.decimalNumber/HALF_UP。不保存result pair、不由Scheduler materialize,也不管理paid/unpaid、settlement、outstanding、recovery或disposition。Exact after-commit signal、source inventory/lock/attempt technical primary key/columns/index/cursor/correction/rebuild schema、operator endpoint/role與其餘projection lifecycle仍待決;batch commit、SKIP LOCKED與claim-token/lease維持G6 evidence-gated,Reconciliation不納入目前估算 |
只有B0完成下列產物後才重估PD:route inventory、request/response field matrix、role-action matrix、SystemCode、transaction boundary、migration batches、focused test inventory。否則舊spec的31–43 PD與83 methods只可作歷史參考。
Legacy API Exclusion Boundary
以下既有API只做regression與防止scope洩漏,不接Supply persistence branch:
/paymentChannel/**、/paymentChannelCard/**、/paymentChannelCardApplication/**;/cardholderAccount/**;/pt/**、/reconciliation/**、/report/paymentChannelDaily/**、/report/paymentChannelCardDaily/**;這些 Legacy report route 不得成為 Supply actor 的替代查詢出口。- Legacy
/supplier/** channel semantics。
POST /public/webhook/line與既有callback/task infrastructure是例外:它們可新增Supply dispatch branch,但不得改寫Legacy table ownership。
Source Drift
- 舊spec的「38 internal mappings」已落後實際source;current inventory是41。
- 舊spec的83條新BO/internal methods以29張table與House ledger能力為基線,不能用於current implementation estimate。
- 歷史 core schema inventory、shared ALTER 與 HOUSE outflow external reservation 假設均已 superseded;current frozen inventory 為 51 relations,Agent API 使用 target-native claim,HOUSE 只可參與 Deposit。
CardholderSubjectContext、Legacy/Supply adapters與/internal/v2/cardholder/**仍是planned seam,不可描述成已實作。- codebase-memory index未包含目前
createV1/findV1 Agent API method版本;本文件以工作樹實際source為準。
Pre-implementation Checklist
- ☐ 由source extractor重新產生4 Agent、40 external、41 internal route inventory並納入contract test。
- ☐ 每個新BO operation都有HTTP method、path、role、request、response、SystemCode與table transaction對照。
- ☐ 每個table write都有唯一性、lock、idempotency、audit與rollback owner。
- ☐ Agent claim固定routing model,query/callback從同一association投影。
- ☐ Cardholder token可辨識subject model,且不允許Legacy id與Supply id碰撞。
- ☐ Legacy APIs沒有Supply DAO/table join;Supply APIs沒有Legacy
PaymentChannel fallback。 - ☐ House Card沒有local假balance/reserved amount或manual Withdrawal route action;有效
receiverBankCode仍須通過authoritative available balance+atomic reservation gate才可auto-route。 - ☐ Deposit/Withdrawal bank pair至少一個、無效code fail-closed、name-only排除House,且candidate query不比較request bank與candidate bank。
- ☐ Commission以first successful claim/routing保存的Prefix/Supplier matched zone snapshot分月,沒有全域
+08:00;Agent offset建立後immutable,Supplier offset mutation依#142 note47090保留且不重算歷史;一般Report與Commission Report共同讀取含UTC hour+Commission month dimensions、eligible_amount DECIMAL(38,10) exact addition且no-hourly-rounding的Hourly Financial Aggregate;late-arriving new fact更新原bucket且duplicate no-op,不拆三張actor tables。每筆claimed work以per-fact transaction共同commit identity/delta/applied evidence/queue completion;獨立signed deltas可不依queue順序完成,但cursor不得skip gap。Source commit後targeted attempt與Scheduler bounded recovery共用projector;recovery每1分鐘於UTC整分鐘第0秒觸發,nextRetryTime到期後可能再等不到60秒,HTTP delivery retry不增加projector attempt count;automatic-selectable due rows依logical eligibility time ASC、queue identity ASC admit,queue identity只作tie-break且order不保證application/completion順序;每個logical fire最多admit 50 IDs,no-op/loser不退回slot、same-fire delivery retry不取得新budget且targeted不消耗cap;bounded candidate fetch/30秒soft execution budget不形成batch commit;budget從第一次candidate query前以monotonic elapsed time起算,active transaction不hard-cancel,SKIP LOCKED/claim-token/lease不作baseline。Application failure rollback後append separate immutable attempt並更新queue current state,兩者同transaction commit;Scheduler candidate query不join history。Initial execution包含在5 total attempts內;只有guard winner實際進入projector才計數,targeted/recovery loser與fetch-only candidate不計數。Failures 1/2/3/4後分別延遲1/5/15/60分鐘;failure 5直接park為NEEDS_ATTENTION,沒有180-minute fallback。Retry values固定於projector-owned Java domain policy,沒有Global/tenant/Spring property/environment override。三個explicit deterministic domain categories可提早park;transient、unknown與classifier failure仍retry,且classification不得解析exception message。Operator只requeue同一row、不重設exhausted history、不修改attempt rows、只增加一次執行機會並等待Scheduler,再失敗append新attempt並立即re-park,SQL只作break-glass。Commission Report query加總target interval內目前已接受facts後套公式與round一次;資料缺漏或coverage未知只是不計入,cursor不作Report gate,normal history不等待all-source coverage,later fact在下一次query反映。Business response只回amounts或真正calculation failure,不回freshness/completeness/partial/projector progress。Controlled rebuild只原子替換既有live bounded scope。不保存Current Result或paid/unpaid state。Focused tests含targeted success、targeted未執行後下一個UTC整分Scheduler recovery、UTC 10:02:20到期row不早於10:03:00 fire取得、HTTP delivery retry不增加projector attempt count、initial/retry/operator-requeue eligibility來源、earliest-time-first與same-time queue identity tie-break、低ID較晚due不得插隊、candidate query無nullable OR/indexed-column COALESCE/function、candidate order不保證application order、兩條trigger競爭single winner且loser不計attempt、fetch-only candidate不計attempt、每fire最多admit 50 IDs、no-op/loser不退回slot、same-fire delivery retry不取得新budget、budget在candidate query前起算、query耗盡budget時零筆開始、elapsed等於30秒不開始下一筆、active transaction跨deadline仍完成且仍逐筆commit、四項application write共同rollback、attempt insert與queue update共同commit/rollback、queue-local sequence從1遞增且duplicate ordinal被拒絕、failure-record rollback不消耗durable sequence、Scheduler candidate query不join history、operator requeue不修改attempt rows、failure 1/2/3/4各自排定1/5/15/60分鐘、failure 5直接park且不排180分鐘、Java-only policy且沒有runtime config fallback、三個allow-list category各自在attempt 1後直接park且nextRetryTime = null、deadlock/timeout/temporary infrastructure/unknown/classifier failure仍依schedule重試、exception message變更不影響classification、diagnostic不含secret/raw payload、parked row不自動選取、operator requeue保留exhausted history且只增加一次執行機會、不inline投影、Scheduler取得due row、額外嘗試失敗append新row並立即re-park、commit後crash replay no-op、目前100+200顯示300、later 50後顯示350、gap後fact已完成但cursor不skip、gap補齊後cursor前進、cursor gap不阻擋Report、response metadata absence、correction/rebuild後latest query、逐hour公式被禁止、invalid metadata/overflow/missing rule不回0或stale value,以及100個requested targets中1個真正計算錯誤時不回傳其餘99個amounts。
Architecture and External Boundaries
Why It Matters
Supply persistence是新的隔離模型,但仍運行於既有spay-bo。最重要的外部限制是OGP House outflow可跳過Spay且不通知Spay;任何以Spay本地snapshot驅動House outflow reservation的設計都會製造假安全。bankCode只決定House pool是否可被考慮,不提供balance authority。
System Contract
| Component | Responsibility | Prohibited |
|---|
spay-bo | Supply schema、auth、BO API、routing、orders、ledger、reports | 不把DB owner移到Cardholder/Scheduler |
spay-cardholder | /cardholder/** facade與internal BO client | 不直接連DB |
spay-scheduler | 觸發BO internal jobs | 不解析business timezone/Order state |
| OGP Agent API | 建立/查詢Order與接收callback | 不視為House balance同步來源 |
| LINE provider | 已收到的銀行通知matching | 不代表所有OGP movement都會送達 |
House outflow Boundary
house_card可作為後台管理與House Deposit account identity。- Withdrawal request沒有
receiverBankCode時只可進入EXTERNAL_POOL;有效code只開啟House eligibility,不要求House Card與收款銀行相同,也不代表強制選House。 house_card_balance_observation只是external evidence,不是authoritative ledger或available balance。- House outflow只有在外部authority能原子證明並預留
available balance >= order amount時才可建立House attempt;reservation reference必須可稽核、release與consume。 - 不以
house_card本地balance/reserved欄位製造authority;在authoritative balance+atomic reservation integration未就緒時,自動selector必須排除House,即使request有有效code。 - OGP外部House取款若日後可匯入,只進reconciliation evidence;不得反向偽造Spay reservation。
Diagram
開啟架構圖
Source Drift
舊spec仍描述Spay本地House balance/counter/reservation/ledger。ADR-0081已要求authoritative readiness;目前外部能力無法滿足該predicate,因此current implementation不是「先用snapshot,失敗再人工補」,而是讓House candidate gate保持關閉。這是能力未就緒,不是永久禁止House outflow的product contract。
Source Links
- Canonical decision handoff
- House authoritative balance ADR
- Repository module boundaries
Backoffice Capability Map
Source Drift — 2026-08-22: 本文件下方Login/Menu/Supply Actor Account與Prefix Allocation段落仍保留2026-08-21 split realm方案。BO Frontend current contract已由ADR-0104、ADR-0105及Frontend接線手冊取代;實作與估算前不得混用兩套contract。
First Delivery
| Capability | System Admin | Supplier actor | Gateway actor | Prefix BO |
|---|
| Supply actor account lifecycle/MFA reset | full | none | none | none |
| Supply Gateway create/soft delete | full | none | none | none |
| Gateway name | full | own children | own name only | none |
| Supplier/Gateway direction config | full | own scope write | read own | none |
| Agent–Supply Gateway allocation(System Admin mutation/Agent-bound read) | write | scoped read | own read | GET /bo/v2/agent/supplyGateways read only |
| Cardholder identity/credential/session | full | own Gateway descendants write | own Gateway scoped read | none |
| Payment Account application/review | full | own Gateway descendants create/review | own Gateway scoped read | none |
| Account status/availability/priority | full | own Gateway descendants write | own Gateway scoped read | none |
| Account balance adjustment | full | own Gateway descendants single-step | own Gateway read projection | none |
| House Card master | cross-Agent full CRUD/assistance(UI可顯示Prefix) | none | none | own Agent VIEW/INSERT/UPDATE/DELETE(UI可顯示Prefix) |
| House outflow manual routing/override | none | none | none | none |
| Principal/surface | Current source evidence | Target contract | Readiness |
|---|
| Prefix/System login | POST /public/login已用LoginRequestVo與既有JWT;current LoginResponseVo另被Agent/Device token rotation共用 | 同一POST /public/login提交username/password/otpCode,成功回專用BoUserLoginResponseVo;不新增verify endpoint | 輸入、path、MFA switch與response type已接受;exact fields/error code仍待B0 review |
| Supplier/Gateway login | Java、SQL皆沒有SupplyActor、superseded Supply actor relation或/supply/auth/** | Supplier與Gateway共用POST /supply/auth/login與SupplyActorLoginResponseVo;Backend依username讀account row解析owner_type/owner_id,不接受client discriminator | 技術可行、尚未實作;session epoch、password-change token slot、單次登入與response type已定案 |
| Prefix/System menu | GET /system/menu依目前role.id查role_method,但現況role.is_admin會直接顯示全部active methods,與authority來源不完全一致 | BO_USER維持同一menu endpoint;新增兩個resources,agent-bound admin只依effective active VIEW grant顯示 | 可實作;default grant與parity修正已由ADR-0085決定,只剩B0 API/error contract review |
| Supplier/Gateway menu | Java、SQL皆沒有GET /supply/menu或SupplyActorPolicy | 由同一SupplyActorPolicy產生menu並執行action enforcement;不建立Supply role tables | 架構已決定、尚未實作;需先有正式SUPPLY_ACTOR principal與fixed menu-code inventory |
Accepted Unified Login Request Fields
| Field | Type/format | Contract |
|---|
username | string | 必填;只在所選API realm內解析principal,不由登入dropdown授權 |
password | string | 必填;不得寫入HTTP/APM log、ChangeLog、audit payload或cache |
otpCode | nullable string | MFA OFF時可省略且即使有值也完全忽略;MFA ON時必填、必須符合^[0-9]{6}$並保留前導零 |
BO_USER使用POST /public/login,SUPPLY_ACTOR使用POST /supply/auth/login。後者同時服務Supplier與Gateway,由Backend account row決定owner type/scope;request不得帶owner discriminator。兩條API都在同一request完成目前switch要求的password/TOTP驗證,不建立MFA_LOGIN challenge或verifyMfa endpoint。
Accepted Realm-specific Login Response Types
| Endpoint | Response VO | Accepted boundary |
|---|
POST /public/login | BoUserLoginResponseVo | 只回formal token、authRealm=BO_USER與UserInfoVo;不重用共用LoginResponseVo |
POST /supply/auth/login | SupplyActorLoginResponseVo | 只服務Supplier/Gateway actor;以AUTHENTICATED或PASSWORD_CHANGE_REQUIRED區分formal token與password-change token |
既有LoginResponseVo保持token/user兩欄,不影響Agent/Device token rotation與device validation。Supply exact fields、branch nullability與HTTP status仍須在B0凍結。
Accepted Manual TOTP Enrollment Response Fields
| Field | Type/format | Description |
|---|
principalType | enum string | PREFIX_BO_USER/SYSTEM_BO_USER/SUPPLY_ACTOR |
principalId | long | 對應user.id或superseded Supply actor relation.id |
accountName | string | 使用者在Google Authenticator手動新增項目時的帳號名稱 |
issuer | string | Backend config產生的固定產品issuer |
totpSecret | Base32 string | 只在create/reset成功response顯示一次;不得再次GET、log或cache |
secretDisclosedAt | ISO-8601 UTC | Backend完成一次性secret response的時間 |
Backend不產生、不保存、不回傳QR image或QR payload。Response遺失只能由System Admin reset並rotate secret。
Blocking Gates
- B0 contract freeze:鎖定兩個realm-specific response的exact fields、
PASSWORD_CHANGE_REQUIRED HTTP status/nullability、token claims、SystemCode、transaction boundary與negative tests;舊文件中的歷史method/table總數不可作為coding acceptance。
Resolved Gate
- G0 — formal session invalidation:依ADR-0082,不新增
SupplyActorSession;在authenticator_credential保存session_epoch、last_session_issued_at與sessions_revoked_at,正式JWT逐request比對epoch。V1只支援principal-level revoke-all。 - AUTH-1 — single-use password-change state:ADR-0088縮限ADR-0083;一般登入不建立challenge,每個principal只保留一個pending
PASSWORD_CHANGE token slot,後發覆蓋前一筆,不新增challenge table。 - G5 — Prefix BO Supply resource-action matrix:Agent-bound Gateway read 固定為
GET /bo/v2/agent/supplyGateways;它從 authenticated Agent 推導 scope、可回 Gateway/Supplier/Currency identity/display 與雙 offset,僅列 current effective binding。System Admin以既有ADMIN跨Agent協助House Card並獨占allocation mutation。 - B0-MENU-GRANT — Prefix Admin defaults:依ADR-0085,既有與未來active agent-bound admin roles自動取得兩個Supply grants;非admin預設none,agentless System Admin沿用
ADMIN。 - B0-AUTH-FLOW — all backoffice realms:依ADR-0088,Prefix、System Admin、Supplier與Gateway全部單次提交username/password/otpCode;不新增
verifyMfa endpoint,enrollment只回一次性Base32 secret字串。 - B0-MFA-SWITCH — OFF/ON semantics:依ADR-0089,OFF時
otpCodenullable且ignored,ON時六位GA code必填並驗證;enrollment/reset在OFF仍可用,enable前必須撤銷OFF期間sessions。 - B0-AUTH-RESPONSE-2 — realm-specific responses:依ADR-0090,Public與Supply login使用不同VO;
/supply/auth/login同時服務Supplier/Gateway並由account row解析owner,既有LoginResponseVo不變。 - 歷史決策:ADR-0086的兩階段API與ADR-0087的
BoLoginFlowResponseVo已標記Superseded,不得作implementation baseline。
Implementable Split
| Order | Slice | Can start when | Deliverable |
|---|
| 1 | B1A realm/session foundation | B0 auth fields凍結 | actor account/credential schema、SUPPLY_ACTOR token validation、ActorContext、revoke/password-change token tests |
| 2 | B1B Supplier/Gateway login | B1A | 單次帳密+TOTP login/temporary-password change、account admin與manual secret enrollment APIs |
| 3 | B1C Supply actor menu | B1A+fixed menu-code inventory | /supply/menu與SupplyActorPolicy action/menu parity tests |
| 4 | B0-P Prefix Supply permission contract | 可立即進入B0 review | RoleSet、menu seed、controller roles與scope-negative test inventory |
| 5 | B2–B5 backoffice domains | B1B/B1C;Agent-bound Prefix endpoints另依B0-P | Gateway/allocation/config/Cardholder/Account/House後台;不含存取款與報表 |
SUPPLY_ACTOR menu由同一SupplyActorPolicy產生,不建立Supply role tables。- System Admin沿用
ADMIN。 - Prefix BO只顯示
SUPPLY_GATEWAY_ALLOCATION與SUPPLY_HOUSE_CARD;其他Supply管理menus不得授權或顯示。 - Agent-bound admin仍依active
role_method的VIEW grant顯示menu;只有agentless System Admin可由ADMIN看全部active methods。 - House頁面不顯示authoritative balance、reserved amount或Withdrawal auto-routing switch。
Accepted Prefix BO Resource-Action Matrix
| Resource | Prefix BO actions/scope | System Admin actions/scope | Backend guard |
|---|
SUPPLY_GATEWAY_ALLOCATION | VIEW;own Agent 的 GET /bo/v2/agent/supplyGateways effective-binding projection | ADMIN;allocation mutation | endpoint 從 authenticated Agent 推導 scope,不接受 Agent/Supplier selector;可回 Gateway/Supplier/Currency identity/display 與雙 offset,但不回 credential、secret、external alias、direction limit、financial/report data;mutation endpoint只接受ADMIN |
SUPPLY_HOUSE_CARD | VIEW/INSERT/UPDATE/DELETE;own Agent(UI可顯示Prefix) | ADMIN;以target agentId跨Agent執行相同CRUD,可代為操作 | 非System Admin一律以authenticated Agent ID收斂;DELETE為logical lifecycle action |
對應RoleSet constants固定為SUPPLY_GATEWAY_ALLOCATION_VIEW及SUPPLY_HOUSE_CARD_VIEW/INSERT/UPDATE/DELETE。System Admin不需要對應role_method rows。
QA Negative Matrix
- Gateway actor只能讀取自身Gateway直接擁有的Cardholders;在Cardholder mutation authority另行凍結前,不得修改Cardholder status、password、session或Account。
- 任一Cardholder Create/Update command都不得接受
agentId/prefixId或Gateway reassignment;Agent scope只能由owner Gateway的allocation推導。 - Supplier actor不可管理其他Supplier的Gateway/Cardholder/Account。
- 除
GET /bo/v2/agent/supplyGateways 的 frozen effective-binding projection 外,Prefix BO不可讀取真實Supplier、Gateway、Cardholder或Payment Account identity;該 projection 僅含契約列出的 Gateway/Supplier/Currency identity/display、allocation id/version/priority 與雙 offset,絕不擴張為其他 Supply read scope。 - Payment Account與House Card後台不得共用generic card CRUD/repository再以owner type分支;兩者必須分別執行Supply Cardholder owner-chain與Agent scope authorization。只有pure validation helper、display projection或routing candidate contract可以共用。
- Prefix BO不可對其他Agent的House Card執行list、detail或mutation;System Admin則必須能以target
agentId協助CRUD。Response以agentPrefix顯示owner,API不得使用prefixId。 - House Card Create固定為單一
POST /houseCard及HouseCardCreateRequestVo,不提供Agent path/query或System Admin alias。System Admin required提交positive body agentId;Prefix只可omitted/null並由authenticated Agent決定owner,任何non-null值即使等於own Agent也拒絕,不得忽略或silent overwrite。Create/Update都不接受可獨立選擇的currencyId;response回Currency identity/display,Backend以resolved Agent current Currency寫入並禁止後續修改。 HouseCardCreateRequestVo固定為九個flat fields:actor-conditional agentId,以及required displayName、bankId、accountNumber、accountName、depositEnabled、withdrawalEnabled、required-present nullable dailyAmountLimit與required-present nullable dailyTxLimit。兩個limit values各自以null表示fallback、non-null表示House override;omitted拒絕且不得建立House或audit。Frontend不得送nested account/availability/limit object或response-only effective/source、derived、audit、alias fields;Backend以exact allow-list及逐欄validation拒絕contract外輸入。成功回完整HouseCardVo。- Create request完整九欄semantic validation固定回HTTP 400+
FIELD_VALIDATE_FAILED (40005)及exact [field, reason]。Backend按agentId、displayName、bankId、accountNumber、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit順序fail-fast,並在Agent/Bank lookup及mutation前完成;Prefix任何non-null agentId使用相同shape且不得形成scope probe,既有七欄precedence不變,兩個limits都錯時先回Amount。Limit property先檢查presence並依ADR-0270固定mapping:omitted回field-level exact [field, "required"],present null合法,非number token回request-level exact ["request", "invalid request"],number token為zero/negative回field-level exact [field, "must be positive"],positive number但Amount超出DECIMAL(30,10) precision/scale或Tx為fractional/超signed INT range回field-level exact [field, "invalid format"]。Reason不得包含raw input,Frontend不得解析message。Malformed JSON、type mismatch及unknown field由ADR-0254處理。 - Semantic reason只允許六個exact lowercase phrases:
required、must be positive、must be omitted、contains control characters、must contain between 1 and 100 code points、invalid format。逐欄mapping與同欄precedence依ADR-0257;Prefix non-null agentId不論value一律回must be omitted,Boolean false是有效值,Account Number non-empty canonicalization failure統一回invalid format。不得把transport invalid request或lookup後domain error混入此表。 - Missing/empty body、top-level JSON
null/非object、malformed syntax、known-field JSON type mismatch及unknown top-level field一律回HTTP 400/40005/exact ["request", "invalid request"]。Unknown fields必須拒絕但不回顯field name;response/production log不含parser path、exception message或raw payload。此strict parsing只作用於House Create,不全域改動Legacy endpoint;known field missing/null仍交由semantic field contract處理。 - 任一duplicate top-level JSON property都在binding/allow-list/semantic validation前拒絕;即使兩次value相同也不接受或merge,並沿用
["request", "invalid request"]。Frontend不依property name分支,Backend不回顯或記錄raw duplicate evidence;House-local duplicate detection不得改變Legacy endpoint。 - House Create success固定為HTTP 200且不回
Location;Controller直接回完整HouseCardVo,由ApiAdvice包成ResponseVo<HouseCardVo>。Frontend只把code=2000及exact 24-field data視為成功,不期待201、void、ID-only或minimal response;相同ACTIVE identity重送仍回409/40007。 - House Card Create/Update required提交
displayName;Backend先拒絕raw Unicode control characters,再作Unicode edge trim及1..100 code-point validation。保留emoji、Thai/其他Unicode、大小寫、標點、combining characters及內部一般空白,不normalize或collapse內容。Update可audited修改;List/detail以它作主要House識別文字。它可重複且不取代銀行戶名accountName,也不參與matching或routing。 - House
accountName由Create required提交且Update可audited修改。Backend在lookup/mutation前先拒絕原始輸入中的Unicode control characters,再移除前後Unicode whitespace/space;結果須為1..100 code points。保留大小寫、Unicode、標點、combining characters與內部一般空白,不作Unicode normalization或collapse;Create/Update共用同一pure validator,response回保存值。 - House Create required提交
depositEnabled/withdrawalEnabled,Update可audited修改,page/detail required回current values。兩者只控制對應方向的新routing;withdrawalEnabled=true仍須通過authoritative balance+atomic reservation。Frontend不得提供lineMatchingEnabled、receiving/sending alias,也不得把關閉direction解讀成取消既有Order。 - House general Update固定呼叫
PUT /houseCard/{houseCardId}並使用full replacement;request只含displayName、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit、expectedVersion,不得夾帶owner/Currency/Bank/Account Number、status、derived或audit fields。Create與Update的兩個limit properties都required-present且nullable;explicit null表示該欄fallback,omitted拒絕且不得mutation,只有non-null才作numeric validation,不得以@NotNull阻擋null。Create與Update對兩欄共用ADR-0270/0271 mapping:omitted回[field, "required"]、present null合法、非number token回["request", "invalid request"]、zero/negative回[field, "must be positive"]、positive但無法無損表示回[field, "invalid format"]。任何Update limit validation failure都不得更新House或其他mutable fields、推進version/modify metadata或寫audit。Update fixed precedence為authenticated route/role gate→House-local strict JSON與allow-list→displayName、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit、expectedVersion完整field validation→scope-protected target lookup/authorization→version comparison→target-dependent domain guards/mutation;invalid request會遮蔽missing/out-of-scope/stale target且不得觸發target lookup或透露target data。Daily Amount Limit只計Deposit amount,DB/Java固定DECIMAL(30,10)/BigDecimal;Daily Tx Limit由Deposit與Withdrawal共用,固定INT/Integer。非null值必須為目標type可無損表示的正值,超出precision/scale/range時拒絕且不得round/truncate/clamp。兩欄各自依House non-null value→owning Agent override→Global system_config解析,mixed state有效;defaults固定為spay.house.daily.amount.limit = 500000與spay.house.daily.tx.limit = 100000,只seed Global rows且不預建Agent override。Agent row只有不存在才fallback;存在但blank/malformed/non-positive,或Global missing/blank/malformed/non-positive都fail closed且不代表unlimited。Amount error只排除new Deposit,Tx error排除new Deposit+Withdrawal。Page/detail對current requested page或target採all-or-nothing resolution;任一實際需要的fallback config無效就不回PageVo、partial rows/VO、total或placeholder effective/source。Create/Update/status以post-command configured state在mutation與audit前解析兩欄;只有null欄位讀fallback,House non-null override不受未使用default config影響;失敗不得insert/update、改status/directions/limits、升版或寫audit,same-state status也不回success。上述BO page/detail/create/update/status resolution failure固定回HTTP 500+HOUSE_LIMIT_CONFIGURATION_INVALID (40053),不重用40005/40044/40048/5000。Success HouseCardVo同時回configured、effective及per-field sources。ADR-0275另固定此configuration error的ResponseVo.data為required空陣列[];不得省略、回null/object/非空array,單筆或多筆、多dimension failure皆相同,且data不帶field/source/reason或config value。ADR-0276另固定:Frontend依code=40053選擇自己的i18n提示;backend message僅供人類診斷,不解析且不凍結exact wording,不作畫面提示的文案authority。本版House limit configuration error依ADR-0277採secret-safe server log/metric供維運診斷,不建立persistent BO alert/incident;失敗command仍不得寫入任何DB row,包含獨立transaction的alert/outbox/audit。ADR-0278固定診斷主體為實際失敗的config scope+key,Agent設定再以owning Agent identity定位;Global根因不因受影響Agent/House而改變,Agent row absent仍是正常fallback。此主體不決定event筆數或去重。ADR-0279固定本次House failure metric labels只使用固定、有限分類值,不帶Agent/House/request/trace等識別值;owning Agent透過診斷log定位,剩餘exact log/metric技術細節後由ADR-0282移交owning implementation slice。ADR-0280將三種numeric failure合併為一類;ADR-0281固定exact diagnostic reason為MISSING(必要Global row不存在)、BLANK(Agent/Global row存在但值為null/blank)、INVALID_NUMERIC(無法解析、非正值或無法無損表示)。Agent row absent仍是正常fallback;reason不放入ResponseVo.data,既有validation不變,ADR-0283固定House config讀取時先用Java 21 String.strip()去除首尾whitespace,present null或strip後empty為BLANK;其餘兩欄共用BigDecimal(String)數值語法,再依Amount正DECIMAL(30,10)與Tx正INT作無損檢查,Tx的100.0/1e2接受為100,fraction/overflow仍拒絕。不改寫DB原字串,也不改JSON request contract。ADR-0282將剩餘House telemetry技術契約交由owning implementation slice在既有約束下定案、記錄,並以測試與operations文件驗證,不再逐項阻擋#142設計收斂;此移交不包含config parser或quota lifecycle產品決策,也不授權立即實作。ADR-0284固定Daily Amount Limit與shared Daily Tx Limit都依House owning Agent的immutable fixed Business Zone切日,每日區間為[當地00:00,次日00:00)。此決策只固定日界與時區authority;初次占用時點後由ADR-0285固定。ADR-0285固定House新assignment成功與所需daily quota占用原子成立:Deposit占用Amount+shared Tx,Withdrawal占用shared Tx;in-flight也減少剩餘capacity,assignment失敗/rollback不留下占用。ADR-0286固定House Deposit初次Daily Amount quota按該筆已驗證Order requested amount全額預留,不扣fee、不預估actual amount;requested為1000時初次預留1000。成功完成的Amount計入基礎後由ADR-0290固定,與成功terminal原子生效後由ADR-0293固定。ADR-0287固定House初次quota以該次成功assignment所記錄的同一事件時間,換算owning Agent local date歸日;Deposit的Amount與shared Tx共用該時間,不各自讀現在時間。當地9/8 23:59建單、9/9 00:01成功assignment時,初次占用9/9 quota;assignment事件時間不要求等於DB commit時鐘。ADR-0288固定新日admission不計入前日仍in-flight的quota;舊占用保留原日,local midnight本身不觸發釋放,也不搬移或重複占用新日quota。Daily Amount與shared Daily Tx採相同原則。每日Amount上限1000、前日600仍進行中且新日尚無其他占用時,新日可再承接1000,跨日進行中總額可達1600;daily quota本身不限制全部跨日in-flight總量,也不保證實際銀行入帳日總量上限。ADR-0289固定已確認沒有該次資金交易且House assignment已正式終止時,完整釋放該次原占用的quota:Deposit釋放Amount+shared Tx,Withdrawal釋放shared Tx。僅到期、未收到通知、待人工判定或已收/付款但未完成,都不符合此釋放前提。反覆assignment後以無交易結案可重複使用額度,shared Tx不能兼作派發次數上限;讓無資金確認與正式終止兩條件俱備的轉換,與原quota完整釋放對admission原子生效,後由ADR-0299固定;確認權限、證據與各狀態mapping依ADR-0300交由#145 grill,定案前不得實作相關未決行為。ADR-0290固定House Deposit已依settlement規則合法成功完成且已有確認actual amount時,最終Daily Amount quota以該actual amount為計入基礎;例如requested為1000、actual為900且已合法確認成功,最終基礎為900。此決策不允許partial payment自動成功,也不直接採raw LINE notification作terminal actual;Actual缺失/無效與fee定義依ADR-0300交由#145 grill,不能由實作自行假設。ADR-0291固定同一次House assignment合法成功完成後,Amount與shared Tx quota仍歸該次initial quota date,不轉到完成日;Withdrawal的shared Tx同理。9/8 assignment預留600、9/9合法成功且actual也是600時,600仍計入9/8;9/9自身已承接900仍為900,不因前日成功結果增加為1500。此決策只選同一次assignment成功歸日;合法重派的新占用與歸日後由ADR-0296固定,重派許可與舊新銜接依ADR-0300交由#145 grill。ADR-0292固定House Deposit已確認真實入款且其餘settlement成功條件均成立時,actual差額造成原日Daily Amount超額也不阻擋成功,完整actual仍計入原initial quota date。例limit始終1000、原預留600、原日其他占用300,確認actual=800後合計1100、超額100;不得把actual裁為700或只為quota不足暫停成功。此例不選overage診斷的limit比較版本;telemetry與實作仍未決。ADR-0293固定House Deposit成功terminal與原日Amount quota由requested轉為確認actual,作為同一次原子狀態轉換對後續admission生效。原占用1000、合法成功actual=900時,成功轉換前不得先釋放100;轉換生效後該筆貢獻為900,其他gates通過才可使用剩餘100。增額與ADR-0292允許的overage同時反映,不得成功後仍留下較小requested造成虛假capacity;跨日只調整原日,不增加今日額度。Exact counter/transaction/lock實作仍未凍結,不要求與外部資金系統形成分散式transaction。ADR-0294固定同一次House assignment合法成功完成後,shared Daily Tx持續保留原占用1筆;Deposit與Withdrawal相同,成功不再加1,也不釋放這1筆。原日總數9筆中一筆由in-flight變成功,仍為9,不變10或8;對admission的貢獻持續為1,不出現中間0或2,跨日仍歸initial quota date。Reserved/completed是否分欄與更新方式交實作;多次重派總數、已發生資金但失敗、人工更正與partial settlement規則仍未決。ADR-0295固定House effective limit合法變更後,新assignment立即採當下新上限,既有每日quota事實保留,不延用當日舊上限,也不因改limit重設/回收占用或取消既有assignment。當日占用800、上限由1000合法降至600,占用仍800且無可用Amount;再合法提高至1200、其他條件不變時可用400。Amount與shared Tx相同;此決策以變更已合法成立為前提;House general Update低於當日占用的儲存規則後由ADR-0298固定,Agent/Global config writer政策與overage診斷比較版本仍未決。ADR-0296固定合法re-dispatch產生的新House assignment,在目標House通過所有gates的前提下,依當下effective limit原子取得自己的quota:Deposit按已驗證requested amount全額占用Amount+shared Tx 1,Withdrawal占用shared Tx 1;新quota date由新assignment記錄的同一事件時間換算目標House owning Agent local date,不沿用舊assignment的占用資格或日期。舊assignment歸9/8、新assignment記錄於當地9/9時,新占用歸9/9;舊占用依原lifecycle處理,不推定已釋放。本決策不授權re-dispatch或選定目標允許範圍;原assignment尚未終止時的失敗效果後由ADR-0297固定,成功替換的舊新占用銜接與相關起始狀態處置依ADR-0300由#145完成產品決策。ADR-0297固定原House assignment尚未終止、且其他規則已允許替換時,若re-dispatch無法成功建立新assignment,這次失敗command保留原assignment/current association、quota與deadline,不因失敗先終止或釋放舊assignment;新目標不留下成功assignment或新占用。例A仍承接Deposit並占用Amount 600+shared Tx 1,合法改派B因容量不足失敗,在沒有其他獨立狀態轉換時,A維持原關係、600+1與原deadline。本規則只限制該失敗command,不阻止獨立expiry/settlement,不復活已終止assignment,也不撤銷先前已提交的結案/release;成功替換的舊新占用銜接依ADR-0300由#145完成產品決策。ADR-0298固定House general Update在其他驗證、權限、version與required config resolution均通過時,允許post-command effective limit低於owning Agent當日已占用quota,不把低於占用本身設為儲存失敗條件;Amount與shared Tx相同。占用800、原上限1000,Update改600可成功;explicit null清除override後繼承有效default 600亦相同。既有占用仍800,新admission依ADR-0295暫無該項capacity,不重設占用或取消既有assignment。Positive/lossless numeric與其他既有guards維持;本決策只涵蓋House general Update,Agent/Global config writer政策仍未決。ADR-0299固定讓House assignment首次同時滿足已確認無資金交易+正式終止的狀態轉換,與適用原quota的完整釋放,作為同一次原子結果對後續admission生效。Deposit釋放原Amount+shared Tx,Withdrawal釋放原shared Tx;不得讓兩條件俱備的結果已生效卻仍等待背景工作交還quota。兩條件可先後成立,只規範使第二個條件成立的轉換;release仍歸原quota date,前日釋放不增加今日額度。確認權限/證據/狀態mapping、重派舊新銜接、外部balance reservation與具體SQL/lock實作不由本決策選定。ADR-0300固定#142保留已接受House quota約束並繼續收斂核心Ownership/Master Schema;剩餘Order/settlement產品決策明確交由#145 grill,包括actual amount確認/有效性/fee定義、無資金確認權限/證據/狀態mapping,以及合法重派與成功替換的舊新銜接。#145定案前不得實作相關未決行為,也不得從quota結果反推partial成功、確認權限或重派許可。#144協作ledger/reservation/reconciliation,#146協作routing selection/fallback;Agent/Global config writer政策不由本次移交定案。這是設計責任移交,不代表#142完成、不變更既有SQL交付順序,也不授權Java/SQL/DB或GitLab操作。ADR-0301固定House Daily Amount與shared Daily Tx的quota identity為houseCardId;即使Agent/Currency/Bank/canonical account相同,不同House IDs也不合併或繼承彼此當日占用。A今日已成功計入800,合法停用A後建立並啟用新ID的B,兩張上限均1000且B無其他占用時,B可承接完整1000,同帳戶當日跨ID合計可達1800。舊quota與Order/Attempt歷史保留原ID,不搬移或重寫;單純同ID停用/啟用不是新identity,不因此重設其占用。新承接仍須通過全部gates,ACTIVE-only uniqueness維持;本決策不新增重派、settlement或SQL規則。其他未決事項須依其owning scope繼續收斂,不得將責任移交當作產品規則已定案。Exact migration collision policy、presence-tracking implementation、其他釋放與重派lifecycle尚未凍結。 - House Create不得提交
cardStatus,Backend明確建立ACTIVE;page/detail required回cardStatus: HouseCardStatus。Authorized status command只允許ACTIVE↔INACTIVE並保留兩個direction flags。即使已有Attempt或active/review-held reservation也可設為INACTIVE,但不得取消/改派既有Order或阻斷LINE/settlement;恢復ACTIVE後Frontend不得顯示為無條件可routing。 - House status固定呼叫
PUT /houseCard/{houseCardId}/status,JSON body只提交required cardStatus與expectedVersion;不把status放query/path segment,也不呼叫activate/inactivate alias或generic Update。成功解開ResponseVo<HouseCardVo>並以完整authoritative VO更新detail;Frontend不得只在local切換status。 - Backend在authorized target lock後先驗證
expectedVersion再比較cardStatus。Stale same-state仍回HTTP 409+RESOURCE_VERSION_CONFLICT (40051),response不回current target data;Frontend必須reload且不得以舊version自動重送。Current-version same-state回200 full VO,但version/modifier/modifyTime不變且沒有audit、routing、reservation、LINE或settlement side effect。 - House page、detail、create、update及status success共用24-field
HouseCardVo:ADR-0247既有18 fields加上nullable configured dailyAmountLimit/dailyTxLimit、required derived effectiveDailyAmountLimit/effectiveDailyTxLimit及required dailyAmountLimitSource/dailyTxLimitSource。兩個source各自使用HouseLimitSource { HOUSE, AGENT, GLOBAL },與同欄effective來自同一次House→Agent→Global resolution,mixed sources有效。Effective/source只讀且不持久化;configured non-null時同欄effective必須相等且source為HOUSE。Bank reference missing時bankCode/bankName可為null,eligible時reason為null;其餘既有欄位及effective/source fields required。不得增加audit/row-status/masked-account、單一hasCustomLimit或pair-level source,亦不得按endpoint回partial VO。 - Activate required提交明確
houseCardId + expectedVersion,成功只更新target row;其他相同identity的INACTIVE rows保持原狀。已有另一張ACTIVE時target保持INACTIVE,version/audit不變並回409/40007。Frontend reload後由operator自行先停用current ACTIVE,不得auto-switch、bulk mutate或自動重送。 - House Delete必須由operator先完成INACTIVE transition,再提交current
expectedVersion。Backend鎖定House後檢查current Order、nonterminal/unsettled Attempt、active/review-held reservation obligation與open reconciliation/recovery;任一存在即fail closed且不得自動處理blocker。Terminal/closed history不阻擋。Frontend不得把「停用成功」顯示成「可立即刪除」。 - House不提供Restore action或permission。Create只在相同ACTIVE identity已存在時回HTTP 409+
UNIQUE_KEY_EXISTS (40007),response不含existing House ID;相同INACTIVE/DELETED rows不阻擋Create。Frontend維持一般Create流程並要求operator重新提交Display/Account Name與direction flags;Backend重驗current Agent/Currency/Bank後建立新ID且House status為ACTIVE。Activate另一筆相同identity的INACTIVE row時若已有ACTIVE也回同一衝突。Frontend不得呈現恢復舊卡、沿用舊設定、自動重試或改寫歷史ID的操作。 - House Card Create的Bank必須為ACTIVE、配置給owning Agent且符合derived Currency。Update不得接受
bankId/accountNumber,只可audited修改accountName及依各自lifecycle允許的非identity欄位;更換Bank/Account須建立新卡。 - House
accountNumber由Backend統一移除ASCII space/tab/hyphen/underscore並轉大寫;canonical結果只允許[A-Z0-9]{1,100}且保留leading zeros。Frontend不得自行產生另一套canonical value;authorized response直接顯示Backend回傳的完整canonical account,不提供masked alias,也不得寫入URL、analytics或client log。 - House page/detail required回derived
bankEligible與nullable bankEligibilityReason。Eligible時reason必為null;ineligible時Backend依BANK_REFERENCE_MISSING、BANK_DELETED、BANK_INACTIVE、BANK_AGENT_MAPPING_MISSING、BANK_CURRENCY_MISMATCH的固定優先序只回一個code。兩種actor都必須先通過House own-Agent/target-Agent scope才可取得reason,不能藉此探測其他Agent Bank存在性。 - House自身status/flags與Bank eligibility分開顯示;Bank失效不得觸發House status mutation。Frontend可disabled顯示
Unavailable: <reason>,但不可從Bank display fields自行推導reason、把bankEligible=false當成House已停用、把ACTIVE當成已滿足全部eligibility,或提供繞過Bank guard的新routing操作。 - 任一actor都不可手動強制House outflow routing或繞過bank/balance gates;auto selector只有在有效
receiverBankCode、authoritative available balance與atomic reservation成立時才可選House。
Backend-first Implementation Slices
Configuration Reference Schema Obligation — ADR-0302
#142的SPAY4 target schema須納入Global key與Agent (agent_id, key) DB唯一約束,適用全部設定keys,且不以status/version分隔同名設定。Implementation驗證重複值被DB阻擋、不同Agents可共存同名key、Global與Agent層同名可共存及concurrent writes不產生duplicates;不得以Optional回傳或precheck代替。#150盤點source,#151決定duplicates/collision處置,#152最後交付SQL;本ADR不授權提前執行。
Order/Settlement Design Ownership
ADR-0300固定#142保留已接受House quota約束並繼續收斂核心Ownership/Master Schema;剩餘Order/settlement產品決策明確交由#145 grill,包括actual amount確認/有效性/fee定義、無資金確認權限/證據/狀態mapping,以及合法重派與成功替換的舊新銜接。#145定案前不得實作相關未決行為,也不得從quota結果反推partial成功、確認權限或重派許可。#144協作ledger/reservation/reconciliation,#146協作routing selection/fallback;Agent/Global config writer政策不由本次移交定案。這是設計責任移交,不代表#142完成、不變更既有SQL交付順序,也不授權Java/SQL/DB或GitLab操作。
B5保留House master與既定quota contract;D1/W1必須承接#145的金融產品決策並完成設計驗收,之後才可實作相關未決行為。Counter/lock等技術選擇不取代這個設計前置條件。
Delivery Rule
每個slice必須形成可操作的backend flow與focused evidence;不可只交付controller或空entity。未取得coding authority前,本文件只作拆票baseline。
Dependency Order
| Slice | Scope | Tables/ALTER | Done evidence | Explicitly deferred |
|---|
| B0 Contract freeze | reconcile columns、enums、API/role/error matrix | none | docs diff、schema review、open decisions列明、DDL/Java field-description completeness gate | production SQL |
| B1 Access foundation | Supply actor account、MFA、ActorContext、audit | 2 tables | login/MFA/replay/scope tests | Cardholder App login |
| B2 Master management | Agent immutable offset、Supplier zone、Supply Gateway、direction config、Agent–Supply Gateway allocation,以及 GET /bo/v2/agent/supplyGateways Agent-bound read | 4 tables + agent.business_utc_offset/supplier.business_utc_offset | CRUD、soft-delete blockers、scope、version tests;Agent-first Querydsl cross-Supplier read、stable priority ASC, allocationId ASC ordering、AgentBoundGatewayVo identity/display、lifecycle/Currency/offset gate、scope-selector rejection、read-only/secret-financial masking tests | routing |
| B3 Cardholder management | Gateway-owned Cardholder、immutable owner、credential/session admin | 4 tables | owner immutability、no-direct-Agent、session scope negative tests | mobile self-service |
| B4 Account management | Cardholder-owned application/review、status、direction、priority、balance adjustment;獨立Payment Account entity/repository/aggregate | 4 tables | duplicate、lock、ledger equality、notification、no-House repository branch tests | Deposit/Withdrawal assignment |
| B5 House management | Agent-owned House master、HouseCardStatus { ACTIVE, INACTIVE }、authorized full canonical account read、guarded irreversible soft delete;獨立House entity/repository/aggregate | 1 table | single POST /houseCard actor-conditional agentId/no path-query-alias/no-silent-overwrite、Create exact flat nine-field allow-list(agentId/displayName/bankId/accountNumber/accountName/depositEnabled/withdrawalEnabled/dailyAmountLimit/dailyTxLimit)/two limits required-present nullable/null fallback/non-null House override/omitted zero-side-effect rejection/no nested-account-availability-limit/no response-only-effective-source-or-alias fields、Create transport empty-null-nonobject-malformed-wrong-type-unknown rejection/HTTP 400/40005/exact ["request", "invalid request"]/unknown no-reflection/no raw parser data/House-local strictness/semantic-null handoff、duplicate top-level property pre-binding rejection/same-value duplicate rejection/no first-last wins/generic response/no reflected evidence/Legacy unchanged、Create semantic validation HTTP 400/40005/exact two-element [field, reason]/fixed nine-field fail-fast order preserving existing seven then Amount then Tx/exact six lowercase reasons for existing fields/Create/Update limits exact mapping(omitted required/null valid/non-number request invalid request/zero-negative must be positive/positive representation failure invalid format)/Update auth-route→strict JSON-allow-list→seven-field request validation→scoped target→version→domain-mutation precedence/invalid-body masks missing-stale target/no-target-lookup negative/config-resolution only-when-fallback/current-page-and-detail all-or-nothing/no-partial-row-total-placeholder/create-update-status post-command pre-mutation resolution/same-state-status config failure/page-detail-create-update-status HTTP500-40053/required-data-exact-empty-array/single-multiple-row-and-dimension-parity/no-field-source-reason-value-in-data/code-driven-frontend-i18n/diagnostic-only-message/no-message-parsing-or-exact-wording-assertion/secret-safe-server-log-and-metric/no-persistent-bo-alert-incident/no-failure-path-db-write-including-requires-new/failing-config-scope-key-subject/owning-agent-config-identity/global-root-independent-of-affected-house/agent-row-absent-is-fallback/fixed-finite-metric-label-values/no-agent-house-request-trace-id-labels/identity-in-diagnostic-log-only/single-numeric-invalid-diagnostic-category/exact-MISSING-BLANK-INVALID_NUMERIC-reasons/Agent-row-absent-is-normal-fallback/reason-not-in-response-data/config-reader-java21-strip-and-shared-BigDecimal-syntax/Tx-decimal-or-exponent-exact-integer/no-config-string-writeback/both-quotas-use-owning-agent-fixed-offset/local-midnight-half-open-day-boundary/assignment-success-and-quota-occupancy-atomic/in-flight-reduces-capacity/assignment-rollback-leaves-no-occupancy/initial-Deposit-Amount-reserves-full-validated-requested-amount/no-fee-deduction-or-actual-estimation/initial-quota-date-from-recorded-assignment-event/same-event-time-for-Amount-and-shared-Tx/new-day-admission-excludes-prior-day-in-flight/no-midnight-release-or-carry/ended-and-confirmed-no-transaction-releases-full-quota/expiry-or-missing-notification-alone-is-insufficient/successful-Deposit-final-Amount-basis-is-confirmed-actual/no-implicit-partial-success-policy/successful-quota-stays-on-initial-assignment-date/no-completion-date-transfer/confirmed-Deposit-quota-overage-does-not-block-success/full-actual-without-quota-clamping/Deposit-success-and-Amount-adjustment-atomic-for-admission/no-early-release-or-stale-requested-capacity/successful-shared-Tx-retains-initial-one/no-success-double-count-or-release/new-admission-uses-current-effective-limit-after-valid-change/no-quota-reset-or-occupancy-recall-on-limit-change/lawful-redispatch-acquires-new-assignment-quota-and-date/no-implicit-old-quota-release-on-redispatch/failed-lawful-replacement-preserves-nonterminated-assignment-quota-and-deadline/no-revival-or-undo-of-independent-lifecycle/house-update-allows-effective-limit-below-occupancy/clear-override-to-lower-valid-default-allowed/no-funds-termination-and-full-original-quota-release-atomic-for-admission/conditions-may-arrive-in-either-order/quota-owned-by-houseCardId-not-cross-generation-financial-identity/new-ID-has-own-capacity-old-facts-preserved/same-ID-status-toggle-is-not-quota-reset/implementation-owned-telemetry-contract-with-tests-and-operations-docs/preserved-error-and-zero-db-write-invariants/parse-nonpositive-unrepresentable-grouping/unchanged-fail-closed-validation/wrong-code non-reuse/no-mutation-version-audit negative/complete existing-field condition matrix/same-field precedence/Prefix non-null always omitted/false-booleans valid/semantic-invalid-format vs transport-invalid-request/lookup-before-all-nine-validation negative/no raw-input reason/no Bean Validation alternate shape、Create success HTTP 200/ApiAdvice single-wrap/exact full VO/no Location/no 201/commit-before-success、displayName raw-control rejection/Unicode-edge trim/1-100 code-point boundaries/supplementary-and-combining tests/no-normalization/Create-Update parity、accountName raw-control rejection/Unicode-edge trim/1-100 code-point boundaries/surrogate-pair count/no-normalization/Create-Update parity、typed PUT /houseCard/{houseCardId}/status request allow-list/field descriptions/exact 24-field full-VO response/no-audit-no-masked allow-list/missing-Bank nullability/full-account logging negative/alternate-route absence、version-first stale-same-state 409/40051、current-version same-state 200 zero-write/zero-audit、real transition single-version-increment、Create-forced ACTIVE/request-status rejection、required no-default status、ACTIVE↔INACTIVE audit/version、two-direction new-routing exclusion、in-flight Attempt/reservation/LINE preservation、targeted activation by ID+version、other-INACTIVE unchanged、activation collision full rollback/no audit/no auto-switch、reactivation full-gate、delete requires INACTIVE/expectedVersion、四類unresolved blocker與terminal non-blocker、same-transaction race/no-cascade/historical read、generated active_identity_key ACTIVE-only UQ、multiple-INACTIVE/multiple-DELETED success、Create-with-INACTIVE success、Create/Activate ACTIVE collision HTTP 409+40007 with no existing ID、concurrent and repeated delete/recreate、full current guard revalidation、no-field-inheritance、old/new reference isolation、LINE no-cross-generation-rebind、required mutable displayName、required no-default direction flags、immutable per-row identity、shared pure canonicalizer、derived bankEligible+deterministic reason without cascade、authorization-scope negative、reason/predicate parity、audited Display/Account Name/directions、SQL COMMENT+Java/enum description completeness、no-generic-card persistence、Order/Attempt mutually-exclusive reference、no-balance-authority guard tests | HOUSE 僅 Deposit;所有 Withdrawal path 硬排除 HOUSE |
| D1 Deposit | target-native Agent claim、HOUSE+Payment Account 單一扁平池、attempt、reservation、proof、LINE matching、callback | D1 11 relations + claim | mapping/owner/config/quota/Freeze/risk gate、並行 reserve、候選耗盡 review、amount/evidence mismatch、expiry、idempotency、matching tests | 排序演算法細節與外部 adapter |
| W1 Withdrawal | target-native Agent claim、Payment Account 單一扁平池、review-held reservation、NOT_TRANSFERRED atomic reassign、duplicate-risk recovery | W1 10 relations + shared claim | HOUSE hard exclusion、餘額 reserve failure 不 fallback、expiry review、disposition、recovery、Supplier emergency Audit tests | 排序演算法細節與外部 adapter |
| R1 Commission | Supplier/Gateway/Cardholder兩方向rule、matched configured-zone month、routing offset snapshot、shared Hourly Financial Aggregate、Eligible Fact Correction、per-fact atomic application、after-commit targeted attempt+Scheduler bounded recovery、same-transaction claim/apply、separate immutable failure attempts+queue current state、queue-local committed failure sequence、每分鐘UTC整分recovery cadence、logical eligibility time+queue identity candidate order、每fire最多50 candidate IDs、30秒soft execution budget、Java-only 5-total-attempt retry policy、explicit deterministic early-park classification、retry exhaustion park+audited operator requeue、controlled aggregate rebuild與available-facts query-time Commission Report calculator | 建立PT rule、Hourly Financial Aggregate相關persistence與1張separate immutable failure-attempt candidate;ADR-0209排除Current Result、scheduled Commission result attempt及paid/unpaid、settlement、outstanding、recovery、disposition tables;ADR-0214不新增snapshot table/token;projection failure attempt只屬operations/retry evidence;ADR-0220排除retry SystemConfig/property/seed persistence;ADR-0221不新增classification config;ADR-0222排除queue JSON history與latest-only overwrite;ADR-0223固定queue-local committed failure sequence,ADR-0224固定每分鐘UTC整分recovery cadence,ADR-0225固定logical eligibility time+queue identity candidate order,ADR-0226固定每fire最多50 IDs且no-op/loser不退回slot;ADR-0227固定從第一次candidate query前起算的30秒soft execution budget,active transaction不hard-cancel;exact technical primary key/columns/index仍待決 | hour/month dual dimensions、exact hourly addition/no hourly rounding、late new fact original-bucket/duplicate no-op、existing fact 100+linked correction -10=original bucket 90且arrival period不變、targeted success、targeted未執行後下一個UTC整分Scheduler recovery、UTC 10:02:20到期row不早於10:03:00 fire取得、HTTP delivery retry不增加projector attempt count、initial/retry/operator-requeue eligibility來源、earliest-time-first與same-time queue identity tie-break、低ID較晚due不得插隊、candidate query無nullable OR/indexed-column COALESCE/function、candidate order不保證application order、兩條trigger競爭single winner、bounded candidate fetch/execution budget仍逐筆commit、同一transaction取得ownership/status guard後identity/delta/applied evidence/queue completion共同commit或rollback、只有guard成功且實際進入projector才消耗attempt、targeted/recovery loser與fetch-only candidate不消耗attempt、每fire最多admit 50 IDs且no-op/loser不退回slot、same-fire delivery retry不取得新budget、rollback後attempt insert/queue current-state update共同commit或rollback、queue-local sequence從1遞增且duplicate ordinal被拒絕、failure-record rollback不消耗durable sequence、Scheduler candidate query不join history、failure-record transaction失敗仍可重試、failure 1/2/3/4分別延遲1/5/15/60分鐘、failure 5 park且沒有180-minute fallback、Java-only policy且沒有Global/tenant/Spring property/environment override、三個explicit domain categories在attempt 1後直接park且不排next time、deadlock/timeout/temporary infrastructure/unknown/classifier failure維持retryable、exception message變更不影響classification、diagnostic不含secret/raw payload、parked row不自動選取、operator requeue保留exhausted history且不修改attempt rows、只增加一次執行機會、不inline投影、下一次Scheduler取得due row、再失敗append新attempt並立即re-park、failure metadata不標記applied或推進cursor、commit後crash replay no-op、獨立delta可不依queue順序完成但contiguous cursor不得skip gap、rebuild failure保留舊live scope、normal history逐步供Report使用、目前只有100+200 facts時Report算300、later 50投影後下一次query算350、query-time完整target公式、direction-monthly-only rounding、amounts-only response、data absence不回error/partial、invalid metadata/overflow/missing rule不回0/stale/partial value、100 targets中1個真正計算錯誤時整份Report不回amounts、並行projection commit時各target可讀不同latest committed state且不鎖projector | Exact after-commit signal、source inventory/lock/attempt technical primary key/columns/index/cursor/correction/rebuild schema、operator endpoint/role、overlap/non-reentry、G6 optimization、其餘Report API;Reconciliation仍待決 |
| A1 App integration | Cardholder facade/internal adapters | no new DB ownership | external contract regression、session scope E2E | release enable |
B5 House limit addendum — ADR-0258/0259/0260/0261/0262/0263/0264/0265/0266/0267/0268/0269/0270/0271/0272/0273/0274/0275/0276/0277/0278/0279/0280/0281/0282/0283/0284/0285/0286/0287/0288/0289/0290/0291/0292/0293/0294/0295/0296/0297/0298/0299/0300/0301: General Update固定為PUT /houseCard/{houseCardId} full replacement,allow-list為displayName、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit、expectedVersion。Create exact flat allow-list由ADR-0250的七欄基線擴充為九欄,新增dailyAmountLimit與dailyTxLimit;Create與Update的兩個limit properties都required-present且nullable,explicit null表示該欄fallback,omitted拒絕且不得mutation,只有non-null才作numeric validation。Create omitted時不得建立House或audit,non-null override與其他Create資料在同一transaction保存。Create semantic fail-fast保留既有七欄順序,最後依序驗證Amount與Tx,全部在lookup/mutation前完成;兩個limits都錯時先回Amount。Create與Update對兩欄共用exact mapping:omitted回[field, "required"]、present null合法、非number token回["request", "invalid request"]、zero/negative回[field, "must be positive"]、positive但Amount precision/scale或Tx fractional/range無法表示回[field, "invalid format"]。Update limit validation failure不得產生row mutation、version/modify metadata變更或audit。Update fixed precedence為authenticated route/role gate→House-local strict JSON與allow-list→displayName、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit、expectedVersion完整field validation→scope-protected target lookup/authorization→version comparison→target-dependent domain guards/mutation;invalid request優先且不觸發target lookup。DB欄位為daily_amount_limit DECIMAL(30,10) NULL與daily_tx_limit INT NULL,Java分別使用BigDecimal與Integer;非null值必須為目標type可無損表示的正值,超出precision/scale/range時拒絕且不得round/truncate/clamp。前者只計Deposit amount,後者跨Deposit+Withdrawal共享。兩欄各自依House non-null value→owning Agent override→Global system_config解析,mixed state有效;House-specific Global initial persisted defaults固定為amount 500000與shared tx 100000,不預建Agent rows。Agent config僅row absent時fallback;present-invalid及Global absent/invalid一律configuration-error fail closed,zero不是unlimited。Amount error只阻擋new Deposit,Tx error阻擋new Deposit+Withdrawal。BO page/detail對current requested page或target採all-or-nothing resolution;只解析實際需要的fallback,House non-null override不受未使用defaults影響。Create/Update/status以post-command configured state在mutation與audit前解析required effective values/sources;任何failure都不回partial/placeholder response,也不insert/update、改status/directions/limits、升版或寫audit,same-state status亦不回success。上述BO page/detail/create/update/status resolution failure固定回HTTP 500+HOUSE_LIMIT_CONFIGURATION_INVALID (40053),並須測試不重用40005/40044/40048/5000。Exact 24-field HouseCardVo新增兩個nullable configured、兩個required effective及兩個required HouseLimitSource { HOUSE, AGENT, GLOBAL } fields;effective/source只讀且不持久化,各欄獨立且mixed sources有效。ADR-0275另固定此configuration error的ResponseVo.data為required空陣列[];不得省略、回null/object/非空array,單筆或多筆、多dimension failure皆相同,且data不帶field/source/reason或config value。ADR-0276另固定:Frontend依code=40053選擇自己的i18n提示;backend message僅供人類診斷,不解析且不凍結exact wording,不作畫面提示的文案authority。本版House limit configuration error依ADR-0277採secret-safe server log/metric供維運診斷,不建立persistent BO alert/incident;失敗command仍不得寫入任何DB row,包含獨立transaction的alert/outbox/audit。ADR-0278固定診斷主體為實際失敗的config scope+key,Agent設定再以owning Agent identity定位;Global根因不因受影響Agent/House而改變,Agent row absent仍是正常fallback。此主體不決定event筆數或去重。ADR-0279固定本次House failure metric labels只使用固定、有限分類值,不帶Agent/House/request/trace等識別值;owning Agent透過診斷log定位,剩餘exact log/metric技術細節後由ADR-0282移交owning implementation slice。ADR-0280將三種numeric failure合併為一類;ADR-0281固定exact diagnostic reason為MISSING(必要Global row不存在)、BLANK(Agent/Global row存在但值為null/blank)、INVALID_NUMERIC(無法解析、非正值或無法無損表示)。Agent row absent仍是正常fallback;reason不放入ResponseVo.data,既有validation不變,ADR-0283固定House config讀取時先用Java 21 String.strip()去除首尾whitespace,present null或strip後empty為BLANK;其餘兩欄共用BigDecimal(String)數值語法,再依Amount正DECIMAL(30,10)與Tx正INT作無損檢查,Tx的100.0/1e2接受為100,fraction/overflow仍拒絕。不改寫DB原字串,也不改JSON request contract。ADR-0282將剩餘House telemetry技術契約交由owning implementation slice在既有約束下定案、記錄,並以測試與operations文件驗證,不再逐項阻擋#142設計收斂;此移交不包含config parser或quota lifecycle產品決策,也不授權立即實作。ADR-0284固定Daily Amount Limit與shared Daily Tx Limit都依House owning Agent的immutable fixed Business Zone切日,每日區間為[當地00:00,次日00:00)。此決策只固定日界與時區authority;初次占用時點後由ADR-0285固定。ADR-0285固定House新assignment成功與所需daily quota占用原子成立:Deposit占用Amount+shared Tx,Withdrawal占用shared Tx;in-flight也減少剩餘capacity,assignment失敗/rollback不留下占用。ADR-0286固定House Deposit初次Daily Amount quota按該筆已驗證Order requested amount全額預留,不扣fee、不預估actual amount;requested為1000時初次預留1000。成功完成的Amount計入基礎後由ADR-0290固定,與成功terminal原子生效後由ADR-0293固定。ADR-0287固定House初次quota以該次成功assignment所記錄的同一事件時間,換算owning Agent local date歸日;Deposit的Amount與shared Tx共用該時間,不各自讀現在時間。當地9/8 23:59建單、9/9 00:01成功assignment時,初次占用9/9 quota;assignment事件時間不要求等於DB commit時鐘。ADR-0288固定新日admission不計入前日仍in-flight的quota;舊占用保留原日,local midnight本身不觸發釋放,也不搬移或重複占用新日quota。Daily Amount與shared Daily Tx採相同原則。每日Amount上限1000、前日600仍進行中且新日尚無其他占用時,新日可再承接1000,跨日進行中總額可達1600;daily quota本身不限制全部跨日in-flight總量,也不保證實際銀行入帳日總量上限。ADR-0289固定已確認沒有該次資金交易且House assignment已正式終止時,完整釋放該次原占用的quota:Deposit釋放Amount+shared Tx,Withdrawal釋放shared Tx。僅到期、未收到通知、待人工判定或已收/付款但未完成,都不符合此釋放前提。反覆assignment後以無交易結案可重複使用額度,shared Tx不能兼作派發次數上限;讓無資金確認與正式終止兩條件俱備的轉換,與原quota完整釋放對admission原子生效,後由ADR-0299固定;確認權限、證據與各狀態mapping依ADR-0300交由#145 grill,定案前不得實作相關未決行為。ADR-0290固定House Deposit已依settlement規則合法成功完成且已有確認actual amount時,最終Daily Amount quota以該actual amount為計入基礎;例如requested為1000、actual為900且已合法確認成功,最終基礎為900。此決策不允許partial payment自動成功,也不直接採raw LINE notification作terminal actual;Actual缺失/無效與fee定義依ADR-0300交由#145 grill,不能由實作自行假設。ADR-0291固定同一次House assignment合法成功完成後,Amount與shared Tx quota仍歸該次initial quota date,不轉到完成日;Withdrawal的shared Tx同理。9/8 assignment預留600、9/9合法成功且actual也是600時,600仍計入9/8;9/9自身已承接900仍為900,不因前日成功結果增加為1500。此決策只選同一次assignment成功歸日;合法重派的新占用與歸日後由ADR-0296固定,重派許可與舊新銜接依ADR-0300交由#145 grill。ADR-0292固定House Deposit已確認真實入款且其餘settlement成功條件均成立時,actual差額造成原日Daily Amount超額也不阻擋成功,完整actual仍計入原initial quota date。例limit始終1000、原預留600、原日其他占用300,確認actual=800後合計1100、超額100;不得把actual裁為700或只為quota不足暫停成功。此例不選overage診斷的limit比較版本;telemetry與實作仍未決。ADR-0293固定House Deposit成功terminal與原日Amount quota由requested轉為確認actual,作為同一次原子狀態轉換對後續admission生效。原占用1000、合法成功actual=900時,成功轉換前不得先釋放100;轉換生效後該筆貢獻為900,其他gates通過才可使用剩餘100。增額與ADR-0292允許的overage同時反映,不得成功後仍留下較小requested造成虛假capacity;跨日只調整原日,不增加今日額度。Exact counter/transaction/lock實作仍未凍結,不要求與外部資金系統形成分散式transaction。ADR-0294固定同一次House assignment合法成功完成後,shared Daily Tx持續保留原占用1筆;Deposit與Withdrawal相同,成功不再加1,也不釋放這1筆。原日總數9筆中一筆由in-flight變成功,仍為9,不變10或8;對admission的貢獻持續為1,不出現中間0或2,跨日仍歸initial quota date。Reserved/completed是否分欄與更新方式交實作;多次重派總數、已發生資金但失敗、人工更正與partial settlement規則仍未決。ADR-0295固定House effective limit合法變更後,新assignment立即採當下新上限,既有每日quota事實保留,不延用當日舊上限,也不因改limit重設/回收占用或取消既有assignment。當日占用800、上限由1000合法降至600,占用仍800且無可用Amount;再合法提高至1200、其他條件不變時可用400。Amount與shared Tx相同;此決策以變更已合法成立為前提;House general Update低於當日占用的儲存規則後由ADR-0298固定,Agent/Global config writer政策與overage診斷比較版本仍未決。ADR-0296固定合法re-dispatch產生的新House assignment,在目標House通過所有gates的前提下,依當下effective limit原子取得自己的quota:Deposit按已驗證requested amount全額占用Amount+shared Tx 1,Withdrawal占用shared Tx 1;新quota date由新assignment記錄的同一事件時間換算目標House owning Agent local date,不沿用舊assignment的占用資格或日期。舊assignment歸9/8、新assignment記錄於當地9/9時,新占用歸9/9;舊占用依原lifecycle處理,不推定已釋放。本決策不授權re-dispatch或選定目標允許範圍;原assignment尚未終止時的失敗效果後由ADR-0297固定,成功替換的舊新占用銜接與相關起始狀態處置依ADR-0300由#145完成產品決策。ADR-0297固定原House assignment尚未終止、且其他規則已允許替換時,若re-dispatch無法成功建立新assignment,這次失敗command保留原assignment/current association、quota與deadline,不因失敗先終止或釋放舊assignment;新目標不留下成功assignment或新占用。例A仍承接Deposit並占用Amount 600+shared Tx 1,合法改派B因容量不足失敗,在沒有其他獨立狀態轉換時,A維持原關係、600+1與原deadline。本規則只限制該失敗command,不阻止獨立expiry/settlement,不復活已終止assignment,也不撤銷先前已提交的結案/release;成功替換的舊新占用銜接依ADR-0300由#145完成產品決策。ADR-0298固定House general Update在其他驗證、權限、version與required config resolution均通過時,允許post-command effective limit低於owning Agent當日已占用quota,不把低於占用本身設為儲存失敗條件;Amount與shared Tx相同。占用800、原上限1000,Update改600可成功;explicit null清除override後繼承有效default 600亦相同。既有占用仍800,新admission依ADR-0295暫無該項capacity,不重設占用或取消既有assignment。Positive/lossless numeric與其他既有guards維持;本決策只涵蓋House general Update,Agent/Global config writer政策仍未決。ADR-0299固定讓House assignment首次同時滿足已確認無資金交易+正式終止的狀態轉換,與適用原quota的完整釋放,作為同一次原子結果對後續admission生效。Deposit釋放原Amount+shared Tx,Withdrawal釋放原shared Tx;不得讓兩條件俱備的結果已生效卻仍等待背景工作交還quota。兩條件可先後成立,只規範使第二個條件成立的轉換;release仍歸原quota date,前日釋放不增加今日額度。確認權限/證據/狀態mapping、重派舊新銜接、外部balance reservation與具體SQL/lock實作不由本決策選定。ADR-0300固定#142保留已接受House quota約束並繼續收斂核心Ownership/Master Schema;剩餘Order/settlement產品決策明確交由#145 grill,包括actual amount確認/有效性/fee定義、無資金確認權限/證據/狀態mapping,以及合法重派與成功替換的舊新銜接。#145定案前不得實作相關未決行為,也不得從quota結果反推partial成功、確認權限或重派許可。#144協作ledger/reservation/reconciliation,#146協作routing selection/fallback;Agent/Global config writer政策不由本次移交定案。這是設計責任移交,不代表#142完成、不變更既有SQL交付順序,也不授權Java/SQL/DB或GitLab操作。ADR-0301固定House Daily Amount與shared Daily Tx的quota identity為houseCardId;即使Agent/Currency/Bank/canonical account相同,不同House IDs也不合併或繼承彼此當日占用。A今日已成功計入800,合法停用A後建立並啟用新ID的B,兩張上限均1000且B無其他占用時,B可承接完整1000,同帳戶當日跨ID合計可達1800。舊quota與Order/Attempt歷史保留原ID,不搬移或重寫;單純同ID停用/啟用不是新identity,不因此重設其占用。新承接仍須通過全部gates,ACTIVE-only uniqueness維持;本決策不新增重派、settlement或SQL規則。其他未決事項須依其owning scope繼續收斂,不得將責任移交當作產品規則已定案。Exact migration collision policy、presence tracking、其他釋放與重派lifecycle仍待決。
First Backend UAT Cut
B1 → B2 → B3 → B4 → B5完成後,Supply switch維持OFF,但後台可驗收:
- actor login/MFA;
- Supplier/Gateway/allocation/direction config;
- Cardholder owner/credential/session administration;
- Payment Account application、review、status、availability、priority、balance adjustment;
- House Card master管理,不顯示authoritative balance或Withdrawal routing action。
Blocked Decisions by Later Slice
| Slice | Remaining decision |
|---|
| B1 | Realm-specific response exact fields、PASSWORD_CHANGE_REQUIRED HTTP status/nullability與SystemCode;response type、formal-session epoch、password-change token slot、統一單次登入與MFA OFF/ON語意已由ADR-0090/0082/0083/0088/0089決定 |
| D1 | 候選排序演算法與外部 adapter;HOUSE 僅為 Deposit 候選。 |
| W1 | 候選排序演算法與外部 adapter;HOUSE 不參與任何 Withdrawal path。 |
| R1 | Currency.decimalNumber已固定0..10;invalid metadata fail closed、使用後immutable。Existing eligible fact correction固定保留原fact並以source-linked immutable delta修正原canonical bucket。Controlled rebuild只在原子替換既有live bounded scope時先產生隔離scope;normal historical projection不等all-source coverage。ADR-0209固定Commission Report query-time latest calculation;ADR-0210經ADR-0213縮限為真正計算錯誤的all-or-nothing failure;ADR-0213固定available-facts semantics、cursor不作Report gate、不需要activation boundary,且business response只回amounts或真正calculation failure。ADR-0214固定允許per-target latest committed reads、不保證request-wide snapshot且不強迫N+1 query。ADR-0215固定per-fact atomic application、獨立delta order不保證與cursor no-skip。ADR-0216固定after-commit targeted attempt+Scheduler bounded recovery、bounded candidate fetch/execution budget不形成batch commit,SKIP LOCKED/claim-token/lease維持G6 evidence-gated。ADR-0217固定同transaction ownership/status guard+apply,application rollback後另記failure attempt,且failure metadata不得推進cursor。ADR-0218固定retry exhaustion後park為NEEDS_ATTENTION,audited operator command只requeue同一row並等待Scheduler,保留history、再失敗立即re-park,SQL只作break-glass。ADR-0219固定initial execution包含在5 total attempts內;只有guard winner且實際進入projector才計數,failures 1/2/3/4依序延遲1/5/15/60分鐘,failure 5直接park且沒有180-minute fallback;operator requeue不重設history,只提供一次額外執行機會。ADR-0220固定projector-owned Java domain policy,不提供Global/tenant/property/environment runtime override。ADR-0221固定三個explicit deterministic domain categories可提早park,transient與unknown預設retryable。ADR-0222固定separate immutable attempt table+queue current state,attempt insert與queue update同transaction commit;ADR-0223固定queue identity + committed failure sequence queue-local unique ordinal;ADR-0224固定Scheduler bounded recovery每1分鐘於UTC整分鐘第0秒觸發,nextRetryTime只代表最早eligibility且HTTP delivery retry不增加projector attempt count;ADR-0225固定automatic-selectable due rows依logical eligibility time ASC、queue identity ASC admit,queue identity只作tie-break且order不保證targeted/application/completion順序;ADR-0226固定每個logical fire最多admit 50 IDs,no-op/loser不退回slot且same-fire delivery retry不取得新budget;ADR-0227固定從第一次candidate query前起算的30秒soft execution budget,active per-fact/failure-record transaction不hard-cancel。接著確認overlap/non-reentry |
| A1 | Supply Cardholder exact external compatibility contract由#148凍結;ADR-0304已固定跨Gateway全域username identity,不再要求Gateway login discriminator |
| BO cross-realm | 無;Prefix resource-action matrix已由ADR-0084決定,SystemCode編號納入B0 contract review |
重要concept仍有4項;登入/menu authentication realm、MFA switch、response type與Prefix menu scope已無domain-level未決問題。B1下一個blocking contract是PASSWORD_CHANGE_REQUIRED的HTTP status與exact fields;B1/B5其餘只剩B0 token/error contract review與migration執行gate。Commission Accounting Month依Prefix/Supplier matched configured Business Zone,沒有全域+08:00;Agent offset建立後immutable,Supplier offset mutation依#142 note47090保留且不重算歷史且Order首次成功routing保存offset。Report固定UTC hourly且不拆三張actor tables;一般Report與Commission Report共同讀取Hourly Financial Aggregate。Commission Report query-time顯示target interval內目前已接受facts的latest result,existing eligible fact correction以immutable delta回投原canonical bucket;controlled rebuild只替換既有live bounded scope,normal history不等full-scope coverage,cursor不作Report gate,也不需要activation boundary。Current Result與backend paid/unpaid lifecycle已排除。Business response只回amounts或真正calculation failure;ADR-0214固定各target可讀實際query時的latest committed aggregate且不保證request-wide snapshot。ADR-0215固定per-fact transaction原子完成identity/delta/applied evidence/queue completion,獨立delta可不依queue順序套用但cursor不得skip gap。ADR-0216固定source commit後targeted attempt與Scheduler bounded recovery共用projector;bounded fetch/execution budget仍逐筆commit。ADR-0217固定同一transaction取得ownership/status guard並apply,失敗rollback後另以短transaction記錄failure evidence;failure metadata不完成queue或推進cursor。ADR-0218固定達retry門檻後park且停止automatic selection;BO operator只requeue同一row並等待Scheduler,保留history,再失敗立即re-park;SQL只作break-glass。ADR-0219固定5 total attempts(含initial execution),只有guard winner實際進入projector才消耗attempt;failures 1/2/3/4後依序等待1/5/15/60分鐘,failure 5直接park且沒有180-minute fallback;operator requeue不重設exhausted history,只增加一次執行機會。ADR-0220固定Java-only retry policy,不提供runtime override。ADR-0221固定只有explicit deterministic domain allow-list提早park,deadlock/timeout/temporary infrastructure與unknown仍retry。ADR-0222固定failure attempt append至separate immutable table、queue只保存current state,兩者同transaction commit;Scheduler不join history,operator requeue不修改attempt rows。ADR-0223固定queue-local committed failure sequence,未commit不占用durable ordinal。ADR-0224固定Scheduler bounded recovery每1分鐘於UTC整分鐘第0秒觸發,nextRetryTime到期後可能再等不到60秒,HTTP delivery retry與projector attempt count分離。ADR-0225固定automatic-selectable due rows依logical eligibility time ASC、queue identity ASC admit,queue identity只作tie-break且order不保證targeted/application/completion順序。ADR-0226固定每個logical fire最多admit 50 IDs,no-op/loser不退回slot、same-fire delivery retry不取得新budget且targeted不消耗cap;ADR-0227固定從第一次candidate query前起算的30秒soft execution budget,active per-fact/failure-record transaction不hard-cancel。Exact after-commit signal、lock/attempt technical primary key/columns/index、cursor schema、operator endpoint/role、overlap/non-reentry與其餘projection lifecycle/API仍待決。
Diagram
開啟backend-first rollout flow
Canonical Time Model
- DB event timestamps一律UTC
DATETIME(6)。 - Supplier/Supply Gateway/Cardholder的Commission Accounting Month,將UTC occurrence依Agent與Gateway owner Supplier已匹配的configured Business Zone offset換算為
YearMonth;沒有全域+08:00fallback。 - 此configured-zone boundary控制rule
effectiveMonth、PENDING_NEXT_MONTH target、各方向eligible monthly volume與query-time Direction Monthly Commission Result;Agent–Supply Gateway Allocation必須Currency與Business Zone同時相同。 - Report canonical period固定為UTC
+00:00 hourly half-open window,且不建立Prefix/Supplier/Superadmin三張actor-specific physical tables。 - Eligible terminal facts先投影到persisted Hourly Financial Aggregate;一般Report讀它做filter/mask/aggregation,Commission Report query則依Commission Accounting Month+owner+currency+direction加總eligible amount後套一次完整月公式。不得逐UTC hour、viewer timezone或current owner zone計算Commission。
Commission Month Example
同一筆completed_time = 2026-08-31T16:30:00Z會依保存的configured offset得到不同Commission月份,但Report hour固定不變:
| Boundary | Interpreted time/bucket | Result |
|---|
Configured Business Zone +07:00 | 2026-08-31 23:30 +07:00 | Commission month 2026-08 |
Configured Business Zone +08:00 | 2026-09-01 00:30 +08:00 | Commission month 2026-09 |
| Canonical Report UTC hour | [2026-08-31T16:00:00Z, 2026-08-31T17:00:00Z) | Report hour 2026-08-31T16:00:00Z |
Hourly Financial Aggregate同時保存UTC hour與Commission Accounting Month dimensions;若canonical ±HH:MM offset使同一UTC hour跨月界,必須依Commission month拆row。Report可以用其他timezone呈現label,但不得改變已依offset snapshot歸屬的Commission month。
Available-facts Interval Example
查詢+07:00的2026-08 Commission month時,若目前Hourly Financial Aggregate只有08-15的100與08-20的200,Report先顯示300。08-01..08-14沒有facts或某source尚未追上都不阻擋Report、不補zero facts,也不標示partial。之後08-25的50完成投影,下一次相同query顯示350。月份仍依各fact保存的configured offset與canonical occurrence判定;計算不需要owner activation timestamp。
Accepted Boundary
- Business Zone是canonical fixed offset
±HH:MM,有效範圍 -14:00..+14:00,不接受 IANA region ID、Z 或省略格式,也沒有DST。 - Agent與Supplier在Create時明確提交canonical
businessUtcOffset;Agent offset建立後immutable,是 House daily boundary 與 Agent–Gateway effective-binding 的 time authority。Supplier依較晚accepted note47090保留System Admin-only mutation、commit後新transaction立即生效、舊daily counts與facts不搬移。Supplier mutation 若使既有 allocation offset 不匹配,歷史 allocation 保留待受控處置,但 Agent read 與 routing 都 fail closed;歷史backfill不是future Create default。 - Deposit與Withdrawal分開累計,但同一effective Agent–Supply Gateway chain共用已匹配的Commission month offset。
- 第一筆Supply Order成功claim/routing先驗證Agent與Gateway Currency相同,且Agent與Gateway owner Supplier的Business UTC Offset相同,再保存共同offset;Canonical fact需保存UTC occurrence、Commission Accounting Month、Currency、direction、owner chain、該routing offset snapshot與rule revision/inputs identity。Month仍以terminal eligible occurrence換算,不使用claim time當month occurrence。
- 一般Report與Commission Report共同讀取authoritative Hourly Financial Aggregate。它保存eligible inputs,不保存hourly Commission result;Commission Report先彙總整月eligible amount,才套threshold/base fee/rate一次。
- Hourly Financial Aggregate的
eligible_amount固定使用DECIMAL(38,10);source fact與受控rebuild只做exact decimal addition,不在hour boundary套Currency display rounding。無法無損表示時fail closed,不clamp/truncate/round。 - UTC hour boundary只決定bucket,不是acceptance cutoff。Late-arriving且此前未投影的全新eligible fact依canonical source identity恰好一次更新原hour row;duplicate delivery不得再次增加
eligible_amount或fact_count,arrival time不得改分桶。 - 已投影fact的amount correction保留原fact,追加source-linked immutable signed delta,並沿用原fact的canonical occurrence與offset snapshot恰好一次修正原UTC hour/Commission month。原amount 100更正為90時原bucket exact減10;correction arrival time不建立新bucket,後續Commission Report query反映latest value。
- 每筆成功claim的projection work以per-fact transaction共同commit fact/correction identity、aggregate delta、applied identity與queue
PROCESSED。互相獨立且signed delta已確定的facts可不依queue ID或claim time套用;final total由exact addition與exactly-once application保證,cursor仍不得越過pending/failed gap。 - Source transaction commit durable queue row後,best-effort targeted attempt與Scheduler bounded recovery共用同一per-fact projector。Recovery每輪最多admit 50 IDs並使用固定30秒soft execution budget,但candidate仍逐筆commit;budget從第一次candidate query前以monotonic elapsed time起算,active per-fact/failure-record transaction不hard-cancel;batch commit、
SKIP LOCKED與claim-token/lease不作baseline,只有G6具體證據成立後才逐項評估。 - Commission Report使用available-facts semantics:target interval內目前已接受facts都參與計算;資料缺漏、尚未抵達或coverage未知只是不計入,不阻擋Report、不標partial,也不合成zero fact。Later fact/correction完成投影後由下一次query反映,不需要activation boundary。
- Per-source/partition contiguous cursor只描述projector progress/resume/operations,不是Report completeness gate;normal historical projection不等待所有source full-scope coverage。Controlled rebuild仍從immutable original facts與accepted corrections在隔離shadow result重建明確bounded scope,但只用於原子替換已上線scope;validation失敗保留舊live scope,Report query只看完整舊版或完整新版。
- 每個owner+currency+direction整月只在Direction Monthly Commission Result round一次。Commission Report query即時計算
DECIMAL(38,16) exact與DECIMAL(38,10) final representable values,Final使用Currency.decimalNumber/HALF_UP,scale只接受0..10且使用後immutable;result不保存成pair、snapshot或Current Result。只有invalid Currency metadata、numeric overflow或missing applicable PT rule等真正計算錯誤才fail closed,不得回commission 0或previous cached value;任何requested target有這類錯誤時整份Report不回傳Commission amounts、partial rows、summaries或totals。Spay4 backend不管理paid/unpaid、settlement、outstanding、recovery或disposition。 - Historical retry/recovery/re-dispatch/rebuild必須重用first successful claim/routing保存的immutable offset snapshot,不得讀current master重新分月。
Remaining Report/Reconciliation Decisions
- Hourly Financial Aggregate除
eligible_amount DECIMAL(38,10)以外的exact columns/unique key、required source inventory/partition topology、cursor type/state placement、canonical source/correction identity columns、attempt technical primary key/columns/index、correction ordering、after-commit signal、lock/conditional SQL、failure-attempt/queue retry persistence、operator endpoint/role/error、overlap/non-reentry、shadow storage/atomic replacement、coverage/checksum fields、concurrent catch-up、G6 optimization與retention; - R1 report fields、API、actors、masking與authority;Deposit/Withdrawal 營運工作台的 list、detail、CSV、Action、viewer display offset 與日期 filter 已由 Order Workspace Contract 定義,且不得演變成新的 summary report;
- table/API/VO與export contract;
- reconciliation run/item grain與evidence;
- Commission Report HTTP/error fields、query plan、timeout、cache/materialized fallback與historical display;ADR-0209已排除result/payment/recovery schema,ADR-0210經ADR-0213縮限為真正計算錯誤的Report-level all-or-nothing failure,ADR-0213已固定available-facts與amounts-only response semantics並supersede ADR-0211/0212,ADR-0214已固定per-target latest committed reads且不保證request-wide snapshot。
Shared Hourly Financial Aggregate topology、eligible_amount DECIMAL(38,10) no-hourly-rounding、late-arriving new fact exactly-once inclusion、existing eligible fact immutable correction delta回投原canonical bucket、controlled rebuild、per-source/partition projector cursor、per-fact atomic application、after-commit targeted attempt+Scheduler bounded recovery、same-transaction claim/apply、rollback後separate failure attempt、Java-only 5-total-attempt retry schedule、explicit deterministic early-park classification、separate immutable attempt table+queue current state、queue-local committed failure sequence、retry exhaustion park+audited operator requeue與available-facts query-time latest Commission已選定;ADR-0209排除result/payment/recovery persistence,ADR-0210經ADR-0213縮限為真正計算錯誤的all-or-nothing failure,ADR-0213固定normal history不等full-scope coverage、Commission不需要activation boundary,且business response只回amounts或真正calculation failure;ADR-0214固定各target可讀取實際query時的latest committed aggregate,同一request不保證單一point-in-time snapshot且不強迫N+1 query;ADR-0215固定每筆identity/delta/applied evidence/queue completion共同commit、獨立delta不保證application order且cursor不得skip gap;ADR-0216固定兩條trigger共用projector、bounded candidate ID fetch/execution budget不形成batch commit,SKIP LOCKED/claim-token/lease只由G6 evidence gate;ADR-0217固定同一transaction取得ownership/status guard並apply,failure metadata不得標記applied或推進cursor;ADR-0218固定達門檻後park為NEEDS_ATTENTION,audited BO operator command只requeue同一row並等待Scheduler,保留history、再失敗立即re-park,SQL只作break-glass;ADR-0219固定initial execution包含在5 total attempts內,只有guard winner實際進入projector才消耗attempt,failures 1/2/3/4後分別等待1/5/15/60分鐘,failure 5直接park且沒有180-minute fallback;operator requeue不重設exhausted history,只提供一次額外執行機會;ADR-0220固定retry values由projector-owned Java domain policy提供,不新增Global/tenant/property/environment runtime override;ADR-0221固定三個explicit deterministic domain categories可在attempt 1至4直接park且不排next time,deadlock/timeout/temporary infrastructure、mutable reference暫時缺少、unexpected runtime exception、unknown code與classifier failure仍retry,classification不得解析exception/SQL message;ADR-0222固定attempt insert與queue current-state update同transaction commit,Scheduler不join history、operator不修改attempt rows且不使用queue JSON history;ADR-0223固定queue identity + committed failure sequence queue-local unique ordinal,未commit不占用durable sequence且不得使用未受guard保護的MAX + 1;ADR-0224固定Scheduler bounded recovery每1分鐘於UTC整分鐘第0秒觸發,nextRetryTime到期後可能再等不到60秒,HTTP delivery retry不增加projector attempt count;ADR-0225固定automatic-selectable due rows依logical eligibility time ASC、queue identity ASC admit,queue identity只作tie-break且order不保證targeted/application/completion順序;ADR-0226固定每個logical fire最多admit 50個candidate IDs,no-op/loser不退回slot且same-fire delivery retry不取得新budget;ADR-0227固定從第一次candidate query前起算的30秒soft execution budget,active per-fact/failure-record transaction不hard-cancel。在exact after-commit signal、lock/attempt technical primary key/columns/index/cursor schema、operator endpoint/role、overlap/non-reentry及其餘欄位級projection lifecycle完成前不建立DDL或API;測試至少覆蓋configured offset兩側月界、同UTC hour跨Commission month時拆row、Prefix/Supplier mismatch fail-closed、offset snapshot retry一致性、requested interval只有08-15與08-20 facts時只加總現有資料、later 08-25 fact在下一次query加入、missing interval與cursor gap都不阻擋Report、response不含freshness/completeness/partial/projector progress、UTC hourly half-open boundary、scale-10 exact addition/overflow fail-closed、late first-delivery updates original bucket、duplicate no-op、source commit後targeted success、targeted未執行後下一個UTC整分Scheduler recovery、UTC 10:02:20到期row不早於10:03:00 fire取得、HTTP delivery retry不增加projector attempt count、initial/retry/operator-requeue eligibility來源、earliest-time-first與same-time queue identity tie-break、低ID較晚due不得插隊、candidate query無nullable OR/indexed-column COALESCE/function、candidate order不保證application order、兩條trigger競爭single winner且loser不計attempt、fetch-only candidate不計attempt、每fire最多admit 50 IDs、no-op/loser不退回slot、same-fire delivery retry不取得新budget、budget在candidate query前起算、query耗盡budget時零筆開始、elapsed等於30秒不開始下一筆、active transaction跨deadline仍完成且仍逐筆commit、同transaction ownership/status guard與四項atomic write共同rollback、attempt insert與queue current-state update共同commit/rollback、queue-local sequence從1遞增且duplicate ordinal被拒絕、failure-record rollback不消耗durable sequence、Scheduler candidate query不join history、operator requeue不修改attempt rows、failure-record transaction失敗仍可重試、failures 1/2/3/4各自排定1/5/15/60分鐘、failure 5 park且不排180分鐘、Java-only policy且沒有runtime fallback、三個allow-list category各自在attempt 1後直接park且nextRetryTime = null、deadlock/timeout/temporary infrastructure/unknown/classifier failure仍retry、exception message變更不影響classification、diagnostic不含secret/raw payload、parked row不自動選取且cursor不越過、operator requeue保留exhausted history且只增加一次執行機會、不inline投影、Scheduler取得due row、額外嘗試失敗append新row並立即re-park、failure metadata不標記applied或推進cursor、commit後crash replay no-op、獨立gap後fact先完成但cursor不skip、gap補齊後cursor前進、既有fact 100以linked delta -10修正原bucket為90且arrival period不變、controlled rebuild中途失敗保持舊live scope、normal historical projection可逐步供Report使用、每次query由latest hourly inputs彙總後只套一次target公式、並行projection commit不鎖住Report且後續target/query可見latest committed state、correction/rebuild後歷史月份顯示latest value、invalid metadata/overflow/missing rule不回0或stale result,以及100個requested targets中1個真正計算錯誤時不回傳其餘99個amounts。
Foundation and Backoffice Table Fields
Source Drift — 2026-08-22: 本文件的superseded Supply actor relation全域username uniqueness、split login/menu及SUPPLY_GATEWAY_ALLOCATION menu seed/Prefix grant已被ADR-0104與ADR-0105取代。Frontend請先讀接線手冊;DDL exact change仍待implementation contract freeze。
Shared Column Convention
除security/append-only特製表外,mutable business table使用:id BIGINT、status VARCHAR(20)、creator VARCHAR(100)、create_time DATETIME(6)、modifier VARCHAR(100)、modify_time DATETIME(6);需要concurrent mutation者另有version BIGINT。所有關聯是service-guarded logical relation,不宣告DB foreign key。
#142 核心 Master/Reference 逐欄契約
本節交付Issue #142 AC-1~AC-3、AC-6 的字典;後面的 B1/candidate inventories 保留來源與下游接口用途,不表示全部已是 SPAY4 runtime 或可執行 DDL。Authority 依已接受 ADR 與 #142 notes 47089/47090/47096;較晚決策優先。House 詳細 command 與 quota 規則仍以本文件後段及 ADR-0231~0302 為準。
字典讀法與來源分類
R(reuse):重用既有 master/reference responsibility 及下列明列的欄位;不是把歷史 DDL 全表照搬。Current SQL facts 來自 v0.1.1、v0.2.1、V1、V2 與本次 baseline entity;沒有連線讀取 live DB。T(target change):既有 root 上由 accepted contract 要求的改變,例如 canonical offset、Gateway Currency、Config UQ。P(planned-only):accepted target 的新 persistence,尚不是 runtime 實作。I(interface):已接受的 owner/identity 接口;其 principal、financial、routing 或 compatibility 完整 lifecycle 由指定 Issue 收斂。不能從本字典推導下游尚未接受的 default、state 或 API。無表示不宣告 persistent DB default,由合法 BO transaction 明確提供;NULL表示 SQL NULL default 且 absence 有明列語意。只有表格明寫的 literal 才是 DB default;Java initializer、Create 初始值與 seed row 都不能冒充 DB default。- 每列未列 UQ 的欄位均不新增單欄 UQ;表末的 composite PK/UQ 才是完整 grain。關聯全為 logical reference,由 BO transaction 保證;不宣告 DB FOREIGN KEY。新 target DDL 仍須
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 與逐欄 SQL COMMENT,SQL package 留給 #152。 - 下列新 mutable target roots 展開共用
M base;沿用既有 physical columns 的 R roots 展開 L base。這是逐欄來源區分,不能將 L 的歷史長度/nullable 偷換成 M 並宣稱無 migration。Supplier/Gateway 的現有 base 差異另列於各表。
Base columns(每張表依下列 mapping 完整展開)
| Base/column | SQL type | Nullable | DB default | PK/UQ | Authority、語意及 query intent |
|---|
M/id | BIGINT AUTO_INCREMENT | 否 | 無;DB 產號 | PK | immutable row identity;detail、relation、穩定分頁 tie-breaker |
M/status | VARCHAR(20) | 否 | 無 | — | BO row lifecycle current value;與 feature operational status 分開;soft delete 保留 identity/history |
M/creator | VARCHAR(100) | 否 | 無 | — | BO authenticated audit identity;建立時固定,不接受 request 自填 |
M/create_time | DATETIME(6) | 否 | 無 | — | BO 建立事件 UTC instant;immutable audit fact,非 business date |
M/modifier | VARCHAR(100) | 是 | NULL | — | 最近合法 mutation actor;未修改可空,不保存 credential |
M/modify_time | DATETIME(6) | 是 | NULL | — | 最近合法 mutation UTC instant;未修改可空,不因 read 或 no-op 更新 |
M/version(有標示者) | BIGINT | 否 | 0 | — | BO mutation concurrency token;同 transaction 推進,非 history/session generation |
L/id | BIGINT AUTO_INCREMENT | 否 | 無;DB 產號 | PK | 同 M.id;重用既有 surrogate key responsibility |
L/status | VARCHAR(45) | 否 | 無 | — | 既有 row lifecycle current value;不能由 Java ACTIVE initializer 推導 DB default |
L/creator | VARCHAR(50) | 否 | 無 | — | 既有建立者 audit fact |
L/create_time | DATETIME | 否 | 無 | — | 既有建立 UTC timestamp;精度升級若需要由 #151/152 明列 |
L/modifier | VARCHAR(50) | 是 | NULL | — | 既有最近異動者,未修改可空 |
L/modify_time | DATETIME | 是 | NULL | — | 既有最近異動時間,未修改可空 |
| Table | Source class/base expansion | 例外及邊界 |
|---|
agent、currency、bank、system_config、agent_system_config | R+L 全部六欄(含 id) | 只改下列明列 T;不為 #142 增加 version 或更改 writer policy |
supplier | R;id 同 L;其餘依下方 Supplier base 表 | v0.2.1 與一般 L 的 nullable 不同,不能忽略 |
supply_gateway | R/T;id 同 M;其餘依下方 Gateway base 表 | 保留 V2 physical base,Currency 是 target change |
supply_gateway_agent_allocation、supplier_direction_config、supply_gateway_direction_config、house_card | P+M 全部七欄(含 version) | direction child version 只是內部 concurrency;Gateway root version 才是對外 command token |
supply_cardholder、payment_account | I/P+M 的 owner-root 接口 | 只凍結下列 ownership/financial identity 欄位;完整 principal/financial base 與新增欄位分別由 #143/144 接手,不把候選 inventory 宣稱完整 schema |
agent_bank | R;special relation,不套 M/L | 只有下列兩個 reference columns,無 surrogate id、status 或 audit columns |
Agent
Source:R Agent entity 與 v0.1.1;T offset 依 ADR-0194/0284、CONTEXT 與 #142 note47090。Agent 沒有獨立 name 欄位;prefix 是顯示來源,不能憑 VO display 建造新 master field。
| Column | SQL type | Nullable | DB default | Class/authority/immutable 或 current 語意 |
|---|
currency_id | BIGINT | 否 | 無 | R;Currency reference,Agent 建立後 immutable;House、Order Currency authority |
prefix | VARCHAR(20) | 否 | 無 | R;immutable Agent readable identifier;UQ (prefix),relation 一律用 id |
business_utc_offset | CHAR(6) | 否 | 無 | T;Agent 明確建立的 immutable ±HH:MM;House 日界 authority;取代 target 中 legacy business_zone,不保留兩個 current authority |
callback_url | VARCHAR(500) | 是 | NULL | R/I;現有 integration current setting;#148 決定 adapter 使用,不當 Order callback history snapshot |
telegram_token | VARCHAR(300) | 是 | NULL | R/I;現有 notification setting,secret 不輸出;#148/151 決定 compatibility/migration 處置 |
telegram_target | VARCHAR(100) | 是 | NULL | R/I;現有 notification destination;不作 business owner |
secret_token_id | BIGINT | 是 | NULL | R/I;現有 credential reference;#143/148 凍結 target credential interface,不在本 Issue 擴張 auth authority |
is_custom_channel_sort | BOOLEAN | 是 | 0 | R/I;v0.2.1 legacy channel 排序開關,並非 Gateway routing policy |
既有 is_custom_channel_sort 及 agent_channel/PaymentChannel 關聯是 legacy routing 接口,不提升為 SPAY4 Gateway policy;#146/148/151 決定對應。Java businessZone = "+07:00" 與舊 SQL 的同值 default 是 current-source fact,不適用 target business_utc_offset。Offset 僅接受 canonical ±HH:MM,範圍 -14:00..+14:00,不接受 IANA timezone、Z 或省略格式;格式及 minute 有效性由 application 驗證。Query:PK 支援 exact scope/lock,prefix UQ 支援 exact lookup;allocation 使用 Agent id,不替每個 display/setting 建 index。
Supplier
R source 為 Supplier entity、v0.2.1、V2;canonical identity 依 ADR-0156/0157,name non-unique 依 accepted Supplier Display Name。以下既有 physical base 是來源字典;未宣告 nullable legacy audit 在本 Issue 自動 tighten。
| Column | SQL type | Nullable | DB default | Class/authority/語意 |
|---|
status | VARCHAR(20) | 是 | ACTIVE | R;v0.2.1 row lifecycle;新 Create 仍依 ADR-0158 由 BO 明確寫 ACTIVE,不靠 nullable/default 判定合法 onboarding |
creator | VARCHAR(50) | 是 | NULL | R;建立 actor audit fact,legacy 可空 |
create_time | DATETIME | 是 | NULL | R;建立 UTC audit fact,legacy 可空 |
modifier | VARCHAR(50) | 是 | NULL | R;最近 mutation actor |
modify_time | DATETIME | 是 | NULL | R;最近 mutation UTC instant |
code | VARCHAR(50) | 否 | 無 | R/T;與 login_id 同一 canonical uppercase value;immutable、UQ (code) |
login_id | VARCHAR(50) | 否 | 無 | R/T;同 transaction 由 canonical loginId 同值寫入;immutable、UQ (login_id) |
name | VARCHAR(50) | 否 | 無 | R;mutable non-unique display;trim 後 1..50,大小寫/Unicode/內部空白保留 |
business_utc_offset | CHAR(6) | 否 | 無 | T;current time authority;Create 明確提交,只有 System Admin 可合法修改,commit 後新 transaction 採用 |
url | VARCHAR(512) | 是 | NULL | R/I;legacy integration endpoint;不是 Supply owner/Currency;#148/151 處置 |
secret_key | VARCHAR(50) | 否 | 無 | R/I;現有 UQ (secret_key) credential compatibility 欄位;不得進一般 BO response;#143/148/151 決定 target credential mapping |
level | INT | 是 | 0 | R/I;legacy Supplier 排序設定;不授予 #146 新 routing priority 語意 |
Supplier 不保存單一 Supply Currency;可透過不同 Gateways 經營多幣別。新 Supplier onboarding 必須原子保存 code = login_id;code/loginId 任一 collision 都 rollback,既有 mismatch 交 #150/151,禁止靜默修復。PK/兩個 identity UQ 支援登入/owner exact lookup;name 模糊搜尋不宣稱 B-Tree 加速。Mutable offset 不複製至 Gateway;歷史 facts/counts 不搬移、不重算。Offset mutation 遇 existing allocation mismatch 的 command 處置由 #146/149 定案。
Supply Gateway
R source 為 V2;T Currency 依 note47089,owner 依 #137/note47096。Supplier 可建立、管理與 soft-delete 旗下 Gateway;System Admin 的 hierarchy authority 保留,Agent allocation mutation 仍為 System Admin-only。Gateway code UQ 是 Supplier scope 內永久保留,不是全域 code UQ,也不加入 lifecycle status。
| Column | SQL type | Nullable | DB default | Class/authority/語意 |
|---|
supplier_id | BIGINT | 否 | 無 | R;唯一 immutable owner reference;不得 reassignment |
currency_id | BIGINT | 否 | 無 | T;required immutable Gateway Currency;改 Currency 必須建立新 Gateway |
code | VARCHAR(50) | 否 | 無 | R;immutable owner-scoped code;UQ (supplier_id, code) |
name | VARCHAR(100) | 否 | 無 | R;mutable、non-unique display,不能當 identity |
supply_gateway_status | VARCHAR(20) | 否 | ACTIVE | R;V2 default,target operational values 只含 ACTIVE/INACTIVE;不代表 row soft-delete |
version | BIGINT | 否 | 0 | R/T;status 與兩 direction limits 共用的 root expectedVersion;只改 limits 也推進 |
status | VARCHAR(20) | 否 | ACTIVE | R;row lifecycle;不可逆 DELETED,無 physical delete |
creator | VARCHAR(50) | 是 | NULL | R;V2 建立 actor audit fact |
create_time | DATETIME(6) | 否 | 無 | R;BO 建立 UTC instant |
modifier | VARCHAR(50) | 是 | NULL | R;最近 mutation actor |
modify_time | DATETIME(6) | 否 | 無 | R;BO 初始化/mutation timestamp;不是 MySQL NOW default |
Query intent:UQ 支援 Supplier+code exact lookup;(supplier_id, currency_id, supply_gateway_status) 支援 owner/currency candidate,row status 是額外 live guard;Supplier page 若依 id 排序,以 id 作穩定 tie-breaker,physical index 是否加 status/id 由實際 predicate/EXPLAIN 收斂。不保存 current offset、Cardholder membership 或 Agent owner。
Agent–Gateway Allocation 與 Direction Config
Source:P,ADR-0105/0229/0230 與本文件 accepted reconciliation。下列均加 M base。未帶參數時省略 predicate,不使用 nullable OR;query index 指向真實 grain,沒有「每欄一個 index」要求。
| Table.column | SQL type | Nullable | DB default | Authority/語意 |
|---|
supply_gateway_agent_allocation.supply_gateway_id | BIGINT | 否 | 無 | Gateway reference;N:M allocation 端點 |
supply_gateway_agent_allocation.agent_id | BIGINT | 否 | 無 | Agent technical identity;不是 prefix 字串 |
supply_gateway_agent_allocation.external_channel_code | VARCHAR(50) | 否 | 無 | Agent scope 的 external adapter identifier;不是另一個 Gateway owner |
supply_gateway_agent_allocation.external_channel_name | VARCHAR(100) | 否 | 無 | allocation display current value,非 identity/history |
supply_gateway_agent_allocation.priority | INT | 否 | 1 | positive、可重複;disabled 保留值,missing mapping 顯示 1;本期 routing 忽略 |
supply_gateway_agent_allocation.allocation_status | VARCHAR(20) | 否 | 無 | BO 明確配置的 current enable state;不是 Gateway operational status |
supplier_direction_config.supplier_id | BIGINT | 否 | 無 | Supplier owner reference |
supplier_direction_config.currency_id | BIGINT | 否 | 無 | config money denomination;Supplier 可以有多 Currency configs |
supplier_direction_config.direction | VARCHAR(20) | 否 | 無 | Deposit/Withdrawal dimension,不能將兩方向相加判限 |
supplier_direction_config.enabled | BOOLEAN | 否 | 無 | Supplier 在該 Currency/direction 的 current intent |
supplier_direction_config.min_amount | DECIMAL(30,10) | 否 | 無 | 該 grain 的 current per-order lower bound;Supplier 數值 sentinel/validation 由 #146 凍結 |
supplier_direction_config.max_amount | DECIMAL(30,10) | 否 | 無 | 該 grain 的 current per-order upper bound;不得直接套 Gateway 的 0 sentinel |
supplier_direction_config.daily_amount_limit | DECIMAL(30,10) | 否 | 無 | 該 grain 的 current daily money limit;非 counter,不能重設 history |
supplier_direction_config.daily_count_limit | INT | 否 | 無 | 該 grain 的 current daily count limit;非 assignment counter |
supply_gateway_direction_config.supply_gateway_id | BIGINT | 否 | 無 | Gateway root reference;Currency 從 Gateway 解析 |
supply_gateway_direction_config.direction | VARCHAR(20) | 否 | 無 | Deposit/Withdrawal dimension |
supply_gateway_direction_config.min_amount | DECIMAL(30,10) | 否 | 0 | per-order lower bound;0 為不限制 |
supply_gateway_direction_config.max_amount | DECIMAL(30,10) | 否 | 0 | per-order upper bound;0 為不限制;兩 bound 皆正時 min ≤ max |
UQ:allocation (supply_gateway_id, agent_id) 與 (agent_id, external_channel_code);Supplier config (supplier_id, currency_id, direction);Gateway config (supply_gateway_id, direction)。Config UQ prefix 已支援 owner config lookup;Agent enabled allocations 可使用 (agent_id, allocation_status, supply_gateway_id) 的 query intent。Supplier+Currency/Gateway+direction 必須用同 transaction 的 authoritative references 解析/寫入。
Allocation 建立/啟用與 routing 必須驗證 Agent Currency=Gateway Currency、Agent offset=owner Supplier current offset;失配 fail closed。allocation 不保存 Supplier owner key,Supplier 僅經 Gateway owner chain 解析,因此同一 Agent 可有跨 Supplier binding。Agent-first read 僅列出 enabled、未刪除 allocation,並同步重驗 Agent/Supplier/Gateway row、Gateway operational status、Currency 與 offset equality;不得以 Supplier id 作隱性排除條件。不得從此字典新增 fallback/weight/priority 排序,remaining routing lifecycle 屬 #146。Gateway direction rows 不保存 enabled/daily limit/current offset;root status+兩方向是單一 version/transaction boundary,child version 不對 caller 開放。Supplier daily admission 的詳細 gate/counter lifecycle 屬 #146,不套 House quota 規則。
Cardholder/session/Payment Account:凍結的 owner interface
I/P:本表只凍結 #142 owner/identity 字段,明確不把後段 B3/B4 candidate list 宣稱為 principal/financial 完整字典。除明列 nullable 外均 required,無 DB default;PK/M base 是 root 字典接口,#143/144 若需要新增安全/財務特製欄位,必須在自身 contract 明列。
| Table.column | SQL type | Nullable | DB default | Authority/immutable 或 projection 語意 |
|---|
supply_cardholder.gateway_id | BIGINT | 否 | 無 | 唯一 immutable Gateway owner,建立後不得移轉 |
supply_cardholder.supplier_id | BIGINT | 否 | 無 | 由 owner Gateway.supplier_id 原子複製的 immutable scope redundancy |
supply_cardholder.currency_id | BIGINT | 否 | 無 | 由 owner Gateway.currency_id 原子複製;Cardholder 不選 Currency |
supply_cardholder_session.supply_cardholder_id | BIGINT | 否 | 無 | authenticated Cardholder reference;#143 定義 session security lifecycle |
supply_cardholder_session.gateway_id | BIGINT | 否 | 無 | 建立 session 時由 Cardholder owner chain 驗證並寫入 |
supply_cardholder_session.supplier_id | BIGINT | 否 | 無 | 同 transaction 的 immutable owner scope redundancy |
supply_cardholder_session.currency_id | BIGINT | 否 | 無 | 同 transaction 的 immutable money scope redundancy |
payment_account.supply_cardholder_id | BIGINT | 否 | 無 | 唯一 immutable Cardholder owner,與 House persistence 分離 |
payment_account.gateway_id | BIGINT | 否 | 無 | 同 transaction 從 Cardholder owner chain 驗證/寫入 |
payment_account.supplier_id | BIGINT | 否 | 無 | immutable owner redundancy,不能獨立改 owner |
payment_account.currency_id | BIGINT | 否 | 無 | immutable scope;必須等於 owner Gateway Currency,亦參與 global financial UQ |
payment_account.bank_id | BIGINT | 否 | 無 | financial Bank identity;Bank options/application validation 交 #144/148 |
payment_account.account_no | VARCHAR(100) | 否 | 無 | canonical normalized account identity 的候選 storage name;#144 凍結 normalizer/application/financial lifecycle,不從 House normalizer 推導 |
Payment Account global UQ grain 為 (currency_id, bank_id, normalized account number),目前 candidate 以 account_no承載 canonical value;不是 raw formatted account UQ。Cardholder/session 不保存 agent_id/prefix_id,不建立 Gateway N:M membership。Cardholder username/status/password/device/session deadline 等完整欄位由 #143;Payment Account balance/reserved amount/ledger/review/effective count 等由 #144。候選 query intent 保留 Cardholder (gateway_id, cardholder_status, id)、(supplier_id, cardholder_status, id) 及 Payment Account (gateway_id, account_status, priority, id),狀態/priority 語意須先由其 owner 凍結再實作;不以本 Issue 宣稱其 runtime 已完成。
House Card
P+M base 全部七欄。Source:ADR-0231~0301;這是獨立 Agent-owned aggregate,不存在 Supplier/Gateway/Cardholder ownership columns,也不持有 authoritative balance。
| Column | SQL type | Nullable | DB default | Authority/immutable 或 current 語意 |
|---|
agent_id | BIGINT | 否 | 無 | immutable owner;由 authenticated Agent 或 authorized System Admin target 解析 |
currency_id | BIGINT | 否 | 無 | owning Agent.currency_id 的 immutable query redundancy |
bank_id | BIGINT | 否 | 無 | immutable financial identity;Create 以 current Bank、Agent mapping、Currency 驗證 |
account_no | VARCHAR(100) | 否 | 無 | immutable canonical [A-Z0-9]{1,100};依 ADR-0233,保留 leading zeros |
display_name | VARCHAR(100) | 否 | 無 | mutable non-unique BO 營運名稱,非 financial identity |
account_name | VARCHAR(100) | 否 | 無 | mutable 銀行戶名;與 display_name 各自 Unicode control/edge-trim/length validation |
card_status | VARCHAR(20) | 否 | 無 | current typed HouseCardStatus ACTIVE/INACTIVE;Create 由 BO 明確寫 ACTIVE |
deposit_enabled | BOOLEAN | 否 | 無 | current Deposit new-admission intent;不取消 existing Attempt |
withdrawal_enabled | BOOLEAN | 否 | 無 | current Withdrawal new-admission intent;不能取代 external balance/atomic reservation |
daily_amount_limit | DECIMAL(30,10) | 是 | NULL | current House override,只計 Deposit;null 才逐欄 fallback;非 null 必須 positive/lossless |
daily_tx_limit | INT | 是 | NULL | current House override,Deposit+Withdrawal 共用 Tx;null 才逐欄 fallback;非 null positive/lossless |
active_identity_key | VARCHAR(255) GENERATED STORED | 是 | 無;由 expression 產生 | non-deleted 且 card_status=ACTIVE 才由 Agent/Currency/Bank/canonical account 組成 key,其他為 NULL;非 Java writable/API field |
UQ uk_house_card_active_identity(active_identity_key);expression 的 exact candidate 保留於後段 house_card inventory。ACTIVE-only uniqueness 不是 permanent financial UQ,不阻擋多筆相同 INACTIVE/DELETED rows。PK 支援 authorized detail/lock;new admission 以 (agent_id, currency_id, card_status, status, id) 為 query intent,再套 direction/Bank current guards,House 名稱模糊查詢不宣稱一般 B-Tree 加速。BO bankEligible/reason、effective limits/sources 是 current read projection,不持久化;quota 按 houseCardId+owning Agent local date,不是 master columns,也不是同帳戶共用計數。
Currency、Bank 與 Agent–Bank Reference
R+L base;source 為 v0.1.1、Bank 後續 ALTER 與 Currency/Bank entity。以下是保留的 reference/display contract;不自行發明 Currency code UQ 或把所有 Bank legacy routing setting 帶入 External Pool。
| Table.column | SQL type | Nullable | DB default | Authority/語意 |
|---|
currency.code | VARCHAR(50) | 否 | 無 | Currency master readable code;不是 Agent/Gateway relation identity |
currency.name | VARCHAR(50) | 否 | 無 | current display name,非 historical name snapshot |
currency.description | VARCHAR(255) | 否 | 無 | current reference 描述 |
currency.is_crypto | BOOLEAN | 否 | 無 | current Currency classification |
currency.decimal_number | INT | 是 | NULL | current numeric metadata;consumer 必須驗證,不以 NULL 當任意 scale default |
currency.rate | INT | 是 | NULL | 現有 reference metadata;不能當 SPAY4 commission rate authority |
bank.currency_id | BIGINT | 否 | 無 | Bank money context reference;House current eligibility 須與 stored House Currency 相同 |
bank.country | VARCHAR(100) | 是 | NULL | current country metadata,不能決定 business UTC offset |
bank.code | VARCHAR(50) | 否 | 無 | R UQ (code);canonical Bank exact lookup |
bank.name | VARCHAR(150) | 否 | 無 | current display name |
bank.locale_name | VARCHAR(150) | 否 | 無 | current localized display name |
bank.provider_bank_code | VARCHAR(50) | 是 | NULL | R/I;通用 provider mapping identifier;目前由 LINE adapter 使用,#148 凍結 external mapping,不取代 Bank.id |
bank.code_id | BIGINT | 否 | 0 | R/I;v0.1.45 App identifier,既有 UQ (code_id);#148 決定相容 mapping,非 Bank.id |
bank.keyword | VARCHAR(1024) | 否 | 空字串 | R/I;v0.1.45 search metadata,非 financial identity |
bank.decimal_fuzzing | BOOLEAN | 是 | NULL | R/I;v0.3.1 legacy amount policy,不能以 Java FALSE initializer 改寫 SQL default |
bank.amount_cooldown_time | INT | 否 | 0 | R/I;v0.3.1 legacy cooldown setting,target routing 使用由 #146/148 定案 |
agent_bank.agent_id | BIGINT | 否 | 無 | available-scope Agent reference,不是 Bank ownership |
agent_bank.bank_id | BIGINT | 否 | 無 | available-scope Bank reference,移除 mapping 不 cascade House state |
agent_bank PK (agent_id, bank_id) 本身保證 relation pair unique;無 id/status/version/audit。PK prefix 支援 Agent Bank list,若 Bank→Agents 反向管理 query 需要,另以 (bank_id, agent_id) 作實際 query intent;不新增 N:M owner 模型。Currency PK、Bank code UQ 與 Bank (currency_id, status, id) 支援 reference lookup;Country/name 不是 offset/financial authority。Bank 既有 code_id、keyword、decimal_fuzzing、amount_cooldown_time 屬 App/LINE/legacy routing interface,#144/146/148/151 決定 target mapping,不由 #142 默認 External Pool 沿用。House 必須保留 ADR-0234/0235 的 Bank guards;Cardholder 沒有固定 Agent,因此 App Bank options 不能偷用 session.agentId。
Global/Agent SystemConfig
R+L base;source 為 v0.1.1、SystemConfig、AgentSystemConfig 與 ADR-0302。UQ 是 T,不能宣稱 existing Optional DAO 證明 DB 已 enforce。
| Table.column | SQL type | Nullable | DB default | Authority/語意 |
|---|
system_config.key | VARCHAR(50) | 否 | 無 | canonical Global key authority;T UQ (key),全部 keys 適用 |
system_config.value | VARCHAR(225) | 是 | NULL | current setting string;是否有效由 consumer 判定,NULL 不等於 row absent |
agent_system_config.agent_id | BIGINT | 否 | 無 | Agent scope reference;不同 Agent 可有同名 key |
agent_system_config.system_config_id | BIGINT | 否 | 無 | referenced Global row identity;其 key 才是 authority |
agent_system_config.key | VARCHAR(50) | 否 | 無 | 從 referenced Global.key 原子寫入的受控 query redundancy;T UQ (agent_id, key) |
agent_system_config.value | VARCHAR(225) | 是 | NULL | current override string;row absent 正常,present-invalid 不 fallback |
兩個 UQ 不帶 status/version/current marker;Global 與 Agent 同名共存,Agent override 可完全不存在。Query 使用 UQ 的 exact key/Agent+key lookup;不額外為 value 建 index。BO writer 必須在同 transaction 解析 references 並驗證 key 一致,不能成功保存 mismatch;只做 precheck 不足以保證 concurrency。House reader 命中 Agent override 後仍不新增 Global lookup。Global seed spay.house.daily.amount.limit=500000、spay.house.daily.tx.limit=100000 是 row seed,非 value column default;不預建 Agent rows。Writer permission、error mapping、rename/delete/cascade 仍未由本 Issue 新增政策。
冗餘與 Transaction Authority 矩陣
| Stored/derived data | 唯一 source 與類型 | BO atomic responsibility | Query/history boundary |
|---|
| Cardholder/session/Payment Account Supplier、Gateway、Currency | immutable owner chain redundancy | 建立時解析同一 chain、驗證 references,與 root/session 一起 commit;不接受 client 提供可信 scope | 依 Supplier/Gateway scope 查詢;沒有 agent_id,不查 membership |
| House currency_id | immutable Agent.currency_id redundancy | Create 與 Agent/Bank scope validation 同 transaction;不可獨立 update | Agent+Currency candidate defense-in-depth;不改歷史 owner |
| Agent config key | referenced Global.key 的受控 current redundancy | reference/key mismatch 不得 commit;UQ race 與 transaction rollback 皆須驗收 | House required resolution chain 不增加 global read |
| Order current assignment pointer+current owner fields | current_assignment_attempt_id 是 authority,current_* 是 projection | assignment/合法 reDispatch/termination 在同 BO transaction 更新 pointer 與互斥 projection | list/scope 用 projection;#145 決定 state mapping,不覆寫 Attempt snapshots |
| Assignment Attempt owner/Agent/Currency/business date | 已驗證 Order 與 assignment chain 的 immutable facts | Agent 必須等於 immutable Order Agent;assignment 原子保存 snapshots、House/External Pool 互斥 reference | history 不依 current master 重算;deadline 只由 Attempt 持有 |
| Order offset snapshot | 首次成功 routing 的 immutable offset | 首次建立時保存;retry/recovery/reDispatch 不改寫 | #145 對齊 Attempt 與 terminal facts,不採 current Supplier offset 重算 |
| reservation/counter/ledger scope | 其 authority reference 加所需 immutable scope/history dimensions | #144/145 的 transaction 同時驗證 references 與寫 facts/projections;balance/reserved 是 ledger-backed projection | 不無版本複製 mutable balance、status 或 current offset;exact financial schema 由 owner 定案 |
| House effective limit/source/Bank eligibility | current master/config resolution 的 read projection | command 以 post-command desired state 解析 required config,失敗整體 rollback | 不保存 derived columns,不把 BO response 當 routing authority |
任何 authority 缺失或 scope mismatch 都不能產生成功的 BO write;duplicate/orphan/mismatch 的 legacy inventory 交 #150,處置/cutover/rollback 交 #151,Operator SQL 交 #152。此矩陣不增加 DB FK、reader side effect、background repair 或新產品政策。
#142 House 與 Config 驗收矩陣
下表是 contract inspection/未來 runtime acceptance,不是已執行的 HTTP、DB 或 concurrency tests。每個案例依已接受 ADR 定義 expected result;不得從 quota 結果反推 #145 尚未決定的 settlement 權限、證據或 state mapping。ADR-0242 已被 ADR-0243 取代;舊七欄 Create/18-field VO 分別由 ADR-0268/0266/0267 擴充。
| Case | Governing ADR | 必須保留的可驗證結果 | AC |
|---|
| H01 owner/currency | 0231、0232、0239 | House 只屬 Agent;Currency 由 Agent 原子複製;owner/Currency/Bank/canonical account 不可 update;與 Payment Account 獨立 persistence | 1、3、4 |
| H02 canonical financial identity | 0233、0244、0247 | normalizer 保留 leading zeros、移除允許 formatting 並 uppercase;Create/UQ/LINE exact 共用;authorized BO 回完整值,log/error 不帶值 | 4 |
| H03 Bank current guards | 0234、0235 | Bank missing→deleted→inactive→mapping missing→currency mismatch 固定 precedence;不 cascade House status、不改既有 Attempt;Bank 恢復時資格可自動恢復 | 3、4、7 |
| H04 display/data descriptions | 0236、0237、0251、0256 | displayName 與 accountName 各自 required、control-first、Unicode edge trim、1..100 code points、non-unique;不當 financial identity;base/data/enum fields 都有語意說明 | 1、4 |
| H05 flags/typed status | 0238、0240 | Deposit/Withdrawal intent 無 default;ACTIVE/INACTIVE 只影響新 admission,不取消、終止、release 或阻斷 existing LINE/settlement | 4 |
| H06 soft delete | 0241 | INACTIVE+無 current assignment/unsettled Attempt/active或review-held obligation/open recovery,scope lock+version+blocker重驗+soft delete/audit 原子;terminal history 保留 | 4 |
| H07 delete/recreate/UQ | 0243(取代0242)、0244 | DELETED 不 restore;Create 取得新 id,不繼承舊 mutable values;多筆 INACTIVE/DELETED 可共存,最多一筆 ACTIVE | 4 |
| H08 activate race | 0245 | 只改 authorized target;另一 ACTIVE 或 concurrent UQ collision→HTTP409/40007 且全 rollback,不回 existing House id、不自動停用別列 | 4 |
| H09 status/version/no-op | 0246、0248 | PUT /houseCard/{houseCardId}/status 只接受 typed cardStatus+expectedVersion;先比 version;stale same-state→409/40051,current same-state→200 full VO、zero mutation/audit | 4 |
| H10 Create owner/allow-list | 0249、0250、0268 | POST /houseCard 九欄 flat request;Prefix 的 agentId 只可 omitted/null,System Admin required positive;不得送 currencyId、cardStatus、expectedVersion 或 derived fields | 4 |
| H11 response envelope | 0252、0266、0267 | commit 成功才回 HTTP200、code2000、message空字串、24-field HouseCardVo;不回201/Location;VO nullability 依後段 exact field contract | 4 |
| H12 transport/semantic layers | 0253~0257、0269、0270 | strict transport/unknown/duplicate rejection 先於九欄 validation;transport data=["request","invalid request"];semantic data=[field,reason];HTTP400/40005,無 target lookup/write/audit | 4 |
| H13 Update precedence | 0265、0271、0272 | full replacement 七欄;完整 request validation 先於 scoped target lookup,再 version,再 domain guards;omitted limit 不當 unchanged;failure 無 mutation/audit | 4 |
| H14 independent overrides | 0258~0261、0266、0267 | Amount 只算 Deposit,Tx 兩向共用;House Amount=800、Tx=null、Agent Tx=12→effective 800/HOUSE 與12/AGENT;另一維度獨立,不 pair fallback | 4 |
| H15 absence vs invalid | 0259、0260、0262、0273 | House non-null 不讀 unused defaults;null 才 Agent→Global;Agent row absent 可 fallback,present null/blank/invalid 絕不 fallback;任何 required dimension 失敗整份 response/command fail closed | 4 |
| H16 numeric/seed | 0263、0264、0270、0271、0283 | positive lossless DECIMAL(30,10)/INT;string config strip+BigDecimal grammar,Tx 100.0/1e2可解析100;不改原字串;Global500000/100000、不預建Agent rows;JSON number/string contract 不混用 | 4 |
| H17 configuration error | 0274~0276 | HTTP500、40053、required data=[],不可 null/object/omitted;Frontend 依 code i18n,message 只供診斷、不凍結 wording | 4 |
| H18 diagnostics | 0277~0282 | secret-safe log+metric,無 persistent alert/outbox/audit;config scope+key 定位,Agent identity只進log;有限 metric labels;exact reasons MISSING/BLANK/INVALID_NUMERIC,剩餘 telemetry 細節交 implementation slice | 4 |
| H19 assignment/date | 0284~0287 | owning Agent local midnight,Amount/Tx 使用同一次成功 assignment event time;requested1000 原子占Amount1000+Tx1,Withdrawal只占Tx1;23:59建單、次日00:01指派歸次日 | 5 |
| H20 cross-day admission | 0288 | limit1000、前日600 in-flight、新日無占用→新日可再1000;午夜不釋放、不搬移舊quota,跨日可1600 | 5 |
| H21 no-funds termination | 0289、0299 | 僅 expiry/無通知不足以 release;確認無資金+正式終止兩者俱備的轉換與原日完整釋放原子生效,不等待背景job;Deposit釋Amount+Tx,Withdrawal釋Tx | 5 |
| H22 confirmed actual/original date | 0290、0291 | 已依#145合法成功且actual900:原reserved1000最終變900;跨日成功仍歸initial quota date,不增加完成日quota | 5 |
| H23 overage/atomic success | 0292、0293 | 原limit1000、reserved600+其他300、合法actual800→原日1100仍成功,不裁actual;success與quota調整同一原子結果,不先釋放差額 | 5 |
| H24 successful Tx | 0294 | 同次assignment成功後Tx仍1;總數9仍9,不變8/10,也不中途0/2 | 5 |
| H25 current limit change | 0295、0298 | 占用800,上限1000合法降600可存、占用仍800無新capacity;升1200才有400;explicit-null繼承600同理,無reset/cancel | 5 |
| H26 new assignment | 0296 | 已合法reDispatch的新assignment依新目標current effective limit與新事件local date重新占 requested Amount+Tx或Tx;不沿用舊資格,不推定舊已release | 5 |
| H27 failed replacement | 0297 | 舊A尚存600+Tx1、改派B失敗→A關係/quota/deadline保留,B無新占用;不阻止獨立expiry/settlement、不復活已終止A | 5 |
| H28 owner of undecided behavior | 0300 | #145凍結actual確認/有效性/fee、no-funds權限/證據/state、合法重派/成功替換;#144/#146協作;#142不從例子推partial成功或新權限 | 5、7 |
| H29 separate House identities | 0301 | A成功800後合法停用、同帳戶新B id無占用、上限1000→B可1000,跨id合計1800;same-id停啟不reset;歷史不搬移 | 5 |
| C01 concurrent uniqueness | 0302 | 同Global key/同Agent+key的兩個並行insert最多一筆commit;不同Agents與Global/Agent同名共存;不靠Optional/precheck,不以status/version迴避UQ | 6 |
| C02 reference consistency | 0229、0302及SystemController source | BO write送入另一Global reference+不相同key不得成功;正確key從referenced row取得,值更新不改identity;House Agent-hit不新增Global query | 3、6 |
House exact Create allow-list(依此順序 validation):agentId、displayName、bankId、accountNumber、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit。General Update allow-list:displayName、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit、expectedVersion。兩限額 property 均 required-present、nullable;present null 是 fallback,非 omitted/unchanged。非 number token 為 transport error;number zero/negative 為 must be positive,positive 不可無損表示為 invalid format,omitted 為 required。
#142 Coverage 與下游 Handoff
| AC | 文件/案例 evidence | Implementation Plan | Published Verification |
|---|
| AC-1 | 本節逐欄字典、base expansion、R/T/P/I來源、各表PK/UQ/query intent | Step 1、4 | table/column completeness、base/default inspection |
| AC-2 | Agent/Supplier/Gateway/Allocation字典、time authority、immutable owner interface、supersession | Step 1、3 | owner/Currency/time source 對照 notes47089/47090/47096 |
| AC-3 | transaction authority矩陣、H01/H03、C02、Order pointer/projection接口 | Step 1、4 | 每個redundancy的source/atomic write、config mismatch、no FK |
| AC-4 | House字典+後段exact contract、H01~H18 | Step 2、4 | ADR0231~0283、mixed/missing/invalid/error case inspection |
| AC-5 | H19~H29、schema-orders responsibility handoff | Step 2、4 | ADR0284~0301、跨日/overage/release/replacement/1800案例 |
| AC-6 | Config字典、C01/C02;Agent absence與cross-scope共存 | Step 2、4 | UQ grain與未來concurrent/mismatch proof requirement |
| AC-7 | 下表逐Issue接口、I字段、no SQL prerequisite | Step 3、4 | #143~152與全部 exclusions read-back |
| AC-8 | canonical reconciliation、knowledge log、generated reader/index/diagrams與completion evidence | Step 3、4 | 三generators兩次hash一致、validate-docs、strict UTF-8、diff、雙軸review |
| Owning Issue | #142 提供的已凍結接口 | 留給 owner 的結果;不得宣稱本次完成 |
|---|
| #143 Principal/Credential | 單一owner type/scope、Cardholder immutable Gateway與無固定Agent | Cardholder status/credential/session的完整欄位與revocation/auth契約 |
| #144 Payment Account/Ledger | Cardholder→Payment Account、global normalized financial identity、query redundancy、House獨立persistence | application/approval/effective-count、Bank options、ledger/reservation/rebuild/reconciliation、normalizer與financial lifecycle |
| #145 Order/Attempt | immutable Agent、current pointer/projection、immutable attempt facts、H19~H29 quota約束 | ADR0300金融決策、state mapping、成功替換舊新銜接與exact settlement schema |
| #146 Allocation/Routing | Agent N:M Gateway、Currency/offset equality、priority既定語意、direction config grain | selection/fallback/weight/quota與Supplier offset mutation遇existing ACTIVE mismatch處置;不可把House Bank guards忽略或默認External Pool沿用 |
| #147 Permission Matrix | Supplier管理旗下Gateway、System Admin-only allocation mutation、House Agent owner | 跨Principal各command完整matrix;沿既有#143/#146 dependencies |
| #148 External Compatibility | Cardholder/session無固定Agent、Gateway Currency、LINE持久化assignment boundary | OGP/LINE/App mapping、App bank options/business context;不能以session.agentId補回舊模型 |
| #149 BO API | readable master displays、已接受House exact request/response/error、source authority | 其餘Master CRUD/offset mutation/menu與API surface,不重定義本次House契約 |
| #150 Source Inventory | 表/column/UQ/scope與reference invariants | duplicate/orphan/mismatch、金融parity等read-only DB inventory;#142不連DB |
| #151 Migration/Cutover | target/source差異與待處置I欄位、永久/ACTIVE-onlyUQ差異 | collision、backfill、retain/retire/archive、cutover/rollback;不靜默修正legacy rows |
| #152 Operator SQL | 經下游凍結且inventory/strategy就緒的schema契約 | 人工SQL package、same-session/preflight/post-verify/abort;本Issue sql: [],無Manual SQL prerequisite |
所有 runtime concurrency/transaction cases 都是後續實作必須證明的義務;本次的完成證據只涵蓋文件契約與其可重現檢查。#138 已完成的 source inventory 不是 target DB validity 或 SQL execution authority。
| Object | Change type | Purpose | Explicit boundary |
|---|
superseded Supply actor relation | CREATE TABLE | Supplier/Supply Gateway後台principal、password與owner scope | 不放role_id;不保存menu或session rows |
authenticator_credential | CREATE TABLE | BO User/Supply Actor共用TOTP、lock、OTP replay、pending password-change token與formal-session epoch | 不繼承BaseEntity;不保存raw secret/OTP/JWT |
system_group | reuse only | BO_USER realm沿用既有Payment Management group | 不新增row;不供SUPPLY_ACTOR menu查詢 |
system_method | seed data only | 新增SUPPLY_GATEWAY_ALLOCATION與SUPPLY_HOUSE_CARD | code/name/sort已由ADR-0084凍結 |
role_method | seed/admin configuration | 將Supply actions授予特定Prefix BO role | System Admin不需要grant row;is_admin=true直接看active methods |
user、role、agent | no DDL | Prefix/System沿用既有principal、role與Prefix scope | 不加入auth_realm或Supply owner columns |
supplier、supply_gateway | owner dependency | superseded Supply actor relation.owner_id的logical owner | Supplier actor可於B1建立;Gateway actor須等B2建立Gateway後才能provision |
RoleSet.java | code only | 新增allocation VIEW與House CRUD的ROLE_{code}_{action}常數 | 不是table;不得供Supply actor fixed profile使用 |
SupplyActorPolicy | code only | Supplier/Gateway fixed action與menu projection | 不建立supply_actor_role/supply_actor_role_method |
B1 Data Dictionary — superseded Supply actor relation
| Column | SQL type/nullability | Meaning | Constraint/write rule |
|---|
id | BIGINT NOT NULL AUTO_INCREMENT | Supply actor account surrogate ID;JWT與audit使用此principal identity | PK;不可由request指定 |
owner_type | VARCHAR(20) NOT NULL | owner discriminator,只允許SUPPLIER/SUPPLY_GATEWAY | 與owner_id一起由backend解析scope;不可從login dropdown信任 |
owner_id | BIGINT NOT NULL | 依owner_type指向supplier.id或supply_gateway.id的logical ID | service驗證owner存在且未退場;IDX (owner_type, owner_id, account_status) |
username | VARCHAR(100) NOT NULL | Supply actor登入帳號 | Supply realm全域unique;建立後不可修改;password policy以此判斷帳號包含 |
password_hash | VARCHAR(255) NOT NULL | 經Spring PasswordEncoder處理的credential hash | 永不回傳/記錄raw password;reset只覆寫hash |
account_status | VARCHAR(20) NOT NULL | 登入principal是否可用;V1至少ACTIVE/INACTIVE | 非ACTIVE不得取得或使用formal session;停用時revoke-all |
must_change_password | BOOLEAN NOT NULL DEFAULT TRUE | 是否只能完成temporary-password self-change flow | true時帳號、密碼與GA code皆成功後仍不得簽發formal session |
version | BIGINT NOT NULL DEFAULT 0 | account lifecycle optimistic lock | 只用於account update;不得當成JWT session version |
status | VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' | row lifecycle status | V1無delete API;保留一致的business row lifecycle語意 |
creator | VARCHAR(100) NOT NULL | 建立此account的BO audit identity | 不保存temporary password |
create_time | DATETIME(6) NOT NULL | account建立UTC timestamp | backend產生 |
modifier | VARCHAR(100) NULL | 最近一次account lifecycle異動者 | login failure/session issue不更新此欄位 |
modify_time | DATETIME(6) NOT NULL | 最近一次account lifecycle異動UTC timestamp | password/status mutation更新 |
Indexes/constraints:
- PK
id。 - UQ
username。 - IDX
(owner_type, owner_id, account_status),支援owner account page、Gateway retirement account blocker與scope verification。 - 不宣告polymorphic DB foreign key;owner relation由service transaction驗證。
B1 Data Dictionary — authenticator_credential
此表是security state,不使用status/creator/modifier,也不繼承BaseEntity。Raw TOTP secret、OTP與raw token都不得進入此表、log、ChangeLog或audit payload;只保存encrypted secret與token hash。
ADR-0307 Q22 固定所有新 BO User(Prefix/Superadmin)與 credential 共同建立,初始 session epoch 沿用 0;任一建立失敗整筆回滾,不留帳號或孤立 credential,不得因 MFA 設定而省略或延後補建。依 Q23,建立時產生並加密保存 TOTP secret,credential 初始為可供驗證的 ACTIVE,Create 成功時一次性交付 Base32 設定密鑰文字,Frontend/Backend 均不提供 QR code。使用者自行配置 GA,直接用一般帳密+GA 登入,不另經 PENDING_VERIFICATION/首次 OTP 啟用;ACTIVE 不代表已成功驗證 GA,六位登入驗證碼由 GA 產生。Secret 不得再次取得,遺失沿用 reset rotation;BO 不新增 mustChangePassword。Q24 固定新 BO 密碼由管理者輸入,Backend 依既有 LoginPasswordPolicy 驗證後雜湊保存;不合政策時拒絕建立且不留下 User/credential,GA secret 仍由 Backend 產生。Q25 固定管理者重設 BO 密碼也由管理者提供新密碼,Backend 依既有政策驗證並雜湊保存;成功重設與 epoch 遞增、撤銷時間及 pending challenge 清除共同提交,全部舊 token 永久失效,沿用 ADR-0094/0095;不新增 BO mustChangePassword。Q26 依使用者修正,管理者重設 BO 密碼時保留 GA secret 與 last_accepted_time_step,將 failed_attempts 歸零並將 locked_until 設為 null,解除驗證鎖定後重新累計;與密碼更新及撤銷共同提交/回滾,失敗不得單獨解鎖。這不會啟用已停用的 User/Role/credential。Q27 將相同規則套用 Supplier/Gateway password reset,取代 ADR-0104 原保留 GA failure/lock 的部分;清零解鎖與其 password/account version/session epoch/pending challenge/audit 同交易完成,失敗不得部分解鎖,temporary password 與 mustChangePassword 流程保留。四類 Backoffice 重設成功後均可輸入新密碼並重新嘗試登入,Frontend/Backend 不得再沿用重設前的驗證鎖定阻擋;之後新失敗仍依既有政策累計,適用的 account/owner/Role/credential guards 仍須通過。依 ADR-0104,V2 強制 GA、沒有 MFA OFF bypass;舊 ADR-0101 的無 row/epoch 0 例外只屬仍保留的既有 legacy/bootstrap 相容,不能套用於 V2 或本決策後新建帳號。
| Column | SQL type/nullability | Meaning | Constraint/write rule |
|---|
id | BIGINT NOT NULL AUTO_INCREMENT | authenticator credential surrogate ID | PK |
principal_type | VARCHAR(30) NOT NULL | PREFIX_BO_USER/SYSTEM_BO_USER/SUPPLY_ACTOR | 與principal_id形成polymorphic logical identity |
principal_id | BIGINT NOT NULL | BO user.id或superseded Supply actor relation.id | UQ (principal_type, principal_id);service驗證principal存在 |
credential_status | VARCHAR(30) NOT NULL | PENDING_VERIFICATION/ACTIVE/RESET_REQUIRED | V2 login要求ACTIVE;新BO依ADR-0307 Q23初始ACTIVE,不經PENDING_VERIFICATION/獨立OTP啟用 |
encrypted_secret | VARBINARY(512) NOT NULL | 由external key加密的TOTP secret ciphertext | 不保存raw secret;reset時rotate |
secret_key_version | VARCHAR(32) NOT NULL | 解密encrypted_secret使用的key版本 | 支援key rotation;不可作session version |
last_accepted_time_step | BIGINT NULL | 最近成功接受的TOTP time step | row lock/atomic CAS,拒絕同time-step replay |
failed_attempts | INT NOT NULL DEFAULT 0 | 連續OTP驗證失敗次數 | 成功後清零;達threshold設定locked_until |
locked_until | DATETIME(6) NULL | credential暫時鎖定截止UTC時間 | 未到期前拒絕OTP verify |
enrolled_at | DATETIME(6) NULL | 目前TOTP credential完成建立/重設而可用的UTC時間,不代表使用者完成GA驗證 | 新BO依ADR-0307建立可用credential時設定;reset依ADR-0104更新,不是終身首次OTP成功時間 |
secret_disclosed_at | DATETIME(6) NULL | one-time Base32 totpSecret成功回傳的UTC時間 | 已有值時不得再次GET;response遺失只能reset rotation |
pending_challenge_type | VARCHAR(30) NULL | 目前pending token種類;V1只允許PASSWORD_CHANGE | 與其他三個pending欄位all-null/all-non-null;後發password-change token覆蓋前一筆 |
pending_challenge_token_hash | CHAR(64) NULL | opaque/signed password-change token的SHA-256或HMAC-SHA256 digest | 不保存raw token;驗證時constant-time compare |
pending_challenge_issued_at | DATETIME(6) NULL | password-change token簽發UTC時間 | 只供temporary-password lifecycle/audit correlation,不作formal session issue time |
pending_challenge_expires_at | DATETIME(6) NULL | password-change token到期UTC時間 | 最長5分鐘;到期不得consume |
session_epoch | BIGINT NOT NULL DEFAULT 0 | 目前credential-backed principal可接受的formal-session generation | 正式JWT保存同值;revoke-all以atomic increment更新;既有legacy/bootstrap無credential例外的claim值為0,不是V2 bypass |
last_session_issued_at | DATETIME(6) NULL | 最近一次簽發正式JWT的UTC時間 | 不記password-change token |
sessions_revoked_at | DATETIME(6) NULL | 最近一次principal-level revoke-all UTC時間 | password/MFA reset、account deactivate或admin revoke更新 |
create_time | DATETIME(6) NOT NULL | credential row建立UTC時間 | backend產生 |
modify_time | DATETIME(6) NOT NULL | 任一credential/lock/epoch state最後異動時間 | backend產生;不代表business entity modifier |
Indexes/constraints:
- PK
id。 - UQ
(principal_type, principal_id)。 - IDX
(credential_status, locked_until),支援security administration與lock掃描。 - V2 session 依 ADR-0104 逐 request 驗證 credential/epoch 與其他 guards;V2 login 強制 GA。ADR-0101 的 MFA OFF 無 row bootstrap claim 0 及 OFF→ON gate 僅屬仍保留的既有 legacy/bootstrap 相容,不得解讀成 V2 bypass。新 BO User 依 ADR-0307 共同建立 credential;
superseded Supply actor relation.version不得替代 session epoch。 - 不新增
supply_actor_session;V1不提供per-session list/logout/refresh rotation。
Accepted Single Password-Change Token Invariant
- 一般登入在同一request驗證帳號、密碼與GA code,不建立
MFA_LOGIN challenge。 - 每個
authenticator_credential同時只有一組pending PASSWORD_CHANGE token;後發token在row lock內覆蓋前一組。 - 四個
pending_challenge_*欄位必須all-null或all-non-null;成功change、password/MFA reset、account deactivate或session revoke-all時清空。 - Signed token payload保存backend已驗證的principal identity;Backend仍鎖定credential row並比對stored token hash,確保single-use。
- Supply actor三項登入資料驗證成功但
must_change_password=true時,只建立password-change token,不建立formal session。 - 不新增
authentication_challenge table;不同principal仍可各自並行登入或改密碼。
system_group — data-only adjustment
| Column | Current SQL type | Meaning/Supply change |
|---|
id | BIGINT NOT NULL AUTO_INCREMENT | menu group PK;seed以name preflight後取得,不硬編環境ID |
name | VARCHAR(50) NULL | 沿用唯一active Payment Management group;不新增Supply專用group |
sort | INT NOT NULL DEFAULT 0 | group顯示順序 |
status | VARCHAR(45) NOT NULL | 只有ACTIVE group會由/system/menu回傳 |
creator | VARCHAR(50) NOT NULL | seed建立者 |
create_time | DATETIME NOT NULL | seed建立時間 |
modifier | VARCHAR(50) NULL | 最近seed/admin異動者 |
modify_time | DATETIME NULL | 最近seed/admin異動時間 |
system_method — data-only adjustment
| Column | Current SQL type | Meaning/Supply change |
|---|
id | BIGINT NOT NULL AUTO_INCREMENT | menu resource PK;role_method.system_method_id使用此ID |
system_group_id | BIGINT NOT NULL | logical relation至system_group.id;不宣告DB FK |
code | VARCHAR(50) NOT NULL | authority resource code;新增SUPPLY_GATEWAY_ALLOCATION與SUPPLY_HOUSE_CARD |
name | VARCHAR(100) NOT NULL | 分別為Supply Gateway Allocation與House Card;不可作permission identity |
sort | INT NOT NULL DEFAULT 0 | Payment Management內分別固定為6與7 |
status | VARCHAR(45) NOT NULL | 只有ACTIVE method會顯示;停用不等於刪除authority history |
creator | VARCHAR(50) NOT NULL | seed建立者 |
create_time | DATETIME NOT NULL | seed建立時間 |
modifier | VARCHAR(50) NULL | 最近seed/admin異動者 |
modify_time | DATETIME NULL | 最近seed/admin異動時間 |
目前SystemGroupServiceImpl.getMenu以method name建立visible集合,而Spring authority使用method code。Supply實作必須改用system_method.id或code對齊role_method,不得讓duplicate display name造成menu/backend authority漂移。
role_method — permission row adjustment
| Column | Current SQL type | Meaning/Supply change |
|---|
id | BIGINT NOT NULL AUTO_INCREMENT | role-resource grant PK |
role_id | BIGINT NOT NULL | logical relation至Prefix BO role.id |
system_method_id | BIGINT NOT NULL | logical relation至Supply system_method.id |
action | JSON NOT NULL | allocation固定["VIEW"];House固定["VIEW","INSERT","UPDATE","DELETE"] |
status | VARCHAR(45) NOT NULL | grant lifecycle;authorization與menu query都必須只採ACTIVE grant |
creator | VARCHAR(50) NOT NULL | grant建立者 |
create_time | DATETIME NOT NULL | grant建立時間 |
modifier | VARCHAR(50) NULL | 最近grant異動者 |
modify_time | DATETIME NULL | 最近grant異動時間 |
UQ (role_id, system_method_id);同一role/resource只保存一筆action集合。Agentless System Admin以ROLE_ADMIN與role.is_admin=true處理,不需要為每個Supply method建立role_method row;agent-bound Prefix Admin仍需下列default grants。現有authority conversion未明確過濾role_method.status,Supply實作必須補focused regression,避免inactive grant仍產生authority。
| Existing group | Method code | Method name | Sort | Prefix role_method.action | System Admin grant |
|---|
Payment Management | SUPPLY_GATEWAY_ALLOCATION | Supply Gateway Allocation | 6 | ["VIEW"] | 無;沿用ADMIN |
Payment Management | SUPPLY_HOUSE_CARD | House Card | 7 | ["VIEW","INSERT","UPDATE","DELETE"] | 無;沿用ADMIN |
Migration必須先驗證Payment Management恰有一筆,依method code執行idempotent upsert,再對全部active、agent-bound、is_admin=true Prefix roles依(role_id, system_method_id) upsert上述固定actions。未來建立同類role時在provisioning transaction套用相同default;非admin role預設不grant但可明確配置。不得硬編role ID、按display name產生authority或把上述rows授予SUPPLY_ACTOR。
Menu與authority parity也屬本次必要修正:只有agentless System Admin可因ADMIN看全部active methods;agent-bound admin仍必須有active VIEW grant。SystemGroupServiceImpl.getMenu與UserServiceImpl.getAuthorities都必須排除inactive role_method並以method ID/code對齊,migration或role mutation後清除menu/user caches。
Explicit No-change Tables
| Table/object | Fields used | Why no change |
|---|
user | id、agent_id、account、password、role_id、status | Prefix/System繼續BO_USER realm;依ADR-0303由immutable agent_id推導合法BO User的SUPERADMIN/PREFIX type,Role不參與type判定,不新增重複type欄位;依ADR-0305沿用單一role_id必填,Superadmin也無例外;authRealm與sessionEpoch放signed token,不ALTER user |
role | id、agent_id、is_admin、status | 沿用Prefix configurable role;依ADR-0305固定owner與is_admin建立後immutable,尚無User引用亦相同;不建立Supply actor role |
agent | id、prefix | 提供Prefix scope;不保存Supply owner type |
supplier | id、status | Supplier actor owner存在性檢查;auth slice本身不ALTER,B2另新增Supplier canonical Business UTC Offset;Currency由Gateway持有 |
supply_gateway | id、supplier_id、lifecycle status | Gateway actor owner存在性檢查;由B2建立,不因auth增加欄位 |
明確禁止新增:supply_actor_session、supply_actor_role、supply_actor_role_method、將Supply fixed menu seed進BO role_method、或在user混入Supplier/Gateway owner columns。
ADR-0303 的 Q11 固定 BO User principal type 由 immutable Agent owner 推導:合法 agentless 帳號為 SUPERADMIN,合法 Agent-bound 帳號為 PREFIX;role.isAdmin 不決定 type。此推導不取代 account/credential/Role 有效性及 permission/principal/owner scope gates。既有不符合合法身分條件的 agentless 資料須由 #150/#151 盤點與明確處置,不能僅因 agent_id=NULL 自動升權;exact validation guard 仍待相應 contract 收斂。
ADR-0305 固定每個 BO User 必須綁定一個存在的 Role,user.role_id 不可空且關係須維持相同 owner:Prefix User 只能指派自己 Agent 的 Role,Superadmin 也必須指派一個無 Agent 且 isAdmin=true 的系統 Role;Superadmin 不符合此條件時拒絕指派及登入/受保護存取,Superadmin writer 不得豁免。先由 immutable owner 判定 type,再驗證 Role 合法性,不把不合法帳號改判 Prefix,也不自動修補 flag;Q20 不禁止未被引用的非 admin 系統 Role 存在。Owner 比對使用目標 User/Role 的實際歸屬,由所有寫入路徑維持,不以 picker 篩選取代。Role owner 建立後 immutable,即使尚無 User 引用也不得跨 Agent 或在系統/Agent owner 間轉換;需要其他 owner 的 Role 時另建。Role 的 isAdmin 建立後同樣 immutable,系統/Agent-owned、已有/尚無 User 引用的 Role 均不得翻轉 true/false,Superadmin 也不豁免;需要不同管理員屬性時另建同 owner Role 再改綁 User,依 ADR-0306 Q17 撤銷該帳號既有 BO token。指派權限由 #147、API/error 由 #149、既有不符資料由 #150/#151 承接。
ADR-0306 固定 Role ACTIVE 是所有 BO User 登入及每次受保護請求的必要條件,包含 Superadmin 與受保護的 menu/bootstrap read。Role 非 ACTIVE 一律拒絕,檢查先於 ROLE_ADMIN permission bypass;Role ACTIVE 仍須通過其他 account、Agent、credential、session epoch、permission、principal 與 owner scope guards。Role 停用會永久撤銷所有引用帳號的既有 BO token,重新啟用也須重新登入,不得只清 cache 或暫時阻擋請求。User 成功改綁另一個同 owner Role 時,亦永久撤銷該帳號全部既有 BO token,要求重新登入;不影響共用新舊 Role 的其他帳號,改回原 Role 也不能恢復舊 token。相同 Role 重送或改綁失敗不觸發本項撤銷。Role resources/actions 實際增減並成功保存時,亦永久撤銷所有引用帳號的全部既有 BO token,要求重新登入;恢復原配置不能恢復舊 token,只改名稱、相同配置重送或儲存失敗不觸發本項撤銷。Menu/authority cache invalidation 仍須維持。依 Q21,Role 停用、User 改綁與 grants 增減均須與全部必要 token 撤銷共同提交;任一撤銷失敗則整筆回滾,不留下本次狀態/關係/權限變更或部分撤銷,不能先回成功再背景補完。回滾不復活先前已撤銷 token,也不撤回其他獨立安全事件的撤銷。此條件不套用到 SupplyActor/Supply Cardholder,也不因此授權停用 admin Role;isAdmin 不可變依 ADR-0305 Q19,其餘 lifecycle 與 exact guard 仍待 #143 接續收斂。
Field Description Contract
- 所有SPAY4新建或修改的migration columns都必須宣告具體SQL
COMMENT,包含每張新table的id、status、creator/modifier與timestamps等base columns,以及shared ALTER columns。 - 所有hand-written Java data fields與record components都必須有說明:API request/response使用
@Schema(description = "...");entity、DTO、criteria、command、projection、configuration與event/message payload使用JavaDoc;每個enum code也必須說明業務語意。 - 說明依欄位性質交代source/authority、用途、格式、單位、nullable、immutable、snapshot/derived、masking或secret handling,不接受只重述欄位名稱。
- Generated source、local variables、dependency-injection collaborators及未觸及Legacy field不在本次補寫範圍;inherited Java fields不在subclass重複說明,但每張實際DDL table仍須為其base columns提供
COMMENT。 - B0 schema/OpenAPI review及各slice completion均以description completeness為blocking gate。
Configuration Reference Identity — ADR-0302
SPAY4設定表採每個scope+key最多一筆,由DB unique constraint保證;這是target contract,不能從current DAO的Optional回傳推定live DB已具備約束。
| Table | Required identity fields | Target DB constraint | Cardinality與authority |
|---|
system_config | key | UQ (key) | 每個Global key最多一筆;是否必須存在依該key的consumer contract |
agent_system_config | agent_id、key | UQ (agent_id, key) | 每個Agent+key最多一筆;不同Agents可各有同名key;沒有override row仍合法 |
UQ不含status/version/current marker;不建立同key多版本current-row選擇模型,也不只靠service先查後寫。Global與Agent同名key可共存;House原有fallback、initial Global seeds及不預建Agent rows維持。全部設定keys適用,不限House keys。
Config reference authority沿用現有BO建立路徑與ADR-0229:被system_config_id參照的Global row是key authority;Agent row的key只作query/UQ所需冗餘,必須與該Global row的key一致,由同一BO transaction解析、驗證並寫入。Current SystemController.saveOrUpdate先findByKey取得Global,缺少即失敗;既有Agent override更新只改value。這是source-grounded一致性收斂,不新增獨立Agent key catalog。
| Config field/relation | Authority/nullability | Consistency obligation |
|---|
兩表 id | 各自PK;row identity與scope+key UQ分開 | exact target生成方式與完整base columns須在Spec data dictionary明列,不從UQ推導 |
system_config.key | required;Global key authority | UQ依ADR-0302;不得由Agent request形成另一個canonical key |
agent_system_config.agent_id | required Agent reference | 驗證Agent scope及reference存在;scope不是Prefix字串 |
agent_system_config.system_config_id | required Global reference | BO建立override先解析已存在Global;不因House lazy fallback推成writer可建立孤兒 |
agent_system_config.key | required query redundancy;來源為referenced Global.key | 同transaction保持相同;兩個UQ本身不證明reference/key一致 |
兩表 value | 現有schema可null;是否有效由各key consumer contract決定 | House present null/blank按ADR-0262/0281視為BLANK,不把row absence與null混同 |
House reader命中Agent override後仍依既定required resolution chain處理,不新增每次解析都查Global reference的guard。Spec驗收須包含concurrent UQ與reference/key mismatch不得由BO寫成成功,exact locking/constraint names/error mapping由implementation contract收斂;維持無DB foreign key原則。Global key rename/delete/cascade、writer permission與source異常處置未由本矩陣新增授權或決定。#150提供duplicate/orphan/mismatch inventory,#151決定處置,#152才交付SQL。詳見ADR-0302。
#142 Master Authority Reconciliation
本節依2026-09-03由Ron確認的Gateway Currency決策及Supplier Business UTC Offset決策整理較晚authority,修正舊ADR與摘要的衝突;不是新增產品選項或SQL實作授權。
| Master/relation | Current authority與required/immutable語意 | 保留的約束與後續責任 |
|---|
| Agent | Agent自身的單一Currency與明確設定的 business_utc_offset CHAR(6) NOT NULL;offset建立後immutable,無runtime/DB default | Agent offset immutable依ADR-0194/0284;House quota按owning Agent local date,不套Supplier mutation規則 |
| Supplier | Supplier擁有 business_utc_offset CHAR(6) NOT NULL;Create明確提交,無persistent DB default;Supplier不是唯一Supply Currency owner | System Admin-only offset mutation,commit後新transaction立即採新值;舊counts/facts不搬移、不重算,依note47090重新採用ADR-0064/0065/0066 |
| Supply Gateway | required immutable supplier_id與 currency_id;每個Gateway單一Currency,Supplier可透過不同Gateways經營多幣別;Gateway不另存current offset | Currency需更換時建立新Gateway並保留舊owner chain;Gateway只由Supplier解析current offset,不接受child override |
| Agent–Gateway Allocation | N:M;Agent Currency等於Gateway Currency,Agent offset等於Gateway owner Supplier current offset | 保存required positive priority INT NOT NULL DEFAULT 1,可重複;disabled保留原值、missing mapping投影1;本期routing忽略priority。UQ保持Gateway+Agent與Agent+external code |
| Supplier Direction Config | required Supplier+Currency+direction;UQ (supplier_id, currency_id, direction) | 多Currency money limits必須分開;offset仍只由Supplier提供,不將current offset無版本複製到config |
| Gateway Direction Config | required Gateway+direction;UQ (supply_gateway_id, direction) | Currency由Gateway、offset由Supplier解析;per-order min/max各NOT NULL DEFAULT 0,0表示該bound不限 |
| Cardholder/session/Payment Account | Cardholder的required immutable Gateway owner是authority;Supplier/Currency是經該chain驗證並同transaction保存的immutable scope redundancy | Cardholder/session不保存固定Agent;credentials/session contract由#143,Payment Account financial lifecycle由#144;redundancy不得變成第二套owner authority |
| House Card | required immutable Agent owner+由Agent複製的Currency;financial identity及ACTIVE-only UQ依ADR-0231/0232/0233/0244 | quota按houseCardId分開;同ID停啟用不重設,新ID不繼承舊占用;原日/assignment lifecycle依ADR-0284至0301 |
Supplier offset固定格式為±HH:MM,application validation限制-14:00..+14:00與有效minute格式,不保存IANA region。Current Supplier offset與immutable assignment/Order/ledger/report snapshots分開;offset mutation不得重算已保存business date。Agent仍immutable,不能把Supplier的可修改規則套到House owner。
Supplier offset mutation恢復後,既有ACTIVE allocation遇到mismatch要保留、原子停用或阻擋mutation,原本以ADR-0194關閉的問題再次成為#146待決項;本次不代選,candidate/enable/routing的current equality gate仍fail closed。#149須在相應決策收斂後凍結offset mutation API/error contract。這不妨礙記錄#142的owner/column authority,但不得宣稱routing mutation lifecycle已完成。
本表是authority核對,並非全部Master Schema已凍結:其餘exact fields、nullability、lifecycle、unique constraints與實際query index intent仍須逐項核對。Physical indexes需追溯accepted predicates/stable sort並由後續schema review驗證,不因有redundant column就各自建立index。Source migration仍由#150/151完成inventory與cutover分類後,#152才交付Operator SQL。
Supply Cardholder 管理端密碼重設
ADR-0040 Q28 固定管理端重設 Supply Cardholder 密碼由管理者提供新密碼,Backend 驗證後以 hash 保存,不合政策時拒絕且不更新原密碼;成功仍撤銷該 Cardholder 全部有效 App session。Q29 固定成功重設同時設定 must_change_password=true;Cardholder 以該新密碼驗證後只能進入受限 change-password flow,本人成功改密碼前不得取得 formal App session/refresh token,成功改密碼後才原子清為 false 並建立一般 App session。Q30 固定成功重設清零 failed_login_count,且只在 lock_reason=FAILED_LOGIN_LIMIT 時清除 lock;ADMIN_LOCKED、RISK_REVIEW 與 INACTIVE 保留。上述 password、flag、session 與 selective unlock effects 同 transaction 提交/回滾。Q32 進一步固定 biometric 是 App/OS device-local 能力,新 supply_cardholder 不保存 failed_biometric_count,因此 Backend password reset 不處理 biometric state。這不套用 BO/SupplyActor 的 GA/session epoch;exact App contract 仍待 #148 收斂。
ADR-0308 Q31 固定同一可登入且未鎖定 Cardholder 連續五次錯誤密碼後形成 FAILED_LOGIN_LIMIT persistent lock;第五次失敗與 cardholder_status=LOCKED、locked_time、全 App session revoke 同 transaction 提交/回滾,不新增 locked_until 或 timed unlock。第五次以前成功驗證正確密碼會清零 failed_login_count;若 must_change_password=true,仍只能進入受限 change-password flow。未知 username 不建立 counter,IP/device throttling 不屬於此計數。failed_login_count 只記錄 Backend 實際執行 password verification 後的錯誤密碼結果。
ADR-0309 Q32 固定手機 biometric 成功或失敗都屬 App/OS device-local state,Backend 不接受 App 自報 biometric failure 來改變帳號、lock 或 session。未來若引入 Backend 可驗證的 biometric/passkey proof protocol,需另行決策,不能沿用手機自報 counter。
ADR-0310 Q33 固定 supply_cardholder.password_policy_version INT NOT NULL 保存目前 password hash 建立時通過的 LoginPasswordPolicy.VERSION。Create/reset/change password 與 hash 同 transaction 寫入 current version;Login 不因舊版本拒絕,版本只供 audit/migration lineage,不是 hash algorithm、session version 或 authorization epoch。新 row 由 application 明確寫入,不依賴 DB default;Legacy mapping/backfill 交 #150/#151。
ADR-0311 Q34 固定 cardholder_status 只允許 ACTIVE/LOCKED/INACTIVE。LOCKED 必須同時具有非 null lock_reason 與 locked_time,reason 只允許 ADMIN_LOCKED/FAILED_LOGIN_LIMIT/RISK_REVIEW;ACTIVE/INACTIVE 的兩個 lock 欄位都必須為 null。進入或離開 lock 的三欄同 transaction 寫入,歷史由 domain audit 保存;不一致既有資料 fail closed 並交 #150/#151。
ADR-0312 Q35 固定任何成功進入 LOCKED 或 INACTIVE 的操作,都要與該 Cardholder 全部 App session revoke 及 domain audit 同 transaction 提交/回滾;回到 ACTIVE 不恢復舊 session,必須重新 authentication。即使 revoke count 為零也記錄真實結果;session event 不改變既有 Order/assignment。
ADR-0313 Q36 固定授權 command 實際完成 LOCKED/INACTIVE → ACTIVE 時,與 status/lock metadata 及 domain audit 同 transaction 將 failed_login_count 清為零;不改 password_hash、password_policy_version 或 must_change_password,也不恢復舊 App session。
ADR-0314 Q37 固定 unlock 只處理 LOCKED → ACTIVE、activate 只處理 INACTIVE → ACTIVE,交叉來源拒絕且無 partial mutation。兩者對已是 ACTIVE 的帳號回 idempotent success,但不得清 counter、升 version、改 modify metadata、撤銷 session 或寫 mutation audit;exact API/error contract 交 #149。
ADR-0315 Q38 固定 deactivate 可由 ACTIVE 或 LOCKED 進入 INACTIVE;lock 來源的原 reason/time 保存於 audit,current lock metadata 清空,但不清 failed_login_count 或改 credential。真正 transition 與 revoke-all/audit 同 transaction;對 INACTIVE 重送為不升 version、不改 modify metadata、不再次撤銷 session或寫 mutation audit 的 idempotent success。
ADR-0316 Q39 固定 FAILED_LOGIN_LIMIT 只能由 Backend 第五次連續 password verification failure 的 threshold flow 產生;manual lock 只可請求 ADMIN_LOCKED 或 RISK_REVIEW。Reason provenance 必須在任何 account/session/version/audit mutation 前驗證;exact permission matrix 與 API error 分別交 #147/#149。
ADR-0317 Q40 固定 manual lock 只可由 ACTIVE 進入 LOCKED,對 INACTIVE 拒絕。已鎖定且 reason 相同時是無 mutation idempotent success;不同合法 manual reason 則不經 ACTIVE,原子更新 reason、locked_time=now、revoke-all 與 audit,舊 tuple 由 audit 保存。FAILED_LOGIN_LIMIT 不得成為 requested reason。
ADR-0318 Q41 固定每個 Supply Cardholder 同時最多一筆 ACTIVE formal App session。每次成功 formal Login 以 principal-level lock 序列化,並在同一 transaction revoke 全部舊 active sessions、建立新 session;後完成的併發 Login 成為唯一有效 session。Temporary-password restricted flow 不算 formal session;session replacement 不改變既有 Order/assignment。Exact App contract 與 SQL/locking 分別交 #148/#152。
ADR-0319 Q42 固定 refresh token 為 one-time rotating credential;每次成功 refresh 在 session transaction 內原子取代 current hash,同一 token 併發最多一個成功。已使用、過期或未知 token 統一 generic authentication failure,不撤銷或改變目前 session;V1 不保存 token family/歷史 hash,不能從未知 token 推定 confirmed replay。Exact App contract 與 SQL/locking 分別交 #148/#152。
ADR-0320 Q43 固定 refresh_token_hash 使用 ASCII binary-comparison VARCHAR(100) NOT NULL,現行 canonical value 為 sha256:<64 lowercase hex>。完整欄位維持 Unique constraint,不保存 raw token,也不做 case folding、trim 或截斷;額外容量只提供未來 representation headroom,不授權實作自行改格式。Legacy conversion 與 DDL 分別交 #150/#151 與 #152。
ADR-0321 Q44 固定 session status 只允許 ACTIVE、INVALIDATED、EXPIRED,並以 ended_time/end_reason 取代 logout-only naming。ACTIVE 的兩欄皆為 null,兩種 terminal status 的兩欄皆非 null,status 與 terminal tuple 同 transaction;scheduled_logout_time 只表示 pending deadline。Exact reason catalog 已由 Q45 固定,legacy repair 與 DDL 分別交 #150/#151 與 #152。
ADR-0322 Q45/Q51 固定 end_reason 由 Backend transition 決定,caller 不得指定。INVALIDATED 只允許 USER_LOGOUT、NEW_SESSION、PASSWORD_RESET、ADMIN_FORCE_LOGOUT、ACCOUNT_LOCKED、ACCOUNT_DEACTIVATED、GATEWAY_DEACTIVATED;EXPIRED 只允許 ABSOLUTE_EXPIRY、INACTIVITY_TIMEOUT、SCHEDULED_LOGOUT。不提供 generic/free-text reason,lock 細部原因保留於 Cardholder state 與 domain audit。
ADR-0323 Q46 固定 command-driven ended_time 使用 transaction 內單一 operationNow;deadline-driven 使用最早已到期的真實 deadline。Deadline 相同時依 INACTIVITY_TIMEOUT、ABSOLUTE_EXPIRY、SCHEDULED_LOGOUT 排序;processing time 只進 modify_time/audit。Terminal tuple 建立後 immutable;必要 deadline basis 缺失時 fail closed,但不得猜測 metadata。
ADR-0324 Q47 固定 Supply Cardholder formal App session 的 required immutable expires_time = issued_time + 7 days。Refresh、activity、change-password 與 scheduled logout mutation 均不得延長;到期後必須重新 Login。七天不提供 owner/environment override;新 scheduled logout 必須晚於 operationNow 且不得晚於 persisted expiry。Inactivity duration、Access Token 與 restricted-flow lifetime 不由此決定。
ADR-0325 Q48 固定 supply_cardholder_session 不繼承 BaseEntity 且不保存通用 status;session_status 是唯一 persisted lifecycle status authority。Session 不提供 soft delete/restore/enable/disable,terminal rows 保留為 history;表仍明確保存 audit metadata 與 version。Future retention/physical purge 必須另行決策。
ADR-0326 Q49 固定 formal session 不保存 device ID、fingerprint/hash、model/name 或 App version;caller-supplied metadata 不參與 authentication、session uniqueness、lock 或 routing。登入後 FCM registration 由 supply_cardholder_device_token 擁有,device model/name 與 App version 只是不可信 snapshots;raw fingerprint 不保存。
ADR-0327 Q50 固定 last_seen_time DATETIME(6) NOT NULL。Formal Login 以同一 operationNow 初始化 issued_time 與 last_seen_time;後續只由 ADR-0081 的有效 Backend request 單調向後更新。Legacy missing baseline 不得轉為 ACTIVE session或以 fallback time 偽造;兩分鐘 inactivity 與既有 activity semantics 不變。
ADR-0328 Q51 延伸 ADR-0111 的完整 Gateway suspension:實際 ACTIVE → INACTIVE 與 Gateway actor/旗下全部 Cardholder App session revoke、audit 同 transaction,Cardholder session 使用 INVALIDATED/GATEWAY_DEACTIVATED。Cardholder account status/credential 不批次改寫;inactive 期間禁止 Login/refresh/protected authentication,reactivate 不恢復舊 session。Orders/assignments 不變。
ADR-0329 Q52 固定 supply_cardholder_device_token required 引用建立它的 supply_cardholder_session_id,只作 registration provenance/lifecycle dependency,不形成 device-bound authentication。只有 current ACTIVE session 可註冊;source session 轉為 INVALIDATED/EXPIRED 時,同 transaction 失效其全部 active tokens。新 session 不繼承,須重新 registration;notification history 不刪除。
ADR-0330 Q53 固定 device-token status 只允許 ACTIVE/INVALIDATED/DEAD;ACTIVE 要求 terminal tuple 皆為 null,兩種 terminal status 要求 ended_time/end_reason 皆非 null 且同 transaction 寫入。INVALIDATED 使用精確的 user/rotation/session-derived catalog,DEAD 只允許 provider-confirmed FCM_INVALID/FCM_UNREGISTERED;reason 由 Backend/provider result 決定,caller 不得指定,也不得使用 OTHER 或 free text。
ADR-0331 Q54 固定每次 authenticated registration 建立新的 token generation/row,terminal row 永不重新啟用或改綁 source session。ACTIVE 保存 required ciphertext/immutable hash;任何 terminal transition 立即清除 ciphertext 並保留 hash 作 audit。(provider, token_hash) 只在 ACTIVE rows 間唯一;新 registration 與所有被取代 active rows 的 TOKEN_ROTATED transition、terminal tuple、ciphertext removal 同 transaction。Exact active-only uniqueness 與 locking 由 #152 落實。
ADR-0332 Q55 固定 installation ACTIVE uniqueness 為 (supply_cardholder_id, provider, device_id),token ACTIVE uniqueness 則維持跨 Cardholder 的 (provider, token_hash)。Registration 原子取代任一 predicate 命中的 active generations;另一 Cardholder 使用相同 device ID、但不同 token hash 時不得修改對方 row。supply_cardholder_id 只由 authenticated source session 解析;device ID input已由ADR-0333固定為edge-trim後1..128的case-sensitive opaque value,constraint 與 lock ordering 由 #152 落實。
ADR-0333 Q56 依使用者修正,device_id 不限制 UUID 或字元 pattern;沿用既有 edge trim,清理後 required 1..128,超長不得截斷,並以 case-sensitive exact equality 保存/比較。Q55 的同 Cardholder ACTIVE uniqueness 與跨 Cardholder token uniqueness 不變;不同 Cardholders 可使用相同 opaque device ID。Exact App validation/DB comparison 由 #148/#152 落實。
ADR-0334 Q57 固定 token_hash 使用 ASCII binary-comparison VARCHAR(100) NOT NULL,current canonical value 為 sha256:<64 lowercase hex>,建立後 immutable 且 terminal generations 保留。完整 algorithm-tagged value 參與 (provider, token_hash) ACTIVE uniqueness;額外容量不放寬 current format。Legacy representation 與 exact DDL 分別交 #150/#151、#152。
ADR-0335 Q58 固定 token_ciphertext TEXT NULL 沿用既有 version:base64url(iv+ciphertext-and-tag) AES-GCM envelope,不另存 key-version 或建立 binary codec。新 registration 使用 current key;只要仍有該 version 的 ACTIVE row,舊 decrypt key 必須保留。Raw token/ciphertext 不得出現在 response、log、audit 或 notification history;terminal transition 依 ADR-0331 清除 ciphertext。Legacy conversion、key retirement prerequisite 與 exact DDL 分別交 #150/#151、#152。
ADR-0336 Q59 固定 provider VARCHAR(20) NOT NULL 為 Backend-owned FCM,並保存 required platform VARCHAR(20) NOT NULL;V1 request 只接受 ANDROID,共用 enum 的 IOS/WEB 不代表已支援。Provider/platform 在 generation 內 immutable;platform 不加入 Q55 ACTIVE uniqueness/replacement predicates。Legacy classification、exact App contract 與 DDL 分別交 #150/#151、#148、#152。
ADR-0337 Q60 固定 device-token row 只保存 registration lifecycle;last_registered_time DATETIME(6) NOT NULL 在 generation 建立時寫入後 immutable,不承接 current token row 的 used/success/failure/error latest telemetry。Per-notification dispatch result 由 notification outbox 擁有;provider-confirmed terminal result 的 outbox finalization 與 token DEAD tuple/ciphertext removal 使用同一 transaction/operationNow。Legacy telemetry、notification compatibility 與 DDL/transaction proof 分別交 #150/#151、#148、#152。
ADR-0338 Q61 固定 raw FCM token 為 case-sensitive opaque credential;Backend 不得 trim、case-fold、Unicode-normalize 或 truncate。Null/blank、Unicode edge whitespace 或超過既有 4096-character upper bound 的輸入在任何 lookup/mutation 前拒絕;除此不新增 charset/pattern。Hash 與 encryption 使用同一 exact accepted string,exact App contract 交 #148。
ADR-0339 Q62 固定 BO 先驗證 current ACTIVE session 與 Cardholder/Gateway owner guards,再以該 Cardholder 的 (provider = FCM, device_id) 找 active unregister target;platform 與 source session ID 不加入 target identity。Source session 只作 provenance,current session 可移除同 Cardholder 的 stale-source row;owner chain 不一致 fail closed。找到 row 時原子寫 INVALIDATED/USER_UNREGISTERED terminal tuple 並清 ciphertext/active slot;找不到是無 row/history/audit 或 synthetic event 的 idempotent no-op,exact response 交 #148。
ADR-0340 Q63 固定每個 logical Supply notification 只建立一筆 supply_cardholder_notification,同時承擔 App history/read 與 current FCM dispatch lifecycle;不重用 Legacy notification_outbox、不新增第二張 Supply outbox,也不建立 FCM/IN_APP sibling。Row 保存內容/source/idempotency/read fields及 delivery channel、target token reference、dispatch attempt/retry/provider result,不保存 raw token或ciphertext snapshot。FCM failure不移除history;exact enum/retry/nullability/physical index與App contract交 #152/#148。
ADR-0341 Q64 固定 Login 與 FCM registration 分離:App 每次 Login 成功後以 current authenticated session重新 registration。每次 FCM claim/retry只從該 Cardholder current ACTIVE formal session選 ACTIVE/FCM 且 owner chain有效的最新 generation,排序 last_registered_time DESC, id DESC,只選一筆不 fan-out。target_device_token_id 是 current attempt evidence而非enqueue-time immutable target,retry可改綁後續 Login產生的更新 generation;send/finalize只作用於 exact claimed generation並防止 superseded result改寫新 target。沒有eligible target、read/expiry、claim recovery與provider result分別由ADR-0343、0344至0349、0350至0355、0373固定;不保存raw token/ciphertext snapshot。
ADR-0342 Q65 固定committed claim作為newer Login/FCM race cutoff。Claim當下合法的舊generation in-flight send可在較晚Login terminalize舊session/token後完成;Login不等待provider、不取消processing claim,只保證後續claims/retries改選current session最新registration。Finalize先核對exact notification/attempt/target;claimed token仍ACTIVE且provider terminal時與notification同transaction轉DEAD,若已被session lifecycle terminalize則保留既有immutable tuple、只記notification result。Claim material僅可短暫存在memory,不持久化token snapshot;notification result catalog與retry由ADR-0345/0346/0349/0355/0373固定。
ADR-0343 Q66 固定正常找不到current-session eligible registration時,同一notification進入非終態WAITING_FOR_DEVICE,不建立provider claim、不增加attempt_count且target為null。Current-session registration commit後best-effort喚醒同Cardholder的waiting rows,失敗不回滾registration;Scheduler作bounded recovery。Wake-up/recovery重新走ADR-0341 claim path且不fan-out;invariant corruption不得偽裝成waiting。最大等待/expiry、waiting cursor shape、dispatch catalog、provider policy與cadence/batch已分別由ADR-0344/0345、0350、0372、0373、0374固定;未建立或執行SQL。
ADR-0344 Q67 固定FCM delivery eligibility為notification immutable create_time起24小時的absolute half-open window;operationNow >= create_time + 24 hours不得建立新claim。Login、registration、wake-up、waiting re-entry、retry與Scheduler不得重設deadline;deadline後registration不再喚醒該row,但history保留。Deadline前已commit的claim沿用ADR-0342,可在deadline後完成/finalize。Exact expiry tuple已由ADR-0345固定;deadline storage由ADR-0356固定為直接從create_time推導,candidate index direction由ADR-0357固定,Scheduler cadence/bounded terminalization由ADR-0374固定,exact DDL與query plan由#152交付;未建立或執行SQL。
ADR-0345 Q68 固定deadline到達時尚未claim的PENDING/WAITING_FOR_DEVICE/RETRY原子轉EXPIRED,寫dispatch_end_reason = DELIVERY_WINDOW_EXPIRED及dispatch_ended_time = deadline,清next_retry_time/current target,attempt count及既有provider diagnostics不變。Tuple建立後immutable且不可由Login/registration/manual redispatch恢復;已commit PROCESSING、SENT/DEAD/SKIPPED不轉expiry。Claim與expiry使用同row guard仲裁;read/expiry precedence由ADR-0347固定,deadline前lease到期的stale PROCESSING由ADR-0348收斂,lease與deadline皆已到達的claimed row則由ADR-0349以outcome-unknown terminal收斂。只核准schema direction,未建立或執行SQL。
ADR-0346 Q69 固定deadline前read是未claim FCM的delivery cancellation boundary:PENDING/WAITING_FOR_DEVICE/RETRY與read_time在同transaction轉SKIPPED/ALREADY_READ/operationNow,清next_retry_time/current target但不改attempt count或既有provider diagnostics。Mark Read/Mark All Read套用相同逐row規則;IN_APP只更新read state。Read與claim以同row guard仲裁,先commit者決定是否可claim;已commit PROCESSING不由read取消、清claim或阻止exact result finalize。既有terminal tuple不重寫;deadline boundary precedence由ADR-0347固定。只核准schema direction,未建立或執行SQL。
ADR-0347 Q70 固定operationNow >= deadline時expiry優先於read cancellation:read/readAll仍保存read_time = operationNow,但未claim FCM寫EXPIRED/DELIVERY_WINDOW_EXPIRED/deadline,不依Scheduler或row-lock先後改成SKIPPED/ALREADY_READ。同一transition清target/retry,attempt count與provider diagnostics不變;deadline前已commit的read skip、deadline前已commit PROCESSING及其他terminal tuples不重寫。Mark All Read使用單一operationNow判斷全部rows;runtime異常的read+non-terminal row依deadline前後收斂。只核准state-transition schema direction,未建立或執行SQL。
ADR-0348 Q71 固定claim lease到期且operationNow仍在24-hour window內時,stale PROCESSING由bounded recovery轉回RETRY。原claim已計一次attempt,recovery不重複計數、清current target且不推定provider結果或token lifecycle;下一個實際claim才增加attempt並重新選current-session最新eligible generation。Finalizer與recovery以exact status/attempt/target/lease guard仲裁,lease到期只開放競爭;recovery勝出後舊result no-op。此為at-least-once delivery,允許有限transport duplicate但不新增history row;deadline後stale outcome由ADR-0349固定,dedicated lease field由ADR-0350固定,lease duration與不續租政策由ADR-0351/0352固定,recovery retry eligibility由ADR-0353固定為next_retry_time = operationNow且無額外backoff,committed claim cap由ADR-0354固定為5,deadline前attempt exhaustion tuple由ADR-0355固定為DEAD/DELIVERY_ATTEMPTS_EXHAUSTED,latest diagnostic由ADR-0361固定,exact DDL由#152交付。未建立或執行SQL。
ADR-0349 Q72 固定exact PROCESSING claim的lease與24-hour deadline皆已到達時,原子轉DEAD/DELIVERY_OUTCOME_UNKNOWN,dispatch_ended_time = max(claim lease deadline, delivery deadline)。Claim lease使用attempt commit時保存且不續租的原始boundary。此tuple表示已有attempt但provider outcome未知且不再重試,不是假稱provider failure或未claim expiry;保留最後target evidence、清future retry eligibility、attempt count與既有provider diagnostics不變,且不得改token lifecycle。Finalizer先commit則照實收斂、recovery no-op;unknown terminal先commit則late result no-op。涵蓋lease先到期但recovery延遲至deadline後的row;dedicated field、lease、不續租、candidate index、diagnostic與cadence已分別由ADR-0350、0351/0352、0357、0361、0374固定;exact DDL由#152交付。未建立或執行SQL。
ADR-0350 Q73 固定在新supply_cardholder_notification新增nullable claim_lease_expires_time DATETIME(6),只保存current PROCESSING claim的authoritative lease boundary;PROCESSING required、其他status必須null,離開processing的finalize/recovery/terminal transition同transaction清除。next_retry_time只屬RETRY eligibility,其他status必須null,不沿用Legacy initial/retry/lease三用cursor,也不從modify_time推算。Q72先取dedicated boundary計算terminal end time再清欄位。Lease duration/不續租、stale recovery、candidate index、provider-result backoff與cadence已分別由ADR-0351/0352、0353、0357、0373、0374固定;exact DDL由#152交付。未建立或執行SQL。
ADR-0351 Q74 固定每個new claim的initial lease為五分鐘:claim transaction使用單一operationNow寫claim_lease_expires_time = operationNow + 5 minutes,operationNow >=該timestamp才具stale recovery eligibility。五分鐘由notification dispatcher Java domain policy擁有,所有Cardholder/Gateway/environment一致,不讀SystemConfig、tenant/Spring/environment或caller runtime override。Lease不是provider network timeout;不續租政策由ADR-0352固定。只核准domain policy,不修改SQL/Java。
ADR-0352 Q75 固定同一dispatch attempt在claim commit後不得續租或改寫claim_lease_expires_time;不提供heartbeat、renew API、provider-progress renewal或environment extension,read_time、較晚Login/session replacement及token terminalization亦不得改變lease。Lease到期只開放Q71/Q72 recovery競爭;若row仍是同一PROCESSING attempt且exact notification/attempt/target/原始lease guard匹配,finalizer仍可先commit,recovery先commit後late result則no-op。Q72 end time使用原始未續租lease boundary。只核准domain policy,不修改SQL/Java。
ADR-0353 Q76 固定deadline前stale PROCESSING → RETRY recovery成功且仍有new-claim額度時,以同一authoritative operationNow寫next_retry_time = operationNow,不增加recovery-specific fixed backoff並立即具備再次claim資格;這只表示earliest eligibility,不保證同一fire立即送出。Recovery不改attempt count,下一個實際claim才增加;不得套用provider-confirmed retryable result的backoff。若recovery時已到deadline則依ADR-0349直接DEAD/DELIVERY_OUTCOME_UNKNOWN,若之後才到deadline則未claim RETRY依既有expiry/read競爭規則收斂;attempt 5 exhaustion由ADR-0354限制並依ADR-0355在deadline前轉DEAD/DELIVERY_ATTEMPTS_EXHAUSTED。只核准domain policy,不修改SQL/Java。
ADR-0354 Q77 固定每筆FCM Supply notification最多五次committed application-level provider claims,initial claim算attempt 1。attempt_count從0開始且只由成功commit的新claim原子增加為1至5;worker crash/unknown outcome仍消耗已commit attempt,candidate fetch、lock loser、WAITING_FOR_DEVICE、wake-up、read/expiry、finalize與stale recovery不計數。Count在同一notification lifecycle單調不重設,即使retry改選不同token generation亦相同;已達5不得建立第六次claim,第五次exact in-flight result仍可正常finalize。第五次retryable/stale exhaustion依ADR-0355在deadline前使用單一attempt-exhausted terminal failure;只核准domain policy,不修改SQL/Java。
ADR-0355 Q78 依使用者簡化要求,固定attempt 5在delivery deadline前若取得retryable provider result或逾lease而無法再claim,統一建立DEAD/DELIVERY_ATTEMPTS_EXHAUSTED,不拆分business reason。Provider-result路徑以finalizer operationNow、stale路徑以persisted lease deadline寫dispatch_ended_time;兩者都保留attempt_count=5與最後target,清future retry/lease,且不因retryable/stale evidence改device-token lifecycle。已到deadline仍由ADR-0349的DELIVERY_OUTCOME_UNKNOWN優先。只核准domain policy,不修改SQL/Java。
ADR-0356 Q79 固定不在supply_cardholder_notification新增persisted或generated delivery deadline欄位;immutable required create_time是唯一持久化來源,exact deadline由Java計算為create_time + 24 hours。Bounded query先以同一operationNow預算expiryCutoff = operationNow - 24 hours,再直接比較create_time,不得在indexed column上套DATE_ADD/function/cast或改用DB current time。Expiry及unknown-outcome event time使用同一推導boundary;未來若調整24小時政策必須另行保留existing-row semantics。Index direction由ADR-0357固定並由#152執行EXPLAIN;未建立或執行SQL。
ADR-0357 Q80 固定三條state-specific bounded candidate lanes:created-time (delivery_channel, dispatch_status, create_time, id)處理pending/waiting discovery及unclaimed expiry,retry (delivery_channel, dispatch_status, next_retry_time, id)只處理due RETRY,lease (delivery_channel, dispatch_status, claim_lease_expires_time, id)只處理stale PROCESSING;各自按timestamp、id升冪且有limit。不得以nullable OR、column function、memory filtering或共用due欄位混合cursor。Targeted wake-up先沿用Cardholder history/ownership index,不預建第四條index。#152需以EXPLAIN驗證physical order;未建立或執行SQL。
ADR-0358 Q81 固定supply_cardholder_notification以DB CHECK及Java雙層保護已確定的single-row invariants:dispatch_status/attempt_count required,count介於0..5;RETRY iff next_retry_time非null;PROCESSING iff claim_lease_expires_time非null,其他status不得殘留各cursor。Constraint只拒絕,不用trigger猜測或補值;不新增DB FK,也不擴張尚未固定的terminal tuple。#152負責具名CHECK、MySQL enforcement與負向測試;未建立或執行SQL。
ADR-0359 Q82 固定terminal tuple亦採DB CHECK與Java validation雙層保護:非終態PENDING/WAITING_FOR_DEVICE/PROCESSING/RETRY不得保存sent_time/dispatch_end_reason/dispatch_ended_time;SENT要求sent_time與dispatch_ended_time非null且相等、reason null;SKIPPED固定ALREADY_READ,EXPIRED固定DELIVERY_WINDOW_EXPIRED,兩者皆要求ended time且不得有sent time;DEAD要求non-null reason/ended time且sent time為null。Provider message id與last diagnostics不由此constraint猜測,exact provider DEAD reason catalog仍由Java/#152收斂,terminal immutability仍靠guarded writer。只核准constraint direction,未建立或執行SQL。
ADR-0360 Q83 固定supply_cardholder_notification新增non-negative dispatch_version INT NOT NULL DEFAULT 0,作application-managed dispatch lifecycle CAS generation而非generic JPA @Version。任何status/attempt/target/cursor/provider result/terminal evidence mutation與version加1同一atomic transition;只更新read_time且不改dispatch tuple時不增加,故PROCESSING finalizer不因純read產生假衝突。Mutation同時比對expected version及exact domain predicates,affected row為0時不得覆寫;負值或Integer.MAX_VALUE fail closed,不wrap/clamp/重設。#152負責具名CHECK、mapping、preflight與concurrency tests;未建立或執行SQL。
ADR-0361 Q84 固定新增nullable last_error_time DATETIME(6),與last_error_code/last_error_message共同保存同notification最近一次provider-confirmed failure。Failure以exact finalizer單一operationNow寫required stable code/time與nullable sanitized message;success清除三欄,waiting/read/expiry/stale recovery/unknown outcome不建立或覆寫並保留既有真實diagnostic。DB CHECK及Java要求code null時message/time同為null、code non-null時time required;不得從modify_time、retry cursor或DB current time推導,也不新增attempt history。#152負責具名CHECK、length、mapping、preflight與tests;未建立或執行SQL。
ADR-0362 Q85 固定provider_message_id VARCHAR(255) NULL的channel/status shape:FCM + SENT必須non-null/non-blank,其餘組合皆null,包含IN_APP + SENT及所有非成功FCM狀態。Identifier只接受Backend provider adapter的exact success value,不trim/normalize/截斷;非法或超長result不得寫SENT或synthetic id。Success同transaction寫provider id與sent/ended time、清error tuple並增加dispatch version。欄位不作identity/idempotency/lookup,不新增unique或index;#152負責具名CHECK、mapping、preflight與tests,未建立或執行SQL。
ADR-0363 Q86 固定target_device_token_id BIGINT NULL的channel/status shape:FCM PROCESSING/SENT/DEAD必須non-null,FCM PENDING/WAITING_FOR_DEVICE/RETRY/SKIPPED/EXPIRED及全部IN_APP必須null。Claim原子建立target;claimed terminal outcome保留final target,回RETRY及unclaimed skip/expiry維持null。Target只是logical generation evidence,不代表token仍ACTIVE或實際送達,不建立DB FK、unique或dedicated index;#152負責具名CHECK、mapping、preflight與tests,未建立或執行SQL。
ADR-0364 Q87 固定idempotency_key VARCHAR(160) CHARACTER SET ascii COLLATE ascii_bin NOT NULL及global single-column UQ。Central factory以registered immutable producer namespace、logical source identity、recipient與必要delivery distinction產生non-blank exact key,不接受caller完整key、不normalize/截斷;service pre-read只優化replay,DB UQ是concurrency最終防線。Same key只有immutable identity/snapshots相符才reuse,不同則stable conflict,不新增hash欄位。#148/#152負責factory、registry、conflict flow、DDL preflight與tests;未建立或執行SQL。
ADR-0369 Q92 固定supply_cardholder_notification.read_time DATETIME(6) NULL是唯一read state authority:null為未讀、non-null為已讀。第一次Mark Read以單一operationNow寫入,Mark All Read同次操作共用同一時間;之後timestamp immutable,重複read不改read time、audit metadata、dispatch tuple或version,且不提供mark-unread。不保存is_read、read_by或獨立read receipt table;純read不增加dispatch version,同時改dispatch tuple時仍依ADR-0360增加。Physical read-query index由#152依實際query plan驗證;未建立或執行SQL,未修改Java。
ADR-0370 Q93 固定supply_cardholder_notification.notification_type VARCHAR(40) NOT NULL直接保存在row,由Backend versioned enum/registered application catalog管理;建立後immutable且納入same-key replay比較。Unknown、blank或超長type在insert前fail closed,不fallback或截斷;type只作content category/client routing與audit drill-down,不作recipient、authorization、dispatch或單獨idempotency authority。不新增notification-type master table、FK、dynamic CRUD、列舉值DB CHECK或預設dedicated index;exact catalog與query plan由#148/#152驗證。未建立或執行SQL,未修改Java。
ADR-0371 Q94 固定supply_cardholder_notification只保存required immutable supply_cardholder_id BIGINT NOT NULL作recipient/owner reference;Gateway/Supplier/Currency由Cardholder required immutable owner chain取得。不冗餘gateway_id/supplier_id/currency_id/agent_id/prefix_id,不使用owner_type/owner_id polymorphic tuple,也不接受caller owner IDs作scope。Association由service transaction/owner guard/preflight維護而不建DB FK;history index維持(supply_cardholder_id, create_time, id),不預建cross-owner notification index。未建立或執行SQL,未修改Java。
ADR-0372 Q95 固定required delivery_channel VARCHAR(20) NOT NULL與dispatch_status VARCHAR(30) NOT NULL使用inline Backend enums及named DB CHECK,不建立master tables/FK/dynamic CRUD。Channel只允許IN_APP/FCM;status只允許PENDING/WAITING_FOR_DEVICE/PROCESSING/RETRY/SENT/SKIPPED/EXPIRED/DEAD。IN_APP只允許建立即terminal的SENT並以create time作sent/ended time、attempt/version為0;FCM建立為PENDING後使用完整dispatch lifecycle。既有cursor/terminal/target/provider CHECK仍共同適用;未建立或執行SQL,未修改Java。
ADR-0373 delegated D1 依Q78授權與current Legacy source固定reversible baseline:每個committed claim最多四次same-target provider calls,retryable結果間依序等待1/2/4秒且不增加attempt_count。最後仍retryable時,attempt 1至4分別寫next_retry_time = operationNow + 1/5/15/60 minutes,attempt 5直接DEAD/DELIVERY_ATTEMPTS_EXHAUSTED且沒有180-minute fallback;non-retryable rejection使用DEAD/DELIVERY_FAILED。只有明確token-terminal evidence才依ADR-0330/0337同transaction收斂exact target token。Pre-claim invariant corruption不改row/attempt、不建立無target DEAD,改發bounded alert並由#152 preflight/constraints排除;數值由Java policy擁有且既有cursor不回溯重算。未建立或執行SQL,未修改Java。
ADR-0374 delegated D2 依current Scheduler/worker source固定reversible baseline:Supply recovery沿用global FCM_DISPATCH、UTC每10分鐘default cadence與BO-owned worker,不新增Supply task type或scheduler durable state。每次Supply invocation按stale lease、due retry、created-time順序執行ADR-0357三條lanes,每條最多100個admissions且每row獨立transaction;targeted registration wake-up不消耗periodic budgets,overlapping trigger仍須exact guard單一claim winner。未建立或執行SQL,未修改Java/config。
Planned Tables — Backend-first 14
payment_account與house_card是兩個獨立persistence roots,各自使用JPA entity、repository與aggregate service。前者由Supply Cardholder owner chain持有,後者由Agent直接持有;不得合併為generic payment_card、以owner_type及nullable columns混合,或建立base/subtype card persistence。兩者只可共用pure normalizer/value object與model-neutral routing candidate contract/projection。
| Table | Domain fields and SQL types | Unique/index intent | Slice |
|---|
superseded Supply actor relation | owner_type VARCHAR(20), owner_id BIGINT, username VARCHAR(100), password_hash VARCHAR(255), account_status VARCHAR(20), must_change_password BOOLEAN, version BIGINT | UQ username; IDX (owner_type, owner_id, account_status) | B1 |
authenticator_credential | TOTP fields+4個single password-change token欄位+session_epoch BIGINT、last_session_issued_at/sessions_revoked_at DATETIME(6) | UQ (principal_type, principal_id);credential lock index;noBaseEntity;20 columns | B1 |
supply_gateway | supplier_id BIGINT, currency_id BIGINT, code VARCHAR(50), name VARCHAR(100), supply_gateway_status VARCHAR(20), version BIGINT | currency_id required且建立後immutable;version是status+兩個direction limits共同使用的aggregate client version,只改limits也推進;operational status只允許ACTIVE/INACTIVE;name為non-unique display field,identity使用ID/immutable code;UQ (supplier_id, code)永久保留;IDX (supplier_id, currency_id, supply_gateway_status) | B2 |
supply_gateway_agent_allocation | supply_gateway_id BIGINT, agent_id BIGINT, external_channel_code VARCHAR(50), external_channel_name VARCHAR(100), priority INT NOT NULL DEFAULT 1, allocation_status VARCHAR(20), version BIGINT | Java entity固定為SupplyGatewayAgentAllocation;UQ (supply_gateway_id, agent_id);UQ (agent_id, external_channel_code);candidate/mutation/routing皆須驗證Agent currency等於Gateway currency,且Agent business UTC offset等於Gateway owner Supplier offset | B2 |
supplier_direction_config | supplier_id BIGINT, currency_id BIGINT, direction VARCHAR(20), enabled BOOLEAN, min_amount/max_amount/daily_amount_limit DECIMAL(30,10), daily_count_limit INT, version BIGINT | Supplier可有不同Currency Gateways,money config不得跨幣別混用;UQ (supplier_id, currency_id, direction);Business UTC offset從Supplier取得 | B2 |
supply_gateway_direction_config | supply_gateway_id BIGINT, direction VARCHAR(20), min_amount/max_amount DECIMAL(30,10) NOT NULL DEFAULT 0, version BIGINT | row version只供transaction內部concurrency;Supplier/Superadmin不得獨立提交。兩筆row與Gateway status由單一root expectedVersion原子更新;不保存enabled或daily amount/count limits;min/max的0表示該bound不限制,兩者皆正數時min <= max;UQ (supply_gateway_id, direction);Currency從Gateway取得,Business UTC offset從owner Supplier取得 | B2 |
supply_cardholder | gateway_id BIGINT, supplier_id BIGINT, currency_id BIGINT, username VARCHAR(100), password_hash VARCHAR(255), password_policy_version INT NOT NULL, display_name VARCHAR(100), cardholder_status VARCHAR(20) NOT NULL, must_change_password BOOLEAN, failed_login_count INT NOT NULL DEFAULT 0, locked_time DATETIME(6) NULL, lock_reason VARCHAR(50) NULL, version BIGINT | gateway_id是authoritative owner;supplier_id/currency_id由Gateway chain解析並冗餘,三者建立後immutable;不保存agent_id/prefix_id;username identity依ADR-0304跨所有Gateway全域唯一,不以gateway_id分隔;Create/Login先做同一Unicode edge trim、空值拒絕,再與DB uniqueness共用英文字母大小寫不敏感相等性;新建username在trim後僅允許ASCII英文字母、數字及_、-、.,長度1–100,空值或超長拒絕且不得截斷;保存/回傳保留建立時大小寫,Login大小寫變體不改寫保存值;username建立後immutable,含只改大小寫;既有不符格式資料交#150/151;停用/軟刪除後username仍永久占名,含大小寫變體,duplicate check/DB uniqueness不得依status排除;Create/reset/change password與hash同transaction明確寫入current policy version,Login不因舊version拒絕;status只允許ACTIVE/LOCKED/INACTIVE,LOCKED要求reason與time皆非null且reason只允許ADMIN_LOCKED/FAILED_LOGIN_LIMIT/RISK_REVIEW,其餘兩種status要求兩欄皆null;進入LOCKED或INACTIVE與全部App session revoke及audit同transaction,回到ACTIVE不恢復舊session;實際完成LOCKED/INACTIVE → ACTIVE時同transaction清零failed_login_count,但不改password_hash、password_policy_version或must_change_password;管理端password reset成功時must_change_password=true且failed_login_count=0,本人成功change-password前不得建立formal App session/refresh token,成功change後才清flag;failed_login_count只計算Backend驗證密碼後的錯誤結果,連續五次原子轉為LOCKED/FAILED_LOGIN_LIMIT、記錄locked_time並revoke-all,正確密碼在threshold前清零;不使用locked_until;reset只解除FAILED_LOGIN_LIMIT,保留ADMIN_LOCKED/RISK_REVIEW/INACTIVE;biometric維持App/OS device-local且不保存server-side counter;IDX (gateway_id, cardholder_status, id)、(supplier_id, cardholder_status, id) | B3 |
supply_cardholder_session | supply_cardholder_id BIGINT, gateway_id BIGINT, supplier_id BIGINT, currency_id BIGINT, refresh_token_hash VARCHAR(100) NOT NULL, session_status VARCHAR(20) NOT NULL, issued_time/expires_time/last_seen_time DATETIME(6) NOT NULL, scheduled_logout_time/ended_time DATETIME(6) NULL, end_reason VARCHAR(50) NULL, explicit audit metadata, version BIGINT | Security lifecycle特製表,不繼承BaseEntity且不保存通用status;session_status是唯一lifecycle authority,不提供soft delete/restore;不保存device ID/fingerprint/model/App version,caller metadata不形成security binding;Login以同一operationNow初始化issued/last_seen,後者只可依ADR-0081單調向後更新,Legacy missing baseline不得轉為ACTIVE;Gateway停用時atomic revoke並使用GATEWAY_DEACTIVATED,inactive期間禁止Login/refresh/protected auth但不改Cardholder status;Owner scope為immutable snapshot且不保存agent_id;每個Cardholder最多一筆ACTIVE,Login序列化並atomic replace;refresh原子輪替hash,同一token最多成功一次;hash採ASCII binary comparison與sha256:<64 lowercase hex>;status tuple、Backend-owned reason、event-time規則分別依ADR-0321/0322/0323;issued/expires required且immutable,expires=issued+7d,不得由refresh/activity延長,scheduled logout不得晚於expiry;UQ refresh_token_hash; IDX (supply_cardholder_id, session_status, expires_time)、(gateway_id, session_status, expires_time) | B3 |
supply_cardholder_device_token | supply_cardholder_id BIGINT, supply_cardholder_session_id BIGINT NOT NULL, device_id VARCHAR(128) NOT NULL, provider VARCHAR(20) NOT NULL, platform VARCHAR(20) NOT NULL, token_ciphertext TEXT NULL, token_hash VARCHAR(100) NOT NULL, token_status VARCHAR(20) NOT NULL, last_registered_time DATETIME(6) NOT NULL, ended_time DATETIME(6) NULL, end_reason VARCHAR(50) NULL, device_name_snapshot VARCHAR(255) NULL, app_version_snapshot VARCHAR(50) NULL | 登入後 FCM registration authority;session reference只作 provenance/lifecycle dependency,不是 device auth proof;只有 current ACTIVE session 可註冊,source session terminal 時同 transaction 失效 active tokens,新 session 須重新註冊;unregister 先驗證 current ACTIVE session/Cardholder owner guards,再以該 Cardholder+FCM+device ID 找 active target,platform/source session不加入 target identity;同 Cardholder stale-source row可移除,owner-chain不一致fail closed,找不到target是無row/history/audit或synthetic event的idempotent no-op;找到時以單一operationNow原子寫 INVALIDATED/USER_UNREGISTERED tuple並清ciphertext/active slot;provider 由 Backend 固定為 FCM,platform required 且 V1 只接受 ANDROID,兩者在 generation 內 immutable,platform 不加入 uniqueness/replacement predicate;row 只保存 registration lifecycle,last registered time 在 generation 建立後 immutable,不保存 used/success/failure/error latest telemetry,每筆 notification result 由同一筆 supply_cardholder_notification 擁有;provider-confirmed terminal result 的 notification dispatch finalization 與 token DEAD tuple/ciphertext removal 使用同一 transaction/operationNow,token 不保存 provider free text;raw FCM token 是 case-sensitive opaque credential,不做 trim/case folding/Unicode normalization/truncation;null/blank、Unicode edge whitespace 或超過 4096-character upper bound 在 lookup/mutation 前拒絕,其餘不限制 charset/pattern,hash 與 encryption 使用同一 exact accepted string;device ID 是 edge-trimmed、required 1..128、case-sensitive opaque value,不限制 UUID/charset/pattern,超長不得截斷;status 只允許 ACTIVE/INVALIDATED/DEAD,ACTIVE 要求 ciphertext 非 null 且 terminal tuple 皆 null,terminal 狀態要求 ciphertext 為 null、terminal tuple 皆非 null 並同 transaction 寫入;hash required immutable、採 ASCII binary sha256:<64 lowercase hex> 且 terminal history 保留;每次 registration 建立新 generation,terminal row 不得重新啟用或改綁 session;INVALIDATED 只允許 USER_UNREGISTERED/TOKEN_ROTATED 或精確 session-derived reason,DEAD 只允許 provider-confirmed FCM_INVALID/FCM_UNREGISTERED;reason 由 Backend/provider result 決定,不接受 caller 輸入、OTHER 或 free text;new registration 與被取代 active rows 的 TOKEN_ROTATED transition 同 transaction;device name 與 App version 是不可信 display snapshots;不保存 raw fingerprint;ACTIVE-UQ (supply_cardholder_id, provider, device_id) 與跨 Cardholder (provider, token_hash),另一 Cardholder 的相同 device ID 不構成 replacement,terminal history 可重複;IDX (supply_cardholder_session_id, token_status) | B3 |
supply_cardholder_notification | supply_cardholder_id BIGINT, notification_type VARCHAR(40), title_snapshot VARCHAR(120) NOT NULL, body_snapshot TEXT NOT NULL, payload_json JSON NULL, read_time DATETIME(6), source_type VARCHAR(40) NOT NULL, source_id BIGINT NULL, idempotency_key VARCHAR(160) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, delivery_channel VARCHAR(20), target_device_token_id BIGINT NULL, dispatch_status VARCHAR(30), attempt_count INT, next_retry_time DATETIME(6), claim_lease_expires_time DATETIME(6), last_error_code, last_error_message, last_error_time DATETIME(6), provider_message_id VARCHAR(255) NULL, sent_time DATETIME(6), dispatch_end_reason, dispatch_ended_time DATETIME(6), dispatch_version INT NOT NULL DEFAULT 0, explicit audit metadata;不保存generic status、raw token/ciphertext snapshot | 單一row同時是App history/read與FCM delivery outbox;specialized table不繼承BaseEntity lifecycle,不提供soft delete/restore/physical delete,dispatch_status是唯一delivery lifecycle,create_time required immutable;global UQ idempotency_key,central factory以registered namespace、logical source與recipient產生exact key,same-key immutable identity/snapshots相符才reuse、不同則conflict;source tuple在建立後immutable,entity-backed type要求source_id > 0,registered non-entity type(目前為BO_MANUAL)要求source_id IS NULL;不新增source_key,manual request key只保留於idempotency key與domain audit;source只作provenance,不作recipient/owner/authorization/idempotency authority,且不加FK/unique/dedicated index;payload_json保存immutable semantic JSON document,沒有payload時使用SQL NULL而非{}或JSON literal null,非null值必須是valid JSON;不以payload作identity/filter,不加generated column或index;所有channel都要求nonblank title/body,application accepted length分別為1..120與1..2000,超長不得截斷;payload不能取代display content,不支援data-only row;兩個snapshot建立後immutable且不加fulltext/dedicated index;IDX (supply_cardholder_id, create_time, id);每次claim/retry從current ACTIVE session依last_registered_time DESC, id DESC選一筆最新ACTIVE FCM generation,target id只代表current attempt且retry可改綁,不fan-out;committed claim是newer Login cutoff,舊generation in-flight send可完成,Login只切斷後續selection;finalize核對exact claimed target與dispatch version,token已terminal時保留既有tuple只記notification result;正常無eligible current-session token時為非終態WAITING_FOR_DEVICE、target null且不增加attempt,registration後targeted wake-up、Scheduler bounded recovery;delivery window固定為create time起24小時且不被Login/registration/retry延長,deadline阻止新claim但不撤銷已commit claim;未claim PENDING/WAITING/RETRY到期原子寫EXPIRED/DELIVERY_WINDOW_EXPIRED/deadline,清target/retry且attempt與provider diagnostics不變;deadline前read把未claim FCM原子寫SKIPPED/ALREADY_READ/read time,清target/retry且attempt與diagnostics不變,已claim PROCESSING可完成;operationNow到達deadline時expiry優先,read仍保存但delivery維持EXPIRED/DELIVERY_WINDOW_EXPIRED/deadline,不依lock或Scheduler先後;lease在window內到期的stale PROCESSING回RETRY且不重複計attempt,清target後下一個claim重新選current-session token,舊result no-op並接受transport duplicate風險;lease與deadline皆到達的PROCESSING轉DEAD/DELIVERY_OUTCOME_UNKNOWN,end time取兩個deadline較晚者,保留最後target、清future retry且不改token,late result no-op;dedicated claim_lease_expires_time只在PROCESSING required,next_retry_time只在RETRY表達eligibility,離開各自status即清除且不得從modify_time推算;lease固定operationNow起五分鐘且無tenant/environment runtime override,同一attempt commit後不續租;到達boundary只開放recovery競爭,exact finalizer在row仍為同一PROCESSING attempt且version/guard匹配時仍可先commit;provider failure寫latest code/nullable message/time,success清除,非provider transition保留;FCM PROCESSING/SENT/DEAD必須有target,FCM其他狀態及全部IN_APP target皆null,target只作logical evidence且不加FK/unique/index;FCM + SENT必須保存non-blank exact provider message id,其餘channel/status皆null,該欄不作unique/index;任何dispatch-owned mutation增加version,純read_time update不增加;FCM不另建IN_APP sibling且skip/failure/expiry不刪history;dispatch end reason exact length與dispatch candidate index exact order由#152驗證 | B3 |
payment_account_application | supply_cardholder_id BIGINT, gateway_id BIGINT, supplier_id BIGINT, currency_id BIGINT, bank_id BIGINT, account_no VARCHAR(100), account_name VARCHAR(100), applicant_type VARCHAR(20), applicant_id BIGINT, review_status VARCHAR(20), reviewer_type VARCHAR(20) NULL, reviewer_id BIGINT NULL, review_time DATETIME(6) NULL, reject_reason VARCHAR(500) NULL, version BIGINT | Gateway/Supplier/Currency由Cardholder owner chain解析;active identity guard (currency_id, bank_id, account_no);IDX (supply_cardholder_id, review_status, create_time)、(gateway_id, review_status, create_time) | B4 |
payment_account | supply_cardholder_id BIGINT, gateway_id BIGINT, supplier_id BIGINT, currency_id BIGINT, bank_id BIGINT, account_no VARCHAR(100), account_name VARCHAR(100), balance/reserved_amount DECIMAL(30,10), receiving_enabled/sending_enabled BOOLEAN, priority INT, account_status VARCHAR(20), version BIGINT | Gateway/Supplier/Currency是immutable owner redundancy;UQ (currency_id, bank_id, account_no);IDX (gateway_id, account_status, priority, id);balance >= reserved_amount >= 0 service invariant | B4 |
payment_account_balance_log | payment_account_id BIGINT, supplier_id BIGINT, gateway_id BIGINT, supply_cardholder_id BIGINT, currency_id BIGINT, agent_id BIGINT NULL, order_type VARCHAR(20) NULL, order_id BIGINT NULL, assignment_attempt_id BIGINT NULL, event_type VARCHAR(40), reference_type VARCHAR(40), reference_id BIGINT, before_amount/delta_amount/after_amount DECIMAL(30,10), reason VARCHAR(500), idempotency_key VARCHAR(160), event_time DATETIME(6) | owner/currency及nullable Agent/Order/Attempt為immutable ledger dimensions;UQ idempotency_key; append-only;IDX (payment_account_id, event_time, id)、(supplier_id, event_time, id)、(gateway_id, event_time, id) | B4 |
house_card | agent_id BIGINT, currency_id BIGINT, display_name VARCHAR(100) NOT NULL, bank_id BIGINT, account_no VARCHAR(100), account_name VARCHAR(100), card_status VARCHAR(20) NOT NULL, deposit_enabled BOOLEAN NOT NULL, withdrawal_enabled BOOLEAN NOT NULL, daily_amount_limit DECIMAL(30,10) NULL, daily_tx_limit INT NULL, active_identity_key VARCHAR(255) GENERATED ALWAYS AS (CASE WHEN status <> 'DELETED' AND card_status = 'ACTIVE' THEN CONCAT(agent_id, '#', currency_id, '#', bank_id, '#', account_no) ELSE NULL END) STORED, version BIGINT | Agent是direct owner;currency_id由Agent.currency_id複製;display_name是required mutable non-unique BO label;card_status無DB default且只允許House ACTIVE/INACTIVE,Create由Backend寫ACTIVE;兩個direction flags required且無DB default,只控制新Attempt;daily_amount_limit只計Deposit amount並以Java BigDecimal表示,daily_tx_limit合併計算Deposit+Withdrawal筆數並以Java Integer表示;非null值必須為目標type可無損表示的正值,超出precision/scale/range時拒絕且不得round/truncate/clamp;兩欄各自依House value→owning Agent House-specific config→Global House-specific config解析,mixed state有效;owner/Currency/Bank/Account required且immutable;account_no只存[A-Z0-9]{1,100} canonical value;Create Bank必須ACTIVE、屬於Agent且同Currency;UQ uk_house_card_active_identity (active_identity_key)只限制ACTIVE identity並允許多筆INACTIVE/DELETED rows | B5 |
Shared ALTER
| Existing table | New column | Purpose |
|---|
supplier | business_utc_offset CHAR(6) NOT NULL | Supplier是canonical fixed UTC offset authority;格式±HH:MM,不保存國家或IANA zone;Gateway與Cardholder繼承且不可override |
Agent與Supplier的persisted/API contract統一使用canonical businessUtcOffset/business_utc_offset CHAR(6),格式±HH:MM,不保存國家或IANA region ID。Agent/Supplier Create必須明確設定;Effective Agent–Gateway binding必須Agent currency等於Gateway currency,且Agent與Gateway owner Supplier的business UTC offset相同。第一筆Supply Order成功routing保存共同offset snapshot;Commission Accounting Month以terminal eligible occurrence套該snapshot換算。Report則獨立固定UTC hourly,不得由owner offset或Commission month反推Report bucket。
Migration維持最後階段:既有Supplier/Agent offset與Gateway currency的backfill值必須先依source data完成preflight,不在schema設計階段硬編國家、Currency ID或環境default。Spay4 Supplier Create required提供businessUtcOffset;Gateway Create required提供currencyId。Final columns不使用persistent DB default替未來Create補值。
Relationship Diagram
兩張ER合計涵蓋本文件14張planned tables;agent與supplier只作既有logical parent顯示。線段代表service-guarded relation,不代表DB foreign key。
Source Drift
business_zone不放在supplier_direction_config。- 現行
AgentController.update(...)仍接受並寫入businessZone,與ADR-0194 creation-only immutable contract不符;實作時須移除update mutation path並保留Create validation。 - Gateway持有required immutable
currency_id;Cardholder、session、Payment Account、Order/Attempt facts中的Currency是經Gateway驗證的query redundancy或immutable snapshot,不是可獨立修改的override。 supply_cardholder.gateway_id是required、immutable authoritative owner;另保存由Gateway chain解析的immutablesupplier_id與currency_id供scope/money query,但不保存agent_id或prefix_id。Agent使用範圍只由Agent–Supply Gateway Allocation決定。- 不建立
supply_gateway_cardholder_membership;Cardholder若需改由另一Gateway營運,必須建立新identity並保留舊資料,不能覆寫owner。 - 冗餘的Gateway/Supplier/Currency scope不是第二套authority;所有Create flow必須在同一transaction鎖定/驗證authoritative owner chain後寫入,generic Update不得修改。
- allocation
priority依ADR-0105/0120/0123/0127/0230保存required positive integer,default 1且允許重複;disabled保留、missing mapping投影1。Page依priority ASC、Gateway name ASC、Gateway ID ASC穩定排序,但本期Deposit/Withdrawal routing忽略priority。 house_card.agent_id是required direct owner;Java/BO API technical owner field使用agentId,response以agentPrefix作顯示。Prefix不是獨立entity,House Card不保存prefix_id。payment_account與house_card不得共用table、JPA entity或repository。Payment Account的application/review/Cardholder action/balance lifecycle與House的Agent ownership/LINE lifecycle分離;shared routing只使用read-only candidate projection及互斥Order/Attempt references。house_card.currency_id是從owning Agent.currency_id複製的required immutable query redundancy。Create固定POST /houseCard與HouseCardCreateRequestVo:System Admin body agentId required positive;Prefix只可omitted/null並從authenticated principal取得owner,任何non-null值都拒絕且不得silent overwrite。兩者都不接受currencyId,Backend在同一transaction載入resolved Agent並寫入兩欄。Response回currencyId、currencyCode、currencyName;Update不得修改owner或Currency。HouseCardCreateRequestVo只含九個top-level fields:actor-conditional agentId,以及required displayName、bankId、accountNumber、accountName、depositEnabled、withdrawalEnabled、required-present nullable dailyAmountLimit與required-present nullable dailyTxLimit。兩個limit values可各自為null或non-null;null保存為House null並fallback,non-null保存House override,omitted拒絕且不得建立House或audit。不建立nested account/availability/limit object,也不接受Currency、status、ID/version、Bank display、derived eligibility/effective limit/limit source、audit、masked account或direction alias fields;每欄都須說明authority、nullability、validation/normalization及security語意。POST /houseCard成功固定在transaction與audit commit後回HTTP 200;Controller直接回完整HouseCardVo,由ApiAdvice包成ResponseVo<HouseCardVo>,wire envelope為code=2000、空message及exact 24-field data。不回201或Location,也不手動double-wrap、回void、ID-only或minimal projection;duplicate仍是409/40007。HouseCardCreateRequestVo的完整九欄semantic validation固定回HTTP 400+FIELD_VALIDATE_FAILED (40005)及exact [field, reason]兩元素字串array。Backend依agentId、displayName、bankId、accountNumber、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit順序fail-fast,並在Agent/Bank lookup或mutation前完成;既有七欄precedence不變,兩個limits都錯時先回Amount。兩個limit fields依ADR-0268/0263/0270先檢查presence並固定mapping:omitted回field-level exact [field, "required"],present null合法,非number token回request-level exact ["request", "invalid request"],number token為zero/negative回field-level exact [field, "must be positive"],positive number但Amount超出DECIMAL(30,10) precision/scale或Tx為fractional/超signed INT range回field-level exact [field, "invalid format"]。Reason不得包含raw input,且不得由Bean Validation產生alternate "field: message" shape。Malformed JSON、type mismatch及unknown field由ADR-0254處理。- Field-specific semantic
reason只可為required、must be positive、must be omitted、contains control characters、must contain between 1 and 100 code points或invalid format。ADR-0257固定每個field/actor/failure condition與同欄precedence;Prefix non-null agentId永遠是must be omitted,兩個Boolean的false皆有效。Transport的invalid request不得與semantic invalid format互換。 - House Create的missing/empty body、top-level JSON
null或非object、malformed syntax、known-field JSON type mismatch及unknown top-level field固定fail closed並回HTTP 400/40005/exact ["request", "invalid request"]。Unknown field不忽略且不反射field name,response/production log不含parser message、JSON path或raw input;House-local strict parsing不得改動Legacy ObjectMapper/handler。Known field missing/null若已形成typed nullable value則進入ADR-0253 field-specific semantic validation。 - House Create任何duplicate top-level JSON property都必須在tree/VO binding前拒絕,即使values相同也不採first/last wins;回應沿用HTTP 400/40005/exact
["request", "invalid request"],不回顯property/value/parser evidence。Strict duplicate detection只作用於House Create,不改Legacy parser。 house_card.display_name是required mutable BO label;Create/Update先拒絕raw input中的Unicode control characters,再移除前後Character.isWhitespace/Character.isSpaceChar code points,結果須為1..100 Unicode code points。保留emoji、Thai/其他Unicode、大小寫、標點、combining characters及內部一般空白,不作Unicode normalization或collapse;DB保存及response回canonical value。它不唯一且不參與financial identity、matching或routing。Legacy cardName不沿用為SPAY4 API name。house_card.deposit_enabled/withdrawal_enabled是required、無DB default的operator direction intent,Java/API為depositEnabled/withdrawalEnabled。Create明確提交兩值,Update可audited修改;只在建立新Attempt前檢查。House不保存line_matching_enabled或receiving/sending aliases,既有Attempt的LINE/settlement/reconciliation不讀current flags。house_card.daily_amount_limit DECIMAL(30,10) NULL/Java BigDecimal/API dailyAmountLimit只限制每日Deposit累計金額;Withdrawal amount不計入。house_card.daily_tx_limit INT NULL/Java Integer/API dailyTxLimit則合併限制每日Deposit+Withdrawal交易筆數,不得按direction各自取得完整額度。Amount非null時必須> 0、最多20位整數與10位小數;Tx非null時必須是1..2147483647。超出precision/scale/range時拒絕,不得round/truncate/clamp。兩欄都是independent nullable overrides,各自依House non-null value→owning agent_id config override→Global system_config解析,mixed custom/default state有效。Defaults固定使用SPAY_HOUSE_DAILY_AMOUNT_LIMIT = spay.house.daily.amount.limit與SPAY_HOUSE_DAILY_TX_LIMIT = spay.house.daily.tx.limit,Global initial persisted values分別為500000與100000;只沿用Legacy數值,不沿用keys或Deposit-only count語意,且不預建Agent override rows。Agent config只有row不存在才fallback;存在但blank/malformed/non-positive,以及Global absent/blank/malformed/non-positive都configuration-error fail closed,zero不代表unlimited。Amount error只排除new Deposit,Tx error排除new Deposit+Withdrawal。BO page/detail對current requested page或target採all-or-nothing resolution;任一實際需要的fallback config無效就不回PageVo、partial rows/VO、total、null/zero/stale/cached effective value或虛構source。Create/Update/status以post-command configured state在mutation與audit前解析required effective values/sources,只有null欄位才讀fallback;任一失敗不insert/update、不改status/directions/limits、不推進version/modify metadata且不寫audit,status same-state亦不得假裝success。House non-null override不因未使用的Agent/Global config無效而失敗。上述BO page/detail/create/update/status resolution failure固定回HTTP 500+HOUSE_LIMIT_CONFIGURATION_INVALID (40053),不重用request validation、dependency、Agent API或generic internal error code。Create與Update的兩個limit properties都required-present且nullable;explicit null表示該欄fallback,omitted拒絕且不得mutation,non-null才作numeric validation。ADR-0275另固定此configuration error的ResponseVo.data為required空陣列[];不得省略、回null/object/非空array,單筆或多筆、多dimension failure皆相同,且data不帶field/source/reason或config value。ADR-0276另固定:Frontend依code=40053選擇自己的i18n提示;backend message僅供人類診斷,不解析且不凍結exact wording,不作畫面提示的文案authority。本版House limit configuration error依ADR-0277採secret-safe server log/metric供維運診斷,不建立persistent BO alert/incident;失敗command仍不得寫入任何DB row,包含獨立transaction的alert/outbox/audit。ADR-0278固定診斷主體為實際失敗的config scope+key,Agent設定再以owning Agent identity定位;Global根因不因受影響Agent/House而改變,Agent row absent仍是正常fallback。此主體不決定event筆數或去重。ADR-0279固定本次House failure metric labels只使用固定、有限分類值,不帶Agent/House/request/trace等識別值;owning Agent透過診斷log定位,剩餘exact log/metric技術細節後由ADR-0282移交owning implementation slice。ADR-0280將三種numeric failure合併為一類;ADR-0281固定exact diagnostic reason為MISSING(必要Global row不存在)、BLANK(Agent/Global row存在但值為null/blank)、INVALID_NUMERIC(無法解析、非正值或無法無損表示)。Agent row absent仍是正常fallback;reason不放入ResponseVo.data,既有validation不變,ADR-0283固定House config讀取時先用Java 21 String.strip()去除首尾whitespace,present null或strip後empty為BLANK;其餘兩欄共用BigDecimal(String)數值語法,再依Amount正DECIMAL(30,10)與Tx正INT作無損檢查,Tx的100.0/1e2接受為100,fraction/overflow仍拒絕。不改寫DB原字串,也不改JSON request contract。ADR-0282將剩餘House telemetry技術契約交由owning implementation slice在既有約束下定案、記錄,並以測試與operations文件驗證,不再逐項阻擋#142設計收斂;此移交不包含config parser或quota lifecycle產品決策,也不授權立即實作。ADR-0284固定Daily Amount Limit與shared Daily Tx Limit都依House owning Agent的immutable fixed Business Zone切日,每日區間為[當地00:00,次日00:00)。此決策只固定日界與時區authority;初次占用時點後由ADR-0285固定。ADR-0285固定House新assignment成功與所需daily quota占用原子成立:Deposit占用Amount+shared Tx,Withdrawal占用shared Tx;in-flight也減少剩餘capacity,assignment失敗/rollback不留下占用。ADR-0286固定House Deposit初次Daily Amount quota按該筆已驗證Order requested amount全額預留,不扣fee、不預估actual amount;requested為1000時初次預留1000。成功完成的Amount計入基礎後由ADR-0290固定,與成功terminal原子生效後由ADR-0293固定。ADR-0287固定House初次quota以該次成功assignment所記錄的同一事件時間,換算owning Agent local date歸日;Deposit的Amount與shared Tx共用該時間,不各自讀現在時間。當地9/8 23:59建單、9/9 00:01成功assignment時,初次占用9/9 quota;assignment事件時間不要求等於DB commit時鐘。ADR-0288固定新日admission不計入前日仍in-flight的quota;舊占用保留原日,local midnight本身不觸發釋放,也不搬移或重複占用新日quota。Daily Amount與shared Daily Tx採相同原則。每日Amount上限1000、前日600仍進行中且新日尚無其他占用時,新日可再承接1000,跨日進行中總額可達1600;daily quota本身不限制全部跨日in-flight總量,也不保證實際銀行入帳日總量上限。ADR-0289固定已確認沒有該次資金交易且House assignment已正式終止時,完整釋放該次原占用的quota:Deposit釋放Amount+shared Tx,Withdrawal釋放shared Tx。僅到期、未收到通知、待人工判定或已收/付款但未完成,都不符合此釋放前提。反覆assignment後以無交易結案可重複使用額度,shared Tx不能兼作派發次數上限;讓無資金確認與正式終止兩條件俱備的轉換,與原quota完整釋放對admission原子生效,後由ADR-0299固定;確認權限、證據與各狀態mapping依ADR-0300交由#145 grill,定案前不得實作相關未決行為。ADR-0290固定House Deposit已依settlement規則合法成功完成且已有確認actual amount時,最終Daily Amount quota以該actual amount為計入基礎;例如requested為1000、actual為900且已合法確認成功,最終基礎為900。此決策不允許partial payment自動成功,也不直接採raw LINE notification作terminal actual;Actual缺失/無效與fee定義依ADR-0300交由#145 grill,不能由實作自行假設。ADR-0291固定同一次House assignment合法成功完成後,Amount與shared Tx quota仍歸該次initial quota date,不轉到完成日;Withdrawal的shared Tx同理。9/8 assignment預留600、9/9合法成功且actual也是600時,600仍計入9/8;9/9自身已承接900仍為900,不因前日成功結果增加為1500。此決策只選同一次assignment成功歸日;合法重派的新占用與歸日後由ADR-0296固定,重派許可與舊新銜接依ADR-0300交由#145 grill。ADR-0292固定House Deposit已確認真實入款且其餘settlement成功條件均成立時,actual差額造成原日Daily Amount超額也不阻擋成功,完整actual仍計入原initial quota date。例limit始終1000、原預留600、原日其他占用300,確認actual=800後合計1100、超額100;不得把actual裁為700或只為quota不足暫停成功。此例不選overage診斷的limit比較版本;telemetry與實作仍未決。ADR-0293固定House Deposit成功terminal與原日Amount quota由requested轉為確認actual,作為同一次原子狀態轉換對後續admission生效。原占用1000、合法成功actual=900時,成功轉換前不得先釋放100;轉換生效後該筆貢獻為900,其他gates通過才可使用剩餘100。增額與ADR-0292允許的overage同時反映,不得成功後仍留下較小requested造成虛假capacity;跨日只調整原日,不增加今日額度。Exact counter/transaction/lock實作仍未凍結,不要求與外部資金系統形成分散式transaction。ADR-0294固定同一次House assignment合法成功完成後,shared Daily Tx持續保留原占用1筆;Deposit與Withdrawal相同,成功不再加1,也不釋放這1筆。原日總數9筆中一筆由in-flight變成功,仍為9,不變10或8;對admission的貢獻持續為1,不出現中間0或2,跨日仍歸initial quota date。Reserved/completed是否分欄與更新方式交實作;多次重派總數、已發生資金但失敗、人工更正與partial settlement規則仍未決。ADR-0295固定House effective limit合法變更後,新assignment立即採當下新上限,既有每日quota事實保留,不延用當日舊上限,也不因改limit重設/回收占用或取消既有assignment。當日占用800、上限由1000合法降至600,占用仍800且無可用Amount;再合法提高至1200、其他條件不變時可用400。Amount與shared Tx相同;此決策以變更已合法成立為前提;House general Update低於當日占用的儲存規則後由ADR-0298固定,Agent/Global config writer政策與overage診斷比較版本仍未決。ADR-0296固定合法re-dispatch產生的新House assignment,在目標House通過所有gates的前提下,依當下effective limit原子取得自己的quota:Deposit按已驗證requested amount全額占用Amount+shared Tx 1,Withdrawal占用shared Tx 1;新quota date由新assignment記錄的同一事件時間換算目標House owning Agent local date,不沿用舊assignment的占用資格或日期。舊assignment歸9/8、新assignment記錄於當地9/9時,新占用歸9/9;舊占用依原lifecycle處理,不推定已釋放。本決策不授權re-dispatch或選定目標允許範圍;原assignment尚未終止時的失敗效果後由ADR-0297固定,成功替換的舊新占用銜接與相關起始狀態處置依ADR-0300由#145完成產品決策。ADR-0297固定原House assignment尚未終止、且其他規則已允許替換時,若re-dispatch無法成功建立新assignment,這次失敗command保留原assignment/current association、quota與deadline,不因失敗先終止或釋放舊assignment;新目標不留下成功assignment或新占用。例A仍承接Deposit並占用Amount 600+shared Tx 1,合法改派B因容量不足失敗,在沒有其他獨立狀態轉換時,A維持原關係、600+1與原deadline。本規則只限制該失敗command,不阻止獨立expiry/settlement,不復活已終止assignment,也不撤銷先前已提交的結案/release;成功替換的舊新占用銜接依ADR-0300由#145完成產品決策。ADR-0298固定House general Update在其他驗證、權限、version與required config resolution均通過時,允許post-command effective limit低於owning Agent當日已占用quota,不把低於占用本身設為儲存失敗條件;Amount與shared Tx相同。占用800、原上限1000,Update改600可成功;explicit null清除override後繼承有效default 600亦相同。既有占用仍800,新admission依ADR-0295暫無該項capacity,不重設占用或取消既有assignment。Positive/lossless numeric與其他既有guards維持;本決策只涵蓋House general Update,Agent/Global config writer政策仍未決。ADR-0299固定讓House assignment首次同時滿足已確認無資金交易+正式終止的狀態轉換,與適用原quota的完整釋放,作為同一次原子結果對後續admission生效。Deposit釋放原Amount+shared Tx,Withdrawal釋放原shared Tx;不得讓兩條件俱備的結果已生效卻仍等待背景工作交還quota。兩條件可先後成立,只規範使第二個條件成立的轉換;release仍歸原quota date,前日釋放不增加今日額度。確認權限/證據/狀態mapping、重派舊新銜接、外部balance reservation與具體SQL/lock實作不由本決策選定。ADR-0300固定#142保留已接受House quota約束並繼續收斂核心Ownership/Master Schema;剩餘Order/settlement產品決策明確交由#145 grill,包括actual amount確認/有效性/fee定義、無資金確認權限/證據/狀態mapping,以及合法重派與成功替換的舊新銜接。#145定案前不得實作相關未決行為,也不得從quota結果反推partial成功、確認權限或重派許可。#144協作ledger/reservation/reconciliation,#146協作routing selection/fallback;Agent/Global config writer政策不由本次移交定案。這是設計責任移交,不代表#142完成、不變更既有SQL交付順序,也不授權Java/SQL/DB或GitLab操作。ADR-0301固定House Daily Amount與shared Daily Tx的quota identity為houseCardId;即使Agent/Currency/Bank/canonical account相同,不同House IDs也不合併或繼承彼此當日占用。A今日已成功計入800,合法停用A後建立並啟用新ID的B,兩張上限均1000且B無其他占用時,B可承接完整1000,同帳戶當日跨ID合計可達1800。舊quota與Order/Attempt歷史保留原ID,不搬移或重寫;單純同ID停用/啟用不是新identity,不因此重設其占用。新承接仍須通過全部gates,ACTIVE-only uniqueness維持;本決策不新增重派、settlement或SQL規則。其他未決事項須依其owning scope繼續收斂,不得將責任移交當作產品規則已定案。Exact migration collision policy、presence-tracking implementation、其他釋放與重派lifecycle仍未決。- House general Update固定為
PUT /houseCard/{houseCardId} full replacement;HouseCardUpdateRequestVo allow-list只含displayName、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit、expectedVersion。兩個limit fields依ADR-0271沿用Create mapping;任何limit validation failure都不得更新House或其他mutable fields、推進version/modify metadata或寫audit。Update fixed precedence為authenticated route/role gate→House-local strict JSON與allow-list→displayName、accountName、depositEnabled、withdrawalEnabled、dailyAmountLimit、dailyTxLimit、expectedVersion完整field validation→scope-protected target lookup/authorization→version comparison→target-dependent domain guards/mutation;request error優先於missing/out-of-scope/stale target且不得觸發target lookup。Owner/Currency/Bank/Account Number、cardStatus、derived、row-status與audit fields全部拒絕。成功回HTTP 200完整24-field HouseCardVo,同時回configured、effective及per-field source;effective/source fields只讀且不得進入request。 house_card.card_status是required、無DB default的House operational status,Java/API enum為HouseCardStatus { ACTIVE, INACTIVE }且兩碼都依ADR-0237說明。Create request不提交status,Backend明確寫ACTIVE;INACTIVE只排除新Attempt並保留direction flags。它不取消existing Attempt、釋放reservation或阻斷LINE/settlement/reconciliation。BaseEntity status=DELETED另屬row lifecycle。- INACTIVE→ACTIVE只依authorized
houseCardId + expectedVersion修改target row;成功只改target status/version/modify metadata/audit,其他相同identity INACTIVE/DELETED rows完全不動。已有另一張相同ACTIVE或concurrent UQ collision時回409/40007並完整rollback,target維持INACTIVE且不產生audit;不得auto-inactivate、delete或merge其他row。 - House status API固定為
PUT /houseCard/{houseCardId}/status,body class為HouseCardStatusRequestVo且只含required cardStatus: HouseCardStatus與required non-negative expectedVersion: Long;success回ResponseVo<HouseCardVo>。houseCardId、request class/fields及HouseCardVo所有fields都必須有@Parameter或@Schema說明。 - Status transaction在authorized target lock後先比較
expectedVersion,再判斷requested status。Stale version即使same-state仍回HTTP 409+RESOURCE_VERSION_CONFLICT (40051)且不回current target data;current-version same-state回200 full HouseCardVo,不改任何column、JPA version、audit或既有流程。Actual transition成功才升version一次並寫audit。 HouseCardVo exact 24 fields固定為houseCardId、agentId、agentPrefix、currencyId、currencyCode、currencyName、displayName、bankId、bankCode、bankName、accountNumber、accountName、cardStatus、depositEnabled、withdrawalEnabled、nullable configured dailyAmountLimit、required derived effectiveDailyAmountLimit、required dailyAmountLimitSource、nullable configured dailyTxLimit、required derived effectiveDailyTxLimit、required dailyTxLimitSource、bankEligible、bankEligibilityReason、version。兩個source使用HouseLimitSource { HOUSE, AGENT, GLOBAL }並各自對應同欄effective resolution;mixed sources有效。只有configured limits、Bank reference missing時的bankCode/bankName及eligible時的bankEligibilityReason可為null;其餘required。Effective/source fields不持久化且不得出現在mutation request。VO不含BaseEntity status、audit metadata、masked account、單一hasCustomLimit或pair-level source。house_card.status=DELETED只可由已INACTIVE且沒有current assignment、nonterminal/unsettled Attempt、active/review-held reservation obligation或open reconciliation/recovery的row進入。Delete以House row lock+expectedVersion在同一transaction重驗blockers、寫soft-delete及audit;terminal/closed history不阻擋且不cascade。一般query排除DELETED,authorized historical lookup可依persisted ID讀取。house_card.active_identity_key是nullable generated DB guard:只有BaseEntity non-deleted且card_status=ACTIVE時,才由Agent/Currency/Bank/canonical Account組成key;INACTIVE及DELETED皆為NULL。UQ因此保證同一identity最多一筆ACTIVE,也允許多筆INACTIVE/DELETED rows。不得改用status/card_status+identity的直接composite UQ,否則第二筆相同狀態會被誤擋。此generated欄位不是Java writable field或API contract,migration仍須有SQL COMMENT完整說明source、condition、nullable與lifecycle語意。- DELETED House不可恢復,也不提供Restore API。Create duplicate precheck只查ACTIVE canonical identity;現有INACTIVE/DELETED history不阻擋Create,完整Create request仍須通過current Agent/Currency/Bank validations並建立新row、新ID、新audit lifecycle,不繼承舊Display/Account Name/direction flags。Create或Activate造成ACTIVE collision時統一回HTTP 409+
UNIQUE_KEY_EXISTS (40007)且不回existing House ID;DB UQ是concurrent race的最終authority。 house_card.bank_id與account_no是immutable financial account identity。Create required提供bankId/accountNumber/accountName,Backend依ACTIVE status、owning Agent mapping及derived Currency解析Bank;Update不得包含bankId/accountNumber,但可在validation與domain audit下修改accountName。更換Bank/Account須建立新House Card。house_card.account_name VARCHAR(100) NOT NULL是mutable銀行戶名,不是financial identity。Create/Update共用pure validation:先拒絕原始輸入中的Unicode control characters,再移除前後Character.isWhitespace/Character.isSpaceChar code points,結果須為1..100 Unicode code points;保留大小寫、Unicode、標點、combining characters與內部一般空白,不作Unicode normalization或collapse。Response回保存值,Update變更寫domain audit。house_card.account_no只存canonical value:完整輸入只允許ASCII letters/digits/space/tab/hyphen/underscore,移除formatting後以Locale.ROOT轉大寫,結果須符合[A-Z0-9]{1,100}且保留leading zeros。Create、duplicate precheck、UQ與LINE full-account exact match共用同一normalizer;不新增raw account欄位。Authorized House BO API以accountNumber回完整canonical value且不提供masked alias;完整值不得進入URL、log、analytics、audit metadata、error data或真實Swagger example。- Bank後續變成INACTIVE/DELETED、移除Agent mapping或Currency mismatch時,不級聯修改
house_card.card_status;new-routing query即時套Bank current guards。House page/detail required回bankEligible與nullable bankEligibilityReason: HouseBankEligibilityReason;eligible時reason為null,ineligible時依BANK_REFERENCE_MISSING > BANK_DELETED > BANK_INACTIVE > BANK_AGENT_MAPPING_MISSING > BANK_CURRENCY_MISMATCH只回第一個原因。Reason是scope-protected diagnostic,不是routing authority。Bank恢復後可自動恢復資格。In-flight settlement/reconciliation使用persisted House identity,不得與new-routing eligibility共用同一DAO scope。 house_card不以本地欄位保存或宣稱authoritative balance;House outflow availability必須由W1凍結的外部authoritative balance+atomic reservation contract即時決定,該能力不存在時House candidate gate關閉。
Order、Routing 與 Settlement Schema Contract
結論
D1/W1 使用 target-native agent_api_order_claim 連接四條 Agent API path 與 formal Supply order;禁止 ALTER、讀取、fallback 或 dual-write Legacy agent_api_order_log。所有表是 Greenfield candidate DDL,沒有 database foreign key。兩種 formal order 各自保存 BO 專用、僅可追加的備註歷程與其 derived query flag;house_card_transaction 則是唯一的已驗證 LINE HOUSE Card inbound Deposit evidence,絕不重建或連接 Legacy transaction、Cardholder、Payment Account 或 Withdrawal transaction。Order money、reservation、counter 與 HOUSE transaction 統一 DECIMAL(30,10);既有 PT/reporting relation 維持 DECIMAL(38,10)。
supply_deposit_order、supply_withdrawal_order 共同保存:
| 欄位群 | 規則 |
|---|
| claim/Agent/order | claim_id unique;agent_id、agent_order_id、Currency、requested amount 是 immutable create snapshot。Deposit 另有建立時正規化、immutable 的 payer_account;matching、callback 與人工操作不得改寫,非授權 API/CSV 只能遮罩或省略。 |
| current assignment | current_assignment_attempt_id 與 current Supplier/Gateway/CH/Payment Account/HOUSE projection 必須同一 transaction 更新;history authority 永遠是 attempt。 |
| financial/callback | actual amount、status、completed time、callback projection 與 safe payload snapshot;callback 失敗不回滾 committed financial state。 |
| BO remark | nullable bo_remark 只保存 BO 操作文字的 append-only 歷程;每次 trim 後追加,以精確 ; 分隔,單次最多 500 字元、累積最多 4,000 字元,空白或超長一律拒絕、不得截斷。 |
| BO query flag | has_bo_remark 是 bo_remark trim 後非空導出的 stored generated TINYINT(1);writer 不得直接寫入。Agent、current Supplier、current Gateway scope index 都以 flag、order_status、create_time、id 支援 list/CSV 精確布林篩選與穩定排序。 |
| business time | assignment 時保存 immutable UTC offset/business date;不從 current owner 設定回推或重算。 |
Deposit 只接受 EXTERNAL_POOL 或 HOUSE fulfillment;Withdrawal 只接受 EXTERNAL_POOL,因此 Withdrawal 的 current_house_card_id 永遠為 NULL,service guard 必須拒絕任何 HOUSE assignment。
2. D1/W1 relation inventory
| Relation | 寫入責任與主要 identity | 關鍵約束/索引 |
|---|
agent_api_order_claim | Agent API claim;agent_id + order_type + agent_order_id | unique idempotency claim;只連一個 target formal order。 |
payment_account_daily_counter | Payment Account 方向日限額 | UQ (payment_account_id, business_date, direction);lock counter 後調整 reserved/used。 |
payment_account_reservation | account capacity/balance reservation | UQ (order_type, order_id, assignment_attempt_id);immutable owner/Agent/Currency redundancy。 |
supply_cardholder_daily_assignment_counter | CH daily fairness/assignment counter | UQ (supply_cardholder_id, business_date);不保存 agent_id。 |
supply_deposit_order | Deposit formal aggregate | claim_id unique;含 payer bank snapshot 與 current projection。 |
supply_withdrawal_order | Withdrawal formal aggregate | claim_id unique;含 receiver bank、review/disposition projection。 |
supply_order_assignment_attempt | immutable assignment history | UQ (order_type, order_id, attempt_no)、idempotency_key;owner/time/deadline snapshot。 |
supply_order_proof | proof metadata | UQ (order_type, order_id, content_hash);不在 DDL/Audit 保存原始憑證內容。 |
house_card_transaction | 已驗證 LINE HOUSE inbound Deposit evidence | UQ (notification_provider_source_id, provider_transaction_id);canonical replay 回既有 row,payload conflict 拒絕;raw notification 僅存 AES-GCM ciphertext。 |
supply_withdrawal_recovery_case | duplicate/late payout human recovery | 每 order/type 一個 active case 的 service invariant;保存 resolution snapshot。 |
house_card_daily_counter | HOUSE Deposit quota | UQ (house_card_id, business_date);HOUSE identity 彼此獨立。 |
house_card_deposit_reservation | HOUSE Deposit quota occupation | UQ assignment_attempt_id;reserve/release/confirmed actual adjustment 可追溯。 |
- future list/CSV 僅接受可選精確布林
hasBoRemark=true|false;未帶時不加入 predicate,帶入時對 has_bo_remark 作純等值 predicate,不加入全文搜尋、nullable-OR 或 indexed column function。 - list、detail 與 CSV 在既有
OrderWorkspaceScope 和 projection policy 內可回傳 hasBoRemark 與完整 boRemark;備註不新增任何 order visibility,Agent API、Cardholder API 與 Legacy endpoint 一律不外露。 - 未來 append command 必須用 order version/CAS 保護,並與
BO_ORDER_REMARK_APPENDED committed Audit 在同一 transaction 寫入。Audit 僅保存 actor、時間、append action、前後 hasBoRemark、總長度與新增字元數,不複製備註原文。
3. 狀態與 reservation ownership
- Deposit:
PENDING_ROUTING → ASSIGNED → SETTLED;未證明逾時為 EXPIRED;金額或證據爭議為 PENDING_REVIEW。 - Withdrawal:
PENDING_ROUTING → ASSIGNED → ACCEPTED → PROOF_SUBMITTED → SETTLED;逾時、差異或證據不足為 PENDING_REVIEW;已確認 NOT_TRANSFERRED 為 REJECTED_NO_TRANSFER;疑似/確認 duplicate payout 為 PENDING_RECOVERY。 - reservation status 僅
RESERVED、CONSUMED、RELEASED;review-held 是 Order/attempt/reservation 的 combined projection,不新增虛假 reservation status。 - target actual amount 不符或證據不足不得轉
SETTLED;Withdrawal PENDING_RECOVERY/supply_withdrawal_recovery_case 終態由 System Admin 全域裁決,或由已驗證 SUPPLIER principal 在 active immutable own-Supplier owner chain 內、通過既有 emergency authority 與 non-empty reason guard 後裁決。Gateway 不得裁決;Supplier 任一 guard 失敗必須不洩漏存在性地 fail closed。裁決與既有 evidence、resolution/verdict、actor、時間及 non-secret Audit 必須同一 committed transaction 寫入。 NOT_TRANSFERRED 的改派先 reserve 新候選,再同 transaction 換 current pointer、結束舊 attempt 與處理舊 reservation;reserve 失敗不改動舊 state。
4. HOUSE Deposit quota
house_card_daily_counter 與 house_card_deposit_reservation 僅由 Deposit 使用。quota 採 house_card_id + owning Prefix local business_date,不因帳號、Bank、Agent 或跨日合併:
- initial assignment reserve validated requested amount;成功且 actual 確認後原子調整為 actual amount。
- 成功、release 與 overage 都只作用於 initial assignment date;跨日不搬移或重算。
- 只有已確認無入款且 assignment 正式終止才可 release;expiry 或無 notification 不是 release evidence。
- HOUSE 的
withdrawal_enabled 不創造 Withdrawal relation;Withdrawal 的任何 Order/attempt/reservation/current projection 都不能 reference house_card_id。
5. HOUSE LINE ingress、matching 與敏感資料邊界
- ingress 僅能由驗證
notification_provider_source 與 credential 後的 LINE adapter 呼叫;adapter 從 source credential 導出唯一 agent_id,BO CRUD endpoint 不得接收 provider payload。無法導出 scope、非 inbound Deposit 或必填正規化欄位無效時必須在建立前拒絕。 - 同一 transaction 以
(notification_provider_source_id, provider_transaction_id) 去重並寫入 immutable row。canonical immutable payload hash 相同時是 idempotent replay,回既有 row;相同 identity 但 hash 不同時拒絕、不覆寫既有 evidence,且只留下不含帳號、payload 或 ciphertext 的安全 conflict reason。 - 收款 Bank/account 只在同 Agent 的 active HOUSE Card 做 exact resolution。沒有或多個候選時仍保存 evidence,
house_card_id=null、match_status=PENDING_REVIEW;不得猜測 owner。解析成功但沒有唯一可自動結案的 ASSIGNED Deposit order 時保留 UNMATCHED、PENDING_REVIEW 或 MATCH_FAILED 與固定安全 remark。 - matcher 以 persisted transaction 為起點,僅檢查同 Agent、目前綁定該 HOUSE、仍
ASSIGNED 的 Deposit order;auto match 必須同時符合 HOUSE、金額與 Deposit immutable payer_account。唯一候選才可 AUTO 結案;金額、付款帳號、HOUSE、direction、order state 或占用不符時絕不 auto settle。 - auto 或 manual settlement 必須鎖定 transaction、Deposit order、current attempt、HOUSE reservation 與 daily counter,重新驗證所有條件後同一 transaction 寫入 match association、actual amount、terminal order state、reservation/counter consume 與 non-secret Audit。callback/outbox 僅能在 commit 後派送。
- 沒有候選才依 commit 後 1、3、5 分鐘順序重試;多候選、不可解析 HOUSE、終態 order 或欄位不符不自動結案。第三次後保留
PENDING_REVIEW 或 MATCH_FAILED 歷史,不提供額外 dismiss/reject action。 /houseCardTransaction/** 僅提供 page、detail、受控 raw-content read 與 manual-match command。Agent 僅可對 own agent_id 操作;System Admin 才可跨 Agent。raw read 在 service scope guard 通過後才解密,以 Cache-Control: no-store 回應;ciphertext、plaintext、完整帳號與 payload 不得進 Audit、ChangeLog、log、telemetry、CSV 或一般 page/detail。- manual-match command 只接受 transaction ID、Deposit order ID 與例外原因;Agent scope 永遠由 principal 推導。HOUSE、Agent、Deposit direction、可匹配 order state、未被其他 transaction 結案是不可繞過的條件;僅金額或 payer account 不符可由有值原因例外,成功仍走同一原子 settlement path 並標記
MANUAL。
6. 服務層不可變與 Audit 規則
client 不得提交任何 owner/time/current projection 或 has_bo_remark。每個 route、reserve/release/consume、reassign、review/recovery、HOUSE auto/manual settlement、BO remark append、CSV export、callback 和代理 CH mutation 都在 committed mutation 同 transaction 寫入 non-secret domain_audit_log。Supplier recovery 終態裁決的 Audit 另保存原 Gateway/CH 與 reason。Audit 只記 safe before/after summary、actor/scope、reason、request/trace 與時間;不得記錄完整帳號、token、secret、raw proof、LINE raw notification/ciphertext 或 BO remark 原文。
Order ER · 流程 contract
Commission Query and UTC Hourly Aggregate Boundary
Current Boundary
本文件只記錄Commission data requirements與Report/Reconciliation boundary,不是可直接實作的DDL。Supplier/Supply Gateway/Cardholder的Deposit/Withdrawal Commission Accounting Month依Agent與Gateway owner Supplier已匹配的configured Business Zone;Report固定UTC hourly且不拆三張actor-specific physical tables。Commission Report query直接讀取persisted Hourly Financial Aggregate並產生latest monthly result。
歷史文件曾出現30與31張new-table inventory,差異包含report projections與house_card_balance_observation。這些數字都不是frozen schema count;實作前必須逐slice重新盤點。
Commission Rule and No-result-persistence Boundary — #24 to #26
| Candidate table | Accepted data need | Still unresolved before DDL |
|---|
supply_pt_rule | owner type/ID、currency、direction、effective month、rate;Cardholder方向另有base fee與threshold;current/pending/historical revision可追溯 | exact columns、status model、unique/overlap constraint、audit fields與index |
| #25 No table — ADR-0209 | Direction Monthly Commission Result由Report query依month+owner+currency+direction加總hourly eligible amount、套完整公式並round一次 | endpoint、fields、pagination、query plan與timeout |
| #26 No table — ADR-0209/0210/0213/0214 | 不建立Current Result、scheduled calculation attempt、request snapshot或paid/unpaid、settlement、outstanding、recovery、disposition state。資料缺漏不是target failure;只有invalid Currency metadata、numeric overflow或missing applicable PT rule等真正計算錯誤,才使整份Report不回傳Commission amounts或partial results。Business response只回amounts或真正calculation failure,不回freshness/completeness/partial/projector progress metadata;各target可讀實際query時的latest committed aggregate | HTTP/error fields、pagination、retry、timeout;未來若需cache或point-in-time snapshot必須另行決策 |
Commission source facts必須保留UTC occurrence、first successful claim/routing保存的matched configured offset snapshot,並能穩定回連owner chain、Currency、direction與使用的rule revision。Historical retry/re-dispatch/rebuild不得讀current zone重新分月。Commission Report query每次加總target interval內目前已接受的latest Hourly Financial Aggregate eligible amount後套完整target公式一次,回傳latest final;資料缺漏、尚未抵達或coverage未知只是不計入,不阻擋Report、不標partial,也不需要activation boundary。Later fact/correction完成投影後由下一次query反映。不保存先前顯示值,也不處理會計外部的付款與對帳紀錄。
Shared Hourly Candidate — #27
| Candidate table | Accepted data need | Still unresolved before DDL |
|---|
supply_financial_hourly_aggregate | authoritative UTC half-open hour、Commission Accounting Month、owner chain、currency、direction、eligible_amount DECIMAL(38,10)、fact count及source/correction/revision trace identity;同一UTC hour跨configured-zone月界時依Commission month拆row。Eligible amount只做exact decimal addition且不在hour boundary round;late-arriving new fact依canonical source identity恰好一次更新原row,duplicate no-op。Existing fact correction保留原fact,追加source-linked immutable signed delta並沿用原occurrence/offset snapshot恰好一次修正原canonical row。每筆work在同一per-fact transaction取得queue row ownership/status guard並共同commit fact/correction identity、aggregate delta、applied identity與queue PROCESSED;ownership不跨transaction,獨立signed deltas可不依queue ID/claim time套用,cursor仍不得skip gap。只有ownership/status guard winner且實際進入projector才消耗attempt;targeted/recovery loser與fetch-only candidate不消耗。Application失敗完整rollback後,以獨立短transaction在separate immutable failure-attempt candidate append本次evidence,並更新queue current status/committed attempt count/logical eligibility time/nextRetryTime/last classification/code/time/version;兩者共同commit或rollback,Scheduler不join attempt history。Failure attempt以queue identity + committed failure sequence作queue-local unique ordinal,sequence在queue guard下由committed count加1取得,未commit不占用durable ordinal。該metadata不標記applied、不完成queue、不推進cursor,failure-record transaction失敗時queue仍可重試。Initial execution包含在5 total attempts內;failures 1/2/3/4 committed failure後分別以1/5/15/60分鐘backoff維持RETRYING,failure 5直接park為NEEDS_ATTENTION且停止automatic selection,沒有180-minute fallback。Retry values固定為projector-owned Java domain policy,不讀取Global/Agent/Prefix config、Spring property或environment override。只有SOURCE_CONTRACT_INVALID、IMMUTABLE_IDENTITY_CONFLICT與NUMERIC_REPRESENTATION_INVALIDstable domain categories可在attempt 1至4保存evidence後直接park且不排next time;deadlock/timeout/temporary infrastructure、mutable reference暫時缺少、unexpected runtime exception、unknown code與classifier failure仍retry。Classification不得解析exception/SQL message,diagnostic只保存secret-safe摘要。Audited BO operator command只把同一row設為RETRYING/due now並等待Scheduler,保留exhausted history、不修改既有attempt rows且只提供一次額外執行機會,再失敗append新row並立即re-park;SQL只作break-glass。Source commit後best-effort targeted attempt與Scheduler bounded recovery共用同一projector;recovery每1分鐘於UTC整分鐘第0秒觸發,nextRetryTime到期後可能再等不到60秒,HTTP delivery retry不增加projector attempt count;automatic-selectable due rows依logical eligibility time ASC、queue identity ASC admit,queue identity只作tie-break,targeted/application/completion order不受此排序保證;每個logical fire最多admit 50個candidate IDs,no-op/loser不退回slot且same-fire delivery retry不取得新budget;bounded candidate ID fetch/固定30秒soft execution budget仍逐筆commit;budget從第一次candidate query前以monotonic elapsed time起算,active per-fact/failure-record transaction不hard-cancel,不採batch commit、SKIP LOCKED或claim-token/lease baseline。Normal historical projection逐步發布目前已接受facts供Report使用;per-source/partition contiguous cursor只描述projector progress/resume/operations,不作Report completeness gate。Controlled rebuild只在替換既有live bounded scope時於隔離結果重放並驗證後原子發布,失敗時normal readers仍讀舊live scope。Late facts/corrections持續更新latest aggregate;一般Report與Commission Report共同讀取,row本身不保存hourly Commission result | exact aggregate/queue/attempt table names與columns、unique key、source/correction/queue/applied identity columns、attempt technical primary key/columns/index、required source inventory、partition topology、cursor type/state placement、correction ordering、after-commit signal、lock/conditional SQL、queue retry/cursor其餘physical schema、operator endpoint/role/error、overlap/non-reentry、shadow storage、atomic replacement、coverage/checksum fields、concurrent catch-up、operations-only progress metadata(不得進Report response)、G6 optimization、retention與index |
R1 Deferred Reconciliation Candidates — #28 and #29
| Historical candidate | Current status |
|---|
supply_reconciliation_run | PM Deferred;run、version、period與purpose未接受;不得用它重新引入backend payment accounting |
supply_reconciliation_item | PM Deferred;reconciliation grain、evidence與status未接受 |
prefix_reconciliation_daily_report | Historical rejected design;ADR-0191禁止依Prefix建立獨立physical report table |
supplier_reconciliation_daily_report | Historical rejected design;ADR-0191禁止依Supplier建立獨立physical report table |
superadmin_accounting_daily_report | Historical rejected design;ADR-0191禁止依Superadmin建立獨立physical report table |
三張historical actor report名稱只為方便追溯舊文件,不能當成最後table name或「一定要拆三張」的決策。Report products仍須定義actors、用途、fields、authorization與export,但其financial source固定為同一Hourly Financial Aggregate。
House Observation Is Separate
house_card_balance_observation是外部balance evidence候選,不是Commission或report projection,也不是authoritative ledger/available balance。它是否需要獨立table須由House outflow external balance authority contract另行決定,不能為湊歷史31張table而先建立。
Implementation Guard
supply_reconciliation_run/item仍需正式Reconciliation contract;三張historical actor report table不得建立。Shared Hourly Financial Aggregate topology已定,但exact fields/key、projection lifecycle、API/authority未定前不得建立DDL、entity、DAO或projector。- 不得把歷史87條API、30/31張table或三report-product數量當成current estimate。
- Commission已固定query-time exact/final representability、Currency scale/mode、hourly eligible
DECIMAL(38,10) exact addition/no hourly rounding、late-arriving new fact exactly-once inclusion/duplicate no-op、existing eligible fact immutable correction delta回投原canonical bucket、controlled rebuild、per-source/partition projector cursor、per-fact atomic application、after-commit targeted attempt+Scheduler bounded recovery、same-transaction claim/apply、rollback後separate failure attempt、Java-only 5-total-attempt retry schedule、explicit deterministic early-park classification、separate immutable attempt table+queue current state、queue-local committed failure sequence、retry exhaustion park+audited operator requeue及routing offset capture。ADR-0209排除result/payment persistence;ADR-0210經ADR-0213縮限為真正計算錯誤的Report all-or-nothing failure;ADR-0213固定available-facts、amounts-only response semantics並supersede ADR-0211/0212,normal history不等full-scope coverage,Commission也不需要activation boundary;ADR-0214固定per-target latest committed reads且不保證request-wide snapshot;ADR-0215固定每筆identity/delta/applied evidence/queue completion共同commit、獨立delta order不保證且cursor不得skip gap;ADR-0216固定兩條trigger共用projector、bounded candidate fetch/execution budget不形成batch commit,SKIP LOCKED/claim-token/lease維持G6 evidence-gated;ADR-0217固定同一transaction取得ownership/status guard並apply、failure metadata不得推進cursor;ADR-0218固定達門檻後park、operator只requeue同一row並等待Scheduler、history不重設且SQL只作break-glass;ADR-0219固定5 total attempts包含initial execution,只有guard winner實際進入projector才計數,failures 1/2/3/4後依序延遲1/5/15/60分鐘,failure 5直接park且沒有180-minute fallback;operator requeue只增加一次執行機會且不重設exhausted history;ADR-0220固定retry values為Java domain policy,不提供runtime override;ADR-0221固定三個explicit deterministic domain categories可提早park,transient與unknown預設retryable;ADR-0222固定attempt insert與queue current-state update同transaction commit、Scheduler不join history且operator不修改attempt rows;ADR-0223固定queue identity + committed failure sequence queue-local unique ordinal,未commit不占用durable sequence且不得使用未受guard保護的MAX + 1;ADR-0224固定Scheduler bounded recovery每1分鐘於UTC整分鐘第0秒觸發,nextRetryTime只代表最早eligibility,HTTP delivery retry不增加projector attempt count;ADR-0225固定due rows依logical eligibility time ASC、queue identity ASC admit,每個automatic-selectable row保存可直接比較的eligibility value,candidate query不得以indexed-column COALESCE/function或nullable OR臨時計算主要predicate/order;ADR-0226固定每個logical fire最多admit 50個candidate IDs,no-op/loser不退回slot且same-fire delivery retry不取得新budget。在exact source inventory/after-commit signal/lock/attempt technical primary key/columns/index/cursor/correction schema、operator endpoint/role、overlap/non-reentry與其餘projection lifecycle完成前,不建立Hourly Aggregate DDL或Commission API。 - 圖表只呈現candidate boundary,不表示migration或Java已存在。
Diagram
開啟Reporting ER
SPay4 Rebuild Consolidated Spec
1. Outcome and authority
SPay4 以獨立 MySQL、全新 target schema 與新 runtime aggregate 平行重建。SPay3 維持 Legacy runtime,正式切換後至少 30 天僅供唯讀歷史觀察。本文件是 #180~#194 的 replacement source,供 SPay4 Rebuild Consolidated Delivery Multi-Issue parent 與其 child Issue 分解使用。
本文件只授權 planning 與 Issue decomposition;不授權 DDL、SQL execution、database connection、snapshot import、deployment、push、SPay3 physical cleanup 或 runtime implementation。
1.1 Replacement priority
- 本文件的明確 confirmed decision 對 #180~#194 的相衝突規劃敘述優先。
- 已接受 ADR(特別是 ADR-0386)維持其既有 authority;本文件不撤銷未明確變更的 ADR。
- 未與本文件衝突的既有契約、概念文件與 source evidence 維持原意,child 不得藉 replacement 擴張 scope。
- 權限、選單與資料模型審閱稿 是歷史審閱材料,不是
supply_account、Legacy credential transition 或跨 realm report scope 的 authority。
2. Confirmed decisions
| Topic | Decision |
|---|
| Greenfield schema | SPay4 的每張 target table 都從全新 CREATE TABLE contract 開始;不以 SPay3 table、DAO、entity、session、routing、financial 或 reporting 作 target authority。 |
| Import | 只允許核准的 allowlist snapshot import;每個來源 table/column 均需 target mapping、reject condition、reconciliation 與 read-back。 |
| Prefix | Prefix Create 只建立 Agent/Prefix 與必要 BO_USER onboarding;不得隱式建立任何 Supply aggregate、allocation 或 Legacy Payment Channel。 |
| Supplier Create | POST /bo/v2/suppliers(或已核准等價 contract)必傳 name、loginId、businessUtcOffset、defaultGatewayCurrencyId。Backend canonicalize login ID,並保存 code=loginId。 |
| Supplier currency | Supplier 沒有自身單一 currency 語意;defaultGatewayCurrencyId 只用來建立本次 atomic onboarding 的 DEFAULT Supply Gateway。後續 Gateway 的 currency 仍是 Gateway-level immutable attribute。 |
| Default Gateway | 同一 transaction 建立 code=DEFAULT、name={Supplier Name} Default Gateway、ACTIVE、兩方向 0/0 direction config 的 Gateway;不得自動配置給 Prefix。 |
| Account aggregate | supply_account 是唯一 canonical 名稱,DDL、API DTO、JWT principal、credential mapping 與 audit 必須一致;不得新增或沿用 superseded Supply actor relation。 |
| Authorization | Supplier/Gateway 使用 SupplyProfile.SUPPLIER/SupplyProfile.GATEWAY fixed policy;不建立 Supply role tables。BO_USER 保留 versioned、idempotent system_method/role_method seed migration。 |
| Report scope | 共用 report fact 必須持有可索引的 agent_id、supplier_id、supply_gateway_id 等適用 owner key;realm 不是 ownership key 的替代品。 |
| Credentials | target credential 必須重新簽發或由合作方 reconnect;不得匯入、讀取、fallback 或 dual-store Legacy secret/configuration。 |
| BO Domain Audit | 每個成功提交且實際異動 SPay4 資料的 BO_USER command 必須同 transaction 追加恰一筆 audit fact;失敗、no-op、讀取、匯出、external-only action、runtime migration、seed 與 cutover artifact 均不寫入。詳見 BO Domain Audit Contract。 |
| Cutover | 可進行多次 rehearsal,但 production 只允許一次切換。先停止新 SPay3 入口並 drain 或依既有人工流程結案所有 non-terminal Deposit/Withdrawal Order,才可取得最終核准 snapshot、reconcile、enable SPay4 並鎖定 SPay3 唯讀。 |
| Observation | SPay4 enable 後不得回切 SPay3 寫入;SPay3 維持至少 30 天唯讀歷史觀察,僅容許 forward recovery。 |
3. Target boundary
3.1 Greenfield schema and data
- 每張 target table 必須明示 SQL
COMMENT、identity、unique constraint、current/snapshot authority、owner key 與 query-shaped composite index intent。 user 與 supply_account 直接保存其 password、TOTP、lock、session epoch 與適用的一次性 password-change challenge state;Agent credential_reference/credential_secret、audit、Supplier、Supply Gateway、PT、direction config、allocation 與 report fact 必須有清楚 boundary;realm 不得取代 agent_id、supplier_id 或 supply_gateway_id。- Agent OGP/LINE integration secret 僅由新的 target
credential_secret 保存;BO User/Supply account TOTP ciphertext 僅保存在其 principal root,Supply password 亦只保存 BCrypt hash。不得建立或保留 authenticator_credential、supply_account_credential_reference 或 superseded Supply actor relation,也不得混用 owner、ciphertext 或 lifecycle。 - target schema 不保存 Legacy credential material,不取得 Legacy persistence、authorization、session、routing、financial 或 reporting authority。
currency、bank、agent_bank、agent、supplier、system_config、agent_system_config 與已核准 BO_USER master/grant 僅是候選 allowlist;exact mapping 必須由 Import and Legacy disposition child 獨立核准。- 每個 DDL、seed、import artifact 都需要其 child Issue 的獨立人工批准;本 Spec 不構成 SQL execution permission。
3.2 Supplier atomic onboarding
同一 application-service transaction 必須依序建立:
- ACTIVE Supplier:canonical
code=loginId、required businessUtcOffset。 - ACTIVE Supplier initial
supply_account:{supplierLoginId lowercase}_supplier_admin、credential、mustChangePassword=true。 - Supplier Deposit/Withdrawal CURRENT PT rules,rate 均為
0。 - ACTIVE DEFAULT Supply Gateway:currency 為
defaultGatewayCurrencyId,及兩筆 Deposit/Withdrawal 0/0 direction config。 - ACTIVE Gateway initial
supply_account:{supplierLoginId lowercase}_gateway_admin、credential、mustChangePassword=true。 - Gateway Deposit/Withdrawal CURRENT PT rules,rate 均為
0。 - 一筆 required
CREATE_SUPPLIER audit fact:以 Supplier 為 target,並在 semantic_effects 記錄全部子資源的 non-secret committed effect。
任一 validation、credential generation、persistence、audit sanitization 或 audit failure 必須 rollback 整個 aggregate。成功 response 必須以 Supplier/Gateway account 分組,一次性揭露各自 credential material;secret 不得進 audit、log、telemetry 或 Frontend persistent state,也不得以 GET 再次讀取。
4. Authorization, menu and reporting
| Realm | Principal | Authority source |
|---|
BO_USER | System Admin、Prefix | user → role → role_method → system_method |
SUPPLY_ACTOR | Supplier、Gateway | active authenticated supply_account、owner chain 與 fixed policy |
- 同版 policy catalog、menu projection、resource-action guard、owner-scope guard、OpenAPI 及正/負 authorization test 必須一起更新;menu visibility 永遠不是 authorization。
- 每個 request 必須驗證 active account/credential/owner chain、profile、lifecycle、version 與 business guard。
- BO grant mutation、affected token revoke 與 menu/authority cache invalidation 必須是原子結果。
- shared report scope 由 authenticated realm 與 server-side owner chain 推導;client 不得藉任意 supplier/gateway ID 提升 scope。
5. External compatibility and credential transition
5.1 OGP
- 維持
POST /v1/agentApi/depositOrder 與 POST /v1/agentApi/withdrawalOrder external wire contract。 - target adapter 必須以 parameter、response、HMAC、callback 與 ACK regression matrix 證明 parity;Legacy
/depositOrder/assign 不得成為 target fallback。
5.2 LINE
- 維持
POST /agentNotification/webhook、既有 HMAC canonical、msg_content、message_id、type=in/out 與 ACK semantics。 - target LINE adapter 不以 Legacy persistence 作 authority;未驗證的
/public/webhook/line 不得因本 Spec 移除。
5.3 Cardholder App
- 保持全部
/cardholder/** external path、payload 與 Legacy field names。 - target 以 subject discriminator 與 versioned internal contract dispatch;Legacy internal path 只可作 rollback seam,不得是 target authorization 或 session authority。
5.4 Credential boundary
- 所有 OGP、LINE 與 Cardholder target credential 均重新簽發或由外部合作方 reconnect。
- Legacy raw secret、secret reference 與 Legacy configuration 不得被 import、read、fallback 或 dual-store。
- credential read-back、external E2E 與 secret-safe log/audit verification 是切換 gate。
6. Import, Legacy disposition and cutover
6.1 Legacy disposition
SPay4 不建立、不匯入、不得 read/fallback/dual-store:
agent.telegram_token、agent.telegram_target、agent.secret_token_id;supplier.secret_key;agent.business_zone(target 使用 business_utc_offset);agent.is_custom_channel_sort。
下列只保留為 SPay3 runtime/read-only history,不得直接作 target authority:secret_token、agent_channel、payment_channel、agent_payment_channel_binding、payment_channel_card、Legacy card binding、Legacy order/routing/PT snapshot/Cardholder session table。
agent.callback_url、supplier.url、supplier.level、channel、Supplier–Channel relation 與 secret_token 完整 reference graph 必須逐項 inventory,並給出 retain/retire rationale;不得憑推測 import、刪除或 repair。
6.2 Cutover sequence
- Rehearsal 先取得每個 gate 的 evidence;其結果不構成 production approval。
- 停止新的 SPay3 入口;drain 所有 non-terminal Deposit/Withdrawal Order,或依既有人工流程正式結案。
- non-terminal Order 未清空即為 cutover blocker;不得以 snapshot import 隱式承接至 SPay4。
- 凍結 SPay3 寫入後取得最終核准 snapshot,執行已核准 import/reconciliation/read-back。
- 所有 import、reconciliation、external reconnect/read-back gate 通過後,啟用 SPay4 並將 SPay3 鎖為唯讀。
- 維持 30 天唯讀觀察與 forward-only recovery;production enable 後不得 reopen SPay3 write。
資料修正、永久刪除或任何非本文件明示欄位的處置,均需新的人工批准。
7. Decision and traceability matrix
| Parent constraint | Source | Replaces / preserves | Owner |
|---|
| Greenfield target、frozen Legacy、no dual-write、30-day observation | ADR-0386;#195 replacement Spec | 取代 #180~#188 的重複規劃,保留未衝突 ADR | A、B、E、F |
| allowlist-only snapshot、Legacy disposition、no speculative repair | #195 replacement Spec;本文件 §6.1 | 收斂 #183、#187、#194 | B、F |
supply_account canonical naming | confirmed replacement decision;本文件 §2 | 明確取代歷史 superseded Supply actor relation proposal | A、C、D |
Supplier 無單一 currency;Create 使用 defaultGatewayCurrencyId 建 DEFAULT Gateway | confirmed replacement decision;本文件 §2、§3.2 | 明確取代「Supplier Create 不帶 currency」的歷史描述;不改變 Gateway currency immutable rule | A、C |
| owner-key report scope | confirmed replacement decision;本文件 §3.1、§4 | 取代跨 realm 以 realm 欄位承載 owner scope 的解讀 | A、D |
| fixed policy、idempotent BO seed、menu 非 authorization | #195 replacement Spec;本文件 §4 | 收斂 #189 及歷史 review proposal | D |
| reissue/reconnect、no Legacy secret authority | #195 replacement Spec;本文件 §5.4 | 取代任何 Legacy secret import/read/fallback/dual-store 選項 | B、E、F |
| single production cutover、non-terminal drain、forward-only recovery | confirmed replacement decision;本文件 §6.2 | 收斂 #187、#193、#194;rehearsal 仍可重複 | B、E、F |
8. Multi-Issue split and dependencies
| Child | Scope | Depends on |
|---|
| A. Target schema foundation | target CREATE TABLE contract、comment/constraint/index/seed boundary、owner/report scope | — |
| D. Authorization and menu | shared auth contract、fixed policy、BO seed migration、report scope guards | A target contract |
| B. Import and Legacy disposition | allowlist/reject list、mapping、reconciliation、Legacy inventory | A/D outputs as applicable |
| C. Prefix and Supplier onboarding | Prefix negative boundary、Supplier/DEFAULT Gateway atomic onboarding | A/D outputs as applicable |
| E. OGP/LINE/Cardholder compatibility | adapters、wire regression、reconnect、rollback seams | A/D outputs as applicable |
| F. Cutover and forward recovery | rehearsal、single production runbook、observation/recovery | verified B–E evidence |
A 先固定 target contract,D 接著固定共用 authorization contract;B、C、E 可在其需要的 A/D 輸出可用後規劃或實作。F 必須等待 B~E 的驗證 evidence。每個 child 仍須依其風險與 prerequisite 決定實際排程。
9. Parent-level exclusions and acceptance
Exclusions
- SPay3
ALTER/DROP、dual-write、Legacy secret read/fallback/dual-store、未核准 data repair、production execution、deployment、push 與永久刪除。 - 未經 child Spec 與人工批准的 DDL、seed、snapshot import、API expansion、cutover 或 destructive cleanup。
- 將 Legacy Payment Channel、Legacy Session、Legacy credential 或 Legacy routing DAO/table join 當作 target authority。
Parent acceptance gate
Multi-Issue parent 的完成定義是六個 child Issue 都具備可獨立驗收的 scope、non-goals、dependencies、acceptance criteria、verification evidence 與 SQL prerequisite boundary;不代表任何 runtime 已完成。跨 Issue invariant 只在 parent 宣告一次,child 以連結引用,不重複或稀釋 authority。
SPay4 Cutover and Forward Recovery Runbook
本文件是 GitLab #202 的 implementation-input runbook contract。它定義 rehearsal 與唯一 production cutover 所需的角色、gate、evidence、停止條件與 recovery boundary;不是任何 SQL、snapshot import、deployment、credential reconnect、production cutover 或資料修正的執行授權或執行紀錄。
1. Authority and deployment isolation
SPay4 Rebuild Consolidated Spec §2、§5.4、§6.2 與 ADR-0386 是本文件 authority。SPay4 target 採用獨立 spay4-bo 模組與獨立環境;SPay3 與 SPay4 不在同一模組混入彼此邏輯,也不共用 target runtime、DB 或 credential authority。
此技術隔離不改變 business cutover boundary:production enable 前仍必須停止 SPay3 新寫入、清空或依既有人工流程結案所有 non-terminal Deposit/Withdrawal Order,並完成最終核准 snapshot、import、reconciliation、read-back 與 external reconnect evidence。SPay4 enable 後不得 reopen SPay3 write、dual-write、回切寫入或把未結 Order 隱式承接到另一系統。
2. Roles and signing boundary
Production 採職能分離簽核。每位 owner 只簽證其職能範圍的 evidence;Cutover Coordinator 只能確認順序與齊備性,不能以自己的簽名取代缺少的專業 evidence。每個 production gate 都必須在 verification matrix 的 record 中保存日期、release identity、owner、evidence locator、結果與簽證。
| Role | Responsibility | 不可取代的 evidence |
|---|
| Cutover Coordinator | 排定 rehearsal/production sequence、確認所有 gate 通過或停止 | complete gate record 與 final go/no-go declaration |
| SPay3 Write Owner | 停止所有已識別 SPay3 新寫入入口、確認 non-terminal Order drain/既有人工結案,以及 read-only lock | ingress write-stop read-back、non-terminal count/read-back、read-only read-back |
| Target Operator | 執行各自已核准的 target command、snapshot import、reconciliation/read-back 與 target enable | exact approved artifact identity、Operator approval、target read-back |
| External Integration Owner | 完成 target credential reissue/partner reconnect、callback verification 與 external E2E | reconnect confirmation、opaque reference read-back、secret-safe E2E evidence |
| Verification Owner | 獨立檢查 B~E evidence、snapshot/import/reconciliation/read-back、authorization/readiness 與 observation record | signed verification checklist |
| Incident Decision Owner | 在 P0、credential compromise 或 discrepancy 發生時作人工 containment/recovery 判斷 | incident record、scope/rationale、new approval when required |
具名人員、on-call 代理與操作權限只在實際 rehearsal 或 production record 綁定;本 runbook 不授予任何角色 SQL、deployment、credential、data-correction 或 permanent-deletion 權限。
3. Rehearsal contract
Rehearsal 可重複,目的為驗證每個 gate 能產生可讀回 evidence;rehearsal 成功不構成 production approval,也不得消耗唯一 production cutover。rehearsal 必須使用隔離 environment、明確 release identity 與 non-production operator approval。
rehearsal 至少證明:
- 所有 required B~E artifact、authorization/readiness、target schema contract 與 external reconnect evidence 能被定位並獨立驗證。
- SPay3 write-stop、non-terminal Order drain/人工結案、final snapshot、approved import、reconciliation/read-back、target enable 與 read-only lock 都有 owner、evidence 與 stop condition。
- 任一 pre-enable gate 失敗時,target 不接收 production authority,且可依已核准 procedure 回到未切換狀態。
- 所有 credential evidence 只使用 opaque reference 與 secret-safe log/audit;不得 import、read、fallback 或 dual-store Legacy secret/reference/configuration。
rehearsal record 的必要欄位與驗收方法見 verification matrix §1。
4. Production-only single-cutover sequence
production 只允許在全部 pre-enable gate 已完成、可讀回且職能分離簽證後進行。任一 gate 未通過、evidence 過期、artifact identity 不符或 owner 未簽證時,結果固定為 NO-GO;不得以口頭確認、部分資料、舊 rehearsal 或 Coordinator 單方判斷取代。
| Order | Gate | Required evidence | Stop condition | Pre-enable recovery point |
|---|
| P0 | Bind production record | release、target environment、approved artifact、owner/on-call identity 皆已讀回 | 任一 identity 或 approval 缺漏/不符 | 尚未停止 SPay3 write |
| P1 | Confirm prerequisite evidence | B~E verified evidence、target readiness、authorization/readiness、external reconnect evidence 完整 | 缺任一 prerequisite 或 evidence 無法驗證 | 尚未停止 SPay3 write |
| P2 | Stop SPay3 new writes | 已識別 ingress write-stop read-back;無新寫入 acceptance | 任一 SPay3 write path 仍可接受新寫入 | 依既有受控 Legacy procedure 回到未切換,尚未 enable SPay4 |
| P3 | Drain non-terminal Orders | 所有 Deposit/Withdrawal non-terminal Order 都已 terminal,或每筆依既有人工流程正式結案 | 任一 non-terminal Order 未處置 | 維持 target disabled;不得以 import 承接未結 Order |
| P4 | Final approved snapshot | frozen-source snapshot identity、範圍、核准與取得時間 | snapshot 未核准、不完整或來源仍可寫入 | 維持 target disabled;重新取得核准 snapshot |
| P5 | Import, reconciliation and read-back | exact approved import artifact、Operator approval、mapping/reject/reconciliation/read-back sign-off | import/reconciliation/read-back 任一失敗或 discrepancy 未處置 | 維持 target disabled;不得自行 data correction/delete |
| P6 | External target readiness | credential reissue/reconnect、opaque reference read-back、callback/HMAC/ACK external E2E 均通過 | 缺 reconnect/read-back/E2E,或有 Legacy credential authority | 維持 target disabled;不得 fallback 至 Legacy credential |
| P7 | Enable SPay4 | Target readiness/readiness gate、all owner sign-off 與 enable read-back | 任一 owner 未簽證或 enable read-back 失敗 | 僅在 enable 前可回到未切換;enable 後進入 forward-only recovery |
| P8 | Lock SPay3 read-only | SPay3 read-only lock read-back;新 write attempt 被拒絕;history read 可用 | read-only lock/read-back 不成立 | enable 後不得 reopen SPay3 write |
| P9 | Start 30-day observation | observation record、daily evidence owner 與 incident routing 已建立 | 缺 record 或 owner | enable 後不得回切;以 forward recovery 補救 |
P7 成功後,SPay3 不能重新寫入,也不能把 SPay4 Order、callback、session、credential 或 data 回寫/轉移到 SPay3。環境與模組隔離不構成略過 P2、P3、P8 的理由。
5. Observation and forward recovery
SPay3 read-only observation 至少持續 30×24 小時。Observation completion 由 Incident Decision Owner 依每日 evidence 作人工判斷;不得以自動閾值、日曆到期或無 evidence 的宣告取代。Observation completion 不授權刪除 SPay3、永久刪除資料、停止 read-only history,或改變 credential retention boundary。
Observation 必須每日記錄:
- SPay3 read-only read-back 與拒絕新寫入的 evidence;
- SPay4 target health、authorization/readiness、reconciliation/read-back 與 external callback/ACK evidence;
- open incident、credential compromise、reconciliation discrepancy 的 scope、owner、rationale 與 resolution evidence;
- 未完成或需要新人工批准的 data correction、permanent deletion、credential reconnect 或 break-glass request。
P0、credential compromise 或 reconciliation discrepancy 不採預先定義的自動 containment。Incident Decision Owner 依事件證據判斷處置;任何 data correction、permanent deletion、credential reconnect 或 SQL/production command 都必須先取得新的 exact approval。受影響 target integration 可被隔離,但不得自動跨環境停止、重開 SPay3 write、dual-write、讀取 Legacy credential,或作不可逆 cleanup。
6. Explicit exclusions
本文件不提供 SQL、DDL、snapshot/import script、deployment command、credential material、secret reference、production write switch、rollback-to-SPay3 procedure、data correction、permanent deletion 或 automatic incident response。它也不把 SPay3 的 Legacy persistence、session、authorization、routing、financial/reporting logic 作為 spay4-bo target authority。
SPay4 Cutover and Forward Recovery Verification Matrix
本文件是 GitLab #202 的 documentary verification evidence。它定義 rehearsal 與 production record 必須收集的 gate-owner-evidence-stop-condition matrix、rehearsal record template、final sign-off checklist 及 observation checklist;不是 rehearsal、SQL、import、deployment、credential reconnect 或 production cutover 的執行結果。
1. Rehearsal record template
每次 rehearsal 建立一筆獨立 record;record 必須標示是 rehearsal,且不得被當作 production approval。
| Field | Required content |
|---|
| Record identity | rehearsal ID、日期、isolated environment、target release identity、target module identity |
| Scope | 被驗證的 P0~P9 gate、使用的 non-production artifact/fixture、明確 exclusions |
| Owners | Cutover Coordinator、SPay3 Write Owner、Target Operator、External Integration Owner、Verification Owner、Incident Decision Owner 與代理資訊 |
| Evidence | 每個 gate 的 immutable locator、artifact hash/version、read-back result、secret-safe log/audit reference |
| Result | GO、NO-GO 或 ABORTED;每個未通過 gate 的 stop condition 與 owner |
| Recovery | target 是否仍 disabled、是否回到未切換狀態、是否遺留任何未授權 data mutation |
| Lessons | 待修正 runbook/artifact/readiness gap;不得把 lesson 當作 production approval |
Rehearsal 至少要成功驗證 P0~P9 的 record 可被填寫、每個 gate 可 fail closed、以及 pre-enable failure 不會使 target 接收 production authority。任何缺少 owner、evidence locator 或 read-back 的 rehearsal 都不是有效 rehearsal evidence。
2. Gate-owner-evidence-stop-condition matrix
| Gate | Accountable owner | Minimum evidence | Stop condition | Required disposition |
|---|
| P0 production binding | Cutover Coordinator | production record、release/target/artifact/owner read-back | identity、approval 或 on-call binding 缺漏 | NO-GO,不停止 SPay3 write |
| P1 prerequisites | Verification Owner | B~E verified evidence、target authorization/readiness、external evidence | 任一 prerequisite 不完整、過期或不可驗證 | NO-GO,不停止 SPay3 write |
| P2 SPay3 write stop | SPay3 Write Owner | 全部已識別 ingress write-stop read-back、拒絕新 write proof | 任一入口仍可接受新 write | 回到 pre-enable 未切換狀態 |
| P3 order drain | SPay3 Write Owner | non-terminal count/read-back、每筆既有人工結案 record | 任一 non-terminal Order 未處置 | target disabled;不得 import 承接 |
| P4 final snapshot | Target Operator | frozen-source snapshot ID、scope、approval、timestamp | source 未凍結、scope 不符或 approval 缺漏 | target disabled,重新取得核准 snapshot |
| P5 import/reconciliation/read-back | Target Operator+Verification Owner | exact approved artifact、Operator approval、mapping/reject/reconciliation/read-back sign-off | import failure、unresolved discrepancy 或 read-back mismatch | target disabled;需新 approval 才能 correction/delete |
| P6 external readiness | External Integration Owner | reissue/reconnect confirmation、non-secret Agent integration/Supply password reference lifecycle read-back、HMAC/callback/ACK E2E proof | 缺 reconnect、E2E、read-back,或發現 Legacy credential authority | target disabled;不得 fallback |
| P7 SPay4 enable | Cutover Coordinator | 全 owner sign-off、target readiness、enable read-back | 任一 sign-off/read-back 缺失或失敗 | 未 enable 前 NO-GO;enable 後僅 forward recovery |
| P8 SPay3 read-only | SPay3 Write Owner+Verification Owner | lock read-back、reject-write proof、history-read proof | lock/read-back 未成立 | 不得宣告 cutover complete;不得 reopen SPay3 write |
| P9 observation start | Verification Owner | daily observation record、incident routing、owner/on-call binding | record 或 owner 缺失 | 不得宣告 observation started;走 forward recovery |
3. Final snapshot, import, reconciliation and read-back sign-off checklist
production P4~P7 前,簽證者必須逐項確認:
- ☐ final snapshot 的 source 已停止 SPay3 新寫入,且 snapshot scope、identity、timestamp 與核准相符。
- ☐ 使用的 import artifact 是 exact approved version,且有對應 Operator approval;沒有未核准 SQL、mapping、repair 或 delete。
- ☐ import allowlist、reject condition 與 rejected-record disposition 都已保存,且不包含 Legacy credential/reference/configuration。
- ☐ reconciliation 對每個已核准 mapping 的 count、identity、business invariant 與 required owner scope 完成比對。
- ☐ target read-back 與 snapshot/import/reconciliation 產物一致;任一 discrepancy 均有明確 unresolved status 或已另行核准處置。
- ☐ target authorization/readiness、release identity 與 external reconnect/read-back 都已通過;menu visibility 不被當作 authorization proof。
- ☐ 所有 owner 已分別簽證,Cutover Coordinator 已記錄 go/no-go;rehearsal record 沒有被當作 production approval。
4. External callback, reconnect and incident checklist
| Scenario | Required evidence | Failure boundary |
|---|
| target credential reissue/partner reconnect | partner confirmation、non-secret Agent integration/Supply password reference lifecycle read-back、secret-safe log/audit review | 不得 import/read/fallback/dual-store Legacy raw secret/ciphertext/reference/configuration;不得 read-back target ciphertext |
| OGP/LINE callback | target HMAC、request/response、ACK、duplicate/idempotency E2E evidence,且 callback runtime destination 只取 agent.callback_url | invalid HMAC、malformed request、duplicate mutation、credential/pending URL/request payload callback source 或 secret log/audit 均 fail closed |
| Cardholder external surface | retained path/object behavior、target-only JWT/session/internal dispatch evidence | Legacy JWT/session/internal path 不得成為 target fallback |
| credential compromise | incident scope、Incident Decision Owner rationale、affected-integration isolation/recovery evidence | 不得自動跨環境停機、重開 SPay3 write或讀取 Legacy credential |
| reconciliation discrepancy | discrepancy identity、scope、read-back、owner decision、new approval if correction required | 不得自行 data correction、SQL repair、permanent deletion或忽略 mismatch |
| P0 incident | incident record、human containment/recovery rationale、target/Legacy state read-back | 不得預設自動處置、dual-write、回切寫入或不可逆 cleanup |
5. Thirty-day observation checklist
每日 observation record 必須至少保留:
- ☐ 日期、target release/environment identity 與值班 owner。
- ☐ SPay3 read-only lock read-back、history read proof 與新 write reject proof。
- ☐ SPay4 authorization/readiness、target health、reconciliation/read-back、external callback/ACK proof。
- ☐ open incident、credential compromise、reconciliation discrepancy 的 scope、owner、人工決策與 evidence locator。
- ☐ 所有待處理 data correction、permanent deletion、credential reconnect 或 break-glass request 均標示為缺少新 approval,不得自動執行。
滿至少 30×24 小時後,Incident Decision Owner 依這些 evidence 決定 formal observation 是否完成。完成 observation 不授權永久刪除、停止 SPay3 read-only history、回切寫入或改變任何 credential boundary。
6. #202 acceptance coverage
| Issue acceptance criterion | Documentary evidence |
|---|
| 至少一次 rehearsal 可依 runbook 滿足全部 gate | §1 rehearsal template 與 §2 P0~P9 gate matrix;未來每次 rehearsal 仍須實際 record 驗證 |
| production runbook 拒絕未 drain Order、缺 reconnect/import/reconciliation evidence、SPay3 write reopen | §2 P2~P8 stop conditions;runbook §4 |
| production enable 後只有 forward recovery;SPay3 至少 30 天唯讀 | §5 observation checklist;runbook §5 |
| 每個 gate 有 owner、evidence、stop condition、回復點與不可回切界線 | §2 gate matrix;runbook §2、§4 |
| data correction/permanent deletion/credential compromise 需要新人工批准 | §3~§5;runbook §5~§6 |