CR #220 » CR-019_契約與實作逐端點核對之檢查點建置與19處不符登記.md
CR-019 契約 ↔ 實作「逐端點核對」之檢查點建置,與 19 處不符之登記與分流
| 項目 | 內容 |
|---|---|
| 文件編號 | CR-019 |
| 版本 | 1.2(§3.8:批 1 之 15 案已執行並真機驗證**——19 處 → 2 處;§3.6.6 之批 2(C-1)與新增 C-4 待裁定)** |
| 提出日 | 2026-09-23 |
| 提出者 | WorkBuddy AI(開發團隊) |
| 緣起 |
GOV-006 §4 #14——CR-016 §8 與 CR-018 於同一日、同一次 C-14 動工前查證中先後被發現,顯示「契約 vs 實作之逐端點核對」尚未形成例行檢查點 |
| 影響文件 | 新增 tools/verify_contract_dto.py;CSP-001(品質關卡);各服務契約(tools/gen_openapi.py)與 DTO |
| 關聯前例 |
CR-016 §8(回應型別:實作回陣列、契約誤宣告單一物件)、CR-018(實作有欄位、契約無) |
1. 為什麼需要(問題定性)
CR-016 §8 與 CR-018 屬同一類缺陷,且其特性使它們極難被既有關卡攔下:
| 特性 | 說明 |
|---|---|
| 編譯不會錯 | 契約是 YAML、DTO 是 Java,兩者無編譯期關聯 |
| 啟動不會錯 | 契約不參與執行期繫結 |
| 單測不會錯 | 單測驗的是實作行為,不驗「契約是否描述實作」 |
| 只有人逐欄看才會發現 | 故缺陷會累積,直到有消費端(如 WebUI)依契約實作才爆 |
→ 結論:必須把它變成可重複執行的關卡,不能依賴人的記憶。
2. 交付:tools/verify_contract_dto.py(零相依)
| 項 | 內容 |
|---|---|
| 檢查項 |
A. 回應型別:契約該端點成功回應之 data(陣列?元素 schema)vs Controller 回傳型別B. 欄位集合:契約 components.schemas.<Name>.properties vs Java record <Name>(…) 元件(雙向) |
| 設計 |
零相依(本環境無 PyYAML,已實測)→ 自製最小 YAML 子集解析器(格式由 gen_openapi.py 之 yaml_dump 固定產生)解析失敗一律拋錯,不靜默略過(靜默=看起來有把關、實則沒擋=假綠) |
| 判定分流 |
hits=確定不符(陣列性不符/欄位不符)→ 須修warns=需人工確認(型別名不符但欄位一致=僅命名差異;JSON 之契約效力在欄位名而非類別名)infos=無對應 DTO/未找到端點,屬正常或無法判定 |
| 人工樣本回測 |
--selftest:4 案例(陣列性不符/DTO 有契約無/契約有 DTO 無/對齊無缺陷)→ 4/4 通過
|
| 用法 |
python3 tools/verify_contract_dto.py(全庫)/--service <name>/--selftest/--show-info
|
2.1 建置過程中修正之自身缺陷(誠實揭露,皆為「差點產生偽陽性」)
| # | 缺陷 | 症狀 | 修正 |
|---|---|---|---|
| a | 最小 YAML 解析器之終止條件誤用 paths:
|
產生之契約 paths: 在 components: 之前 → 一進檔案就 break,全部服務誤報「解析失敗」
|
改為「先進入 components/schemas 區才開始收」 |
| b |
components: 之下 parameters: 先於 schemas:
|
看到 parameters: 即把 in_schemas 關掉後再也進不來
|
旗標拆為 in_components/in_schemas,先判 ind==2 再判 in_schemas
|
| c | 非貪婪 (.*?)> 解析 ApiResponse<List<X>>
|
在第一個 > 就停 → 得到 List<X,元素型別被截斷 → 12 處偽陽性 |
改深度計數取最外層泛型內容 |
| d | 未解析具名陣列 schema |
AuthorizationSummaryList 之 type: array + items: $ref X → 誤判「契約單一物件 vs 實作陣列」 |
parse_contract_schemas 一併解析 type/items,端點側做 resolve()
|
| e |
resolve() 覆蓋端點內聯陣列之判定 |
端點已偵測為 type: array,卻被「元素 schema 非陣列」覆蓋 → clearing/statement/pricing 偽陽性 |
兩者取 OR(兩種寫法都要認) |
| f | 只掃 application/dto/
|
DTO 常為 Controller/Service 之巢狀 record(CardStatusView 在 CardQueryService、SpecialLimitsResponse 在 LimitAdjustmentController)→ 產生「無法比對欄位」之無效警告 |
改遞迴掃描整個 src/main/java
|
| g | 命中訊息中之 ** 粗體切開關鍵詞 |
回測斷言比對不到(陣列性不符 被切成 陣列性**不符) |
訊息用詞去除行內粗體 |
⚠️ 教訓(比缺陷本身重要):本工具首輪產出 12 處偽陽性——若不逐一鑑別就當成 12 個缺陷回報,
將嚴重損害可信度。掃描器之「判定力」必須以人工樣本回測,且真實輸出須逐筆鑑別,
不可直接採信(此即HANDOFF所載「別輕信掃描器皆 0 命中」之對偶原則)。
3. 掃描結果:19 處不符(2026-09-23,17 服務)
命令:
python3 tools/verify_contract_dto.py(回測 4/4 通過後執行)
3.1 分類 A:回應陣列性不符(契約單一物件、實作回集合)— 1 處
| 服務 | 端點 | 契約 | 實作 | 判定 |
|---|---|---|---|---|
posting |
GET /api/v1/accounts/{accountId}/transactions |
$ref TransactionView(單一) |
List<TransactionView>(BalanceQueryController) |
契約缺陷(與 CR-016 §8 同型;語意為「分頁查詢交易」=清單) |
3.2 分類 B:回應型別名不符且欄位不符 — 6 處
| 服務 | 端點 | 契約宣告 | 實作回 | 契約有而實作無 | 實作有而契約無 |
|---|---|---|---|---|---|
card |
GET /cards/{cardId} |
CardView |
CardStatusView |
partyId/productCode/bin/instantIssued/tokens/createdAt
|
transactable/expired
|
card |
POST /cards/{cardId}/lifecycle-transitions |
CardView |
CardStatusView |
同上 | 同上 |
card |
POST /card-applications |
CardApplicationView |
ApplicationResult |
—(欄位齊) | replayed |
card |
GET /card-applications/{applicationId} |
CardApplicationView |
ApplicationResult |
— | replayed |
card |
POST /card-applications/{applicationId}/transitions |
CardApplicationView |
ApplicationResult |
— | replayed |
rewards |
POST /reward-rules |
RedemptionView(兌換視圖!) |
RewardRuleView |
12 欄(redemptionId…cashValue…) |
7 欄(ruleId…effectiveTo) |
rewards POST /reward-rules為最明確之契約缺陷:端點為「設定回饋規則」,
契約卻宣告兌換視圖——兩者欄位完全不相交。此為gen_openapi.py之端點宣告誤引 schema。
3.3 分類 C:欄位集合不符(同名 schema ↔ 同名 DTO)— 12 處
| 服務 | schema | 契約有、DTO 無 | DTO 有、契約無 |
|---|---|---|---|
authorization |
AuthorizeRequest |
merchantName/entryMode/emvData
|
— |
card |
CardView |
createdAt |
— |
card |
TransitionRequest |
— | operatorId |
installment |
InstallmentPeriod |
— | principalAmount |
limit |
HoldRequest |
— | currency |
limit |
HoldView |
availableAmount |
authorizationId/currency/createdAt
|
limit |
LimitView |
specialLimits |
holdAmount |
limit |
ReleaseRequest |
releaseAmount/reason
|
amount/currency
|
posting |
SubAccountEntry |
— | currency |
⚠️ 分類 C 須逐案判定方向(三種可能):
①契約過時(實作已改、契約未同步)→ 改契約;
②實作漏做(契約有、實作未接)→ 改實作;
③刻意不對外(如內部欄位)→ 應於契約加註或自 DTO 分離,不可留白。
例:limit ReleaseRequest之releaseAmount/reasonvsamount/currency屬命名不一致,
消費端依契約送releaseAmount將不被接收——極可能是真缺陷。
3.4 需人工確認(1 則,非缺陷)
| 服務 | 端點 | 說明 |
|---|---|---|
limit |
GET /accounts/{accountId}/special-limits |
契約 SpecialLimitsView vs 實作 SpecialLimitsResponse——欄位完全一致,僅類別命名差異(JSON 契約效力在欄位名,故不影響消費端;惟建議對齊命名以免誤導) |
3.5 ⚠️ 分類 D:同名字段之語意不一致(本工具無法偵測,於 C-14 批次二實測中發現)— 1 處
發現途徑:
C-14批次二之商店管理頁實測(非本工具)——建檔後畫面顯示特店代碼MERCH000002,
但商店請款成功後,回應之merchantId卻是 UUIDc9f1a464-…。
| 服務 | 欄位 | 契約 | 實作 | 語意 |
|---|---|---|---|---|
merchant |
MerchantView.merchantId |
string(未指明格式) |
MERCHANTS.MERCHANT_ID |
業務代碼(MERCH000002) |
merchant |
MerchantSettlementRequest.merchantId |
string(未指明格式) |
findByMerchantId(...) 查 MERCHANTS.MERCHANT_ID
|
業務代碼(一致 ✓) |
merchant |
MerchantSettlementView.merchantId |
string(未指明格式) |
MERCHANT_SETTLEMENTS.MERCHANT_ID ← merchant.id()
|
⚠️ UUID(RAW(16) 之代理主鍵) |
實測證據(2026-09-23):
| 證據 | 值 |
|---|---|
| 畫面(建檔結果) | 特店代碼 MERCH000002
|
| 畫面(請款結果) | 特店代碼 c9f1a464-097e-3c15-824b-958820b17870
|
DB MERCHANT_SETTLEMENTS.MERCHANT_ID
|
C9F1A464097E3C15824B958820B17870(=MERCHANTS.ID) |
| 程式 |
SettlementService:findByMerchantId(request.merchantId()) → settlement.submit(..., merchant.id(), ...)
|
影響:消費端若做往返(讀請款結果之 merchantId → 回頭查 GET /merchants/{merchantId})將 404,
因為前者是 UUID、後者要業務代碼。同一欄位名在兩張視圖中代表不同識別碼,
且契約未以 format/pattern 區分——契約本身亦未盡描述之責。
建議修正方向(待裁定):三選一——
(a) 回應改回業務代碼(與 MerchantView 一致;最小驚訝原則);
(b) 保留 UUID 但契約加 format: uuid,並另加 merchantCode 欄位回傳業務代碼;
(c) 兩者皆回(merchantId=UUID、merchantCode=業務代碼),契約明載。
建議 (a)——本案其他服務之「對外識別碼」慣例為業務代碼(如 DSP202609000002/SET202609000003)。
⚠️ 本工具之能力邊界(誠實揭露):分類 D 屬「欄位名相同、型別相同、語意不同」,
本工具之比對維度為名稱與集合,結構上無法偵測。要涵蓋此類需引入
語意標註(如契約format/pattern/description與 DTO 之@Schema對照)或人工複核——
列後續(見 §7 限制 5)。
3.6 ★ 逐案判定表(供一次核定;契約與 DTO 原文對照+預判方向+影響面)
本表用途:BEAM PMO 裁定「先出逐案判定表再一次核定,避免逐筆來回」。
19 處 hits + 1 則 warn = 20 案,逐案列出:位置|契約原文|DTO 原文|差異|預判方向|影響面。
方向代號:(改契)=改tools/gen_openapi.py後重生;(改實)=改實作;(待決)=需您定方向。
3.6.1 分類 A:回應陣列性(1 案)
| 案 | 位置 | 契約原文 | 實作原文 | 預判方向 | 影響面 |
|---|---|---|---|---|---|
| A-1 |
posting GET /accounts/{accountId}/transactions
|
data: $ref TransactionView(單一物件) |
ApiResponse<List<TransactionView>>(BalanceQueryController) |
(改契)→ TransactionView[]
|
消費端依契約視為單一物件 → 型別錯誤、取不到清單。端點語意為「分頁查詢交易」=清單(與 CR-016 §8 同型,方向無疑義) |
3.6.2 分類 B:回應型別名與欄位不符(6 案)
| 案 | 位置 | 契約宣告 | 實作回 | 差異 | 預判方向 | 影響面 |
|---|---|---|---|---|---|---|
| B-1 |
card GET /cards/{cardId}
|
CardView(13 欄:cardId,accountId,partyId,cardNoMasked,productCode,bin,expiryDate,blockCode,status,instantIssued,ticketType,tokens,createdAt) |
CardStatusView(9 欄:cardId,accountId,cardNoMasked,blockCode,status,expiryDate,ticketType,transactable,expired) |
契約有而實作無 6 欄(partyId,productCode,bin,instantIssued,tokens,createdAt);實作有而契約無 2 欄(transactable,expired) |
(改契)→ 指 CardStatusView
|
消費端預期 13 欄、實得 9 欄 → partyId/productCode 等取不到。⚠️ CardStatusView 契約早已宣告(供 GET /cards/lookup/{cardNoHash} 用)→ 實作與該端點一致,契約未同步
|
| B-2 |
card POST /cards/{cardId}/lifecycle-transitions
|
同上 CardView
|
同上 CardStatusView
|
同上 |
(改契)→ 指 CardStatusView
|
同上(狀態轉換後回狀態視圖合理) |
| B-3 |
card POST /card-applications
|
CardApplicationView(11 欄) |
ApplicationResult(12 欄=11 欄 + replayed) |
實作多 replayed
|
(改契)→ 新增 ApplicationResult schema(含 replayed) |
消費端無從得知是否為冪等重播(replayed 未宣告);反之契約宣告之欄位實作全有 → 低風險但應對齊 |
| B-4 |
card GET /card-applications/{applicationId}
|
同上 | 同上 | 同上 |
(改契);⚠️ 惟 GET 之 replayed 語意存疑 → 建議保留(恆 false)以簡化,或拆分兩型別 |
同上 |
| B-5 |
card POST /card-applications/{applicationId}/transitions
|
同上 | 同上 | 同上 | (改契) | 同上 |
| B-6 |
rewards POST /reward-rules
|
RedemptionView(13 欄:redemptionId,redemptionNo,redemptionType,settlementMethod,status,pointsUsed,amountPaid,cashValue,pointsBefore,pointsAfter,merchantShare,authorizationId,redeemedAt) |
RewardRuleView(7 欄:ruleId,ruleCode,ruleType,status,priority,effectiveFrom,effectiveTo) |
欄位完全不相交(僅 status 同名) |
(改契)→ 新增 RewardRuleView schema 並指之 |
最嚴重:端點為「設定回饋規則」,契約卻宣告兌換視圖。消費端/前端完全無法依契約解析;且 RewardRuleView 未在契約中宣告 → 前端無型別可依 |
3.6.3 分類 C:欄位集合(12 案)
| 案 | 位置(schema) | 契約原文 | DTO 原文 | 預判方向 | 影響面 |
|---|---|---|---|---|---|
| C-1 |
authorization AuthorizeRequest
|
11 欄(含 merchantName,entryMode,emvData) |
8 欄(stan,cardNo,txType,amount,currency,mcc,merchantId,txTime) |
(待決) | ⚠️ 三欄實作未接收。Spring Boot 預設忽略未知欄位 → 消費端送 emvData 被靜默丟棄(客戶以為送了 EMV 資料)——「靜默不做」型風險。且 emvData/entryMode 屬 13 大項檢核之已知未實作(SCS-001 §10 L-1)。建議:契約加註「本期未實作」(誠實揭露)+ SCS-001 §10 記明;不建議現在補實作(需 EMV 資料來源) |
| C-2 |
card CardView
|
13 欄(含 createdAt) |
12 欄 | **(改契)**移除 createdAt
|
消費端依契約取 createdAt 得 null。實作之 CardView.of(IssueResult) 未帶此欄;cards.created_at 存在但未回傳 → 移除為誠實(若需,另立 CR 補實作) |
| C-3 |
card TransitionRequest
|
event,reason,threeFactors
|
+operatorId
|
**(改契)**補 operatorId
|
消費端無從得知可傳操作者(稽核留痕;SCS-024 要求 operatorId)。屬契約漏列 |
| C-4 |
installment InstallmentPeriod
|
periodNo,amount,feeAmount,dueDate,status,remainingPrincipal
|
+principalAmount
|
**(改契)**補 principalAmount
|
攤還表核心欄位(每期本金)未宣告 → 前端無法呈現「本期本金」(SDD-007 §5.4.8 之攤還表要求此欄) |
| C-5 |
limit HoldRequest
|
accountId,authorizationId,holdAmount,holdType
|
+currency
|
**(改契)**補 currency
|
消費端不知可指定幣別 → 多幣別佔用無法依契約表達 |
| C-6 |
limit HoldView
|
holdId,accountId,holdAmount,releasedAmount,status,availableAmount
|
+authorizationId,currency,createdAt(無 availableAmount) |
(改契)補三欄;availableAmount 建議(改契)移除 |
①消費端取 availableAmount 得 null(契約宣告不存在之欄位);②實作已有之三欄未宣告(含 authorizationId 可回查授權)。⚠️ 移除 availableAmount 係因實作需額外查主檔;若您認為該欄必要,改為**(改實)補** |
| C-7 |
limit LimitView
|
11 欄(含 specialLimits) |
+holdAmount(無 specialLimits) |
(改契)補 holdAmount;specialLimits 建議(改契)移除 |
①消費端取 specialLimits 得 null;②specialLimits 已有專用端點 GET /accounts/{id}/special-limits → 兩處來源易漂移,移除為宜(單一真實來源) |
| C-8 |
limit ReleaseRequest
|
releaseAmount,reason
|
amount,currency
|
(改契)→ amount+currency;移除 reason
|
⚠️ 消費端依契約送 releaseAmount 不會被綁定 → amount 為 0 → @Min(1) 擋下(400)。屬真缺陷。reason 移除係因 LIMIT_HOLDS 無該欄(SCS-002 §5)且規格未要求;若需稽核原因,須另立 CR 加欄(V4 遷移) |
| C-9 |
posting SubAccountEntry
|
subAccount,amount,beforeAmount,afterAmount
|
+currency
|
**(改契)**補 currency
|
分錄之幣別未宣告 → 消費端無法判斷分錄幣別(多幣別分戶時尤需) |
3.6.4 分類 D:僅命名差異(1 案,非缺陷)
| 案 | 位置 | 契約 | 實作 | 預判方向 | 影響面 |
|---|---|---|---|---|---|
| D-1 |
limit GET /accounts/{accountId}/special-limits
|
SpecialLimitsView(accountId,count,items) |
SpecialLimitsResponse(欄位完全一致) |
**(改實)**將 record 改名為 SpecialLimitsView
|
無(JSON 契約效力在欄位名,類別名不影響消費端);改名僅為一致性,零風險(不改 JSON)。⚠️ 此為我方 C-11 交付時之命名不一致 |
3.6.5 分類 E:同名字段語意不一致(1 案,已裁定)
| 案 | 位置 | 說明 | 方向 | 影響面 |
|---|---|---|---|---|
| E-1 |
merchant merchantId
|
MerchantView.merchantId=業務代碼(MERCH000002);MerchantSettlementView.merchantId=UUID(c9f1a464-…) |
✅ 已裁定採 (a):回應改回業務代碼 | 消費端往返(讀請款之 merchantId → 查商戶)現為 404;修正後可往返。完整證據見 §3.5;執行結果見 §3.7
|
3.6.6 建議之執行分批(供核定)
| 批 | 內容 | 案數 | 理由 |
|---|---|---|---|
| 批 1(方向無疑義) | A-1、B-1~B-6、C-2~C-9、D-1 | 16 | 皆為「契約過時/漏列」或「零風險命名對齊」,方向無設計空間 |
| 批 2(需您定方向) |
C-1(AuthorizeRequest 三欄:加註未實作 vs 補實作) |
1 | 涉及「是否揭露未實作」之取捨 |
| 批 3(已裁定) | E-1(merchantId 語意) |
1 | 您已裁定 (a) |
| — | C-6 之 availableAmount/C-7 之 specialLimits
|
(含於批 1) | 建議移除;若您認為應補實作,請於核定時指示 |
3.7 分類 E(E-1)之執行結果(2026-09-23 完成,BEAM PMO 已裁定採 (a))
修正內容
| # | 檔案 | 變更 |
|---|---|---|
| 1 | MerchantViewAssembler.toSettlementView(settlement, merchantCode) |
簽名加參數;merchantId 改回傳傳入之業務代碼(原為 settlement.merchantId().toString()=UUID) |
| 2 | MerchantSettlementService.submit |
①新建路徑:直接用已載入之 merchant.merchantId()(業務代碼),無須再查;②冪等重播路徑:新增 resolveMerchantCode(UUID)——以 merchantRepository.findById(UUID) 回查業務代碼;⚠️ 查無即拋 IllegalStateException(fail-loud),不靜默回退為 UUID(否則缺陷再次潛伏) |
| 3 | tools/gen_openapi.py |
MerchantView/MerchantSettlementRequest/MerchantSettlementView 之 merchantId 三處補 description 明載語意(「特店業務代碼(如 MERCH000002)——非資料庫主鍵 UUID…可安全往返」)→ 防再犯 |
| 4 | 契約重生 | 影響範圍嚴格驗證:cp -r 基準 → 重生 → diff -rq 全目錄比對 → 僅 merchant-service.yaml 變動(3 行 description);OpenAPI 0 錯誤
|
設計依據(不是我方臆測):MerchantRepository#findByMerchantId 之 javadoc 早已明載
「對外 API 之 merchantId 即業務代碼(如 MERCH000001),非資料庫主鍵」
——即本缺陷係實作偏離自身已載明之設計意圖,故 (a) 為回歸設計而非新決策。
真機 e2e(8095,3 情境)
| # | 情境 | 結果 |
|---|---|---|
| ① | 商店請款(新冪等鍵) |
201/SET202609000004/merchantId: "MERCH000002"(修正前為 UUID) |
| ② |
同鍵重播(測 resolveMerchantCode 路徑) |
201/同 settlementId/merchantId: "MERCH000002"
|
| ③ |
往返驗證:以回應之 merchantId 回查 GET /merchants/{merchantId}
|
200(修正前為 404)——{"merchantId":"MERCH000002","merchantName":"示範特店",…}
|
DB 複核(證明未改寫資料、僅於組裝時解析)
| 請款單 |
MERCHANT_ID(DB) |
解析後代碼 |
|---|---|---|
SET202609000001 |
241442B3D98B3E56A64D317FC4E547B5(UUID) |
MERCH000001 |
SET202609000002 |
同上 | MERCH000001 |
SET202609000003 |
C9F1A464097E3C15824B958820B17870(UUID) |
MERCH000002 |
SET202609000004(本次) |
同上 | MERCH000002 |
→ ①欄位仍為 UUID(未動 DDL、未做資料遷移);②既有 3 列亦可解析出代碼 → 無回填需求。
品質:merchant-service 單測通過(BUILD SUCCESSFUL);契約 0 錯誤;verify_contract_dto --service merchant-service 0 不符。
殘留風險(誠實揭露):resolveMerchantCode 依賴 MERCHANTS 與 MERCHANT_SETTLEMENTS 之參照完整性,
惟 DDL 未建外鍵(本專案跨表一律以應用層保證)。若日後出現孤兒請款單,將拋錯而非靜默——
此為刻意之 fail-loud,並已於方法 javadoc 載明。
3.8 批 1 之執行結果(2026-09-23 完成,15 案)
執行範圍:§3.6.6 之批 1 16 案中,C-4 經查證後剔除(見 §3.8.3),實際執行 15 案。
3.8.1 變更清單
| 案 | 變更 | 類型 |
|---|---|---|
| A-1 |
posting GET /accounts/{accountId}/transactions 回應 → TransactionView[]
|
契約 |
| B-1/B-2 |
card GET /cards/{cardId}/POST …/lifecycle-transitions 回應 → CardStatusView
|
契約 |
| B-3~B-5 |
card 申請案件 3 端點回應 → 新增 ApplicationResult(= CardApplicationView + replayed) |
契約(新增 schema) |
| B-6 |
rewards POST /reward-rules 回應 → 新增 RewardRuleView(原誤引 RedemptionView) |
契約(新增 schema) |
| C-2 |
card CardView 移除 createdAt(實作無此欄) |
契約 |
| C-3 |
card TransitionRequest 補 operatorId
|
契約 |
| C-5 |
limit HoldRequest 補 currency
|
契約 |
| C-6 |
limit HoldView 補 authorizationId/currency/createdAt、移除 availableAmount
|
契約 |
| C-7 |
limit LimitView 補 holdAmount、移除 specialLimits(已有專用端點,單一真實來源) |
契約 |
| C-8 |
limit ReleaseRequest → amount+currency(原 releaseAmount/reason) |
契約 |
| C-9 |
posting SubAccountEntry 補 currency
|
契約 |
| D-1 |
limit record 改名 SpecialLimitsResponse → SpecialLimitsView(欄位一致,零風險) |
實作 |
做法:一律改 tools/gen_openapi.py(不可手改 contracts/openapi/*.yaml)→ 重生 → 以 cp -r + diff -rq 證明影響範圍(僅 posting/card/rewards/limit 四檔)→ 重跑比對器。
3.8.2 驗證結果
| 項 | 結果 |
|---|---|
| 比對器 | 19 處 → 2 處(剩餘恰為排除之兩案:C-1 待裁定、C-4 已剔除) |
| OpenAPI 驗證 | 0 錯誤/74 端點 |
| 影響範圍 |
diff -rq 確認僅 4 檔(+merchant 於 §3.7 已改) |
limit-service 單測+bootJar
|
BUILD SUCCESSFUL |
| 真機回歸實測(4 端點) | ①GET /accounts/{id}/transactions → 200/data 為 list(3 筆),與新契約 type: array 相符② GET /cards/{cardId} → 200,回應欄位 cardId,accountId,cardNoMasked,blockCode,status,expiryDate,ticketType,transactable,expired——含 CardStatusView 之 transactable/expired、不含 CardView 之 partyId/productCode③ POST /reward-rules → 201,欄位 恰為 ruleId,ruleCode,ruleType,status,priority,effectiveFrom,effectiveTo(=新契約 RewardRuleView)④ GET /accounts/{id}/special-limits → 200,欄位 accountId,count,items(改名後正常) |
3.8.3 ⚠️ C-4 之判定方向更正(由批 1 剔除,改列待裁定)
§3.6.3 原判 C-4(installment InstallmentPeriod 之 principalAmount)為「改契約補」。
執行前查證發現該判定有誤:
| 證據 | 內容 |
|---|---|
| 程式 |
InstallmentPlan 產生期數:new InstallmentPeriod(i, principal, principal, fee, …)——第 1、2 參數同為 principal
|
| DDL |
INSTALLMENT_PERIODS 有 amount 與 principal_amount 兩欄(皆 NOT NULL),值相同 |
| 規格 |
SCS-009 §2/§7 僅定義「每期本金/手續費/到期日/剩餘本金」——無「應繳總額」之概念
|
| 契約 |
amount 之描述明載「本期本金…手續費另見 feeAmount」 |
→ principalAmount 係 amount 之重複欄位(DDL 冗餘),契約省略它是正確的。
若照原判定「補進契約」,將使 API 出現兩個語意相同的欄位(消費者混淆)。
⚠️ 仍待裁定之設計問題:INSTALLMENT_PERIODS.AMOUNT 之設計意圖究竟是
(a) 本期本金(=現行實作與契約描述,則 PRINCIPAL_AMOUNT 為冗餘欄,建議 V3 清理)
還是 (b) 本期應繳總額=本金+手續費(則現行實作與契約描述皆為缺陷)?
→ 此非機械性修正,須設計確認(已登錄 GOV-006 §4 #17)。
3.8.4 ⚠️ 施行中自身事故(誠實揭露)
事故:以 str.replace(old, new, 1) 批次修補產生器時,C-9 之錨點不唯一——
beforeAmount/afterAmount 兩行連續之樣式同時存在於 limit-service 之 LimitAdjustmentView,
而 replace(..., 1) 取第一處,致 currency 被加進 LimitAdjustmentView(錯)、
posting 之 SubAccountEntry(對)未加。比對器隨即顯示 2 個異常命中(新增 LimitAdjustmentView、未消 SubAccountEntry)→ 逐檔 diff 後定位並更正。
教訓(已寫入 skill):批次文字替換之錨點必須唯一——
應先 assert s.count(old) == 1(或加入足以辨識之上下文行),不可預設「看起來夠特別」。
本專案多處 schema 有相同欄位序列(beforeAmount/afterAmount、holdAmount/holdType 等),風險高。
4. 執行清單
| # | 動作 | 狀態 |
|---|---|---|
| 1 | 建置 tools/verify_contract_dto.py(零相依) |
✅ |
| 2 | 人工樣本回測(4 案例) | ✅ 4/4 通過 |
| 3 | 全庫掃描並逐筆鑑別(剔除自身偽陽性 5 類) | ✅ 19 處確定、1 則需確認 |
| 4 | 本 CR 登記 | ✅ |
| 5 |
CSP-001 §7.1 增列檢查點 G-8(含實作紀律三條) |
✅ |
| 6 | 19 處之逐案分流與修正 | 🟡 批 1 之 15 案 ✅ 已執行(見 §3.8;19 處 → 2 處);⬜ 批 2(C-1)待裁定;⬜ C-4 已剔除並改列待裁定(見 §3.8.3) |
| 7 | 分類 D(§3.5)之發現與登記 | ✅ 已登記(於 C-14 批次二實測中人工發現,非工具) |
| 8 | §3.6 逐案判定表 | ✅ 已產出(20 案,含契約與 DTO 原文對照+預判方向+影響面) |
| 9 | E-1(merchantId 語意)之修正與驗證 |
✅ 已執行(BEAM PMO 裁定採 (a))——見 §3.7 |
5. 檢查點之制度化
於 CSP-001 §7.1(Quality Gate)增列 G-8:契約 ↔ 實作逐端點核對——
新增/修改端點或回應 DTO 時,須執行 verify_contract_dto.py 並使 --service <name> 為 0 不符。
理由:本 CR §1 之四項特性(編譯/啟動/單測皆不報),唯有此關卡可攔。
6. 待核定(逐案判定表)
已產出 §3.6 逐案判定表(20 案),供 BEAM PMO一次核定,避免逐筆來回。建議之執行分批:
| 批 | 內容 | 案數 | 理由 | 狀態 |
|---|---|---|---|---|
| 批 1(方向無疑義) | A-1、B-1~B-6、C-2~C-3、C-5~C-9、D-1 | 15 | 皆為「契約過時/漏列」或「零風險命名對齊」,無設計空間 | ✅ 已執行(§3.8;原列 16 案,C-4 剔除) |
| 批 2(需定方向) |
C-1(AuthorizeRequest 之 merchantName/entryMode/emvData) |
1 | 涉及「加註未實作(誠實揭露)vs 補實作(需 EMV 資料來源)」之取捨 | ⬜ 待核定 |
| 批 3(已裁定並執行) | E-1(merchantId 語意) |
1 | BEAM PMO 2026-09-23 裁定採 (a) | ✅ 已完成(§3.7) |
| — | C-6 之 availableAmount/C-7 之 specialLimits
|
(含於批 1) | 已依建議移除(契約不宣告實作未提供之欄位;specialLimits 已有專用端點) |
✅ 已執行 |
| 批 4(新增,需設計確認) |
C-4(INSTALLMENT_PERIODS.AMOUNT 之設計意圖:(a) 本期本金 vs (b) 本期應繳總額) |
1 | 執行前查證發現原判定有誤(principalAmount 係 amount 之重複欄位)→ 已剔除;屬設計問題非機械修正(見 §3.8.3) |
⬜ 待裁定(GOV-006 §4 #17) |
⚠️ 批 2(C-1)之風險特別說明:Spring Boot 預設忽略未知請求欄位,
故消費端送emvData/entryMode/merchantName會被靜默丟棄——
客戶以為送了 EMV 資料,實際未檢核。此屬「靜默不做」型風險(HANDOFF所載反模式)。
我方建議:契約加註「本期未實作」(description明載)+SCS-001 §10記明,
不建議現在補實作(EMV/entryMode 需資料來源,屬 13 大項檢核之既有未實作範圍L-1)。
7. 誠實揭露
- 本 CR 僅建置工具與登記,未修改任何契約或實作——因多數項目之修正方向涉及對外行為契約。
- 工具之已知限制:
-
不檢查請求端(
requestBody之 schema 未與 Request DTO 比對)——本期僅做回應側與 schema 欄位;請求側列後續。 -
端點比對以「路徑+動詞」為鍵,若 Controller 之
@RequestMapping寫法特殊(如多路徑、params條件)可能配不上 → 列為INFO,不偽裝成通過。 -
欄位比對只看名稱、不看型別/必填(
requiredvs@NotNull)——列後續。 - 無法判定 Java 端之
@JsonInclude/@JsonProperty改名(本期專案未使用)。 -
無法偵測「同名字段之語意不一致」(分類 D,見 §3.5)——比對維度為名稱與集合,
名稱相同、型別相同但語意不同(如業務代碼 vs UUID)者結構上測不到。
此類為C-14批次二實測時人工發現,證明「工具不可取代端到端實測」。
-
不檢查請求端(
- 本 CR 之 19 處尚未逐案鑑別真偽(僅抽驗 1 處為真:
InstallmentPeriod.principalAmount);
分類 C 之方向判定須看實作與契約原文,列 §6 之判定表作業。
8. 修訂記錄
| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 1.2 | 2026-09-23 |
批 1 之 15 案執行完成並真機驗證(見 §3.8)——依 BEAM PMO「請繼續」:①契約修正 14 案(A-1/B-1~B-6/C-2/C-3/C-5~C-9)+實作 1 案(D-1 record 改名);一律改 gen_openapi.py → 重生 → cp -r + diff -rq 證明影響範圍(僅 4 檔)。比對器 19 處 → 2 處(剩餘恰為排除之 C-1/C-4);OpenAPI 0 錯誤;limit-service 單測通過;真機回歸實測 4 端點(transactions 為陣列/CardStatusView 欄位/RewardRuleView 欄位/SpecialLimitsView 改名後正常)。⚠️ 併更正 C-4 之判定——查證發現 InstallmentPeriod.principalAmount 係 amount 之重複欄位(DDL 冗餘),契約省略正確 → 由批 1 剔除,改列「需設計確認」之待裁定(GOV-006 §4 #17)。⚠️ 併揭露自身事故:批次替換之錨點不唯一致 C-9 之 currency 誤植 LimitAdjustmentView(§3.8.4)——已修正並將「錨點須唯一」寫入 skill |
| 1.1 | 2026-09-23 |
新增 §3.6 逐案判定表(20 案,供一次核定)——依 BEAM PMO 指示「先出逐案判定表再一次核定,避免逐筆來回」:逐案列契約原文/DTO 原文/差異/預判方向/影響面,並分為批 1(16 案,方向無疑義)/批 2(1 案,C-1 待定)/批 3(1 案已裁定)。併完成 §3.7:E-1(merchantId 語意)之修正與真機驗證——BEAM PMO 裁定採 (a) 回應改回業務代碼;改 MerchantViewAssembler/MerchantSettlementService(新增 resolveMerchantCode,查無即拋 fail-loud)+ gen_openapi.py 三處補 description 明載語意;真機 e2e 3 情境(新鍵請款/同鍵重播/往返回查 200,修正前 404)+ DB 複核(未動 DDL、未做資料遷移)。設計依據:MerchantRepository#findByMerchantId 之 javadoc 早已明載「對外 merchantId 即業務代碼」→ 本缺陷係實作偏離自身設計意圖,故 (a) 為回歸設計而非新決策 |
| 1.0 | 2026-09-23 | 提出並建置。GOV-006 §4 #14 之執行:新增 tools/verify_contract_dto.py(零相依、--selftest 4/4);建置中修正自身 7 項缺陷(5 類偽陽性);全庫掃描得 19 處不符(A 陣列性 1/B 型別與欄位 6/C 欄位集合 12)+ 1 則僅命名差異;CSP-001 → 1.9(§7.1 增列 G-8 檢查點)。19 處之修正待分流裁定。⚠️ 併新增 §3.5 分類 D:於 C-14 批次二實測中發現 |
「merchant 之 merchantId 同名字段語意不一致」(請求=業務代碼、MerchantSettlementView 回應=UUID) |
||
| ——本工具結構上測不到(名稱/型別皆相同),已列為限制 5 |