Project

General

Profile

CR #221 » CR-020_卡片生命週期端點之四項缺陷.md

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

 

CR-020 卡片生命週期端點(POST /cards/{cardId}/lifecycle-transitions)之四項缺陷

項目 內容
文件編號 CR-020
版本 1.1(2026-09-23:四項已全數裁定並執行完畢——BEAM PMO 裁定「不得以 workaround 心態處理」,故 F1 採 A 補實作、F2 採 B 補實作、F3 改 404、F4 採 B 完整映射表;真機實測 9 項+單測 10 例+負樣本驗證。見 §7 執行結果)
提出日 2026-09-23
提出者 WorkBuddy AI(開發團隊)
緣起 C-14 批次四(WebUI 頁面 /card/lifecycle 卡片生命週期管理)之動工前查證——依 CSP-001 §7.1 G-8 精神逐端點核對時,以真機實測(非靜態比對)發現 4 項缺陷
影響範圍 card-service(8082)之生命週期轉換端點;contracts/openapi/card-service.yaml(經 tools/gen_openapi.py 重生);SDD-005 §2 之落地完整性;SDD-007 §5.4.16(本批新增之逐鈕定義)
關聯前例 CR-019 批 2 之 C-1(AuthorizeRequest 之 merchantName/entryMode/emvData 實作未接收→靜默丟棄)——本 CR 之 F1/F2 屬同一型;#A-89 ③(C-4 判定更正)——同為「執行前查證攔下錯誤判定」之案例

1. 為什麼這些缺陷靜態比對抓不到

tools/verify_contract_dto.py(CSP-001 §7.1 G-8)比對的是「回應 schema 之欄位集合」與「Java DTO record 元件名」。
本 CR 之四項缺陷皆落在該工具之檢查面之外:

缺陷 為何工具測不到
F1 請求欄位被靜默丟棄 工具只比回應;TransitionRequest 之 threeFactors 在契約有、實作 record 也有(型別 Map<String,String>)→ 欄位集合相符,但未被使用
F2 契約宣告之必要標頭未實作 工具不檢查 parameters/headers
F3 查無資源回 409 而非 404 屬語意(同一錯誤碼用於兩種情境),非欄位集合
F4 兩條寫入路徑結果不一致 屬跨路徑一致性,須比對兩支 SQL 與 DB 實況

★ 結論:G-8 為必要但不充分。本 CR 之四項皆須真機實測 + 讀實作 + 查 DB 方能發現。
與 #A-87 ⑨(merchantId 同名語意不一致)之教訓一致——工具不可取代端到端實測。


2. 實測證據(2026-09-23,真機;CARDCORE_SECURITY_ENABLED=true)

取 token 方式:cardcore-web(public client,未開 DAG)→ 以 Authorization Code + PKCE 取得 demo.csr 之 token
(azp=cardcore-web、role cardcore-csr;與 tools/verify_sso_pkce.py 同一協定)。
測試卡取自 CC_CARD.CARDS(實測 16 列)。

2.1 F1 threeFactors 契約宣告但實作未接收 → 三要素驗證完全缺失

契約原文(card-service.yaml,TransitionRequest):

        threeFactors:
          type: object
          nullable: true
          description: 開卡三要素(身分證字號、生日、核准序號)

端點描述原文:"狀態轉換經狀態機(SDD-005);快發須 threeFactors;非法轉換回 CC-CARD-3001/3002;三要素不符回 CC-CARD-1002。"

實作:CardController.TransitionRequest 有 Map<String,String> threeFactors 元件,但
CardQueryService.transition(cardId, event, operatorId, reason) 完全不接收該參數
→ 介面層收到後未傳入,屬靜默丟棄。

實測(A2):對 INACTIVE 卡送出明顯錯誤之三要素 → 201 成功

POST /api/v1/cards/0ec94427-9980-44ed-9077-0ba6b4484cb1/lifecycle-transitions
  X-Channel: WEB ; X-Idempotency-Key: c14-b4-a2-0002
{"event":"ACTIVATE","reason":"實測 A2(三要素錯誤)","operatorId":"CSR001",
 "threeFactors":{"idNo":"Z999999999","birthDate":"1900-01-01","approvalNo":"NOT-EXIST"}}

→ HTTP 201
{"data":{"cardId":"0ec94427-…","cardNoMasked":"411111******2599",
         "blockCode":"ACTIVE","status":"UNACTIVATED","transactable":true,…},
 "success":true}

判定:idNo=Z999999999(非本國身分證格式)、birthDate=1900-01-01、approvalNo=NOT-EXIST
皆不可能通過驗證,卻仍開卡成功。→
CC-CARD-1002 於此路徑不可達;SDD-005 §2.1 轉換 1 之前置條件「三要素驗證通過」未落地。

⚠️ 對照:同一服務之 快發快綁(POST /cards/{cardId}/instant-issuance)有接收 threeFactors
並轉為 ThreeFactors 值物件(CardController L194-207)→ 同一業務概念在兩端點行為不一致。

2.2 F2 X-Channel/X-Idempotency-Key 契約宣告為必要,實作未綁定

契約原文(該端點 parameters):

        - $ref: "#/components/parameters/ChannelHeader"
        - $ref: "#/components/parameters/IdempotencyHeader"

實作(CardController.transition)僅有 @PathVariable cardId + @RequestBody,無任何 @RequestHeader。

實測(A1):完全不帶兩個標頭 → 201 成功

POST /api/v1/cards/2519efc2-0d15-4d71-97bb-f66dea114d84/lifecycle-transitions
  (無 X-Channel、無 X-Idempotency-Key)
{"event":"ACTIVATE","reason":"C-14 批次四 實測 A1(不帶標頭、不帶三要素)"}

→ HTTP 201(開卡成功)

判定:(i) 契約宣告之必要標頭實為選用——契約過度宣告(consumer 照契約送無害,但契約失真);
(ii) 更實質之影響——冪等鍵無效:consumer 依契約帶 X-Idempotency-Key 期待「重送不重複」,
實則完全被忽略;重送之結果僅靠狀態機擋下(ACTIVE → ACTIVATE 為非法轉換 → 409),
屬巧合而非設計(見 §2.3 之 F3:查無卡片亦回 409,兩者無法區分)。

對照:同服務之 POST /cards(發卡)、POST /card-applications 皆 @RequestHeader 綁定兩者;
POST /cards/{cardId}/instant-issuance、POST /cards/{cardId}/tokens 綁定 X-Channel 但未綁定 X-Idempotency-Key。

2.3 F3 查無卡片回 CC-CARD-3001(409) 而非 404——同服務內語意不一致

實測(B):對不存在之 UUID 呼叫 →

POST /api/v1/cards/00000000-0000-0000-0000-0000000000FF/lifecycle-transitions
→ HTTP 409
{"error":{"code":"CC-CARD-3001","message":"查無此卡片:00000000-0000-0000-0000-0000000000ff",…}}

判定:

  • CC-CARD-3001 之語意為「狀態機衝突」;以之表示「查無資源」語意不符;
    契約亦明列 404: 資源不存在、409: 衝突(冪等鍵衝突/樂觀鎖衝突/狀態衝突) → 實作回 409 與契約語意相悖。
  • ★ 同服務內已有正確前例:CardQueryService.findById 於 HANDOVER #C-10 已將「查無」由
    CC-CARD-3001 修正為 CC-CARD-4040(404),其 javadoc 明載理由「查無資源應為 404」。
    惟 transition() 未同步修正 → 同一服務內兩套真相。
  • 併發後果:consumer 無法區分「卡片不存在」與「狀態轉換非法」(皆 409、皆 CC-CARD-3001)
    ——對照 §2.2 之「重送靠狀態機擋下」,此區分能力正是冪等語意所依賴者。

2.4 F4 ACTIVATE 未同步 cards.status → 產生 block_code=ACTIVE 卻 status=UNACTIVATED 之組合

DDL(card-service/V1__baseline.sql,兩者為不同維度):

  block_code  VARCHAR2(16) DEFAULT 'INACTIVE'    NOT NULL CHECK (block_code IN ('INACTIVE','ACTIVE','LOCKED','LOST','REISSUED','EXPIRED','CLOSED')),
  status      VARCHAR2(16) DEFAULT 'UNACTIVATED' NOT NULL CHECK (status     IN ('UNACTIVATED','ACTIVE','SUSPENDED','CLOSED')),

兩條寫入路徑(JdbcCardRepository):

路徑 SQL 更新欄位
生命週期轉換(updateBlockCode) SET block_code = ?, updated_at = …, version = version + 1 僅 block_code
快發快綁(UPDATE_INSTANT_SQL) SET block_code = ?, status = ?, instant_issued = 1, … block_code + status

實測(DB 複核,CC_CARD.CARDS 全表分群):

BLOCK_CODE|STATUS|筆數        ← 實測後
INACTIVE  |UNACTIVATED|9
ACTIVE    |ACTIVE     |4      ← 快發快綁路徑產生(含種子)
LOCKED    |SUSPENDED  |1
ACTIVE    |UNACTIVATED|2      ← ★ 本次 A1/A2 之 ACTIVATE 產生;**測試前此組合為 0 列**

判定:同一業務事件(「開卡」)因路徑不同而結果不同——

  • 經生命週期轉換:ACTIVE / UNACTIVATED
  • 經快發快綁:ACTIVE / ACTIVE

SDD-005 §2.1 轉換 1 之副作用為「寫入開卡時間、發 card.card.status-changed.v1」
——事件名即為 status-changed,而 status 竟未變。
→ 建議採 (a) 轉換 1 同步 status=ACTIVE(與快發快綁對齊、與事件名一致)。

⚠️ 對 WebUI 之直接影響:CardStatusView 同時回傳 blockCode 與 status;
若畫面兩欄並列,使用者將看到「ACTIVE / 未開卡」之矛盾組合。
本批之 SDD-007 §5.4.16 已明訂呈現規則(見 §3.4)。


3. 建議修法(逐項,供一次裁定)

# 缺陷 建議 理由 風險
F1 threeFactors 未接收 (a) 轉換 1(ACTIVATE)補實作三要素驗證,不符回 CC-CARD-1002;(b) 契約加註「本期未實作」 (a) 較正確——SDD-005 §2.1 已明列前置條件,且快發快綁已有可用之 ThreeFactors 與驗證邏輯,非從零開發;(b) 僅為契約誠實化,仍留「開卡不需驗證」之實質漏洞 (a) 需確認 threeFactors 之權威來源(card_applications.approval_no + 持卡人 id_no/birth_date)——屬跨服務(party)查詢,須確認呼叫路徑
F2 標頭未綁定 (a) 契約移除該端點之 IdempotencyHeader(承認不支援冪等);(b) 實作補上 @RequestHeader 並落實冪等;(c) 維持現狀 (a) 為最低限度之誠實化(成本最低、立即消除契約失真);(b) 最完整但工作量最大(需 card_idempotency 表之既有機制,該表已存在);(c) 不建議——留「契約說有、實際沒有」之靜默陷阱 (a) 為契約縮減,須確認無 consumer 依賴;(b) 須處理「同鍵重送之回應重播」語意
F3 查無卡片回 409 改回 CC-CARD-4040(404),與 findById 對齊 findById 已有同一修正之前例(#C-10),理由與作法皆現成;且為 F2(b) 冪等語意之前提 極低——但須先查證無 consumer 依賴原 409(#C-10 之查證方式可直接沿用)
F4 status 未同步 (a) 轉換 1 同步 status=ACTIVE;(b) 廢除 status 欄位(單一真實來源);(c) 維持,但契約/畫面明訂「兩維度、各自獨立」 (a) 建議——與快發快綁一致、與 status-changed 事件名一致;(b) 牽涉 LOCKED↔SUSPENDED 之對應(見下)不建議本期做 (a) 須確認 status 之其他轉換是否亦需同步(LOCK → SUSPENDED?CLOSE → CLOSED?)——若只修 ACTIVATE,將再產生新的不一致

⚠️ F4 之關鍵細節(不可只修一半):實測種子資料顯示 LOCKED|SUSPENDED 成對存在,
故 status 並非「僅開卡時同步」——LOCK/CLOSE 亦應有對應。
若採 (a),須一次定義 7 個事件各自之 status 映射(或明確宣告「僅 ACTIVATE 同步,其餘不變」並說明理由),
否則即為局部修正造成新不一致。


4. 與既有項目之關係

既有項 關係
CR-019 批 2(C-1) 同型(契約宣告之請求欄位、實作未接收→靜默丟棄)。C-1 為 authorization-service、本 CR F1 為 card-service;建議一併裁定(同一原則)
CR-019 C-3(TransitionRequest.operatorId 契約補欄) 互補——C-3 已使 operatorId 納入契約(實作確有接收並用於日誌);本 CR F1 則為「實作未接收」之反向案例。兩者並存顯示:同一個 record 內,有的欄位有接、有的沒接
GOV-006 §4 #17(AMOUNT 設計意圖) 同屬「不可由工具或機械推論決定」之帳務/語意類裁定
#A-87 ⑨(merchantId 同名語意不一致) 同屬「工具測不到、須實測」之類型

5. 誠實揭露(我方施行中之副作用與限制)

  1. ⚠️ 本次實測改變了測試資料狀態(可回退):為驗證 F1/F2,對 2 張 INACTIVE 卡執行 ACTIVATE:
    • 2519efc2-0d15-4d71-97bb-f66dea114d84(411111******0148)→ block_code=ACTIVE(status 仍 UNACTIVATED)
    • 0ec94427-9980-44ed-9077-0ba6b4484cb1(411111******2599)→ 同上
      → 此為 F4 之實證來源,同時是本次實測之副作用。回退方式(若需):
      UPDATE cards SET block_code='INACTIVE' WHERE id IN (…)(本機 dev 資料,未執行)。
      教訓:以「寫入型端點」作查證時,須先記下變更前狀態方可回退。
  2. 未驗證之範圍:REISSUE/EXPIRE 等其餘 6 個事件未逐一實測(僅驗 ACTIVATE/LOCK/查無);
    F4 之「其餘事件之 status 映射」亦未查證(已列為 §3 之待確認項)。
  3. 未查證 consumer 依賴:F3 之「改 404」是否影響既有呼叫端,尚未盤點(#C-10 之查證方式可沿用)。
  4. 本 CR 尚未修改任何程式或契約——僅登記與建議,待 BEAM PMO 裁定。

6. 待裁定(彙總)

# 議題 選項
F1 三要素驗證 (a) 補實作 / (b) 契約加註未實作
F2 標頭與冪等 (a) 契約移除冪等宣告 / (b) 補實作 / (c) 維持
F3 查無卡片之錯誤碼 改 CC-CARD-4040(404)/維持 CC-CARD-3001(409)
F4 status 同步 (a) 補同步(須定義全事件映射)/ (b) 廢欄位 / (c) 明訂兩維度獨立

我方建議之最小可行組合:F3 → 改 404(成本最低、有前例、且為 F2(b) 之前提);
F1 → 先 (b) 契約加註(避免繼續誤導)+ 另立後續項做 (a);
F2 → (a) 契約移除冪等宣告(消除靜默陷阱)+ 後續項評估 (b);
F4 → (a) 但須先定義全事件之 status 映射(不可只修 ACTIVATE)。


7. 執行結果(2026-09-23,BEAM PMO 裁定後執行)

BEAM PMO:「不能用 walk around 的心態處理,以免造成未來接手的開發團隊又有別的方法
然後推翻現在的解決方案又重來一遍。」
→ 故四項全部修根因,不採「契約加註本期未實作」之縮手方案。

7.1 裁定與落地

# 裁定 落地方式
F1 A 加 card_id 關聯(最嚴謹) V7__card_application_card_id.sql 加 CARD_APPLICATIONS.CARD_ID+索引;CreateCardRequest 新增 applicationId(發卡時回填關聯);CardQueryService.transition 於 ACTIVATE 時以 findByCardId 反查核准序號並確實驗證三要素,不符/查無案件回 CC-CARD-1002
F2 B 補實作(確實綁定+落實冪等) CardController 加 @RequestHeader X-Channel/X-Idempotency-Key;transition() 複用既有 IdempotencyStore(指紋=cardId|event|reason|operatorId),同鍵同指紋重放、同鍵異指紋回 CC-CARD-2001(鑑別值 CARD_LIFECYCLE_TRANSITION)
F3 改 CC-CARD-4040(404) 全庫搜尋無任何服務端呼叫端(僅前端),故無破壞風險;與 findById 一致
F4 B 完整映射表 新增 CardStatus 列舉;映射內建於 CardBlockCode#status()(switch 運算式之窮盡性檢查=新增區塊碼未定義對應 status 時編譯即失敗);Card#transition 重算 status;JdbcCardRepository.updateBlockCode 不收 status 參數(一律由 blockCode 推導,呼叫端無從傳錯)→ 兩條寫入路徑現皆同步

7.2 真機實測(9 項,全部符合預期)

# 情境 結果
1 F3 查無卡片 CC-CARD-4040/404(修正前 CC-CARD-3001/409)
2 F2-a 完全不帶 X-Channel/X-Idempotency-Key CC-PLT-4001/400「缺少必要標頭:X-Channel」(修正前 201)
3 F1 該卡無來源申請案件 CC-CARD-1002/422(修正前 201 開卡成功)
4 發卡帶 applicationId 201,blockCode=INACTIVE/status=UNACTIVATED
5 F1 開卡但核准序號錯誤 CC-CARD-1002「開卡三要素驗證未通過」(修正前 201)
6 F1+F4 開卡三要素正確 201,blockCode=ACTIVE/status=ACTIVE
7 F2-b 同鍵同指紋重放 回當前狀態(ACTIVE/ACTIVE),不重複轉換
8 F2-c 同鍵異指紋 CC-CARD-2001
9 F4 LOCK blockCode=LOCKED/status=SUSPENDED,transactable=false

DB 複核:CARDS 為 LOCKED|SUSPENDED;CARD_APPLICATIONS.CARD_ID 已回填
(LINKED 由 0 → 1);CARD_IDEMPOTENCY 出現 CARD_LIFECYCLE_TRANSITION 2 筆。

7.3 單測與負樣本

新增 CardLifecycleTransitionTest(10 例,涵蓋 F1-a~f/F2-a,b/F3/F4)。
★ 負樣本驗證:暫時回退 F4(next.status() → status)後重跑 →
F1-a 與 F4 兩例精準轉紅、其餘 8 例仍綠 → 證明測試確實鎖住該行為,非附帶通過。

7.4 ⚠️ 施行後之連帶影響(誠實揭露)

  1. 既有種子/測試卡多數無來源申請案件 → ACTIVATE 將回 CC-CARD-1002(fail-closed,刻意)。
    實測 16 張卡中僅 5 張能對上;V7 後僅於「發卡時帶 applicationId」之新卡才會建立關聯。
  2. 前端規格須同步更新(否則為過時記載):SDD-007 §5.4.16 原載「不提供三要素輸入」
    ——修正後即應提供,否則變成「功能已存在而畫面不給用」。已於 SDD-007 1.13 更正。
  3. 未驗證範圍:其餘 6 個生命週期事件(UNLOCK/REPORT_LOST/REISSUE/EXPIRE/CLOSE
    之組合)未逐一實測其 status 映射(單測僅涵蓋 ACTIVATE/LOCK/CLOSE)。

8. 修訂記錄

版本 日期 修訂內容
1.0 2026-09-23 初版:C-14 批次四動工前查證所發現之四項缺陷(F1 三要素未接收/F2 標頭未綁定/F3 查無卡片回 409/F4 status 未同步),含真機實測證據、DB 複核、逐項建議修法與待裁定彙總
1.1 2026-09-23 BEAM PMO 裁定「不得以 workaround 心態處理」→ 四項全部修根因並執行完畢:F1 採 A(V7 加 CARD_APPLICATIONS.CARD_ID)/F2 採 B(確實綁定+冪等生效)/F3 改 CC-CARD-4040(404)/F4 採 B(完整映射表,內建於 CardBlockCode#status())。新增 §7 執行結果(真機實測 9 項+DB 複核+單測 10 例+負樣本)與 §7.4 連帶影響。SDD-005 → 1.8(新增 §2.4 衍生關係)/SDD-007 → 1.13/SDD-001 → 1.14/SCS-003 → 1.5/GOV-006 → 1.21/HANDOVER → 1.70(#A-92)
    (1-1/1)