# SCS-011　帳單服務（statement-service）程式設計規格

| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-011 |
| 版本 | **1.11**（**`CR-033`：更正 §7.2 與 §10 之消費者**——原載「`payment`」，經查**現行 `SAD-004 §5.2` 未列 `payment`**（`CR-030 §2 #5` 重寫後已移除）；「出帳後觸發自扣」屬**未實作功能**（`SCS-008 §10`）而非事件消費，且自扣之**執行**為**寫入**，依 `CR-030 §0` **P2** 命令不得走事件 → **`payment` 非本事件之消費者**。前版 **1.10**（`CR-029` 批次 C-2：**§7.2 payload 補可選 `currency`**——供 `notification-service` 以既有種子模板 `STATEMENT_ISSUED` 建立帳單通知；依 `SAD-004 §5.3` **不升版**）。前版 **1.9**（`CR-027` 批次 A-4：**新增 §7.1 事件輸出**——`statement.statement.issued.v1`）。（`GOV-007 §5`：**新增 §8.2 帳單 PDF 產生與物件儲存**——BEAM PMO 2026-10-01 裁定「必做」；函式庫經 `version-compat-verification` 實查採 **Apache PDFBox 3.0.8**（Apache 2.0，避 iText AGPL）；產生 → 上傳物件儲存 → 回寫 `PDF_REF`；中文須內嵌字型。§10 之「PDF 產生器仍待」解除。前版 **1.7**（`HANDOVER #A-106`：§10 帳單 PDF 之**儲存媒體已定**——S3 API／本地 Garage，見 `SAD-006 §3.2`；PDF 產生器與寄送通道仍待） |
| 狀態 | **已核定（規格先行）→ 已實作並實測通過**（2026-09-19）；1.1 依 `CR-003` 補狀態機對照 |
| 對應需求 | `ISS-2.8`（信用卡帳單管理） |
| 服務埠號 | **8092**（`application.yml` 實測） |
| 套件根 | `com.beam.cardcore.stmt` |
| 權威 schema | `CC_STMT` |
| 關聯文件 | `SDD-001 §5.6`、`SDD-002 §3.5`、`BRD-002`、`GOV-005` |

---

## 1. 對應需求

| FR 編號 | 需求名稱 | 來源 |
|---|---|---|
| `ISS-2.8` | 信用卡帳單管理 | `BRD-002` |

### 1.1 上層設計依據（本規格**不重複**這些圖，僅引用）

| 圖種 | 文件 | 數量（實測） | 本服務相關 |
|---|---|---|---|
| **ER-Diagram** | `SDD-001_領域模型與ER-Diagram` | 16 個 `erDiagram` | `STATEMENTS`/`STATEMENT_LINES` 實體（`SDD-001 §5.6`） |
| **DFD** | `SDD-002_DFD與流程分解` | 10 個 flowchart | 帳單產生流（`SDD-002 §3.5`） |
| **Sequence** | `SDD-003_Sequence-Diagrams` | 15 個 `sequenceDiagram` | 帳單產生時序 |
| **狀態機** | `SDD-005_狀態機設計` | 10 個 `stateDiagram` | ⚠️ `STATEMENTS.status`（GENERATED→SENT→PAID…）以 CHECK 控管，未定義正式狀態機 |
| **API Contract** | `SDD-006` + `contracts/openapi/statement-service.yaml` | 17 份 | 本服務 3 支端點 |

---

## 2. 服務職責與邊界

**承作**：
- 帳單週期執行（`statement-cycles/{cycleCode}/runs`）
- 帳單產生（`STATEMENTS`：前期餘額、本期消費／繳款／費用／利息、新餘額、最低應繳、點數）
- 帳單明細（`STATEMENT_LINES`：逐筆交易，含遮蔽卡號 `CARD_NO_MASKED`）
- 帳單寄送（`SEND_TYPE`、`E_STATEMENT_FLAG`、`PDF_REF`）

**不做（屬他服務）**：
- 餘額來源 → `posting-service`
- 實際交易 → `authorization-service`／`posting-service`
- 點數 → `rewards-service`

---

## 3. 領域模型

- 聚合：`Statement`（帳單）、`StatementItem`（帳單明細，依 `services.yaml` aggregates）
- 值物件：`CycleCode`、`StatementStatus`、`SendType`、`Money`、`Currency`
- 狀態：`STATEMENTS.status ∈ {GENERATED, SENT, PAID, PARTIALLY_PAID, OVERDUE, DISPUTED, CLOSED}`（CHECK，預設 GENERATED）

---

## 4. 程式結構

| 套件／類別 | 職責 | 對應需求 |
|---|---|---|
| `domain/model/Statement` `StatementItem` | 聚合與值物件 | `ISS-2.8` |
| `domain/port/StatementRepository` | 持久化埠 | — |
| `application/StatementGenerationService` | 週期執行／產生 | `ISS-2.8` |
| `infrastructure/jdbc/StatementRepositoryImpl` | JDBC 實作 | — |
| `interfaces/rest/StatementController` | REST 端點 | `ISS-2.8` |

---

## 5. 資料模型（約束為 Oracle 實測值）

| 表 | 關鍵欄位 | 約束（實測） |
|---|---|---|
| `STATEMENTS` | `STATEMENT_NO` VARCHAR2(24) **UNIQUE**；`ACCOUNT_ID`；`STATEMENT_MONTH` VARCHAR2(8)；`CYCLE_CODE`；`STATEMENT_DATE`/`PAYMENT_DUE_DATE` DATE；`GRACE_PERIOD_DAYS` NUMBER(5)；`PREV_BALANCE`/`PAYMENTS_CREDITS`/`PURCHASES_DEBITS`/`FINANCE_CHARGES`/`FEES`/`ADJUSTMENTS`/`NEW_BALANCE`/`MINIMUM_DUE` NUMBER(19)；`REWARD_POINTS_EARNED`/`BALANCE` NUMBER(19)；`CURRENCY` CHAR(3) DEFAULT `'TWD'`；`SEND_TYPE` DEFAULT `'ELECTRONIC'`；`E_STATEMENT_FLAG` NUMBER(1) DEFAULT 1；`STATUS` DEFAULT `'GENERATED'`；`GENERATED_AT`；`SENT_AT` | `send_type IN ('PAPER','ELECTRONIC','NONE')`；`status IN ('GENERATED','SENT','PAID','PARTIALLY_PAID','OVERDUE','DISPUTED','CLOSED')`；`UQ_STMT_MONTH` UNIQUE(`ACCOUNT_ID`,`STATEMENT_MONTH`) |
| `STATEMENT_LINES` | `STATEMENT_ID`（FK→`STATEMENTS.ID`）；`SEQ` NUMBER(10)；`POSTING_DATE`；`TX_NO`；`DESCRIPTION` VARCHAR2(120)；`MCC`；`TX_TYPE`；`CARD_NO_MASKED` VARCHAR2(20)；`AMOUNT`；`CURRENCY` | FK(`STATEMENT_ID`) REFERENCES `STATEMENTS`(`ID`) |

> 含 `CREATED_AT`/`UPDATED_AT` TIMESTAMP(6) WITH TIME ZONE、`VERSION` 樂觀鎖。`CARD_NO_MASKED` 落實 PII 遮蔽（`common-security` `PiiMasker`）。

---

## 6. API 設計（來自 `contracts/openapi/statement-service.yaml`）

| 端點 | 方法 | 說明 | 對應 FR |
|---|---|---|---|
| `/api/v1/statement-cycles/{cycleCode}/runs` | POST | 帳單週期執行 | `ISS-2.8` |
| `/api/v1/statement-runs/{runId}` | GET | 執行結果查詢 | `ISS-2.8` |
| `/api/v1/accounts/{accountId}/statements` | GET | 帳單查詢（分頁，`data` 為陣列） | `ISS-2.8` |
| `/api/v1/statements/{statementId}` | GET | 單筆帳單查詢（**含明細**，CR 2026-09-19 新增） | `ISS-2.8` |
| `/api/v1/statement-runs` | GET | **批次執行清單**（最近 N 筆；2026-09-30 補建，見 §6.1） | `ISS-2.8` |

> **明細載入策略**：分頁查詢之 `statementLines` 為**空陣列**（避免逐筆載入造成 N+1）；
> 明細由單筆查詢端點取得，供前端點選後展開。

> 錯誤碼：`404` 找不到帳單、`409` 週期重複執行、`400` 參數錯誤。

### 6.1 批次執行清單端點（2026-09-30 補建，`HANDOVER #A-104`）

> **緣由**：`SDD-007 §5.4.26` 之桶 C「**批次作業監控**」（`/system/batch`）——
> `bff-web` 依 `SCS-020 §9.9` 聚合四服務之批次執行紀錄，**需各服務提供「列出批次」端點**。
> ⚠️ **實測揭露**：本服務原**僅有**「觸發」（`POST /statement-cycles/{cycleCode}/runs`）與
> 「單筆查詢」（`GET /statement-runs/{runId}`）——**無「列出」端點**
> （`GET /statement-cycles/{code}/runs` 實測回 **405**）。
> `SCS-020 §9.9.1` 原載「四個既有端點，皆已實作並真機驗證」**與事實不符**，已一併更正。

**`GET /api/v1/statement-runs?limit=N`**（回應 `CycleRunView[]`，`200`）

| 參數 | 型別 | 說明 |
|---|---|---|
| `limit` | integer（可選） | 回傳筆數上限。預設 **20**，上限 **100** |

**行為要求**：

1. **唯讀**——僅查詢，**不觸發**任何批次（觸發屬 `POST /statement-cycles/{cycleCode}/runs`）。
2. **依 `startedAt` 遞減**（最近執行者在前）——與 `SCS-020 §9.9.4 #3` 一致。
3. **無資料回 `200` ＋ 空陣列**——**非** `404`（「尚無批次執行」是合法狀態，不是錯誤）。
4. **不快取**（`SCS-020 §9.9.4 #4`）。
5. 回應**沿用既有 `CycleRunView`**（已含 `skippedCount`／`checkpoint`／時間欄位）——**不另造 DTO**，
   避免同一資源出現兩套形狀。

> 錯誤碼：`400` 參數錯誤（`limit` 超出 `1..100`）。**無 `404` 情境**。

**未竟**：

- **不支援分頁游標**（僅取最近 N 筆）——作業台為**即時總覽**；長期歷史查詢屬觀測平台（`SCS-020 §9.8.4` 同一原則）。

---

## 7. 關鍵邏輯

- **帳單產生**：由 `posting-service` 取**當期**交易與餘額，計算新餘額／最低應繳（參 `pricing-service` 利息），寫入 `STATEMENTS`＋`STATEMENT_LINES`。
  - **「當期」之意**即 `SCS-007 §7.1` 之半開區間 **`(上期結帳日, 本期結帳日]`**；
    `PostingQueryPort.getTransactions` **以期間為必要參數**（型別即為約束，遺漏無法編譯）。
    > 此為 **`CR-007`**（P0：每期重複計收）之根治。核定前之缺陷實測見 §10 #2。
  - **上期結帳日之取得**：`StatementRepository.findByAccountId(accountId, 1, 0)` 取上一期帳單之
    `statementDate`；**無前期帳戶**則以 `businessDate.minusMonths(1)` 為 `from`
    （與 §8.1 計息區間起點一致，屬簡化，見 §10）。
  - **分頁採 Fail-Closed**：逐頁取至「不足一頁」為止（`size=200`）；
    頁數上限 **20** 頁（4,000 筆／期），超限即丟錯（`CC-STM-5001`）。
    僅取第一頁等同**靜默截斷**＝帳單短收，故明令禁止（見 `SCS-007 §7.1`）。
  - **⚠️ 不得依 `STATUS` 過濾交易（含 `REVERSED`）**（`CR-009`，P0）：
    分錄簿為 **append-only**——沖正是「**新增一筆反向分錄**」，不是「把原交易刪掉」。
    故結帳時**原始借方與沖正貸方皆須照列**，由金額**相抵**：

    | 情境 | 處理 | 結果 |
    |---|---|---|
    | **同期**（原始消費與沖正同落本期） | 借方照列 ＋ 貸方照列 | 本期**淨 0**（該交易實質未成立） |
    | **跨期**（消費落前期、沖正落本期） | 前期借、本期貸 | 跨期**淨 0**（符合會計期間歸屬） |
    | **跨期**（消費落本期、沖正落下期） | 本期**照列借方** | 下期才抵 |

    > **為何明列**：原實作以「已沖正之交易不列入帳單」排除 `REVERSED` 之借方，
    > 卻仍將其 `REVERSAL` 分錄計入貸方 → **同一筆交易被減免兩次**，
    > 實測 10 月帳單短收 **12,345** 分（`new_balance` 57,898 應為 70,243）。
    > 該寫法研判為 `CR-007` 期間過濾前「交易全量取回」時代之防護，
    > 期間過濾上路後已失去理由且**轉為有害**。
    >
    > **配套**：明細將出現「一借一貸」**兩行**——此為**正確**行為
    > （稽核須看見原始消費與其沖正），**非缺陷，不得「優化」掉**。
- **週期冪等**：`UQ_STMT_MONTH` 防止同帳戶同月份重複產生。
- **PII 遮蔽**：明細卡號一律 `CARD_NO_MASKED`（遮蔽後），不得存明文。

### 7.1　事件輸出（Transactional Outbox；`CR-027` 批次 A-4）

**掛點**：`StatementGenerationService.generateOne` 於帳單落庫處、**同一交易**內 append。

> **⚠️ 為何需要一個專屬的交易單元（本節之關鍵設計）**
>
> `runCycle` **刻意不標** `@Transactional`（§7 開頭：大量批次包在單一交易會使 Checkpoint 因回滾而失效）。
> 原架構之交易邊界落在 **`JdbcStatementRepository.save`（`@Transactional`，每帳戶一筆交易）**。
>
> 因此**不可**在 `generateOne` 內「先呼叫 `save()`、再呼叫 `outbox.append()`」——
> 前者已自成一個交易並提交，後者將落在**另一個**交易 → **違反「與權威寫入同交易」**
> （斷電時會出現「帳單已出、事件未發」）。
>
> 故新增 `application/StatementIssueService`（**`@Component`，非 private 方法**）：
> `@Transactional public void issue(Statement, String traceId)` —— 內部依序
> `statementRepository.save(statement)`（`REQUIRED` → 併入同一交易）與 `outbox.append(...)`。
> `generateOne` **經由注入之 bean** 呼叫它，確保 Spring 代理生效。
>
> > ⚠️ **反例（不得採用）**：把 `generateOne` 改標 `@Transactional`——它由**同類別**之 `runCycle`
> > **自我呼叫**，Spring 代理**不會**介入，交易**靜默不生效**（「看起來有標註、實際沒有」）。

**事件**：`statement.statement.issued.v1`（`SAD-002 §3.11`、`SDD-006 §4.2`）

| 項目 | 值 | 依據 |
|---|---|---|
| Kafka 主題 | `cc.statement.statement.issued`（＝`cc.<aggregateType>.<eventType 去版本>`） | `OutboxPublisher.topicFor` |
| 分區鍵 | `aggregateId` ＝ **`statementId`** | `SAD-004 §5.4`（同聚合有序） |
| `aggregate_type` | `statement` | — |
| `event_type`（outbox 欄） | **`statement.issued`**（**不含**版本） | `SDD-001 §3.15` |
| `event_version` | `1` | — |
| payload | `statementId`／`accountId`／`statementMonth`／`totalAmount`／`minimumDue`／`paymentDueDate`／**`currency`（可選，`§7.2`）** | `SDD-006 §4.2` ＋ `§7.2` |

**payload 欄位之來源對映**（**不得自行推導**）：

| payload 欄 | 來源 | 說明 |
|---|---|---|
| `statementId` | `statement.id()` | 聚合主鍵 |
| `accountId` | `statement.accountId()` | — |
| `statementMonth` | `statement.statementMonth()` | 帳單月份（`YYYY-MM`） |
| **`totalAmount`** | **`statement.newBalance()`** | **本期應繳總額**。⚠️ 對映依據＝契約 `StatementView.totalAmount` 即 `newBalance()`（`StatementViewAssembler` 實作），**非** `purchasesDebits`／`adjustments` 等單項 |
| `minimumDue` | `statement.minimumDue()` | 最低應繳 |
| `paymentDueDate` | `statement.paymentDueDate()` | 繳款截止日（`YYYY-MM-DD`） |

> ⚠️ **金額一律最小單位（分）**（`SDD-006 §2.5`）——**禁止浮點**。

**發射條件**：**逐帳戶**——`generateOne` 成功產生並落庫一筆帳單即發射**恰一則**事件。
**不發射之情形**（皆**早於**掛點）：
| 情形 | 原因 |
|---|---|
| 同帳戶同月份已存在帳單（`UQ_STMT_MONTH`，`runCycle` 之 `findByAccountAndMonth` 早退） | 冪等——不重複產生，故不重複發射 |
| 該帳戶計息失敗 → 跳過不產生帳單（§8.1） | 無帳單即無事件 |
| 零產出（該期無交易） | **仍產生帳單並發射**——`totalAmount` 如實為 0（與 `SCS-013 §7.1` 同立場：不虛構語意） |

**消費者**：`notification`（`STATEMENT_ISSUED`）——✅ **已實作**（`#A-141`，`CR-029 §5 C-2`）。

> ★ **更正（`CR-033`，2026-10-02）**：本節原載消費者含 **`payment`**——**不成立**：
> 現行權威清單 `SAD-004 §5.2` **未列 `payment`**（`CR-030 §2 #5` 重寫後已移除）。
> 「**出帳後觸發自扣**」屬**未實作功能**（`SCS-008 §10` 自載「自扣執行流程尚未實作」），
> **非事件消費缺口**；且自扣之**執行**是一筆真實付款（**寫入**）——依 `CR-030 §0` **P2**，
> **命令不得走事件**，須走 API。→ **`payment` 非本事件之消費者。**

> ⚠️ **outbox `event_type` 之推導慣例（本節為明確化，非新規則）**：
> 事件**全名**去 `.vN` 後，**再移除一層與 `aggregate_type` 相同之首段**——
> 例：`card.card.issued.v1` → `card.issued`（主題 `cc.card.card.issued`，見 `SCS-003 §7.11`）；
> `limit.limit.adjusted.v1` → `limit.adjusted`（`cc.limit.limit.adjusted`，見 `SCS-002 §7.5`）。
> 故本事件 `statement.statement.issued.v1` → **`statement.issued`**。
>
> ⚠️ **已知不一致（`CR-027 §5` 待裁定）**：`product`／`clearing`／`dispute`／`posting`
> 目前寫入**完整名**（如 `dispute.case.opened`），主題因而**多一層重複**
> （`cc.dispute.case.opened`）。本服務依**既有文件慣例**實作，
> 不一致之對齊方向待 BEAM PMO 裁定（**現階段無消費者**，變更成本低）。

### 7.2 payload 補**可選** `currency`——2026-10-01 新增（`CR-029` 批次 C-2）

> **緣由**：`notification-service` 訂閱本事件並以**既有種子模板 `STATEMENT_ISSUED`** 建立帳單通知
> （`SAD-004 §5.2` 載消費者含 `notification`）。惟該模板需要 `{statementMonth}`／`{totalAmount}`／
> **`{currency}`**／`{paymentDueDate}`，而 `§7.1` 之 payload **無 `currency`** →
> **變數不足即無法建立通知**（`CC-NOT-1003`）。與 `SCS-008 §7.2`（`payment.posted.v1`）**同族**。

**處置**：依 **`SAD-004 §5.3`「新增**可選**欄位 → 不升版」**，於 `v1` payload 增列 `currency`（**可選**）。

| 欄位 | 來源 | 必填 | 說明 |
|---|---|---|---|
| **`currency`** | `statements.currency` | **否**（可選） | ISO 4217 三碼。**本即存在於 `statements` 表**（非新增資料）；補入僅為使下游（通知）得渲染 |

> ⚠️ **`currency` 為「本期應繳金額」之幣別**（與 `totalAmount` 同源，皆取自同一 `Statement` 聚合）
> ——**不得**改取帳戶之其他幣別。

**相容性**：**不升版**（`SAD-004 §5.3`）。
- **舊事件**（無 `currency`）之消費者須容忍缺欄——`notification-service` 之處理見 `SCS-016 §7.12`：
  **缺 `currency` 時不建立通知並記 WARN**（**不臆造幣別**）。
- 本服務之事件**目前無其他消費者**。★ **更正（`CR-033`）**：原載「`payment` 之消費端尚未實作，屬 `CR-029 §4 #13` 待裁定」——**已裁定：`payment` 非消費者**（`CR-030 §2 #5`＋`CR-033 §2.1`；「自扣」屬未實作**功能**，非事件流）。

**先例**：`CR-027 §5 #11`（`#A-125`）／`#14`（`#A-128`）／`SCS-008 §7.2`（`#A-141`）——
**皆為「依建議執行 ＋ 登記待裁定」**（見 `CR-029 §8.4`）。

---

## 8. 外部依賴與降級

| 依賴 | 失敗處理（Stand-in） |
|---|---|
| `posting-service`（餘額／交易）、`pricing-service`（利息）、`rewards-service`（點數） | 經查詢／事件取得；若來源暫不可用，帳單產生排程延後重試（冪等） |
| **計息觸發 `posting-service`（同步，結帳前）** | 見 §8.1——**該帳戶計息失敗即跳過、不產生帳單**，批次繼續 |

### 8.1　結帳前觸發計息（`CR-006`／`HANDOVER #C-38`）

> **時序為本設計之核心**：利息必須**先入帳**，結帳時才彙總得到。
> 若於結帳後才計息，本期帳單將缺少循環息（金額少於應收）。

**呼叫點**：`StatementGenerationService.runCycle` → 逐帳戶於 `generateOne` **之前**
呼叫 `POST /api/v1/posting-interest/recalculate`（`SCS-007 §8.1`）。

**幣別來源（`C-32`，2026-09-22）**

- 計息之 `currency` **取自該帳戶之 balance 快照**，且**與帳單幣別共用同一次查詢**
  （`runCycle` 先 `getBalance`，再以其幣別觸發計息，並將同一快照傳入 `generateOne`）。
- ⛔ **不得硬編 `TWD`**：原實作寫死 `"TWD"`，非 TWD 帳戶將出現
  「利息以 TWD 入帳、帳單標示他幣」之**跨服務不一致**（本輪修正）。
- 真機實證：帳戶幣別改為 `USD` → 入帳交易 `TXN20270215000016` 與帳單 `STMT2027020012`
  同步為 **USD**（1066 分）；還原 `TWD` 後恢復。
- ⚠️ 連帶效果：`getBalance` 失敗改於**計息前**發生（原為計息後），
  故不再留下「已入息、未產帳單」之中間態；失敗處理同下表（外層標記批次 FAILED）。

**計息區間之推算**

| 項目 | 規則 |
|---|---|
| `periodEnd` | 本期結帳日 `businessDate`（**不含**——`pricing` 採半開區間） |
| `periodStart` | `businessDate.minusMonths(1)` |
| 區間長度 | 約 30 日（與 `pricing` 實測之 `[08-16, 09-15)` 一致） |

> ⚠️ 本期**未**使用「上一期實際結帳日」作為起點（須逐帳戶多一次查詢），
> 一律以「往前推一個月」推算。此簡化已列 §10。
>
> ⚠️ **計息區間與帳單區間錯開一日**：計息為 `[B-1月, B)`，帳單為 `(B-1月, B]`。
> 故結帳日 `B` **當日**發生之消費會進入本期帳單，但**下期**才開始計息
> （銀行實務之「結帳日交易次期起息」）。此為刻意設計，非缺陷。

**失敗處理（本期定奪）**

| 情境 | 處理 |
|---|---|
| 計息成功 | 繼續產生帳單 |
| **計息失敗**（`CC-PLT-5005` 等） | **跳過該帳戶、不產生帳單**，計入 skipped，批次**繼續** |

> **為何是「跳過」而非「中止批次」**：
> 產生一張**缺少利息**的帳單等於短收，屬帳務錯誤，代價高於延後；
> 反之若因單一帳戶失敗就中止整批，將使大量帳戶無法結帳。
> 故採銀行批次慣例——**錯誤帳戶跳過、其餘繼續**。
>
> ⚠️ 限制：個別帳戶之失敗原因本期**僅記錄於日誌**（WARN），
> 未納入 `StatementRun` Checkpoint 之明細（見 §10）。

### 8.2　帳單 PDF 產生與物件儲存（`GOV-007 §5`；2026-10-01 BEAM PMO 裁定「必做」）

> **緣起**：BEAM PMO 2026-10-01 裁定——「**現有系統有帳單 PDF，微服務不能少**」（`GOV-007 §5`）。
> 原 §10 載「儲存媒體已定（S3 API／本地 Garage），**PDF 產生器本身仍待**」→ 本節補之。

**規格先行**：本節 → 依賴查證 → 加依賴 → 實作 → 物件儲存上傳 → 真機驗證。

#### 8.2.1　函式庫選型（依 `version-compat-verification`，**Maven Central 實查**）

| 候選 | 最新穩定 | 授權 | 判定 |
|---|---|---|---|
| **Apache PDFBox** | **3.0.8** | **Apache 2.0** | ✅ **採用**——授權寬鬆（無 AGPL 風險）、純 Java、父 POM `source/target 1.8` → **Java 21 相容** |
| OpenPDF | 3.0.5 | LGPL／MPL | 🟡 可用但依賴較重（`icu4j`／`fop`／`brotli4j`） |
| iText 7 | — | **AGPL／商業** | ❌ **不採用**（銀行案授權風險） |

> ⚠️ **傳遞依賴揭露**：PDFBox 3.0.8 會帶入 `commons-logging`（Spring Boot 以 `spring-jcl` 橋接）與
> `bouncycastle`（`bcprov-jdk18on`，用於 PDF 加密；本服務**不使用加密**，惟仍為傳遞依賴）。

#### 8.2.2　產生流程與降級

| 步驟 | 說明 |
|---|---|
| 1. 產生 | `StatementGenerationService.generateOne` 於帳單落庫**後**，以 `StatementPdfPort`（port）產生 PDF 位元組 |
| 2. 上傳 | 以 `ObjectStoragePort.put(key, bytes, "application/pdf")` 上傳；鍵＝`statements/{statementNo}.pdf` |
| 3. 回寫 | 上傳**成功後**，將物件鍵寫入 `STATEMENTS.PDF_REF` |

**降級政策（PDF 為衍生物——fail-soft 但不靜默）**：

- ⚠️ **帳單為帳務權威、PDF 為衍生物**——PDF 產生／上傳失敗時，**帳單仍落庫**（`PDF_REF` 留空）並記 **WARN**；
  **不因單一 PDF 失敗而擋掉整批結帳**（物件儲存短暫中斷不應使帳務停擺）。
- ⚠️ **先上傳成功、才寫 `PDF_REF`**——順序顛倒會留下「DB 有鍵、儲存無檔」之死連結（**假成功**）。
- ⚠️ **不退回本地檔案**——**不以本地檔案冒充成功**；`PDF_REF` 留空即為**可見**之失敗（**不靜默**）。
- ⚠️ **憑證 fail-closed**：缺 `cardcore.storage.*` 即**啟動期失敗**，不靜默略過。

#### 8.2.3　PDF 版型（本期）

| 區塊 | 內容 |
|---|---|
| 標頭 | 「信用卡帳單」（中文）＋ 帳單編號 |
| 客戶與帳期 | 帳戶、帳單月份、結帳日、繳款截止日 |
| 應繳摘要 | 前期餘額／本期繳款／本期消費／循環息／費用／調整 → **新餘額**／**最低應繳** |
| 明細 | 逐筆消費（日期／商店／金額） |
| 頁尾 | 產生時間、幣別 |

> ⚠️ **中文字型須內嵌**——PDF 標準字型**不含 CJK**，未內嵌將顯示為方框。
> 本期自**系統字型**載入（本地 macOS：`/System/Library/Fonts/...`）或專案內附之**開源 CJK 字型**（如 Noto Sans CJK，SIL OFL）。
> ⚠️ **正式環境**應改用**經授權之商業字型**或客戶指定字型（**不得散布未授權字型**）。

#### 8.2.4　真機驗證表

| 驗證 | 結果 |
|---|---|
| 產生 PDF（含中文） | ✅ 真實 PDFBox 產生 **35,471 bytes**；抽出文字含「信用卡帳單」與帳單編號（**CJK 已內嵌**） |
| 上傳物件儲存 | ✅ 上傳真實 **Garage**（鍵 `statements/STMT-RM-*.pdf`） |
| 自儲存取回且為**合法 PDF** | ✅ 取回**逐位元組一致**；`Loader.loadPDF` 可解析、**1 頁** |
| 單元測試 | ✅ `PdfBoxStatementPdfAdapterTest`（PDF 合法＋中文可抽出）；既有批次測試 3 檔補替身後**全綠** |
| **驗證方式** | `StatementPdfRealMachineTest`——以 `CARDCORE_REAL_STORAGE=1` 守門，**打真實 Garage**；未設即略過（不污染 CI） |

> ⚠️ **本節僅涵蓋「產生 ＋ 入庫」**——**寄送通道**（簡訊／Email／推播）仍為**真阻塞**（供應商未定，§10／`GOV-006 §3.D D-3`）。

---

## 9. 測試案例（對應 `TRP-001`）

- `UT-STM-001`：帳單管理（`ISS-2.8`）——週期執行、產生、查詢、PII 遮蔽
- 週期冪等、最低應繳計算、跨幣別

**單測現況（2026-09-20 實測）**：`statement-service` 共 **22 例、0 失敗**
（`StatementTest` 6／`LayeringTest` 4／`StatementGenerationInterestTriggerTest` 6／
`StatementReversalNettingTest` 3／**`StatementZeroOutputTest` 3**）。

| 測試類 | 案例數 | 涵蓋 |
|---|---:|---|
| `StatementGenerationInterestTriggerTest` | 6 | `CR-006` 結帳前計息（時序、半開區間、失敗跳過）、`CR-007` 期間過濾（3 例） |
| **`StatementReversalNettingTest`**（`CR-009`） | **3** | ①同期沖正相抵 ②跨期（消費前期／沖正本期）僅列貸方 ③跨期（消費本期／沖正下期）仍列借方 |
| **`StatementZeroOutputTest`**（`#C-42`） | **3** | ①帳戶清單為空 → 記 WARN 且狀態 `GENERATED` ②有處理卻零產出 → 記 ERROR ③`skippedCount` ＝ `processedCount − statementCount` 正確導出 |

**9.1 `CR-009` 之判定力（負樣本實測）**

> 將 `REVERSED` 排除之缺陷寫法加回後，案例 ①由 `80,190` **掉回** `67,845`、
> 案例 ③由 `12,345` **掉回** `0` → 二者皆**轉紅**；
> 案例 ②不受影響（跨期情境下被排除者本就不在本期）——與理論一致，非假綠。

**9.2 `#C-42` 之判定力（負樣本實測）**

> 移除「零產出」之 ERROR 日誌後，案例 ②**精準轉紅**（其餘不受影響）→
> 證明該防呆**真被測到**，不是「寫了但沒生效」的靜默不做。

**9.3 `CycleRunView` 加值欄位 `skippedCount`**

| 欄位 | 型別 | 來源 | 用途 |
|---|---|---|---|
| `skippedCount` | `long` | `processedCount − statementCount`（**非**領域模型推導，屬組裝層加值） | 使「**成功產生 0 張**」與「**全部跳過**」在 API 回應上**可區分** |

> 純加值（additive）、非 breaking；已同步 `tools/gen_openapi.py` 並重產契約
> （72 端點 0 錯誤，變動僅落在 `statement-service.yaml`）。

---

## 10. 已知限制（誠實揭露）

- ✅ **帳單 PDF 產生（`PDF_REF`）已實作（2026-10-01；`GOV-007 §5`／**§8.2**）**——儲存媒體
  （契約 **S3 API**／本地 **Garage `v2.4.1`**／正式環境客戶企業物件儲存）＋ **產生器
  （Apache PDFBox 3.0.8，Apache 2.0）** 皆已就位並真機驗證（§8.2.4）。
  ⚠️ **仍待**者為**電子帳單寄送通道**（`§3.D D-3` **真阻塞**——供應商未定）。

**實作後補記（2026-09-19，誠實揭露）**：

1. ~~**契約與 DDL 不一致（須提 CR）**~~ → **✅ 已於 CR（2026-09-19）修約完成**：
   - `StatementView.sendType` 枚舉已改為 `PAPER / ELECTRONIC / NONE`，
     與 `STATEMENTS.send_type` 之 CHECK 約束一致，檢視層不再需要映射。
   - 帳單查詢之 `data` 已修約為 `StatementView[]`（陣列），與分頁語意一致。
   - `StatementView` 已補 `statementLines` 明細欄位，並新增
     `GET /api/v1/statements/{statementId}` 單筆查詢端點（含明細）。
   - 修約方式是改 `tools/gen_openapi.py`（契約之單一真實來源）後重生；
     該產生器並已新增 `SchemaName[]` 陣列語法以支援清單端點。
2. ✅ ~~**交易未依期間過濾 → 每期重複計收（P0）**~~ → **已於 `CR-007` 核定並修復**
   - **原缺陷實測**（2026-09-20，真實 Oracle，帳戶 `018f2a3b-…-0010`）：
     9 月帳單 `pay=12,345／fee=300`；10 月帳單 **`pay=12,345／fee=300` 完全相同**
     （明細 3 行 → 6 行）→ 同一筆交易被兩期帳單計收；
     且 `finance_charges=2064` ＝ 9 月息 `1032` ＋ 10 月息 `1032`（**逐期等差累加**）。
   - **根因**：`PostingQueryPort.getTransactions` 無期間參數，契約端亦僅 `page`／`size`。
   - **修法（`CR-007` 方案 A，BEAM PMO 2026-09-20 核定）**：`posting` 交易查詢新增
     `from`（不含）／`to`（含）參數，`statement` 以 `(上期結帳日, 本期結帳日]` 查詢
     （`SCS-007 §7.1`／`SCS-011 §7`）。
   - ⚠️ **歷史髒資料仍在**：上述 9／10 月兩筆 `INTEREST` 交易與帳單係缺陷修復**之前**產生，
     其金額**仍為錯的**；修復僅保證**往後**之週期不再重複計收。
     驗證一律以**新月份**（11 月）為準，勿以舊帳單判斷修復成效。
3. **新增 `STATEMENT_RUNS` 與 `STATEMENT_CYCLE_ACCOUNTS` 兩表（V2 遷移）**：
   V1 基線無處存放批次進度與「哪些帳戶屬於哪個週期」，
   若僅存記憶體則重啟即失去續跑能力，故補建。
4. **批次刻意不包單一交易**：若整批包在同一交易，Checkpoint 會因回滾而無法留存，
   喪失續跑意義。改採「每帳戶一筆交易」＋逐步推進 Checkpoint。
5. **最低應繳採簡化公式**（新餘額 10%、下限 1,000 分）：實務尚涉循環息、
   逾期加成與法規上限，待 `pricing-service` 費率引擎就緒後參數化。
6. **`CARD_NO_MASKED` 本期為 null**：`posting-service` 未回傳卡號，
   故明細無卡號可遮蔽；待其提供後由 `PiiMasker` 遮蔽寫入（欄位可為 null，不違約）。
7. **`parallelism`（多工並行度）參數已收未實作**：本期為循序處理。
8. **結帳分類**：`REVERSAL` 反向交易計入貸方沖抵（`payments`），
   因其 `TxType.sign() = -1`；若誤列借方將使沖正金額反向加成欠款（已修正並實測驗證）。
   ⚠️ **須與 `CR-009` 一併理解**：貸方照列的前題是**借方也照列**——
   只列貸方而排除 `REVERSED` 之借方，會使同一筆交易**減免兩次**（見 §7）。
9. **狀態機已建模（`CR-003` 解除）**：原「`STATEMENTS.status` 轉換未建模為 `SDD-005` 正式狀態機」
   之限制**已解除**——`SDD-005 §9` 已**拆分為兩個狀態機**：

   | 小節 | 狀態機 | 對應欄位 | 值域（實測 CHECK） |
   |---|---|---|---|
   | §9.1 | 帳單本體 | `STATEMENTS.status` | `GENERATED`／`SENT`／`PAID`／`PARTIALLY_PAID`／`OVERDUE`／`DISPUTED`／`CLOSED` |
   | §9.2 | 批次執行 | `STATEMENT_RUNS.status` | `GENERATING`／`GENERATED`／`FAILED` |

   **二者皆有 `GENERATED` 但語意不同**（前者「帳單已產製」、後者「批次跑完」），
   不得混用同一狀態對照表（`SDD-007 §5.4.5` 已同步明訂）。
   原 §9 將兩實體繪於單一圖，且**缺 `DISPUTED`／`CLOSED`**，已一併更正。
10. **`DISPUTED` 狀態之產生者尚未實作**：`§9.1` 轉換 6／10／11／12 涉及與
   `dispute-service` 之連動（`BRD-001` BF-09），該服務目前**僅有骨架**，故無程式路徑可達。
   同理 `CLOSED`（轉換 12～14）之結案／歸檔排程亦未實作。
11. **平台層請求層例外之狀態碼缺口**：缺必要標頭、非法 JSON、不支援之方法等
    會回 **500 `CC-PLT-5000`**（契約要求 400／405）。屬**全平台**問題，
    已立 **`CR-004`**（待核定）。

**`CR-006`（2026-09-20 核定）後補記**：

- **循環利息之來源已打通**：`posting-service` 新增計息服務
  （`POST /api/v1/posting-interest/recalculate`，見 `SCS-007 §8.1`），
  由結帳／批次流程**於結帳前**觸發，利息以 `TxType.INTEREST` 入帳後，
  本服務彙總時即可讀得（此前**無人產生** `INTEREST` 交易）。
- **計息區間以「往前推一個月」推算**（§8.1）——未使用上一期實際結帳日；個別帳戶計息失敗**僅記錄於日誌**，未納入 `StatementRun` Checkpoint 明細。
- **最低應繳維持簡化（`CR-006 #6`）**：`computeMinimumDue` 本期**不納入逾期加成**
  ——實務最低應繳尚涉及循環息、逾期加成與法規上限，本期維持既有簡化公式，
  並於此明列以供查核。

**`CR-009`（2026-09-20 核定採 A）後補記**：

12. ✅ ~~**排除 `REVERSED` 交易 → 與沖正分錄重複減免、帳單短收（P0）**~~ → **已修復**
    - **原缺陷實測**（2026-09-20，真實 Oracle）：10 月帳單
      `purchases_debits=67,845`、`payments_credits=12,345`、`new_balance=57,898`；
      正確應為 `80,190`／`12,345`／**`70,243`** → **短收 12,345 分**。
    - **根因**：`TXN2026091900001`（`PURCHASE` 12,345，狀態 `REVERSED`）被跳過，
      其沖正分錄 `TXN2026091900003`（`REVERSAL` 12,345）仍計入貸方 → **減免兩次**。
    - **修法**：移除狀態過濾，借貸照列相抵（見 §7 三情境對照表）。
    - **重跑實測（2026-09-20）**：清帳單三表與 `INTEREST` 交易後以每月 15 日重跑三期 →
      10 月 `purchases_debits=80,190`／`new_balance=70,243`（**與上列正確值完全一致**）、
      11 月 `prev_balance=70,243`／`new_balance=71,309`；明細 9 月 1／10 月 6（含一借一貸）／11 月 1，各期互斥。
    - ⚠️ **修復前產生之帳單金額皆為短收**，且 `prev_balance` 會**逐期傳遞**錯誤
      （11 月 `prev_balance=57,898` 亦錯）→ 既有帳單資料須**重建**。
13. **明細出現「一借一貸」兩行係正確行為**：同期沖正時，帳單明細會同時列出
    原始消費與其沖正分錄。此為**稽核留痕所需**，非缺陷。已於 §7 明列，
    避免日後為「讓明細變乾淨」而改成成對排除（反而會在跨期時漏掉貸方）。

**`C-32`（2026-09-22）後補記**：

14. ✅ ~~**計息幣別硬編 `TWD`**~~ → **已修復**（見 §8.1）
    - **原實作**：`interestClient.accrueInterest(..., "TWD")` 寫死，而帳單幣別取自
      `balance.currency()` → 非 TWD 帳戶將「利息以 TWD 入帳、帳單標示他幣」。
    - **修法**：`runCycle` 先取 balance，以其幣別觸發計息，並將**同一快照**傳入
      `generateOne`（計息幣別與帳單幣別**同源**，且每帳戶僅查一次餘額）。
    - **真機實證**：帳戶幣別 `USD` → 入帳 `TXN20270215000016` 與帳單 `STMT2027020012`
      同步為 `USD`（1066）；還原 `TWD` 後恢復。
15. **與 `pricing` 之四捨五入一致性——已驗證（`C-32`）**：
    取整僅發生於 `pricing`（`HALF_UP` 至分），本服務**以入帳交易加總、不重算**，
    故三方逐分相同。真機實測（帳戶 `…0010`，本金 67,845 分 × 18.5%）：
    `pricing` 試算 **1066** ＝ `posting` 交易 `TXN20270115000015` **1066**
    ＝ 本服務 `STMT2027010011` `finance_charges` **1066**；
    帳單明細並**直接引用該入帳交易編號**（可逐筆追溯）。
    ⚠️ 本服務之「最低應繳」仍為**整數截斷**（`newBalance × 10 ÷ 100`），
    與 `pricing` 之 `HALF_UP` **不同調**——惟該欄位非 `pricing` 產出、
    且屬簡化公式（§10 #5），故不構成跨服務不一致；俟參數化時一併統一取整方向。

---

## 11. 修訂記錄

| 版本 | 日期 | 修訂者 | 說明 |
|---|---|---|---|
| **1.11** | 2026-10-02 | WorkBuddy AI | **`CR-033`：更正事件消費者之記載**——①**§7.2** 原載「消費者：`payment`」→ 更正為**僅 `notification`**（**已實作**，`#A-141`）；「出帳後觸發自扣」屬**未實作功能**（`SCS-008 §10`）而非事件消費，且自扣之**執行**為**寫入**，依 `CR-030 §0` **P2** 命令不得走事件 → **`payment` 非本事件之消費者**；②**§10** 之「`payment` 之消費端尚未實作，屬 `CR-029 §4 #13` 待裁定」同步更正為「**已裁定：非消費者**（`CR-030 §2 #5`＋`CR-033 §2.1`）」。★ **零程式變更**（純文件更正） |
| **1.10** | 2026-10-01 | WorkBuddy AI | **`CR-029` 批次 C-2：§7.2 payload 補【可選】`currency`**——`notification-service` 訂閱本事件並以**既有種子模板 `STATEMENT_ISSUED`**（EMAIL，需 `{statementMonth}`／`{totalAmount}`／**`{currency}`**／`{paymentDueDate}`）建立帳單通知，惟 §7.1 之 payload 無 `currency` → **變數不足即無法建立通知**（`CC-NOT-1003`；與 `SCS-008 §7.2` **同族**）。依 `SAD-004 §5.3`「**新增可選欄位 → 不升版**」補入；`statements.currency` **本即存在**（非新增資料）；★ 明訂 `currency` 為「**本期應繳金額**之幣別」（與 `totalAmount` 同源），**不得**改取帳戶之其他幣別。**相容性**：舊事件之消費者須容忍缺欄（`SCS-016 §7.12`：缺則**不建立並記 WARN**，**不臆造幣別**）。**先例**：`CR-027 §5 #11`（`#A-125`）／`#14`（`#A-128`）／`SCS-008 §7.2`——皆「依建議執行 ＋ 登記待裁定」 |
| **1.9** | 2026-10-01 | WorkBuddy AI | **`CR-027` 批次 A-4（`#A-135`）：新增 §7.1「事件輸出」**——`statement.statement.issued.v1`：主題（`cc.statement.statement.issued`）／分區鍵（`statementId`）／payload 逐欄來源對映（★ **`totalAmount` ＝ `newBalance()`**，對映依據為契約 `StatementView.totalAmount`，**不得**誤取 `purchasesDebits` 等單項）／發射與否（冪等重跑**不發射**、零產出**仍發射**）。★ **併載本節之關鍵設計**：`runCycle` 刻意不用外層交易（批次需 Checkpoint），原交易邊界在 `JdbcStatementRepository.save`（每帳戶一筆）→ **不可**「先 `save()` 再 `append()`」（會各自提交成兩個交易）；故抽出**獨立 bean** `StatementIssueService`（`@Transactional issue(...)` 內完成 save ＋ append）。⚠️ **併載反例**：把 `generateOne` 標 `@Transactional` **無效**——同類別自我呼叫，Spring 代理**不介入**，交易**靜默不生效**。⚠️ **本列為回填**：`#A-135` 當時**漏升版次與補列**（由 `#A-141` 自查發現並補正） |
| **1.8** | 2026-10-01 | WorkBuddy AI | **新增 §8.2「帳單 PDF 產生與物件儲存」（`GOV-007 §5`；BEAM PMO 裁定「必做」）**——**規格先行** → **依賴查證**（`version-compat-verification`；Maven Central 實查：**Apache PDFBox 3.0.8** 採 Apache 2.0、父 POM `source/target 1.8` → Java 21 相容；OpenPDF 依賴較重；**iText 之 AGPL 不採**）→ 加依賴 → 實作 → 上傳物件儲存 → 真機驗證。**設計要點**：`StatementPdfPort`（產生）＋ `ObjectStoragePort`（上傳，複用 `SCS-017 §8.1` 之埠模式）→ **先上傳成功才寫 `PDF_REF`**（避死連結）；**不退回本地檔案**（fail-loud）；憑證 **fail-closed**。⚠️ **中文字型須內嵌**（PDF 標準字型無 CJK）。**§10 之「PDF 產生器仍待」解除**；**寄送通道仍為真阻塞** |
| **1.7** | 2026-10-01 | WorkBuddy AI | **§10 帳單 PDF 儲存媒體更正（`HANDOVER #A-106`）**：原載「儲存媒體（物件儲存）**待基礎設施設計凍結**」→ 更正為「**已定**」——契約為 **S3 API**，本地替代方案為 **Garage `v2.4.1`**（已建置並實測，見 `SAD-006 §3.2`），正式環境為客戶之企業物件儲存。**仍待**：PDF 產生器本身（需額外函式庫）與**電子帳單寄送通道**（`§3.D D-3` 真阻塞）。★ 併載：本地物件儲存原規劃為 **MinIO**，因 MinIO 開源版**已封存不再維護**（官方下載站 410 Gone）而改採 Garage |
| **1.6** | 2026-09-30 | WorkBuddy AI | **新增 §6.1「批次執行清單端點」**（`GET /api/v1/statement-runs?limit=N`）——解鎖 `SDD-007 §5.4.26` 桶 C「批次作業監控」（`SCS-020 §9.9`）。⚠️ **併載實測揭露**：本服務原**僅有觸發與單筆查詢**、**無「列出」端點**（`GET /statement-cycles/{code}/runs` 實測 **405**）——`SCS-020 §9.9.1` 原載「四個既有端點」與事實不符，已更正。回應**沿用既有 `CycleRunView`**（不另造 DTO）；`limit` 預設 20／上限 100；依 `startedAt` 遞減；無資料回空陣列非 404 |
| 1.0 | 2026-09-19 | 開發團隊 | 初版（規格先行），並完成實作與實測 |
| 1.1 | 2026-09-19 | 開發團隊 | 依 **`CR-003`** 更新：①§10 移除「`STATEMENTS.status` 未建模為正式狀態機」之限制（`SDD-005 §9` 已拆分為二）；②新增 #8（兩狀態機與值域對照、`GENERATED` 語意區分）、#9（`DISPUTED`／`CLOSED` 之產生者未實作）、#10（`CR-004` 平台缺口）；③**新增本修訂記錄區**（原文件欠缺） |
| 1.2 | 2026-09-20 | WorkBuddy AI | ①**新增 §8.1「結帳前觸發計息」**（`CR-006`／`HANDOVER #C-38`）：於 `generateOne` **之前**逐帳戶呼叫 `POST /api/v1/posting-interest/recalculate`，區間 `[businessDate-1個月, businessDate)`；計息失敗→**跳過該帳戶、不產生帳單**，批次繼續（實測：利息 1,032 分入 `INTEREST` 交易、帳單 `finance_charges` 讀得）。| | | | ②**§7 揭露「當期交易」實作未做到**並於 §10 新增 #2：交易查詢無期間參數 → **每期重複計收**（繳款／費用／循環息），已提 **`CR-007`**（P0，待核定）。§10 後續條次順移 |
| 1.3 | 2026-09-20 | WorkBuddy AI | 依 **`CR-007`**（BEAM PMO 核定採**方案 A**）修復 P0「每期重複計收」：①**§7** 「取當期交易」改為明訂 `(上期結帳日, 本期結帳日]` 半開區間，`PostingQueryPort` 以期間為**必要參數**；載明無前期帳戶時之 `from` 推算與分頁 **Fail-Closed**；②**§8.1** 補「計息區間與帳單區間錯開一日」之說明；③**§10 #2** 由「待核定」改列為**已修復**，並揭露**歷史髒資料仍在** |
| 1.4 | 2026-09-20 | WorkBuddy AI | 依 **`CR-009`**（BEAM PMO 核定採**方案 A：不排除已沖正交易**）修復 P0「帳單短收」：①**§7** 新增「**不得依 `STATUS` 過濾交易**」及其三情境對照表（同期淨 0／跨期前期借本期貸／跨期本期借下期貸），並載明明細「一借一貸」兩行為正確之稽核留痕；②**§9** 補單測現況（19 例 0 失敗）與 `StatementReversalNettingTest` 3 例之**負樣本判定力實測**；③**§10** 新增 #12（缺陷已修復，含實測短收 12,345 與「錯誤會逐期傳遞、須重建」）、#13（一借一貸非缺陷），並於 #8 加註須與 `CR-009` 一併理解 |
| 1.5 | 2026-09-22 | WorkBuddy AI | **`C-32`：計息幣別一致性**——①**§8.1 新增「幣別來源」**：計息幣別取自帳戶 balance 快照且與帳單幣別**同源**（原硬編 `TWD`，非 TWD 帳戶將「以 TWD 入息、帳單標他幣」）；②**§10 新增 #14**（缺陷已修復，含 `USD` 真機實證）與 **#15**（與 `pricing` 之四捨五入一致性**已驗證**：三方逐分相同、明細回引入帳交易編號；並揭露「最低應繳整數截斷」與 `pricing` `HALF_UP` 不同調但非跨服務不一致）；③單測 29 例 0 失敗（新增 2 例：計息幣別取帳戶幣別、balance 僅查一次） |
