# 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` 的卡片級額度查詢是：

```sql
-- 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）。**本文件不改程式** |
