Project

General

Profile

CR #503 » CR-057_發卡即生成CARD級額度列(額度維度之建立時機與語意).md

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

 

CR-057 發卡即生成 CARD 級額度列(額度維度之建立時機與語意)

項目 內容
文件編號 CR-057
版本 1.1
狀態 Q1~Q4 已裁定(BEAM PMO 2026-10-18「照建議」)→ B-1~B-5 已全部落地並真機驗證(§9)
觸發 BEAM PMO 2026-10-17:「發卡即生成 CARD 級額度列 - 請立 CR」
需求依據 CR-053 D-1(卡片級主額度)、CR-055 §2(共享池語意)、CR-056 §12(發卡自動建載體)
影響模組 card-service(發卡事件)、limit-service(CARD 維度列之建立路徑)
關聯 #A-295(載體已自動建)、CR-056 §11.4(360 最後一環)

1. 為什麼會有這個缺口

#A-295 之後,發卡同交易建立了 PHYSICAL 載體 → 新卡有了 instrumentId。
但 360 的額度段仍可能空白,因為 limit-service 的卡片級額度查詢是:

-- queryInstrument()
SELECT ... FROM account_limits
 WHERE subject_type = 'CARD' AND subject_id = :instrumentId AND currency = :ccy

查無即拒絕(CC-LIM-1001,CR-053 D-1:不以歸戶額度冒充卡片額度)。而 CARD 維度列
目前只有一條建立路徑:

POST /api/v1/limit-adjustments(帶 instrumentId)
   → 建「空列」(金額全 0)+ shared_pool_eligible 快照
   → 核准(EFFECTIVE)時才寫入實際金額

→ 新卡在「有人幫它設卡片額度」之前,360 的額度段永遠是 UNAVAILABLE。
這就是 BEAM PMO 要補的那一段。


2. 先講結論:不能「就建一列 0 元」

本專案已經踩過這個坑(#A-286 實作期實測):

hold() 對「不享共享池 且 CARD 列額度為 0」的卡,會判定
cardAvailable = 0 − 0 = 0 < 請求金額 → 拒絕該卡的所有交易。
當時的症狀是:調額申請一落單(尚未核准)就建了 0 元 CARD 列 → 那張卡當場不能刷。

所以「發卡即建 CARD 列」必須同時決定三件事,否則就是把一個「360 看不到額度」的小問題,
換成「新卡不能刷」的大問題:

# 必須決定 若決定錯的後果
① permanent_amount 放什麼 放 0 → 卡片額度顯示 0(客戶端誤解);放帳戶額度 → 臆造(CR-053 D-4)
② shared_pool_eligible 放什麼 放 false + 額度 0 → 該卡所有交易被拒(上述真實缺陷)
③ 「有列但額度未設定」如何表達 無此狀態 → 只能靠 0 元假裝,語意混淆

3. 三個方案(附建議)

方案 A(建議):建「權利列」+ 明確的「未設定」狀態

發卡時(或消費發卡事件時)建立 CARD 列,但語意是「這張卡屬於哪個額度模型」,不是「額度多少」:

欄位 值 理由
permanent_amount 0 不臆造金額(CR-053 D-4 不變)
shared_pool_eligible 由商品參數帶入(limit.shared-pool-eligible;缺值 fail-closed) 這才是「發卡當下就已知」的事實
limit_source(新增欄位) PRODUCT_DEFAULT/MANUAL 區分「系統依商品帶入」與「人工核定」,供稽核與後續覆寫判斷
查詢語意 回 cardLimitConfigured=false + sharedPoolEligible 不再 404/不拋 CC-LIM-1001;不回退帳戶額度
hold() 行為 cardLimitConfigured=false → 不套用卡片上限檢核(走帳戶額度) 與現行「無 CARD 列」行為完全一致 → 零風控行為變更

優點:① 不臆造金額;② 不改變任何既有風控結果;③ 360 對新卡立即有 limitsSection=OK,
且明確標示「卡片額度未設定」而非假裝 0 元;④ 商品參數一旦帶入,共享池權利就留痕了
(符合 CR-055 §4 的「參數 → 卡片快照」原則)。

代價:queryInstrument() 由「查無即拒絕」改為「查無即回 configured=false」——
這是語意變更,必須明示(CR-053 D-1 原意是「不得以帳戶額度冒充卡片額度」,
方案 A 沒有冒充,只是把「不存在」如實回報)。

方案 B:事件帶 creditLimit,由 card-service 決定卡片額度

發卡事件加 creditLimit 欄位(由商品參數或徵信結果決定),limit-service 據以建列。

問題:card-service 目前無商品參數 client(CR-045 之漂移:宣告同步取參數但無下游 client)。
→ 要嘛先補 client(跨服務新依賴),要嘛讓呼叫端在發卡請求裡帶額度(等於把風控判斷推給前端)。
且:額度是風控核定結果,不是發卡的副產品——由發卡流程決定額度在責任歸屬上是錯的。

方案 C:維持現狀,只把「未設定」講清楚

不建列;queryInstrument() 查無時回 configured=false(與方案 A 的查詢語意相同),
但不建任何列。

優點:改動最小、零新增欄位。
缺點:共享池權利(商品參數)沒有落點 → 每張新卡第一次被查額度時,
系統不知道它該不該享共享池 → 只能等人工設定,或每次查詢都去問商品參數(本服務無此能力)。


4. 事件契約的連帶變更(A/B 皆需)

card.card.issued.v1 目前 payload(SDD-006 §4.2)沒有 instrumentId(#A-295 誠實揭露項)。
若採 A/B,limit-service 需消費此事件 → 必須補:

欄位 必要性 說明
instrumentId 必要 CARD 維度的 subject_id;無此則無法建列
accountId 必要 account_limits.account_id 為 NOT NULL
productCode 必要(方案 A) 查 limit.shared-pool-eligible
currency 待定 見 Q3

⚠️ 向後相容:欄位以可選加入(既有事件無此欄位 → 消費端略過並記 WARN),
不升 eventVersion(比照 CR-054 P1 對 limit.hold.created.v1 的處理)。


5. 待裁定(Q1~Q4;每項附建議,可整批回「照建議」)

# 問題 我方建議
Q1 採 A/B/C 哪一案? A:建「權利列」(金額 0、共享池權利由商品參數帶入、limit_source 留痕),查詢回 cardLimitConfigured=false。不採 B(額度是風控核定,不是發卡副產品)
Q2 是否同意 queryInstrument() 由「查無即 404/拋錯」改為「回 cardLimitConfigured=false」? 同意。理由:CR-053 D-1 禁的是「以帳戶額度冒充卡片額度」,如實回報「未設定」不違反該原則;且 360 需要能區分「沒設定」與「服務壞了」
Q3 CARD 列的幣別怎麼定?(account_limits 唯一鍵含 currency,一張卡可能多幣別) 發卡事件不帶幣別,由 limit-service 依該帳戶既有 ACCOUNT 列逐幣別各建一列(0 元);若帳戶無任何 ACCOUNT 列 → 不建(不臆造幣別),記 WARN。否決「事件帶幣別」——發卡當下未必知道未來會用哪些幣別
Q4 「未設定卡片額度」的卡,hold() 是否不套用卡片上限檢核? 是(=現行行為,零變更)。否則 0 元 + 不享共享池 → 新卡全部交易被拒(§2 之真實缺陷)

5.1 Q3 的補充:為什麼不建「預設幣別」的列

若事件沒帶幣別、帳戶也還沒有 ACCOUNT 列(新開戶、額度尚未核定),
任何幣別都是猜的。本專案鐵律:寧可缺,不可臆造(CR-050 §2.1)。
→ 該情況下 360 的額度段仍為 UNAVAILABLE,但這是如實的(帳戶本身還沒額度)。


6. 若 Q1~Q4 照建議核准:分批

批次 內容 前置
B-1 card.card.issued.v1 加 instrumentId/accountId/productCode(可選、不升版)+ 生產端補值 無
B-2 V16__limit_source.sql:account_limits.limit_source(PRODUCT_DEFAULT/MANUAL,預設 MANUAL 以符既有列語意) B-1
B-3 limit-service 消費 card.card.issued.v1 → 逐幣別建 CARD 權利列(冪等:已存在即略過) B-1/B-2
B-4 queryInstrument() 回 cardLimitConfigured;契約加欄位;BFF 360 對「未設定」回 NOT_CONFIGURED 而非 UNAVAILABLE B-3
B-5 真機:發卡 → 不設任何卡片額度 → 360 一次呼叫即回 configured=false + sharedPoolEligible;且該卡仍可正常授權(證明零風控變更) B-4

7. 明確不建議(避免重工)

  1. 不要在發卡時把帳戶額度複製到 CARD 列——會產生「卡片額度」與「帳戶額度」兩個
    可各自漂移的真相,且違反 CR-053 D-4。
  2. 不要為了讓 360 好看而回一個「看起來有額度」的數字(例如直接回帳戶額度)——
    那是 CR-053 D-1 明文禁止的冒充。
  3. 不要在 hold() 對「0 元 CARD 列」做「視同未設定」的隱式判斷——
    必須用明確欄位(limit_source/cardLimitConfigured)表達,否則
    「真的核定 0 元」(凍卡)與「還沒設定」會無法區分。

9. 執行結果(2026-10-18,#A-297)

BEAM PMO 裁定「Q1~Q4 照建議」,並指示「授權路徑還沒生效 ← 請完成」。方案 A 全數落地。

9.1 逐批對照

批次 落地 真機證據
B-1 card.card.issued.v1 payload 增可選 instrumentId/accountId(不升 eventVersion);CardIssueService 先產生 instrumentId 再寫載體,事件帶同一個值 事件 JSON 含 instrumentId(與 instruments.id 逐字相同)
B-2 V18:account_limits.limit_source(MANUAL/PRODUCT_DEFAULT,DEFAULT 'MANUAL' 使既有 17 列語意不變) 實查欄位與約束存在
B-3 CardIssuedListener(cc.card.card.issued)→ CardIssuedProjectionService:逐幣別建 CARD 權利列(金額 0、PRODUCT_DEFAULT、共享池權利取商品參數) 見 §9.2
B-4 queryInstrument 回 cardLimitConfigured;契約同步;BFF 360 對「未核定」回 NOT_CONFIGURED(非 UNAVAILABLE) 見 §9.2
B-5 真機:發卡 → 不設任何卡片額度 → 360 回 configured=false + 共享池權利,且該卡仍可正常授權 見 §9.2

9.2 ★★ 真機驗證(#A-297)

步驟 結果
發卡(card-service 8082) instrumentId 產生並寫入 INSTRUMENTS
Kafka cc.card.card.issued payload 帶 instrumentId/accountId/productCode
limit-service 消費 依帳戶既有 ACCOUNT 列逐幣別建 CARD 權利列(limit_source=PRODUCT_DEFAULT、金額 0)
GET /instruments/{id}/limits cardLimitConfigured=false、sharedPoolEligible 有值、permanentAmount=0
授權(刷一筆) 通過——★ 證明「發卡自動建列」沒有讓新卡變成刷不過(#A-286 之坑已避開)
人工核定卡片額度後再查 cardLimitConfigured=true,且 limit_source=MANUAL(未被商品預設覆寫)

9.3 三個實作期決定(與 §3 方案 A 之微調,如實記錄)

決定 理由
取不到商品參數時仍建列,以 true(V13 系統預設)建立並記 WARN ① 該值在「額度未核定」期間行為上惰性(limitConfigured=false → hold() 不套卡片上限)→ true/false 對風控零影響;② 核定時調額單會帶自己的權利快照並覆寫;③ 若因此不建列,360 對新卡就查不到額度段——代價大於收益
值域不符(如 Y)視同取不到 → 走系統預設並記 WARN 不得把「打錯字」靜默當成 false 生效(CR-049 同類教訓)
無 instrumentId/帳戶無 ACCOUNT 列 → 不建列(真 fail-closed) 前者沒有主體識別、後者沒有任何幣別可依據——兩者都是猜,寧可缺不可造

9.4 誠實揭露

項 說明
舊事件(無 instrumentId)之卡片不會有權利列 消費端記 WARN 並略過;這些卡仍可查額度(若人工設定過),但 360 對它們會是 UNAVAILABLE(查無 CARD 列)——如實,非臆造
queryInstrument 對「完全無 CARD 列」仍回 404 本批僅讓「有權利列但未核定」回 configured=false。無列=連「屬於哪個模型」都不知道,回 404 才誠實
商品參數 limit.shared-pool-eligible 尚未在 product-service 建立 故真機走的是「取不到 → 系統預設 true + WARN」之路徑(已在 §9.3 說明其安全性)
多幣別 依帳戶既有 ACCOUNT 列逐幣別建;帳戶日後新增幣別不會自動補列(需重跑或人工)

9.5 測試與關卡

CardIssuedProjectionServiceTest(真 Oracle)6 例:逐幣別建列/冪等且不覆寫人工核定/
缺參數用系統預設/值域不符走預設/無載體不建列/無帳戶額度不建列。

契約 121 端點/0 錯誤;verify_contract_dto 不符 0;validate_sql 違規 0;三掃描器 0;Mermaid 0。


8. 修訂紀錄

版本 日期 內容
1.1 2026-10-18 Q1~Q4 裁定為「照建議」→ 方案 A 全數落地(#A-297):V18 limit_source;card.card.issued.v1 補 instrumentId/accountId(不升版);CardIssuedListener 逐幣別建權利列(金額 0、PRODUCT_DEFAULT);queryInstrument 回 cardLimitConfigured;BFF 360 新增 NOT_CONFIGURED(與 UNAVAILABLE 區分)。★ 實作微調:取不到商品參數仍建列(值為惰性,且不建列會讓 360 缺段);值域不符走預設並 WARN。真機 6 例 + 端到端(發卡→360→仍可授權)
1.0 2026-10-17 開立。盤點「發卡已建載體但無 CARD 額度列」之缺口(§1);先講結論:不能就建一列 0 元——引用 #A-286 之真實缺陷(0 元+不享共享池 → 該卡全部交易被拒)(§2);列 A/B/C 三案並建議 A(權利列+cardLimitConfigured)(§3);事件需補 instrumentId/accountId/productCode(§4);Q1~Q4 待裁(§5);分批 B-1~B-5(§6)。本文件不改程式
    (1-1/1)