# CR-016　`clearing-service` 契約所需之「清算執行」與「對帳結果」**無對應表**，且 `org` 值域脫鉤

| 項目 | 內容 |
|---|---|
| 文件編號 | CR-016 |
| 版本 | **1.2（`C-14` 動工前查證攔下「契約與實作脫鉤」：對帳結果回應型別——實作回**陣列**、契約誤宣告**單一物件**；已修正產生器並真機複驗。見 §8）** |
| 提出日 | 2026-09-22 |
| 提出者 | WorkBuddy AI（開發團隊） |
| 緣起 | `GOV-006` 2.3.3（**C-5** `clearing-service`）動工前之查證 |
| 影響文件 | `contracts/openapi/clearing-service.yaml`（產生器 `tools/gen_openapi.py`）、`SCS-013`、`SDD-001`（`clr` schema） |
| 關聯前例 | `CR-015`（商戶契約與 DDL 脫鉤 → 先立 CR 再實作）、`CR-012`（值域完全脫鉤） |

---

## 1. 緣起

`clearing-service` 目前**僅有骨架**（`ClearingServiceApplication` ＋ `LayeringTest`），
`CC_CLR` 已有 3 張表（`CLEARING_BATCHES`／`CLEARING_ITEMS`／`SETTLEMENTS`），
契約有 3 端點。動工前逐項比對後發現：**契約所要求的兩個資源在 DDL 完全不存在**，
若直接實作，`POST /clearing/runs` 之 `runId` **無處可存**、
`GET /clearing/reconciliations/{runId}` **無資料來源**——兩端點將無法誠實實作。

---

## 2. 缺口明細（DDL 為 2026-09-22 實測值）

### 2.1 ⛔ 缺表一：清算執行（`ClearingRun`）——契約之核心資源

| 契約元素 | 用途 | DDL 現況 |
|---|---|---|
| `POST /clearing/runs` → `ClearingRunView.runId` | 每次清算執行之識別 | **無**（`CLEARING_BATCHES` 為**單一批次檔**，非一次執行） |
| `status`：`RUNNING`／`MATCHED`／`DIFFERENCE`／`FAILED` | 執行結果 | **無**（`CLEARING_BATCHES.status` 為 `RECEIVED`／`PARSED`／`MATCHED`／`SETTLED`／`REJECTED`，語意為**檔案處理**，非執行） |
| `txCount`／`totalAmount`／`differenceCount`／`differenceAmount` | 執行彙總 | **無**（批次表有 `tx_count`／`total_amount`，惟無差異欄） |

> **釐清**：一次「清算執行」可涵蓋**多個批次檔**（多組織、多檔型），
> 與 `CLEARING_BATCHES`（一檔一列）為**一對多**關係——故不可挪用批次表冒充執行。

### 2.2 ⛔ 缺表二：對帳結果（`Reconciliation`）

| 契約元素 | 用途 | DDL 現況 |
|---|---|---|
| `GET /clearing/reconciliations/{runId}` → `ReconciliationView` | 依**執行**查各組織之差異 | **無表** |
| `org`／`differenceType`（`TIMING`／`FX_RATE`／`SUBSTANTIVE`）／`differenceCount`／`differenceAmount`／`resolution` | 差異分類與處理 | **無欄**（`CLEARING_ITEMS.status` 僅有 `MATCHED`／`UNMATCHED`／`EXCEPTION`，無差異**分類**） |

### 2.3 `org` 值域脫鉤（6 vs 7）

| 來源 | 值域 | 差異 |
|---|---|---|
| DDL（實測） | `VISA`／`MASTERCARD`／`NCCC`／`AMEX`／`DFS`／`JCB`（6） | 有 `DFS` |
| 契約（`ClearingRunRequest.orgs[]`） | `VISA`／`MASTERCARD`／`NCCC`／`AMEX`／`JCB`／`DINERS`／`DISCOVER`（7） | 有 `DINERS`／`DISCOVER`，無 `DFS` |

> **判讀**：`DFS` 即 **Discover Financial Services**（部分系統沿用之交換碼），
> 故 `DFS` 與 `DISCOVER` 為**同一組織之兩種寫法**；`DINERS`（Diners Club）
> 則為**獨立組織**，DDL 確實缺漏。三者非同一件事，須明確處置。

### 2.4 `settlements` 缺 `direction` 欄

契約 `SettlementRequest.required = [runId, direction]`，
`direction` ∈ `ACQUIRER_PAYOUT`／`ISSUER_RECEIVABLE`；
DDL `SETTLEMENTS` **無**此欄（僅 `ORG`／`SETTLEMENT_DATE`／金額／`STATUS`）。

### 2.5 契約示例錯誤（複製貼上）

`SettlementRequest.example` 為 `{settlementType: EARLY, reason: 提前結清}`——
① `settlementType` **不在** 該 schema 之 properties（僅 `runId`／`direction`）；
② 語意為**分期提前結清**，與清算結算無關。屬產生器之示例殘留，須更正。

---

## 3. 建議方案（供裁定）

| # | 項目 | 建議 | 理由 |
|---|---|---|---|
| A | 清算執行 | **新增 `CLEARING_RUNS` 表**（`RUN_NO` UNIQUE、`BUSINESS_DATE`、`STATUS` 4 值 CHECK、`TX_COUNT`／`TOTAL_AMOUNT`／`DIFFERENCE_COUNT`／`DIFFERENCE_AMOUNT`、`ORGS`） | 契約之核心資源；與批次表為一對多，不可挪用 |
| B | 對帳結果 | **新增 `CLEARING_RECONCILIATIONS` 表**（`RUN_ID` FK、`ORG`、`DIFFERENCE_TYPE` 3 值 CHECK、`DIFFERENCE_COUNT`／`DIFFERENCE_AMOUNT`、`RESOLUTION`） | 同上；差異**分類**為契約明載之語意 |
| C | `org` 值域 | **DDL 增列 `DISCOVER`／`DINERS`**（保留 `DFS` 供相容），契約值域不動；服務層以契約值寫入 | 契約 7 值為對外語意；`DFS` 為既有資料之遺留碼，保留以免破壞既有列（`CR-015` 同一原則：**不為相容而刪值**） |
| D | `settlements.direction` | **DDL 新增 `DIRECTION`**（CHECK 2 值） | 契約必要欄位，且為結算之**方向語意**（收單撥付／發卡應收） |
| E | 契約示例 | **修正 `SettlementRequest.example`** 為 `{runId: …, direction: ACQUIRER_PAYOUT}` | 現例之欄位不存在且語意錯誤（分期），會誤導實作 |
| F | 對帳之資料來源 | **本期以 `CLEARING_ITEMS` 彙總**（依 `batch_id` 所屬 `org` 分組、`status='EXCEPTION'` 計數與金額）；`differenceType` 暫以 **`SUBSTANTIVE`** 標記，並於 `SCS-013 §10` 揭露「時序差（`TIMING`）與匯差（`FX_RATE`）之自動判別需**帳務交易面**資訊（`posting` 之未入帳交易、匯率表），本期不具備」 | 不靜默假造分類；寧可誠實標為實質差異 |

**交付方式**：一律以 **`V2__clearing_runs_and_reconciliations.sql`** 追加
（`V1` 基線已凍結，`SAD-003 §9`）；契約改 `tools/gen_openapi.py` 後重生；
`SCS-013` 升版並補 §5／§7／§10。

---

## 4. 誠實揭露

- 本 CR 之 DDL 為 **Oracle 實測**（`CC_CLR`），非文件抄錄。
- `CC_CLR` 三表目前資料量**待實測確認**（若有既有列，`settlements` 加 `DIRECTION` 需處理 `NOT NULL` 之回填或設為可空）。
- **清算檔解析（VCF／EMS／SETTLEMENT／CHARGEBACK／FEES）不在本期範圍**
  （`GOV-006` C-5 明載「檔格式除外」）——故 `CLEARING_BATCHES`／`ITEMS` 之**寫入者**
  （檔解析器）本期不實作；本期之對帳以**既有批次資料**為輸入，並在 `SCS-013 §10` 揭露。
- `CLEARING_BATCHES`／`ITEMS` 狀態轉換未建模為 `SDD-005` 正式狀態機（`SCS-013 §10` 已載）。

---

## 5. 施行中追加之事項（我方依「替代方案推進」原則處理，供事後覆核）

| # | 事項 | 處理 | 理由 |
|---|---|---|---|
| G | 結算之**費用來源** | `INTERCHANGE_FEE` ＝ Σ 清算明細之 `CLEARING_ITEMS.FEE`；`NETWORK_FEE` **固定 0** | `CC_CLR` **無費率設定表**，網路費率無從取得；明細之逐筆費用為**檔案帶入之事實**，可作 interchange。**不假造費率**（`SCS-013 §10`） |
| H | 執行之**成功碼** | `POST /clearing/runs` 回 **202**（非 201） | 契約明訂（產生器端即 `202`）——**契約優先**；原實作一度寫 201，查證契約後更正 |
| I | 契約 schema **命名碰撞** | 清算之 `SettlementRequest` **改名** `ClearingSettlementRequest` | 產生器之**示例登錄表以 schema 名為鍵**；原命名與 `installment-service` 同名 → 清算撿到分期之示例（`{settlementType: EARLY, reason: 提前結清}`）。此為 §2.5 之**真因**，非單純示例筆誤 |
| J | 零筆數之執行 | 允許完成為 `MATCHED` 但 `txCount = 0`，並**必記 WARN** | 業務日無明細時，`MATCHED` 與「全數相符」於狀態欄位無法區分；**不新增狀態以免虛構語意**，改以 `txCount` 與日誌辨識（比照 `#C-42`「零產出必須可區分」） |
| K | 結算回應不含結算單識別碼 | 依契約回 `ClearingRunView`（202） | 契約未定義結算單視圖；已產生之結算單 ID **不回傳**（列為 `SCS-013 §10` 限制） |

---

## 6. 執行清單（2026-09-22）

| 項 | 交付 |
|---|---|
| 遷移 | `V2__clearing_runs_and_reconciliations.sql`——新增 **`CLEARING_RUNS`**（契約之執行資源）與 **`CLEARING_RECONCILIATIONS`**（對帳結果，含 `DIFFERENCE_TYPE` 3 值 CHECK）；`SETTLEMENTS` 補 `DIRECTION`（2 值 CHECK）；`clearing_batches.org` 之 CHECK **動態尋得後重建為具名**（`CK_CLEARING_BATCHES_ORG`，增列 `DISCOVER`／`DINERS`）；新增 `clearing_run_no_seq` |
| 契約 | 改 `tools/gen_openapi.py`（schema 改名避碰撞）後重生；**驗證 0 錯誤、全案 74 端點** |
| 程式 | DDD 四層：`ClearingRun`（狀態**由結果推導**）／`Reconciliation`／`Settlement`／`Org`（8 值）；4 埠＋4 JDBC（含明細彙總）；3 應用服務＋組裝器；1 控制器（3 端點，成功碼 202／200／202） |
| 品質 | 單測 **24 例 0 失敗**（領域 9／應用 15；含狀態推導、差異落列、零筆數、冪等、404／400）；`scan_scaffold`／`scan_faketest` 0 命中；`validate_sql` 0 違規 |

---

## 7. 真機 e2e（8094／`FREEPDB1`，2026-09-22）

**種子**（3 批次／6 明細，業務日 2026-09-17）：`VISA` 3 筆（含 1 筆 `EXCEPTION`）／`MASTERCARD` 2 筆／`DISCOVER` 1 筆。

| # | 情境 | 結果 |
|---|---|---|
| ⓪ | **遷移前**插入 `org='DISCOVER'` 之批次 | **被拒**（舊 CHECK 6 值無 `DISCOVER`）——證明值域確為約束所管 |
| ① | 遷移後重插 | **成功**；`CK_CLEARING_BATCHES_ORG` 為具名約束、**8 值** |
| ② | `POST /clearing/runs`（3 組織） | **HTTP 202**；`CLR20260922000001`／**`DIFFERENCE`**／`txCount=6`／`totalAmount=750000`／`differenceCount=1`／`differenceAmount=100000`——**與手算逐項相符** |
| ③ | `GET /clearing/reconciliations/{runId}` | **1 列**：`VISA`／`SUBSTANTIVE`／`1`／`100000`／「待人工覆核」 |
| ④ | `POST /clearing/settlements`（`ACQUIRER_PAYOUT`） | **HTTP 202** |
| ⑤ | 同冪等鍵重送執行 | 回**同一** `runId`（未新增、未重抽序號） |
| ⑥ | **DB 複核** | `CLEARING_RUNS`：1 列，欄位與 ② 全同；`CLEARING_RECONCILIATIONS`：1 列（VISA）；`SETTLEMENTS`：**3 列**——`VISA` 500000／12500／0／**487500**、`MASTERCARD` 200000／5000／0／**195000**、`DISCOVER` 50000／1250／0／**48750**，`DIRECTION=ACQUIRER_PAYOUT` |

---

## 8. 追加修正（2026-09-22，v1.2）：對帳結果之回應型別應為**陣列**

> **緣起**：`C-14`（WebUI 清算與請款頁）動工前之查證——欲接 `GET /clearing/reconciliations/{runId}`
> 時發現**契約與實作脫鉤**：契約宣告 `data` 為**單一物件**（`$ref: ReconciliationView`），
> 實作卻回**陣列**（`List<ReconciliationView>`）。

| # | 項目 | 內容 |
|---|---|---|
| a | **實測（2026-09-22）** | `GET /api/v1/clearing/reconciliations/{runId}` → **200**，`data` 為 **list**（1 元素）。**與原契約之單一物件不符** |
| b | **設計意圖判定：應為陣列** | ①`CLEARING_RECONCILIATIONS` 以**（執行 × 組織）**為鍵（`CR-016` 裁定 B）→ **一執行必然可有多列**；②`ReconciliationView` **含 `org` 欄位**——單一物件無從表達多組織；③`SCS-013 §6` 之語意為「對帳結果**查詢**」（清單）。→ **實作正確，契約為缺陷** |
| c | **真因** | 產生器之 `data_schema()` **支援**以 `Schema[]` 表達陣列（其 docstring 明載「原僅支援單一 $ref，致『分頁查帳單』等清單端點誤輸出單一物件」），但本端點於 `CR-016` 新增時**漏了 `[]` 後綴**——**同類缺陷再次發生** |
| d | **修正** | `tools/gen_openapi.py`：`"ReconciliationView"` → **`"ReconciliationView[]"`** → 重生契約 |
| e | **影響範圍（已嚴格驗證）** | 以「還原原始碼 → 重生 → `diff -rq` 全目錄比對 → 重新套用 → 再比對」確認：**18 份契約中僅 `clearing-service.yaml` 變動**（`data` 改為 `type: array` + `items.$ref`）；OpenAPI 驗證 **0 錯誤／74 端點** |
| f | **修正後複驗（真機）** | 同端點 → **200**，`data` 型別 `list`（1 元素，`org=VISA`／`SUBSTANTIVE`／`1`／`100000`）——**與修正後契約相符** |
| g | **未變更** | 後端**程式不動**（原即回陣列）；`SCS-013` §6 之敘述與實作一致，**不須改**；`SDD-001` 不動 |

> **⚠️ 教訓（供後續服務參考）**：`CR-016` 之 §2.5 已記錄「契約示例錯誤」之真因（示例登錄表以 schema 名為鍵），
> 惟**同一支服務之另一端點仍有型別缺陷**——顯示「新增端點時**逐端點核對回應型別**」尚未形成檢查點。
> 建議：`gen_openapi.py` 之 `svc(...)` 呼叫處，凡路徑含 `{id}` 之 GET 或語意為「清單／明細／歷史」者，
> 一律複核是否需 `[]` 後綴。

---

## 9. 修訂記錄

| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 1.0 | 2026-09-22 | 提出。動工前查證發現：契約之「清算執行」與「對帳結果」**兩個資源在 DDL 完全不存在**、`org` 值域脫鉤、`settlements` 缺 `direction`、契約示例為分期殘留。建議新增兩表＋補欄＋修示例；對帳分類之 `TIMING`／`FX_RATE` 自動判別建議本期揭露為限制。待 BEAM PMO 裁定 |
| 1.2 | 2026-09-22 | **`C-14` 動工前查證攔下「契約與實作脫鉤」：對帳結果之回應型別**——`GET /clearing/reconciliations/{runId}` 契約宣告**單一物件**、實作回**陣列**。判定**實作正確、契約為缺陷**（表以「執行 × 組織」為鍵、視圖含 `org`、語意為清單）；真因為產生器之 `Schema[]` 陣列語法**漏用**（同類缺陷再次發生）。修正 `gen_openapi.py` → 重生；**嚴格驗證僅 `clearing-service.yaml` 變動**、0 錯誤／74 端點；**真機複驗** `data` 為 list。後端程式**不動**。⚠️ 併修本文件**標題編號重複**（`## 4.` 出現兩次、`## 3.1` 層級錯誤）→ §5～§9 重新編號，並將兩處懸空之「`§10`」明確為「`SCS-013 §10`」 |
| 1.1 | 2026-09-22 | **BEAM PMO 裁定：A／B／D／E 一次核准；`org` 由 DDL 增列、契約不動**。新增 **§5 施行中追加事項**（G 費用來源／H 成功碼 202／I 命名碰撞之真因／J 零筆數／K 結算單 ID 不回傳，均附理由）、**§6 執行清單**、**§7 真機 e2e 七項**（含遷移前後對照與 DB 複核）。單測 24 例 0 失敗；`SCS-013` 升 1.1、`SDD-001` 升 1.9 |
