CR #201 » SCS-001_授權服務程式設計規格.md
SCS-001 授權服務(authorization-service)程式設計規格
| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-001 |
| 版本 |
1.17(CR-054 P2/P3:§7.15 佔用事件消費補 instrumentId 落地;新增 §7.18 卡片級額度投影查詢——V14 投影加 instrument_id、GET /limit-projections/…、ProjectionLagMonitor 落後告警。前版 1.16+(CR-048:AuthorizeRequest.payPath 結構化支付路徑欄位;V13__authorization_pay_path.sql)(CR-043 §2.1/§2.2:reversals/overrides 之回應型別修正——原契約誤宣告 AuthorizeResponse(含 decision/checkResults,對沖正/核決語意不合理**),改為 ReversalResult(authNo/type/amountMinor/holdReleased/reversedAt)與 OverrideResult(authNo/decision/force/holdReleased/overriddenAt);§6 API 表更新+§7.6/§7.8.2 補記。★ 契約重生 0 錯誤;verify_contract_dto 不符 0 處)。前版 1.15(CR-043 §2.3:新增 §7.17 授權請求之特店名稱/輸入模式/EMV 資料之接收——契約 AuthorizeRequest 宣告之 merchantName/entryMode/emvData 三欄,實作 DTO 無此三欄 → 靜默忽略;BEAM PMO 2026-10-15 裁定「請補實作接收」(CR-043 §4 之 (B) 案)。★ merchant_name/entry_mode 早已在 V1__baseline.sql 卻從未被綁定(恆 NULL)——本次接線;emv_data 由 V10__authorization_entry_emv.sql 新增(Oracle 23ai JSON)。★ 回應不擴充(契約未宣告)=接收=落庫可稽核。併記 CR-044 ③(§7.6/§7.15 之權威鏡射:部分沖正維持 HELD)。前版 1.14(CR-035 §2.2:新增 §7.16 額度池選擇——POC-8 之 8-3/8-4/8-5;SpecialLimitPort + StandInSpecialLimitPort(安全降級)+ 三段式池選擇;★ BR-04-01 為結構性成立)。前版 1.13(CR-030 架構裁定:#8 REFERRAL 發射(真缺陷)+ decision 欄位;#7 DECLINED 刻意不發射)。前版 1.12(GOV-006 4.8 W-2 推廣・CR-027 批次 B:新增 §7.15 佔用事件消費(limit.hold.created.v1/.released → 本地 limit_holds 投影)——補 V2__read_model.sql 明文載明卻從未有人餵養之第三個讀模型缺口。★ 以 authorizationId 去重避免 stand-in 後送造成「同一筆佔用兩列 → 重複扣減」;★ 亂序判準置於 consumed_events(不動業務表);★ 併修正 JdbcLimitPort.availableLimit 之 status 過濾缺陷(原僅計 HELD,漏計 PARTIALLY_RELEASED → 低估佔用、高估可用額度)。真機驗證(真實 Kafka + Oracle,含 stand-in 去重之負樣本)。併補 §10 #9~#11。前版 1.11(GOV-006 4.8 W-2 推廣・事件 #3:新增 §7.14 授權核准事件輸出(authorization.authorization.approved.v1)——SDD-003 時序圖與 SDD-006 §4.2 均已載明、卻從未有人發射;生產端(同交易寫 Outbox + relay),消費端待下一片。併補 §10 #6/#7 誠實揭露。前版 1.10(GOV-006 4.8 W-2 推廣・事件 #2:新增 §7.13 額度事件消費(limit.limit.adjusted.v1 → account_limit_caches)——補 V2__read_model.sql 明文載明卻從未有人餵養之第二個讀模型缺口;含冪等(共用 consumed_events)+亂序防護(account_limit_caches.last_sequence_no);只覆寫 total_amount、used_amount 原值保留。⚠️ 併處理命名落差:V2 註解之 limit.limit.changed.v1 vs SAD-004 §5.2(權威)之 limit.limit.adjusted.v1——依 SAD-004 §5.2。真機驗證(真實 Kafka + Oracle)通過。前版 1.9(GOV-006 4.8 W-2 第一切片:新增 §7.12 卡片狀態事件消費(card.card.status-changed.v1 → card_status_caches)——補 V2__read_model.sql 明文載明卻從未有人餵養之讀模型缺口;含冪等(consumed_events,與 upsert 同交易)+亂序防護(last_sequence_no);真機驗證(真實 Kafka + Oracle)通過。前版 1.8(桶 C 解鎖(#A-114):新增 §7.11 交易狀態機檢視(GET /api/v1/authorizations/{authNo}/state)——解鎖 SDD-007 §5.1.2.3 桶 C「交易狀態機與冪等」;狀態機視角(當前 decision + limit_holds 佔用 + 由 created_at/override_at/reversed_at 推導之轉移歷程);冪等(Redis)如實揭露不重現。契約重生 0 錯誤。前版 1.7(CR-026 新增統計端點 GET /api/v1/authorizations/stats(2026-10-01):新增 §7.10 授權統計(供 /dashboard 業務 KPI);pendingUpload=standin_flag=1 AND standin_upload_at IS NULL(依 V4__standin_upload.sql 權威註解);today=當日授權數(decision 不篩選)。契約重生 0 錯誤。前版 1.6(CR-025 稽核軌跡查詢端點(2026-09-29):新增 §7.9 GET /authorizations/audits——解鎖 SDD-007 §5.1.2.3 桶 C「稽核軌跡」(原記「表存在但無查詢端點」)。設計三要點:統一時間軸(合併 auth_overrides/auth_reversals)/action 值域正規化**/actor 對沖正如實回 null(該表無操作者欄位——不臆造)。append-only 唯讀。契約重生 79 端點、0 錯誤)。前版 1.5(CR-023 後送作業台之端點補齊(2026-09-24):§7.7 之「職責」表補明端點對應——原表僅列三項職責而未對應端點,實作僅有單筆之 POST …/{authNo}/advices,致「查詢待補送佇列」與「批次手動上送」無端點可達(SDD-007 §5.4.23 之 /auth/stip 頁無法完整實作)。新增 GET /authorizations/standin?status=PENDING|UPLOADED|ALL 與 POST /authorizations/standin/advices;契約重生 77 端點、驗證 0 錯誤;併明訂清單不得以決策碼篩選(§7.7.1))。前版 1.4(CR-022 真機驗證後補記(2026-09-24):新增 §7.7.1 standin_flag 之判定依據——須依「佔用方式」而非決策碼;併修實作。實測發現 §7.8 新增 REFERRAL 後「decision=STANDIN」與「standIn=true」不再等價 → 轉人工件在額度服務不可用時漏標 standin_flag → 永不後送(超額風險)。§9 補 24 例單元測試+7 項真機實測)。前版 1.3(CR-022 裁定後之實作規格:§7.7 改寫為 (A) 後送授權通知(BEAM PMO 2026-09-24 裁定;含與排程上送之分工、冪等守門、契約修訂定案);新增 §7.8 人工授權/強制授權——RFP FR 1.1(授權/發卡授權)第 4 項明文要求「支援人工授權、強制授權、人工取消授權」,廠商回覆欄已填「滿足」;其中人工取消=沖正(§7.6)已完成,本節補人工/強制授權;含參數化轉人工門檻(CheckCode.MANUAL_REVIEW)、狀態守門、僅限對 REFERRAL 核決(不得對 DECLINED 強制核准,以免繞過風控)。前版 1.2(CR-021 真機驗證後補記 §7.6 雙軌釋放——StandInLimitPort.release 與 hold 對稱(遠端優先、失敗降級本地並 WARN、遠端成功後 best-effort 清本地同列);FULL 沖正已真機驗證(AUTH000035:hold 25000 → RELEASED、AUTH_REVERSALS 落列、reversed_at 標記、重複沖正 CC-AUTH-3002)。⚠️ 併發現既有缺陷:LimitServiceClient(Feign)對 limit-service 之呼叫回 401(未接 M2M outbound token)→ 授權一律降級 Stand-in——此為 CR-010/011 範疇之既有缺口,已登 GOV-006 §4 #21)。前版 1.1(CR-021 補實作之規格先行:§6 補 check-results/reversals 兩端點(契約此前宣告而未實作)+ advices 標記「語意待裁定、本期未實作」;新增 §7.5 檢核明細查詢(查落庫、不重算;併修正契約之陣列性不符)、§7.6 沖正(SDD-005 §3 轉換 5;新增 LimitPort.release;同交易寫 auth_reversals+reversed_at;不修改原決策欄)、§7.7 advices 兩種語意之裁定建議。併載明路徑鍵 {authNo}(業務代碼)之一致性。前版 1.0
|
| 狀態 | 已核定(回溯補建,與現有程式一致) |
| 對應需求 | 見 §1 |
| 服務埠號 | 8081 |
| 套件根 | com.beam.cardcore.auth |
| 權威 schema | CC_AUTH |
1. 對應需求
| FR 編號 | 需求名稱 | 來源 |
|---|---|---|
AUT-1.1 |
授權處理(含即時性) | 建議書授權章節 |
AUT-1.2 |
授權分流(Stand-in/代授權) | 建議書授權章節 |
AUT-1.3 |
國際組織 compliance(檢核) | 建議書授權章節 |
SYS-2.1 |
讀寫分離(CQRS) | 橫切能力 |
2. 服務職責與邊界
承作:
- 受理授權請求、執行檢核、產生決策
- 額度佔用之協調(實際佔用由
limit-service執行) - 卡片狀態檢核之協調(權威狀態由
card-service提供) - 降級(Stand-in)時以本地讀模型代判並標記
standin_flag
不做(屬他服務):
- 額度主檔維護 →
limit-service - 卡片主檔與生命週期 →
card-service - 入帳/請款 →
posting-service(未開發)
3. 領域模型
3.1 聚合與值物件
| 型別 | 職責 |
|---|---|
Authorization |
授權聚合(不可變)。含決策、檢核結果、佔用金額、standIn 標記 |
CheckCode |
13 大項檢核列舉;isImplemented() 標示是否有資料來源 |
CheckResult |
單項檢核結果(passed/respCode/message) |
HoldResult |
佔用結果(holdId + standIn 標記) |
CardSnapshot |
卡片快照(可否交易以 blockCode 判定,見 §7.3 |
3.2 授權決策狀態機
| 來源 | 事件 | 目標 |
|---|---|---|
| — | 核准 | APPROVED |
| — | 拒絕 | DECLINED |
| — | 轉人工 | REFERRAL |
| — | 額度服務不可用而本地代佔 | STANDIN |
依契約
AuthorizeResponse.decision列舉。isApproved()對APPROVED/STANDIN回傳 true。
關鍵設計:降級佔用時決策為 STANDIN 而非 APPROVED——使下游可識別「此筆未經額度服務確認,須事後上送」。
4. 程式結構
| 套件/類別 | 職責 | 對應需求 |
|---|---|---|
domain/model/Authorization |
授權聚合 | AUT-1.1 |
domain/model/CheckCode |
檢核項目列舉 | AUT-1.3 |
domain/model/CardSnapshot |
卡片快照 | AUT-1.1 |
domain/port/CardPort |
卡片查詢埠 | AUT-1.2 |
domain/port/LimitPort |
額度埠 | AUT-1.2 |
domain/port/AuthorizationRepository |
授權持久化埠 | AUT-1.1 |
application/AuthorizeUseCase |
授權編排(檢核→佔用→裁定→持久化) | AUT-1.1 |
application/StandInUploadService |
Stand-in 佔用事後上送 | AUT-1.2 |
infrastructure/jdbc/JdbcAuthorizationRepository |
JDBC 實作 | AUT-1.1 |
infrastructure/jdbc/JdbcCardPort |
本地讀模型(降級用) | AUT-1.2 |
infrastructure/jdbc/JdbcLimitPort |
本地代佔(降級用) | AUT-1.2 |
infrastructure/client/CardServiceClient |
Feign → card-service | AUT-1.2 |
infrastructure/client/LimitServiceClient |
Feign → limit-service | AUT-1.2 |
infrastructure/card/RemoteCardPort |
遠端卡片查詢 | AUT-1.2 |
infrastructure/card/StandInCardPort |
卡片降級裝飾器 | AUT-1.2 |
infrastructure/limit/RemoteLimitPort |
遠端額度 | AUT-1.2 |
infrastructure/limit/StandInLimitPort |
額度降級裝飾器 | AUT-1.2 |
infrastructure/limit/RemoteLimitUploadAdapter |
上送(不降級) | AUT-1.2 |
infrastructure/scheduler/StandInUploadScheduler |
上送排程 | AUT-1.2 |
interfaces/rest/AuthorizationController |
REST 端點 | AUT-1.1 |
5. 資料模型
| 表 | 關鍵欄位 | 實測約束 |
|---|---|---|
authorizations |
auth_no(UNIQUE)、decision、resp_code、hold_amount、standin_flag、hold_id、standin_upload_at、merchant_name、entry_mode、emv_data
|
decision IN ('APPROVED','DECLINED','REFERRAL','STANDIN');standin_flag NOT NULL DEFAULT 0;hold_id/standin_upload_at 由 V3/V4 新增;merchant_name/entry_mode 見 V1__baseline.sql(惟至 CR-043 §2.3 方接線落庫),emv_data 由 V10 新增(JSON 型別)——見 §7.17。 |
| auth_check_results | check_code、check_order、result、reason_code | result IN ('PASS','FAIL','SKIP') ← 實測。曾誤寫 PASSED/FAILED 導致 ORA-02290。
| card_status_caches(本地讀模型) | card_no_hash、block_code、status、expiry_date | 無 enum 約束(自建表);block_code 值須與 CC_CARD.CARDS.block_code 一致
6. API 設計
| 端點 | 請求 | 回應 | 錯誤碼 |
|---|---|---|---|
POST /api/v1/authorizations |
AuthorizeRequest |
AuthorizeResponse |
CC-AUTH-1001~1006、CC-LIM-1001
|
| GET /api/v1/authorizations?limit | — | AuthorizationSummary[] | — |
| GET /api/v1/authorizations/{authNo} | — | AuthorizeResponse(含檢核明細) |
| GET /api/v1/authorizations/{authNo}/check-results | — | AuthCheckResult[](逐項檢核) | CC-AUTH-4040(查無) |
| POST /api/v1/authorizations/{authNo}/reversals | ReversalRequest | ReversalResult(★ CR-043 §2.1:原誤宣告 AuthorizeResponse) | CC-AUTH-3002(已沖正)、CC-AUTH-4040、CC-LIM-1001 |
| POST /api/v1/authorizations/{authNo}/overrides | OverrideRequest | OverrideResult(★ CR-043 §2.2:原誤宣告 AuthorizeResponse) | CC-AUTH-3001(非轉人工件/已核決)、CC-AUTH-3002(已核決)、CC-AUTH-4040(查無) |
| POST /api/v1/authorizations/{authNo}/advices | (待裁定,見 §7.7) | — | — |
| GET /api/v1/authorizations/stats | — | AuthorizationStats(total/today/byDecision/pendingUpload)★ CR-043 §3.1 #2:原 …View 更名 | — |
⚠️ 路徑鍵之說明:契約以
{authId}命名、實作沿用既有明細端點之{authNo}
(AUTH000123業務代碼)——與GET /authorizations/{authNo}一致,避免同服務兩種鍵。
已於gen_openapi.py補description明載「授權編號 authNo(業務代碼)」。
契約登錄:三端點已由CR-021補實作(此前契約宣告而未實作,消費端呼叫為 404)。
7. 關鍵邏輯
7.1 檢核順序
AMOUNT → CURRENCY → CARD_STATUS → CARD_EXPIRY → MCC → LIMIT
任一失敗即 DECLINED,回應碼取第一個失敗項之 respCode。
7.2 交易邊界
授權紀錄與檢核明細同一交易(AuthorizeUseCase 為 @Transactional)。
7.3 可否交易之判定(重要修正)
以 blockCode 判定,不是 status。
-
依
SDD-005 §2.3:僅blockCode == ACTIVE可交易;LOCKED/LOST須即時拒絕。 -
曾發生的洞:原以
status判定,但卡片生命週期轉換改的是block_code→ 卡片已CLOSED時status仍ACTIVE→ 授權照樣核准。已修正並端到端驗證。
7.4 Stand-in 降級
| 依賴 | 降級行為 |
|---|---|
card-service 失敗 → StandInCardPort 讀本地 card_status_caches,WARN 記錄(鎖卡可能未即時生效) |
| limit-service 失敗 → StandInLimitPort 本地代佔,HoldResult.standIn=true → 決策 STANDIN,standin_flag=1 |
上送不降級(RemoteLimitUploadAdapter 失敗即拋,保留 standin_upload_at IS NULL 待重試。
⚠️ 排查教訓:降級會掩蓋整合失敗。曾因欄位改名(
active→transactable)導致 FeignDecodeException→ 每次都降級,系統看似正常但其實一直用快取。
排查請先看降級 WARN 次數。
7.5 檢核明細查詢(GET /authorizations/{authNo}/check-results)
- 資料來源為
CC_AUTH.auth_check_results(授權寫入時同一交易落庫,
見 §7.2)——查詢不重算:重算即「兩套真相」(檢核當下之輸入事後可能已變)。 - 回應欄位對應契約
AuthCheckResult:checkCode=CheckCode.name()、
checkOrder=檢核順序(§7.1)、result=PASS/FAIL(SKIP保留予未實作項目)、
reasonCode=respCode。 - ⚠️ 契約修正(
CR-021):原宣告單一物件、描述卻載「回傳 13 項檢核明細」
——陣列性不符(與CR-016 §8同型),本服務實作前先修契約為AuthCheckResult[]。 - 查無授權 →
CC-AUTH-4040(404)。
7.6 沖正(POST /authorizations/{authNo}/reversals)
-
依據:
SDD-005 §3佔用狀態機轉換 5——HELD --取消/沖正--> RELEASED
(全額釋放)。
★CR-044 ③(BEAM PMO 2026-10-15 裁定「以limit-service為權威、讀模型鏡射」):
部分沖正(PARTIAL)對應部分釋放,而權威端之釋放 SQL 為「僅全額才轉RELEASED」
→ 部分釋放只累計released_amount、不改變status(維持HELD)。
★ 讀模型(CC_AUTH.LIMIT_HOLDS)必須鏡射之,不得改寫PARTIALLY_RELEASED
(該值於本期權威不可達,僅保留於 DDL/契約值域,見SDD-005 §3.0.1)。 -
流程:①查授權(查無 →
CC-AUTH-4040;已沖正 →CC-AUTH-3002(409,終態不得再沖正))
②以authorization.holdId呼叫LimitPort.release(holdId, amount)(FULL=原佔用金額、
PARTIAL=請求之amount,須 ≤ 未釋放餘額)③寫入auth_reversals(稽核留痕)
④標記authorizations.reversed_at(同一交易——「額度已釋放但沖正紀錄寫入失敗」
為難以收拾之半完成狀態)。 - ★ 回應型別(
CR-043 §2.1):回ReversalResult(authNo/type/amountMinor/
holdReleased/reversedAt)——★ 原契約誤宣告AuthorizeResponse(含decision/checkResults,
對沖正語意不合理;疑為三端點同範本之複製貼上);現已改為ReversalResult(實作語意正確)。 - ★ 雙軌釋放(與
hold對稱——「怎麼佔的就怎麼放」):StandInLimitPort.release
先試遠端(佔用可能已上送、或本來就是遠端佔用);遠端失敗(即 Stand-in 情境:
額度服務不可用)→ 改由本地讀模型(CC_AUTH.LIMIT_HOLDS)釋放並 WARN 記錄
(與 hold 降級同規則:容許但必須可觀測)。遠端成功後以 best-effort 釋放本地同列
(避免上送前後之雙軌狀態漂移)。本地釋放語意與 limit-service 一致:
查無/終態/超釋皆拒絕(CC-LIM-1001)。 -
冪等由佔用狀態機保證:
RELEASED為終態、重複釋放由額度服務拒絕(CC-LIM-1001),
加上本服務之reversed_at檢查,雙保險。 - ⚠️ 新增
LimitPort.release(holdId, amount)(原埠僅有hold/availableLimit)——
由RemoteLimitPort轉呼額度服務POST /limit-holds/{holdId}/releases。 -
回應:200
AuthorizeResponse(沖正後之授權視圖,含最新檢核明細)。 -
不修改授權之決策欄(decision/respCode 為當時之事實)——沖正係新增反向紀錄
(SDD-005 §4轉換 4「產生反向交易,不修改原紀錄」之同一原則),僅以
reversed_at標記不可再沖正。
7.7 後送授權通知(POST /authorizations/{authNo}/advices)——(A) 後送,BEAM PMO 2026-09-24 裁定
裁定:採 (A) 後送(Advice)。理由:與原型模組 9 之 L2
「Stand-in 獨立授權(開關門檻/超時代行/待補送佇列/補送結果)」高度吻合、
既有StandInUploadService可重用、且沖正(§7.6)已完成 → 可讓
Stand-in 生命週期閉環(佔用 → 上送 → 沖正 → 後送查詢)。
依據:CR-022三案對照(原summary/description誤導,已一併修契約)。
⚠️ RFP 對照:本項在 RFP 無明文 FR(業務功能需求(授權)未列);
其依據為原型 L2 + 業界實務(stand-in 後送為信用卡核心基本機制)。
語意:將離線/Stand-in 完成之交易事後補送(advice)進核心,以收斂
「本地代佔」與「額度權威」之落差。本服務已具備該機制之執行體
(StandInUploadService + StandInUploadScheduler + standin_flag/standin_upload_at),
本端點為其人工介面。
| 職責 | 端點 | 行為 |
|---|---|---|
| 查詢待補送佇列/補送結果 |
GET /authorizations/standin?status=PENDING|UPLOADED|ALL(預設 PENDING) |
回報 Stand-in 佔用清單(standin_flag=1):PENDING=standin_upload_at IS NULL(待補送)/UPLOADED=已上送/ALL=全部。含 authNo/決策/金額/holdId/交易時間/standinUploadAt(非空=已上送) |
| 手動觸發上送(批次) | POST /authorizations/standin/advices |
立即執行一次「待補送上送」(等同排程之一次執行),回報本次上送筆數與剩餘待送數 |
| 手動觸發上送(單筆) | POST /authorizations/{authNo}/advices |
立即上送該筆(冪等:已上送者略過並回報原上送時間) |
⚠️ 2026-09-24 補記(實作落差之補正,
CR-023):本表原僅列三項「職責」而未對應端點,
實作僅有單筆之POST …/{authNo}/advices→ 「查詢待補送佇列」與「批次手動上送」無端點可達,
致SDD-007 §5.4.23(/auth/stip頁)無法完整實作(僅能做「輸入 authNo → 後送」之盲操作)。
本節補明端點對應;新增之兩端點依「規格先行」先立於此 → 再改tools/gen_openapi.py重生契約 → 後實作。路徑設計說明:
- 清單端點為
GET /authorizations/standin(資源導向;status以查詢參數篩選),
不設/standin/pending之子路徑——因「補送結果」與「待補送佇列」為同一資源之不同狀態,
拆兩個端點會產生兩處來源(必漂移)。PENDING/UPLOADED/ALL皆由同一 SQL 加條件。standin為字面路徑段,與/authorizations/{authNo}之模板段並存時,
Spring 依「literal 優先於 template」匹配;authNo格式固定為AUTH+ 6 位數字,不衝突。- ⚠️ 待補送件之決策不限於
STANDIN——依§7.7.1,REFERRAL(及APPROVED)於本地代佔時
standin_flag亦為 1,故本清單不得以decision='STANDIN'篩選。
與排程之分工(避免兩條路徑打架):
-
排程續行定時上送(
StandInUploadScheduler)——不得因本端點而停用; - 端點為手動補位(客服/維運發現落差時立即觸發);
- 兩者共用同一冪等守門:
markStandInUploaded(authNo, holdId, at)以
standin_upload_at IS NULL為條件,重複上送不會產生第二筆佔用
(額度側以authorizationId冪等)。 - ★ 回寫權威
holdId(BEAM PMO 2026-10-01 裁定;#A-129):上送前hold_id指向
本地代佔之 UUID;上送成功後額度服務回傳權威holdId,於同一交易回寫
authorizations.hold_id。
⚠️ 為何必須:若不回寫,後續release會用本地 UUID 呼叫額度服務 →
查無此佔用 → 降級本地釋放 → 權威端之 hold 永久佔用額度(超額風險)。
⚠️ 配套(缺一不可):本地投影limit_holds之同列id亦須同步為權威holdId
——由limit.hold.created.v1之消費端完成(改 id、不新增列,見§7.15);
否則release(權威 holdId)之「本地 best-effort 釋放」將找不到列。
二者合起來才使authorizations.hold_id= 本地列id= 權威holdId三者一致。 - ⚠️ 端點不取代排程:端點是「現在就送」,排程是「保證終會送」;
若端點失敗(額度服務不可用),保留待上送狀態並回報,由排程續試
(§7.4:上送不降級、失敗即拋,保留standin_upload_at IS NULL待重試)。
7.7.1 standin_flag 之判定依據——依佔用方式,非依決策碼
緣起(2026-09-24 真機實測發現):
§7.8新增REFERRAL決策後,standin_flag
之既有實作(以decision == STANDIN推導)不再等價於「是否為本地代佔」,
導致轉人工件在額度服務不可用時漏標 → 永不後送。本節明訂判定依據。
authorizations.standin_flag 之語意為「該筆佔用未經額度服務確認」,
故其判定必須依佔用方式(HoldResult.standIn,即 LimitPort.hold 之回傳旗標),
不得由決策碼推導:
| 佔用方式 | 決策 | standin_flag |
是否須後送 |
|---|---|---|---|
| 遠端佔用(limit-service 成功) | APPROVED |
0 | 否 |
| 遠端佔用 | REFERRAL |
0 | 否 |
| 本地代佔(降級) | STANDIN |
1 | 是 |
| 本地代佔(降級) | REFERRAL |
1 |
是 ← §7.8 新增後之新情境 |
為何不得由決策碼推導:
-
§7.8之前,standIn=true⟺decision=STANDIN(見Authorization.decide:
!approved ? (REFERRAL|DECLINED) : (standIn ? STANDIN : APPROVED)),
故以決策碼推導恰為等價、無誤。 -
新增
REFERRAL後此等價關係破裂:轉人工件走!approved分支,
該分支不參考standIn;但holdApplied = approved || referralOnly為 true
→ 確實佔額(且額度服務不可用時即為本地代佔)。 - 若仍以決策碼推導 → 該筆
standin_flag=0→findPendingStandIn
(standin_flag=1 AND standin_upload_at IS NULL)永遠查不到它 →
額度權威側與本地永久落差 → 超額風險(StandInUploadService類註解所述之同一風險)。
實作要求:Authorization 聚合須保留 standIn 旗標(decide(...) 已接收該參數),
持久化時以之寫入 standin_flag;standIn 僅於實際佔用(holdApplied)時為 true,
拒絕件(DECLINED)恆為 false。
契約修訂(本節一併定案,見 CR-022 §5):
-
summary:「後送授權通知」(原「人工授權與強制授權」係誤植——人工/強制授權另立 §7.8) -
description:改為後送語意,移除「須具 OVERRIDE 權限」(後送不需 OVERRIDE) - 路徑參數:
{authId}→{authNo}(業務代碼,與同服務另兩端點一致) - 回應碼:201(產生補送紀錄,語意正確,不變)
-
requestBody:由完整AuthorizeRequest改為AdviceRequest{reason?}(全部欄位選填)——
後送之交易資料已存於本地(authorizations),端點無須重送完整授權請求;
reason為選填之稽核參考(此即 (A) 案與原契約AuthorizeRequest之關鍵差異:
原設計隱含「重送一次交易」,與後送語意不符)
7.8 人工授權/強制授權(POST /authorizations/{authNo}/overrides)——2026-09-24 新增
依據(RFP 明文):
業務功能需求(授權)S/N 1.1(授權/發卡授權)第 4 項:
「支援人工授權、強制授權、人工取消授權」。
⚠️ 廠商回覆欄([a] Fully comply)已填「滿足」(原文:「滿足,CAKU的授權檢查規則可以按照需要配置」)
→ 本項為已承諾之需求,未實作即為未履約。
三項之現況:人工取消授權 = 沖正(§7.6)✅ 已完成;人工授權/強制授權 = 本節(新增)。需求追溯:
RFP FR 1.1(授權/發卡授權)第 4 項 → 本節 → 端點
POST /authorizations/{authNo}/overrides。
語意:交易經檢核後轉人工(REFERRAL),由具權限之授權中心人員核決
(核准=人工授權;強制核准=force=true,略過軟性拒絕)——留審核軌跡。
7.8.1 REFERRAL 之產生(轉人工門檻,參數化)
原型模組 9 之 L2 明列「開關門檻」→ 採參數驅動,不依賴未實作之風控檢核
(CheckCode.RISK/DAILY_LIMIT/GEO/VELOCITY/BLACKLIST目前
isImplemented()=false,不得假造檢核結果,CSP-001 §8 B2)。
| 項目 | 內容 |
|---|---|
| 檢核項 | 新增 CheckCode.MANUAL_REVIEW(具資料來源:參數表,故 implemented=true) |
| 門檻來源 | 參數(namespace 如 AUTH.MANUAL_REVIEW):單筆金額門檻/MCC 清單/卡種/時段
|
| 命中行為 | 該檢核項 result=FAIL、reasonCode=01(轉人工),決策 REFERRAL
|
| 佔額 |
仍佔額(hold)——交易已發生,先保留額度;核決 DECLINE 時釋放(§7.8.2) |
| 參數未設 | 不轉人工(預設關閉=fail-safe:不因缺參數而把正常交易卡住) |
⚠️ REFERRAL 為本節新增之產生點:查證確認 AuthDecision.REFERRAL 於 enum 中存在,
但全服務原本零產生處(CR-022 §4);本節為其唯一產生來源。
7.8.2 核決(POST /authorizations/{authNo}/overrides)
| 欄位 | 規則 |
|---|---|
decision |
APPROVE(人工核准)/DECLINE(人工婉拒)——2 種列舉 |
force |
選填;true =強制授權(略過軟性拒絕,如超額容差內),須另具權限
|
operatorId |
必填——稽核留痕(與 lifecycle-transitions 之 operatorId 選填不同,核決必須可歸責) |
reason |
必填(≤200) |
| headers |
X-Channel/X-Idempotency-Key 必帶 |
狀態守門(SDD-005 §3.1):
| 來源決策 | 允許事件 | 結果 |
|---|---|---|
REFERRAL |
APPROVE |
APPROVED(保留原佔額) |
REFERRAL |
DECLINE |
DECLINED(釋放原佔額,呼叫 LimitPort.release,§7.6 之同一路徑) |
APPROVED/DECLINED/STANDIN
|
任何 |
CC-AUTH-3001(非轉人工件,不得核決) |
已核決(override_at 非空) |
任何 |
CC-AUTH-3002(終態不可再核決) |
稽核:寫入 auth_overrides(決策、force、操作者、理由、時間)——不修改原檢核明細
(auth_check_results 為當時事實;核決為新增紀錄,與沖正之「不修改原紀錄」同一原則)。
★ 回應型別(
CR-043 §2.2):回OverrideResult(authNo/decision/force/
holdReleased/overriddenAt)——★ 原契約誤宣告AuthorizeResponse;現已改為OverrideResult。
⚠️ 與 (B) 案之區別:本節僅允許對 REFERRAL 核決——不得對已 DECLINED 之件
強制核准(那會繞過風控檢核)。force=true 之範圍限於「軟性拒絕」
(如超額容差、單筆限額邊界),不包含硬性拒絕(卡片 LOCKED/CLOSED、黑名單)。
7.9 稽核軌跡查詢(GET /authorizations/audits)——2026-09-29 新增
緣由:SDD-007 §5.1.2.3 之桶 C「稽核軌跡查詢」(/system/audit)原記
「表存在但無查詢端點」——本節即為該端點之規格。
資料來源(既有兩張稽核表,皆為 append-only):
| 表 | 產生者 | 內容 |
|---|---|---|
auth_overrides |
人工/強制核決(§7.8,#A-98) |
決策、force、操作者、理由、時間 |
auth_reversals |
沖正(§7.6,#A-95/96) |
沖正類型、金額、理由、時間 |
端點:GET /authorizations/audits?limit=N&eventType=ALL|OVERRIDE|REVERSAL
回應:AuthAuditEventList——統一時間軸(兩表 UNION ALL,依 occurredAt 遞減)。
| 欄位 | 說明 |
|---|---|
auditId |
稽核紀錄識別碼 |
eventType |
OVERRIDE(人工/強制核決)/REVERSAL(沖正) |
authNo |
授權編號 |
actor |
操作者(核決者 operatorId);沖正無操作者欄位 → null(如實呈現,不臆造) |
action |
APPROVE/DECLINE/FULL_REVERSAL/PARTIAL_REVERSAL
|
amountMinor |
沖正金額(分);核決事件為 null
|
reason |
理由 |
occurredAt |
發生時間 |
設計要點:
- ★ 統一時間軸:兩類稽核事件合併排序——使用者關心的是「這筆授權發生過什麼」,
而非「它屬於哪張表」;分開兩頁會迫使使用者自行拼湊時間序。 - ⚠️
actor之誠實揭露:auth_reversals無操作者欄位(該表未記錄)→ 回null,不臆造;
畫面顯示「未記錄」。若要補齊,須於沖正路徑加operator_id(列為後續,另立 CR)。 - ⚠️ append-only 唯讀:本端點僅查詢,不提供修改/刪除——
稽核紀錄之可竄改性將使其失去證據力。 -
limit預設 20、上限 200(避免一次拉全表)。 -
服務範疇(誠實揭露):本端點涵蓋授權服務之稽核軌跡。
其餘服務之稽核(如party管制碼異動、card生命週期)尚未有查詢端點,
待各自就緒後擴充(C-46)。
7.10 統計端點(GET /api/v1/authorizations/stats)——2026-10-01 新增(CR-026)
緣由:
CR-026(營運總覽之業務 KPI 統計端點)。/dashboard之「業務 KPI」區塊需各服務提供統計端點——經查 17 服務皆無,故本節為規格先行之補建。
端點:GET /api/v1/authorizations/stats → AuthorizationStats(★ CR-043 §3.1 #2:原 AuthorizationStatsView,與實作 domain/model/AuthorizationStats 統一)
| 欄位 | 來源 | 說明 |
|---|---|---|
total |
COUNT(*) |
授權總數(由後端計算;前端不得自行相加分組) |
today |
COUNT(*) WHERE 授權日 = 業務日 |
當日授權數(decision 不篩選——含核准/拒絕/轉人工/stand-in) |
byDecision |
GROUP BY decision |
APPROVED/DECLINED/REFERRAL/STANDIN(DDL CHECK 之 4 值,不補 0) |
pendingUpload |
COUNT(*) WHERE standin_flag=1 AND standin_upload_at IS NULL |
待後送件數(依 V4__standin_upload.sql 權威註解;stand-in 佔用尚未上送對帳者) |
實作要求(與 SCS-003 §7.9、SCS-012 §7.x 一致):
- 以
GROUP BY聚合(單次查詢),不得 N+1。 -
total/today由後端計算,不得由前端相加分組。 -
byDecision僅含實際存在之值(不補 0)。 - 不快取、無查詢參數(本階段為總量統計)。
7.11 交易狀態機檢視(GET /api/v1/authorizations/{authNo}/state)——2026-10-01 新增(桶 C 解鎖)
緣由:
SDD-007 §5.1.2.3之桶 C「交易狀態機與冪等」(/switch/auth/state)原記「均無端點」。本節為該端點之規格先行補建(比照#A-101/#A-102/#A-113模式)。
端點:GET /api/v1/authorizations/{authNo}/state → AuthorizationState(★ CR-043 §3.1 #3:原 AuthorizationStateView,與實作統一)
語意:對單一授權交易呈現其狀態機視角——當前狀態、佔用狀態、狀態轉移歷程。
| 欄位 | 來源 | 說明 |
|---|---|---|
authNo |
authorizations.auth_no |
授權編號 |
currentState |
authorizations.decision |
當前狀態(APPROVED/DECLINED/REFERRAL/STANDIN;DDL CHECK 4 值) |
respCode |
authorizations.resp_code |
回應碼 |
standinFlag |
authorizations.standin_flag |
是否本地代佔(該筆佔用未經額度服務確認,§7.7.1) |
standinUploadAt |
authorizations.standin_upload_at |
後送時間;null=待後送
|
hold |
limit_holds(以 authorizations.hold_id 關聯) |
佔用狀態;無佔用者 null
|
transitions |
authorizations 之 created_at/override_at/reversed_at
|
狀態轉移歷程(時間遞增) |
hold 子物件:holdId/holdAmountMinor/releasedAmountMinor/status/expiresAt(來源 limit_holds)。
transitions[] 元素:occurredAt/event(AUTHORIZED/OVERRIDDEN/REVERSED)/toState/detail。
設計要點:
- ★ 狀態機視角 vs 事件時間軸:本頁聚焦「這一筆授權現在的狀態與其轉移歷程」;
§7.9稽核軌跡為「所有授權之核決/沖正事件統一時間軸」——兩者互補、不重複。 - ⚠️ 不以
decision推導「是否曾核決/沖正」:改用override_at/reversed_at(權威時間欄)判定,避免與§7.7.1同型之推導缺陷。 - ⚠️ 冪等之誠實揭露:
X-Idempotency-Key之冪等由 API 層(Redis)保證,未落 Oracle → 本頁不重現冪等明細,不臆造欄位(機制見SCS-022/CR-010)。 -
不快取;查無
authNo→404。 -
服務範疇:涵蓋
authorization-service之授權交易。
7.12 卡片狀態事件消費(card.card.status-changed.v1)——2026-10-01 新增(GOV-006 4.8 W-2 第一切片)
緣由:
V2__read_model.sql明文載「card.card.status-changed.v1→card_status_caches」,
惟從未有人餵養該讀模型——JdbcCardPort讀之,卻無任何寫入者(讀模型恆空/過期)。
本節為該消費端之規格(契約見SAD-004 §5.4)。
訂閱:主題 cc.card.card.status-changed(命名規則見 SAD-004 §5.4);群組 authorization-service。
事件 payload(v1):
| 欄位 | 來源(card-service cards) |
|---|---|
cardNoHash |
card_no_hash(比對鍵,讀模型之唯一鍵) |
cardId |
id |
accountId |
account_id |
cardNoMasked |
card_no_masked |
status |
status |
blockCode |
block_code |
expiryDate |
expiry_date |
處理:
-
冪等:
eventId寫入consumed_events(與 upsert 同交易);已存在 → 略過。 -
亂序防護:
sequenceNo落後於該cardNoHash已套用者 → 略過並 WARN。 -
Upsert:以
card_no_hash(TRIM比對,同JdbcCardPort)為鍵 upsertcard_status_caches。 -
未知
eventType:略過並 WARN(不拋錯,SAD-004 §5.4)。
設計要點:
- ★ 讀模型為「本地投影」、非權威——權威仍在
CC_CARD.CARDS;本消費使授權之降級路徑(遠端不可用時讀本地)不致讀到過期狀態。 - ⚠️ 不覆寫較新狀態(亂序略過)——避免「舊事件蓋掉新狀態」之靜默錯誤。
- 不快取。
真機驗證(2026-10-01;真實 Kafka + Oracle):
| 驗證 | 結果 |
|---|---|
| 訂閱 | ✅ 啟動日誌 partitions assigned: [cc.card.card.status-changed-0]
|
| 遷移 | ✅ Flyway Successfully applied 1 migration to schema "CC_AUTH", now at version v7
|
| 首播(缺口證明) | ✅ BEFORE card_status_caches count=0(讀模型本為空,證實「無人餵養」) |
| 套用 | ✅ 發布事件 → AFTER ****9999|SUSPENDED|LOCKED|seq=1(已 upsert);consumed_events 落列 |
| 冪等 | ✅ 重送同 eventId → version 仍 0、consumed_events 仍 1 列(未重複套用) |
| 亂序防護 | ✅ 送 sequenceNo=0(較舊、狀態 ACTIVE)→ 讀模型仍 SUSPENDED|LOCKED|seq=1(未被子舊事件覆寫) |
| 單元測試 | ✅ CardStatusProjectionServiceTest 3 例(含未知型別略過不拋錯) |
✅ 生產端已實作(2026-10-01,
#A-118):card-service於lifecycle-transitions時,
於同一交易寫outbox,由 relay(OutboxRelayScheduler+KafkaEventPublisher)發布
(見SCS-003 §7.10)。
端到端真機驗證:LOCK→ 本讀模型SUSPENDED|LOCKED|seq=1;UNLOCK→ACTIVE|ACTIVE|seq=2|v=2。⚠️ 未竟(誠實揭露):死信佇列(DLQ)、消費延遲指標、事件重放工具尚未實作。
7.13 額度事件消費(limit.limit.adjusted.v1)——2026-10-01 新增(GOV-006 4.8 W-2 推廣・事件 #2)
緣由:
V2__read_model.sql明文載「limit.limit.changed.v1→account_limit_caches」,
惟從未有人餵養(同§7.12之卡片狀態讀模型——讀模型恆空/過期)。
本節為該消費端之規格(契約見SAD-004 §5.4;生產端見SCS-002 §7.5)。⚠️ 命名落差之處理:
V2註解寫limit.limit.changed.v1,惟SAD-004 §5.2(事件清單,權威)載為
limit.limit.adjusted.v1。本規格依SAD-004 §5.2(事件清單為契約層、V2註解為說明性文字)。
訂閱:主題 cc.limit.limit.adjusted(命名規則見 SAD-004 §5.4);群組 authorization-service。
事件 payload(v1):
| 欄位 | 來源(limit-service account_limits) |
對應讀模型欄位 |
|---|---|---|
accountId |
account_id |
account_limit_caches.account_id(比對鍵之一) |
currency |
currency |
account_limit_caches.currency(比對鍵之一) |
totalAmountMinor |
套用後之總額度(永久 + 有效臨時 + 彈性) | account_limit_caches.total_amount |
處理:
-
冪等:
eventId寫入consumed_events(與 upsert 同交易);已存在 → 略過。 -
亂序防護:
sequenceNo落後於該(account_id,currency)已套用者 → 略過並 WARN。 -
Upsert:以(
account_id,currency)為業務鍵 upsertaccount_limit_caches;currency為CHAR(3)→ 比對一律TRIM。 -
未知
eventType:略過並 WARN(不拋錯,SAD-004 §5.4)。
設計要點:
- ★ 只更新
total_amount、不動used_amount:used_amount之權威在帳務(SCS-002 §10 #3),本消費不得臆測。 - ⚠️ 既有列之保護:
account_limit_caches可能由本地代佔路徑(JdbcLimitPort)讀取;本 upsert 僅覆寫total_amount與序號,used_amount原值保留。 - ⚠️ 不覆寫較新狀態(亂序略過)。
- 不快取。
真機驗證(2026-10-01;真實 Kafka + Oracle):
| 驗證 | 結果 |
|---|---|
| 訂閱 | ✅ 啟動日誌 Subscribed to topic(s): cc.limit.limit.adjusted;partitions assigned: [cc.limit.limit.adjusted-0]
|
| 遷移 | ✅ Flyway Successfully validated 9 migrations(含 V8) |
| 缺口證明 | ✅ BEFORE:account_limit_caches 既有 3 列之 last_sequence_no 皆為 0(非由消費者寫入 → 證實該讀模型本無餵養者) |
| 生產端(端到端) | ✅ limit-service POST /limit-adjustments(10,000 → 410,000 分、EFFECTIVE)→ outbox 落列(limit.adjusted)→ relay 標記 published_at → 本讀模型 total_amount 10,000 → 410,000、version 0 → 1
|
| 冪等 | ✅ 重送同 eventId → version 仍 2、consumed_events 總數不變;日誌 額度事件未套用(重複或亂序,已略過):eventId=E2E-IDEM-1
|
| 亂序防護 | ✅ 送 sequenceNo=1000(落後,totalAmountMinor=999)→ 讀模型仍 500,000(未被舊事件覆寫);日誌同上(eventId=E2E-OOO-1 seq=1000) |
| 未知型別不毒化 | ✅ 期間一則無法辨識之訊息 → WARN 略過、消費者未中斷(後續訊息照常處理) |
| 單元測試 | ✅ AccountLimitProjectionServiceTest 4 例(含未知型別略過不拋錯、跨事件型別守門) |
✅ 生產端已實作(2026-10-01,
#A-119):limit-service於額度調整生效時,
於同一交易寫outbox,由 relay 發布(見SCS-002 §7.5)。
⚠️ 未竟(誠實揭露):撤銷/到期路徑未發射事件(SCS-002 §10 #5);DLQ/消費延遲指標/事件重放工具尚未實作。
7.14 授權核准事件輸出(authorization.authorization.approved.v1)——2026-10-01 新增(GOV-006 4.8 W-2 推廣・事件 #3)
緣由:
SDD-003之授權時序圖(「Authz->>Outbox: 寫入 authorization.authorization.approved.v1」)
與SDD-006 §4.2(payload 欄位)均已載明本事件,惟從未有人發射——
同§7.12/§7.13之「文件已載、程式全無」型缺口。
本節為該生產端之規格(消費端為posting/limit/notification,SAD-004 §5.2——待下一片)。
主題:cc.authorization.authorization.approved(命名規則 SAD-004 §5.4)。
| 項目 | 值 |
|---|---|
eventType |
authorization.authorization.approved.v1 |
Outbox aggregate_type
|
authorization |
Outbox event_type
|
authorization.approved(不含版本) |
| Kafka 分區鍵 |
authorizationId(同一授權之事件落同一分區 → 聚合內有序) |
事件 payload(v1;欄位名依 SDD-006 §4.2):
| 欄位 | 來源 | 說明 |
|---|---|---|
authorizationId |
authorizations.id |
授權識別碼(亦為信封 aggregateId) |
authNo |
authorizations.auth_no |
授權編號(對外可見) |
cardId |
authorizations.card_id |
卡片識別碼;查無卡片時為 null
|
accountId |
authorizations.account_id |
歸戶識別碼;查無卡片時為 null
|
amount |
authorizations.amount |
最小單位(分)(SDD-006 §2.5) |
currency |
authorizations.currency |
ISO 4217 |
holdId |
authorizations.hold_id |
額度佔用識別碼。⚠️ 核准必然佔用額度(Authorization.decide:holdApplied)→ 本事件之 holdId 恆非 null;仍保留 null 之序列化處理作為防禦 |
holdAmount |
authorizations.hold_amount |
最小單位(分)。⚠️ 聚合之 holdAmount 恆非 null——未佔用時為 Money.zero(0)→ 不以 null 表示「未佔用」(「佔用 0 元」與「未佔用」在本事件中不會並存,因僅核准路徑發射) |
standinFlag |
authorizations.standin_flag |
是否本地代佔(§7.7.1:依佔用方式,非依決策碼) |
cardNoMasked |
authorizations.card_no_masked |
2026-10-01 新增之可選欄位(#A-125)——供通知服務之 AUTH_APPROVED 樣板取 cardLast4。已遮罩,非明文;先例見 card.card.status-changed.v1
|
merchantId |
authorizations.merchant_id |
2026-10-01 新增之可選欄位(#A-125)——同上,供樣板之 merchant。非敏感(商戶代號) |
⚠️ 末兩欄為契約擴充:依
SAD-004 §5.3「新增可選欄位不升版」→ 不升v2;
惟仍屬契約擴充,已列CR-027 §5 #11待裁定。
緣由:AUTH_APPROVED樣板(既有種子資料)需要cardLast4/merchant,
而原 payload 未帶——見SCS-016 §7.9。
處理:
-
同交易寫 Outbox:
AuthorizeUseCase.execute於repository.save(authorization)之後、同一交易(類別層@Transactional)寫入outbox;否則會出現「授權已核准、事件未發」之落差。 - ★ 發射條件=「是否佔用額度」(
Authorization.holdsLimit())——即decision ∈ {APPROVED, STANDIN, REFERRAL}。
本事件之語意重新界定為「授權已佔用額度」(CR-030 §2 #8裁定):-
APPROVED/STANDIN——正常核准/代授權; -
REFERRAL(轉人工)——亦佔用額度(見上方willApprove之判定與limitPort.hold之呼叫),
故必須發射,否則下游無從得知該筆佔用。
⚠️ 原以AuthDecision.isApproved()(不含REFERRAL)為條件——造成「額度被佔用而下游完全不知」之真缺陷,已更正。
⚠️DECLINED不佔用額度 → 不發射任何事件:拒絕率/原因分佈改以 metrics 涵蓋(CR-030 §2 #7)。
-
-
payload 補
decision(可選)(CR-030 §2 #8)——下游據此分辨「核准」與「轉人工待覆核」。
⚠️notification-service對decision = REFERRAL不建立通知(無對應樣板,不臆造文案,見SCS-016 §7.9)。 -
relay 發布:
OutboxRelayScheduler週期性呼叫common-event之OutboxPublisher(SDD-004 §4.3)。
設計要點:
- ★
standinFlag依「佔用方式」而非決策碼(§7.7.1之既有鐵律)——直接取聚合之standIn(),不由decision反推。 - ⚠️
DECLINED不在本節:拒絕事件為authorization.authorization.declined.v1(SDD-006 §4.2)——尚未發射(見 §10 #6)。 - ⚠️ 序列化為
StringSerializer(Outboxpayload已為信封 JSON 字串;JsonSerializer會多包引號,消費端解析失敗——SAD-004 §5.4)。
真機驗證(2026-10-01;真實 Oracle + Kafka):
| 驗證 | 結果 |
|---|---|
| 遷移 | ✅ CC_AUTH.outbox 建立成功(V9;查詢得 0 列=BEFORE 無餵養者) |
| 生產(核准件) | ✅ POST /api/v1/authorizations(15,000 分、APPROVED)→ outbox 落 1 列(authorization/authorization.approved)→ relay 標記已發布(published=YES、retry_count=0) |
| Kafka | ✅ 主題 cc.authorization.authorization.approved 自動建立;消費得信封:eventType/aggregateId(=authorizationId)/sequenceNo/traceId/schemaRef 皆正確
|
| payload 契約 | ✅ authorizationId/authNo/cardId/accountId/amount=15000/currency=TWD/holdId/holdAmount=15000/standinFlag=false——與 SDD-006 §4.2 逐欄一致(金額為最小單位) |
★ REFERRAL 不發射(負樣本) |
✅ 送 25,000 分(≥ 門檻 20,000)→ decision=REFERRAL、已佔用(holdId 非空)→ outbox 仍為 0 列——證實 isApproved() 不含 REFERRAL(此即 §10 #8 之落差) |
| 單元測試 | ✅ AuthEventFactoryTest 4 例(信封固定欄位/payload 契約欄位名/standinFlag 依佔用方式/序號嚴格遞增) |
7.15 佔用事件消費(limit.hold.created.v1/limit.hold.released.v1)——2026-10-01 新增(GOV-006 4.8 W-2 推廣・CR-027 批次 B)
緣由:
V2__read_model.sql明文載「limit_holds為額度佔用之本地投影(權威在CC_LIM.LIMIT_HOLDS)」,
惟從未有人餵養——JdbcLimitPort.hold()僅於 stand-in 降級路徑寫入;正常路徑(遠端佔用成功)
完全不寫 → 本地投影缺列(同card_status_caches/account_limit_caches之型,此為第三個)。
訂閱關係與契約之化解見CR-028(SAD-002 §3.1已補為訂閱limit.hold.*二者)。
訂閱:主題 cc.limit.limit.hold.created + cc.limit.limit.hold.released;群組 authorization-service。
處理:
-
冪等:
eventId寫入consumed_events(與投影寫入同交易);已存在 → 略過。 -
亂序防護:
sequenceNo落後於該aggregateId已消費之最大值 → 略過並 WARN。
⚠️ 判準取自consumed_events(MAX(sequence_no) WHERE aggregate_id = ?),不在limit_holds增設技術欄位
——理由見下表。 -
created→ upsert 本地投影(★ 以authorizationId為業務鍵去重)。
★instrumentId落地(CR-054P2,2026-10-17):事件帶之載體寫入
limit_holds.instrument_id(V14新增,可空——V10前之歷史佔用無載體)。
⚠️ 更新既有列時用instrument_id = NVL(?, instrument_id):事件未帶載體時
不得清空既有值——否則事件重放(或舊事件)會把已補的載體抹掉。 -
released→ 累加released_amount:以holdId定位本地列;
released_amount = LEAST(hold_amount, released_amount + ?);
★status→RELEASED(全額)/全額前維持HELD(CR-044 ③)。⚠️ 原實作於部分釋放寫
PARTIALLY_RELEASED——與權威(limit-service)不一致,
致「同一佔用兩種陳述」(權威HELD、讀模型PARTIALLY_RELEASED)。
BEAM PMO 2026-10-15 裁定「以 limit 為權威、讀模型鏡射」→ ★ 改為鏡射權威語意
(status = CASE WHEN released_amount + ? >= hold_amount THEN 'RELEASED' ELSE status END)。 -
未知
eventType:略過並 WARN(不拋錯,SAD-004 §5.4)。
★★ 設計要點(每一項皆為避免「重複計算額度」之具體防護)
| 決策 | 理由 |
|---|---|
★★ created 以 authorizationId 去重;且已有同授權之列時「改其 id 為權威 holdId」(而非略過)
|
⚠️ 關鍵風險:StandInUploadService 上送時會呼叫 limit-service 建立權威 hold(→ 發 created 事件),而本地早已有一列(stand-in 當下寫的、id 為本地 UUID)。若消費端以 holdId 盲目插入 → 同一筆佔用兩列 → JdbcLimitPort.availableLimit 之 SUM(hold_amount − released_amount) 重複扣減 → 可用額度被低估。故:不新增列。⚠️ 但亦不可「純略過」——BEAM PMO 2026-10-01 裁定「上送成功後回寫 authorizations.hold_id 為權威 holdId」(§7.7),若本地列 id 不同步,release(權威 holdId) 之本地 best-effort 釋放將找不到列 → 本地投影永久 HELD。→ 故改其 id(+ hold_amount)為權威值,使三者一致 |
| 正常路徑(遠端佔用)則會寫入 | 遠端佔用成功時本地無列 → 插入(id =權威 holdId)→ 本地投影終於有餵養者
|
亂序判準置於 consumed_events |
limit_holds 為業務表(stand-in 路徑亦寫入),不應為事件消費增設技術欄位——與 §7.12/§7.13(該二處之目標表為純讀模型)情況不同;同一理由已見於 SCS-002 §7.7
|
released 找不到列 → 略過並記 INFO |
stand-in 情境之本地列 id 為本地 UUID,權威 released 事件之 holdId 對不上 → 略過(該情境之釋放已由 StandInLimitPort.release 之降級路徑處理,見該類別註解) |
| 不快取 | — |
| 幣別之決定 |
limit_holds.currency 為 NOT NULL,惟事件之 currency 為可選欄位(2026-10-01 補)→ 實作:事件帶者優先;未帶時回查 account_limit_caches(須恰一列);仍無法決定 → WARN 並略過寫入 |
holdType 不入本地投影 |
CC_AUTH.LIMIT_HOLDS 無 hold_type 欄位——刻意不為它增欄(本投影之用途為可用額度計算,不含佔用類型) |
⚠️ 未竟(誠實揭露):(i) stand-in 後送成功後,權威端之 hold 不會被後續
release釋放
(authorizations.hold_id仍指向本地 UUID)——既有落差,非本節引入;
(ii) 本地與權威之holdId於 stand-in 情境不一致(本節以authorizationId去重即為此);
(iii) DLQ/消費延遲指標/事件重放工具未實作。
真機驗證(2026-10-01;真實 Oracle + Kafka):
| 驗證 | 結果 |
|---|---|
| 訂閱 | ✅ partitions assigned: [cc.limit.limit.hold.created-0, cc.limit.limit.hold.released-0]
|
| Test A:正常路徑 | ✅ 送授權(15,000 分、APPROVED)→ 遠端佔用成功 → 事件 → 本地 limit_holds 8 → 9(consumed_events 8 → 9);新列 id =權威 holdId、authorization_id 正確、currency=TWD(由事件帶入)、hold_amount=15000、status=HELD
|
| ★ Test B:stand-in 去重(關鍵風險) | ✅ 停 limit-service → 送授權(20,000 分)→ 降級 stand-in(本地列 id=本地 UUID)→ 重啟 limit-service → StandInUploadScheduler 上送(CC_LIM.limit_holds 25 列、outbox 6 列)→ 事件 → 消費端 本地投影已有同授權之佔用列,略過插入(避免重複計算) → 該 authorizationId 之列數=1(非 2)、本地總數仍 10(未增加)——證實「同一筆佔用兩列 → 重複扣減」之風險已被阻斷
|
★ availableLimit 之 status 過濾修正 |
✅ 造一列 PARTIALLY_RELEASED(hold_amount 15,000/released_amount 5,000)後,修正前 SUM(僅 HELD)=86,000、修正後(HELD+PARTIALLY_RELEASED)=96,000——差額 10,000 即該列之未釋放餘額;證實原 SQL 低估佔用、高估可用額度(超額風險) |
| 單元測試 | ✅ LimitHoldProjectionServiceTest 7 例(含 stand-in 改 id、亂序、冪等、型別守門) |
★ Test C:回寫權威 holdId 後之「三者一致」(BEAM PMO 2026-10-01 裁定;#A-129) |
✅ 停 limit-service → 送授權(12,000 分、STANDIN)→ 本地 holdId=a5280009-…(本地 UUID)→ 重啟 → 排程上送(standin_upload_at=11:57:31)→ 事件 → 消費端改本地列 id。結果三者完全一致:authorizations.hold_id = 本地 limit_holds.id = 2B72AB60…CAA4E7(權威 holdId);該授權之列數=1(不重複)、本地列 version 0 → 1(UPDATE 而非 INSERT)。★ 原本地 UUID 已被取代——證實「回寫 + 改 id」之配套同時生效
|
★ 順手修正之既有缺陷(JdbcLimitPort.availableLimit)
AVAILABLE_SQL 原僅計 status = 'HELD',惟 release() 於部分釋放時會將狀態改為 PARTIALLY_RELEASED
——該列之未釋放餘額仍屬佔用卻被排除 → 低估佔用、高估可用額度 → 超額風險。
已改為 status IN ('HELD', 'PARTIALLY_RELEASED')(終態 RELEASED/EXPIRED 之未釋放餘額為 0,本即無影響)。
7.16 額度池選擇(專用額度;POC-8 之 8-3/8-4/8-5)——2026-10-02 新增(CR-035 §2.2)
緣由:
CR-014 v1.1完成後專用額度可設定但不能動用——本服務只查卡人額度,
專用額度完全不參與 →BR-04-01(不佔卡人額度)與 8-5(超額拒絕)皆不成立。
7.16.1 三段式池選擇
① 查「適用之專用額度」(account, currency, mcc)→ 得 (specialLimitId, 可用專用額度)
② checkLimit:
若 ① 有適用且池內足夠 → PASS(pool = SPECIAL)
否則若 卡人可用額度足夠 → PASS(pool = CARDHOLDER,8-4)
否則 → FAIL(CC-LIM-1001,8-5)
③ hold:pool = SPECIAL → holdType='SPECIAL' + specialLimitId(**不觸碰卡人額度**)
pool = CARDHOLDER → holdType='GENERAL'
★ BR-04-01 為結構性成立:專用池之佔用只寫 limit_holds(special_limit_id 非空)
與 special_limits.used_amount,完全不動 account_limits ——
故下游之 available = total − used − holds 天然不含該筆,非靠事後沖銷。
⚠️ 需求語意之裁定(
CR-035 §2.2):BRD-004之 8-4 明訂「未設定專用額度」才回歸卡人額度;
8-5 則為「繳稅金額 > 額度 → 拒絕」。本實作採三段式(含回退),
理由:設定專用額度不得使客戶之交易能力反而下降
(否則有 5 萬專用額度者會比完全沒設定者更差)。此為對需求之解釋,非隱含假設;
如客戶採「嚴格解釋(已設定即不得回退)」,只需調整AuthorizeUseCase#checkLimit一處。
7.16.2 埠與降級
| 項目 | 內容 |
|---|---|
| 埠 | SpecialLimitPort#findApplicable(accountId, currency, mcc, amountMinor) |
| 實作 |
RemoteSpecialLimitPort(即時呼叫 limit-service 之適用性端點) |
| 裝飾 |
StandInSpecialLimitPort(@Primary)——失敗即回 empty(視為「無適用」) |
★ 降級為安全降級:limit-service 不可用 → 本筆退回卡人額度(8-4),
不會超額放行專用池。代價是 Stand-in 期間專用額度不可用——如實揭露,不假裝有。
★ 不建本地投影:專用額度之定義與用量皆在 CC_LIM,維護投影只為 Stand-in 而存在,
卻引入「兩份定義漂移」之風險(CR-001 §7 之權型)→ 刻意不做。
★ 專用池之佔用於 Stand-in 期間 fail-closed:JdbcLimitPort#hold 收到 specialLimitId 非空時
拋出業務例外(CC-LIM-1003)——若以卡人額度代佔,將同時違反 BR-04-01 並造成雙重佔用。
7.16.3 真機驗證(2026-10-02)
見 SCS-002 §7.10.4(同一輪驗證,11 項)。摘要:8-3 之 availableLimit 不因專用佔用而減少
(310,000 保持)而 8-4 之卡人池確有扣減(310,000 → 295,000);account_limits 全程未變;
8-5 → DECLINED 51。
7.17 授權請求之特店名稱/輸入模式/EMV 資料之接收(CR-043 §2.3)——2026-10-15 新增
★ 緣由:契約 AuthorizeRequest 宣告 merchantName/entryMode/emvData 三欄,
惟實作 DTO 無此三欄——消費端依契約送此三欄會被靜默忽略(CR-043 §2.3、#A-187 ⑥)。
BEAM PMO 2026-10-15 裁定「請補實作接收」(選 CR-043 §4 之 (B) 案,非「修契約移除」)。
| 契約欄位 | 型別/值域 | DDL 欄位 | 可空 |
|---|---|---|---|
merchantName |
string,maxLength 80
|
authorizations.merchant_name VARCHAR2(80 CHAR) |
是 |
entryMode |
enum:CHIP/MAGSTRIPE/CONTACTLESS/EC/MANUAL/QR
|
authorizations.entry_mode VARCHAR2(16 CHAR) |
是 |
emvData |
object(自由物件,如 {"arqc":"A1B2C3D4"}) |
authorizations.emv_data JSON |
是 |
★ DDL 事實(動查):merchant_name/entry_mode 早已存在於 V1__baseline.sql
(← SDD-001 §3)——惟 JdbcAuthorizationRepository 之 INSERT 從未綁定
(merchant_name 未列入、entry_mode 綁死 null)→ 兩欄恆為 NULL,即「欄位在、資料不進」。
emv_data 則為新欄,由 V10__authorization_entry_emv.sql 新增。
★ 落庫語意:
-
merchantName/entryMode以字串直接綁定(entryMode之值域由契約 enum + 介面層
@Pattern把關;DDL 無 CHECK,故不於資料庫層再設值域——維持與既有mcc一致之寬鬆)。 -
emvData以 JacksonObjectMapper序列化為 JSON 字串,經JSON(?)綁定
(Oracle 23aiJSON型別;與JdbcPartyRepository.FLAGS同型)。
⚠️ 讀取(若需)以JSON_SERIALIZE(emv_data RETURNING VARCHAR2(4000))——不可直接
rs.getString取JSON欄(型別不符)。 -
emvData未做遮罩:其為裝置端 EMV 資料(ARQC/ATC 等),不含卡號明碼
(卡號另以cardNo欄位加密傳輸)——故不另遮罩,如實落庫。
★ 回應不擴充:AuthorizeResponse(及 GET /{authNo} 之明細)契約未宣告此三欄,
故本期不加入回應——避免「契約外欄位」造成契約↔實作漂移(CR-019 之教訓)。
接收=落庫可稽核,非「回吐」。
★ 測試:AuthorizationRepositoryIntegrationTest 新增直查 DB 之往返測試
(SELECT merchant_name, entry_mode, JSON_SERIALIZE(emv_data ...) FROM authorizations)——
證明三欄確實落庫(非僅 DTO 有欄位=防假綠)。
7.18 卡片級額度投影查詢(GET /api/v1/limit-projections/accounts/{accountId}/instruments)——2026-10-17 新增(CR-052 C-2(b)/CR-054 P3)
緣由:
CR-052 §4BEAM PMO 裁定 C-2=(b) 事件投影;CR-054盤點後確認投影之唯一可行範圍
為「每卡未釋放佔用」(事件可重建),並經 BEAM PMO 同意 Q1~Q4 後落地 P1(事件補欄)→ P2(投影加欄)→ P3(查詢+告警)。
7.18.1 ★★ 語意邊界(風控底線,不可逾越)
| 用途 | 走哪裡 |
|---|---|
| 授權決策(佔用/可用額度判斷) |
權威即時:limit-service 之 account_limits+special_limits+limit_holds
|
| 人讀查詢(客服/報表/App) | 本端點(投影,可能落後) |
★ 本端點不得供授權使用(
CR-052 §4.1,BEAM PMO 已確認)。額度正確性為風控底線——寧可查詢慢。
7.18.2 ★ 投影只涵蓋「每卡未釋放佔用」(誠實揭露)
| 分量 | 是否在投影 | 原因 |
|---|---|---|
| 每卡未釋放佔用 | ✅ | 由 limit.hold.created/released 事件重建(P1 補 instrumentId) |
| 每卡主額度 | ❌ | 在 limit-service 之 CARD 維度(V11);卡片級調額刻意不發帳戶事件(SCS-002 §7.16.3)→ 本服務無事件來源 |
| 每卡已用額度 | ❌ | 權威為帳戶層 used_amount,無 per-card 來源
|
| 每卡可用額度 | ❌ | =主額度 − 已用 − 佔用;前二者皆不在投影 → 不提供(不以部分分量拼湊) |
★ 若客戶要求「投影之每卡可用額度」,須先將卡片額度事件化(另立事件、凍結 payload)
並解決「每卡已用」來源——屬 P4/另案,本節不臆造。
7.18.3 回應與新鮮度
| 欄位 | 語意 |
|---|---|
asOf |
投影已套用事件之最新時間(MAX(limit_holds.updated_at)) |
lagSeconds |
當下 − asOf;供前端顯示新鮮度與告警 |
holdAttribution |
FULL=所有未釋放佔用皆帶載體;PARTIAL=存在無載體之歷史佔用(V10 不得回填)→ 各卡佔用可能低估
|
instruments[] |
僅含可歸屬載體之佔用;無載體者不臆造分配 |
★ 查無資料回 404(CC-PLT-4040)——不以「0 筆、asOf=now」冒充已同步:
那會讓「尚未投影」看起來像「已同步且無佔用」。
7.18.4 落後告警
指標 cardcore.limit.projection.lag.seconds(ProjectionLagMonitor,每分鐘量測):
- 值=全域投影最新事件時間與當下之差(秒)。
- ★ 無資料時為
-1(不是 0)——回 0 會讓維運誤以為「零落後」。
告警規則須同時涵蓋「lag > 閾值」與「lag == -1且預期應有資料」。 - 量測失敗時維持上次值並 WARN(不設 0,避免假健康)。
7.18.5 驗收(真機)
| # | 驗證 | 結果 |
|---|---|---|
| ① | 每卡彙總=手算(含部分釋放之 hold_amount − released_amount) |
✅ |
| ② | 無載體佔用不分配給任何卡,且 holdAttribution=PARTIAL
|
✅ |
| ③ | 查無資料 → 404(不冒充已同步) |
✅ |
| ④ | 多幣別不混算 | ✅ |
| ⑤ |
lagSeconds 由 asOf 與時鐘計算(可預測) |
✅ |
| ⑥ | 事件帶載體 → 投影落地;重放不帶載體不得清空既有值(NVL 保護) |
✅ |
8. 外部依賴
| 依賴 | 用途 | 失敗影響 |
|---|---|---|
card-service (8082) |
卡片狀態 | 降級讀快取 |
limit-service (8083) |
額度查詢/佔用 | 降級本地代佔 + 事後上送 |
| Oracle CC_AUTH | 授權持久化 | 服務不可用 |
9. 測試案例
| 編號 | 情境 | 結果 |
|---|---|---|
| UT-AUTH-01 | 正常卡、額度充足 |
APPROVED/00 |
| UT-AUTH-02 | 額度不足 |
DECLINED/51(LIMIT) |
| UT-AUTH-03 | 卡片已鎖卡 |
DECLINED/51(CARD_STATUS) |
| UT-AUTH-04 | 查無卡片 | DECLINED |
| UT-AUTH-05 | 卡片逾期 |
DECLINED(CARD_EXPIRY) |
| UT-AUTH-06 | 不支援幣別 |
DECLINED(CURRENCY) |
| UT-AUTH-07 | MCC 格式不符 |
DECLINED(MCC) |
| UT-AUTH-08 | 命中人工複核門檻(金額 ≥ 門檻) |
REFERRAL/01,仍佔額(holdAmount 非零、有 holdId) |
| UT-AUTH-09 | 人工複核門檻未達 |
MANUAL_REVIEW PASS → APPROVED
|
| UT-AUTH-10 | 人工複核政策未啟用(enabled=false) |
不轉人工(fail-safe,SM-D5) |
| UT-AUTH-11 | 門檻邊界(=門檻即命中)/MCC 命中/MCC 為 null/門檻為 0 |
見 ManualReviewPolicyTest(5 例,含 null 不 NPE) |
| UT-AUTH-12 | ★ 本地代佔之 REFERRAL
|
standIn=true,且進入待上送清單(§7.7.1 迴歸) |
| UT-AUTH-13 | ★ 遠端佔用之 REFERRAL
|
standIn=false,不入待上送(§7.7.1 迴歸) |
| UT-AUTH-14 | 核准且本地代佔 |
STANDIN 決策、standIn=true(既有行為不變) |
| UT-AUTH-15 | 拒絕件(DECLINED) |
standIn 恆 false(無佔用即無須後送) |
| UT-AUTH-16 | 核決:REFERRAL+APPROVE
|
APPROVED,保留原佔額(不呼叫 release) |
| UT-AUTH-17 | 核決:REFERRAL+DECLINE
|
DECLINED,釋放原佔額(呼叫 LimitPort.release) |
| UT-AUTH-18 | 核決守門:非轉人工件(含 force=true) |
CC-AUTH-3001(強制授權不得繞過風控) |
| UT-AUTH-19 | 核決守門:已核決(終態) | CC-AUTH-3002 |
| UT-AUTH-20 | 核決守門:查無授權/缺 operatorId/缺 reason/非法 decision
|
CC-AUTH-4040/CC-AUTH-3001
|
| UT-AUTH-21 | 後送:Stand-in 件上送成功 | 標記 standin_upload_at、回報上送時間 |
| UT-AUTH-22 | 後送:重複後送 | 冪等略過,不產生第二次上送 |
| UT-AUTH-23 | 後送:非 Stand-in 件(無後送目標) | 拋出,不得上送 |
| UT-AUTH-24 | 後送:上送失敗 | 不標記(保留待上送),例外外拋供排程續試 |
| UT-AUTH-25 | 批次上送遇失敗 | 中斷本批,成功者已標記、其餘留待下次 |
| E2E-AUTH-01 | 鎖卡後授權即時拒絕 |
DECLINED ✅ |
| E2E-AUTH-02 | 額度服務停止 → Stand-in → 恢復 → 上送 |
STANDIN → standin_upload_at 寫入 ✅ |
| E2E-AUTH-03 | 未帶 token 呼叫 API(SSO 啟用時) |
401(fail-closed)✅ |
| E2E-AUTH-04 | 超門檻授權(25,000 分/門檻 20,000 分) |
REFERRAL/MANUAL_REVIEW FAIL-01;LIMIT_HOLDS 為 HELD(仍佔額)✅ |
| E2E-AUTH-05 | 對 REFERRAL 核決 APPROVE
|
APPROVED;佔用仍 HELD;auth_overrides 留痕(operatorId/reason 完整)✅ |
| E2E-AUTH-06 | 對 REFERRAL 核決 DECLINE
|
DECLINED;佔用 RELEASED(released_amount=原額)✅ |
| E2E-AUTH-07 | 核決守門:非轉人工/已核決/force=true
|
CC-AUTH-3001/CC-AUTH-3002/CC-AUTH-3001 ✅ |
| E2E-AUTH-08 | 後送:非 Stand-in 件 |
CC-AUTH-3001(409)✅ |
| E2E-AUTH-09 | 後送:額度服務不可用(Feign 401,GOV-006 §4 #21) |
500 CC-PLT-5000;保留待上送(standin_upload_at 仍空)✅ |
| E2E-AUTH-10 | ★ 降級之 REFERRAL 之 standin_flag(§7.7.1 修正驗證) |
修正前 0 → 修正後 1,且列入待上送清單 ✅ |
10. 已知限制(誠實揭露)
-
13 大項檢核僅實作 6 項(
AMOUNT/CURRENCY/CARD_STATUS/CARD_EXPIRY/MCC/LIMIT);其餘 7 項(EMV/CVV/風控/日限/地域/頻率/黑名單/爭議)缺資料來源,標isImplemented=false,不產生假的通過結果。 -
檢核順序未依契約完整排序(契約規範 13 大項順序)。
-
GET /authorizations(清單)與明細未登錄契約(明細鍵為authNovs 契約{authId})。 -
上送冪等鍵未做:目前靠「成功才寫
standin_upload_at」;若 limit-service 收到重複authorizationId無保護。 -
監控未接(
CSP-004僅追蹤碼,Prometheus/Grafana 未串)。 -
authorization.authorization.approved.v1僅有生產端(§7.14,#A-121):消費端(posting/limit/notification,SAD-004 §5.2)尚未實作;拒絕事件authorization.authorization.declined.v1(SDD-006 §4.2)亦尚未發射。 -
sequenceNo為同 JVM 內遞增之替代序號(§7.14):多實例部署可能重複;正式應改為聚合層級之持久化序號。 -
⚠️
REFERRAL(轉人工)件不發射任何事件——但其已佔用額度(§7.14,實測確認):AuthDecision.isApproved()之定義為{APPROVED, STANDIN},不含REFERRAL(該值由CR-022後加)。故轉人工件之佔用下游(limit/posting)無從得知——與§7.7.1同型之「靜默落差」風險。待裁定:應 (a) 另立authorization.authorization.referred.v1,或 (b) 讓approved.v1涵蓋REFERRAL(並以standinFlag區分)。本次不擅自擇一。 -
✅
→ 已解除(2026-10-01,limit_holds本地投影無餵養者§7.15,#A-128):現由limit.hold.created.v1/.released餵養;正常路徑(遠端佔用)亦會寫入本地投影。 -
stand-in 情境下「本地→ ✅ 已裁定並實作(BEAM PMO 2026-10-01;holdId與權威holdId不一致」#A-129):
上送成功後回寫authorizations.hold_id為權威holdId(§7.7),並由§7.15之消費端
同步改本地列之id→ 三者一致(authorizations.hold_id= 本地列id= 權威holdId)。
⚠️ 未竟:既有歷史資料(本次裁定前已上送者)之hold_id仍為本地 UUID,需一次性回填
(未做,見 §10 #12)。 -
limit.hold.created.v1payload 之currency為可選欄位(§7.15,#A-128):原 payload 未帶,惟limit_holds.currency為NOT NULL→ 消費端以「事件帶者優先,未帶則回查account_limit_caches(恰一列)」處理。已列CR-027 §5 #14待裁定(與 #10posted.v1缺currency同型)。 -
既有 stand-in 歷史資料之
hold_id未回填(§7.7,#A-129):本次裁定(回寫權威holdId)僅對新上送生效;裁定前已上送之授權,其hold_id仍為本地 UUID → 該批之權威 hold 仍不會被release釋放(既有落差)。待辦:一次性回填腳本(需以authorizationId對照權威端limit_holds)。本次未做(不臆造對照規則)。
CR-030 架構裁定執行後之結案(2026-10-01)
✅ 原 §10 #8(
REFERRAL不發射事件)已結案——CR-030 §2 #8裁定:
approved.v1之語意重新界定為「授權已佔用額度」,發射條件由isApproved()改為holdsLimit()
(APPROVED/STANDIN/REFERRAL),並補decision(可選)欄位。
不另立authorization.authorization.referred.v1——理由:事件名approved之語意界定為
「佔用成立」(拒絕不佔用),REFERRAL屬之;另立事件會使下游多接一條而語意重疊。
⛔ §10 新增 #11(刻意之設計,非缺口):
DECLINED不發射事件(CR-030 §2 #7)——
拒絕不改變下游狀態(不佔額、不產待請款),依「事件=已發生的狀態變更」原則不應灌入事件流;
其統計改以 metrics(cardcore_authorization_declined_total{reason})涵蓋。
SDD-006 §4.2之declined.v1payload 保留,供日後「拒絕率即時看板」需求時啟用(無契約破壞)。
11. 修訂記錄
| 版本 | 說明 |
|---|---|
| 1.16 |
CR-043 §2.1/§2.2(2026-10-15,#A-187 ⑥):reversals/overrides 之回應型別修正——原契約誤宣告 AuthorizeResponse(疑為三端點同範本之複製貼上;沖正不應回 decision/checkResults、核決有自身結果語意)。改為 ReversalResult(authNo/type〔FULL/PARTIAL〕/amountMinor/holdReleased/reversedAt)與 OverrideResult(authNo/decision〔APPROVE/DECLINE〕/force/holdReleased/overriddenAt)——皆依實作欄位(ReversalService.ReversalResult/OverrideService.OverrideResult)。★ 改 gen_openapi.py(新增兩 schema + 兩端點回應 $ref)→ 重生 → verify_contract_dto 不符 0 處。§6 API 表更新;§7.6/§7.8.2 補回應型別記 |
| 1.15 |
CR-043 §2.3(2026-10-15,#A-187 ⑥):新增 §7.17 授權請求之特店名稱/輸入模式/EMV 資料之接收——契約 AuthorizeRequest 宣告 merchantName/entryMode/emvData 三欄,實作 DTO 無此三欄 → 消費端依契約送此三欄被靜默忽略。BEAM PMO 2026-10-15 裁定「請補實作接收」。實作:AuthorizeRequest(+@Size/@Pattern enum)→ AuthorizeCommand(補 merchantName/emvData;entryMode 原已存在惟控制器傳 null)→ Authorization 聚合(+3 欄)→ JdbcAuthorizationRepository(INSERT 綁 merchant_name/entry_mode/emv_data;emvData 以 Jackson 序列化經 JSON(?) 綁定)。★ DDL 事實:merchant_name/entry_mode 早在 V1__baseline.sql 卻從未被綁定(恆 NULL);emv_data 由 V10__authorization_entry_emv.sql 新增(Oracle 23ai JSON,比照 JdbcPartyRepository.FLAGS)。★ 回應不擴充(契約未宣告 AuthorizeResponse 含此三欄)=接收=落庫可稽核,非「回吐」。★ 併記 CR-044 ③(§7.6/§7.15 之權威鏡射——部分沖正維持 HELD,讀模型不得寫 PARTIALLY_RELEASED)。測試:AuthorizationRepositoryIntegrationTest 新增直查 DB 之三欄往返測試(防假綠) |
| 1.14 |
CR-035 §2.2(2026-10-02,#A-154):新增 §7.16 額度池選擇(POC-8 之 8-3/8-4/8-5)——本服務原僅查卡人額度,專用額度完全不參與,致 BR-04-01(不佔卡人額度)與 8-5(超額拒絕)皆不成立。新增:SpecialLimitPort(RemoteSpecialLimitPort + StandInSpecialLimitPort);AuthorizeUseCase 之三段式池選擇(checkLimit 改收「專用額度適用性」);LimitPort#hold 增 specialLimitId(RemoteLimitPort/JdbcLimitPort/StandInLimitPort 同步)。★ BR-04-01 為結構性成立(專用佔用只寫 limit_holds.special_limit_id + special_limits.used_amount,完全不動 account_limits)。★ 降級為安全降級(limit-service 不可用 → 回 empty → 退回卡人額度;不建本地投影,避免兩份定義漂移);★ Stand-in 期間專用池佔用 fail-closed(JdbcLimitPort 拋 CC-LIM-1003,不以卡人額度代佔——否則違反 BR-04-01 且雙重佔用)。⚠️ 需求語意之裁定(8-4「未設定」vs 8-5「> 額度」):採三段式含回退,理由為「設定專用額度不得使交易能力下降」——已明載為解釋而非隱含假設。測試:65 測 0 失敗(新增 5) |
| 1.12 |
GOV-006 4.8 W-2 推廣・CR-027 批次 B:§7.15 佔用事件消費(2026-10-01,#A-128)——新增 limit.hold.created.v1/limit.hold.released.v1 → 本地 limit_holds 之消費端規格(V2__read_model.sql 明文載明卻從未有人餵養之第三個讀模型缺口)。實作:LimitHoldProjectionPort/LimitHoldProjectionService/JdbcLimitHoldProjectionRepository/LimitHoldListener。★★ 以 authorizationId 去重——阻斷「stand-in 後送 → 同一筆佔用兩列 → SUM 重複扣減 → 可用額度被低估」之關鍵風險(真機以負樣本驗證:停 limit-service 觸發 stand-in → 重啟上送 → 本地仍 1 列)。★ 亂序判準置於 consumed_events(limit_holds 為業務表,不增技術欄位)。★ 併修正 JdbcLimitPort.availableLimit 之 status 過濾缺陷(原僅計 HELD,漏計 PARTIALLY_RELEASED;實測 86,000 vs 96,000)。併補 §10 #9~#11 |
| 1.13 | 2026-10-01 |
| 1.11 |
GOV-006 4.8 W-2 推廣・事件 #3:§7.14 授權核准事件輸出(2026-10-01,#A-121)——新增 authorization.authorization.approved.v1 之生產端規格(SDD-003 時序圖與 SDD-006 §4.2 均已載明,惟從未有人發射)。主題 cc.authorization.authorization.approved;同交易寫 Outbox + relay;僅「已核准」路徑發射(REFERRAL 轉人工亦屬核准路徑——已佔用額度)。實作:V9__outbox.sql + OutboxPort/AuthEventFactory/JdbcOutboxRepository/KafkaEventPublisher/OutboxRelayConfig/OutboxRelayScheduler。併補 §10 #6/#7 之誠實揭露(消費端未實作、拒絕事件未發射、序號簡化) |
| 1.10 |
GOV-006 4.8 W-2 推廣・事件 #2:§7.13 額度事件消費(2026-10-01)——新增 limit.limit.adjusted.v1 → account_limit_caches 之消費端規格(SAD-004 §5.2 載明卻從未實作;V2__read_model.sql 之投影無餵養者)。實作:AccountLimitProjectionService/AccountLimitProjectionPort/JdbcAccountLimitProjectionRepository(冪等+亂序+upsert 同交易,只覆寫 total_amount)/AccountLimitChangedListener(@KafkaListener);遷移 V8__limit_consumption.sql(account_limit_caches.last_sequence_no,共用 consumed_events)。併處理命名落差(V2 之 limit.limit.changed.v1 → 依 SAD-004 §5.2 之 limit.limit.adjusted.v1)。真機驗證通過 |
| 1.9 |
GOV-006 4.8 W-2 第一切片:§7.12 卡片狀態事件消費(2026-10-01)——新增 card.card.status-changed.v1 → card_status_caches 之消費端規格。緣由:V2__read_model.sql 明文載明該投影,惟從未有人餵養(JdbcCardPort 讀之、無寫入者 → 讀模型恆空)。實作:CardStatusProjectionService/CardStatusProjectionPort/JdbcCardStatusProjectionRepository(冪等+亂序+upsert 同交易)/CardStatusChangedListener(@KafkaListener);遷移 V7__card_status_consumption.sql(consumed_events + card_status_caches.last_sequence_no)。真機:BEFORE count=0 → 發布事件 → upsert 成功;冪等(重送同 eventId → version 不變)/亂序(送舊 seq → 不覆寫)皆通過。⚠️ 生產端(card-service 發射)未實作(下一片) |
| 1.8 |
桶 C 解鎖(#A-114):§7.11 交易狀態機檢視(2026-10-01)——新增 GET /api/v1/authorizations/{authNo}/state(AuthorizationStateView),解鎖 SDD-007 §5.1.2.3 桶 C「交易狀態機與冪等」(原記「均無端點」)。語意:對單一授權呈現狀態機視角——當前狀態(decision)、佔用(limit_holds,以 hold_id 關聯)、狀態轉移歷程(由 created_at/override_at/reversed_at 權威時間欄推導)。設計要點:(i) 狀態機視角 vs §7.9 事件時間軸——互補不重複;(ii) 不以 decision 推導「是否曾核決/沖正」(改權威時間欄,避 §7.7.1 同型缺陷);(iii) 冪等(Redis)如實揭露、不重現(不臆造欄位)。契約重生 0 錯誤 |
| 1.7 |
CR-026 新增統計端點 GET /api/v1/authorizations/stats(2026-10-01)——新增 §7.10 授權統計(供 /dashboard 業務 KPI);pendingUpload=standin_flag=1 AND standin_upload_at IS NULL(依 V4__standin_upload.sql);today=當日授權數;byDecision 取自 DDL CHECK 之 4 值。契約重生 0 錯誤;:services:authorization-service:test BUILD SUCCESSFUL(3 個測試 fake 補 stats()) |
| 1.6 |
CR-025 稽核軌跡查詢端點(2026-09-29)——新增 §7.9 GET /authorizations/audits,解鎖 SDD-007 §5.1.2.3 桶 C「稽核軌跡」(原記「表存在但無查詢端點」——資料寫得進去、讀不出來)。設計三要點:(i) 統一時間軸(auth_overrides 與 auth_reversals 兩表 UNION ALL 後依時間遞減——使用者關心「這筆授權發生過什麼」而非「屬於哪張表」);(ii) action 值域正規化(reversal_type || '_REVERSAL');(iii) actor 對沖正如實回 null(auth_reversals 無操作者欄位——不臆造)。append-only 唯讀(可竄改即失去證據力)。契約重生 79 端點、0 錯誤。真機 3 項:ALL 4 筆正確排序/OVERRIDE/REVERSAL 各 2 筆/null 欄位如實 |
| 1.5 |
CR-023 後送作業台之端點補齊(2026-09-24)——§7.7 之「職責」表補明端點對應:原表僅列三項職責而未對應端點,實作僅有單筆之 POST …/{authNo}/advices → 「查詢待補送佇列」與「批次手動上送」無端點可達,致 SDD-007 §5.4.23(/auth/stip 頁)無法完整實作。新增 GET /authorizations/standin?status=PENDING|UPLOADED|ALL 與 POST /authorizations/standin/advices;併明訂清單條件為 standin_flag = 1、不得以決策碼篩選(§7.7.1——否則轉人工件被漏)。契約重生 77 端點、驗證 0 錯誤
|
| 1.4 |
CR-022 真機驗證後補記(2026-09-24):新增 §7.7.1 standin_flag 之判定依據——明訂須依「佔用方式」(HoldResult.standIn)而非決策碼;併修實作(Authorization 聚合保留 standIn 旗標、JdbcAuthorizationRepository 改用之)。緣起:真機實測發現 §7.8 新增 REFERRAL 後,「decision=STANDIN」與「standIn=true」不再等價,轉人工件在額度服務不可用時漏標 standin_flag → 該筆永不後送(額度權威側永久落差=超額風險)。§9 補 24 例單元測試與 7 項真機實測
|
| 1.3 |
CR-022 裁定後之實作規格(2026-09-24):§7.7 改寫為 (A) 後送授權通知(BEAM PMO 裁定;含與排程上送之分工、冪等守門、契約修訂定案);新增 §7.8 人工授權/強制授權(RFP FR 1.1 第 4 項明文要求;含參數化轉人工門檻 CheckCode.MANUAL_REVIEW、狀態守門 SM-D1~D5、僅限對 REFERRAL 核決)。併重生 17 份契約、新增遷移 V6__auth_overrides.sql
|
| 1.2 |
CR-021 真機驗證後補記:§7.6 雙軌釋放——StandInLimitPort.release 與 hold 對稱(遠端優先、失敗降級本地並 WARN、遠端成功後 best-effort 清本地同列);FULL 沖正已真機驗證(AUTH000035)。⚠️ 併發現既有缺陷:LimitServiceClient(Feign)呼叫 limit-service 回 401(未接 M2M outbound token)→ 授權一律降級 Stand-in(已登 GOV-006 §4 #21) |
| 1.1 |
CR-021 補實作之規格先行:§6 補 check-results/reversals 兩端點(契約此前宣告而未實作)+ advices 標記「語意待裁定、本期未實作」;新增 §7.5 檢核明細查詢(查落庫、不重算)、§7.6 沖正、§7.7 advices 兩種語意之裁定建議
|
| 1.0 | 初版:回溯補建,與既有程式一致 |