# SCS-025　徵審（信用審查）介面契約規格

| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-025 |
| 版本 | **1.1** |
| 狀態 | **我方建議契約（proposed）——待客戶確認；adapter **已依本契約實作完成並測試通過**（v1.1）** |
| 撰寫者 | WorkBuddy AI |
| 審查者 | BEAM PMO |
| 服務 | `card-service`（介接方）｜外部徵審系統（被介接方） |
| 對應需求 | `BRD-001 D-01`（徵審系統，僅列名無規格）、`BRD-001 BF-01`（受理→徵信→核決） |
| 對應設計 | `SDD-005 §12`（申請案件狀態機）、`SDD-006`（介面契約）、`SAD-002`（bounded context）、`SAD-005`（個資保護） |
| 關聯規格 | `SCS-024`（進件處理，本期呼叫方）、`SCS-022 §7.4`（fail-closed 設計思路） |
| 建立日期 | 2026-09-20 |

> ⚠️ **本契約之性質，務必先講清楚**：
> 原 RFP（`BEAM_核心現代化_信用卡核心_RFP_FR_20210312`、`…RFP_NFR_…`）**未含徵審介面規格**；
> `BRD-001 D-01` 僅將徵審列為外部系統，**未給介面規格**。
> 故本文件為**我方自行設計之建議契約**——目的是讓 POC 能完整演示、並於客戶質疑
> 「徵審在哪」時可立即提出具體介面與程式，**不是**客戶既有規格。
> 客戶若提供其自身規格，僅需替換 `ExternalCreditReviewAdapter`（`§7`），
> 領域層與狀態機**不受影響**（`SAD-002` bounded context）。

---

## 1. 目的與範圍

### 1.1 為何需要本文件（POC 風險）

| # | 風險 | 說明 |
|---:|---|---|
| 1 | **追溯矩陣缺口** | RFP 無徵審介面需求 → `GOV-004` 追溯矩陣無處可掛，客戶稽核時易被指「漏做」 |
| 2 | **POC 中途質疑** | 客戶於 POC 演示進件流程時，極可能質疑「**徵審在哪？**」——若答「外部系統、未定義」，POC 完整性受挑戰 |
| 3 | **整合責任模糊** | 未定義介面 → 後續爭議「是誰沒提供」將耗费往來成本 |

**對策**：我方自行設計標準徵審介面契約並實作 adapter。POC 時可具體展示：

> 「徵審介面在這裡（`SCS-025`）、程式已寫好（`ExternalCreditReviewAdapter`）、
> 只差把貴行徵審系統的 endpoint 填進組態。
> 在填上前，系統會**轉人工核決**，不會自動核准。」

### 1.2 範圍內／外

**範圍內**

- 徵審介面之**建議契約**（端點、請求／回應 schema、狀態碼）
- 安全與可靠度要求（逾時、斷路器、fail-closed）
- 與 `SDD-005 §12` 申請案件狀態機之對應
- `ExternalCreditReviewAdapter` 之設計（含未設定時之降級）
- POC 演示方案

**範圍外**

- 徵審系統本身之建置（客戶既有系統）
- 徵審決策邏輯（評分模型、規則）— 屬徵審系統內部
- 聯徵（JCIC）等外部查詢之實際介接 — 由徵審系統負責

---

## 2. 名詞

| 名詞 | 說明 |
|---|---|
| 徵審 | 信用審查：徵信＋核決。本文件專指「我方呼叫外部徵審系統」之介面 |
| 徵信 | 向徵審系統查詢信用與風險，取得決策 |
| 決策 | `APPROVE`／`DECLINE`／`REFER`／`SUPPLEMENT` 四值（見 `§4.3`） |
| `REFER` | 轉人工核決；亦為**徵信不可用時之 fail-closed 結果** |
| fail-closed | 外部呼叫失敗時，採「**不安全**」之一側（轉人工），而非靜默放行 |

---

## 3. 通訊模式

| 項目 | 設計 | 理由 |
|---|---|---|
| 模式 | **同步**請求／回應（REST over HTTPS） | 進件流程需即時取得決策才能推進狀態機（`SDD-005 §12` 轉換 #2→#5） |
| 協定 | HTTPS／JSON | 與 `SDD-006` 全系統一致 |
| 呼叫時機 | `SUBMIT_REVIEW` 事件（`SCS-024 §5.2`） | 受理後由系統送徵信；非建立時立即呼叫（見 `SCS-024 §8.3`） |
| 冪等 | 請求帶 `applicationId` 供徵審系統去重 | 避免重複查詢產生重複成本（見 `§6.2`） |

> **為何不用非同步（webhook／訊息）**：徵審決策為進件流程之**必要前置**，
> 非同步會使案件長時間懸於 `REVIEWING` 且需額外補償機制。同步較單純，
> 且逾時有明確 fail-closed 路徑（`§6`）。若客戶既有系統僅支援非同步，
> 則以 `REFER` 為常態、由人工核決推進——仍不需改狀態機。

---

## 4. 介面契約

### 4.1 端點

```
POST {credit-review.base-url}/api/v1/credit-reviews
```

| 項目 | 值 |
|---|---|
| 方法／路徑 | `POST /api/v1/credit-reviews` |
| `Content-Type` | `application/json; charset=utf-8` |
| 認證 | mTLS **或** `Authorization: Bearer {token}`（見 `§5`） |
| `X-Idempotency-Key` | **必帶**；值為 `applicationId` 之字串（供徵審系統去重） |

> ⚠️ 路徑與埠號由組態 `cardcore.credit-review.base-url` 提供（見 `§7.2`），
> **不硬編**。這是「換客戶系統只改組態」之前提。

### 4.2 請求（我方 → 徵審系統）

```json
{
  "applicationId": "018f2a3b-0000-0000-0000-000000000001",
  "applyNo":       "APP2026092000012345",
  "accountId":     "018f2a3b-0000-0000-0000-000000000011",
  "partyId":       "018f2a3b-0000-0000-0000-0000000000b1",
  "productCode":   "PLATINUM",
  "channel":       "WEB",
  "requestedAt":   "2026-09-20T10:30:00+08:00"
}
```

| 欄位 | 型別 | 必填 | 說明 |
|---|---|:--:|---|
| `applicationId` | UUID | ✅ | 申請案件識別碼；**供徵審系統去重** |
| `applyNo` | string | ✅ | 申請編號（人讀） |
| `accountId` | UUID | ✅ | 帳戶識別碼 |
| `partyId` | UUID | ➖ | 當事人識別碼（**選填**）：本期 `CardApplication` 未持有，故**不傳**；待 `party-service` 串接後補（`SCS-025 §10 #6`） |
| `productCode` | string | ✅ | 商品代碼（徵審規則可能依商品而異） |
| `channel` | string | ✅ | 通路：`WEB`／`MOBILE`／`API`／`BRANCH`／`MERCHANT` |
| `requestedAt` | ISO-8601 | ✅ | 請求時間（含時區） |

> 🔒 **個資最小化（`SAD-005`）**：本請求**不含**身分證字號、姓名、生日等明文個資——
> 僅傳識別碼，由徵審系統**自行**依 `accountId`（未來為 `partyId`）查其既有資料。
> 如此我方不必在跨系統傳輸中攜帶個資明文，亦降低外洩面。
> 若客戶徵審系統要求個資明文，須另經 `SAD-005` 個資影響評估後方可調整。

### 4.3 回應（徵審系統 → 我方）

```json
{
  "decision":    "APPROVE",
  "referenceNo": "CR-20260920-000123",
  "score":       720,
  "reasons":     ["INCOME_STABLE"],
  "decidedAt":   "2026-09-20T10:30:05+08:00"
}
```

| 欄位 | 型別 | 必填 | 說明 |
|---|---|:--:|---|
| `decision` | enum | ✅ | **決策四值**（見下表），唯一影響狀態機之欄位 |
| `referenceNo` | string | ✅ | 徵審系統之案件編號（供稽核追溯） |
| `score` | integer | ➖ | 信用評分（僅供參考／留痕，不影響狀態機） |
| `reasons` | string[] | ➖ | 決策理由代碼（留痕用） |
| `decidedAt` | ISO-8601 | ✅ | 決策時間 |

**`decision` 值域與狀態機對應（`SDD-005 §12`）**

| `decision` | 意義 | 案件狀態轉換 | 觸發者 |
|---|---|---|---|
| `APPROVE` | 自動核准 | `REVIEWING → APPROVED`（產生核准序號） | 系統 |
| `DECLINE` | 自動婉拒 | `REVIEWING → REJECTED` | 系統 |
| `REFER` | 轉人工核決 | **維持 `REVIEWING`** | 徵審人員 |
| `SUPPLEMENT` | 要求補件 | `REVIEWING → SUPPLEMENT` | 系統 |

> ⚠️ **`decision` 出現四值以外之內容（含 `null`、未知字串）**：
> 一律視同 **`REFER`**（fail-closed），**不得**視為 `APPROVE`。
> 理由：無法解讀之決策若放行，即為靜默放行——與認證 fail-open 同類
> （見 `SCS-022 §7.4`）。

### 4.4 HTTP 狀態碼處理

| 狀態碼 | 我方處置 | 是否重試 |
|---:|---|:--:|
| `200` + 合法 `decision` | 依 `§4.3` 轉態 | — |
| `200` + 無法解析／未知 `decision` | **視同 `REFER`**，記 WARN | ❌ |
| `400`／`422` | 我方請求錯誤 → 記 **ERROR** 告警 **+ 轉 `REFER`**（刻意不拋出，見下） | ❌ |
| `401`／`403` | 認證失敗 → 記 ERROR、轉 `REFER`（憑證問題，非案件問題） | ❌ |
| `429` | 被限流 → 轉 `REFER`，記 WARN | ⚠️ 見 `§6.2` |
| `5xx` | 徵審系統故障 → 轉 `REFER`，記 ERROR | ⚠️ 見 `§6.2` |
| 連線逾時／拒絕／DNS 失敗 | 轉 `REFER`，記 ERROR | ⚠️ 見 `§6.2` |

> **⚠️ `400`／`422` 為何仍轉 `REFER` 而不拋出（v1.1 實作決定）**：
> 那是「我方程式有 bug」（如欄位缺漏），但刻意**不拋出**——
> 若拋出，單一整合 bug 會讓**全部**進件失敗，屬「一顆螺絲癱整條產線」。
> 改以 **ERROR 層級日誌＋告警**暴露問題（大聲，但不阻斷），
> 案件仍轉人工核決。此與 fail-closed 一貫：**既不自動核准，也不靜默放行**。
>
> ⚠️ 初版（v1.0）原寫「拋 `CC-CARD-4001`」，與 `safeReview` 之
> 「捕獲所有例外轉 `REFER`」（`SCS-024 §5.2` 核心 fail-closed）相衝突，
> 故 v1.1 改為上述做法，並同步修正 §9 #9。

---

## 5. 安全

| 層面 | 要求 | 理由 |
|---|---|---|
| 傳輸 | **HTTPS（TLS 1.2+）**，禁止明文 HTTP | 銀行基本 requirement（`SAD-005`） |
| 客戶端認證 | **mTLS 優先**；次選 `Bearer` token（由 Vault／K8s Secret 注入） | 徵審屬高敏感；mTLS 可防憑證外洩後被重放 |
| 憑證存放 | 存放於 repo 之外（如 `~/.config/cardcore/`），權限 **600**；**不進版控** | 與 Stripe 憑證同一慣例（見 `SCS-023 §7.3`） |
| 個資 | 請求不含個資明文（見 `§4.2`）；日誌**只記** `applicationId`，不記評分／理由 | 個資最小化 |
| 稽核 | 每次呼叫留存（`applicationId`／`referenceNo`／`decision`／耗時） | 金融稽核可追溯 |

---

## 6. 可靠度與 fail-closed

### 6.1 逾時

| 項目 | 建議值 | 說明 |
|---|---|---|
| connect timeout | **2s** | 連線建立 |
| read timeout | **5s** | 等待決策 |
| 總上限 | **7s** | 逾時即 fail-closed 轉 `REFER` |

> ⚠️ **已於 `application.yml` 專屬覆寫**（v1.1）：專案全域預設為
> `resilience4j.timelimiter 800ms`（`CSP-003 §5`）＋ `retry 3 次`——
> 兩者對徵審**皆不適用**，故為 `creditReviewClient` 設 `instances` 覆寫：
> `timeout-duration: 7s`、`retry.max-attempts: 1`（不重試，見 `§6.2`）。
> 若未覆寫，800ms 會讓大量正常徵信被誤判逾時而轉人工。

> 進件非授權流程（授權有 2 秒硬限制），故可容許較長等待；
> 但**仍須有上限**——無上限的等待會把 HTTP 連線池耗盡，
> 是「慢性癱瘓」型缺陷，比快速失敗更難察覺。

### 6.2 重試策略（⚠️ 與一般 HTTP 呼叫不同）

| 項目 | 設計 | 理由 |
|---|---|---|
| **預設不自動重試** | 僅 `429` 且帶 `Retry-After` 時，最多重試 **1 次** | 徵信查詢常涉及**聯徵（JCIC）等外部計費查詢**，且會留下查詢紀錄 |
| 連線逾時／`5xx` | **不重試**，直接轉 `REFER` | 同上；且重試會拉長案件處理時間 |

> ⚠️ **這是本契約最容易被忽略的一點**：
> 一般微服務呼叫習慣「失敗就重試」，但**徵信查詢有成本與副作用**
> （重複查詢可能產生重複費用、並在聯徵留下多次查詢紀錄，
> 反而影響客戶信用評分）。故採「**不自動重試、轉人工**」——
> 人工可判斷是否值得再查一次。此設計亦與 fail-closed 一貫。

### 6.3 斷路器（Resilience4j）

專案已有 `spring-cloud-starter-circuitbreaker-resilience4j` 相依（`SAD-001 AP-05`）：

| 項目 | 建議值 |
|---|---|
| 滑動視窗 | 最近 20 次呼叫 |
| 開斷路閾值 | 失敗率 ≥ **50%** |
| 斷路後等待 | 30s（half-open 試探） |
| 斷路期間行為 | **直接轉 `REFER`**，不真的發出呼叫 |

> 斷路器目的：徵審系統整體故障時，避免每次進件都卡 7 秒逾時、
> 拖垮 card-service 的連線池。斷路期間案件仍可受理，只是轉人工。

### 6.4 fail-closed 總結

**任何**「無法取得可信決策」之情況，一律轉 `REFER`、維持 `REVIEWING`：

- 連線失敗／逾時／DNS 錯誤
- `5xx`／`429`（超過重試上限）
- 回應無法解析、未知 `decision`
- **未設定 `base-url`（尚未接上徵審系統）**
- 斷路器開啟

> **不自動核准、亦不自動婉拒**——一律交給人判斷。
> 與 `SCS-024 §5.2`、`SCS-022 §7.4` 同一思路：
> 「不知道」不等於「可以」。

---

## 7. 程式實作（adapter 設計）

### 7.1 結構（`SAD-002` bounded context）

```
domain/port/CreditReviewPort            ← 領域埠（已存在，SCS-024）
domain/model/CreditReviewRequest        ← 領域請求（已存在）
domain/model/CreditReviewResult         ← 領域決策（已存在）
infrastructure/adapter/
  ├─ NoopCreditReviewAdapter            ← 本地用，一律 REFER（已存在，SCS-024）
  └─ ExternalCreditReviewAdapter        ← 本規格新增：呼叫真實徵審系統
infrastructure/client/CreditReviewClient← Feign 客戶端（本服務自有 DTO）
```

**關鍵**：外部系統之 DTO **不共用**領域類別，僅於 adapter 內轉換
（照 `CardServiceClient` 既有慣例，見 `SAD-002`）。
故客戶若換徵審系統／改欄位，**僅需改 adapter 與 client**，領域層不動。

### 7.2 組態

```yaml
cardcore:
  credit-review:
    # 空白＝未接上徵審系統 → adapter 一律回 REFER（fail-closed）
    base-url: ${CREDIT_REVIEW_BASE_URL:}
    connect-timeout-ms: 2000
    read-timeout-ms: 5000
    # true 時用 NoopCreditReviewAdapter（本地／POC 演示用）
    stub-enabled: ${CREDIT_REVIEW_STUB:false}
```

> ⚠️ **`base-url` 預設為空白（而非指向某個假 URL）**：
> 這是刻意的——空白時 `ExternalCreditReviewAdapter` **不發出呼叫**，
> 直接回 `REFER` 並記 WARN，使「尚未接上」在日誌上一目了然，
> 且**不會**因預設值指向不存在的服務而產生無意義的連線錯誤噪音。

### 7.3 adapter 行為決策表

| `stub-enabled` | `base-url` | 使用之 adapter | 行為 |
|---|---|---|---|
| `true` | 任意 | `NoopCreditReviewAdapter` | 一律 `REFER`（本地／POC） |
| `false` | 空白 | `ExternalCreditReviewAdapter` | **不呼叫**，回 `REFER` + WARN |
| `false` | 已設定 | `ExternalCreditReviewAdapter` | 呼叫真實徵審系統（`§4`） |

> ⚠️ **未設定 `stub-enabled` 時**（`matchIfMissing=true`）行為同 `true`——走 `Noop`。
> 即「接真實徵審必須**顯式宣告** `stub-enabled=false`」——
> 與認證「停用只能顯式宣告」同一思路（`SCS-022 §7.4`）。
>
> 三者**皆為 fail-closed**（無一會自動核准）。差別只在「有沒有真的發出呼叫」。

---

## 8. POC 演示方案

客戶質疑「徵審在哪」時，POC 可依序展示：

| 步驟 | 演示內容 | 證明什麼 |
|---:|---|---|
| 1 | 出示 `SCS-025` 本文件 | 介面已定義（端點／schema／決策／安全／逾時） |
| 2 | 展示 `ExternalCreditReviewAdapter` 程式碼 | 已依契約實作，非紙上談兵 |
| 3 | 於組態填入一個**模擬徵審服務**之 URL | 實際跑一次：`APPROVE`→核准、`DECLINE`→婉拒、`REFER`→轉人工 |
| 4 | 拔掉 `base-url`，重送一次 | 系統**轉人工、不自動核准**（fail-closed 成立） |
| 5 | 說明替換方式 | 客戶提供其規格時，僅換 adapter，領域層與狀態機不動 |

> 第 4 步是**最有說服力**的一步：證明「徵審沒接上時，系統不會偷偷核准」——
> 這正是銀行最在意的風險。

---

## 9. 測試要求

| # | 案例 | 預期 |
|---:|---|---|
| 1 | 徵審回 `APPROVE` | → `APPROVED`，產生核准序號 |
| 2 | 徵審回 `DECLINE` | → `REJECTED` |
| 3 | 徵審回 `REFER` | 維持 `REVIEWING` |
| 4 | 徵審回 `SUPPLEMENT` | → `SUPPLEMENT` |
| 5 | **徵審連線逾時** | 轉 `REFER`，**不核准**（fail-closed） |
| 6 | 徵審回 `5xx` | 轉 `REFER`，不自動重試 |
| 7 | 回應 `decision` 為未知值 | 視同 `REFER` |
| 8 | **`base-url` 空白** | **不發出呼叫**，回 `REFER` + WARN |
| 9 | 徵審回 `400` | 記 **ERROR** 告警 + 轉 `REFER`（**不拋出**，避免阻斷全部進件；見 `§4.4`） |
| 10 | 斷路器開啟 | 直接 `REFER`，不發出呼叫 |

> ⚠️ **#5 與 #8 為本契約之核心測試**。
> **禁用**任何「回 `APPROVE`」之假 adapter 來通過測試——
> 那會讓「徵審不可用不核准」永遠是綠的，屬**假驗證**
> （與 `SCS-024 §11 #5` 同一要求）。

### 9.1 實測證據（2026-09-20，`ExternalCreditReviewAdapterTest`）

以可控假 `CreditReviewClient`（記錄呼叫次數、可指定回應或拋出例外）執行：

| # | 項目 | 實測結果 |
|---:|---|---|
| 1 | 測試數 | **14 支全過** |
| 2 | #8 `base-url` 空白 | ✅ 回 `REFER` 且 `callCount=0`——**證明真的沒發出呼叫** |
| 3 | 連線失敗／`5xx`／`400` | ✅ 全部 `REFER`（fail-closed） |
| 4 | `decision` 四值映射 | ✅ `APPROVE`／`DECLINE`／`REFER`／`SUPPLEMENT` 正確 |
| 5 | `decision` 未知值／`null`／回應 `null` | ✅ 全部視同 `REFER` |
| 6 | `"APPROVED"`（非契約四值） | ✅ 視同 `REFER`——**不得**被誤判為核准 |
| 7 | 大小寫（`approve`） | ✅ 容許（trim + `toUpperCase`） |

> ⚠️ **實測中發現並排除之一處假綠風險**：adapter 的兩個 catch 分支
> （`FeignException` 依 status 分流、其餘例外）**結果都是 `REFER`**，
> 故即便測試用之 stub 例外 `status()` 失效、走錯分支，測試**仍會全綠**。
> 已加斷言 `assertEquals(400, feign(400).status())` 確保 status 有效，
> 進而確保「status 分流」確實被測到。
>
> 另：初版測試以 `mock(FeignException)` 為之，觸發 `scan_faketest` 之
> **T3 誤判**（有 `thenReturn` 卻無 `verify`）。已改以**真實子類別** `StubFeignException`
> 為之——既消除誤判，亦不為過關而加無意義的 `verify`。

---

## 10. 已知限制（誠實揭露）

1. **本契約為我方建議版，尚未經客戶確認**。客戶若提供其自身規格，
   `§4` 之欄位與端點可能全數改動；adapter 須重寫（領域層不受影響）。
2. **未含真實徵審系統之實作**。本期 adapter 為「可接」狀態，
   實際連線對象待客戶提供 endpoint 與憑證。
3. **聯徵（JCIC）等外部查詢由徵審系統負責**，本契約不涵蓋其介面與費用歸屬。
4. **非同步模式未實作**。若客戶徵審系統僅支援非同步（webhook），
   須另立補償機制；短期替代為「常態 `REFER`＋人工核決」（不需改狀態機）。
5. **逾時／斷路器數值為建議值**，須於 SIT 壓力測試後校準。
6. **`partyId` 本期不傳**：`CardApplication` 未持有該欄位，契約中列為選填；
   待 `party-service` 串接後方可補入請求。

---

## 11. 修訂記錄

| 版本 | 日期 | 修訂者 | 說明 |
|---|---|---|---|
| 1.1 | 2026-09-20 | WorkBuddy AI | **依本契約實作 `ExternalCreditReviewAdapter` 完成並測試通過**：
   新增 `CreditReviewClient`（OpenFeign，照 `CardServiceClient` 既有慣例：自有 DTO、不共用領域類）
   與 `ExternalCreditReviewAdapter`（`@ConditionalOnProperty` 與 `Noop` **互斥**，避免兩 bean 注入歧義）；
   領域 `CreditReviewRequest` 補 `applyNo`／`channel` 兩欄（契約 §4.2 需要而原領域類別無）；
   `application.yml` 為 `creditReviewClient` **專屬覆寫** resilience4j（7s、retry 1 次）
   ——因專案全域 800ms／3 次對徵審不適用（見 §6.1）。
   測試 14 支全過（§9.1），含「`base-url` 空白時 `callCount=0`」之證明。
   ⚠️ 三處**依實作回改契約**（文件不得與實物不一致）：
   ①`400`／`422` 由「拋 `CC-CARD-4001`」改為「記 ERROR + 轉 `REFER`（不拋出）」——
   原設計與 `safeReview` 之 fail-closed 相衝突，且拋出會讓單一 bug 阻斷全部進件；
   ②`partyId` 改選填（本期 `CardApplication` 未持有，故不傳）；
   ③補「未設定 `stub-enabled` 時同 `true`（接真實徵審須顯式宣告）」。
   另排除一處**假綠風險**（兩 catch 分支皆 REFER，故加 status 斷言確保分流被測到） |
| 1.0 | 2026-09-20 | WorkBuddy AI | 初版。依 BEAM PMO 2026-09-20 指示「RFP 無徵審規格，POC 會被質疑，須自行設計提供」建立：§1 POC 風險與對策、§3 同步模式、§4 介面契約（請求／回應／`decision` 四值／狀態碼）、§5 安全與個資最小化、§6 可靠度（**不自動重試**——徵信查詢有成本與副作用）、§7 adapter 設計與組態（**`base-url` 空白即 fail-closed**）、§8 POC 演示方案、§9 測試（#5/#8 為核心，禁假驗證）、§10 限制 |
