# CR-056　身分證字號 **360 查詢**（跨帳戶／跨企業之卡片視角）

| 項目 | 內容 |
|---|---|
| 文件編號 | CR-056 |
| 版本 | **1.4** |
| 狀態 | **360 全鏈路已通並真機驗證**：身分證 → party → 卡 → **額度**，**BFF 一次呼叫**取得（§11）。真實客戶資料待提供 |
| 觸發 | BEAM PMO 2026-10-17：「由身分證字號查詢時，要能把張三在 Z 公司的兩張卡訊息帶出來，**這一點很重要**」 |
| 需求依據 | `CR-055 §1`（自然人／公司戶額度隔離之同一情境） |
| 影響模組 | `card-service`（查卡清單）、`party-service`（`id_no_hash`）、`bff-web`（聚合） |
| 關聯 | `CR-055`（維度與隔離）／`CR-052 C-5`（無 card→instrument 對映） |

---

## 1. BEAM PMO 之情境（原文要點）

> 張三在 A 銀行有個人信用卡帳戶，總額度 **5 萬**（不論幾張主卡／附卡）。
> Z 公司也在 A 銀行有公司戶，總額度 **100 萬**；張三持有 Z 的兩張公司卡：
> **差旅卡 10 萬**、**採購卡 30 萬（限資產類消費）**。
> **張三在 A 銀行的帳戶總額度仍是 5 萬**（個人與公司額度**不混算**）。
> 但**由身分證字號查詢時，要能把張三在 Z 公司的兩張卡訊息帶出來**。

---

## 2. 現況盤點（實查）

| # | 事實 | 影響 |
|---|---|---|
| F-1 | `cards` 有 `account_id`／`party_id`／`product_code` | ✅ 卡的歸屬與產品可查 |
| F-2 | **`card-service` 原本無「依帳戶／依持卡人查卡」端點**（僅 `byId`／`byCardNoHash`） | ❌ 360 查詢無從起手 → **本 CR 已補**（§3） |
| F-3 | `parties` 表**綁 `account_id`**（`account_id NOT NULL REFERENCES accounts(id)`） | ★★ 同一自然人在**不同帳戶＝不同 party 記錄** |
| F-4 | `parties` 之身分證僅有 `id_no_enc`（**加密 BLOB**）與 `id_no_masked`（**遮罩**） | ★★ **無可比較鍵**：加密無法直接比對、遮罩不可作比較鍵（專案鐵律） |
| F-5 | **無 `id_no_hash`** | ★★ **跨帳戶 360 查詢目前技術上不可行** |

### 2.1 為何「依 partyId 查卡」不足以滿足 BEAM PMO 之要求

```
張三（個人帳戶）  → party 記錄 P1（id_no_enc 同一身分證）
張三（Z 公司帳戶）→ party 記錄 P2（**另一筆**，同身分證、不同 account_id）
                        ↓
cards.party_id = P1 → 個人卡；cards.party_id = P2 → 公司卡
```

* `GET /api/v1/cards?partyId=P1` → **只得個人卡**（BEAM PMO 要的兩張公司卡拿不到）。
* 要「一次帶出全部」→ 必須先由**身分證字號**解析出 **P1 與 P2 兩個 partyId** → 需 `id_no_hash`。

---

## 3. 已落地（第一步，`#A-290`）

| 項目 | 內容 |
|---|---|
| 端點 | `GET /api/v1/cards?accountId=…`／`?partyId=…`（**至少一項**，皆缺 → `CC-PLT-4003`） |
| 回應 | `CardSummaryView[]`（cardId／accountId／partyId／cardNoMasked／productCode／bin／expiryDate／status／blockCode／instantIssued） |
| 守界 | **不含**卡號雜湊與明碼；**不含**額度（權威在 `limit-service`） |
| 已支援 | ① **自然人情境**：以 `accountId` 查即該帳戶所有主／附卡（額度主體＝帳戶，`CR-055 §6`）<br>② **已知 partyId** 之公司卡查詢 |

★ 這一步**不足以**完成 BEAM PMO 之「身分證 → 全部卡」，但它是必要組件（有了卡清單，才能逐卡查額度）。

---

## 4. 待裁定：`id_no_hash`（**資安決策，不擅自實作**）

### 4.1 建議方案

| 項 | 建議 |
|---|---|
| 欄位 | `parties.id_no_hash CHAR(64)`（SHA-256 hex）＋索引 `(id_no_hash)` |
| 計算 | 建檔／換證時以**正規化後**之身分證字號計算（去空白、統一大寫） |
| **是否加鹽** | ★ **建議加「系統級固定鹽」**（存於 KeyProvider，不入 DB）——避免彩虹表；但**不可用隨機鹽**（隨機鹽無法跨記錄比對，失去查詢用途） |
| 回填 | 既有資料需**解密 `id_no_enc` → 重算 hash**（一次性 Java 回填程式，需 KeyProvider）；⚠️ 不可用 SQL 直接算（DB 無金鑰） |
| 查詢端點 | `GET /api/v1/parties?idNo=…`（**明文入、內部 hash 比對、不回傳 hash**）——⚠️ 需授權與稽核（見 §4.3） |
| 360 聚合 | BFF：`parties?idNo` → 得所有 partyId → `cards?partyId=` 逐個 → `limit/instruments/{id}/limits` 逐卡 |

### 4.2 為何**不能**用既有欄位替代

| 替代方案 | 為何不可行 |
|---|---|
| 用 `id_no_masked` 比對 | ★ **遮罩值不可作比較鍵**（專案鐵律；且 `A123****89` 大量碰撞） |
| 解密 `id_no_enc` 後全表掃描比對 | 每次查詢需解密全表 → 效能與資安皆不可接受 |
| 用 `party_no` 比對 | `party_no` 為**每帳戶**獨立編號，跨帳戶不同 |

### 4.3 需 BEAM PMO 裁定之三點

| # | 問題 | 建議 |
|---|---|---|
| **H-1** | 是否同意新增 `id_no_hash`（含系統級固定鹽）？ | **建議同意**——這是業界標準做法，且**不影響**既有加密欄 |
| **H-2** | 是否允許「以**明文身分證**查詢」之 API？ | **建議允許但嚴格管制**：限客服角色、每次查詢寫稽核軌跡（誰查誰、何時、結果筆數）、回應**不含** hash |
| **H-3** | 既有資料回填之授權（需解密） | 建議以一次性維運程式執行（本機可先做），並記錄回填筆數 |

> ⚠️ **未裁定前不動**：不加欄、不回填、不開明文查詢 API。
> 理由：`id_no_hash` 涉及個資處理政策（可比較鍵＝可關聯），屬資安決策，**不應由實作端自行決定**。

---

## 5. 分批

| 批次 | 內容 | 依賴 |
|---|---|---|
| **第一步** | 查卡清單端點（依帳戶／持卡人） | ✅ 已完成 |
| **第二步** | `id_no_hash` 欄位＋建檔計算＋回填 | H-1／H-3 |
| **第三步** | `parties?idNo=` 查詢端點（含稽核） | H-2 |
| **第四步** | BFF 360 聚合（卡清單＋逐卡額度＋所屬企業） | 第二、三步 |

---

## 6. 順帶記錄：採購卡之「限資產類消費」

BEAM PMO 提到採購卡「**只能限制在資產類的消費**」——此為**用途限制（MCC 白名單）**，
現行可由 `special_limits.scope_ref`（MCC 白名單，逗號分隔）承載（`CR-035 §2.1`）。

⚠️ 但 `special_limits` 為**帳戶層**；若客戶要求「**卡片層**之用途限制」，需另立資料模型
（`cards` 或 `account_limits(CARD)` 加用途限制欄）。**本 CR 不動**，僅記錄待辦。

---

## 8. H-1／H-2／H-3 執行結果（2026-10-17，`#A-291`）

BEAM PMO 2026-10-17 核准：**H-1 同意**（業界標準、360 視角重點）／**H-2 允許但嚴格管制**／**H-3 一次性維運程式並記錄筆數**。

### 8.1 H-1：`id_no_hash`（`V5`）

| 項 | 實作 |
|---|---|
| 欄位 | `parties.id_no_hash CHAR(64)`＋索引 `ix_parties_id_no_hash` |
| 計算 | `SHA-256(系統級固定鹽 ∥ 正規化字號)`；正規化＝**去空白＋轉大寫** |
| 鹽值 | `cardcore.pii.id-no-salt`；**缺值即啟動失敗**（fail-closed，不以預設頂替） |
| 元件 | `IdNoHasher`（6 例單測：大小寫／空白變體同 hash、不同鹽不同 hash、空值拒絕、缺鹽失敗） |

### 8.2 H-2：PII 查詢端點（`POST /api/v1/party-lookups`）

★ **刻意用 POST 而非 GET**：身分證若置於 query string 會進入 **access log／瀏覽器歷史／Referer**。

| 項 | 實作／真機結果 |
|---|---|
| 權限 | 角色取自**已驗證權杖** `realm_access.roles`；不在 `cardcore.pii.lookup-allowed-roles` 即拒絕 |
| 真機（客服 `demo.csr`） | 查到 **1 筆**（`P001 王小明 A1******89`） |
| 真機（持卡人 `demo.holder`） | **403** `CC-PLT-4030`（訊息列出角色與允許清單） |
| 查無 | **空陣列**（非 404——「查無此人」與「無權限」語意不同） |
| 稽核 | `pii_access_audits` 實測 **2 筆**：`result_count=1` 與 **`result_count=0`**（★ 探測行為須可見）；記錄 actor／role／**hash 前綴（非原文）** |

### 8.3 H-3：回填——**實測 0 筆可回填（如實揭露）**

| 項 | 結果 |
|---|---|
| 工具 | `IdNoHashBackfillRunner`（`--cardcore.pii.backfill-id-no-hash=true` 啟用；可重跑、冪等、逐筆錯誤不中斷） |
| 實查 | `CC_PARTY.PARTIES` 共 **2 筆**，`ID_NO_ENC` **全部 NULL** → **候選 0 筆** |
| 結論 | 回填**無資料可做**——這不是程式缺陷，是**資料來源缺口** |

### 8.4 ★★ 根本缺口：**身分證字號從未進入系統**

| # | 事實 | 影響 |
|---|---|---|
| N-1 | party-service **只有** `GET /parties/{id}` 與 `POST /parties/{id}/flags`——**無建檔端點** | 無處接收身分證 |
| N-2 | `id_no_enc` 全為 NULL（2/2） | 無密文可解密、無 hash 可算 |
| N-3 | 現有 2 筆為 **seed 資料** | 不代表真實開戶流程 |

**→ 360 查詢目前「技術就緒、資料為空」**：端點、權限、稽核、hash 皆已備，
但**查不到任何人**（因為沒有任何 party 有 hash）。

**需 BEAM PMO 裁定（N-Q）**：身分證字號**從哪裡進來**？

| 選項 | 說明 | 影響 |
|---|---|---|
| **(a) 開戶／進件流程帶入** | 由 `card-applications`（進件）或帳戶開立時寫入 party | 需在該流程加「身分證」欄位並呼叫 party（目前無此路徑） |
| **(b) 由外部核心匯入** | 遷移時自既有系統匯入身分證（加密＋hash） | 需客戶提供匯出檔與欄位對映 |
| **(c) 客服補登端點** | 新增 `PUT /parties/{id}/id-no`（權限＋稽核） | 最快可動，但屬「事後補資料」，量大時成本高 |

★ 建議 **(a)＋(b) 並行**：(b) 解既有資料、(a) 保新資料正確；(c) 僅作例外補救。
⚠️ 未定前**不臆造**：不建假資料、不以遮罩推導原文。

### 8.5 驗證

`party-service` **68 測 0 失敗**（新增 7：6 例 hash ＋ 1 例 PII 端點 400）；
契約 **114 端點／0 錯誤**；`verify_contract_dto` 不符 0；`validate_sql` 違規 0；三掃描器 0。

---

## 9. (a)＋(b) 執行結果（2026-10-17，`#A-292`）

BEAM PMO 裁定 **(a)＋(b)**：開戶／進件帶入 ＋ 外部核心匯入。

### 9.1 補上 N-1 缺口：當事人**建檔**端點

盤點確認：本服務原本**只有查詢與旗標**、`id_no_enc` 從未寫入 → (a)(b) 皆無處落地。
故先補建檔能力，**兩個入口共用同一條路徑**（避免兩套寫入邏輯漂移）：

| 端點 | `POST /api/v1/parties` |
|---|---|
| 請求 | `RegisterPartyRequest`（accountId／partyNo／partyType／name／**idNo 明文**／birthDate／relationType） |
| 處理 | 明文僅用於 ①`FieldEncryptor` → `id_no_enc` ②`IdNoHasher` → `id_no_hash` ③`PartyIdMasker` → `id_no_masked`；**寫入後即丟棄** |
| 防重 | `partyNo` 唯一鍵（重複 → `CC-PLT-4003`「建檔不覆寫既有資料」）——故**不需冪等鍵** |
| 稽核 | `pii_access_audits` 以 `PARTY_REGISTER` 型別留痕（`V6` 擴充值域） |
| 回應 | `PartyView`（**僅遮罩**，不含 hash 與明文） |

**真機**（客服 `demo.csr`）：
* 建檔 `P900` → **201**，`idNoMasked=A1******89`
* DB 實查：`ID_NO_ENC` **65 bytes（密文）**、`ID_NO_HASH=41A3E6830E07…` ✅
* 360 查詢以 **`a123 456789`（小寫＋空白）** → 仍查到 `P900`（**正規化生效**）
* 重複建檔同 `partyNo` → **400**「建檔不覆寫既有資料」
* 稽核 **2 筆**：`PARTY_REGISTER` + `ID_NO_LOOKUP`

### 9.2 (b) 外部核心匯入

`PartyImportRunner`（`--cardcore.pii.import-parties=<csv>`）：

| 項 | 實作 |
|---|---|
| 格式 | UTF-8 CSV（表頭）：`partyNo,accountId,name,partyType,idNo,birthDate,relationType` |
| 路徑 | **逐筆呼叫 `PartyRegistrationService.register`**（與 (a) 同一條路徑） |
| 可重跑 | 已存在之 `partyNo` → **跳過並計數**（不覆寫） |
| 失敗不中斷 | 單筆失敗記錄續行；結束輸出「總筆數／成功／跳過／失敗」＝**回填紀錄** |
| 明碼 | 不複製檔案、不寫 log（失敗訊息只帶 `partyNo` 與錯誤碼）；執行後應刪檔 |

**真機**：1 筆 CSV → 日誌「**總筆數 1、成功 1、跳過 0、失敗 0**」；DB `P901` 有遮罩 `C1******89` 與 hash ✅

### 9.3 仍未做（誠實揭露）

| 項 | 說明 |
|---|---|
| **進件流程接線** | (a) 之「建檔端點」已備，但 `card-service` 之進件（`card-applications`）**仍無 party 欄位**——需在進件請求加 `partyId` 並由前端／流程先建 party。屬 `card-service` 資料模型變更，**另批** |
| **既有 2 筆 seed** | 仍無 hash（無密文可回填）——須由 (b) 匯入真實資料覆蓋，或客服補登 |
| **客服補登端點** | (c) 未做（BEAM PMO 選 a+b，c 僅列備案） |

### 9.4 驗證

`party-service` **68 測 0 失敗**；契約 **115 端點／0 錯誤**；`verify_contract_dto` 不符 0；
`validate_sql` 違規 0；三掃描器 0；Mermaid 0。

---

## 10. 進件接線＋360 端到端（2026-10-17，`#A-293`）

BEAM PMO 指示「先做完進件流程接線，再匯入真實資料把 360 跑起來」。

### 10.1 進件接線（`V10`）

| 項 | 內容 |
|---|---|
| DDL | `card_applications.party_id RAW(16)`（**可空**——V10 前案件無申請人，不得臆造回填）＋`ix_card_app_party` |
| 契約 | `CardApplicationRequest.partyId`（nullable） |
| 四層 | 領域建構子 overload（含 `partyId`）→ `received(...)` overload → service `create(accountId, partyId, …)` → controller 傳遞 |
| ★ 存在性 | **不驗證**——本服務 ACL 未允許 card→party（`CR-052 C-5`）；`party_id` 由**呼叫端保證**（開戶流程先建 party 再進件）。文件與契約皆已載明 |

**真機**：建 party `P910`（`partyId=4d2abd3f…`）→ 進件帶該 `partyId` → `201`／`APPLY_NO=APP2026101738705406`；
DB 實查 `PARTY_ID=4D2ABD3FB5BD404E8D00147310FD68E8` **與 partyId 一致** ✅

### 10.2 ★★ 360 跨帳戶端到端（張三情境）

以 `(b)` 匯入 3 筆（同一身分證、3 個帳戶）模擬 BEAM PMO 之「張三／Z 公司」情境：

| partyNo | 名稱 | 類型 | 帳戶 |
|---|---|---|---|
| P920 | 張三(個人) | PRIMARY | …0001 |
| P921 | 張三(Z公司-差旅) | COMPANY | …0002 |
| P922 | 張三(Z公司-採購) | COMPANY | …0010 |

**匯入日誌**：`★ 身分證匯入完成：總筆數 3、成功 3、跳過 0、失敗 0`

**★ 360 查詢**（`POST /api/v1/party-lookups {"idNo":"D999888777"}`）：

```
count = 3
  P920 | 張三(個人)        | PRIMARY | D9******77
  P921 | 張三(Z公司-差旅)  | COMPANY | D9******77
  P922 | 張三(Z公司-採購)  | COMPANY | D9******77
```

→ **一次查詢即帶出跨帳戶之全部記錄**（含公司卡之持卡人身份）——BEAM PMO 要求之「360 視角」成立。

**★ 可重跑（冪等）**：同檔重跑 → `總筆數 3、成功 0、跳過（已存在）3、失敗 0`，DB 筆數不變。

### 10.3 誠實揭露

| 項 | 說明 |
|---|---|
| **示範資料非真實** | §10.2 之 3 筆為**虛構示範**（已清理），僅證明鏈路；**客戶真實資料尚未提供** |
| 範本 | 已置 `ops/local/pii-import-sample.csv`（明確標示虛構）＋`ops/local/README-pii-import.md`（操作步驟） |
| **360 目前只到「人＋進件」** | 卡清單（`GET /api/v1/cards?partyId=`）與額度（`/instruments/{id}/limits`）需**實際發卡**後才有資料；本輪未走完整發卡流程 |
| BFF 聚合 | 未實作（需串 party→cards→limits 三服務）；目前由呼叫端自行串接 |

### 10.4 驗證

`card-service` **149 測 0 失敗**、`party-service` **68 測 0 失敗**；
契約 **115 端點／0 錯誤**；`verify_contract_dto` 不符 0；`validate_sql` 違規 0；三掃描器 0；Mermaid 0。

---

## 11. 360 全鏈路（BFF 聚合）＋真機（2026-10-17，`#A-294`）

BEAM PMO 指示「把 360 這條鏈路完善」。

### 11.1 補上「卡 → 載體」對映

`CardSummaryView` 加 `instrumentId`（由 `instruments.card_id` 批次取，**避免 N+1**）。
★ 卡若尚未註冊載體 → `instrumentId=null`（**該卡無額度可查**，非錯誤）。

### 11.2 BFF 360 端點（`POST /api/v1/bff/customer-360`）

| 項 | 內容 |
|---|---|
| 鍵 | 身分證**明文置於 body**（不進 URL） |
| 流程 | 身分證 → `party-lookups` → 各 party → `cards?partyId=` → 各卡 `instruments/{id}/limits` |
| ★ 權限 | BFF 為 **token-relay** 模式（`SAD-005 §3.4`）→ 轉傳**使用者權杖**，故 PII 權限由 party-service 依**使用者角色**判定（非服務身分） |
| 降級 | 各層**獨立**：某卡查不到額度 → 該卡 `limitsSection=UNAVAILABLE`，不拖垮其他；**parties 失敗**則整體 `UNAVAILABLE` 並**保留原錯誤碼**（`CC-PLT-4030` 不吞成 500） |
| 資料 | 下游**原樣轉出**（不重命名欄位）——避免 BFF 成為第二個契約真相 |

### 11.3 ★★ 真機（4 服務：party 8084／card 8082／limit 8083／bff 8101）

| 步驟 | 結果 |
|---|---|
| ① 建 party（`E111222333`） | `partyId=cfe8320a…` |
| ② 發卡 | `cardId=54665fa6…`、`411111******0098` |
| ③ 註冊載體（PHYSICAL） | `instrumentId=2a136f2c…` |
| ④ 設卡片額度 200,000（享共享池） | `EFFECTIVE` |
| **⑤ BFF 360 一次呼叫** | **`idNoMasked=E1******33`、`partyCount=1`、`partiesSection=OK`**<br>`P930 360測試` → `cardsSection=OK`<br>`411111******0098`、`instrumentId=2a136f2c…`<br>**`limitsSection=OK`：permanent=200,000、available=200,000、sharedPool=9,800,000** |

★ `sharedPool=9,800,000` ＝ 總額度 10,000,000 − 帳戶已用 200,000 − 佔用 0（**與共享池語意一致**）。

### 11.4 仍未做（誠實揭露）

| 項 | 說明 |
|---|---|
| ~~發卡不自動建載體~~ | **已補（見 §12）** |
| **BFF 僅回原始資料** | 未做欄位裁切／重命名；前端需自行解讀下游結構 |
| 真實資料 | 仍待客戶提供（範本見 `ops/local/`） |

---

## 12. 發卡自動建載體（2026-10-17，`#A-295`）

BEAM PMO 裁定：「『發卡自動建載體』這個洞（小改動，但讓 360 對新卡自動生效）← 麻煩您補上」。
本節記錄**為何這是洞**、**改了什麼**、**如何證明閉環**。

### 12.1 問題本質：`instrumentId` 是額度的唯一入口

`limit-service` 的卡片級額度查詢是 `GET /api/v1/instruments/{instrumentId}/limits`
——**鍵是 `instrumentId`，不是 `cardId`**。而 `instruments` 表在本輪之前
**只能靠 `POST /api/v1/instruments` 手動註冊**才會有人寫入。

因此新卡（未手動註冊載體）在 360 的表現是：

```
parties ✅ → cards ✅（但 instrumentId = null）→ limits ✗（無鍵可查）
```

→ **「360 對新卡不生效」不是 BFF 的問題，是發卡鏈路缺一環**。
這正是 `#A-294 §11.4` 誠實揭露的那一項。

### 12.2 改動

| 層 | 內容 |
|---|---|
| 應用層 | `CardIssueService` 注入 `InstrumentRepository`；`issue()` 在 `cardRepository.save(...)` **之後、同一交易內**建立 PHYSICAL 載體 |
| 語意 | `Instrument.register(UUID, Type.PHYSICAL, cardId, cardNoHash, null, null, accountId)`——`cardNoHash` 取**已寫入之卡片**的雜湊，確保 `card_id`／`card_no_hash` 同源 |
| ★ 失敗語意 | 建載體失敗 → **整筆回滾（含已寫入之卡片）**。理由：留一張「有卡無載體」的卡＝**假成功**，該卡在授權與 360 都缺一環且需人工補登 |
| 不變式 | `instruments` 對 `(type, natural_key)` 有唯一約束，且 `InstrumentService.register` 於寫入**前**先 `existsByNaturalKey` → 手動重複註冊得 `CC-CARD-1004`，**不會產生第二列** |

**為何不用事件驅動（非同步建載體）**：載體是**發卡的固有部分**，不是外部副作用；
非同步會產生「發卡成功、載體稍後才到」的視窗，而該視窗內 360 仍缺額度。
故採**同交易**（與 `pool.reserve` 之樂觀鎖衝突回滾語意一致）。

### 12.3 真機驗證（4 服務：card 8082／limit 8083／party 8084／bff 8101）

★ **關鍵：全程未呼叫 `POST /api/v1/instruments`**。

| 步驟 | 結果 |
|---|---|
| ① 建 party（`F111222333`） | `partyId=74363cef…` |
| ② 發卡（PLATINUM） | `cardId=8a03b324-80b2-4e8d-b4b1-fe788c08f25b`、`411111******2508` |
| ③ **DB 實查 `INSTRUMENTS`** | `B8F1A461D7804E9690B877DB9B1DBA5C`、`INSTRUMENT_TYPE=PHYSICAL`、`CARD_NO_HASH=e8badf7c03…` ✅ **自動出現** |
| ④ `GET /cards?partyId=` | `instrumentId=b8f1a461-d780-4e96-90b8-77db9b1dba5c` ✅ |
| ⑤ 設卡片額度 150,000 | `EFFECTIVE` |
| **⑥ BFF 360 一次呼叫** | `idNoMasked=F1******33`、`partyCount=1`、`P940 自動載體測試`<br>`instrumentId=b8f1a461…`<br>**`limitsSection=OK`：permanent=150,000、available=150,000、sharedPool=9,800,000** |

★ 對照 `#A-294 §11.3`：該次**必須手動**註冊載體才有 `instrumentId`；
本次**發卡即得**，360 對新卡**自動生效**。

**清理**：已刪 `LIMIT_ADJUSTMENTS`／`ACCOUNT_LIMITS` 之 CARD 列、`INSTRUMENTS`／`CARDS`、
`PARTIES`（P940）、`PII_ACCESS_AUDITS`；`parties` 筆數回到 2。

### 12.4 測試與關卡

| 項 | 結果 |
|---|---|
| `CardIssueServiceTest` | 新增 `Instruments` 替身（**其餘方法明確拋 `UnsupportedOperationException`**——避免假綠）＋4 項斷言：載體數 1、型別 PHYSICAL、`cardId` 相符、`accountId` 相符、`cardNoHash` 非空 |
| `card-service` 全套 | **BUILD SUCCESSFUL** |
| `verify_contract_dto` | 18 服務、**不符 0**、需確認 0、INFO 9 |
| `validate_sql` | 全庫 101 檔、**違規 0**（登錄檔警告 0） |
| `scan_scaffold`／`scan_mockdata`／`scan_faketest`（card-service） | **各 0** |
| `validate_mermaid` | **0 錯** |

### 12.5 仍未做（誠實揭露）

| 項 | 說明 |
|---|---|
| **`card.card.issued.v1` 未帶 `instrumentId`** | 目前**無消費端**需要（`limit-service` 之 CARD 級額度列依 `CR-055` 於**調額核准時**才建立，不隨發卡建立）→ 未改事件，避免「有人發、沒人收」。若未來要讓發卡自動生成 CARD 級額度列，**須同時**補事件欄位與消費端，屆時另立 CR |
| **虛擬卡／換卡路徑** | 現行 `CardIssueService.issue()` 是**唯一**建立 `cards` 之入口（已 grep 確認 `cardRepository.save` 僅 1 處）→ 本改動覆蓋全部發卡；未來新增換卡／補發流程時**必須**同樣建載體 |
| 卡號重用 | 載體自然鍵為卡號雜湊；卡號由號碼池配置、不重用 → 無碰撞風險。若未來開放卡號重用，需先處理載體歷史 |

---

## 7. 修訂紀錄

| 版本 | 日期 | 內容 |
|---|---|---|
| **1.5** | 2026-10-17 | **發卡自動建載體（`#A-295`）**：`CardIssueService.issue()` **同交易**建立 PHYSICAL 載體（失敗即整筆回滾，不留「有卡無載體」之假成功）。真機**全程未呼叫 `POST /instruments`**：發卡 → DB 自動出現 `INSTRUMENTS`（PHYSICAL＋`CARD_NO_HASH`）→ `GET /cards` 帶 `instrumentId` → **BFF 360 一次呼叫回額度**（sharedPool 9,800,000）。★ 揭露：`card.card.issued.v1` **未帶 `instrumentId`**（目前無消費端，依「不發無人收之事件」原則不動） |
| **1.4** | 2026-10-17 | **360 全鏈路（`#A-294`）**：`CardSummaryView` 加 `instrumentId`（批次、避免 N+1）；**BFF `POST /api/v1/bff/customer-360`**（token-relay 轉傳使用者權杖、分層獨立降級、下游原樣轉出）。真機 4 服務：建人→發卡→註冊載體→設額度→**一次呼叫回「人＋卡＋額度」（sharedPool 9,800,000）**。★ 揭露：**發卡未自動建載體**（需手動 `POST /instruments`） |
| 1.3 | 2026-10-17 | **進件接線＋360 端到端（`#A-293`）**：`V10` `card_applications.party_id`（可空、不跨服務驗證）；四層與契約同步。真機：建 party → 進件帶 partyId → DB 一致；**匯入張三 3 帳戶 → 一次查詢回 3 筆**（跨帳戶 360 成立）；重跑冪等（跳過 3）。★ 揭露：示範資料非真實、360 未含卡清單與額度（需實際發卡）、BFF 聚合未做 |
| 1.2 | 2026-10-17 | **BEAM PMO 裁 (a)＋(b) → 已實作（`#A-292`）**：補 **N-1 缺口**（`POST /api/v1/parties` 建檔：加密＋hash＋遮罩＋`PARTY_REGISTER` 稽核、`partyNo` 防重）；**外部匯入** `PartyImportRunner`（CSV、逐筆走同一路徑、可重跑、筆數即回填紀錄）。真機：建檔 → DB 有密文與 hash → 360 查得到（小寫／空白亦命中）→ 重複建檔 400。★ **未做**：進件流程接線（`card-applications` 無 party 欄位） |
| 1.1 | 2026-10-17 | **H-1～H-3 落地（`#A-291`）**：`V5` 加 `id_no_hash`＋`pii_access_audits`；`POST /api/v1/party-lookups`（body 傳 PII、角色管制、**含 0 筆之稽核**）；回填 runner（**實測 0 筆——無密文**）。★ **揭露根本缺口 N-1～N-3**：本服務**無建檔端點**、`id_no_enc` 從未寫入 → 360 查詢「技術就緒、資料為空」，列 N-Q (a)(b)(c) 待裁 |
| 1.0 | 2026-10-17 | 開立：承載 BEAM PMO「身分證字號 360 查詢」需求。盤點揭露 **F-3／F-4／F-5**（`parties` 綁帳戶、身分證僅加密與遮罩、無 `id_no_hash`）→ **跨帳戶 360 目前不可行**；已落地第一步（`GET /api/v1/cards?accountId|partyId`）；列 H-1～H-3 資安決策待裁 |
