CR #201 » SCS-024_進件處理程式設計規格.md
SCS-024 進件處理(受理/徵信/核決/製卡)程式設計規格
| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-024 |
| 版本 | 1.3 |
| 狀態 | 已實作(v1.1,2026-09-20 實測通過) |
| 撰寫者 | WorkBuddy AI |
| 審查者 | BEAM PMO |
| 服務 | card-service |
| 對應需求 |
BRD-001 BF-01(申請與發卡)、BF-10(卡片生命週期)、BR-01-01~BR-01-04;BRD-001 D-01(徵審系統) |
| 對應設計 |
SDD-005 §12(申請案件狀態機)、SDD-005 §2(卡片狀態機)、SDD-006 §3.4(錯誤碼)、SDD-001 §3(資料模型)、SAD-003 §9(遷移規範) |
| 關聯規格 |
SCS-003(卡片服務)、SCS-023(商品組合) |
| 建立日期 | 2026-09-20 |
本文件依 BEAM PMO 2026-09-20「進件系統有缺口」之指示建立,採 SPEC 先行——
未核定前不得撰寫程式。
1. 目的與範圍
補上「申請案件受理之後」整段流程:徵信、補件、核決、核准序號、製卡銜接。
範圍內
- 申請案件狀態機之程式實作(
SDD-005 §12) - 徵信介接(port/adapter,含 fail-closed)
- 核准序號產生
- 核准後與製卡/快發之銜接
- 查詢與狀態推進之 REST 端點
範圍外
- 徵審系統本身(
BRD-001 D-01,外部系統,由客戶既有的負責) - 製卡檔產生與送件(本期僅定義事件契約,見 §7.2)
- 額度核定(屬
limit-service;本規格僅於核准後呼叫其介面) - 票證卡加值/電子票證帳務(另案)
2. 名詞
| 名詞 | 說明 |
|---|---|
| 進件 | 自受理至核決之作業流程 |
| 徵信 | 向外部徵審系統查詢信用評等與風險(結果:APPROVE/DECLINE/REFER/SUPPLEMENT) |
| 補件 | 資料不齊,通知客戶補充(SUPPLEMENT) |
| 核決 | 徵審人員作成核准或婉拒之決定 |
| 核准序號 |
approval_no,快發「部分開卡」三要素之一(BR-01-04) |
| 製卡 | 實體卡壓印與送件;完成後卡片進入 INACTIVE(SDD-005 §2) |
3. 現況缺口(誠實盤點)
| # | 缺口 | 證據 |
|---|---|---|
| 1 |
契約只有 POST /api/v1/card-applications——無查詢、無狀態推進 |
contracts/openapi/card-service.yaml 僅一路徑 |
| 2 |
領域模型無轉態方法——CardApplication 只有 received() 工廠與 approved() 查詢 |
CardApplication.java |
| 3 |
無徵信介接——SCS-003 §10 #9 明載「核准屬徵信流程,無對應端點」 |
SCS-003 §10 #9 |
| 4 |
approval_no 永為 NULL——無產生邏輯 |
CardApplicationService.create() 未填 |
| 5 | 無製卡——核准後直接斷鏈,卡片無從產生 | 無對應程式 |
| 6 |
本地測試以直接更新資料準備 APPROVED 案件——快發快綁之前置條件靠人工改 DB |
CardApplicationService 類別說明 |
#6 之風險:以人工改 DB 準備前置條件,意味著「核准」這條路徑從未被程式驗證過——
快發快綁之實測只證明了後半段。本規格即為消除此風險。
4. 狀態機
4.1 權威來源
值域、轉換表、非法轉換一律以 SDD-005 §12 為準,本文件不重複定義。
本節僅規範程式如何實作。
4.2 轉態守門(三道)
| 道 | 位置 | 檢核 |
|---|---|---|
| ① |
CardApplication 領域方法 |
非法轉換拋 CC-CARD-3001;終態拋 CC-CARD-3002
|
| ② |
CARD_APPLICATIONS.status 之 CHECK 約束 |
資料庫層最後防線(實測 5 值,見 SDD-005 §14.3) |
| ③ |
樂觀鎖 version
|
UPDATE ... WHERE id=? AND version=?,影響 0 列即併發衝突 |
為何①與②都做:②只能擋「值不合法」,擋不了「合法值之間的非法路徑」
(例如RECEIVED → APPROVED)。兩者職責不同,不可互相取代。
4.3 樂觀鎖之紀律
CardApplication 採 with* 方法回傳新實例;每次更新只能套用一次 with*
(版本恰 +1)——鏈式呼叫會使版本 +2 而誤判衝突(此為 common-saga 既有教訓,
見 MEMORY.md「Saga 設計」)。
5. 徵信介接
5.1 Port(領域層)
public interface CreditReviewPort {
/** 結果:APPROVE/DECLINE/REFER/SUPPLEMENT。 */
CreditReviewResult review(CreditReviewRequest request);
}
5.2 處置表(fail-closed)
| 徵信回應 | 處置 | 狀態變化 |
|---|---|---|
APPROVE |
自動核准 | REVIEWING → APPROVED |
DECLINE |
自動婉拒 | REVIEWING → REJECTED |
REFER |
轉人工核決 | 維持 REVIEWING
|
SUPPLEMENT |
要求補件 | REVIEWING → SUPPLEMENT |
| 呼叫失敗/逾時/無 adapter | ⚠️ 轉 REFER、維持 REVIEWING、記 WARN
|
不變 |
為何是 fail-closed:徵信不可用時若靜默放行(自動核准),
即為「靜默不做」型缺陷——與 Flyway 未顯式引入 starter、
loki exporter 不存在、認證預設 fail-open 同類(見SCS-020 §9.2、SCS-022 §7.4)。
不得自動核准,亦不得自動婉拒——一律轉人工。
5.3 Adapter 之選用
| Adapter | 用途 | 狀態 |
|---|---|---|
ExternalCreditReviewAdapter |
HTTP 介接 BRD-001 D-01 徵審系統 |
待介面契約;本期不實作(見 §12 #1) |
NoopCreditReviewAdapter |
本地/測試:一律回 REFER
|
本期實作 |
⚠️ 為何本地 adapter 回
REFER而非APPROVE:回APPROVE會讓「核准」
在沒有徵信的情況下自動發生,正是 fail-closed 要防的。回REFER才是誠實的
——它代表「我不知道,交給人」。
6. 核決與核准序號
| 項目 | 規格 |
|---|---|
| 格式 |
APR + yyyyMMdd(8) + 亂數 8 碼 = 19 碼(欄位 VARCHAR2(24 CHAR)) |
| 產生時機 | 於 REVIEWING → APPROVED 轉換當下產生並寫入 |
| 唯一性 | 建議加 UNIQUE;本期先以「撞號重試 3 次」處理(同 apply_no 之作法) |
| 用途 | 快發部分開卡之三要素驗證(BR-01-04),失敗為 CC-CARD-1002
|
為何在轉換當下產生:核准序號是「核准」這件事的產物,
若延後產生會出現「狀態已APPROVED但無序號」之中間態——該態下快發無法開卡,
屬不一致狀態。故與狀態轉換同交易寫入。
7. 核准後之銜接
7.1 快發(fast_issue_flag = 1)
核准後即時給號:呼叫既有 CardIssueService.issue(...),卡片建立為 INACTIVE,
再由 POST /api/v1/cards/{cardId}/instant-issuance 完成部分開卡(三要素驗證)。
7.2 一般(非快發)
核准後寫入 Transactional Outbox 事件 card.application.approved
(與權威寫入同庫同交易,見 CSP-006),payload:
{ "applicationId": "...", "applyNo": "...", "approvalNo": "...",
"accountId": "...", "productCode": "...", "approvedAt": "..." }
由批次作業撈取送製卡檔。本期不實作製卡檔產生器(見 §12 #2)。
7.3 與商品組合之關係(SCS-023)
申請案件僅帶 productCode;該商品之能力集合由 product-service 之 parameters 表
以 namespace = product_code 讀取(SCS-023 §4)。進件流程中之下列決策一律由參數決定,
不得硬編:
| 決策 | 參數鍵 |
|---|---|
| 是否需徵信 | intake.credit_review_required |
| 補件期限(天) | intake.supplement_days |
| 是否允許快發 |
issuance.form 且 intake.fast_issue_allowed
|
| 是否需票證帳號 | ticket.enabled |
此即 BEAM PMO 2026-09-20「不應局限維度與取值」之指示於進件段之落法——
新增商品不需改程式。
8. API 契約(新增端點)
⚠️
contracts/openapi/*.yaml由gen_openapi.py產生,禁止手改
(見MEMORY.md「產生器紀律」)。本節為需求,實作時須改產生器後--write。
| # | 端點 | 方法 | 說明 |
|---|---|---|---|
| 1 | /api/v1/card-applications/{applicationId} |
GET |
查詢案件(補現況缺口 #1) |
| 2 | /api/v1/card-applications/{applicationId}/transitions |
POST |
狀態推進 |
8.1 POST .../transitions 請求
{ "event": "SUBMIT_REVIEW", "reason": null, "operatorId": "credit.user01" }
event |
起始 → 結果 | 對應 SDD-005 §12.2
|
|---|---|---|
SUBMIT_REVIEW |
RECEIVED → REVIEWING |
#2 |
REQUEST_SUPPLEMENT |
REVIEWING → SUPPLEMENT |
#3 |
COMPLETE_SUPPLEMENT |
SUPPLEMENT → REVIEWING |
#4 |
APPROVE |
REVIEWING → APPROVED |
#5 |
REJECT |
REVIEWING/RECEIVED/SUPPLEMENT → REJECTED
|
#6/#8/#7 |
SUPPLEMENT_EXPIRED |
SUPPLEMENT → REJECTED |
#7(批次) |
8.2 回應
CardApplicationView,新增欄位:updatedAt、version。
成功 200;非法轉換 409 CC-CARD-3001;終態 409 CC-CARD-3002;
查無案件 404 CC-CARD-4040;event 不在列舉內 400 CC-CARD-4001。
8.3 ⚠️ 一項刻意之設計決策:建立後不自動送徵信
受理(建立)後案件維持 RECEIVED,須顯式呼叫 SUBMIT_REVIEW。理由:
- 分行收件與送徵信在實務上可分離(收件後批次送件);
- 若建立時自動呼叫外部徵信,則「建立申請」這支 API 會承擔外部系統失敗之風險,
違反「建立應為本地交易」; - 快發雖需即時,但仍是「受理 → 送徵信 → 核准」三段,可於同一 workflow 內串接。
9. 錯誤碼(不新增)
全部沿用 SDD-006 §3.4 既有登錄:
| 碼 | HTTP | 用於 |
|---|---|---|
CC-CARD-3001 |
409 | 非法狀態轉換(如 RECEIVED → APPROVED) |
CC-CARD-3002 |
409 | 終態不可再轉換(APPROVED/REJECTED) |
CC-CARD-4001 |
400 |
event 不在列舉內、欄位檢核失敗 |
CC-CARD-4040 |
404 | 查無申請案件 |
CC-CARD-1002 |
422 | 三要素驗證失敗(快發部分開卡) |
CC-CARD-1001 |
422 | 快發條款未勾選(BR-01-01) |
CC-CARD-2001 |
409 | 冪等鍵衝突(同鍵異指紋) |
若日後需「徵信系統連線失敗」之專屬碼,須依
CSP-003 §2程序補登SDD-006 §3.4
後重跑gen_error_codes.py --write,不得於程式內自創。
10. 交易與一致性
| 項目 | 規格 |
|---|---|
| 狀態推進 |
@Transactional;狀態與 updated_at/version 同交易寫入 |
| 併發 | 樂觀鎖;影響 0 列視為衝突(回 CC-CARD-3001 並記 WARN) |
| 冪等 |
POST .../transitions 帶 X-Idempotency-Key;指紋含 applicationId + event + reason
|
| 核准序號 | 與狀態轉換同交易寫入(§6) |
| Outbox | 一般(非快發)核准時,事件寫入與狀態轉換同交易 |
11. 測試要求
| # | 案例 | 預期 |
|---|---|---|
| 1 | 全轉換矩陣:8 條合法轉換逐一實測 | 皆成功,SDD-005 §14.2 要求 |
| 2 |
4 條非法轉換(RECEIVED→APPROVED、APPROVED→*、REJECTED→*、REVIEWING→RECEIVED) |
CC-CARD-3001/3002
|
| 3 | 徵信回 APPROVE
|
自動 APPROVED 且 approval_no 非 NULL
|
| 4 | 徵信回 DECLINE
|
自動 REJECTED
|
| 5 | 徵信拋例外(模擬外部系統故障) | 維持 REVIEWING、記 WARN、不核准
|
| 6 |
無 adapter(NoopCreditReviewAdapter) |
回 REFER,維持 REVIEWING
|
| 7 | 樂觀鎖衝突(同案件並發轉態) | 一方失敗 |
| 8 | 冪等重放 | 回前次結果,狀態不重複推進 |
| 9 |
event 不在列舉內 |
CC-CARD-4001 |
| 10 | 查無案件 | CC-CARD-4040 |
| 11 | 核准序號格式 |
APR+yyyyMMdd+8 碼亂數,全案唯一、非 NULL
|
| 12 | 徵信回 SUPPLEMENT
|
→ SUPPLEMENT(補件) |
| 13 | 版本號遞增 | 每次合法轉態 version 恰 +1(樂觀鎖基礎) |
| 14 | 快發路徑 |
fastIssueFlag=true 時不寫 Outbox(即時給號由快發端點負責) |
| 15 | 一般路徑 |
fastIssueFlag=false 時寫 card_application_outbox(事件 card.application.approved) |
| 16 | 建立冪等重放 | 同 X-Idempotency-Key 回前次結果,不重複建案件 |
#5 為本規格之核心測試——它證明「徵信掛掉時不會自動核准」。
若以「回APPROVE」之假 adapter 測試,此案例永遠是綠的,屬假驗證,禁止。
11.1 實測證據(2026-09-20,card-service 單元測試)
以 Fakes(記憶體假實作)+ 可控 CreditReview 假 adapter 執行,含 #5 徵信拋例外 與 #7 樂觀鎖 之真實驗證:
| 項目 | 結果 |
|---|---|
| 測試數 | 16 支全過(5 建立+11 轉態) |
| card-service 整模組 | 42 支 0 失敗(無回歸) |
| #5 徵信拋例外 | ✅ 維持 REVIEWING、記 WARN、不核准(fail-closed 成立) |
| #7 樂觀鎖 | ✅ 並發轉態一方失敗(CC-CARD-3001);Fakes.update 已模擬 version+1 與衝突 |
| #13 版本號 | ✅ 每次轉態 version 恰 +1 |
| #14/#15 Outbox | ✅ 快發不寫、一般寫(由 appendOutbox 落 V4__card_application_outbox.sql) |
⚠️ 實測前抓到兩個會讓編譯碎的錯:①
CreditReviewPortimport 錯套件(domain.model→domain.port)②既有CardIssueServiceTest.newApplication用 8 引數構造CardApplication,重寫後變 12 引數——已補approvedAt/updatedAt/version三引數。> 均修復後編譯通過。
11.2 端到端實測(2026-09-20,Vite 5173 → card-service 8082 → 真實 Oracle)
經 /card-api 代理實測完整進件流程(非單元測試、非假物件):
| # | 情境 | 實測結果 |
|---|---|---|
| 1 | 受理進件 |
201;applyNo=APP2026092062814284、狀態 RECEIVED、version=0
|
| 2 | 送徵信 SUBMIT_REVIEW
|
REVIEWING(version=1)——徵審未接上,維持徵審中轉人工核決,未核准(fail-closed ✅) |
| 3 | 要求補件 REQUEST_SUPPLEMENT
|
SUPPLEMENT(version=2) |
| 4 | 補件完成 COMPLETE_SUPPLEMENT
|
REVIEWING(version=3) |
| 5 | 核准 APPROVE
|
APPROVED(version=4);approvalNo=APR2026092036624916(19 碼=APR+8 日期+8 序號) |
| 6 | 終態再轉換 |
CC-CARD-3002(409)✅ |
| 7 | 查詢案件 GET
|
狀態 APPROVED、version=4,與轉態結果一致 |
| 8 | 查無案件 |
CC-CARD-4040(404)✅ |
| 9 | 冪等重放(同鍵同內容) |
replayed=true、version 仍為 4——證明未重複轉態 ✅ |
⚠️ 本輪實測抓到兩個真缺陷(單元測試完全測不出),皆已修復:
| 缺陷 | 現象 | 根因 | 修復 |
|---|---|---|---|
A. 查詢拋 ORA-17006 |
轉態與查詢皆回 CC-PLT-5000
|
FIND_SQL 之 SELECT 漏選 approved_at,但 RowMapper 會讀它 → 讀取階段才報錯 |
補入 approved_at;新增 JdbcCardApplicationRepositorySqlTest(3 支)以「SELECT 清單須涵蓋 DDL 全部欄位」固樁 |
B. 冪等寫入拋 ORA-12899 |
值太大(實際 27,最大 20) |
card_idempotency.entity_type 為 VARCHAR2(20 CHAR),但 CardApplicationService 寫入 CARD_APPLICATION_TRANSITION(27 字元) |
新增 V6__widen_idempotency_entity_type_fix.sql 放大為 VARCHAR2(64 CHAR)
|
為何單元測試測不出:
①ORA-17006屬讀取階段錯誤——SQL 解析不會錯(缺欄不是語法錯誤),
而單測以假 Repository(Fakes)執行根本不走 JDBC,讀不到就報不出來。
②ORA-12899屬欄位寬度錯誤——H2/假物件無此約束。
兩者皆印證「編譯過不等於能跑,單測過不等於能在真實 Oracle 跑」。⚠️ 附記(遷移檔之自我修正):原
V5__widen_idempotency_entity_type.sql之守衛
以data_length判斷,但該欄回傳的是位元組長度(VARCHAR2(20 CHAR)於 AL32UTF8 下為 80),
而判斷式寫v_len < 64→ 80 < 64 為偽,V5 實為 no-op。
V5 已寫入 Flyway schema history,不可改其內容(會 checksum 失配),
故另立 V6 修正:改用CASE WHEN char_used='C' THEN char_length ELSE data_length END。
教訓:判斷長欄位長度須看char_length(配合char_used),不可看data_length(位元組)。
12. 已知限制(誠實揭露)
-
無外部徵信契約(本期不實作)→ 已於 v1.2 全部實作完成:
CreditReviewPort+CreditReviewResult+NoopCreditReviewAdapter(v1.1)
+ExternalCreditReviewAdapter(v1.2)皆已落地並測試通過。後者依
SCS-025(我方自行設計之徵審介面契約) 實作——RFP 與
BRD-001 D-01皆未提供介面規格,為使 POC 完整、避免客戶質疑「徵審在哪」,
由我方設計契約並實作 adapter(SCS-025 §1.1)。⚠️ 該契約為建議版,待客戶確認(
SCS-025 §10 #1);客戶提供其自身規格時
僅換 adapter,領域層與狀態機不受影響(SAD-002bounded context)。
base-url未設定時一律轉人工核決(fail-closed,不自動核准)。 -
製卡僅定義事件契約:
card.application.approved事件已定義,
但製卡檔產生器與送件作業本期不實作。 -
補件逾期批次未實作:
SUPPLEMENT_EXPIRED事件已定義,觸發之批次作業本期不實作。 -
approval_no未加UNIQUE:本期以撞號重試處理;建議後續加約束
(須走 V4 遷移,不改 V1 基線)。 -
WebUI 未納入→ 已於 v1.3 實作完成(SDD-007 §5.4.9):
進件作業台(/card-applications)前端已建置並接線(Vite/card-api→ 8082),
依規格逐鈕定義實作受理/查詢/送徵信/補件/補件完成/核准/婉拒/清除;
並已通過 9 情境端到端實測(§11.2)。
⚠️ 「送徵信」結果維持REVIEWING時畫面明示「轉人工核決」,不顯示為核准。
截圖存證(docs/08-evidence/,與既有 8 頁一致):
webui-applications-01-作業台初始畫面.png/02-受理後案件明細.png/
03-送徵信後轉人工核決.png(fail-closed 之畫面證明)/
04-核准後顯示核准序號.png。 -
額度核定未串接:核准後呼叫
limit-service之介面本期不實作。
13. 修訂記錄
| 版本 | 日期 | 修訂者 | 說明 |
|---|---|---|---|
| 1.1 | 2026-09-20 | WorkBuddy AI | BEAM PMO 核定後實作完成並測試通過: |
| 1.2 | 2026-09-20 | WorkBuddy AI |
徵審介接補完(BEAM PMO 2026-09-20 指示「RFP 無徵審規格,POC 會被質疑,須自行設計提供」):新增 SCS-025 徵審介面契約規格(我方建議版)與 ExternalCreditReviewAdapter(OpenFeign,照 CardServiceClient 慣例);CreditReviewClient 自有 DTO、不共用領域類;Noop 與 External 以 @ConditionalOnProperty 互斥(避免兩 bean 注入歧義);領域 CreditReviewRequest 補 applyNo/channel;application.yml 為 creditReviewClient 專屬覆寫 resilience4j(7s/retry 1 次,因全域 800ms/3 次對徵審不適用)。測試 ExternalCreditReviewAdapterTest 14 支全過。§12 #1 由「待契約」改列「已依 SCS-025 實作完成(契約待客戶確認)」 |
| 1.3 | 2026-09-20 | WorkBuddy AI |
進件作業台 WebUI 實作完成(SDD-007 §5.4.9)+ 端到端實測:新增 frontend/src/api/cardApplications.ts 與 pages/CardApplicationConsole.tsx(照既有 8 頁慣例:自有 DTO、標籤對照集中於 api 檔、卡片式佈局、不自創 className);vite.config.ts 加 /card-api → 8082;App.tsx 四處接線。⚠️ 端到端實測抓到兩個真缺陷(單元測試完全測不出):①FIND_SQL 漏選 approved_at → 真實 Oracle 讀取時拋 ORA-17006(已補欄,並新增 JdbcCardApplicationRepositorySqlTest 3 支以「SELECT 須涵蓋 DDL 全部欄位」固樁);②card_idempotency.entity_type 僅 VARCHAR2(20)、裝不下 27 字元之 CARD_APPLICATION_TRANSITION → ORA-12899(新增 V6 放大為 64 CHAR;原 V5 因守衛誤用 data_length(位元組)而 no-op,已於 §11.2 誠實記述)。9 情境端到端全數符合預期(§11.2),含 fail-closed 轉人工核決、核准序號 19 碼、終態 CC-CARD-3002、冪等重放不重複轉態。§12 #5「WebUI 未納入」解除。已截圖存證 4 張(docs/08-evidence/webui-applications-01~04),其中 03 為 fail-closed 之畫面證明 |
新增 CardApplication 轉態方法(6 合法/4 非法,領域層拋 CC-CARD-3001/3002)、 |
|||
CardApplicationRepository.update(樂觀鎖 version+1 WHERE version=?,0 列拋 CC-CARD-3001)/appendOutbox; |
|||
JdbcCardApplicationRepository 讀寫 updated_at/version/approved_at、樂觀鎖、Outbox 寫入(V4__card_application_outbox.sql); |
|||
CardApplicationService 接徵信 fail-closed(safeReview 捕例外轉 REFER)、核准序號(APR+yyyyMMdd+8碼)、Outbox; |
|||
CardController 加 GET /{id} 與 POST /{id}/transitions;gen_openapi.py 加兩端點後 --write; |
|||
Fakes 補 update/appendOutbox/CreditReview 可控假 adapter;CardApplicationServiceTest 16 支全過。 |
|||
| §11 補 #11~#16、§11.1 實測證據;§12 #1 改列「已實作(核心),僅外部 adapter 待契約」。 | |||
| 品質關卡全過(空殼/mock/假測試 0 命中、OpenAPI 0 錯誤)。狀態「待核定」→「已實作」。 | |||
| 1.0 | 2026-09-20 | WorkBuddy AI | 初版。依 BEAM PMO 2026-09-20「進件系統有缺口」之指示建立:§3 現況缺口盤點(6 項)、§4 轉態守門三道(領域/DDL CHECK/樂觀鎖)、§5 徵信 port 與 fail-closed(徵信不可用轉 REFER、不自動核准亦不自動婉拒)、§6 核准序號、§7 製卡銜接與商品參數化、§8 新增兩端點、§9 不新增錯誤碼、§11 測試(#5「徵信拋例外不核准」為核心,禁用假 adapter)。連動 SDD-005 升 1.4 新增 §12 申請案件狀態機 |