# CR-007　帳單交易**未依期間過濾** → 每期重複計收（含循環息）

| 項目 | 內容 |
|---|---|
| 編號 | CR-007 |
| 版本 | **1.2（2026-09-20 核定並執行完成）** |
| 狀態 | ✅ **已核定／已執行並實測通過** |
| 提出日期 | 2026-09-20 |
| 核定日期 | 2026-09-20（BEAM PMO：採 **A**——`posting` 交易查詢加 `from`／`to`，`statement` 以本期區間查詢；逾時改**可組態＋重試**） |
| 提出者 | WorkBuddy AI |
| 影響文件 | `SCS-011 §7`／`§8.1`／`§10`、`SCS-007`（新增交易查詢之期間參數）、`contracts/openapi/posting-service.yaml`（由 `tools/gen_openapi.py` 產生）、`PostingQueryPort`／`PostingServiceClient` |
| 關聯 | `CR-006`（已核定並執行——**本次缺陷由其「每月計息」而放大為利息重複計收**）、`HANDOVER §3.C #C-38` |
| 嚴重度 | 🔴 **P0（帳務金額錯誤）**——客戶端多收款，且循環息每期累加 |

---

## 1. 背景

`CR-006` 核定後，`statement-service` 已於結帳前觸發計息（`SCS-011 §8.1`），
實機執行 10 月帳單週期（`M01`／`businessDate=2026-10-15`）成功：
帳單產生、`finance_charges=2064`。

**驗證金額時發現該數字不對**——10 月帳單把**兩個月**的利息都算進去了。
追查後確認根因不在計息，而在**帳單彙總交易時沒有任何期間過濾**。

---

## 2. 實測證據（真實 Oracle，2026-09-20）

### 2.1 帳單金額（同一帳戶 `018f2a3b-…-0010`）

| 帳單月份 | 前期餘額 | 繳款 | 消費 | 循環息 | 費用 | 新餘額 |
|---|---:|---:|---:|---:|---:|---:|
| 2026-09 | 0 | 12,345 | 55,500 | **0** | 300 | 43,455 |
| 2026-10 | 43,455 | **12,345** | 67,845 | **2,064** | **300** | 101,319 |

- **繳款 12,345 與費用 300 兩期完全相同** —— 同一筆交易被**兩期帳單重複計收**。
- 帳單明細筆數：9 月 **3** 行 → 10 月 **6** 行（舊的 3 行**原封不動再列一次**）。

### 2.2 循環息

`CC_POSTING.TRANSACTIONS` 之 `INTEREST` 交易：

```
TX 2026-09-15  amt=1032   （CR-006 實測時手動觸發）
TX 2026-10-15  amt=1032   （本次 10 月週期自動觸發）
```

10 月帳單 `finance_charges = 1032 + 1032 = 2064`
——**9 月已發生之利息又在 10 月帳單計收一次**。

> 依此機制，第 N 期帳單會計收**第 1～N 期全部利息**，呈等差累加（第 3 期 = 3096、第 4 期 = 4128…）。

---

## 3. 根因

`SCS-011 §7` 明載：「由 `posting-service` 取**當期**交易與餘額」。
但實作並未做到：

```java
// StatementGenerationService.generateOne
var transactions = postingClient.getTransactions(accountId);   // 無任何日期參數

// PostingQueryPort
List<TxnSnapshot> getTransactions(UUID accountId);              // 簽章本身就沒有期間

// PostingServiceClient
get("/api/v1/accounts/" + accountId + "/transactions?page=1&size=200");  // 僅分頁
```

`contracts/openapi/posting-service.yaml` 之
`/api/v1/accounts/{accountId}/transactions` **僅有 `page`／`size` 兩個查詢參數**——
**契約層就不支援期間過濾**，故這不是「忘了傳參數」，而是**端到端都沒有這個能力**。

**結論**：文件（§7「當期」）與實物（抓全部）**不一致**，屬實作缺陷，非設計取捨。

---

## 4. 為何現在才爆（與 `CR-006` 之關係）

- 9 月帳單產生時，`posting` 尚無任何 `INTEREST` 交易 → `finance_charges=0`，
  缺陷存在但被掩蓋（消費／繳款重複計收其實**早已發生**，見 §2.1）。
- `CR-006` 讓利息**每期自動產生**後，缺陷轉為**利息逐期累加**，金額放大且持續惡化。

---

## 5. 選項與建議

| # | 方案 | 做法 | 優點 | 代價／風險 |
|---:|---|---|---|---|
| **A** ⭐ | **查詢端加期間參數** | `posting` 交易查詢端點新增 `from`／`to`（`postingDate` 區間）；`statement` 以本期區間查詢 | 語意最正確（與 §7「當期」一致）；無跨服務寫入 | 須改**契約**（`gen_openapi.py`）、`posting` 查詢邏輯、`PostingQueryPort` 簽章、`statement` 客戶端；分頁上限仍須處理 |
| B | **已出帳標記** | `posting` 交易表加 `statement_id`，產生後回寫標記 | 不會遺漏 | 跨服務寫入**權威表**（`posting` 擁有資料所有權），一致性複雜，須 Saga |
| C | **帳單端水位** | `statement` 自存 `LAST_BILLED_AT`，依 `postingDate > 水位` 過濾 | 不動 `posting` 契約 | 水位與分頁上限（200 筆）疊加，仍可能截斷；新增狀態須與 Checkpoint 續跑一致 |

**建議採 A**：
① 與 `SCS-011 §7` 既有文字一致（改的是實物，不是文件）；
② 不讓 `statement` 寫入 `posting` 的權威資料——符合「權威來源單一」原則；
③ 期間條件下推至 SQL，可避開分頁截斷（C 辦不到）。

**A 之配套（須同時定奪）**：分頁 `size=200` 之上限——期間過濾後單期交易仍可能 >200 筆，
建議改為「依期間查完為止」（分頁迴圈）或將 `size` 上限提高，並於 `SCS-011 §10` 明列。

---

## 6. 影響範圍（若採 A）

| 產出 | 變更 |
|---|---|
| `tools/gen_openapi.py` | `posting-service` 交易查詢端點新增 `from`／`to` 參數（**不可手改 YAML**） |
| `contracts/openapi/posting-service.yaml` | 由產生器重產 |
| `posting-service` | 查詢邏輯支援 `postingDate` 區間（`SCS-007` 須補對應章節） |
| `statement-service` | `PostingQueryPort.getTransactions` 簽章加期間；`PostingServiceClient` 帶參數 |
| `SCS-011 §7`／`§8.1`／`§10` | 明列期間條件與「計息日 `periodEnd` 須落在本期區間內」 |
| 測試 | 新增「跨期不重複計收」之整合與單元測試（`TRP-001` 須補編號） |

---

## 7. 風險揭露（未核定前之現況）

> ⚠️ **在核定前，`statement-service` 之帳單週期批次會產出金額不正確之帳單**
> （重複計收歷史交易與歷史利息）。
> 目前僅於本機開發環境執行過，**未於任何客戶環境啟用**。
> 建議核定前不要再對同一帳戶重複執行週期批次（每次執行都會再累加一層重複金額）。

---

## 8. 核定結果

| # | 核定 | 日期 |
|---:|---|---|
| 5（方案選擇） | ✅ **採 A**——`posting` 交易查詢加 `from`／`to`，`statement` 以本期區間查詢 | 2026-09-20 |
| A 配套（分頁上限） | ✅ **逐頁取至「不足一頁」為止**（`size=200`），頁數上限 **20**（4,000 筆／期），超限丟錯（`CC-STM-5001`）。**不靜默截斷**、亦不調高共用 `SizeParam` 上限以免牽動全平台 | 2026-09-20 |
| 相關：#C-40 逾時 | ✅ **改為可組態＋重試**（批次語意下放寬預設值） | 2026-09-20 |

---

## 9. 實作結果與實測證據

### 9.1 區間語意（半開 `(from, to]`）

| 邊界 | 語意 | 理由 |
|---|---|---|
| `to` | `POSTING_DATE <= to`（**含**） | 結帳日當日計收之循環息 `postingDate` 即為結帳日，不含將**漏收當期利息** |
| `from` | `POSTING_DATE > from`（**不含**） | 上期結帳日（含）前之交易已於上期計收，含入將**重複計收** |

### 9.2 實測（真實 Oracle，2026-09-20 07:40）

#### ① 交易查詢端點之區間語意

帳戶 `018f2a3b-…-0010`，帳上共 7 筆交易（含 09-15 與 10-15 兩筆 `INTEREST`）：

| 查詢 | 筆數 | 09-15 利息 | 10-15 利息 |
|---|---:|---|---|
| 無 `from`／`to` | 7 | ✅ 在 | ✅ 在 |
| `from=2026-09-15&to=2026-10-15` | **6** | ❌ **已排除**（`from` 不含） | ✅ **保留**（`to` 含） |

→ 半開 `(from, to]` 之語意**經實機證實**，且兩個邊界各自發揮作用。

#### ② 帳單週期（關鍵：不再累加）

執行 11 月週期（`businessDate=2026-11-15`）：

| 帳單月 | 前期餘額 | 繳款 | 消費 | 循環息 | 費用 | 明細 |
|---|---:|---:|---:|---:|---:|---:|
| 2026-09 | 0 | 12,345 | 55,500 | 0 | 300 | 3 |
| 2026-10 | 43,455 | 12,345 | 67,845 | **2,064**（累加） | 300 | 6 |
| **2026-11** | 101,319 | **0** | **0** | **1,066** | **0** | **1** |

- 11 月帳單**只含本期之 1 筆利息**，歷史繳款／消費／費用**全數不再重複計收**。
  （若缺陷仍在，`finance_charges` 應為 `1032+1032+1066 = 3,130`、明細 8 筆。）
- 利息為 **1,066** 而非 1,032 係正確：10/15→11/15 為 **31 天**
  （67,845 × 18.5% × 31/365 = 1,066.4 → 1,066）。

#### ③ 逾時（#C-40 一併驗證）

11 月週期為**重啟後之首次呼叫**（冷路徑），過去會於 800ms 逾時而致
「帳戶被跳過、`statementCount=0`」；本次**首次即成功**（`statementCount=1`）。

#### ⚠️ 歷史髒資料仍在

10 月帳單之 `2,064` 與 11 月帳單之 `prev_balance=101,319` 皆為**修復前**產生，
金額**仍是錯的**。修復僅保證**往後**之週期不再重複計收；
驗證成效一律以**新月份**為準。

---

## 9. 修訂記錄

| 版本 | 日期 | 說明 |
|---|---|---|
| 1.0 | 2026-09-20 | 初版。實測發現帳單未依期間過濾交易，致繳款／費用／循環息重複計收；提列三方案並建議採 A |
| 1.2 | 2026-09-20 | BEAM PMO 核定採 A 後執行完成並實機實測：①交易查詢新增 `from`／`to`，半開 `(from, to]` 經實機證實（09-15 排除、10-15 保留）；②11 月帳單僅含本期 1 筆利息 `1,066`、明細 1 筆，**不再累加**（缺陷仍在時應為 3,130／8 筆）；③逾時改可組態後冷路徑首次呼叫即成功。並揭露歷史髒資料仍在 |
