Project

General

Profile

CR #210 » CR-009_沖正交易重複減免致帳單短收.md

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

 

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 核定):

  1. 同期情境(本例):借方 12,345 + 貸方 12,345 → 淨 0,與「這筆交易實際未成立」之事實相符;
  2. 跨期情境:前期借 12,345(當期確實發生)、本期貸 12,345 → 跨期淨 0,符合會計期間歸屬;
  3. 與 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 57,898
2026-11 70,243 0 0 1,066 0 71,309 58,964
  • 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)
    (1-1/1)