# CR-006　`statement`／`posting` 串接 `pricing-service` 之**資料面缺口**

| 項目 | 內容 |
|---|---|
| 編號 | CR-006 |
| 版本 | **1.1（2026-09-20 核定）** |
| 狀態 | ▶ **已核定／執行中** |
| 提出日期 | 2026-09-19 |
| 核定日期 | 2026-09-20（BEAM PMO：③ 採 B「posting 新增計息服務」，**其餘照我方建議推進**） |
| 提出者 | WorkBuddy AI |
| 影響文件 | `SCS-007`（補計息服務章節）、`SCS-006`（`cardNo` 選填）、`contracts/openapi/pricing-service.yaml`（由 `tools/gen_openapi.py` 產生）、`SCS-011 §10` |
| 關聯 | `HANDOVER §3.C #C-3`（★ 關鍵路徑最後一哩）、`CR-005`（已核定並執行）、`SCS-019` |

---

## 1. 背景

`pricing-service` 之 3 個 REST 端點已於 `CR-005` 核定後完成並實機實測
（利息 **796**、費用 5 筆、違約金夾擠 3 期＝90000）。
`HANDOVER §3.C #C-3` 隨之列為關鍵路徑最後一哩：
「`statement` 逾期加成、`posting` `recalculateInterest` **尚未呼叫**它」。

**動工前查核發現實況與該描述不符**，且缺口在**資料面**而非接線面：

| 查核項 | 實況（2026-09-19 實測／讀碼） |
|---|---|
| `posting-service` 之 `recalculateInterest` | **不存在**——應用程式層僅 `PostingService`／`DuplicateCheckService`／`PostingQueryService`，**無任何利息計算** |
| `statement-service` 是否計算利息 | **否**——`StatementGenerationService.generateOne` 僅**彙總 posting 既有之 `INTEREST` 交易**（`case "INTEREST" -> interest += amount`），本身不產出利息 |
| 利息從何而來 | **無人產生 `TxType.INTEREST` 之交易**——`posting` 有該型別可入帳，但沒有任何程式呼叫它 |

> 換言之，C-3 不是「把既有佔位接上」，而是「**新增一個尚不存在的能力**」：
> 誰去呼叫 `pricing`、算出來的利息如何變成 `INTEREST` 交易入帳。

---

## 2. 缺口明細

### #1　每日餘額（ADB）資料來源 —— 🔴 阻塞

`pricing` 之 ADB 計息法（`calcMethod=ADB`，`CR-005 #3` 定案之四種計息方法之一）
需輸入 **`dailyBalances`（日終餘額序列）**。
查 `posting-service` 之 DDL，表僅有：
`balance_history`、`balances`、`sub_account_entries`、`transactions`
——**沒有每日餘額表**，亦無任何 `daily_balance`／`ADB` 相關欄位。

→ **ADB 計息目前無資料可供輸入。**

### #2　卡號來源 —— 🔴 阻塞

`InterestCalcRequest` 之 **必填欄位為 `accountId`、`cardNo`、`periodStart`、`periodEnd`**。
但 `posting-service` 之交易表**不含 `card_no` 欄位**（查 DDL 無此欄）。

→ 即使接線完成，**必填欄位仍無法滿足**。

### #3　計息之觸發點與責任歸屬 —— 🔴 阻塞（設計決策）

利息究竟由誰、在何時計算並入帳？三個候選：

| 選項 | 說明 | 優點 | 疑慮 |
|---|---|---|---|
| A | **結帳時**由 `statement-service` 呼叫 `pricing`，再把利息交 `posting` 入帳 | 貼近「帳單週期」語意 | statement 須同時依賴 pricing 與 posting；`SCS-011 §8` 原僅定義對 posting 之依賴 |
| B | **入帳域**由 `posting-service` 新增計息應用服務（即 C-3 所稱 `recalculateInterest`），由批次／結帳流程觸發 | 責任單純（利息＝一種入帳），statement 仍只讀 posting | 需在 `SCS-007` 新增能力；且 posting 仍缺 #1、#2 之資料 |
| C | **獨立批次作業**（`SCS-019`）：週期性掃描未結清帳戶 → 呼叫 pricing → 入帳 | 與結帳解耦、可重試 | 需批次框架支援；時序（先計息或先結帳）須定義 |

### #4　違約金（`lateFeeAmount`）之入帳科目 —— 🟡

`InterestCalcView` 回傳 **`lateFeeAmount`／`lateFeeCount`**。
`posting` 之 `TxType` 目前有 `FEE`、`INTEREST`、`ADJUSTMENT` 等，
**違約金應歸 `FEE` 還是新增 `LATE_FEE`？** 影響分戶與報表歸類。

### #5　四捨五入與幣別一致性 —— 🟡（原 `HANDOVER #C-32`）

`pricing` 計息結果傳遞至 `posting` 入帳、再由 `statement` 彙總，
**三段之取整時點與幣別尚未驗證一致**（`SCS-006 §10` 已列為限制）。
串接後此風險由「潛在」變為「實際」。

### #6　`statement` 最低應繳之「逾期加成」 —— 🟡

`Statement.computeMinimumDue` 目前為**簡化版**
（`Statement.java` 註明：實務尚涉及循環息、逾期加成與法規上限）。
是否於本期納入逾期加成？若納入，需求哪些輸入？

---

## 3. 建議方案（待 BEAM PMO 裁定）

| # | 建議（我方意見） | 須 BEAM PMO 定奪之點 |
|---|---|---|
| 1 | 若本期**不**走 ADB，可用 `CONSTANT_PRINCIPAL`＋`principalAmount` 先行串接（`pricing` 已支援，實測 796 即此法）；ADB 待新增日終餘額表後再啟 | **本期是否接受先以非 ADB 計息串接？** 或**直接新增日終餘額表**（新增 `V*__daily_balances.sql`，須定義留存與回填） |
| 2 | 由 `posting` 呼叫 `card-service` 取得卡號，或改由呼叫端（`statement`／批次）帶入 | **卡號由誰提供？** 是否放寬 `pricing` 之 `cardNo` 為選填（須改契約，另提 CR） |
| 3 | **建議 B**：`posting-service` 新增計息應用服務（即 `recalculateInterest`），責任歸入帳域，`statement` 維持只讀 posting（不改 `SCS-011 §8` 之依賴邊界） | **A／B／C 擇一**；並定義觸發時序（計息是否必須早於結帳） |
| 4 | 建議歸 `FEE` 並於 `SUB_ACCOUNT` 以子型區分，避免 `TxType` 擴張 | 是否接受？或新增 `LATE_FEE` |
| 5 | 建議定為「**一律於 pricing 端取整**（`HALF_UP`、最小單位**分**），下游不得再取整」，並於串接時補一致性整合測試（即 `HANDOVER #C-32`） | 取整責任歸屬 |
| 6 | 建議本期**維持簡化**，但於 `SCS-011 §10` 明列「未含逾期加成」 | 是否本期納入 |

---

## 4. 不受缺口影響、可立即施作之範圍

- `pricing-service` 端點本身（已完成並實測，**不須改動**）。
- `posting-service` 之 `pricing` 客戶端**骨架**：可依既有
  `PostingServiceClient`（標準 `HttpClient`、逾時 800ms、失敗拋領域例外）模式先行建立，
  待 #1／#2 定案後填入欄位對應。
- `HANDOVER §3.C #C-32`（Round-off 一致性整合測試）之**測試設計**，
  可先於 `pricing` 側準備。

---

## 5. 誠實揭露（提出時）

1. **`HANDOVER §3.C #C-3` 之描述與實況不符**：該列稱兩服務「仍為待定價就緒之佔位」，
   實測讀碼後確認 **`posting-service` 並無 `recalculateInterest` 方法**，
   `statement-service` 亦**未計算**利息（僅彙總既有 `INTEREST` 交易）。
   → 已於本 CR 更正描述；`HANDOVER` 將同步修訂。
2. **本 CR 未做任何程式改動**——動工前查核階段發現阻塞即提出，
   依「不邊寫邊猜」原則，**未逕自假設計息方法或新增資料表**。
3. 上列缺口**任一**未裁定即動工，將導致「串接完成但資料湊不出來」之假象——
   此為本次刻意避免之結果。

---

## 6. 核定結果與定奪（2026-09-20 BEAM PMO）

> BEAM PMO 批示：「③ 觸發點歸屬 —— posting 新增計息服務，**其餘也由您的建議推進**」。

| # | 核定結果 |
|---:|---|
| 1 | **照建議**：本期以 `CONSTANT_PRINCIPAL` ＋ `principalAmount` 串接（ADB 已實測可行，值 796）；**ADB 須待新增日終餘額表後再啟**，列為後續 |
| 2 | ⚠️ **依新事實調整建議**（見下）：改為**放寬 `pricing` 之 `cardNo` 為選填**，不再由 `posting` 向 `card-service` 取卡號 |
| 3 | **B**：`posting-service` 新增計息應用服務（`recalculateInterest`），責任歸入帳域；由結帳／批次流程於**結帳前**觸發 |
| 4 | **照建議**：違約金歸 `TxType.FEE`（`SubAccount.FEE`），不新增 `LATE_FEE` 型別 |
| 5 | **照建議（用詞已依實作更正）**：取整責任歸 `pricing`——四捨五入 `HALF_UP` **取整至最小單位（分）**（`SCS-006 §7`、`CSP-007 §5.1`）；`posting`／`statement` 下游不再取整 |
| 6 | **照建議**：`statement` 最低應繳維持簡化，於 `SCS-011 §10` 明列「未含逾期加成」 |

### #2 為何調整——動工後查得之新事實（誠實揭露）

原建議「由 `posting` 呼叫 `card-service` 取得卡號」。進一步查證後發現**此路不通且不宜**：

1. **查無其端點**：`card-service` 契約僅 `POST /api/v1/cards`（發卡）與
   `GET /api/v1/cards/{cardId}`，**沒有依 `accountId` 查詢之端點**。
2. **更關鍵——明文卡號在本系統內不存在**：
   `Card` 領域模型僅有 `cardNoMasked`，資料表亦只存卡號雜湊；
   `GET /api/v1/cards/{cardId}` 回傳之為 `**** **** **** 1111`。
   這是**正確的 PCI 設計**，不應為串接而開明文出口，
   更不應讓明文卡號在服務間傳遞。

故改為：**`InterestCalcRequest.cardNo` 放寬為選填**，計息以 `accountId` 為識別主鍵，
`cardNo` 若帶入僅供對帳顯示、**應為遮罩值**。回傳 `InterestCalcView.cardNo` 同步改為選填。
此調整同時省去新增 `card-service` 查詢端點之工作量，且**更符合資安要求**。

---

## 7. 實作結果與實測證據（2026-09-20）

### 交付

| 項目 | 內容 |
|---|---|
| `posting-service` | 新增 `InterestCalculationPort`（domain/port）、`PricingServiceClient`（infrastructure/client）、`InterestPostingService`（application）、`InterestRecalcRequest`／`InterestRecalcView`、`InterestPostingController` |
| 端點 | `POST /api/v1/posting-interest/recalculate` |
| `pricing-service` | `InterestCalcRequest.cardNo` 移除 `@NotBlank`（配合 #2 放寬為選填） |
| 契約 | `tools/gen_openapi.py` 調整後重產（驗證 0 錯誤） |
| 錯誤碼 | 新增 **`CC-PLT-5005`（503 定價服務呼叫失敗）**，錯誤碼 **114 → 115** |
| 測試 | `posting-service` 新增 6 測試（`UT-POST-021`／`022`），全服務 **17 測試 0 失敗** |

### 實機實測（真實 Oracle ＋ 真實 `pricing-service`）

帳戶 `018f2a3b-…0010`，本金 **67,845 分**，區間 2026-08-16～09-15，利率 18.5%(`ACT_365`)。

| # | 情境 | 結果 |
|---:|---|---|
| ① | `dryRun=true` 試算 | 200，`interestAmount=1032`、`calcBasis=CONSTANT_PRINCIPAL`，**`postingIds` 為空** ✅ |
| ② | 實際入帳 | 200，產生 `postingId`；**Oracle 查得 `tx_type=INTEREST`、`amount=1032`** ✅ |
| ③ | 同期重送 | 回傳**同一個** `postingId`；Oracle `COUNT=1`（未重複計息） ✅ |
| ④ | **`pricing` 不可用** | **503 `CC-PLT-5005`**；Oracle `INTEREST_TX_COUNT` **仍為 1**——**未 fail-open** ✅ |

金額自證：67,845 × 18.5% × 30/365 = **1,031.6 → 1,032 分**（`HALF_UP`），與定價端一致。
分戶餘額同步為 `interest_amount=1032`、`total_amount=69,177`（67,845＋1,032）✅

### 動工後修正之兩處（誠實揭露）

1. **#2 建議方向調整**（理由見 §6 之 #2 說明）：卡片無明文卡號，改為放寬 `cardNo` 選填。
2. **#5 用詞更正**：原建議寫「取整至元」，實作查得 `SCS-006 §7`/
   `InterestCalcView` 明定金額為**最小單位（分）**、`HALF_UP` 取整至分——
   已更正 CR 與 `SCS-007 §8.1` 之措辭（數字不變，但單位用詞若沿用將導致帳務誤解）。
3. **客戶端漏 `X-Idempotency-Key`**：`pricing` 之 `POST interest-calculations`
   要求該標頭為必填，初次呼叫得 HTTP 400（實測後補上，鍵值由帳戶＋區間衍生）。
