Project

General

Profile

CR #213 » CR-012_兌換類型契約列舉與DDL_CHECK完全脫鉤.md

內容更新:內部稱謂去識別化(2026-10-18) - Redmine Admin, 10/18/2026 05:32 AM

 

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 驗證範圍擴大、回歸風險變高。

    (1-1/1)