# 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. 根因

```java
// 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`：

```java
// 修改前
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） |
