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 |