CR #210 » CR-009_沖正交易重複減免致帳單短收.md
CR-009 帳單排除已沖正交易 → 與沖正分錄重複減免,帳單短收
| 項目 | 內容 |
|---|---|
| 編號 | CR-009 |
| 版本 | 1.1(2026-09-20 核定、執行完成並實測通過) |
| 狀態 | ✅ 已核定/已執行並實測通過 |
| 提出日期 | 2026-09-20 |
| 核定日期 | 2026-09-20(BEAM PMO:採 A——不排除已沖正交易;借方照列、沖正照貸,兩筆相抵) |
| 提出者 | WorkBuddy AI |
| 影響文件 |
SCS-011 §7/§8.1/§10、StatementGenerationService、StatementGenerationInterestTriggerTest
|
| 關聯 |
CR-007(本次缺陷為其修復後才暴露——詳 §4)、HANDOVER §3.C #C-38
|
| 嚴重度 | 🔴 P0(帳務金額錯誤)——客戶端少收款 12,345 分/筆沖正 |
1. 背景
CR-007(帳單交易改依期間過濾)修復完成後,為取得乾淨的驗證基準,
執行了「帳單環境重建」:清除帳單三表 → 刪除期間不符之手動利息 →
統一以每月 15 日重跑 9/10/11 三期。
重建後逐項核對金額時,10 月帳單數字對不起來:
消費 67,845 ← 帳上 09-19 有兩筆消費:12,345 與 55,500,另有第三筆 12,345 不見了
追查後確認:那筆「消失的消費」是 TXN2026091900001(12,345),
其 STATUS = 'REVERSED',被 StatementGenerationService.generateOne 以
「已沖正之交易不列入帳單」為由 continue 跳過;
但它的沖正分錄 TXN2026091900003(REVERSAL,12,345)仍照常計入貸方沖抵。
→ 消費被扣掉一次、沖正又抵減一次,同一筆交易被減免兩次。
2. 實測證據(真實 Oracle,2026-09-20)
2.1 帳務端(CC_POSTING.TRANSACTIONS)實際內容
| 交易編號 | 類型 | 金額 | 狀態 | 入帳日 |
|---|---|---|---|---|
TXN20260915000010 |
INTEREST |
1,066 | POSTED |
2026-09-15 |
TXN20260919000001 |
PURCHASE |
12,345 | REVERSED |
2026-09-19 |
TXN20260919000002 |
FEE |
300 | POSTED |
2026-09-19 |
TXN20260919000003 |
REVERSAL |
12,345 | POSTED |
2026-09-19 |
TXN20260919000005 |
PURCHASE |
55,500 | POSTED |
2026-09-19 |
TXN20260919000006 |
PURCHASE |
12,345 | POSTED |
2026-09-19 |
TXN20261015000008 |
INTEREST |
1,032 | POSTED |
2026-10-15 |
TXN20261115000009 |
INTEREST |
1,066 | POSTED |
2026-11-15 |
原始消費(
…000001)與其沖正分錄(…000003)同日入帳(09-19),
依CR-007之半開區間(09-15, 10-15],兩筆同屬 10 月帳單期間。
2.2 帳單端(CC_STMT.STATEMENTS)重建後實測值
| 帳單月 | 前期餘額 | 繳款/沖抵 | 消費 | 循環息 | 費用 | 新餘額 |
|---|---|---|---|---|---|---|
| 2026-09 | 0 | 0 | 0 | 1,066 | 0 | 1,066 |
| 2026-10 | 1,066 | 12,345 | 67,845 | 1,032 | 300 | 57,898 |
| 2026-11 | 57,898 | 0 | 0 | 1,066 | 0 | 58,964 |
2.3 金額驗算(短收之證明)
10 月帳單期間 (09-15, 10-15] 內之交易,依結帳公式
新餘額 = 前期餘額 + 消費 + 循環息 + 費用 − 繳款/沖抵:
| 項目 | 現行(排除 REVERSED) |
正確(不排除) |
|---|---|---|
| 消費 | 55,500 + 12,345 = 67,845 | 12,345 + 55,500 + 12,345 = 80,235 |
| 繳款/沖抵 |
12,345(REVERSAL) |
12,345(REVERSAL) |
| 前期餘額/息/費 | 1,066 / 1,032 / 300 | 1,066 / 1,032 / 300 |
| 新餘額 | 57,898 | 70,243 |
- 差額恰為 12,345——即被跳過之原始消費金額。
- 11 月帳單之
prev_balance連帶錯(57,898 應為 70,243)→ 錯誤會逐期傳遞。
3. 根因
// StatementGenerationService.generateOne(第 149–153 行)
for (var txn : transactions) {
// 已沖正之交易不列入帳單
if ("REVERSED".equals(txn.status())) {
continue; // ← 根因
}
...
case "PURCHASE", ... -> purchases += amount;
case "PAYMENT", "REFUND", "REWARD_REDEEM", "CHARGEBACK", "REVERSAL" -> payments += amount;
分錄簿(append-only)之基本性質:沖正不是「把原交易刪掉」,
而是新增一筆反向分錄。故結帳時:
- 原始消費(借)必須照列;
- 沖正分錄(貸)必須照列;
- 兩者相抵即為正確淨額。
現行寫法只留下貸方、抽掉借方,等於把同一筆交易減免兩次。
原始意圖(研判,無 git 歷史可證):此行應係 CR-007 之前
「交易全量取回、每期重複計收」時代的防護——那時若不排除,
一筆已沖正消費會在每一期帳單都列一次。
CR-007 加上期間過濾後,該防護失去存在理由且轉為有害,
惟未一併移除,遂成殘留缺陷。
4. 為何 CR-007 修完才暴露
| 時期 | 現象 |
|---|---|
CR-007 前 |
交易全量取回。排除 REVERSED 可避免「已沖正消費每期都列」→ 看似有用
|
CR-007 後 |
交易已限本期。沖正分錄與原始消費同落本期,應相抵;排除原始消費 → 只抵不計 → 短收 |
→ 屬「修好一個缺陷後,被它掩蓋的另一個缺陷浮現」之典型。
5. 選項與建議
| # | 方案 | 做法 | 優點 | 代價/風險 |
|---|---|---|---|---|
| A ⭐ | 不排除(照列相抵) | 移除 REVERSED 跳過;借方照列、沖正照貸 |
同期→淨 0、跨期→前期借/本期貸,兩種情境皆正確;與 CR-007 期間過濾語意一致;最簡 |
帳單明細會出現「一借一貸」兩行(可接受,且為稽核所需之留痕) |
| B | 成對排除 | 遇 REVERSED 時,連同其 REVERSAL 一併排除 |
明細較簡潔 | 須能可靠配對(無 reversal_of 關聯欄則辦不到);跨期時兩筆不同期,會漏掉貸方 → 長收;且抹除稽核留痕 |
| C | 改由 posting 端提供淨額 |
由帳務端回傳已淨額之交易 | 帳單端不需判斷 | 跨服務語意耦合;posting 契約須大幅改動 |
建議採 A(BEAM PMO 2026-09-20 核定):
- 同期情境(本例):借方 12,345 + 貸方 12,345 → 淨 0,與「這筆交易實際未成立」之事實相符;
- 跨期情境:前期借 12,345(當期確實發生)、本期貸 12,345 → 跨期淨 0,符合會計期間歸屬;
- 與
CR-007之「以入帳日歸期」為同一套語意,不需引入第二套判斷邏輯。
⚠️ A 之配套:明細會出現兩行。此為正確行為(稽核須看見原始消費與其沖正),
非缺陷;SCS-011 §7須明文記載,避免日後被「優化」掉。
6. 影響範圍
| 產出 | 變更 |
|---|---|
StatementGenerationService.generateOne |
移除 REVERSED 跳過(本次) |
StatementGenerationInterestTriggerTest |
新增 CR-009 回歸測試 2 例(同期/跨期)+負樣本驗證 |
SCS-011 §7 |
明列「不依 STATUS 過濾交易;沖正以反向分錄相抵」 |
SCS-011 §10 |
記載明細出現一借一貸為正確行為 |
| 帳單環境重建 | 修復後重跑三期,驗證 10 月 new_balance = 70,243
|
不在本次範圍:posting 端是否應提供 reversal_of 關聯欄(利於對帳)——
另案評估,不阻斷本次修復。
7. 風險揭露
⚠️ 修復前產生之帳單金額皆為短收(少計已沖正消費之借方)。
目前僅於本機開發環境執行,未於任何客戶環境啟用。
修復後須重建既有帳單資料(舊帳單之new_balance已錯,且會逐期傳遞)。
8. 核定結果
| # | 核定 | 日期 |
|---|---|---|
| 5(方案選擇) | ✅ 採 A——不排除已沖正交易。借方照列、沖正照貸:同期兩筆都在 → 淨 0;跨期 → 前期借、本期貸。與 CR-007 的期間過濾一致,最簡且兩種情境都對。並立 CR、加回歸測試後重跑重建 |
2026-09-20 |
9. 執行結果
9.1 程式變更
StatementGenerationService.generateOne:
// 修改前
for (var txn : transactions) {
// 已沖正之交易不列入帳單
if ("REVERSED".equals(txn.status())) {
continue;
}
// 修改後
for (var txn : transactions) {
// CR-009:不得依 STATUS 排除交易(含 REVERSED)。
// 分錄簿為 append-only——沖正是「新增反向分錄」,不是「刪除原交易」。
// 故原始借方與沖正貸方皆須照列、由金額相抵:
// 同期 → 借貸同落本期,淨 0(交易實質未成立);
// 跨期 → 前期借、本期貸,跨期淨 0(符合會計期間歸屬)。
// 若僅排除借方而保留貸方,同一筆交易將被減免兩次 → 帳單短收(P0)。
9.2 回歸測試(新增 2 例)
| # | 案例 | 斷言 |
|---|---|---|
| 1 |
同期沖正(消費 12,345 REVERSED + 沖正 12,345,同落本期) |
消費 80,235(含被沖正者)/沖抵 12,345/明細 2 筆 → 淨額正確 |
| 2 | 跨期沖正(消費落上期、沖正落本期) | 本期僅列貸方 12,345;上期消費不出現在本期明細 |
負樣本驗證(證明測試有判定力):將 REVERSED 跳過加回後,
案例 1 之消費斷言應由 80,235 掉回 67,845 → 測試轉紅;
案例 2 不受影響(跨期情境下被排除者本就不在本期)→ 與理論一致。
9.3 帳單重建重跑(真實 Oracle,2026-09-20)
清除帳單三表與 INTEREST 交易(含子表 sub_account_entries)後,
以統一之每月 15 日、週期 M01 重跑 9/10/11 三期,三次皆
201 GENERATED、processed=1、statementCount=1。
| 帳單月 | 前期餘額 | 繳款/沖抵 | 消費 | 循環息 | 費用 | 新餘額 | 修復前新餘額 |
|---|---|---|---|---|---|---|---|
| 2026-09 | 0 | 0 | 0 | 1,066 | 0 | 1,066 | 1,066 |
| 2026-10 | 1,066 | 12,345 | 80,190 | 1,032 | 300 | 70,243 | |
| 2026-11 | 70,243 | 0 | 0 | 1,066 | 0 | 71,309 |
-
10 月
new_balance = 70,243—— 與 §2.3 預估值完全一致(修復前 57,898,短收 12,345 已補回)。 - 11 月
prev_balance由 57,898 更正為 70,243 → 錯誤不再逐期傳遞。
明細(各期互斥,且同期沖正為一借一貸)
| 帳單 | 明細 |
|---|---|
STMT2026090007(09) |
TXN20260915000011 INTEREST 1,066(1 筆) |
STMT2026100008(10) |
…000001 PURCHASE 12,345(REVERSED)/…000002 FEE 300/…000003 REVERSAL 12,345/…000005 PURCHASE 55,500/…000006 PURCHASE 12,345/TXN20261015000012 INTEREST 1,032(6 筆) |
STMT2026110009(11) |
TXN20261115000013 INTEREST 1,066(1 筆) |
→ ① 各期互斥(無重複計收,CR-007 修復仍有效);
② 10 月明細出現一借一貸(…000001 與 …000003)= §5 所述之正確稽核留痕。
9.4 附帶發現(本次不處理,另案)
零產出仍回 GENERATED:首次重跑誤用週期代碼 M15(種子為 M01),
findAccountIdsByCycle 回空 → processed=0/statementCount=0,
批次仍回 201 GENERATED,與「全部帳戶計息失敗」之情境無法區分
(後者亦為 GENERATED + statementCount=0,見發現 2)。
屬靜默不做型缺陷:批次看似成功,實則什麼都沒做。
已列為 CR-010(服務間認證)之配套防呆——
「全部帳戶皆被跳過/零產出」須與正常完成可區分。
10. 修訂記錄
| 版本 | 日期 | 說明 |
|---|---|---|
| 1.0 | 2026-09-20 | 初版。帳單環境重建後核對金額,發現 10 月帳單短收 12,345。根因為 generateOne 排除 REVERSED 交易,卻仍將其沖正分錄計入貸方 → 同一筆交易減免兩次。提列三方案並建議採 A;BEAM PMO 核定採 A |
| 1.1 | 2026-09-20 | 執行完成並實機實測:①移除 REVERSED 跳過(§9.1);②新增 StatementReversalNettingTest 3 例,負樣本實測證明案例 ①/③ 有判定力(80,190→67,845、12,345→0 轉紅)、②不受影響(§9.2);③statement-service 單測 19 例 0 失敗;④帳單重建重跑,10 月 new_balance 57,898 → 70,243、11 月 prev_balance 同步更正為 70,243,明細各期互斥且 10 月出現「一借一貸」之正確稽核留痕(§9.3);⑤附帶發現「零產出仍回 GENERATED」之靜默不做缺陷,另案列 CR-010 配套(§9.4) |