# CR-012　兌換類型**契約列舉與 DDL CHECK 完全脫鉤**——契約 5 值全數被服務拒收（400）

| 項目 | 內容 |
|---|---|
| 文件編號 | CR-012 |
| 版本 | **1.1**（BEAM PMO 2026-09-22 授權裁定方向並執行——**採「雙欄位」**；已執行並真機實測通過，見 §8） |
| 狀態 | ✅ **已核定／已執行**：採 **C2 雙欄位**——`REDEMPTIONS.REDEMPTION_TYPE` 維持 DDL 6 值（兌換**標的類別**），新增 `SETTLEMENT_METHOD`（**折抵方式**，V4 遷移）；契約之 `redemptionType` 對齊 DDL 6 值、新增選填 `settlementMethod`（預設 `POINTS_ONLY`） |
| 提出日期 | 2026-09-21（1.0）／執行 2026-09-22（1.1） |
| 提出者 | WorkBuddy AI（開發團隊） |
| 關聯 | `SCS-010 §10 #1`／`§10 #11`（`CASH_VALUE`）、`SDD-001 §3.8`（`REDEMPTIONS`）、`contracts/openapi/rewards-service.yaml`、`SettlementMethod.java`（新增）、`V4__redemptions_settlement_method.sql`、`SCS-010 §9.3`（2026-09-21 重跑） |
| 嚴重度 | 🟡 中（1.0 提案時）→ **已解除**（1.1 執行後：契約值域與實作／主檔完全一致） |

---

## 1. 背景

`SCS-010 §10 #1` 早已記載契約與 DDL 之列舉脫鉤，並定性「本期以 DDL 為準、待 CR」。
2026-09-21 依 BEAM PMO「往下做 ④」重跑 `SCS-010 §9.3` 時**實測再次證實**，且發現其影響比
「文件落差」更實際：**契約 `RedeemRequest.redemptionType` 的每一個合法值，服務都會拒收**。
依契約 CR 流程，正式立案待裁定。

## 2. 實測證據（2026-09-21，rewards-service 8091／真實 Oracle `FREEPDB1`）

### 2.1 全列舉實測（CR 流程第①步：逐值實測）

**契約列舉**（`rewards-service.yaml` → `RedeemRequest.redemptionType`）——**5/5 全數 400**：

| 契約值 | HTTP | 錯誤碼 | 訊息 |
|---|---|---|---|
| `POINTS_ONLY` | 400 | `CC-RWD-4000` | 未知兌換類型：POINTS_ONLY |
| `POINTS_PLUS_CASH` | 400 | `CC-RWD-4000` | 未知兌換類型：POINTS_PLUS_CASH |
| `ONLINE_REDEEM` | 400 | `CC-RWD-4000` | 未知兌換類型：ONLINE_REDEEM |
| `MERGE` | 400 | `CC-RWD-4000` | 未知兌換類型：MERGE |
| `EXCHANGE_CENTER` | 400 | `CC-RWD-4000` | 未知兌換類型：EXCHANGE_CENTER |

**DDL CHECK 列舉**（`REDEMPTIONS.redemption_type`）——**6/6 全數 201 成功**：

| DDL 值 | HTTP | 備註 |
|---|---|---|
| `CASH`／`CATEGORY`／`GOODS`／`MILEAGE`／`FEE_WAIVER`／`DONATION` | 201 | 各扣 1 點 |

### 2.2 帳務自洽佐證

上表 6 筆成功兌換後，帳戶 0010 餘額 `100123 → 100117`（6 筆各扣 1 點）——
**服務行為本身自洽且正確，問題僅在「契約宣告的值域」與「實作接受的值域」不一致**。

### 2.3 錯誤攔截位置

`RedemptionType.fromCode()`（`domain/model`）在**應用層**即拋 `CC-RWD-4000`——
請求**進不了任何業務邏輯**（點數檢核、跨階段扣抵皆未執行）。即：依契約開發的客戶端
不是「部分功能不可用」，而是**兌換端點整個不可用**。

## 3. 爭議本質：**兩個語意維度被塞進同一個欄位**

| 維度 | 說明 | 值域 | 出現於 |
|---|---|---|---|
| **折抵方式**（How to pay） | 這筆兌換**怎麼付**：純點數／點數＋現金／線上兌換／合併折抵 | `POINTS_ONLY`／`POINTS_PLUS_CASH`／`ONLINE_REDEEM`／`MERGE`／`EXCHANGE_CENTER` | **契約** |
| **兌換標的類別**（What for） | 這筆兌換**換什麼**：現金／類別／商品／哩程／費用減免／捐贈 | `CASH`／`CATEGORY`／`GOODS`／`MILEAGE`／`FEE_WAIVER`／`DONATION` | **DDL CHECK**（`SDD-001`） |

⚠️ 兩組值域**不是同義詞互譯**——是正交的兩個維度。例：「用點數＋現金換哩程」
需要 **標的＝`MILEAGE`** ＋ **折抵＝`POINTS_PLUS_CASH`**，單一欄位無法同時表達。
故「單純把契約改成 DDL 值」會**永久失去折抵方式資訊**；「把 DDL 改成契約值」則
**失去標的類別資訊**且語意混雜。這是本 CR 必須由 BEAM PMO 裁定的原因——不是機械式對齊。

## 4. 處置選項

| 選項 | 內容 | 優點 | 缺點／代價 |
|---|---|---|---|
| **A：契約對齊 DDL**（本期最小變更） | 改 `gen_openapi.py` 之 `RedeemRequest.redemptionType` enum 為 DDL 6 值 → 重生契約 → 再實測；折抵方式語意**本期不承載**（`targets` JSON 已可彈性承載部分語意） | 與硬約束一致；與 `§10` 既定「以 DDL 為準」一致；不動 schema／遷移；BFF／WebUI 立即可用；工時最小 | 若客戶 FR 確有「折抵方式」要求，二期須再加（見選項 C） |
| **B：DDL 對齊契約** | 新遷移（V4+）卸除並重建 CHECK 為契約 5 值（**已套用之遷移不可改**，`#C-36` 慣例） | 契約不用改 | **失去標的類別資訊**（`FEE_WAIVER`／`DONATION` 等報表常用維度消失）；一欄混裝「怎麼付」語意；動權威 schema 代價大 |
| **C：雙欄位**（語意最正確） | `redemption_type` 維持 DDL 值域（標的）；**新增** `settlement_method` 欄位（折抵方式，契約值域） | 兩個維度各自單一真實來源；「點數＋現金換哩程」可完整表達 | 動 schema（新遷移）＋契約＋DTO＋程式＋測試，工時最大；**若本期無 FR 要求折抵方式，屬過度設計** |

## 5. 待確認事項（裁定前須核對）

1. **客戶 FR／BRD 是否要求「折抵方式」維度**——須回查 `BRD-002`（FR 68 項）與 RFP FR
   中兌換相關條款（如 Online Redeem、點數＋現金合併折抵）。若有 → 選項 C（或 A＋二期 C）；
   若無 → 選項 A。
2. `§10 #11` 之 **`CASH_VALUE` 固定 0**（折算率無來源）與本 CR 同屬「兌換契約欄位缺口」，
   建議**併案釐清**，避免同一契約改兩次。
3. 若核選項 A：執行順序依慣例為「改 `gen_openapi.py` → 重生 → **再實測** → 回寫本 CR ＋
   `SCS-010`」，**不手改 `contracts/openapi/*.yaml`**。

## 6. 建議

**本期採選項 A**（契約對齊 DDL，最小變更、解除「契約客戶端 100% 失敗」之實際阻塞）；
**選項 C 列為二期候補**，待第 5.1 節核對結果決定。**未經核定前，契約／DDL／程式皆不動。**

> ✅ **BEAM PMO 2026-09-22 裁定**：「實務上『用點數＋現金換哩程』兩個都要」→ **採選項 C（雙欄位）**，
> 並授權決定方向且執行。設計取 C 之 **C2 變體**：欄位命名與主檔互相對齊——
> `redemptionType` ↔ `REDEMPTIONS.REDEMPTION_TYPE`（標的類別，維持 DDL 6 值），
> **新增** `settlementMethod` ↔ `REDEMPTIONS.SETTLEMENT_METHOD`（折抵方式，契約 5 值）。
> 執行結果見 §8。

## 7. 修訂記錄

| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 1.0 | 2026-09-21 | 提案立案：`SCS-010 §9.3` 重跑實測證實契約 5 值全數被拒（400 `CC-RWD-4000`）、DDL 6 值全數 201；定性為「折抵方式 vs 兌換標的」兩維度脫鉤而非同義詞互譯；列 A／B／C 三選項與待確認事項，待 BEAM PMO 裁定 |
| 1.1 | 2026-09-22 | **BEAM PMO 授權裁定採 C2 雙欄位並執行完畢**：①V4 遷移新增 `SETTLEMENT_METHOD`（含 CHECK 與 DEFAULT）；②契約（改 `gen_openapi.py` 重生，checksum 比對確認**僅 rewards-service.yaml 變動**，驗證 0 錯誤）；③新增 `SettlementMethod` 列舉；④`RedeemRequest`／`RedemptionView`／`RedemptionOrder`／`RedemptionService`／`JdbcRedemptionRepository` 全鏈落地（缺省 → `POINTS_ONLY`，空字串／未知值 → `CC-RWD-4000` 不靜默降級）；⑤單測 **55 → 69 例 0 失敗**（新增 `SettlementMethodTest` 5、`OracleRawIdTest` 6、`RedemptionServiceTest` 8 → 11）；⑥真機 e2e 4 情境通過＋DB 落值複核（詳 §8）；⑦三支掃描器 0 命中、`validate_sql` 0 違規；⑧**WebUI 補「折抵方式」下拉並真機瀏覽器驗證**（§8.3，截圖存證） |

## 8. 執行結果（2026-09-22，真機 8091／`FREEPDB1`）

### 8.1 落地清單

| 層 | 變更 |
|---|---|
| 遷移 | `V4__redemptions_settlement_method.sql`：`ALTER TABLE redemptions ADD (settlement_method VARCHAR2(20 CHAR) DEFAULT 'POINTS_ONLY' NOT NULL, CONSTRAINT ck_redemptions_settlement_method CHECK (settlement_method IN (...5 值...)))`——啟動時 Flyway 自動套用 |
| 契約 | `gen_openapi.py`：`RedeemRequest.redemptionType` enum → DDL 6 值；新增選填 `settlementMethod`（default `POINTS_ONLY`）；`RedemptionView` 補 `redemptionType`／`settlementMethod`。**checksum 比對：17 份契約僅 rewards-service.yaml 變動；驗證 0 錯誤（72 端點）** |
| 領域 | 新增 `SettlementMethod`（5 值＋`DEFAULT_METHOD`＋`fromCode` 拋 `CC-RWD-4000`）；`RedemptionOrder` 增欄（`create`／`reconstitute` 同步） |
| 應用 | `RedeemRequest` 增選填 `settlementMethod`；`RedemptionService`：null → `POINTS_ONLY`（與主檔 DEFAULT 同值），空字串／未知值 → `CC-RWD-4000` 不靜默降級 |
| 基礎設施 | `JdbcRedemptionRepository`：`COLUMNS`／兩支 INSERT／`save`／`mapRow` 全部帶入新欄 |
| 檢視 | `RedemptionView` 補 `redemptionType`／`settlementMethod`（回應可自證兩維度） |
| WebUI | `RewardsConsole.tsx` 兌換表單新增「**折抵方式（怎麼付）**」下拉（5 值含中文說明，預設 `POINTS_ONLY`）；「兌換類型」改名「**兌換標的類別（換什麼）**」；結果區顯示 `標的類別／折抵方式`；`api/rewards.ts` `redeemPoints` 增參數並帶入 body。`typecheck` 0 錯誤、`build` 成功（290.53 kB／gzip 84.32 kB） |

### 8.2 真機 e2e（4 情境，帳戶 0010）

| # | 情境 | 結果 |
|---|---|---|
| 1 | `redemptionType=MILEAGE` ＋ `settlementMethod=POINTS_PLUS_CASH`（「用點數＋現金換哩程」） | **201**；回應與 DB（`RD2026090016`）兩欄同值 ✅ |
| 2 | 不帶 `settlementMethod` | **201**；補預設 `POINTS_ONLY`，DB 同值（`RD2026090017`）✅ |
| 3 | `settlementMethod=BOGUS` | **400 `CC-RWD-4000`**（不靜默降級）✅ |
| 4 | `redemptionType=POINTS_ONLY`（原契約值作標的類別） | **400 `CC-RWD-4000`**（標的類別未知）✅ |

**帳務自洽**：0010 餘額 `100117 → 100103`（2 筆成功各扣 7 點）。

### 8.3 WebUI 端到端（真機瀏覽器＋Keycloak 登入，2026-09-22）

以 `demo.csr` 經 Keycloak 登入（Vite 5173 → `/rewards` 代理 → 8091），於表單選
標的 `MILEAGE` ＋ 折抵 `POINTS_PLUS_CASH`、兌換 15 點：畫面回 `RD2026090018`／
**`MILEAGE／POINTS_PLUS_CASH`**／`CONFIRMED`、可兌換 `7,500 → 7,485`；
DB 直查同值。**「用點數＋現金換哩程」自畫面至主檔全程可表達、可稽核**。
截圖：`docs/08-evidence/webui-rewards-dual-field-01.png`。

### 8.4 併案完成：`SCS-010 §10 #11` 之 `CASH_VALUE`

❌ **未併案處理**——`CASH_VALUE`（折算率來源）與本 CR 之「值域脫鉤」屬不同問題，
本次僅修值域；`CASH_VALUE` 維持固定 0，仍待 BEAM PMO 裁定（見 `SCS-010 §10 #11`）。
1.0 版建議「併案釐清」，實際判定**不併**：兩者改動面（CHECK vs 金額來源）無交集，
併案反而使本 CR 驗證範圍擴大、回歸風險變高。
