Project

General

Profile

CR #201 » SCS-024_進件處理程式設計規格.md

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

 

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。理由:

  1. 分行收件與送徵信在實務上可分離(收件後批次送件);
  2. 若建立時自動呼叫外部徵信,則「建立申請」這支 API 會承擔外部系統失敗之風險,
    違反「建立應為本地交易」;
  3. 快發雖需即時,但仍是「受理 → 送徵信 → 核准」三段,可於同一 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)

⚠️ 實測前抓到兩個會讓編譯碎的錯:①CreditReviewPort import 錯套件(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. 已知限制(誠實揭露)

  1. 無外部徵信契約(本期不實作) → 已於 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-002 bounded context)。
    base-url 未設定時一律轉人工核決(fail-closed,不自動核准)。

  2. 製卡僅定義事件契約:card.application.approved 事件已定義,
    但製卡檔產生器與送件作業本期不實作。

  3. 補件逾期批次未實作:SUPPLEMENT_EXPIRED 事件已定義,觸發之批次作業本期不實作。

  4. approval_no 未加 UNIQUE:本期以撞號重試處理;建議後續加約束
    (須走 V4 遷移,不改 V1 基線)。

  5. 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。

  6. 額度核定未串接:核准後呼叫 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 申請案件狀態機
(59-59/67)