Project

General

Profile

CR #201 » SCS-001_授權服務程式設計規格.md

內容更新:內部稱謂去識別化(2026-10-18) - Redmine Admin, 10/18/2026 05:32 AM

 

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)導致 Feign DecodeException → 每次都降級,系統看似正常但其實一直用快取。
排查請先看降級 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 一致):

  1. 以 GROUP BY 聚合(單次查詢),不得 N+1。
  2. total/today 由後端計算,不得由前端相加分組。
  3. byDecision 僅含實際存在之值(不補 0)。
  4. 不快取、無查詢參數(本階段為總量統計)。

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

處理:

  1. 冪等:eventId 寫入 consumed_events(與 upsert 同交易);已存在 → 略過。
  2. 亂序防護:sequenceNo 落後於該 cardNoHash 已套用者 → 略過並 WARN。
  3. Upsert:以 card_no_hash(TRIM 比對,同 JdbcCardPort)為鍵 upsert card_status_caches。
  4. 未知 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

處理:

  1. 冪等:eventId 寫入 consumed_events(與 upsert 同交易);已存在 → 略過。
  2. 亂序防護:sequenceNo 落後於該(account_id, currency)已套用者 → 略過並 WARN。
  3. Upsert:以(account_id, currency)為業務鍵 upsert account_limit_caches;currency 為 CHAR(3) → 比對一律 TRIM。
  4. 未知 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。

處理:

  1. 同交易寫 Outbox:AuthorizeUseCase.execute 於 repository.save(authorization) 之後、同一交易(類別層 @Transactional)寫入 outbox;否則會出現「授權已核准、事件未發」之落差。
  2. ★ 發射條件=「是否佔用額度」(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)。
  3. payload 補 decision(可選)(CR-030 §2 #8)——下游據此分辨「核准」與「轉人工待覆核」。
    ⚠️ notification-service 對 decision = REFERRAL 不建立通知(無對應樣板,不臆造文案,見 SCS-016 §7.9)。
  4. 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(Outbox payload 已為信封 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。

處理:

  1. 冪等:eventId 寫入 consumed_events(與投影寫入同交易);已存在 → 略過。
  2. 亂序防護:sequenceNo 落後於該 aggregateId 已消費之最大值 → 略過並 WARN。
    ⚠️ 判準取自 consumed_events(MAX(sequence_no) WHERE aggregate_id = ?),不在 limit_holds 增設技術欄位
    ——理由見下表。
  3. created → upsert 本地投影(★ 以 authorizationId 為業務鍵去重)。
    ★ instrumentId 落地(CR-054 P2,2026-10-17):事件帶之載體寫入
    limit_holds.instrument_id(V14 新增,可空——V10 前之歷史佔用無載體)。
    ⚠️ 更新既有列時用 instrument_id = NVL(?, instrument_id):事件未帶載體時
    不得清空既有值——否則事件重放(或舊事件)會把已補的載體抹掉。
  4. 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)。

  5. 未知 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 新增。

★ 落庫語意:

  1. merchantName/entryMode 以字串直接綁定(entryMode 之值域由契約 enum + 介面層
    @Pattern 把關;DDL 無 CHECK,故不於資料庫層再設值域——維持與既有 mcc 一致之寬鬆)。
  2. emvData 以 Jackson ObjectMapper 序列化為 JSON 字串,經 JSON(?) 綁定
    (Oracle 23ai JSON 型別;與 JdbcPartyRepository.FLAGS 同型)。
    ⚠️ 讀取(若需)以 JSON_SERIALIZE(emv_data RETURNING VARCHAR2(4000))——不可直接
    rs.getString 取 JSON 欄(型別不符)。
  3. 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 §4 BEAM 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. 已知限制(誠實揭露)

  1. 13 大項檢核僅實作 6 項(AMOUNT/CURRENCY/CARD_STATUS/CARD_EXPIRY/MCC/LIMIT);其餘 7 項(EMV/CVV/風控/日限/地域/頻率/黑名單/爭議)缺資料來源,標 isImplemented=false,不產生假的通過結果。

  2. 檢核順序未依契約完整排序(契約規範 13 大項順序)。

  3. GET /authorizations(清單)與明細未登錄契約(明細鍵為 authNo vs 契約 {authId})。

  4. 上送冪等鍵未做:目前靠「成功才寫 standin_upload_at」;若 limit-service 收到重複 authorizationId 無保護。

  5. 監控未接(CSP-004 僅追蹤碼,Prometheus/Grafana 未串)。

  6. authorization.authorization.approved.v1 僅有生產端(§7.14,#A-121):消費端(posting/limit/notification,SAD-004 §5.2)尚未實作;拒絕事件 authorization.authorization.declined.v1(SDD-006 §4.2)亦尚未發射。

  7. sequenceNo 為同 JVM 內遞增之替代序號(§7.14):多實例部署可能重複;正式應改為聚合層級之持久化序號。

  8. ⚠️ 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 區分)。本次不擅自擇一。

  9. ✅ limit_holds 本地投影無餵養者 → 已解除(2026-10-01,§7.15,#A-128):現由 limit.hold.created.v1/.released 餵養;正常路徑(遠端佔用)亦會寫入本地投影。

  10. stand-in 情境下「本地 holdId 與權威 holdId 不一致」 → ✅ 已裁定並實作(BEAM PMO 2026-10-01;#A-129):
    上送成功後回寫 authorizations.hold_id 為權威 holdId(§7.7),並由 §7.15 之消費端
    同步改本地列之 id → 三者一致(authorizations.hold_id = 本地列 id = 權威 holdId)。
    ⚠️ 未竟:既有歷史資料(本次裁定前已上送者)之 hold_id 仍為本地 UUID,需一次性回填
    (未做,見 §10 #12)。

  11. limit.hold.created.v1 payload 之 currency 為可選欄位(§7.15,#A-128):原 payload 未帶,惟 limit_holds.currency 為 NOT NULL → 消費端以「事件帶者優先,未帶則回查 account_limit_caches(恰一列)」處理。已列 CR-027 §5 #14 待裁定(與 #10 posted.v1 缺 currency 同型)。

  12. 既有 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.v1 payload 保留,供日後「拒絕率即時看板」需求時啟用(無契約破壞)。

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 初版:回溯補建,與既有程式一致
(38-38/67)