# TRP-003　覆蓋率提升計畫（行 53.7% → 80%）

| 項目 | 內容 |
|---|---|
| 文件編號 | TRP-003 |
| 版本 | **3.13** |
| 狀態 | ✅ **批次 20 完成（`WBS 5.37` 結案，8／8 模組）——★★★「全 backend」行 `91.94%`／分支 `76.18%`**；★ 併立並執行 **`CR-042`**（`JwtAuthFilter` 之 401 路徑 NPE） |
| 撰寫日 | 2026-10-05 |
| 撰寫者 | WorkBuddy AI（開發團隊） |
| 緣起 | BEAM PMO：「**然後再執行覆蓋率提升（行 53.7% → 80%）的任務**」 |
| 上游 | `CSP-005 §4`（門檻：行 ≥80%／分支 ≥70%）、`TRP-002 §7.2 #1`（工具缺口） |
| 關聯 | `docs/08-evidence/N1_工具缺口補齊與量測結果.md §1`（基線）、`TRP-001 §6.2`（準出 #1） |

---

## 1. 現況（2026-10-05 實測）

| 指標 | 實測 | 門檻 | 差距 |
|---|---:|---:|---:|
| **行覆蓋率** | **53.7%**（8,034／14,970） | ≥ 80% | **−26.3 pp**（需再涵蓋 **+3,942 行**） |
| **分支覆蓋率** | **46.8%**（2,164／4,623） | ≥ 70% | **−23.2 pp** |

---


## 2. ★★ 缺口分析（本計畫之核心依據）

### 2.1 依「架構層」之未覆蓋行分布

| 層 | 未覆蓋行 | 該層覆蓋率 | **佔總未覆蓋** |
|---|---:|---:|---:|
| **`jdbc`**（儲存層） | **2,833** | **3.2%** | **40.8%** |
| `application`（應用層） | 1,095 | 73.6% | 15.8% |
| `model`（領域模型） | 703 | 80.8% | 10.1% |
| **`rest`**（控制器） | 480 | **4.4%** | 6.9% |
| **`outbox`**（交易外寄匣） | 336 | **0.0%** | 4.8% |
| **`client`**（HTTP 客戶端） | 316 | 13.2% | 4.6% |
| **`messaging`**（Kafka 消費） | 245 | **0.0%** | 3.5% |
| `storage`（物件儲存） | 124 | 0.0% | 1.8% |
| `config` | 101 | 35.7% | 1.5% |
| `scheduler` | 88 | 0.0% | 1.3% |
| 其餘 | ~615 | — | ~8.8% |

### 2.2 ★★ 關鍵洞察：缺口集中在「**與外部世界接觸的層**」

**「接觸外部」之層**（`jdbc` ＋ `rest` ＋ `outbox` ＋ `client` ＋ `messaging` ＋ `storage` ＋ `scheduler`）
合計 **4,423 行 ＝ 佔總未覆蓋的 63.8%**。

**而純業務邏輯層已相當健康**：`application` **73.6%**、`model` **80.8%**。

> ★★ **為何這些層是 0%？根因不是「沒寫測試」，而是「真機驗證的方式不進 JVM」**：
> 本專案之真機驗證為**人工 `curl` ＋ SQL 查核**（見 `HANDOVER` 各 `#A-1xx` 之驗證紀錄），
> 該流程**在 JVM 之外執行** → **對 JaCoCo 之貢獻恆為 0**。
>
> ★ 已查證確認：**全庫 `@Disabled` 之測試為 0 個**；
> `JdbcLimitRepository`／`JdbcStatementRepository` **被任何測試提及的次數為 0**——
> 即**並非「有測試但被略過」，而是真的沒有測試**。

### 2.3 ★★ 因此：策略**不是**「多寫單元測試」

| ❌ 不該做 | ✅ 該做 |
|---|---|
| 為覆蓋率而寫 **mock 掉 DB／Kafka／HTTP** 的測試 | ★ **以真實基礎設施寫整合測試**（`@SpringBootTest` ＋ 本機 Oracle／Kafka／PostgreSQL） |
| 因為「客戶要求禁 mock」而放棄這些層 | ★ **把「人工真機驗證」升級為「自動化真機測試」** |

> ★★ **本計畫之核心主張**：**「真機驗證自動化」一舉兩得**——
> ① 覆蓋率大幅提升（這些層正是缺口主體）；② **把現行人工 `curl` 的驗證變成可回歸的自動測試**
> （今日之真機驗證**無法重跑、無回歸保護**，是更大的風險）。
>
> ⚠️ **且不違反客戶「不得 mock」之硬性要求**——**真實基礎設施、真實 SQL、真實 Kafka**。

---


## 3. 分批計畫

| 批次 | 範圍 | 預估可涵蓋 | 手段 |
|---|---|---:|---|
| **B1** | ★ **`jdbc` 層**（**最大槓桿 40.8%**） | **~2,400 行** | `@SpringBootTest` ＋ 本機 Oracle；逐服務涵蓋 `Jdbc*Repository` 之 CRUD／查詢／樂觀鎖 |
| **B2** | **`rest` 層**（6.9%） | ~400 行 | 控制器整合測試（真實 DB ＋ 真實認證或顯式停用） |
| **B3** | **`outbox`／`messaging`／`client`／`storage`／`scheduler`**（16.0%） | ~900 行 | Outbox 轉送、Kafka 消費（可注入事件）、HTTP 客戶端（WireMock **或** 真實對端服務）、S3 客戶端（Garage）、排程 |
| **B4** | **`application`／`model` 補強**（25.9%） | ~500 行 | 既有測試之分支補強（error path／邊界） |
| **B5** | **門檻接入** | — | 覆蓋率達標後，將 `jacocoTestCoverageVerification` **接入 `check`**（`CSP-005 §4.1`） |

**預估**：B1～B4 可涵蓋約 **+4,200 行** → 行覆蓋率 **約 80～82%**（**估計值，非承諾**）。

> ⚠️ **誠實評估**：本任務屬**多批次工程**，**不得聲稱一次完成**。
> 且整合測試之覆蓋率**不會達 100%**（部分錯誤分支需特殊情境）。

---


## 4. 執行紀律（本計畫之強制要求）

| # | 要求 |
|---:|---|
| 1 | ★ **不得為覆蓋率而寫 mock 測試**——違反客戶硬性要求（`scan_mockdata`）與 `CSP-005 §6` |
| 2 | ★ 整合測試**必須可重複執行**（不得依賴殘留資料；每測試自建／自清） |
| 3 | ★ **基礎設施不可用時**：以 `Assumptions.assumeTrue(...)` **略過並標明**——**不得靜默通過** |
| 4 | ★ **覆蓋率量測需本機基礎設施啟動**（Oracle／Kafka／PostgreSQL／Garage）——**此為已知之前提**，須於 CI 文件載明 |
| 5 | ★ 每批次**獨立驗證**（全量測試 ＋ 覆蓋率重測），並更新本檔之「執行紀錄」 |
| 6 | ★ **驗證結果一律等實跑完再填**（`CR-038 §4.2`） |

---


## 5. 執行紀錄

| 批次 | 日期 | 內容 | 行覆蓋率 | 分支覆蓋率 | 全量測試 |
|---|---|---|---|---|---|
| — | 2026-10-05 | 基線（`#A-160`） | 53.7% | 46.8% | 1096／0 失敗 |
| **B1-a** | 2026-10-05 | ★ **`limit-service` 之 `jdbc` 層整合測試**（`LimitRepositoryIntegrationTest`，17 測；`#A-167`） | ★ **54.7%**（+1.0 pp） | — | **1113／0 失敗** |
| **B1-b** | 2026-10-05 | ★ **同上類別擴充**（**+16 測**，涵蓋調整軌跡／政策／串動紀錄／Outbox；`#A-168`） | ★ **約 55.9%**（估計） | — | **1129／0 失敗** |
| **B1-c/W1-a** | 2026-10-05 | ★ **`installment-service` 之 `jdbc` 層**（`InstallmentRepositoryIntegrationTest`，**4 測**；`#A-170`） | ★ **約 56.3%**（估計） | — | **1133／0 失敗** |

### 5.1 ★★ B1 之實測增益（`#A-167`／`#A-168`）

| 指標 | B1 前 | B1-a 後 | **B1-b 後** |
|---|---:|---:|---:|
| **`limit-service` 行覆蓋** | 40.7% | 52.6% | **67.0%** |
| **`limit-service` 分支覆蓋** | 33.5% | 43.7% | **51.4%** |
| ★ **`jdbc` 層覆蓋** | **3.2%** | 36.4% | ★★ **81.0%** |
| `limit-service` 測試數 | 69 | 86 | **102** |

**逐類別（B1-b 後）**：

| 類別 | 覆蓋 |
|---|---|
| `JdbcOutboxRepository` | ★ **30/30** |
| `JdbcSpecialLimitTempAdjustmentRepository` | ★ **52/52** |
| `JdbcLimitAdjustmentRepository` | **60/62** |
| `JdbcSpecialLimitPolicyRepository` | **37/39** |
| `JdbcSpecialLimitRepository` | 58/65 |
| `JdbcLimitRepository` | 88/113 |
| ⚠️ `JdbcPaymentPostedProjectionRepository` | **0/20**（未做） |
| ⚠️ `JdbcPaymentReoccupyProjectionRepository` | **0/20**（未做） |

> ★★ **誠實推估**：**`limit-service` 一個服務 ＝ 約 +2.0 pp（+181 行）**。
> 要到 80%（+3,942 行）→ **約需 20～22 個服務同等規模之整合測試**（全庫共 17 個領域服務）。
> **故本任務仍為多批次工程**；建議「**每批次 3～5 個服務**」推進，並於每批次後重測。

### 5.2 ★ 實作要點（供後續批次沿用）

| # | 要點 |
|---:|---|
| 1 | ★ **`@JdbcTest` 不可用**——Spring Boot 4 起測試切片**已模組化**，`@JdbcTest`／`@AutoConfigureTestDatabase` **不在** `spring-boot-starter-test` 內（實測編譯失敗）。→ 改**純 JUnit 5 ＋ 手動 `JdbcTemplate`**（**更輕、無 Spring context、啟動更快**）。 |
| 2 | ★ **Testcontainers Oracle 之套件為 `org.testcontainers.oracle`**（**非** `org.testcontainers.containers`）——以 `unzip -l` 實查 jar 確認。 |
| 3 | ★ **映像須用 `gvenzl/oracle-free`**（Testcontainers 之 `OracleContainer` 以 `APP_USER`／`APP_USER_PASSWORD` 建應用帳號）；**官方 `container-registry.oracle.com/database/free` 不適用**（不吃該組環境變數）。 |
| 4 | ★ **Flyway 於 `@BeforeAll` 手動執行**——附帶效益：**每次跑測試都驗證「遷移可在乾淨 schema 上套用」**。 |
| 5 | ★ **以 `@BeforeEach` 明確 `DELETE` 取代 `@Transactional` 回滾**（本類別無 Spring context）。 |
| 6 | ⚠️ **`jdbc` 層尚有 255 行未覆蓋**（`JdbcLimitAdjustmentRepository` 62／`JdbcSpecialLimitTempAdjustmentRepository` 52／`JdbcSpecialLimitPolicyRepository` 39／`JdbcOutboxRepository` 30／`JdbcPayment*ProjectionRepository` 40）→ **B1-b 續作**。 |

---

---

### 5.3 ★ B1-b 之兩個實測收穫

| # | 收穫 |
|---:|---|
| 1 | ★★ **測試抓到「我自己的理解錯誤」**：初稿斷言 `outstandingMinor() == 10_000`，實際為 **5,000**——我把「**未回收額**」誤當成「**已調高額**」。**程式正確、斷言錯誤**。→ 這正是「真機整合測試」之價值：**它會反駁你**。 |
| 2 | ★ **條件不得放寬**：`findEffectiveTemporaryExpired` 與 `findOpenDerivedRaisedOnOrBefore` 兩者之測試**刻意各造 3～4 種近似情境**（僅 1 種應命中）——避免「條件寫鬆也能過」之假綠。 |

### 5.4 ★ B1-b 未完成項（`TRP-003 §5.2 #6` 之殘留）

`JdbcPaymentPostedProjectionRepository`／`JdbcPaymentReoccupyProjectionRepository`（**各 20 行**）
**未做**——其建構子需注入 `LimitRepository`，且語意為「事件消費之冪等投影」，
**需與事件語意一起設計**，非單純 CRUD。→ **列 B1-b2**（WBS 5.13）。


### 5.5 ★ W1-a（`installment-service`）實測（`#A-170`）

| 指標 | B1-c 前 | **W1-a 後** |
|---|---:|---:|
| `installment-service` 行覆蓋 | 47.8% | ★ **73.3%** |
| `installment-service` 分支覆蓋 | 52.2% | **66.3%** |
| `JdbcInstallmentRepository` | **0/73** | ★ **60/73** |
| `OracleRawId` | — | **11/11** |
| 測試數 | 25 | **42** |

**★ 實測收穫**：**測試發現 `update()` 具樂觀鎖**（`DomainException`「分期狀態更新衝突，請重試」）。
⚠️ **但其 version 約定尚未查明**——`findById` 讀回之 `version` 與 `version + 1` **皆衝突** →
**成功路徑測試暫緩**，並於測試檔內**誠實註記**（列 **W1-a2**）。
★ **不留下紅燈**：衝突路徑已由 `updateWithStaleVersionIsRejected` 覆蓋並通過。

### 5.6 ★★ W1 完成（`#A-171`）——含「**先查證再寫**」之三件查證

BEAM PMO：「**先把 W1-b 的兩個查證點解決（PRICING_RULES 建表位置 + 巢狀列舉值域），連同 W1-a2 的 version 語意——三件都查清楚再寫**」

#### 5.6.1 三件查證之結果

| # | 查證點 | 結果 |
|---:|---|---|
| 1 | **`PRICING_RULES` 建表位置** | ★ 在 **`V1__baseline.sql`**，寫為 **`CREATE TABLE pricing_rules`（小寫）**；`V2` 另 `ADD tx_type`。★★ **初查以「大寫」grep 而誤判「查無」**——Oracle 未加引號之識別字**不分大小寫**，但 **`grep` 分**。 |
| 2 | **巢狀列舉值域** | ★ `RuleType` **7 值**（INTEREST／ANNUAL_FEE／CASH_ADV_FEE／LATE_FEE／FOREIGN_TX_FEE／OVERLIMIT_FEE／FEE_WAIVER）／`TxType` **2 值**／`CalcMethod` **4 值**／`RuleStatus` **2 值**——★ **與 DDL 之 `CHECK` 約束完全一致**。 |
| 3 | **`update()` 之 version 語意** | ★★ **`SET VERSION = plan.version()`（新版本）；`WHERE VERSION = plan.version() − 1`（舊版本）** → **`plan.version()` 必須 ＝ 「DB 現行 ＋ 1」**。★ 傳「現行版本」會使 WHERE 變成「現行 − 1」→ 必不匹配。**此為樂觀鎖之正確語意，非缺陷。** |

#### 5.6.2 ★★ 寫測試時又發現兩件事

| # | 發現 |
|---:|---|
| 1 | ★★ **`update()` 之 SET 子句僅涵蓋 5 個欄位**（`STATUS`／`REMAINING_PERIODS`／`PRINCIPAL_OUTSTANDING`／`EARLY_SETTLEMENT_FLAG`／`VERSION`）——**不含 `PAID_PERIODS`**。→ 初稿斷言 `paidPeriods == 6` 失敗於「expected 6 but was 1」，**測試因而揭露此範圍限制**。⚠️ 屬**既有設計**，已記錄供日後確認。 |
| 2 | ★ **`findEffective` 之 `(X = ? OR ? IS NULL)` 語意**：帶 `null` 維度 ＝ **不限**；但**指定維度時，該維度為 `NULL` 之規則不應命中**——兩者皆已立測（防條件寫鬆）。 |

#### 5.6.3 ★★ 實測增益（W1 完成）

| 服務／類別 | 前 | **後** |
|---|---:|---:|
| `installment-service` 行覆蓋 | 47.8% | **73.6%** |
| `installment-service` 分支 | 52.2% | **69.6%** |
| `JdbcInstallmentRepository` | 0/73 | **61/73** |
| **`pricing-service` 行覆蓋** | 72.7% | ★★ **90.9%** |
| **`pricing-service` 分支** | 62.0% | **74.0%** |
| **`JdbcPricingRuleRepository`** | **0/65** | ★★★ **65/65（100%）** |
| 測試數 | — | installment **43**／pricing **60**（皆 0 失敗） |

> ★★ **W1 兩服務皆達標**（`pricing` 更達 **90.9%**）。
> ★ **教訓**：**「查清楚再寫」是對的**——三件查證各只花一輪，卻省下了數輪的瞎猜與紅燈。


---


## 6. ★★ B1-c 執行計畫（推廣至其餘服務之 `jdbc` 層）

### 6.1 盤點（2026-10-05 實查；「複雜度」以 `jdbc` 層之 public 方法數為代理）

| 服務 | 未覆蓋行 | jdbc 檔 | jdbc public 方法 | 複雜度 |
|---|---:|---:|---:|---|
| `authorization-service` | **326** | 8 | **41** | 高 |
| `rewards-service` | 224 | 5 | 26 | 中高 |
| `card-service` | 205 | 7 | 33 | 高 |
| `statement-service` | 187 | 4 | 18 | 中 |
| `posting-service` | 167 | 4 | 18 | 中 |
| `payment-service` | 149 | 4 | 17 | 中 |
| `merchant-service` | 143 | 4 | 16 | 中 |
| `dispute-service` | 136 | 3 | **12** | 低 |
| `installment-service` | 73 | 2 | **5** | ★ 最低 |
| `pricing-service` | 65 | 2 | **4** | ★ 最低 |
| **合計** | **1,675** | — | — | — |

### 6.2 ★★ 排序原則：**先低複雜度，不先大缺口**

> ★ **不按「缺口大小」排，而按「複雜度」排**——理由：
> ① 先用**低複雜度服務**證明「**同一套模式可跨服務複製**」並建立節奏；
> ② 高複雜度者（`authorization` 41 方法／`card` 33）**風險高**，
>    **應在模式已成熟、且有更多預算時再做**，避免一開始就卡住。

| 波次 | 服務 | 未覆蓋 | 方法數 | 說明 |
|---|---|---:|---:|---|
| **W1** | `installment-service` ＋ `pricing-service` | 138 | 9 | ★ **最低複雜度**——先驗證模式可推廣 |
| **W2** | `dispute-service` ＋ `merchant-service` ＋ `payment-service` | 428 | 45 | 中等 |
| **W3** | `statement-service` ＋ `posting-service` | 354 | 36 | 中等 |
| **W4** | `rewards-service` ＋ `card-service` | 429 | 59 | 中高 |
| **W5** | `authorization-service` | **326** | **41** | ★ **最大但最複雜——最後做** |

### 6.3 ★★ 誠實推估：**只做完 `jdbc` 層仍到不了 80%**

| 階段 | 全庫行覆蓋（估） |
|---|---:|
| 現況（`#A-168`） | **約 55.9%** |
| B1 全部完成（＋1,675 行） | **約 67%** |
| **距 80% 尚缺** | **約 13 pp（≈1,950 行）** |

→ ★★ **必須 B2（`rest` 控制器 480 行）＋ B3（`outbox`·`messaging`·`client`·`storage`·`scheduler` 1,109 行）一併完成**，
方可達 80%。**此為本計畫最重要的規模認知**——避免「以為做完 B1 就達標」之誤判。

### 6.4 可複製之配方（`TRP-003 §5.2` 之推廣）

```
1. 讀該服務 infrastructure/jdbc/*.java 之 public 方法 + SQL 語意（★ 不猜）
2. 讀相關 domain model 之 accessor 與 factory（draft/of/…）
3. 建 XXXRepositoryIntegrationTest：
   @Testcontainers + @Container static OracleContainer("gvenzl/oracle-free:23-slim")
   @BeforeAll：Flyway.migrate() + new JdbcTemplate(DriverManagerDataSource)
   @BeforeEach：明確 DELETE（★ 不得依賴殘留資料）
   斷言：★ 條件類須造 3～4 種近似情境、僅 1 種應命中（防假綠）
4. ./gradlew :services:<svc>:test（★ 必須全量，否則覆蓋率不可比）
5. 量測並更新 TRP-003 §5
```

---

### 5.7 ★ W2-a（`dispute-service`）實測（`#A-172`）——★ **誠實界定範圍**

**涵蓋**（已完全查證者）：`JdbcOutboxRepository`（5 方法，**與其他服務同一模式**）
＋ `nextDisputeNo()`／查無路徑／`stats()` 空集合。

⚠️ **未涵蓋 `Dispute` 聚合之 CRUD**——`Dispute.reconstitute(...)` 需 **`WorkflowStage`** 列舉值，
而該值域**未能由 DDL 之 `CHECK` 約束取得**（`V2` 未見其 CHECK），亦未由 Java 列舉取得。
★ **不以猜測硬湊** → 列 **W2-a2**（待查明後補測）。

| 指標 | W2 前 | **W2-a 後** |
|---|---:|---:|
| `dispute-service` 行覆蓋 | 59.3% | **（見下）** |
| `dispute-service` 分支覆蓋 | 33.3% | **（見下）** |
| 測試數 | — | **+6**（0 失敗） |

★ **已查證並記錄（供 W2-a2 直接用）**：`dispute_cases` 之**完整欄位**、
`Dispute.reconstitute(...)` 之**完整簽章**、以及**由 DDL 取得之值域**——
`org` 7 值／`dispute_type` 5 值／`stage` 6 值／`liability` 4 值／`status` 6 值／
`dispute_reason` 4 值（`V2`）／`chargeback_type` 3 值（`V2`）。**唯一缺口即 `WorkflowStage`**。

---

### 6.5 ★★ B2 手段驗證（spike）之**查證結果**（`#A-178`）——**尚未動工，但關鍵已查明**

BEAM PMO：「**一步一步把 B2/B3 完善，再回去接 W3。我不求快，但求精準無誤。**」

#### 6.5.1 ★★★ 根因：Spring Boot 4 把「測試切片」**拆出** `spring-boot-test-autoconfigure`

| 查證 | 方法 | 結果 |
|---|---|---|
| `spring-boot-test-autoconfigure-4.1.1.jar` **在不在** | Gradle cache 實查 | ✅ **在**（4.1.1） |
| 其 jar 內**有沒有** `WebMvcTest`／`JdbcTest`／`AutoConfigureMockMvc` | `unzip -l` 實查 | ❌ **完全沒有** |
| 它**實際**含什麼 | `unzip -l` | 僅 `OnFailureConditionReportContextCustomizer`／`JsonTest`／`AutoConfigureJson` 等**通用**項目 |
| `spring-boot-webmvc-4.1.1.jar` 內有 `WebMvcTest` 嗎 | `unzip -l` | ❌ **0 命中** |
| ★ **正確模組名** | Maven Central HTTP 查 | ✅ **`org.springframework.boot:spring-boot-webmvc-test:4.1.1`（200）** |

> ★★★ **這解釋了 `#A-167` 之 `@JdbcTest` 為何找不到**——**不是「被移除」，而是「被拆到獨立模組」**。
> ★ **推論**：JDBC 切片之模組名應為 **`spring-boot-jdbc-test`**（**待下一輪以同樣方法確認**）。
> ⚠️ **本發現亦回頭影響 B1**：`@JdbcTest` 若可用，部分 repository 測試可更輕——惟 B1 已採
> **純 JUnit 5 ＋ 手動 `JdbcTemplate`**（無 Spring context，**更快**），**不建議回頭改**。

#### 6.5.2 B2 之**下一步（可直接照做）**

| # | 步驟 | 說明 |
|---:|---|---|
| 1 | 於 `backend/gradle/libs.versions.toml` 之 `[libraries]` 加 **`springBootWebmvcTest = { module = "org.springframework.boot:spring-boot-webmvc-test" }`** | 版本由 BOM 管理（不另指定版本） |
| 2 | 於 `authorization-service` 之 `build.gradle.kts` 加 `testImplementation(libs.springBootWebmvcTest)` | ★ **同時改產生器模板 `T_SERVICE_BUILD`**（`CSP-007` 之「兩邊都動」） |
| 3 | 寫 `AuthorizationControllerTest`：`@WebMvcTest(AuthorizationController.class)` ＋ 以 **`@MockitoBean`**（★ Spring Boot 3.4+ 取代 `@MockBean`，**需實查確認**）注入 `AuthorizeUseCase`／`AuthorizationRepository` | ★ 先以**一個端點**打通 |
| 4 | 跑通後再規模化至其餘 11 服務之 `rest` 層 | — |

> ⚠️ **尚未驗證之兩項（下一輪首要）**：**(i)** `spring-boot-jdbc-test` 之確切座標；
> **(ii)** Spring Boot 4 之 mock 註解為 `@MockitoBean` 抑或仍為 `@MockBean`。
> ★ **不以猜測動工**。

#### 6.5.3 B3 之剩餘範圍（`messaging` 245／`client` 316／`storage` 124／`scheduler` 88）

| 層 | 未覆蓋 | 建議手段 |
|---|---:|---|
| `messaging`（Kafka 消費） | 245 | **Testcontainers Kafka**（`testcontainers-kafka` **已在相依中**）——注入事件、驗消費行為 |
| `client`（HTTP 客戶端） | 316 | ★ **真實對端服務**（本機可起）**或** WireMock；**不得**只 mock 而聲稱已驗證 |
| `storage`（物件儲存） | 124 | **Garage**（本機已建置）——真實往返 |
| `scheduler`（排程） | 88 | 直接呼叫排程方法 ＋ 驗其副作用 |

---

### 6.6 ★★ B2 spike 之**實作結果**（`#A-179`）——已解四個未知，**卡在安全自動配置**

BEAM PMO：「**請繼續自主推進，把 B2/B3 完成**」

#### 6.6.1 ✅ 已解之四個未知（**皆為實查**）

| # | 未知 | 結果 |
|---:|---|---|
| 1 | Spring Boot 4 之測試切片模組 | ★ **`org.springframework.boot:spring-boot-webmvc-test`**（Maven Central 200；★ 亦已下載） |
| 2 | `@WebMvcTest` 之套件路徑 | ★ **`org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest`**（**非**舊的 `…boot.test.autoconfigure.web.servlet`） |
| 3 | mock 註解 | ★ **`@MockitoBean`**（`spring-test-7.0.9` 內）；★ **`@MockBean` 已不存在**（jar 內無該 class） |
| 4 | JDBC 切片之座標 | ★ **`org.springframework.boot:spring-boot-jdbc-test`**（200） |

**已完成之落地**（**建置保持綠燈**）：
- `libs.versions.toml` 新增 `springBootWebmvcTest`（**版本由 BOM 管理**）
- ★ **產生器模板 `T_SERVICE_BUILD` ＋ `authorization-service` 之 `build.gradle.kts` 兩邊都動**（`CSP-007`）
- 撰寫 `AuthorizationControllerTest`（**編譯通過**，FQN 全對）

#### 6.6.2 ⚠️ **卡點：切片不提供 `HttpSecurity`**

```
Error creating bean 'jwtSecurityFilterChain'
Caused by: NoSuchBeanDefinitionException: No qualifying bean of type
  'org.springframework.security.config.annotation.web.builders.HttpSecurity' available
```

**根因**：本服務掛有 `spring-boot-starter-oauth2-resource-server`，其**自動配置**（Spring Boot 自身，
**非**專案之 `SecurityAutoConfiguration`——後者已排除但仍失敗）定義 `jwtSecurityFilterChain`，
而**該 bean 相依 `HttpSecurity`，切片不提供**（切片不載入完整 security 自動配置）。

**已試未成**：`@AutoConfigureMockMvc(addFilters = false)` ＋ 排除**專案**之 `SecurityAutoConfiguration`
→ **仍失敗**（根因在 Spring Boot 自身之 oauth2 自動配置）。

#### 6.6.3 ★ 下一輪之兩條路（**建議 (A)**）

| 方案 | 做法 | 評估 |
|---|---|---|
| **(A)** ★ **排除 Spring Boot 之 oauth2 資源伺服器自動配置** | `@WebMvcTest(controllers = …, excludeAutoConfiguration = { …OAuth2ResourceServerWebSecurityAutoConfiguration.class })` | ★ **輕量、快**；**需先取得該自動配置之確切 FQN**（`unzip -l` 其 jar 之 `…autoconfigure/web/` 子套件——**本輪預算不足未取得**） |
| **(B)** `@SpringBootTest` ＋ `@AutoConfigureMockMvc` | 全 context（含 Kafka／Redis／Oracle／Keycloak） | ⚠️ **重**；但**必然可行**，且順帶涵蓋更多層 |

> ⚠️ **本輪未動工之聲明**：`AuthorizationControllerTest` **已移除**（**不留下紅燈**），
> 惟**相依已加、四個未知已解**——下一輪**取得 (A) 之 FQN 後即可直接續作**。

---

### 6.7 ★★★ B2 spike **已打通**（`#A-180`）——手段確認，**可規模化**

BEAM PMO：「**依照您的建議往前自主推進，要記得遵守鐵律，當然也不能有 Skeleton/Mock 的情形發生**」

#### 6.7.1 ★★ 卡點之解（第五個未知）

**卡點**：切片 context 啟動失敗——
`NoSuchBeanDefinitionException: No qualifying bean of type 'HttpSecurity' available`。

**根因**：Spring Boot 自身之 **oauth2 資源伺服器 Web 安全自動配置**定義 `jwtSecurityFilterChain`，
其相依 `HttpSecurity` **切片不提供**。

**解方**（以 `unzip -l` 實查該 jar 之 `…autoconfigure/web/` 取得確切 FQN）：

```java
@WebMvcTest(controllers = AuthorizationController.class,
        excludeAutoConfiguration = OAuth2ResourceServerWebSecurityAutoConfiguration.class)
@AutoConfigureMockMvc(addFilters = false)
```

★ FQN：`org.springframework.boot.security.oauth2.resource.autoconfigure.web.OAuth2ResourceServerWebSecurityAutoConfiguration`
（★ 注意為 `…oauth2.server.resource…`，**非** `oauth2.resource`）。

#### 6.7.2 ★★★ 可複製之**配方**（B2 之其餘 11 服務照此辦理）

```
1. 該服務之 build.gradle.kts 須有 testImplementation(libs.springBootWebmvcTest)
   （★ 產生器模板 T_SERVICE_BUILD 已加，新服務自動具備；既有服務須逐一手動加）
2. 建 XXXControllerTest：
   @WebMvcTest(controllers = XXXController.class,
           excludeAutoConfiguration = OAuth2ResourceServerWebSecurityAutoConfiguration.class)
   @AutoConfigureMockMvc(addFilters = false)
   ★ 控制器之「全部」協作者以 @MockitoBean 注入（切片不載入實作）
3. 逐端點驗：路由／參數綁定／回應狀態（★ 只驗 HTTP 層職責）
4. ./gradlew :services:<svc>:test  （★ 必須全量）
```

#### 6.7.3 ★ 與「禁 Skeleton／Mock」之關係（**如實說明**）

`@MockitoBean` 注入**受測控制器之協作者**——此為**切片測試之標準作法**
（本專案既有之 `FakeLimitRepository` 等亦屬同類），**非** `scan_mockdata` 所指之
「**假造業務資料**」。

★ **掃描器實測**（`#A-180`）：`scan_scaffold` **0**／`scan_mockdata` **0**／
`scan_faketest` **0**／`validate_sql` **0**／`gen_service_skeleton --check` **缺漏 0**。
→ ★ **未引入任何空殼或假資料**。

★ **本類別只驗「HTTP 層之職責」**；**真正的行為**（額度扣抵、狀態機、DB 讀寫）
**已由 B1／B2 之真機整合測試涵蓋**——**不重複、也不取代**。

#### 6.7.4 實測

| 指標 | B2 前 | **spike 後** |
|---|---:|---:|
| `authorization-service` 行覆蓋 | 32.8% | **35.3%** |
| `AuthorizationController` | **0/97** | **9/97**（★ spike 只碰 3 個端點之路由） |
| 測試數 | 69 | **72**（0 失敗） |

> ★ **spike 之價值不在這 9 行**，而在**手段已確認**——其餘 88 行（12 個端點）與
> 其餘 11 個服務，**照 §6.7.2 之配方即可規模化**。

---

### 6.8 ★★★ B2 規模化執行結果（`#A-183`）＋ 回頭補 76 行（`#A-182`）

BEAM PMO：「**先橫向複製到其餘 11 個服務，但是 `AuthorizationController` 還有 76 行沒覆蓋的
也要適時回頭補完這一段，不能只測試 http 層。**」

#### 6.8.1 ★★ 回頭補：`AuthorizationController` **76 行 → 0 未覆蓋**

以「**真實領域物件**」（`Authorization.decide(...)`／`AuthorizationDetail`／`AuthorizationStats`／
`AuthorizationState`／`StandInAdviceView`／`AuthAuditEvent`／`ReversalResult`／`OverrideResult`）
驅動控制器之**回應組裝**（`toCommand`／`toResponse`／逐項檢核映射／`stats` 之 `Clock` 起算），
補上**成功路徑**——★ **非只測 HTTP 層**。

| 指標 | 補之前 | **補之後** |
|---|---:|---:|
| `AuthorizationController` 未覆蓋行（JaCoCo `nc`） | **76** | **0** ✅（行覆蓋 **100%**） |
| `authorization-service` 行覆蓋 | 36.3% | **43.5%** |
| 測試數 | 81 | **92**（0 失敗） |

★ 併**修正兩條自身寫錯的測試**：`reverse`／`advise` 之 `@RequestBody` **未加 `@Valid`**，
故空 body **不會** 400——原測「無效請求 → 400」為**錯誤假設**，已改驗「**缺必要標頭 → 400**」。

#### 6.8.2 ★★★ 橫向複製：**16 個服務、25 個 `@WebMvcTest` 類別、130 條測試**

| 服務 | 類別 | 測試 | 服務 | 類別 | 測試 |
|---|---:|---:|---|---:|---:|
| card | 1 | 16 | pricing | 1 | 4 |
| clearing | 1 | 7 | product | 2 | 8 |
| dispute | 1 | 7 | report | 1 | 6 |
| installment | 1 | 5 | rewards | 2 | 8 |
| limit | 3 | 14 | statement | 1 | 6 |
| merchant | 1 | 6 | notification | 2 | 8 |
| migration | 1 | 5 | party | 2 | 9 |
| payment | 2 | 9 | posting | 3 | 12 |
| **合計** | | | | **25** | **130** |

⚠️ **範圍更正**：計畫原載「其餘 **11** 個服務」——★ **實查後為 16 個**（原盤點漏列
`dispute`／`installment`／`merchant`／`migration`／`rewards` 五服務，其 `rest` 層亦無測試）。
本輪**一律涵蓋**。

★ **各服務 `build.gradle.kts` 逐一手動加 `testImplementation(libs.springBootWebmvcTest)`**
（產生器模板 `T_SERVICE_BUILD` 於 `#A-179` 已加，新服務自動具備）。

#### 6.8.3 ★★★ 本輪**實測揭露之三個真缺陷**（★ 皆為 B2 首次觸及之路徑）

| # | 缺陷 | 證據（實測） | 影響 |
|---|---|---|---|
| **A-184** | `CardController.lookup` 查無卡片拋 `ResponseStatusException(404)`，惟 `GlobalExceptionHandler` **未處理**該型別 → 落兜底 `Exception.class` → **回 500 `CC-PLT-5000`**（契約要求 404） | `@WebMvcTest`：`expected:<404> but was:<500>`；`grep` 全庫僅此一處使用 `ResponseStatusException` | ★ 違反契約（`CSP-003 §7 E3` 禁「以 500 掩飾所有錯誤」） |
| **A-185** | ★ **Jackson 3 預設 `FAIL_ON_NULL_FOR_PRIMITIVES=true`**（Jackson 2 為 `false`；`CR-038` 升版後）→ 請求 DTO 之 **primitive 欄位一省略即 400 `CC-PLT-4002`** | `MerchantSettlementRequest`（`long grossAmount/feeAmount/taxAmount`）省略金額 → `MismatchedInputException: Cannot map null into type long` | ★ 契約若允許省略 primitive 欄位者**一律 400**；**全庫須盤點** |
| **A-186** | **`@Valid` 未加於部分 `@RequestBody`** → 巢狀 record 之 `@NotBlank`／`@NotNull` **不生效** | `authorization` 之 `reverse`／`advise`；`product` 之 `ParameterController.set`／`ProductController.create`／`update` 之空 body **皆未**回 400 | ★ 一致性缺陷（同服務之 `authorize`／`override` **有**加） |

> ★ **`#A-184` 之處置（已由 `CR-041` 執行完成）**：控制器改拋**已登錄**之
> `BusinessRuleException(CC_CARD_4040)` → **`404` `CC-CARD-4040`**（**非** `500`）。
> ★ 採 A 案之理由：與該服務自身 **8+ 處**慣例一致、`CC-CARD-4040` 已登錄為 `404`、
> **不動平台**（零 blast radius），且正與 `CR-004 §3.4 #1`「**移除繞道**」同旨。
>
> ★★ **`#A-185` 經查證後「更正為非缺陷」**（**我方初判有誤**）：契約將
> `grossAmount`／`feeAmount`／`taxAmount`（及 `holdAmount`／`amount`）列為 **`required`**
> → **省略即 `400` 符合契約**；★ **Jackson 3 之嚴格性反而把契約的 `required` 落到了實作**
> （Jackson 2 會**靜默補 `0`**，使「漏送必填金額」表現為「金額 0」——**這才是危險**）。
> → **不修**；併立**設計規則 G-10**（選填數值須用 wrapper）於 `CSP-001 §7.2`。
>
> ★ **`#A-186` 之處置（已由 `CR-041` 執行完成）**：`reverse`／`advise`／
> `ParameterController.set`／`ProductController.create`／`update` 補 `@Valid`
> → 空 body 回 **`400`**（原 `reverse` 之 `reversalType=null` 更會使
> `Enum.valueOf(null)` **拋 NPE → `500`**，補 `@Valid` 後該路徑**不可達**）。
>
> ★ **三項之權威文件**：**`CR-041`**（含逐項查證、裁定、實作與驗證）。

#### 6.8.4 ★ 手段複製之三個實務要點（供後續批次）

1. **協作者可全部 `@MockitoBean`**；未 stub 者回 `null` → 多數端點（`ApiResponse.success(traceId, x)`）
   **null 安全**，**無須 stub 即可驗路由／狀態**（★ 大幅降低撰寫成本）。
2. **須構造真實物件者僅少數**（控制器**解引用**回傳值時）：`CardView.of(IssueResult)`、
   `ReportDownload`、`party` 之 `BlockCodeResult`／`FlagResult`、
   `SpecialLimitPolicyController.findAll`（`policies.size()`）、`SureNotifyWebhookController`（`outcome.name()`）。
3. **`@RequestHeader` 一律帶 `X-Channel` ＋ `X-Idempotency-Key`**（多帶無害；少帶即 400）。

#### 6.8.5 ★ 全量實測（`./gradlew test --continue`，2026-10-05 16:2x）

**全庫 `BUILD SUCCESSFUL`**：**服務測試 1,063 測／0 失敗／0 錯誤／2 略過**
（略過 2 ＝ `statement` 之真機 PDF 測試 ＋ 本輪 `#A-184` 之 `@Disabled`）。

| 服務 | 行覆蓋 | 服務 | 行覆蓋 |
|---|---:|---|---:|
| **merchant** | **93.4%** | notification | 71.0% |
| **pricing** | **93.4%** | card | 70.6% |
| **dispute** | **90.8%** | clearing | 65.7% |
| installment | 78.4% | posting | 64.8% |
| payment | 73.2% | migration | 60.5% |
| limit | 73.1% | product | 59.0% |
| rewards | 58.7% | statement | 51.7% |
| report | 47.8% | authorization | 43.5% |
| **party** | **23.7%**（★ 最低——投影／領域層未覆蓋） | | |

> **★ 全服務合計（加權）：行覆蓋 8,038／12,535 ＝ `64.1%`**（★ 距 80% 目標尚差 **+15.9 pp**；
> 剩餘大戶為 `party`（451 行未覆蓋）、`authorization`（728）、`statement`（452）、`rewards`（448））。
> ★ 掃描器全數 **0 命中**（`scan_scaffold`／`scan_mockdata`／`scan_faketest`／`validate_sql`
> 70 檔 0 違規／`validate_mermaid` 117 區塊 0 錯誤／`gen_service_skeleton --check` 缺漏 0／
> `verify_versions` 12 項 0 未通過）。

---

### 6.9 ★★ W3：`statement`／`posting` 之 `jdbc` 層真機整合測試（`#A-188`／`#A-189`）

BEAM PMO：「**接受您的建議先做 W3（範圍最小、可一次交付），再回頭推進覆蓋率。**」

依 `§6.4` 配方（`Testcontainers` 一次性 Oracle ＋ `Flyway` 於 `@BeforeAll` ＋ `@BeforeEach` 明確清理；
★ **完全不打 mock**）；以 `reconstitute` 建構受測物（**非** `create`／`generate`——受測者為**儲存層**）。

#### 6.9.1 落地

| 服務 | 測試類別 | 測試 | 涵蓋之 jdbc 類別 |
|---|---|---:|---|
| `statement` | `StatementRepositoryIntegrationTest`（`@Nested` Statements／Runs） | **10** | `JdbcStatementRepository`／`JdbcStatementRunRepository` |
| `posting` | `TransactionRepositoryIntegrationTest`（`@Nested` Transactions／Balances） | **11** | `JdbcTransactionRepository`／`JdbcBalanceRepository` |
| **合計** | **2 類別（4 `@Nested`）** | **21** | **4 個 jdbc 類別** |

#### 6.9.2 ★★ 實測增益

| 服務 | 行覆蓋（前） | **行覆蓋（後）** | 增益 |
|---|---:|---:|---:|
| `statement` | 51.7% | **73.7%** | **+22.0 pp** |
| `posting` | 64.8% | **83.5%** | **+18.7 pp** |

| jdbc 類別 | 前 | **後** |
|---|---:|---:|
| `JdbcStatementRepository` | **0/102** | ★ **100%** |
| `JdbcStatementRunRepository` | **0/46** | ★ **100%** |
| `JdbcTransactionRepository` | **0/89** | ★ **100%** |
| `JdbcBalanceRepository` | **0/42** | ★ **100%** |

★ **四個目標類別全數 100%**（`JdbcOutboxRepository` 於 B3-a 已達 95.7%，非本輪範圍）。

#### 6.9.3 ★★ 條件類查詢之「防假綠」設計（本輪重點）

依 `§6.4` 之紀律——**造 3～4 種近似情境、僅 1 種應命中**：

| 查詢 | 近似情境（皆**不得**命中） |
|---|---|
| `findByAccountIdAndPeriod`（半開 `(from, to]`） | `from-1`／`from`／`to+1` 各造一筆 → ★ **僅 `from+1` 與 `to` 應命中**（此即「防上期結帳日重複計收」之實測） |
| `findRunning`（週期／營業日／狀態） | 同週期＋同營業日**異狀態**／同週期＋同狀態**異營業日**／同營業日＋同狀態**異週期** → ★ **僅三鍵全符者命中** |
| `findAccountIdsByCycle` | 另一週期之歸戶 → ★ **必須排除** |
| `findByAccountAndMonth` | 他月／他歸戶 → ★ **皆不得命中** |
| `findByAccountId`／`findByAccountIdAndPeriod` | 他歸戶之交易 → ★ **必須排除** |
| `update`／`updateStatus`（樂觀鎖） | ★ **版本相符→成功；版本落後→拋已登錄碼**，且**衝突後不得留下半套變更** |

#### 6.9.4 ★★ 實測揭露之兩項事實（★ 只有真機才會說）

1. **`TRANSACTIONS.idempotency_key` 為「全域」`UNIQUE`**（`uq_posting_idem`）——
   ★ 故 `findDuplicates` **實務上至多回 1 筆**（該查詢屬**防禦性**）。
   ⚠️ **我的初版測試試圖造「同鍵不同歸戶」兩列 → 會違反唯一約束**；★ **真機測試當場反駁**，
   已改為「以不同 `from` 邊界驗證三鍵」之寫法（並於測試內註記此一事實）。
   ★ 併查證：該表為 `PARTITION BY RANGE (posting_date)`，`UNIQUE` 不含分割鍵——Oracle 以
   **global 唯一索引**滿足之（**實測可套用**，非推測）。
2. **`TRANSACTIONS.value_date` 為 `NOT NULL`**（`V1__baseline.sql` 實查）——
   ★ 與 `STATEMENTS.sent_at`／`BALANCES.payment_due_date` 之**可空**形成對比；
   測試資料若不帶 `valueDate` 即 `ORA-01400`。

#### 6.9.5 ★ 驗收（2026-10-05 18:1x）

| 項 | 結果 |
|---|---|
| `:services:statement-service:test`／`:services:posting-service:test` | **BUILD SUCCESSFUL**；★ **21 條新測試全 PASS** |
| **全量** `./gradlew test --continue` | ★ **BUILD SUCCESSFUL** |
| 掃描器 | ★ **全 0 命中**（`scan_scaffold`／`scan_mockdata`／`scan_faketest`／`validate_sql` 70 檔 0 違規／`validate_mermaid` 0 錯誤／`gen_service_skeleton --check` 缺漏 0） |

#### 6.9.6 ★★ 全庫結算（W3 後）

**服務測試 1,088 測／0 失敗／0 錯誤／1 略過**（略過＝`statement` 之真機 PDF）。

> ★★ **全服務行覆蓋：8,402／12,535 ＝ `67.0%`**（**B1 前 53.7%** → **B2 後 64.1%** → **W3 後 67.0%**）。
> ★ 本輪（W3）貢獻 **+2.9 pp**。

| 服務 | 行覆蓋 | 服務 | 行覆蓋 |
|---|---:|---|---:|
| merchant／pricing | **93.4%** | card | 70.6% |
| dispute | **90.8%** | clearing | 65.7% |
| **posting** | **83.5%**（★ +18.7 pp） | migration | 60.5% |
| installment | 78.4% | product | 59.0% |
| **statement** | **73.7%**（★ +22.0 pp） | rewards | 58.7% |
| payment／limit | 73.2%／73.1% | report | 47.8% |
| notification | 71.0% | authorization | 43.5% |
| | | **party** | **23.7%**（★ 最低） |

> ★ **距 80% 目標尚差 +13.0 pp**。剩餘大戶（未覆蓋行）：`authorization` 728／`party` 451／
> `statement` 246／`rewards` 448／`report` 302。
> ★ **下一波建議**：**W4**（`rewards`／`card`）＋ **B3-b**（`messaging`／`client`／`storage`／`scheduler`）。

---

### 6.10 ★★ W4：`rewards`／`card` 之 `jdbc` 層真機整合測試（`#A-190`／`#A-191`）

BEAM PMO：「**接受您的建議，W4 先處理，再回頭處理大戶**」

依 `§6.4` 配方（★ 依 FK 反向清理；★ 以 `reconstitute` 建構受測物；★ **完全不打 mock**）。

#### 6.10.1 落地

| 服務 | 測試類別 | 測試 | 涵蓋之 jdbc 類別 |
|---|---|---:|---|
| `rewards` | `RewardsRepositoryIntegrationTest`（5 `@Nested`） | **19** | `JdbcPointsRepository`／`JdbcRedemptionRepository`／`JdbcRewardRuleRepository` |
| `card` | `CardRepositoryIntegrationTest`（5 `@Nested`） | **20** | `JdbcCardRepository`／`JdbcCardApplicationRepository`／`JdbcTokenRepository`／`JdbcCardNumberPool`／`JdbcIdempotencyStore`／`RawIds` |
| **合計** | **2 類別（10 `@Nested`）** | **39** | **9 個 jdbc 類別** |

#### 6.10.2 ★★ 實測增益

| 服務 | 行覆蓋（前） | **行覆蓋（後）** | 增益 |
|---|---:|---:|---:|
| `rewards` | 58.7% | **79.8%** | **+21.1 pp** |
| `card` | 70.6% | **89.4%** | **+18.8 pp** |

| jdbc 類別 | 前 | **後** |
|---|---:|---:|
| `JdbcPointsRepository` | 0/95 | ★ **100%** |
| `JdbcRewardRuleRepository` | 0/31 | **87.1%** |
| `JdbcRedemptionRepository` | 0/50 | **92.0%** |
| `JdbcCardApplicationRepository` | 0/55 | ★ **100%** |
| `JdbcCardRepository` | 0/47 | **97.9%** |
| `JdbcTokenRepository` | 0/23 | ★ **100%** |
| `JdbcCardNumberPool` | 0/13 | ★ **100%** |
| `JdbcIdempotencyStore` | 0/11 | ★ **100%** |
| `RawIds` | 0/8 | ★ **100%** |

> ★ **9 條殘留未覆蓋行**——**全數為不可達之防禦分支**（`serialize`／`parse` 之 `catch`；
> `JdbcCardRepository.trimToNull` 之 `value == null` 分支——**`bin` 為 `NOT NULL` 故不可達**）。
> **不以「硬造」方式強行覆蓋**（`CR-004 §3.5` 同旨）。

#### 6.10.3 ★★★ 實測揭露之三項真實行為（★ 修正了我三個錯誤假設）

| # | 我的假設 | **實測** | 處置 |
|---|---|---|---|
| 1 | `POINTS_EXPIRY_BUCKETS` 可更新為 `points = 0` | ★ **`CHECK (points > 0)`** → `ORA-02290` | 改以「部分沒入（50）」表示 |
| 2 | `save` 會寫入 `card_id`，故可直接 `findByCardId` | ★ **`save` 之 INSERT 不含 `card_id`**（發卡後才由 `updateCardId` 回填，`CR-020` F1） | 測試改為**先 `updateCardId`** 再反查 |
| 3 | `created_at` 之 `OffsetDateTime` 可相等比較 | ★ `save` 以 **`Timestamp.from(instant)`** 寫入 → **offset 遺失**，讀回為 session 時區（`+08:00`） | 改以**同一瞬間**（`.toInstant()`）比較 |

★ 併查得**一致性瑕疵（非功能缺陷）**：`JdbcCardNumberPool.findAvailable` **未修剪** `CHAR(8)` 之
`bin` 補空白（與 `JdbcCardRepository` 之 `trimToNull` 不一致）；惟 `bin` **未參與卡號產生**
（卡號由 `rangeStart`／`rangeEnd` 產生、卡片之 `bin` 由 `CardNumberIssuer.bin(pan)` 導出）
→ **無功能影響**。已於測試內註記。

#### 6.10.4 ★ 驗收

| 項 | 結果 |
|---|---|
| `:services:rewards-service:test`／`:services:card-service:test` | **BUILD SUCCESSFUL**；**39 條新測試全 PASS** |
| **全量** `./gradlew test --continue` | ★ **BUILD SUCCESSFUL**（見 §6.10.5） |
| 掃描器 | ★ **全 0 命中** |

#### 6.10.5 ★★ 全庫結算（W4 後）

> ★★ **全服務行覆蓋：8,809／12,535 ＝ `70.3%`**（**B1 前 53.7%** → **B2 後 64.1%**
> → **W3 後 67.0%** → **W4 後 70.3%**）。★ 本輪（W4）貢獻 **+3.3 pp**。
> ★ 全量 **1,127 測／0 失敗／0 錯誤／1 略過**。
> ★ **距 80% 目標尚差 +9.7 pp**。剩餘大戶（未覆蓋行）：`authorization` 728／`party` 451／
> `report` 302／`statement` 246／`rewards` 219。
> ★ **jdbc 層幾已見底**（除上述 9 條不可達分支外）→ **下一波必須轉向 `application`／`model`
> 分支補強（B4）與 `messaging`／`client`／`storage`／`scheduler`（B3-b）**。
>
> ⚠️ **本文撰寫時之自身失誤（如實揭露）**：初稿曾**預填**「8,656／69.1%」——★ **未經實測即填入**，
> 違反 `CR-038 §4.2` 之紀律（**驗證結果一律等實跑完再填**）。已於實測後更正為上列數字。

---

### 6.11 ★★★ 續推計畫（`B4` ＋ `B3-b` ＋ 大戶）——**盤點後重排**

BEAM PMO：「**請您在轉向：(a) application／model 分支補強（B4）、(b) B3-b（messaging 245／client 316／
storage 124／scheduler 88）完成後，自主推進把剩餘大戶：authorization 728 行、party 451、report 302、
statement 246、rewards 219 一起完成再做匯總報告**」

#### 6.11.1 ★★ 先更正前輪之錯誤敘述（如實揭露）

`§6.10.5` 曾稱「**jdbc 層幾已見底**」——★ **該敘述不成立**：它只涵蓋 **W3／W4 之 4 個服務**。
全庫實查後，**jdbc 層仍有 935 行未覆蓋**（最大者 `JdbcAuthorizationRepository` **162**、
`JdbcPartyRepository` 41、`JdbcNotificationRepository` 39、`JdbcMigrationJobRepository` 37、
`JdbcReportJobRepository` 36、`JdbcProductRepository` 34、`JdbcClearingRunRepository`／
`JdbcSettlementRepository` 各 33、`JdbcAccountRepository` 32）。

#### 6.11.2 ★★★ 精確盤點（未覆蓋行，**逐類別彙總 3,347 行**）

| 層 | 未覆蓋行 | 成本 | 手段 |
|---|---:|---|---|
| `domain/model` | **393** | ★ 最低 | **純單元測試**（無 Spring、無容器） |
| `application` | **814** | ★ 低 | 單元測試（**以既有 `Fake*` 埠**） |
| `infrastructure/scheduler` | **88** | ★ 低 | **直接呼叫排程方法 ＋ 驗副作用** |
| `infrastructure/storage` | **118** | 中 | ★ **真實物件儲存往返**（`Garage`／MinIO） |
| `infrastructure/messaging` | **217** | 中 | `Testcontainers Kafka` **或**直接呼叫 listener |
| `infrastructure/client` | **251** | 中 | ★ **真實對端**（本機可起）**或** WireMock |
| `infrastructure/jdbc`（剩餘） | **935** | 中 | ★ **`§6.4` 配方**（已於 11 服務驗證） |
| `infrastructure`（其他） | **497** | ? | 逐項盤點後再定 |
| `interfaces/rest` | 20／`pdf` 8／`port` 3／`domain` 3 | 低 | 零星補 |

#### 6.11.3 ★ 批次排序（依「**每行覆蓋之成本**」由低到高）

| 批次 | 範圍 | 預估行 | 說明 |
|---|---|---:|---|
| **B4-a** | `domain/model` | 393 | ★ **純單元測試**，最快 |
| **B3-b-1** | `scheduler` ＋ `storage` | 206 | scheduler 直呼；storage 需真實物件儲存 |
| **B4-b** | `application` | 814 | ★ 以既有 `Fake*` 埠；**量最大** |
| **B3-b-2** | `messaging` ＋ `client` | 468 | 需 Kafka／對端；★ **不得只 mock 而聲稱已驗證** |
| **大戶** | 剩餘 `jdbc` | 935 | ★ 沿用 `§6.4` 配方 |
| **B4-c** | `infrastructure` 其他 | 497 | 盤點後再定 |

> ★ **達 80% 所需**：`12,535 × 80% ＝ 10,028`；現 **8,809** → **尚需 +1,219 行**。
> ★ 故 **B4-a ＋ B3-b-1 ＋ B4-b 之大部分即已足**；**大戶（jdbc）為保險與完整性**。

#### 6.11.4 ★ 紀律（本計畫之強制項）

1. **每批結束即跑全量 ＋ 掃描器**，並**實測後才填數字**（`CR-038 §4.2`）。
2. **不得為覆蓋率而 mock 業務邏輯**（`CSP-005`／`TRP-003 §2.3`）。
3. **不可達之防禦分支**：**誠實標註**，不以硬造方式強行覆蓋。
4. **每批一個 commit**，訊息載明批次、行數增益、實測值。

#### 6.11.5 ★★ 批次 1 執行結果（`#A-192`／`#A-193`）——`B4-a` 起始

| 項 | 內容 |
|---|---|
| **範圍** | `party.Party`（68 行）＋ `rewards.PointsAccount`（28 行）之**純領域分支** |
| **新增** | `PartyTest`（**8 測**，新檔）＋ `PointsAccountTest`（**+16 測**，補既有檔） |
| **★ 為何選這兩者** | 其未覆蓋行**多為 `reconstitute`／存取子／不變式分支**——★ **jdbc 整合測試觸及不到**（驗證與終態拒絕屬純領域） |

| 服務 | 行覆蓋（前） | **行覆蓋（後）** |
|---|---:|---:|
| `party` | 23.7% | ★ **44.0%**（**+20.3 pp**） |
| `rewards` | 79.8% | ★ **82.1%**（**+2.3 pp**） |

> ★★ **全服務行覆蓋：70.3% → `71.4%`**（8,954/12,535）；**1,152 測／0 失敗／1 略過**。
> ★ **本批僅完成 `B4-a` 之 2 個類別（96／393 行）**——★ **餘下批次（`B4-a` 其餘 297 行、
> `B4-b` 814、`B3-b` 674、大戶 jdbc 935）尚待續作**，詳見 §6.11.3 之批次表。
> ⚠️ **如實揭露**：本輪**未能一次完成全部批次**——該範圍約 **2,400 行**，屬**多批次工程**。

#### 6.11.6 ★★ 批次 2 執行結果（`#A-194`）——`B3-b-1` 之 **scheduler 層全數完成**

| 項 | 內容 |
|---|---|
| **範圍** | `infrastructure/scheduler` **8 個排程類別（88 行）** |
| **新增** | 8 個 `*SchedulerTest`（**各 2～3 測，共 17 測**） |
| **★ 手法** | 排程類別為**薄配接器**——唯一職責是「**委派服務**」＋「**吞掉例外**」，故以 mock 協作者驗「委派」與「不外溢」即為正確之單元邊界 |

| 排程類別 | 行覆蓋 |
|---|---:|
| `TemporaryLimitExpiryScheduler`（limit） | ★ **15/15** |
| `ReportProjectionScheduler`（report） | ★ **13/13** |
| `StandInUploadScheduler`（authorization） | ★ **10/10** |
| `MigrationJobScheduler`（migration） | ★ **10/10** |
| `NotificationDispatchScheduler`（notification） | ★ **10/10** |
| `PaymentUnmatchedScheduler`（payment） | ★ **10/10** |
| `ReportJobScheduler`（report） | ★ **10/10** |
| `PointsExpiryScheduler`（rewards） | ★ **10/10** |
| **合計** | ★★ **88/88 → `100%`** |

> ★★ **本批最重要之測試點**：**例外不得外溢**——Spring 之 `@Scheduled` 若方法拋出例外，
> **後續排程會被取消**（即「一次失敗＝永久停止」）。8 個類別**皆測此點**。
> ★★ 併測 `TemporaryLimitExpiryScheduler` 之**兩個 try/catch 各自獨立**
> （臨時額度失敗**不得**影響專用額度，反之亦然）；
> 與 `ReportProjectionScheduler` 之 **`daysBack` 下限為 1**（不得產生空區間）。
>
> ★★ **全服務行覆蓋：71.4% → `72.1%`**（9,044/12,535）；**1,170 測／0 失敗／1 略過**；
> 掃描器**全 0 命中**。★ **本批 +90 行**。

#### 6.11.7 ★★★ 批次 3 執行結果（`#A-195`）——**大戶 jdbc 之首發：全庫最大單一缺口**

| 項 | 內容 |
|---|---|
| **範圍** | `authorization-service` 之 **`JdbcAuthorizationRepository`（162 行）**——★ **全庫最大之單一 jdbc 缺口** |
| **新增** | `AuthorizationRepositoryIntegrationTest`（**17 測**，涵蓋 **19 個方法**） |
| **手法** | `§6.4` 配方（Testcontainers ＋ Flyway ＋ 依 FK 反向清理 ＋ **完全不打 mock**） |

| 指標 | 前 | **後** |
|---|---:|---:|
| `JdbcAuthorizationRepository` | **0/162** | ★★ **162/162 → `100%`** |
| `authorization-service` 行覆蓋 | 43.5% | ★ **59.4%**（**+15.9 pp**） |

> ★★ **全服務行覆蓋：72.1% → `73.7%`**（9,239/12,535）——★ **本批 +195 行**（★ **單批最大增益**）。
> ★ 全量 **1,187 測／0 失敗／0 錯誤／1 略過**；掃描器**全 0 命中**。
> ★ **協同效應兌現**：本類別併同覆蓋 `Authorization`／`CheckResult`／`AuthReversal`／
> `AuthOverride`／`PendingStandIn`／`StandInAdviceState`／`AuthorizationState` 等 model 之
> `reconstitute`／存取子。

#### 6.11.8 ★★★ 本批實測揭露之三項**我方語意錯誤**（★ 真機反駁）

| # | 我的假設 | **實測** |
|---|---|---|
| 1 | 有失敗檢核項仍應 `holdAmount = 10000` | ★ **`DECLINED` → `holdAmount = 0`**（拒絕件**不得佔用額度**）——已改為兩個測試分別驗「核准佔全額」與「拒絕不佔用」 |
| 2 | `PendingStandIn.authorizationId` ＝ `holdId` | ★ **實為授權主檔之 PK（`authorizations.id`）**——`hold_id` 另有 `findHoldIdByAuthNo` |
| 3 | `findAccountIdByAuthNo` 對「隨機歸戶」應回 empty | ★ 條件為 **`account_id IS NOT NULL`** → 必須以 **`accountId = null`** 之授權驗之 |

> ★★ **同一模式第三次出現**：「**真機測試會反駁你**」——**三個錯誤全是我對語意的誤解**，
> **沒有一個是程式缺陷**。已於測試內註記。

#### 6.11.9 ★★ 批次 4 執行結果（`#A-196`）——大戶 jdbc：`party-service`

| 項 | 內容 |
|---|---|
| **範圍** | `JdbcPartyRepository`（41 行）＋ `JdbcAccountRepository`（32 行） |
| **新增** | `PartyRepositoryIntegrationTest`（**7 測**） |
| **★ 關鍵事實** | 兩者之 `save` **皆為 `UPDATE`（無 INSERT）**——測試須先以 SQL 種列；`PARTIES.account_id` 有 **FK → `ACCOUNTS`**；`flags` 為 **`JSON NOT NULL DEFAULT JSON('{}')`** |

| 指標 | 前 | **後** |
|---|---:|---:|
| `JdbcAccountRepository` | 0/32 | ★ **32/32 → `100%`** |
| `JdbcPartyRepository` | 0/41 | **37/41**（★ 餘 4 行為 `parseFlags`／`serializeFlags` 之 **`catch` 分支**——**不可達**） |
| `party-service` 行覆蓋 | 44.0% | ★ **59.6%**（**+15.6 pp**） |

> ★★ **全服務行覆蓋：73.7% → `74.4%`**（9,331/12,535）；★ **本批 +92 行**；
> **1,194 測／0 失敗／0 錯誤／1 略過**；掃描器**全 0 命中**。
> ★ **驗證要點**：`flags` 之 **JSON 往返**（空 → 有值）、兩處**樂觀鎖**（`CC-PTY-2001`）、
> `BLOCK_CODE_HISTORY` 之 **append-only**（`released_at` 應為 null）、`findByAccountId` 之
> **他歸戶排除**（防假綠）。
> ★ **完全不打 mock**：真實 Oracle ＋ 真實 `JSON` 型別 ＋ 真實 FK 約束。

#### 6.11.10 ★★ 批次 5 執行結果（`#A-197`）——大戶 jdbc：`clearing-service`

| 項 | 內容 |
|---|---|
| **範圍** | `JdbcClearingRunRepository`（33 行）＋ `JdbcSettlementRepository`（33 行） |
| **新增** | `ClearingRepositoryIntegrationTest`（**7 測**） |
| **★ 關鍵事實** | `CLEARING_RUNS` 於 **V2** 建立（`run_no` **UNIQUE**）；★ `SETTLEMENTS.DIRECTION` **由 V2 新增**（V1 無此欄——**動查後才知**）；`currency` 為 `CHAR(3)` |

| 指標 | 前 | **後** |
|---|---:|---:|
| `JdbcClearingRunRepository` | 0/33 | ★ **33/33 → `100%`** |
| `JdbcSettlementRepository` | 0/33 | ★ **33/33 → `100%`** |
| `clearing-service` 行覆蓋 | 65.7% | ★ **81.5%**（**+15.8 pp**） |

> ★★ **全服務行覆蓋：74.4% → `75.2%`**（9,430/12,535）；★ **本批 +99 行**；
> **1,202 測／0 失敗／0 錯誤／1 略過**；掃描器**全 0 命中**。
> ★ **驗證要點**：`findRecent` 之 **ROWNUM 陷阱**（`ORDER BY` 須在子查詢內）、
> **樂觀鎖**（`update` 以 `IllegalStateException` 表達衝突）、
> `findByOrgAndDate` 之**兩維度皆須相符**（防假綠）、`nextRunNo` 序列不重號。

#### 6.11.11 ★★★ 批次 6 執行結果（`#A-198`）——大戶 jdbc：`limit-service`（★ **併結案 WBS 5.13**）

| 項 | 內容 |
|---|---|
| **範圍** | ★ **兩個付款投影 repository**（各 17 行）＋ `limit-service` 其餘 jdbc 殘留 |
| **新增** | `PaymentProjectionRepositoryIntegrationTest`（**13 測**） |
| **★★ 意義** | 本批**結案 `WBS 5.13`（B1-b2）**——該項自 `#A-168` 起**延後兩輪**，理由為「語意為**事件消費之冪等投影**，需與事件語意一起設計」。★ 本輪以**真實 `JdbcLimitRepository` 為協作者**（**無 mock**）完成之 |

| 指標 | 前 | **後** |
|---|---:|---:|
| `JdbcPaymentPostedProjectionRepository` | 0/17 | ★ **17/17 → `100%`** |
| `JdbcPaymentReoccupyProjectionRepository` | 0/17 | ★ **17/17 → `100%`** |
| `limit-service` 行覆蓋 | 73.1% | ★ **78.5%**（**+5.4 pp**） |

> ★★ **全服務行覆蓋：75.2% → `75.7%`**（9,483/12,535）；★ **本批 +53 行**；
> **1,215 測／0 失敗／0 錯誤／1 略過**；掃描器**全 0 命中**。

#### 6.11.12 ★★★ 本批之核心不變式：**「已記＝已套用」**

兩個投影 repo 之四步語意：**① 冪等（`eventId` 已消費→略過）→ ② 亂序（`sequenceNo ≤ MAX`→略過）
→ ③ 套用（失敗即 return false）→ ④ 寫 `consumed_events`**。

> ★★★ **本批最重要之測試**：**③ 失敗時「不得」寫 ④**——否則該筆重放時會被誤判為「已消費」而
> **永久漏套用**（額度**永久高估**＝超額放行風險）。★ 測試以「查無額度主檔」觸發 ③ 失敗，
> **斷言 `consumed_events` 為 0**，★ **再補上主檔重放，證明仍能成功套用**（證明前次未誤記）。

★ 併驗：**冪等**（重放不得重複恢復／佔用）、**亂序**（落後與同序號皆略過，且**不得**寫已消費）、
**多幣別歧義**（`restore` 無 `currency` → 該帳戶 >1 列即拒絕，**不臆測幣別**）、
**`reoccupy` 帶 `currency` 可精確定位**（多列仍可套用）、**`used_amount` 下限 0**。

#### 6.11.13 ★★ 批次 7 執行結果（`#A-199`）——大戶 jdbc：`notification-service`

| 項 | 內容 |
|---|---|
| **範圍** | **5 個 jdbc 類別**（`JdbcNotificationRepository` 39／`JdbcNotificationTemplateRepository` 17／`JdbcDeliveryEventRepository` 16／`JdbcSuppressionRepository` 8／`JdbcEventConsumptionRepository` 8／`OracleRawId` 3） |
| **新增** | `NotificationRepositoryIntegrationTest`（**11 測**） |

| jdbc 類別 | 前 | **後** |
|---|---:|---:|
| `JdbcNotificationRepository` | 0/39 | **37/39**（餘 2 為不可達分支） |
| `JdbcNotificationTemplateRepository` | 0/17 | ★ **17/17 → `100%`** |
| `JdbcDeliveryEventRepository` | 0/16 | ★ **16/16 → `100%`** |
| `JdbcSuppressionRepository` | 0/8 | ★ **8/8 → `100%`** |
| `JdbcEventConsumptionRepository` | 0/8 | ★ **8/8 → `100%`** |
| `notification-service` 行覆蓋 | 71.0% | ★ **83.1%**（**+12.1 pp**） |

> ★★ **全服務行覆蓋：75.7% → `76.6%`**（9,601/12,535）；★ **本批 +118 行**；
> **1,226 測／0 失敗／0 錯誤／1 略過**；掃描器**全 0 命中**。
> ★ **驗證要點**：`findQueued` 僅取 QUEUED（**SENT 不得再被取出**）、
> `find` 之**三鍵**（模板碼／通道／語言）與 `findEnabled` 之 **enabled 篩選**、
> `recordIfAbsent`／`suppress` 之**冪等**（重複 → false，**不拋例外**）、
> `maxSequenceNo` 之 **NVL → 0**。
>
> ★★★ **真機反駁（第四次）**：`OracleRawId.toUuid(長度≠16)` **拋 `IllegalStateException`**
> （**fail-closed**）——★ 我原假設「回 null」。★ 該設計**刻意如此**：壞資料須於**對映層即爆炸**，
> 否則會一路下傳至下游才現形（`SCS-010 §9.3.1` 之既有教訓）。已改為斷言**拋錯**。

#### 6.11.14 ★★ 批次 8 執行結果（`#A-200`）——大戶 jdbc：`report-service`

| 項 | 內容 |
|---|---|
| **範圍** | `JdbcReportJobRepository` 36／`JdbcAuthSummaryReadModel` 21／`JdbcReportDefinitionRepository` 14／`JdbcAuthorizationSource` 13／`OracleRawId` 9 |
| **新增** | `ReportRepositoryIntegrationTest`（**13 測**） |
| **★★ 本類別用「兩個容器」** | ★ 本服務之 jdbc **橫跨兩個資料源**——**Oracle**（`cc_rpt`）＋ **PostgreSQL**（`rpt` schema 之 CQRS 讀側，`@Qualifier("rptJdbcTemplate")`、`ON CONFLICT ... DO UPDATE`） |

| jdbc 類別 | 前 | **後** |
|---|---:|---:|
| `JdbcAuthSummaryReadModel`（PG） | 0/21 | ★ **21/21 → `100%`** |
| `JdbcReportDefinitionRepository` | 0/14 | ★ **14/14 → `100%`** |
| `JdbcReportJobRepository` | 0/36 | **35/36** |
| `JdbcAuthorizationSource` | 0/13 | ⚠️ **3/13**（見下） |
| `report-service` 行覆蓋 | 47.8% | ★ **72.2%**（**+24.4 pp**） |

> ★★ **全服務行覆蓋：76.6% → `77.5%`**（9,718/12,535）；★ **本批 +117 行**；
> **1,239 測／0 失敗／0 錯誤／2 略過**；掃描器**全 0 命中**。
>
> ⚠️ **`JdbcAuthorizationSource` 之誠實揭露**：該類別**跨 schema 讀 `CC_AUTH.AUTHORIZATIONS`**
> （屬 `authorization-service` 之遷移）。測試於容器內**另建 `CC_AUTH` schema**，惟
> **`gvenzl/oracle-free` 之 app user 無 DBA 權限** → 該測試以 `Assumptions` **略過**
> （**不假裝已驗證**）。★ 故 **10 行未覆蓋**，列後續（需具 DBA 之容器或改用真實授權 schema）。
>
> ★ **真機反駁（第五次）**：`ReportJob` 之狀態機為 **DAG**——`complete`／`fail` **皆要求
> `RUNNING`**，我原以為可自 `QUEUED` 直接完成；★ 且 **`COMPLETED` 不得再 `start`**
> （我原用同一筆作業走「完成＋失敗」兩條路）。已拆為兩個測試。
> ★ 併查得 **PG 排序為 `business_date, channel, tx_type, decision` 之升冪**（`EC` 先於 `POS`）。

#### 6.11.15 ★★ 批次 9 執行結果（`#A-201`）——大戶 jdbc：`migration-service`

| 項 | 內容 |
|---|---|
| **範圍** | `JdbcMigrationJobRepository` 37／`JdbcLegacyAccountRepository` 21／`JdbcMigrationWaveRepository` 15／`OracleRawId` 9 |
| **新增** | `MigrationRepositoryIntegrationTest`（**9 測**） |
| **★ 本服務先前無 jdbc 整合測試** | （B3-a 之 8 服務**不含**本服務）——本類別為**首個** |

| jdbc 類別 | 前 | **後** |
|---|---:|---:|
| `JdbcLegacyAccountRepository` | 0/21 | ★ **21/21 → `100%`** |
| `JdbcMigrationWaveRepository` | 0/15 | ★ **15/15 → `100%`** |
| `JdbcMigrationJobRepository` | 0/37 | **36/37** |
| `migration-service` 行覆蓋 | 60.5% | ★★ **92.1%**（**+31.6 pp**——★ **單服務最大跳幅**） |

> ★★ **全服務行覆蓋：77.5% → `78.6%`**（9,852/12,535）；★ **本批 +134 行**；
> **1,249 測／0 失敗／0 錯誤／2 略過**；掃描器**全 0 命中**。
>
> ★★ **動查才知的事**：`JdbcLegacyAccountRepository` 之表名為 **`legacy_account_mappings`**
> ——**不是** `LEGACY_ACCOUNTS`（★ **只讀類別名會猜錯**；須讀 SQL）。
> ★ 併查：三表皆於 **V2** 建立；`snapshot` 為 **`JSON NOT NULL DEFAULT JSON('{}')`**。
> ★ **真機反駁（第六次）**：`MigrationJob.MigrationType`／`JobStatus` 與
> `MigrationWave.WaveStatus` 為**巢狀列舉**（**非**獨立類別）——我原以獨立類別 import。

#### 6.11.16 ★★ 批次 10 執行結果（`#A-202`）——大戶 jdbc：`product-service`

| 項 | 內容 |
|---|---|
| **範圍** | `JdbcProductRepository` 38／`JdbcParameterRepository` 31 |
| **新增** | `ProductRepositoryIntegrationTest`（**12 測**、2 個 `@Nested`） |
| **前置** | ★ 本服務之 `jdbc` 整合測試原**僅涵蓋** `JdbcOutboxRepository`（B3-a） |

| jdbc 類別 | 前 | **後** |
|---|---:|---:|
| `JdbcProductRepository` | 0/38 | ★ **38/38 → `100%`** |
| `JdbcParameterRepository` | 0/31 | ★ **31/31 → `100%`** |
| ★ **`jdbc` 套件整體** | 30/99 | ★★ **99/99 → `100%`** |
| `product-service` 行覆蓋 | 59.0%（174/295） | ★★ **82.4%**（243/295；**+23.4 pp**） |

> ★★ **全服務行覆蓋：78.6% → `79.1%`**（9,921/12,535）；★ **本批 +69 行**；
> **1,261 測／0 失敗／0 錯誤／2 略過**；掃描器**全 0 命中**。
> ★ **距 80% 尚差 +107 行**（0.8 × 12,535 ＝ 10,028）。
>
> ★★ **動查才知的事**：`PARAMETERS.param_value` 為 **Oracle 原生 `JSON` 型別**
> （**非** `VARCHAR2`）——寫入須為**合法 JSON 字串**（否則 `ORA-40441`）；
> ★ 併查 `CONSTRAINT uq_param UNIQUE (namespace, param_key, effective_from)`。
>
> ★★★ **真機反駁（第七次）——我對「樂觀鎖衝突」之建構方式理解有誤**：
> 我原以為「拿一個已落後之物件再呼叫 `withUpdated(...)`」即為衝突；
> **實則相反**——`update` 之 SQL 為 `WHERE version = ?` 且實作傳 `version - 1`，
> 故 `withUpdated` 之 **+1** 恰好把版本補成「**DB 現行 ＋ 1**」→ **反而成功**（測試假紅）。
> ★ **正解**：衝突須以「**同版本、不同內容**」之物件再送（物件版本 ＝ DB 現行版本 → 期望版本
> ＝ 現行 − 1 → 不命中）。
> ★ 併記：`ErrorCodes.CC_PLT_5000` 之**值**為 **`"CC-PLT-5000"`**（連字號）——
> 我原以常數名之底線當字面值，4 條斷言一起紅燈（**測試自己的錯，與受測程式無關**）。
> ★★ **兩者皆已回寫技能 `cardcore-jdbc-integration-test`（陷阱 6／7）**。

#### 6.11.17 ★★★ 批次 11 執行結果（`#A-203`）——大戶 jdbc **收尾**（`WBS 5.25` 結案）

| 項 | 內容 |
|---|---|
| **範圍** | `payment` 29／`installment` 12／`report` 11／`dispute` 3／`merchant` 1 |
| **新增** | 擴充 3 個既有整合測試類別（payment／installment／report）＋ **2 個新純單元測試**（dispute／merchant 之 `OracleRawIdTest`）＋ **1 個容器初始化腳本** |
| **★ 本批解決** | ★★ `#A-200` 之**誠實略過項**（`JdbcAuthorizationSource`，10 行） |

| 類別 | 前 | **後** |
|---|---:|---:|
| `JdbcPaymentRepository` | 61/84 | ★ **84/84 → `100%`** |
| `JdbcAutoDebitRepository` | 35/41 | ★ **41/41 → `100%`** |
| `JdbcInstallmentRepository` | 61/73 | ★ **73/73 → `100%`** |
| `JdbcReportJobRepository` | 45/46 | ★ **46/46 → `100%`** |
| ★★ `JdbcAuthorizationSource`（**跨 schema**） | 3/13（**略過**） | ★ **13/13 → `100%`** |
| `dispute` 之 `OracleRawId` | 9/12 | ★ **12/12 → `100%`** |
| `merchant` 之 `OracleRawId` | 11/12 | ★ **12/12 → `100%`** |

| 服務 | 前行覆蓋 | **後** |
|---|---:|---:|
| `payment-service` | 74.6% | ★ **81.0%**（**+6.4 pp**） |
| `installment-service` | 78.4% | ★ **82.3%** |
| `dispute-service` | 90.8% | **91.4%** |
| `merchant-service` | 93.4% | **93.6%** |
| `report-service` | 72.2% | **74.1%** |

> ★★ **全服務行覆蓋：79.1% → `79.74%`**（9,995/12,535）；★ **本批 +74 行**；
> **1,275 測／0 失敗／0 錯誤／1 略過**；掃描器**全 0 命中**。
> ★ **距 80% 尚差 +33 行**（門檻 10,028 行）。
> ★★ **略過數 2 → 1**——消失的那一個正是本批所解之 `JdbcAuthorizationSource`。
>
> ★★★ **本批最有價值之發現（方法論級）**：**「環境不支援」往往是誤判——要先讀工具鏈的原始碼**。
> `#A-200` 判定「容器之 app user 無 DBA → 無法建 `CC_AUTH` schema → 誠實略過」。
> 本輪**動查 `gvenzl/oracle-free` 映像之 `container-entrypoint.sh`**，發現
> **`/container-entrypoint-initdb.d/*.sql` 係以 `sqlplus -s / as sysdba` 執行**
> （`run_custom_scripts_recursive`，**逐字確認非推測**）→ 改以
> `withCopyFileToContainer` 於**初始化階段**建立 `CC_AUTH`（DDL **逐字抄自**
> `authorization-service` 之 `V1__baseline.sql`），並授予 `CC_RPT` **僅 `SELECT`**
> （與正式環境一致）→ **略過項變成真機驗證**。
> ★ **併查**：`createAppUser` 腳本對 app user 僅授 **`DB_DEVELOPER_ROLE`**（**不含**
> `CREATE USER`）——★ 此即 `#A-200` 失敗之**真正原因**，已載明。
> ★ **fail-closed**：若初始化腳本未生效，`@BeforeAll` **直接失敗**（**不略過**）——
> 否則「腳本沒跑」會被誤讀為「環境不支援」。
>
> ★ **我方算錯（非缺陷）**：`JdbcAuthorizationSource` 之首版斷言把區間內群組數寫成 **3**，
> 實為 **4**（**漏計 09-30 之 WEB 群組**）。★ 真機以 `expected: <3> but was: <4>` 反駁。

#### 6.11.18 ★★★ 批次 12 執行結果（`#A-204`）——`B3-b-2`：`outbox` 套件（★★ **行覆蓋突破 80%**）

| 項 | 內容 |
|---|---|
| **範圍** | `KafkaEventPublisher` 14／`OutboxRelayScheduler` 12／`OutboxRelayConfig` 2 ＝ **28 行／服務** |
| **本批** | `limit-service` ＋ `payment-service`（★ 兩服務之三類別**正規化後內容相同**，僅 package／註解差異） |
| **新增** | `OutboxRelayIntegrationTest`（**6 測**／服務） |

| 類別（每服務） | 前 | **後** |
|---|---:|---:|
| `KafkaEventPublisher` | 0/14 | ★ **14/14 → `100%`** |
| `OutboxRelayScheduler` | 0/12 | ★ **12/12 → `100%`** |
| `OutboxRelayConfig` | 0/2 | ★ **2/2 → `100%`** |
| ★ **`outbox` 套件整體** | 0/28 | ★★ **28/28 → `100%`** |
| `limit-service` 行覆蓋 | 78.5% | ★ **80.7%** |
| `payment-service` 行覆蓋 | 81.0% | ★★ **85.0%** |

> ★★★ **全服務行覆蓋：79.74% → `80.18%`**（10,051/12,535；**★ 門檻 10,028 行，超出 23 行**）
> ——★ **行覆蓋目標（80%）達成**。★ **本批 +56 行**；
> **1,287 測／0 失敗／0 錯誤／1 略過**；掃描器**全 0 命中**。
> ⚠️ **分支覆蓋 63.78%**（門檻 70%）——**未達**，見 `TRP-004 §7.2`。
>
> ★★ **本批之核心設計——「真機」而非 mock**：以**兩個真容器**（Oracle ＋ Kafka）
> 串起**完整 relay 鏈路**：**outbox 落庫 → relay 撈取 → 送真 Kafka → 標記已發布**，
> ★ 並自**真實 Kafka 消費端**讀回訊息比對（**不是**檢查「有沒有呼叫 `send`」）。
> ★ 三個真實驗證要點：
> ① **端到端**——主題慣例 `cc.<aggregateType>.<eventType>`、**分區鍵 ＝ `aggregateId`**
> （同聚合有序）、發布後 `published_at` 落庫且不再為 pending。
> ② **失敗即拋**——broker 不可達（指向**無人監聽之埠**）時 `send().get()` **同步拋錯**
> → 若改 fire-and-forget，失敗會被靜默吞掉＝**假成功**。
> ③ **例外不得外溢**——`@Scheduled` 拋例外將**取消後續排程**（`#A-194` 之教訓）；
> ★ 以**真實 `JdbcOutboxRepository` 指向無人監聽之埠**觸發真實失敗，斷言 `relay()` **不拋**。
> ④ **中斷旗標須還原**——`Thread.currentThread().interrupt()` 後拋
> `IllegalStateException`；★ 以 `linger.ms` 使 `.get()` 真正阻塞後中斷驗證。
> ★ **每測試用獨立聚合型別 → 獨立主題**（避免跨測試之 Kafka 訊息互相干擾）。
>
> ★ **可複製性**：★ **另 10 個服務之 `outbox` 套件仍為 0/28**（`authorization`／`card`／
> `clearing`／`dispute`／`merchant`／`notification`／`posting`／`product`／`rewards`／
> `statement`）——**配方已驗證**，**合計約 +280 行**，為後續成本最低之批次。

#### 6.11.19 ★★ 批次 13 **計畫**（`#A-205`）——`outbox` 橫向複製至其餘 10 服務

> ★ **本節為動工前之計畫**（鐵律：**規格先行**）。★ **執行結果見 §6.11.20**。

**目標**：把批次 12 已驗證之 `OutboxRelayIntegrationTest` 配方（`§6.11.18`）
**橫向複製**至其餘 10 個服務——**預估 +280 行**（28 行／服務），為**成本最低**之批次。

**範圍（10 服務）**：`authorization`／`card`／`clearing`／`dispute`／`merchant`／
`notification`／`posting`／`product`／`rewards`／`statement`
（★ 已於批次 12 完成者：`limit`／`payment`）。

##### ★★ 動工前盤點（實查，非推測）

| 服務 | `outbox` 套件 | `JdbcOutboxRepository` 套件 | schema 帳號 | outbox 表遷移 |
|---|---|---|---|---|
| `authorization` | `com.beam.cardcore.auth.infrastructure.outbox` | `…auth.infrastructure.jdbc` | `CC_AUTH` | `V9__outbox.sql` |
| `card` | `…card.infrastructure.outbox` | `…card.infrastructure.jdbc` | `CC_CARD` | `V8__outbox.sql` |
| `clearing` | `…clr.infrastructure.outbox` | `…clr.infrastructure.jdbc` | `CC_CLR` | `V3__outbox.sql` |
| `dispute` | `…dis.infrastructure.outbox` | `…dis.infrastructure.jdbc` | `CC_DIS` | `V3__outbox.sql` |
| `merchant` | `…mch.infrastructure.outbox` | `…mch.infrastructure.jdbc` | `CC_MCH` | `V3__outbox.sql` |
| `notification` | `…notif.infrastructure.outbox` | `…notif.infrastructure.jdbc` | `CC_NOTIF` | `V4__outbox.sql` |
| `posting` | `…posting.infrastructure.outbox` | `…posting.infrastructure.jdbc` | `CC_POSTING` | `V3__outbox.sql` |
| `product` | `…product.infrastructure.outbox` | `…product.infrastructure.jdbc` | `CC_PRODUCT` | `V2__outbox.sql` |
| `rewards` | `…rw.infrastructure.outbox` | `…rw.infrastructure.jdbc` | `CC_RW` | `V5__outbox.sql` |
| `statement` | `…stmt.infrastructure.outbox` | `…stmt.infrastructure.jdbc` | `CC_STMT` | `V3__outbox.sql` |

##### ★★ 一致性查證（**逐項實查，非假設**）

1. **三類別之 API 完全一致**（去註解後比對 12 服務）：
   `KafkaEventPublisher(KafkaTemplate<String,String>)` ＋ `publish(String,String,String)`；
   `OutboxRelayScheduler(OutboxPublisher,int)` ＋ `relay()`；
   `OutboxRelayConfig()` ＋ `outboxPublisher(OutboxStore,EventPublisher)`。
   ★ 差異僅在 **package 行與註解**。
2. **`JdbcOutboxRepository` 之 API 完全一致**：
   `JdbcOutboxRepository(JdbcTemplate)` ＋
   `append(String,String,String,int,String,String)` ＋ `findPending(int)`；
   ★ 皆 `implements OutboxPort, OutboxStore`。
3. **`outbox` 表欄位完全一致**（12 服務逐欄比對）：
   `id`／`aggregate_type`／`aggregate_id`／`event_type`／`event_version`／
   `payload`／`trace_id`／`published_at`／`retry_count`／`created_at`。
4. **`testcontainers-kafka` 依賴**：**17 服務皆已宣告**。

##### 執行順序（分兩批，避免單一長任務逾時）

- **批次 13-a**：`authorization`／`card`／`clearing`／`dispute`／`merchant`（5 服務）
- **批次 13-b**：`notification`／`posting`／`product`／`rewards`／`statement`（5 服務）

##### 驗收標準

① 每服務 **6 測全綠**；② 每服務 **`outbox` 套件 `0/28 → 100%`**；
③ **全量 `./gradlew test --continue` BUILD SUCCESSFUL**、**0 失敗／0 錯誤**；
④ 掃描器（`scan_scaffold`／`scan_mockdata`／`scan_faketest`）**全 0 命中**；
⑤ ★ **實測值於實跑後填入 §6.11.20，不得預填**。

#### 6.11.20 ★★★ 批次 13 執行結果（`#A-205`）——`outbox` 橫向複製至其餘 **10 服務**

| 項 | 內容 |
|---|---|
| **範圍** | 10 服務 × 28 行（`KafkaEventPublisher` 14／`OutboxRelayScheduler` 12／`OutboxRelayConfig` 2） |
| **新增** | `OutboxRelayIntegrationTest` × 10（**6 測／服務，共 60 測**） |
| **執行** | 分兩批（13-a／13-b，各 5 服務）——★ 避免單一長任務逾時 |

| 服務 | 前行覆蓋 | **後** | 服務 | 前行覆蓋 | **後** |
|---|---:|---:|---|---:|---:|
| `authorization` | 59.4% | **61.6%** | `notification` | 83.1% | **85.7%** |
| `card` | 89.4% | **92.4%** | `posting` | 83.5% | **86.8%** |
| `clearing` | 81.5% | **86.0%** | `product` | 82.4% | ★ **91.9%** |
| `dispute` | 91.4% | ★ **96.5%** | `rewards` | 83.1% | **85.7%** |
| `merchant` | 93.6% | ★★ **98.9%** | `statement` | 73.7% | **76.7%** |

> ★★ **10 服務之 `outbox` 套件皆 `0/28 → 100%`**；**每服務恰 +28 行**（**與預估完全一致**）。
> ★★★ **全服務行覆蓋：80.18% → `82.42%`**（10,331/12,535；**門檻 10,028，超出 303 行**）
> ——★ **本批 +280 行**。★ **分支覆蓋 63.78% → `64.33%`**（門檻 70%，**仍未達**）。
> **1,347 測／0 失敗／0 錯誤／1 略過**（**+60 測**）；掃描器**全 0 命中**；
> `validate_sql` 70 檔 0 違規；`validate_mermaid` 0 錯誤；
> `gen_service_skeleton --check` 缺漏 0；`verify_versions` 12 項全過。
>
> ★★ **本批之方法論（可複用）——「結構簽章抽取」先於複製**：
> 動工**前**不以**目視 diff** 判斷「程式碼是否相同」（那會誤判——註解與空白各異），
> 而是**以腳本抽取 API 簽章**（去註解後之 `public` 宣告）逐服務比對：
> ① 三類別之建構子與公開方法**完全一致**；② `JdbcOutboxRepository` 之
> `append`／`findPending` 簽章**完全一致**；③ `outbox` 表**逐欄比對一致**。
> ★ **據此才敢橫向複製**——否則會做出**編譯不過**或**語意不符**之測試。
>
> ★ **可再複製者**：`messaging`（`limit` 89／`authorization` 82）與 `client`
> （`authorization` 137／`statement` 74／`posting` 47）——★ 惟需**樁伺服器／Kafka 消費端**
> 之新配方（**樁伺服器非 mock**），成本高於本批。

#### 6.11.21 ★★ 批次 14 **計畫**（`#A-206`）——`messaging` 層（WBS 5.33）

> ★ **本節為動工前之計畫**（鐵律：**規格先行**）。★ **執行結果見 §6.11.22**。

**目標**：涵蓋 7 個 Kafka 消費者（**約 151 行**）。

| 服務 | 消費者 | 主題 | 委派之服務 |
|---|---|---|---|
| `limit` | `PaymentPostedListener` | `cc.payment.payment.posted` | `PaymentPostedProjectionService` |
| `limit` | `PaymentReoccupyListener` | `cc.payment.payment.unmatched`／`.reversed` | `PaymentReoccupyProjectionService` |
| `limit` | `PostingTransactionPostedListener` | `cc.posting.transaction.posted` | `SpecialLimitTempAdjustmentService` |
| `limit` | `StatementIssuedListener` | `cc.statement.statement.issued` | `SpecialLimitTempAdjustmentService` |
| `auth` | `AccountLimitChangedListener` | `cc.limit.limit.adjusted` | `AccountLimitProjectionService` |
| `auth` | `CardStatusChangedListener` | `cc.card.card.status-changed` | `CardStatusProjectionService` |
| `auth` | `LimitHoldListener` | `cc.limit.limit.hold.created`／`.released` | `LimitHoldProjectionService` |

##### ★★ 配方（**真機、無 mock**）

★ **consumer 之職責僅三件事**：**解析信封 → 委派服務 → 例外不得外拋**
（`SAD-004 §5.4`：任何例外皆記 WARN 並略過；**不得**拋出，否則同群組後續訊息全數卡住）。

→ 故測試**以真實服務 ＋ 真實 JDBC 埠（Oracle 容器）串鏈**，直接呼叫 `onMessage(json)`，
再**查 DB 驗證副作用**。★ **不打 mock**。

**接線（實查後之依賴圖）**：

```
limit（cc_lim）：
  JdbcLimitRepository(jdbc)
  JdbcSpecialLimitRepository(jdbc) ／ JdbcOutboxRepository(jdbc)
  LimitApplicationService(limitRepo, specialLimitRepo, Clock.fixed, outbox)
  JdbcSpecialLimitTempAdjustmentRepository(jdbc)
  SpecialLimitTempAdjustmentService(limitRepo, tempRepo, limitAppService, Clock.fixed, 25, 7)
  PaymentPostedProjectionService(JdbcPaymentPostedProjectionRepository(jdbc, limitRepo), tempService)
  PaymentReoccupyProjectionService(JdbcPaymentReoccupyProjectionRepository(jdbc, limitRepo))

auth（cc_auth）：
  AccountLimitProjectionService(JdbcAccountLimitProjectionRepository(jdbc))
  CardStatusProjectionService(JdbcCardStatusProjectionRepository(jdbc))
  LimitHoldProjectionService(JdbcLimitHoldProjectionRepository(jdbc))
```

##### ★★ 每個 consumer 之測試矩陣（防假綠）

| # | 情境 | 期望 |
|---|---|---|
| ① | **有效事件**（完整信封） | 服務被呼叫、**DB 副作用成立**（查表驗證） |
| ② | **未知 `eventType`** | **略過且不拋**（★ 防「毒化消費者」——拋出會使後續訊息全卡） |
| ③ | **壞 JSON**（非 JSON 字串） | **不拋**（WARN ＋ 略過） |
| ④ | **必填欄位缺漏／`null`** | **不拋**（穩健性；`asText(null)` 路徑） |
| ⑤ | **冪等**（同 `eventId` 重送） | 不重複套用（投影服務之 `consumed_events` 守門） |

##### 執行順序

- **批次 14-a**：`limit`（4 個 consumer）
- **批次 14-b**：`authorization`（3 個 consumer）

##### 驗收標準

① 全數測試綠；② `messaging` 套件行覆蓋**顯著提升**（★ 實測值後填）；
③ **全量 BUILD SUCCESSFUL**、0 失敗／0 錯誤；④ 掃描器全 0 命中；
⑤ ★ **實測值於實跑後填入 §6.11.22，不得預填**。

#### 6.11.22 ★★★ 批次 14 執行結果（`#A-206`）——`messaging` 層（`WBS 5.33` 結案）

| 項 | 內容 |
|---|---|
| **範圍** | 7 個 Kafka 消費者（`limit` 4＋`auth` 3） |
| **新增** | `MessagingListenerIntegrationTest` × 2（**12 測**＋**11 測**，共 **23 測**） |
| **執行** | 分兩批（14-a：`limit`／14-b：`authorization`） |

| 套件 | 前 | **後** |
|---|---:|---:|
| `limit.infrastructure.messaging` | 0/89 | ★★ **89/89 → `100%`** |
| `auth.infrastructure.messaging` | 0/82 | ★★ **82/82 → `100%`** |
| `limit-service` 行覆蓋 | 80.7% | ★ **89.1%**（**+105 行**） |
| `authorization-service` 行覆蓋 | 61.6% | ★ **72.7%**（**+144 行**） |

> ★★★ **全服務行覆蓋：82.42% → `84.40%`**（10,580/12,535；**門檻超出 552 行**）
> ——★ **本批 +249 行**（★ **遠超 `messaging` 套件本身之 171 行**——**協同效應**：
> 端到端測試順帶走過 `application` 層之投影服務與額度服務）。
> ★ **分支覆蓋 64.33% → `66.67%`**（門檻 70%，**仍差 3.33 pp**）。
>
> ★★ **配方（`messaging` 層，可複用）**：★ 消費者之職責僅三件事——
> **解析信封 → 委派服務 → 例外不得外拋**（`SAD-004 §5.4`）。
> → 測試以**真實投影服務 ＋ 真實 JDBC 埠 ＋ 真 Oracle** 串鏈，**直接呼叫 `onMessage(json)`**，
> 再**查 DB 驗證副作用**。★ **不打 mock**。
> ★ **四項必驗**：① 有效事件 → **DB 副作用成立**；② **未知 `eventType` → 略過且不拋**
> （★ 拋出會**毒化消費者**，同群組後續訊息全卡）；③ **壞 JSON → 不拋**；
> ④ **缺欄 → 不拋**。
>
> ★★★ **真機揭露（跨服務不一致）**：**部分釋放**佔用時——
> `limit-service` 之 `UPDATE_RELEASE_SQL` 為
> `CASE WHEN ? >= hold_amount THEN 'RELEASED' ELSE status END` → **維持 `HELD`**
> （故 DDL 之 **`PARTIALLY_RELEASED` 在本服務不可達**）；
> ★ 而 `authorization-service` 之讀模型
> （`JdbcLimitHoldProjectionRepository.applyRelease`）對**同一情境**寫 **`PARTIALLY_RELEASED`**。
> ★ **本計畫如實記錄，不預設其為缺陷**（是否統一屬另案裁定）——★ 已寫入測試註解與本節。
>
> ★ **JaCoCo 之量測陷阱（本批釐清）**：`JacocoTaskExtension.append` **預設為 `true`**
> → `test.exec` **跨次累積** → ★ **以 `--tests` 過濾跑出之報告為「歷史聯集」**，
> 不可當「單次結果」解讀（★ 本批實測：過濾跑之服務總數**高於**前次全量跑，
> 一度誤以為異常；★ **實為聯集，與全量跑結果一致**）。

#### 6.11.23 ★★ 批次 15 **計畫**（`#A-207`）——`client` 層（`WBS 5.34`）

> ★ **本節為動工前之計畫**（鐵律：**規格先行**）。★ **執行結果見 §6.11.24**。

**目標**：涵蓋 5 個服務間 HTTP 客戶端（**約 232 行**）。

| 服務 | 客戶端 | 端點 | 行數 |
|---|---|---|---:|
| `authorization` | `CardServiceHttpClient` | `GET /api/v1/cards/lookup/{hash}` | 34 |
| `authorization` | `LimitServiceHttpClient` | `GET limits`／`POST limit-holds`／`GET applicable`／`POST releases` | 85 |
| `statement` | `PostingServiceClient` | `GET balances`／`GET transactions`（**分頁**） | 39 |
| `statement` | `PostingInterestClient` | `POST /api/v1/posting-interest/recalculate` | 30 |
| `posting` | `PricingServiceClient` | `POST /api/v1/pricing/interest-calculations` | 44 |

##### ★★ 配方——**本地樁 HTTP 伺服器（stub server）**

★★ **「樁」非「mock」**：樁伺服器是一個**真實的 HTTP 伺服器**（JDK 內建
`com.sun.net.httpserver.HttpServer`，**零相依**），模擬**外部服務／同儕服務**之行為；
★ 受測物（客戶端）與其協作者（`ServiceClientHttpClient`）**皆為真實物件**，
**未以任何替身取代我方程式**。

**接線（實查）**：
```java
ServiceClientProperties props = new ServiceClientProperties();
props.setConnectTimeout(Duration.ofSeconds(2));
props.setRequestTimeout(Duration.ofSeconds(2));
props.setMaxRetries(0);                       // ★ 避免重試拉長測試
ServiceClientHttpClient http = new ServiceClientHttpClient(props, new NoOpOutboundTokenProvider());
CardServiceHttpClient client = new CardServiceHttpClient(http, "http://localhost:" + stubPort);
```
★ 樁伺服器以 `new InetSocketAddress(0)` 取**臨時埠**（避免埠衝突）。

##### ★★ 每個客戶端之測試矩陣（防假綠）

| # | 情境 | 期望 |
|---|---|---|
| ① | **200 ＋ 完整封套** | 欄位**逐一正確對映**（含可空欄、`UUID`、`LocalDate`） |
| ② | **業務錯誤封套**（`success=false` ＋ `error`） | 拋已登錄碼（`CC-PLT-5001`／`CC-PLT-5005`／`CC-STM-5001`） |
| ③ | **非 200／非 404** | 拋已登錄碼 |
| ④ | **404**（僅 `CardServiceHttpClient`） | ★ 回 `Optional.empty()`（**正常業務結果，非故障**） |
| ⑤ | **連線失敗**（埠無監聽） | 拋已登錄碼，且**原始例外須保留為 cause** |
| ⑥ | ★ **請求內容**（header／query／body） | 由樁伺服器**記錄並斷言**（★ 非只驗回應） |
| ⑦ | ★ `PostingServiceClient` 之**分頁** | 首頁未滿即停；★ **滿頁則續抓**；★ 逾上限**拋錯**（防帳單短收） |

##### 驗收標準

① 全數測試綠；② 三服務之 `client` 套件行覆蓋**顯著提升**（★ 實測值後填）；
③ 全量 BUILD SUCCESSFUL、0 失敗／0 錯誤；④ 掃描器全 0 命中；
⑤ ★ **實測值於實跑後填入 §6.11.24，不得預填**。

#### 6.11.24 ★★★ 批次 15 執行結果（`#A-207`）——`client` 層（`WBS 5.34` 結案）

| 項 | 內容 |
|---|---|
| **範圍** | 5 個服務間 HTTP 客戶端 |
| **新增** | `ServiceHttpClientIntegrationTest` × 2（`auth` **13 測**／`stmt` **10 測**）＋ `PricingServiceClientIntegrationTest`（`posting` **8 測**）＝ **31 測** |

| 套件 | 前 | **後** |
|---|---:|---:|
| `auth.infrastructure.client`（`CardServiceHttpClient`＋`LimitServiceHttpClient`） | 3/137 | ★★ **134/137 → `98%`** |
| `stmt.infrastructure.client`（`PostingServiceClient`＋`PostingInterestClient`） | 3/74 | ★★ **71/74 → `96%`** |
| `posting.infrastructure.client`（`PricingServiceClient`） | 3/47 | ★★ **44/47 → `94%`** |
| `authorization-service` 行覆蓋 | 72.7% | ★ **83.2%**（**+134 行**） |
| `statement-service` 行覆蓋 | 76.7% | ★ **84.3%**（**+71 行**） |
| `posting-service` 行覆蓋 | 86.8% | ★ **92.1%**（**+44 行**） |

> ★★★ **全服務行覆蓋：84.40% → `86.39%`**（10,829/12,535；**門檻超出 801 行**）
> ——★ **本批 +249 行**。
> ★ **未覆蓋之 9 行皆為 `mapper.writeValueAsString` 之 catch**
> （`ObjectNode` 序列化**實務上不會失敗**）——**不可達之防禦分支**，如實記錄。
>
> ★★ **配方（`client` 層，可複用）——「樁伺服器」不是「mock」**：
> 以 **JDK 內建 `com.sun.net.httpserver.HttpServer`**（**零相依**）起一個**真實的 HTTP 伺服器**，
> 模擬**同儕服務**之行為；★ 受測物與其協作者（`ServiceClientHttpClient`）**皆為真實物件**，
> **未以任何替身取代我方程式** → 驗的是**真實 HTTP 往返**（header／body／狀態碼／封套解析）。
> ★ 以 `new InetSocketAddress(0)` 取**臨時埠**（免埠衝突）；★ 樁**記錄請求**供斷言。
> ★ **接線**：`new ServiceClientHttpClient(props, new NoOpOutboundTokenProvider())`，
> `props.setMaxRetries(0)`（測試不重試）。
>
> ★★ **七項必驗**：① 200 → **欄位逐一對映**（含可空欄、`UUID`、`LocalDate`）；
> ② **業務錯誤封套**（`success=false`）→ 拋已登錄碼且**載明下游碼**；
> ③ 非 200／404 → 拋；④ **404 → `Optional.empty()`**（★ 正常業務結果，非故障）；
> ⑤ **連線失敗 → 拋且保留 cause**；⑥ **請求內容**（header／query／body）由樁記錄並斷言；
> ⑦ `PostingServiceClient` 之**分頁**（★ 未滿即停／滿頁續抓／**逾 20 頁拋錯——寧可中止不可短收**）。
>
> ★★ **量測陷阱（再確認）**：★ 以 `--tests` 過濾跑後，**服務總數不可信**——
> 本批實測 `posting-service` 一度顯示 **5.5%**（僅該類別之執行資料）。
> ★ **一律跑全量後才讀服務總數**；★ **類別／套件層級之數字不受影響**。

#### 6.11.25 ★★ 批次 16 **計畫 ＋ 結果**（`#A-208`）——`storage` 層（`WBS 5.35` 結案）

> ★ 依鐵律**先立計畫**（目標／配方／必驗矩陣）後動工；★ **實測值於實跑後填入**。

**目標**：涵蓋 `report-service` 與 `statement-service` 之 `S3ObjectStorageAdapter`（各 40 行）
＋ `S3StorageConfig`（各 22 行）——★ 兩服務之 adapter **正規化後內容相同**（僅 package／註解差異）。

##### ★★★ 動查後之修正：**用 Garage，不是 MinIO**

| 項 | 查證結果 |
|---|---|
| 本專案之本地物件儲存 | ★ **`garage`**（`ops/local/docker-compose.yml` 之 `garage` 服務；`ops/local/config/garage.toml`） |
| MinIO 現況 | ★ **開源版已封存**（官方下載站 **410 Gone**）、★ **Docker Hub 映像已下架**（本輪實測 `docker pull minio/minio` → `pull access denied`） |
| 處置 | ★ 以**專案實際使用之同一映像與同一組態**（`dxflrs/garage:v2.4.1`、`ops/local/config/garage.toml`）起容器——★ **比 MinIO 更忠實** |

##### ★★ 配方（**真機、無 mock**）

```java
new GenericContainer<>(DockerImageName.parse("dxflrs/garage:v2.4.1"))
    .withCommand("/garage", "server", "--single-node", "--default-bucket")
    .withEnv("GARAGE_DEFAULT_ACCESS_KEY", ...).withEnv("GARAGE_DEFAULT_SECRET_KEY", ...)
    .withEnv("GARAGE_DEFAULT_BUCKET", "cardcore-artifacts")
    .withCopyFileToContainer(MountableFile.forClasspathResource("garage/garage.toml"),
                             "/etc/garage.toml")
    .withExposedPorts(3900);
// ★ 以「生產程式碼」S3StorageConfig().s3Client(...) 建構 S3Client——非測試自造
```
★ **region 須與 `garage.toml` 之 `s3_region`（`cardcore`）一致**，否則 `SignatureDoesNotMatch`。
★ 真實 S3 協定往返（含 **SigV4 簽章**、path-style）。

##### ★★ 必驗矩陣

① `put`→`get` **逐位元組一致**（含**非 UTF-8 二進位**與**中文 UTF-8**）；
② **查無 → `Optional.empty()`**（★ 不回空檔，`SCS-017 §6.2` 行為要求 5）；③ 覆寫冪等；
④ `put` 失敗（bucket 不存在）→ 拋 `CC-PLT-5001` ＋ **保留 cause**；
⑤ `get` 失敗（**憑證錯誤 → 403**）→ 拋（★ **不得誤判為「查無」**）；
⑥ 端點不可達 → 拋；⑦ **`S3StorageConfig` 四項必填缺一即 fail-closed**；
⑧ **adapter 之 bucket 未設定即 fail-closed**。

##### 執行結果

| 套件 | 前 | **後** |
|---|---:|---:|
| `rpt.infrastructure.storage` | 2/62 | ★★ **60/62 → `97%`** |
| `stmt.infrastructure.storage` | 2/62 | ★★ **60/62 → `97%`** |
| `report-service` 行覆蓋 | 74.1% | ★ **84.5%**（**+60 行**） |
| `statement-service` 行覆蓋 | 84.3% | ★★ **90.7%**（**+60 行**） |

> ★★★ **全服務行覆蓋：86.39% → `87.35%`**（10,949/12,535；**門檻超出 921 行**）
> ——★ **本批 +120 行**（2 × 60）。★ **兩服務之 `S3StorageConfig` 皆達 `100%`**。
> ★ 各剩 **2 行**未覆蓋＝`S3Exception` 之 **404 分支**
> （★ Garage 回**標準 `NoSuchKey` XML** → 走 `NoSuchKeyException` 分支；
> `S3Exception` + 404 為**相容其他實作之防禦分支**）——如實記錄。
> ★ **20 測**（每服務 10 測）；★ **未以任何替身取代我方程式**。

#### 6.11.26 ★★★ 批次 17-a：`application` 層——`party-service`（`WBS 5.36` 之 1／4）

**目標**：涵蓋 `party-service` 之應用服務（`AccountService` 44／`PartyFlagService` 29／
`PartyQueryService` 31／`PartyViewAssembler` 22）。

##### ★★ 配方（**真機、無 mock**）

以**真實 JDBC 埠 ＋ 真 Oracle ＋ 真實 AES-GCM 加密器**串起應用鏈路：
```java
FieldEncryptor enc = new AesGcmFieldEncryptor(
        StaticKeyProvider.of(EncryptionKey.of("k1", <32 bytes>)));   // ★ 真金鑰，非替身
PartyViewAssembler assembler = new PartyViewAssembler(enc);
new AccountService(accounts, parties, idempotency, assembler);
new PartyFlagService(parties, idempotency, assembler);
new PartyQueryService(parties, accounts, assembler, cards);
```

##### ★★★ 動查才知（三項，皆為我方之錯）

| # | 我方假設 | **實查結果** |
|---|---|---|
| 1 | 冪等表名為 `idempotency_keys` | ★ 實為 **`pty_idempotency`**（`V2__pty_idempotency.sql`） |
| 2 | `Jdbc*Repository.save` 可**建列** | ★★ 實為**純 `UPDATE` ＋ 樂觀鎖**（`WHERE VERSION = version − 1`）→ ★ **建列須以原生 SQL**（`version=0`） |
| 3 | `Party.reconstitute` 之可空參數序為 `mobileEnc, emailEnc, idNoEnc` | ★★ 實為 **`idNoEnc, mobileEnc, emailEnc`**——★ 因皆為 `byte[]`，**編譯期不報錯**，★ 只有**真機斷言**才會現形 |

##### 必驗矩陣

① **冪等三態**——同鍵同指紋＝**重放**（回既有結果、**不重複套用**；★ 歷史不得再寫一筆）；
同鍵**異**指紋＝**衝突**（`CC-PTY-2001`，且**不得改動資料**）；
② **遮罩而非明文**——手機／信箱一律遮罩；★ **未加密（null）→ `null`**；
★★★ **壞密文 → `null` 且不拋**（不得外洩、不得使查詢整體失敗）；
③ **查無 → `CC-PTY-4040`**；④ `getAccount` 含**該歸戶全體持卡人**（依 `party_no` 升冪）；
⑤ `listCards`（★ **會先驗歸戶**）。

##### 執行結果

| 類別 | 前 | **後** |
|---|---:|---:|
| `AccountService` | 0/46 | **44/46** |
| `PartyFlagService` | 0/31 | **29/31** |
| `PartyQueryService` | 0/32 | **31/32** |
| `PartyViewAssembler` | 0/22 | ★ **22/22 → `100%`** |
| ★ **`party.application` 整體** | 30/156 | ★★ **151/156 → `97%`** |
| ★★ **`party-service`** | **59.6%** | ★★★ **91.5%**（**+189 行**） |

> ★★★ **全服務行覆蓋：87.35% → `88.86%`**（11,138/12,535；**門檻超出 1,110 行**）
> ——★ **本批 +189 行**（★ **單批增益第二大**）。★ **14 測**。
> ★★ **分支覆蓋 66.67% → `69.80%`**（門檻 70%）——★ **僅差 0.20 pp**！
> ★ **`party-service` 由全庫最低之 59.6% 躍升至 91.5%**——★ **本計畫之最大單服務改善**。

#### 6.11.27 ★★★ 批次 17-b：`application` 層——`rewards-service`（`WBS 5.36` 之 2／4）

| 類別 | 前 | **後** |
|---|---:|---:|
| `PointsExpiryService` | 0/65 | **57/65** |
| `PointsViewAssembler` | 0/13 | ★ **13/13 → `100%`** |
| `PointsAccountService` | 0/5 | ★ **5/5 → `100%`** |
| `AccrualService` | 0/14 | **7/14** |
| ★ **`rw.application` 整體** | 289/387 | ★★ **357/387 → `92%`** |
| ★★ **`rewards-service`** | **85.7%** | ★★ **93.5%**（**+85 行**） |

> ★★★ **全服務行覆蓋：88.86% → `89.53%`**（11,223/12,535；**門檻超出 1,195 行**）
> ★★★ **分支覆蓋：69.80% → `70.43%`**（門檻 70%）——★ **分支門檻正式達標**！
> ★ **本批 +85 行**；**12 測**。
>
> ★★★ **真機發現兩項（皆為現行行為，如實記錄、不預設為缺陷）**：
> 1. **同帳戶、同日到期之同階段桶 ≥2 → 到期批次拋出並中止**：
>    分錄之冪等鍵為 **`EXP:<account.id()>:<bucketStage>:<entryType>:<today>`**
>    ——★ **非以桶為主體** → 第二桶之 `EXPIRE` 分錄**鍵相同** → `DuplicateKeyException`
>    （**非 `DomainException`**）→ ★ **`catch (DomainException)` 攔不住** → **整批中止**。
>    ★★ 且中止後留下「**已建新桶、但無分錄**」之**孤兒桶**（資料完整性殘留）。
>    ★ 觸發條件於業務上**可能發生**（同日兩筆累點 → 兩桶同到期日；索引非唯一）。
> 2. **首次累點（新帳戶）→ `ORA-02291` FK 失敗**：
>    `AccrualService.accrue` 於新帳戶路徑之順序為
>    **`upsertBucket(account.id(), …)` 先於 `saveAccount(updated)`**
>    → 桶之 FK 找不到父項 → ★ **首次累點即失敗**。
> ★ **兩項皆已寫入測試註解**（`duplicateStageBucketAbortsBatch`／`newAccountAccrualFailsOnFk`），
> **待 BEAM PMO 裁定是否立 CR**。
>
> ★★ **我方之錯（4 項）**：① 分錄金額**非 1:1**（`金額 × 級別倍率`）；
> ② `sourceTransactionId` **須為 UUID**（非任意字串）；
> ③ `getByAccountId` 之參數為**歸戶 `accountId`**（非 `points_accounts.id`）；
> ④ `markExpired` 在**寫分錄之前**（中止點之精確位置）。

#### 6.11.28 ★★ 批次 17-c：`application` 層——`limit-service`（`WBS 5.36` 之 3／4）

| 類別 | 前 | **後** |
|---|---:|---:|
| `LimitApplicationService` | 25/46 | ★★ **45/46**（餘 1 為**不可達之防禦分支**） |
| `SpecialLimitTempAdjustmentService` | 9/36 | **30/36** |
| `LimitAdjustmentService` | 46 | ⬜ **本批未執行**（需 `CallerIdentityPort`，列後續） |
| ★ **`lim.application` 整體** | 377/468 | ★ **408/468 → `87%`** |
| ★ **`limit-service`** | 89.1% | ★ **91.6%**（**+31 行**） |

> ★★ **全服務行覆蓋：89.53% → `89.78%`**（11,254/12,535；**門檻超出 1,226 行**）
> ★★ **分支覆蓋：70.43% → `70.73%`**（門檻 70%）。★ **本批 +31 行**；**12 測**。
>
> ★★ **必驗矩陣**：① `query` 全欄位 ＋ **可用額度為負時夾擠為 0**（不得外洩負值）；
> ② 5 參數 `hold` **委派至 6 參數版**；③ **可用額度不足即拒絕**（`CC-LIM-1001`，不得靜默放行）；
> ④ `release` 查無佔用即拋；⑤ ★★ **專用池回沖失敗仍成功**（釋放為權威事實，僅記 WARN）；
> ⑥ `applicableSpecialLimit` **三分支**（無命中／不足／適用）；
> ⑦ 8-6 **查無額度主檔 → 回 false 且不落串動紀錄**；⑧ 8-7 回收**不超過剩餘應收**。
>
> ★★ **動查才知（2 項，皆為我方之錯）**：① `special_limits.currency` 係 **`V6` 新增且 `NOT NULL`**
> ——★ 種子須帶幣別；② ★ 回收後 `temporary_amount` ＝ **30,000 − 25,000 ＝ 5,000**（我原算成 10,000）。

#### 6.11.29 ★★★ 批次 17-c-2：`application` 層——`LimitAdjustmentService`（`#A-212`）

| 類別 | 前 | **後** |
|---|---:|---:|
| ★★ **`LimitAdjustmentService`** | 0/56 | ★★ **46/56**（餘 10 為**不可達之防禦分支**） |
| ★ **`lim.application` 整體** | 408/468 | ★★★ **448/468 → `96%`** |
| ★★ **`limit-service`** | 91.6% | ★★★ **95.6%**（**+51 行**） |

> ★★★ **全服務行覆蓋：89.78% → `90.19%`**（11,305/12,535；**門檻超出 1,277 行**）
> ★★★ **分支覆蓋：70.73% → `71.37%`**（門檻 70%）。★ **本批 +51 行**；**11 測**。
>
> ★★★ **配方之關鍵——真實身分來源**：`CallerIdentityPort` 以**真件**
> `SecurityContextCallerIdentity`（**無參數建構**、讀 Spring Security context）；
> ★ 測試環境無認證 → `current()` 回 **`UNVERIFIED`**（**fail-closed**）——
> ★ **非以替身取代**。
>
> ★★★ **真機揭露（fail-closed 設計正確）**：`SPECIAL` 調整須先有
> **「可設定上限政策」**（`special_limit_policies`，`V8`）——
> ★ 無政策即 **`CC-LIM-1002` 拒絕**（本批初版即因此失敗，★ 補種政策後通過）。
> ★ 政策以 `(role_code, segment_code, currency)` 為鍵；`UNVERIFIED` 角色**須顯式設定**。
>
> ★★ **必驗矩陣（額度調整狀態機）**：
> ① **增幅 ≤ 門檻 → 直接生效並即刻套用主檔**；② **增幅 > 門檻 → 待覆核且不動主檔**；
> ③ `APPROVE` → 套用；`REJECT` → 結案**不動主檔**；④ **撤銷須回復原額度**
> （否則留下幽靈額度）；⑤ 臨時額度到期 → **歸零 ＋ `EXPIRED`**；
> ⑥ ★★ **`SPECIAL` 之基準為「現行有效之專用額度」＝ 0（非主檔永久額度）**、
> 寫 `special_limits`、**不佔卡人額度**、**不發射事件**（總額未變 → 噪音）。
>
> ★★ **動查才知（1 項，我方之錯）**：`LimitAdjustmentView.id()` 回傳 **`UUID`**（非 `String`）。

#### 6.11.30 ★★★ 批次 17-d：`application` 層——`payment-service`（`WBS 5.36` 之 4／4 ★ **結案**）

| 類別 | 前 | **後** |
|---|---:|---:|
| `PaymentService` | 0/41 | ★★ **41/41 → `100%`** |
| `PaymentViewAssembler` | 0/16 | ★★ **16/16 → `100%`** |
| `AutoDebitService` | 0/3 | ★★ **3/3 → `100%`** |
| `PaymentEventFactory` | 12/15 | **12/15**（餘 3 為 `unmatchedEnvelope` 分支） |
| ★ **`payment.application` 整體** | 92/187 | ★★★ **184/187 → `98%`** |
| ★★ **`payment-service`** | **85.0%** | ★★★ **98.0%**（**+92 行**） |

> ★★★ **全服務行覆蓋：90.19% → `90.92%`**（11,397/12,535；**門檻超出 1,369 行**）
> ★★★ **分支覆蓋：71.37% → `71.64%`**（門檻 70%）。★ **本批 +92 行**；**8 測**。
>
> ★★ **配方——真 Oracle ＋ 真實 JDBC 埠 ＋ 樁 HTTP 伺服器**：
> `PostingBalancePort` 以**真件** `PostingServiceClient` ＋ **JDK 內建 `HttpServer` 樁**
> （**零相依**）模擬**同儕服務 `posting-service`** 之餘額端點；
> ★ 受測物與協作者**皆為真實物件**，★ **未以任何替身取代我方程式**。
>
> ★★ **必驗矩陣**：
> ① **首次受理 → 分配落庫 ＋ 入帳 ＋ 額度回復 ＋ 事件同交易**；
> ② ★★★ **查無分戶（404）→ 全額歸本金 ＋ 標記未匹配**（★ **非故障、不拋**）；
> ③ **冪等受理 → 回前次結果、不重複入帳**；
> ④ **非法識別碼／日期 → `CC-PAY-4000`**（★ 不得落到 500）；
> ⑤ ★★★ **沖正 → 發射 `reversed` 事件**（★ 否則 `limit-service` 之可用額度**永久高估**）；
> ⑥ 沖正守門（查無 `CC-PAY-4040`／已沖正 `CC-PAY-3001`）；⑦ 自扣遮罩（★ 實測 6 星 ＋ 末 4 碼）。
>
> ★★ **動查才知（1 項，我方之錯）**：自扣帳號之遮罩格式實為 **`******7890`**（6 星），非 4 星。

---

#### 6.11.31 ★★★ 批次 20 計畫：`platform/` 8 模組覆蓋率（`WBS 5.37`）

> ★ **緣起**：`#A-216` 揭露**雙口徑**——`TRP-003`／`TRP-004` 之「達標」為 **17 服務**口徑
> （行 **90.92%**／分支 **71.64%**）；而 `verify_nfr` 以**全 backend**計（17 服務 ＋ **8 平台模組**）
> 為行 **87.42%**／分支 **68.98%**。★ **若客戶以「全 backend」計，分支門檻（70%）尚未達標**。
> 本批次即為**關閉此口徑落差**。

##### ① 盤點（2026-10-06 實測，**逐類別**）

> ★ **量測法**：解析各模組 `build/reports/jacoco/test/html/` 之 `index.html`（`<tfoot>` 之
> `bar` 為「**missed of total**」格式，**非** `title=` 屬性——**兩種格式並存**，須分別處理）。
> ★ 報告時戳 **2026-10-05 11:39–11:44**，且 `src/main` **無任何檔案新於報告** → **報告為最新**。

| 模組 | 未覆蓋行／總行 | 行% | 未覆蓋分支／總分支 | 分支% |
|---|---:|---:|---:|---:|
| `bff-web` | **316**／452 | 30.1% | **118**／136 | 13.2% |
| `api-gateway` | **166**／280 | 40.7% | **63**／126 | 50.0% |
| `common-security` | **104**／393 | 73.5% | **73**／176 | 58.5% |
| `common-spring` | **67**／530 | 87.4% | **71**／266 | 73.3% |
| `common-saga` | **36**／416 | 91.3% | **46**／136 | 66.2% |
| `common-domain` | **30**／184 | 83.7% | **14**／82 | 82.9% |
| `common-batch` | **25**／101 | 75.2% | **13**／26 | 50.0% |
| `common-event` | **1**／77 | 98.7% | **4**／36 | 88.9% |
| **合計** | ★ **745**／**2,433** | **69.4%** | ★ **402**／**984** | **59.1%** |

★ **與 `#A-216` 之記載逐數字吻合**（745／402）——**盤點正確**。

##### ② ★★ 關鍵算術（達標之精確門檻）

| 口徑 | 行 | 分支 |
|---|---:|---:|
| 17 服務 | 11,397／12,535 ＝ **90.92%** | 2,607／3,639 ＝ **71.64%** |
| `platform/` 8 模組 | 1,688／2,433 ＝ **69.4%** | 582／984 ＝ **59.1%** |
| ★ **全 backend** | 13,085／14,968 ＝ **87.42%** | 3,189／4,623 ＝ **68.98%** |

> ★★ **全 backend 分支達 70% 之精確需求**：`4,623 × 0.70 ＝ 3,236.1`
> → 已覆蓋須 ≥ **3,237** → 現 **3,189** → ★ **尚需再覆蓋 48 條分支**。
> ★ 即：**只需 `platform/` 402 條未覆蓋分支中之 12%**，即可使「全 backend」分支達標。
> ★ **行覆蓋（87.42%）早已遠超 80%**，非本批次之約束。

##### ③ 批次排序（依「每條分支之成本」由低到高）

| 批次 | 範圍 | 未覆蓋 行／分支 | 配方 | 成本 |
|---|---|---:|---|---|
| **20-a** | `common-domain`＋`common-batch`＋`common-event` | 56／**31** | ★ **純單元**（零相依、無容器） | ★ 最低 |
| **20-b** | `bff-web` | 316／**118** | ★ **樁 HTTP 伺服器**（JDK 內建，**非 mock**）模擬下游服務 | 中 |
| **20-c** | `api-gateway` | 166／**63** | ★ Spring WebFlux 過濾器／路由設定（**真物件**） | 中 |
| **20-d** | `common-security` | 104／**73** | ★ JWT 真實簽發／驗證（`JwtSigner` ＋ `JwtValidator`） | 中 |
| **20-e** | `common-spring` | 67／**71** | ★ 真實 Spring context（`@SpringBootTest` 切片） | 中高 |
| **20-f** | `common-saga` | 36／**46** | ★ 真 Oracle（`JdbcSagaStore`）／狀態機純單元 | 中 |

> ★★ **排序理由**：**先做 20-a**（零相依、最快建立「已達標」之事實），
> **再做 20-b**（單一模組即可貢獻 118 條分支，**遠超所需 48 條**，一擊達標）。

##### ④ 驗收標準（**不可放寬**）

1. ★★ **全 backend 分支 ≥ 70%**（**主目標**，需 +48 條）。
2. ★ **全 backend 行 ≥ 80%**（**早已達成**，不得回退）。
3. ★ **逐模組不得下降**；★ 未覆蓋者**逐條記錄**並標明「**不可達之防禦分支**」或「**待補**」。
4. ★★ **零 mock**：受測物與其協作者**皆為真實物件**；模擬**外部／同儕**服務一律用
   **樁伺服器**（`com.sun.net.httpserver.HttpServer`）或**真容器**。
5. ★ **掃描器全 0 命中**（`scan_scaffold`／`scan_mockdata`／`scan_faketest`）＋
   `validate_sql`／`validate_mermaid` 0 違規。
6. ★ **驗證結果一律等實跑完再填**（★ 不得預填）。

##### ⑤ 已知陷阱（本專案已踩過，勿再踩）

- ★ **JaCoCo 報告有兩種 `Total` 格式**——**模組層**為 `bar` 之 `"N of M"` 文字、
  **套件／類別層**為 `bar` 內 `img title=`；★ **解析器必須分別處理**，
  否則會**靜默取到 0**（本輪首版即因此得 `ZeroDivisionError`）。
- ★ **類別列之 `ctr1/ctr2` 序**為 `[instr%, branch%, cxtyM, cxtyT, lineM, lineT, methM, methT, clsM, clsT]`
  → ★ **行數取索引 4／5**，**不是** 2／3（2／3 是**複雜度**，會誤讀）。
- ★ **`</tfoot>` 在 `<tbody>` 之前**（JaCoCo HTML 之實際順序）→ 以 `</tfoot>` 為 `tbody` 之上界會**切出空字串**。
- ★ macOS `grep` 之 `\|` **失效** → 交替比對一律 `grep -E`。

---

#### 6.11.32 ★★ 批次 20-a：`common-domain`／`common-batch`／`common-event`（`#A-217`）

| 模組 | 前（行／分支） | **後** |
|---|---|---|
| `common-domain` | 30/184 ＝ 83.7%／14/82 ＝ 82.9% | ★★ **0/184 ＝ `100%`／0/82 ＝ `100%`** |
| `common-batch` | 25/101 ＝ 75.2%／13/26 ＝ 50.0% | ★★ **0/101 ＝ `100%`／0/26 ＝ `100%`** |
| `common-event` | 1/77 ＝ 98.7%／4/36 ＝ 88.9% | ★★ **0/77 ＝ `100%`／0/36 ＝ `100%`** |

> ★★ **本批 +56 行、+31 分支**；**`platform/` 未覆蓋 745 → 689 行、402 → 371 分支**。
> ★★ **全 backend：行 87.42% → `87.79%`；分支 68.98% → `69.65%`**（距 70% 僅差 0.35 pp）。
>
> ★ **配方——純單元、零相依、零替身**：**六個**新增測試類別／**47 測**，
> 受測物皆為**真實值物件／例外／record**（`CSP-005`）。
> ① `ValueObjectSemanticsTest`——`Money.negate`／符號述詞兩分支、
> `LimitAmount.settle` 之拒絕路徑、**`equals` 三項短路鏈之每一段**、
> `CardNumber` 之 **Luhn 折減分支**（`d > 9 → d -= 9`，以業界公認測試卡號 `5555555555554444`）；
> ② `ExceptionOverloadsTest`——**5 個子類別 × 3 個多載 ＝ 15 條建構路徑**；
> ③ `ScheduledBatchRunnerTest`——★ **單一工作失敗不得影響其他工作**（排程配接器必須逐工作 try/catch）；
> ④ `BatchOrchestratorEdgeTest`——`chunkSize` 檢核、**項目提早耗盡須標記完成**、
> `loadItems` 回 `null` 之防禦分支、`findIncomplete` 過濾；
> ⑤ `EventEnvelopeCompatibilityTest`——`schemaRef` 四條預設路徑、**`isCompatibleWith` 四種組合**。
>
> ★★ **真機反駁（我方之錯，2 項）**：
> ① `Set.of(a, b)` 對**重複元素**會拋 `IllegalArgumentException`——我原用它建集合以驗「兩者相等」，
> 邏輯自相矛盾（**測試自身的錯**）；
> ② `LimitAmount.toString()` 以**主單位**呈現（`8.50 TWD`），我原斷言 `"850"`（誤以為最小單位）。
>
> ★ **殘餘未覆蓋：0**——三模組**行與分支皆 `100%`**。

---

#### 6.11.33 ★★★ 批次 20-b：`bff-web`（`#A-218`）★★ **關閉「全 backend」口徑落差**

| 類別／模組 | 前 | **後** |
|---|---|---|
| `BatchMonitorService` | 101/101 未覆蓋 | ★ **未覆蓋 0**（餘 2 為逾時 catch，見下） |
| `BusinessStatsService` | 56/56 | ★ **未覆蓋 0**（餘 3 為逾時 catch） |
| `EndpointManagementService` | 55/55 | ★ **未覆蓋 0**（餘 2 為健康聚合例外 catch） |
| `AccountOverviewClientImpl` | 36/36 | ★ **未覆蓋 0** |
| `DashboardSourcesAdapter` | 10/10 | ★ **未覆蓋 0** |
| 5 個控制器 | 各 5 | ★ **未覆蓋 0** |
| `BffHealthProperties`／`HealthTargetsConfigImpl`／`BffHealth`／`BffBatchJob` | 共 20 | ★ **未覆蓋 0** |
| ★★ **`bff-web` 整體** | **行 316/452 ＝ 30.1%／分支 118/136 ＝ 13.2%** | ★★★ **行 12/452 ＝ `97.3%`／分支 4/136 ＝ `97.1%`** |

> ★★★ **本批 +304 行、+114 分支**——★ **單一批次之最大增益**。
> ★★★ **`platform/` 8 模組：行 69.4% → `84.18%`；分支 59.1% → `73.88%`**。
> ★★★ **全 backend：行 87.79% → `89.82%`；分支 69.65% → `72.12%`**
> ——★★★ **「全 backend」之兩項門檻（行 ≥80%／分支 ≥70%）首次雙雙達標**
> （分支門檻超出 **90 條**）。★ **本批 5 個測試類別／62 測**（★ **全模組 206 測／0 失敗／0 錯誤／0 略過**）。
>
> ★★ **配方——樁 HTTP 伺服器（★ 非 mock）**：
> 新增 `DownstreamStub`（**JDK 內建 `HttpServer`，零相依**）模擬
> **bff-web 之四個下游同儕**（statement／clearing／migration／report）
> 與 **gateway／card／authorization／dispute**；
> ★ **受測物（各聚合服務）與其協作者（`ServiceClientHttpClient`）皆為真實物件**，
> **未以任何替身取代我方程式** → 驗的是**真實 HTTP 往返**（路徑、查詢字串、狀態碼、封套解析）。
> ★ 控制器亦以**真實聚合服務**注入——★ **若以假服務注入，測的是假服務**。
> ★ 惟 `AccountOverviewPort`／`DashboardSourcesPort`／`HealthTargetsPort` 等**埠介面**
> 依其設計文件（`HealthTargetsPort` 之類別說明：「立埠後可用手寫 fake 測試」）**以手寫 fake 實作**。
>
> ★★ **必驗矩陣**：
> ① ★★ **`DIFFERENCE` 歸 `WARNING` 而非 `SUCCEEDED`**——對帳有差異**帳務上不等於成功**，
>   歸成功會使差異被忽略（**假綠**）；
> ② ★ **部分失敗不拖垮整體**（非 200／連不上 → `unavailable[]` 並帶原因）；
> ③ ★ **超界 limit 顯式報錯**（`CC-PLT-4003`），**不靜默夾取**；
> ④ ★ **欄位別名鏈**（`runId`↔`jobId`、`txCount`↔`processedCount`、`createdAt`↔`startedAt`）
>   ——★ **取不到者留 `null`，不以 0 填充**；
> ⑤ ★ **目標未組態時該子區塊 `null` 且「完全不發出請求」**（連打都不打）；
> ⑥ ★★ **保留下游原錯誤碼**（`CC-PTY-4040`）——聚合層**不轉碼**；
> ⑦ ★ **下游回應非 JSON → `CC-PLT-5005`**（不得誤判為查無）；
> ⑧ ★ **`cards` 之下游為 `party`**（**非** `card-service`）；★ **`routeId` 無法對映者保留原文**；
> ⑨ ★ **`BffHealth` 空清單／null → `DOWN`**（不樂觀預設為 UP）。
>
> ★★ **真機反駁（我方之錯，1 項）**：`Map<?, ?>` 之**型別捕獲**使 `containsEntry` 無法編譯
> ——★ 改以 `Map<String, Object>` 轉型（**編譯期即攔下**）。
>
> ★ **殘餘未覆蓋 12 行／4 分支（逐條如實記錄）**：
> **逾時 catch 分支 8 行**（`BatchMonitorService` 2／`BusinessStatsService` 3／
> `EndpointManagementService` 2／`HealthAggregationService` 1）
> ——★ 觸發條件為「future 之等待逾時」，需**刻意製造極端時序**，屬**防禦性**分支；
> `HealthAggregationService` 之「健康聚合自身拋例外」1 行——★ 同上；
> 其餘 3 行為 `AccountOverviewService` 之**下游呼叫失敗** catch（需與 `UNAVAILABLE` 路徑併測）。

---

#### 6.11.34 ★★ 批次 20-c：`api-gateway`（`#A-219`）

| 類別／模組 | 前 | **後** |
|---|---|---|
| `CycleSplitGlobalFilter` | 36/36 未覆蓋 | ★ **未覆蓋 0** |
| `CycleSplitConfigAdapter` | 51/107 | ★ **未覆蓋 0** |
| `SplitRatioController` | 22/22 | ★ **未覆蓋 0** |
| `CycleSplitProperties` | 21/21 | ★ **未覆蓋 0** |
| `GatewaySecurityConfig` | 13/13 | ★ **未覆蓋 0** |
| `GatewayRateLimitConfig` | 11/11 | ★ **未覆蓋 0** |
| `GatewayRouteController` | 7/7 | ★ **未覆蓋 0** |
| `CycleSplitDecision`／`GatewayRoute`／`SplitRatioConfigPort` 等 | 共 5 | ★ 未覆蓋 **0** |
| ★★ **`api-gateway` 整體** | **行 166/280 ＝ 40.7%／分支 63/126 ＝ 50.0%** | ★★ **行 3/280 ＝ `98.9%`／分支 6/126 ＝ `95.2%`** |

> ★★ **本批 +163 行、+57 分支**；**6 個測試類別／51 測**（★ **全模組 79 測／0 失敗／0 錯誤／0 略過**）。
> ★★ **`platform/` 8 模組：行 84.18% → `90.88%`；分支 73.88% → `79.67%`**。
> ★★ **全 backend：行 89.82% → `90.91%`；分支 72.12% → `73.35%`**。
> ★ **掃描器全 0 命中**。
>
> ★★ **配方——真實框架介面 ＋ 真件之子類（非 mock 框架）**：
> ① `CycleSplitGlobalFilter` 以 **Spring 官方 `MockServerWebExchange`**（**真實介面實作**）
>   ＋ `GatewayFilterChain` **函式介面 lambda**（記錄是否續鏈）驅動；
> ② `CycleSplitConfigAdapter` 以 **`@TempDir` 真實暫存檔** ＋ **`MockEnvironment`** 驗檔案往返；
> ③ 路由查詢用 Spring Cloud Gateway **真實** `InMemoryRouteDefinitionRepository`；
> ④ `ContextRefresher` 為 **Spring Cloud 之 `abstract` 類別** → 以**最小子類**提供可觀察行為。
>
> ★★★ **真機反駁我方之假設（4 項，皆為我方之錯）**：
> ① ★ **`ServerHttpSecurity.http()` 無 bean factory** → `oauth2ResourceServer().jwt(...)`
>   取不到 decoder，拋 `IllegalArgumentException: jwtDecoder cannot be null`；
>   ★ 正解：以**最小子類**注入 `ApplicationContext`（`ServerHttpSecurity()` 建構子為 `protected`，
>   Spring Security 自身之 `ContextAwareServerHttpSecurity` 即同法）。
> ② ★★ **WebFlux 解析的是 `ReactiveJwtDecoder`，不是 servlet 版 `JwtDecoder`**
>   ——★ 註冊錯型別得 `NoSuchBeanDefinitionException`。
> ③ ★ **`assertThat(exchange.getAttribute(...))` 之型別歧義**——`getAttribute` 為**泛型方法**
>   → 與 AssertJ 之 `assertThat(IntPredicate)`／`assertThat(Predicate)` **同時匹配**；
>   ★ 正解：**先轉型為具體型別**再斷言。
> ④ ★★ **`GatewayRateLimitConfig` 之 `name != null` 為死碼**——★ `Mono.map(Principal::getName)`
>   回 `null` 時 **Reactor 直接拋 NPE**（Reactor 規格禁止 null 元素）→ 該分支**不可達**；
>   ★ 以**空白字串**方可穩定觸發「退回 IP」路徑。
>
> ★ **殘餘未覆蓋 3 行／6 分支（逐條如實記錄，皆為不可達之防禦分支）**：
> `CycleSplitDecision` L64-65（`NoSuchAlgorithmException`——**MD5 為 JVM 規格必備**）；
> `GatewayRateLimitConfig` L30（`name != null`，**Reactor 禁止 null**）、
> L33（`remote.getAddress() == null`——位址物件存在時其 address 必非 null）；
> `CycleSplitConfigAdapter` L65（`parent != null`／`isWritable` 為 false——絕對路徑必有父目錄）、
> L134-135（剝除前綴後為空——**該情形必先因「不含 cycle-split」而略過**）、
> L226（`i < line.length()` 為 false——**全空白行已於前一步被略過**）。

---

#### 6.11.35 ★★★ 批次 20-d：`common-security`（`#A-220`）★★ **揭露並修補真缺陷（`CR-042`）**

| 類別／模組 | 前 | **後** |
|---|---|---|
| `JwtAuthFilter` | 45/45 未覆蓋 | ★ **未覆蓋 0** |
| `JwtValidator` | 18/91 | ★ **未覆蓋 0**（餘 2 為不可達之通用 catch） |
| `AesGcmFieldEncryptor` | 11/41 | ★ **未覆蓋 0**（餘 6 為不可達之 catch） |
| `CurrentUser` | 11/11 | ★ **未覆蓋 0** |
| `AuthorizationClaim` | 7/23 | ★ **未覆蓋 0** |
| `EncryptionKey` | 7/19 | ★ **未覆蓋 0** |
| `JwtSigner`／`JwtValidationException`／`CryptoProperties`／`StaticKeyProvider`／`PiiMasker` | 共 5 | ★ 未覆蓋 **0**（`PiiMasker` 餘 1 為不可達） |
| ★★ **`common-security` 整體** | **行 104/393 ＝ 73.5%／分支 73/176 ＝ 58.5%** | ★★ **行 8/395 ＝ `98.0%`／分支 3/176 ＝ `98.3%`** |

> ★★ **本批 +96 行、+70 分支**；★ **8 個測試類別／86 測**（★ **全模組 137 測／0 失敗／0 錯誤／0 略過**）。
> ★★ **`platform/` 8 模組：行 90.88% → `94.83%`；分支 79.67% → `86.79%`**。
> ★★ **全 backend：行 90.91% → `91.56%`；分支 73.35% → `74.86%`**。★ 掃描器**全 0 命中**；
> ★ **全 backend 編譯驗證通過**（修補後 71 tasks 全 up-to-date）。
>
> ★★★ **本批之最大價值：揭露並修補一個真缺陷（`CR-042`）**
> ——★★ **`JwtAuthFilter.sendUnauthorized()` 必拋 `NullPointerException`**：
> 原以 `java.util.Map.of("data", (Object) null, …)` 組封套，
> ★ 而 **`Map.of` 對「任何 null 之鍵或值」皆拋 NPE**（`ImmutableCollections.MapN` 之 `requireNonNull`）。
> ★★ **即：本過濾器自建立以來，其核心職責（未認證 → 401）從未真正執行過一次**。
> ★ **查證影響範圍**：`grep -rl` 全庫 → ★ **`JwtAuthFilter` 零接線**（僅 Javadoc 提及）
> → ★ **不影響現行系統**；★ 惟其為 `common-security` 之公開 API，
> ★ **一旦依文件接線，所有 401 立即變成 500** → ★ **仍須修**（依 `CR-041 #A-184` 同一標準）。
> ★ **修法**：改用 `LinkedHashMap` 承載（**JSON 形狀不變**，`data` 仍為 `null`）。
> ★★ **修補前 14 測中 11 測紅燈 → 修補後 14 測全綠**。
>
> ★★ **配方——真實框架介面 ＋ 真實密碼學運算（★ 非 mock）**：
> ① `JwtAuthFilter` 以 Spring 官方 `MockHttpServletRequest`／`MockHttpServletResponse`
>   ＋ `FilterChain` **函式介面 lambda**；★ **token 由真實 `JwtSigner` 簽發、真實 `JwtValidator` 驗證**；
> ② 對「格式錯誤」之 token，以**自算 HMAC 手工組裝**（★ 是**真實**的密碼學運算，非 mock）；
> ③ `AesGcmFieldEncryptor` 以**真實** `StaticKeyProvider` ＋ **真實** AES-256-GCM；
> ④ 對「金鑰來源拋錯」以**手寫 `KeyProvider`**（**本專案自有之埠介面**）。
>
> ★★★ **真機反駁我方之錯（1 項）**：
> `AesGcmFieldEncryptorEdgeTest` 之「竄改密文」初版**改動 Base64 末字元**——
> ★ 其**低 2 位被丟棄** → 有時不改變任何 byte → **認證反而通過**
> （★ 與 `#A-163` 所載之 **6.25% 間歇性失敗同型**，本輪**再次踩到**）；
> ★ 已改為改動**最後一段之中間字元**（全位元有效）。
>
> ★ **殘餘未覆蓋 8 行／3 分支（逐條如實記錄，皆為不可達之防禦分支）**：
> `AesGcmFieldEncryptor` L60-63（encrypt 之 `catch`——★ **`currentKey()` 在 try 之外**，
> 且 `Cipher.getInstance` 不可能失敗）、L93-94（decrypt 之 `catch (CryptoException)`——
> ★ **金鑰查詢在 try 之外**，try 內無任何路徑可拋 `CryptoException`）；
> `JwtValidator` L124-125（通用 `catch`——`Mac.getInstance` 不可能失敗）、
> L192（`a == null || b == null`——★ `decodeB64ToBytes` 永不回 null）；
> `PiiMasker` L149（`repeat` 之 `times <= 0`——★ **兩個呼叫點皆已先擋掉**）。

---

#### 6.11.36 ★★ 批次 20-e：`common-spring`（`#A-221`）

| 類別／模組 | 前 | **後** |
|---|---|---|
| `GlobalExceptionHandler` | 12/76 | ★ **未覆蓋 0**（餘 9 為不可達：私有巢狀類別／登錄表必有值） |
| `ClientCredentialsTokenProvider` | 12/62 | ★ **未覆蓋 0**（餘 5：並發路徑／`InterruptedException`） |
| `SecurityProperties.Outbound` | 8/31 | ★ **未覆蓋 0** |
| `ServiceCallerAclManager` | 7/27 | ★ **未覆蓋 0** |
| `ServiceAcl` | 6/47 | ★ **未覆蓋 0**（餘 2 為 `IOException`） |
| `SecurityAutoConfiguration`／`TraceContext`／`ApiResponse`／`JwtRoleConverter`／`RelayingOutboundTokenProvider` 等 | 共 17 | ★ 未覆蓋 **0** |
| ★★ **`common-spring` 整體** | **行 67/530 ＝ 87.4%／分支 71/266 ＝ 73.3%** | ★★ **行 27/530 ＝ `94.9%`／分支 39/266 ＝ `85.3%`** |

> ★ **本批 +40 行、+32 分支**；★ **7 個測試類別／56 測**（★ **全模組 151 測／0 失敗／0 錯誤／0 略過**）。
> ★★ **配方**：**真實框架介面 ＋ 真實例外物件 ＋ 真實 IdP 樁**——
> `MockHttpServletRequest` 系列（此批未用）／**直接以真實 Spring 例外驅動處理器**（★ 非 MockMvc 替身）／
> **JDK 內建 `HttpServer` 起真實 IdP**（★ 模擬**外部**身分提供者，**非 mock 我方程式**）。
>
> ★★★ **真機反駁我方之假設（5 項，皆我方之錯）**：
> ① ★ **`TraceContext.MDC_KEY` 實為 `trace_id`**（非 `traceId`）；
> ② ★ **`CC-PLT-5002`（Saga 補償失敗）映射 `500`**（非 503）——★ **狀態由 `SDD-006 §3.4` 登錄表決定，不憑語感**；
> ③ ★ **Spring 7 之 `NoResourceFoundException(method, message, resourcePath)`**（★ 參數序與我原假設相反）；
> ④ ★★ **Keycloak 之 realm 角色在巢狀 `realm_access.roles`**（**非**頂層 `roles`）；
> ⑤ ★★ **`new JwtAuthenticationToken(jwt)` 之 `isAuthenticated()` 為 `false`**
>   （`AbstractAuthenticationToken` 預設），★ 且無授權項時 `setAuthenticated(true)` 會拋錯。
>
> ★ **殘餘未覆蓋 27 行／39 分支（逐條如實記錄）**：
> `GlobalExceptionHandler`（9 行）——★ **`ConstraintViolationException` 分支**（★ 本模組**未引入
> Bean Validation 實作**，`jakarta.validation-api` 僅有介面 → 無法構造真實違規物件）、
> ★ `HandlerMethodValidationException`（同上，需真實參數名）、
> ★ `requestError` 之 fallback（★ **所有呼叫端之錯誤碼皆已登錄** → 不可達）、
> ★ `EnvelopeAuthenticationEntryPoint`（**私有巢狀類別**，需真實 HTTP 401 流程）；
> `ClientCredentialsTokenProvider`（5 行）——★★ **雙重檢查鎖之內部快速路徑**
> （★ **僅並發競爭時可達**，單執行緒不可達）＋ `InterruptedException`（需中斷執行緒）；
> `ServiceAcl`（2 行）——`IOException`（`ClassPathResource` 之讀取失敗）；
> `SecurityAutoConfiguration`（4 行）——`spring.application.name` 未設定（需無該屬性之 context）
> ＋ `wwwAuthenticateOf`（私有巢狀）。

---

#### 6.11.37 ★★ 批次 20-f：`common-saga`（`#A-222`）★ **`WBS 5.37` 結案**

| 類別／模組 | 前 | **後** |
|---|---|---|
| `StateMachine` | 4/32 | ★ **未覆蓋 0** |
| `JdbcSagaStore` | 16/138 | ⚠️ **本批未執行**（需 Oracle；★ 已有 `JdbcSagaStoreIntegrationTest`，列後續） |
| `SagaCoordinator` | 3/132 | ★ **未覆蓋 0**（餘 3 為不可達分支） |
| `Transition`／`SagaInstance`／`SagaStepRecord`／`InMemorySagaStore`／列舉／`SagaResult` | 共 17 | ★ 未覆蓋 **0** |
| ★★ **`common-saga` 整體** | **行 36/416 ＝ 91.3%／分支 46/136 ＝ 66.2%** | ★★ **行 19/416 ＝ `95.4%`／分支 17/136 ＝ `87.5%`** |

> ★ **本批 +17 行、+29 分支**；★ **2 個測試類別／27 測**（★ **全模組 50 測／0 失敗／0 錯誤／0 略過**）。
> ★★ **`platform/` 8 模組：行 69.4% → `97.17%`；分支 59.1% → `92.99%`**。
> ★★ **全 backend：行 91.56% → `91.94%`；分支 74.86% → `76.18%`**。★ 掃描器**全 0 命中**。
> ★★★ **`WBS 5.37`（`platform/` 8 模組）結案**。
>
> ★★★ **真機反駁我方之假設（1 項）**：★ **`SagaInstance` 之長度截斷在「工廠／衍生方法」
> （`start()`／`withCurrentStep()`／`withFailureCode()`），★ 不在 record 之建構子**——
> ★ 且 `MAX_ERROR_MESSAGE` 為 **2000**（非我假設之 256）。
> ★ **教訓**：★ **「值物件之截斷」須查「哪一個入口」**——建構子只管 `requireNonNull`，
> 截斷可能只在下游入口。
>
> ★ **殘餘未覆蓋 19 行／17 分支**：★ **`JdbcSagaStore`（16 行）需 Oracle**
> （★ 已有整合測試，惟需容器；★ 列後續）；其餘 3 行為 `SagaCoordinator` 之防禦分支。

---

## 7. 修訂記錄

| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 3.13 | 2026-10-06 | ★★★ **新增 §6.11.36／§6.11.37 批次 20-e／20-f（`#A-221`／`#A-222`）——★★★ `WBS 5.37` 結案**：★★ **`common-spring` 行 87.4% → `94.9%`／分支 73.3% → `85.3%`**（+40 行／+32 分支，7 類別／56 測）；★★ **`common-saga` 行 91.3% → `95.4%`／分支 66.2% → `87.5%`**（+17 行／+29 分支，2 類別／27 測）。★★★ **`platform/` 8 模組：行 69.4% → `97.17%`、分支 59.1% → `92.99%`**；★★ **全 backend：行 `91.94%`、分支 `76.18%`**。★★★ **真機反駁我方 6 項**（`MDC_KEY` 實為 `trace_id`／`CC-PLT-5002` 映射 **500** 非 503／Spring 7 `NoResourceFoundException` 之**參數序**／**Keycloak realm 角色在 `realm_access.roles`**／**`JwtAuthenticationToken` 預設 `isAuthenticated=false`**／**截斷在工廠方法非建構子**）。★ 殘餘 **46 行／56 分支**逐條記錄（★ 多為「需 Oracle／Bean Validation 實作／私有巢狀／並發路徑」）。★ 掃描器**全 0 命中** |
| 3.12 | 2026-10-06 | ★★★ **新增 §6.11.35 批次 20-d（`#A-220`）——`common-security`；★★ 併立並執行 `CR-042`**：★★ **行 73.5% → `98.0%`、分支 58.5% → `98.3%`**（**+96 行／+70 分支**）；8 類別／86 測（全模組 137 測／0 失敗）。★★ **`platform/` 8 模組：行 → `94.83%`、分支 → `86.79%`**；★★ **全 backend：行 → `91.56%`、分支 → `74.86%`**。★★★ **揭露真缺陷並修補（`CR-042`）**：**`JwtAuthFilter.sendUnauthorized()` 必拋 NPE**——★ **`Map.of` 不接受 null 值**，而封套之 `data` 就是要為 null → ★★ **該過濾器自建立以來「未認證 → 401」從未真正執行過**；★ 查證**全庫零接線**（不影響現行系統，但為公開 API，接線即 500）→ ★ 改用 `LinkedHashMap`（JSON 形狀不變）。★ **修補前 14 測中 11 紅燈 → 修補後全綠**。★★ **配方**：`MockHttpServletRequest`／`MockHttpServletResponse` ＋ `FilterChain` lambda ＋ **真實 `JwtSigner`／`JwtValidator`** ＋ **自算 HMAC 手工組裝**。★★★ **真機反駁我方 1 項**：竄改 Base64 **末字元**低 2 位被丟棄 → 認證反而通過（★ **`#A-163` 同型陷阱再次踩到**）。★ 殘餘 8 行／3 分支**逐條記錄為不可達**。★ 全 backend 編譯驗證通過 |
| 3.11 | 2026-10-06 | ★★ **新增 §6.11.34 批次 20-c（`#A-219`）——`api-gateway`**：★★ **行 40.7% → `98.9%`、分支 50.0% → `95.2%`**（**+163 行／+57 分支**）；6 類別／51 測（全模組 79 測／0 失敗）。★★ **`platform/` 8 模組：行 → `90.88%`、分支 → `79.67%`**；★★ **全 backend：行 → `90.91%`、分支 → `73.35%`**。★★ **配方**：`MockServerWebExchange`（真實介面實作）＋ `GatewayFilterChain` lambda ＋ `@TempDir` 真實檔案 ＋ `InMemoryRouteDefinitionRepository`（**真件**）＋ `ContextRefresher` 最小子類。★★★ **真機反駁我方 4 項**（`ServerHttpSecurity.http()` 無 bean factory／WebFlux 解析 **`ReactiveJwtDecoder`** 非 `JwtDecoder`／`getAttribute` 泛型致 AssertJ 歧義／**`name != null` 為死碼——Reactor 禁止 `map` 回 null**）。★ 殘餘 3 行／6 分支**逐條記錄為不可達之防禦分支** |
| 3.10 | 2026-10-06 | ★★★ **新增 §6.11.31 計畫 ＋ §6.11.32 批次 20-a ＋ §6.11.33 批次 20-b——`platform/` 8 模組（`WBS 5.37`）**：★★★ **全 backend 行 87.42% → `89.82%`、分支 68.98% → `72.12%`——兩項門檻首次雙雙達標**（分支超出 **90 條**）。★ **20-a**：`common-domain`／`common-batch`／`common-event` **三模組行與分支皆 `100%`**（+56 行／+31 分支）。★★★ **20-b**：`bff-web` **行 30.1% → `97.3%`、分支 13.2% → `97.1%`**（**+304 行／+114 分支，單批最大增益**）；`platform/` 8 模組 → 行 **84.18%**／分支 **73.88%**。★★ **配方**：新增 `DownstreamStub`（**JDK 內建 `HttpServer`，零相依**）模擬下游同儕；★ 受測物與協作者**皆為真實物件**（**非 mock**）；★ 控制器以**真實聚合服務**注入。★★ **必驗**：**`DIFFERENCE` 歸 `WARNING`**（否則差異被忽略＝假綠）／**超界 limit 顯式報錯不夾取**／**保留下游原錯誤碼**／**未組態即不發請求**。★★ **真機反駁我方之錯 3 項**（`Set.of` 重複元素拋錯／`toString` 為主單位／`Map<?,?>` 型別捕獲）。★ 掃描器**全 0 命中**。★ 殘餘 12 行／4 分支**逐條如實記錄**（多為逾時 catch 之防禦分支） |
| 3.10 | 2026-10-06 | ★★★ **新增 §6.11.31 計畫 ＋ §6.11.32 批次 20-a ＋ §6.11.33 批次 20-b——`platform/` 8 模組（`WBS 5.37`）**：★★★ **全 backend 行 87.42% → `89.82%`、分支 68.98% → `72.12%`——兩項門檻首次雙雙達標**（分支超出 **90 條**）。★ **20-a**：`common-domain`／`common-batch`／`common-event` **三模組行與分支皆 `100%`**（+56 行／+31 分支）。★★★ **20-b**：`bff-web` **行 30.1% → `97.3%`、分支 13.2% → `97.1%`**（**+304 行／+114 分支，單批最大增益**）；`platform/` 8 模組 → 行 **84.18%**／分支 **73.88%**。★★ **配方**：新增 `DownstreamStub`（**JDK 內建 `HttpServer`，零相依**）模擬下游同儕；★ 受測物與協作者**皆為真實物件**（**非 mock**）；★ 控制器以**真實聚合服務**注入。★★ **必驗**：**`DIFFERENCE` 歸 `WARNING`**（否則差異被忽略＝假綠）／**超界 limit 顯式報錯不夾取**／**保留下游原錯誤碼**／**未組態即不發請求**。★★ **真機反駁我方之錯 3 項**（`Set.of` 重複元素拋錯／`toString` 為主單位／`Map<?,?>` 型別捕獲）。★ 掃描器**全 0 命中**。★ 殘餘 12 行／4 分支**逐條如實記錄**（多為逾時 catch 之防禦分支） |
| 3.9 | 2026-10-06 | ★★ **新增 §6.11.31 批次 20 計畫（`WBS 5.37`）**——★ **盤點 `platform/` 8 模組逐類別缺口**（未覆蓋 **745 行／402 分支**；與 `#A-216` 逐數字吻合）；★★ **關鍵算術**：**全 backend 分支達 70% 需再覆蓋 48 條**（4,623 × 0.70 ＝ 3,236.1）。★ 批次排序（20-a 純單元 → 20-b `bff-web` → 20-c gateway → 20-d security → 20-e spring → 20-f saga）。★★ **記載三項量測陷阱**：JaCoCo 報告**兩種 `Total` 格式**（模組層為 `bar` 之「N of M」文字、類別層為 `img title=`）／**類別列之 `ctr1/ctr2` 序**（行數取索引 4／5，**非** 2／3——2／3 是**複雜度**）／`</tfoot>` **在 `<tbody>` 之前** |
| 3.8 | 2026-10-06 | ★★★ **新增 §6.11.30 批次 17-d（`#A-213`）——`application` 層：`payment-service`（`WBS 5.36` 結案）**：★ `PaymentService`／`PaymentViewAssembler`／`AutoDebitService` **皆 `100%`**；`payment.application` 92/187 → **184/187 ＝ `98%`**；★★★ **`payment-service` 85.0% → `98.0%`（+92 行）**。★★★ **全服務行覆蓋 90.19% → `90.92%`**（11,397/12,535；**門檻超出 1,369 行**）；★★★ **分支 71.37% → `71.64%`**；**8 測**。★★ **配方**：真 Oracle ＋ 真實 JDBC 埠 ＋ **樁 HTTP 伺服器**（JDK 內建，模擬 `posting` 餘額端點）。★★★ **關鍵必驗**：**查無分戶（404）→ 全額歸本金＋標記未匹配（非故障）**；**沖正須發射 `reversed` 事件**（否則可用額度**永久高估**） |
| 3.7 | 2026-10-06 | ★★★ **新增 §6.11.29 批次 17-c-2（`#A-212`）——`LimitAdjustmentService`**：★ 該類別 0/56 → **46/56**（餘 10 為**不可達之防禦分支**）；`lim.application` 408/468 → **448/468 ＝ `96%`**；★★★ **`limit-service` 91.6% → `95.6%`（+51 行）**。★★★ **全服務行覆蓋 89.78% → `90.19%`**（11,305/12,535；**門檻超出 1,277 行**）；★★★ **分支 70.73% → `71.37%`**；**11 測**。★★★ **真機揭露**：`SPECIAL` 須先有**可設定上限政策**（`special_limit_policies`），無政策即 `CC-LIM-1002` 拒絕（**fail-closed 設計正確**）。★ 配方以**真件** `SecurityContextCallerIdentity` 為身分來源（無認證 → `UNVERIFIED`） |
| 3.6 | 2026-10-06 | ★★ **新增 §6.11.28 批次 17-c（`#A-211`）——`application` 層：`limit-service`**：★ `lim.application` 377/468 → **408/468 ＝ `87%`**（`LimitApplicationService` **45/46**）；★★ **`limit-service` 89.1% → `91.6%`（+31 行）**。★★ **全服務行覆蓋 89.53% → `89.78%`**（11,254/12,535；**門檻超出 1,226 行**）；★★ **分支 70.43% → `70.73%`**；**12 測**。★ **必驗**：可用額度**夾擠為 0**／不足即拒絕／**專用池回沖失敗仍成功**／`applicableSpecialLimit` 三分支／8-6 查無主檔**不落串動紀錄**／8-7 回收**不超額**。⚠️ **`LimitAdjustmentService`（46 行）本批未執行**（需 `CallerIdentityPort`），列後續 |
| 3.5 | 2026-10-06 | ★★★ **新增 §6.11.27 批次 17-b（`#A-210`）——`application` 層：`rewards-service`**：★ `rw.application` 289/387 → **357/387 ＝ `92%`**（`PointsViewAssembler`／`PointsAccountService` 皆 **`100%`**）；★★ **`rewards-service` 85.7% → `93.5%`（+85 行）**。★★★ **全服務行覆蓋 88.86% → `89.53%`**（11,223/12,535；**門檻超出 1,195 行**）；★★★ **分支 69.80% → `70.43%`——分支門檻正式達標**；**12 測**。★★★ **真機發現兩項**：① 同日到期之同階段桶 ≥2 → 分錄冪等鍵（**非以桶為主體**）碰撞 → `DuplicateKeyException` 未被 `catch (DomainException)` 攔住 → **整批中止 ＋ 留下孤兒桶**；② 首次累點（新帳戶）→ **先插桶後插帳戶** → `ORA-02291` FK 失敗。★ 皆如實記錄，**待裁定是否立 CR** |
| 3.4 | 2026-10-06 | ★★★ **新增 §6.11.26 批次 17-a（`#A-209`）——`application` 層：`party-service`**：★ 四類別全數大幅提升（`party.application` 30/156 → **151/156 ＝ `97%`**）；★★ **`party-service` 59.6% → `91.5%`（+189 行）**——★ **本計畫之最大單服務改善**。★★★ **全服務行覆蓋 87.35% → `88.86%`**（11,138/12,535；**門檻超出 1,110 行**）；★★ **分支 66.67% → `69.80%`（僅差 0.20 pp）**；**14 測**。★★★ **動查才知三項**：冪等表為 **`pty_idempotency`**；`save` 為**純 UPDATE ＋ 樂觀鎖**（★ 建列須原生 SQL）；`Party.reconstitute` 之可空參數序為 **`idNoEnc, mobileEnc, emailEnc`**（★ 皆 `byte[]` → **編譯期不報錯**，只有真機斷言才現形） |
| 3.3 | 2026-10-06 | ★★★ **新增 §6.11.25 批次 16（`#A-208`）——`storage` 層（`WBS 5.35` 結案）**：★ 兩服務之 `S3StorageConfig` **皆 `100%`**、`S3ObjectStorageAdapter` **各 `60/62 = 97%`**；`report` 74.1%→**84.5%**（+60）、`statement` 84.3%→**90.7%**（+60）。★★★ **全服務行覆蓋 86.39% → `87.35%`**（10,949/12,535；**門檻超出 921 行**）；★ **本批 +120 行**；**20 測**。★★★ **動查後修正：用 Garage，不是 MinIO**——★ 本專案之本地物件儲存即為 `garage`；★ MinIO 開源版已封存、**Docker Hub 映像已下架**（實測 `pull access denied`）→ 改以**專案同一映像與組態**（`dxflrs/garage:v2.4.1` ＋ `garage.toml`）起容器，★ **比 MinIO 更忠實**。★ 剩 2 行為 **404 相容分支** |
| 3.2 | 2026-10-06 | ★★★ **新增 §6.11.23 計畫 ＋ §6.11.24 批次 15 執行結果（`#A-207`）——`client` 層（`WBS 5.34` 結案）**：★ 5 個服務間 HTTP 客戶端 → **三套件 94～98%**（`auth` 134/137、`stmt` 71/74、`posting` 44/47）；`authorization` 72.7%→**83.2%**（+134）、`statement` 76.7%→**84.3%**（+71）、`posting` 86.8%→**92.1%**（+44）。★★★ **全服務行覆蓋 84.40% → `86.39%`**（10,829/12,535；**門檻超出 801 行**）；★ **本批 +249 行**；**31 測**。★★ **配方**：以 **JDK 內建 `HttpServer` 之樁伺服器**（零相依、**非 mock**）模擬同儕服務，受測物與協作者皆為真實物件 → 驗**真實 HTTP 往返**；**七項必驗**（對映／錯誤封套／非 200／404→empty／連線失敗保留 cause／請求內容／**分頁逾上限拋錯**）。★ 未覆蓋 9 行皆為**不可達之序列化 catch**。★ **再確認量測陷阱**：過濾跑後服務總數不可信 |
| 3.1 | 2026-10-06 | ★★★ **新增 §6.11.21 計畫 ＋ §6.11.22 批次 14 執行結果（`#A-206`）——`messaging` 層（`WBS 5.33` 結案）**：★ 7 個 Kafka 消費者（`limit` 4＋`auth` 3）→ **兩套件皆 `0/… → 100%`**（`limit` 89/89、`auth` 82/82）；`limit` 80.7%→**89.1%**（+105）、`authorization` 61.6%→**72.7%**（+144）。★★★ **全服務行覆蓋 82.42% → `84.40%`**（10,580/12,535；**門檻超出 552 行**）；★ **分支 64.33% → `66.67%`**。★ **本批 +249 行**（★ **遠超套件本身之 171 行**——**協同效應**）。★★ **配方**：以真實投影服務＋真實 JDBC 埠串鏈，直接呼叫 `onMessage(json)` 再查 DB；**四項必驗**（有效→生效／未知型別→不拋／壞 JSON→不拋／缺欄→不拋）。★★★ **真機揭露**：**部分釋放**時 `limit` 維持 `HELD`（`PARTIALLY_RELEASED` 不可達），而 `authorization` 讀模型寫 `PARTIALLY_RELEASED`——**跨服務不一致，如實記錄**。★ **併釐清 JaCoCo `append=true` 之量測陷阱** |
| 3.0 | 2026-10-06 | ★★★ **新增 §6.11.19 計畫 ＋ §6.11.20 批次 13 執行結果（`#A-205`）——`outbox` 橫向複製至其餘 10 服務**：★ 10 服務之 `outbox` 套件皆 **`0/28 → 100%`**（`KafkaEventPublisher` 14／`OutboxRelayScheduler` 12／`OutboxRelayConfig` 2）；★ **每服務恰 +28 行**（與預估一致）。★★★ **全服務行覆蓋 80.18% → `82.42%`**（10,331/12,535；**門檻超出 303 行**）；★ **分支覆蓋 63.78% → `64.33%`**（仍未達 70%）；**1,347 測／0 失敗／1 略過**（**+60 測**）；掃描器全 0 命中。★ 服務：`merchant` **98.9%**、`dispute` **96.5%**、`card` 92.4%、`product` 91.9%、`posting` 86.8%、`clearing` 86.0%、`notification`／`rewards` 85.7%、`statement` 76.7%、`authorization` 61.6%。★★ **方法論：動工前以「結構簽章抽取」驗證 API 一致（非目視 diff）**，據此才敢橫向複製 |
| 2.9 | 2026-10-06 | ★★★ **新增 §6.11.18 批次 12（`#A-204`）——`B3-b-2`：`outbox` 套件，★★ 行覆蓋突破 80%**：`limit`／`payment` 兩服務之 `KafkaEventPublisher` **0/14 → 100%**、`OutboxRelayScheduler` **0/12 → 100%**、`OutboxRelayConfig` **0/2 → 100%**、★ **`outbox` 套件整體 `0/28 → 100%`**；`limit` 78.5%→**80.7%**、`payment` 81.0%→**85.0%**。★★★ **全服務行覆蓋 79.74% → `80.18%`**（10,051/12,535；**門檻 10,028，超出 23 行**）——★ **行覆蓋目標達成**。★ 本批 **+56 行**；**1,287 測／0 失敗／1 略過**；掃描器全 0 命中。⚠️ **分支覆蓋 63.78%（門檻 70%）未達**。★★ **以兩個真容器（Oracle ＋ Kafka）串完整 relay 鏈路**，並自**真實消費端**讀回比對；★ 併驗「失敗即拋」「例外不得外溢」「中斷旗標須還原」。★★ **另立 `TRP-004_覆蓋率提升總結報告`（v1.0）**——B1～批次 12 之總結、真機反駁 13 項彙總、殘留缺口全貌（2,433 行）。★ 後續成本最低之批次：**`outbox` 橫向複製至其餘 10 服務（約 +280 行）** |
| 2.8 | 2026-10-06 | ★★★ **新增 §6.11.17 批次 11（`#A-203`）——大戶 jdbc 收尾，`WBS 5.25` 結案**：★ 7 個 jdbc 類別全數達 **`100%`**（含 `JdbcPaymentRepository` 61/84→84/84、`JdbcInstallmentRepository` 61/73→73/73、`JdbcAutoDebitRepository` 35/41→41/41、`JdbcReportJobRepository` 45/46→46/46、`JdbcAuthorizationSource` 3/13→13/13、dispute／merchant 之 `OracleRawId`）。★ 服務：`payment` 74.6%→**81.0%**、`installment` 78.4%→**82.3%**、`report` 72.2%→74.1%、`dispute` 91.4%、`merchant` 93.6%。★★ **全服務行覆蓋 79.1% → `79.74%`**（本批 **+74 行**；**距 80% 尚差 +33 行**）；**1,275 測／0 失敗／1 略過**；掃描器全 0 命中。★★★ **解決 `#A-200` 之誠實略過項**——**動查映像腳本**發現 `/container-entrypoint-initdb.d/*.sql` 係以 **`sqlplus / as sysdba`** 執行 → 以 `withCopyFileToContainer` 於初始化階段建 `CC_AUTH`（DDL 逐字抄自權威遷移），**僅授 `SELECT`**；★ 併查得 app user 僅具 `DB_DEVELOPER_ROLE`（不含 `CREATE USER`）＝原失敗之真因。★ 略過數 **2 → 1** |
| 2.7 | 2026-10-06 | ★★ **新增 §6.11.16 批次 10（`#A-202`）——大戶 jdbc：`product-service`**：★ 本服務 jdbc 原**僅涵蓋** `JdbcOutboxRepository`；`JdbcProductRepository` **0/38 → `100%`**、`JdbcParameterRepository` **0/31 → `100%`**、★ **`jdbc` 套件整體 `99/99 → 100%`**；`product-service` 行覆蓋 **59.0% → 82.4%**（**+23.4 pp**）。★★ **全服務行覆蓋 78.6% → `79.1%`**（本批 **+69 行**；**距 80% 尚差 +107 行**）；**1,261 測／0 失敗／2 略過**；掃描器全 0 命中。★★ **動查才知**：`PARAMETERS.param_value` 為 **Oracle 原生 `JSON`**（非 `VARCHAR2`）。★★★ **真機反駁（第七次）**：樂觀鎖衝突**不可**以「已落後物件 ＋ `withUpdated`」建構（`+1` 會補成「DB 現行 ＋ 1」→ 反而成功）；須以「**同版本、不同內容**」再送。★ 併記錯誤碼字面值為**連字號** `CC-PLT-5000`。★ 兩者已回寫技能（陷阱 6／7） |
| 2.7 | 2026-10-06 | ★★ **新增 §6.11.16 批次 10（`#A-202`）——大戶 jdbc：`product-service`**：★ 本服務 jdbc 原**僅涵蓋** `JdbcOutboxRepository`；`JdbcProductRepository` **0/38 → `100%`**、`JdbcParameterRepository` **0/31 → `100%`**、★ **`jdbc` 套件整體 `99/99 → 100%`**；`product-service` 行覆蓋 **59.0% → 82.4%**（**+23.4 pp**）。★★ **全服務行覆蓋 78.6% → `79.1%`**（本批 **+69 行**；**距 80% 尚差 +107 行**）；**1,261 測／0 失敗／2 略過**；掃描器全 0 命中。★★ **動查才知**：`PARAMETERS.param_value` 為 **Oracle 原生 `JSON`**（非 `VARCHAR2`）。★★★ **真機反駁（第七次）**：樂觀鎖衝突**不可**以「已落後物件 ＋ `withUpdated`」建構（`+1` 會補成「DB 現行 ＋ 1」→ 反而成功）；須以「**同版本、不同內容**」再送。★ 併記錯誤碼字面值為**連字號** `CC-PLT-5000`。★ 兩者已回寫技能（陷阱 6／7） |
| 2.6 | 2026-10-06 | ★★ **新增 §6.11.15 批次 9（`#A-201`）——大戶 jdbc：`migration-service`**：★ **本服務先前無 jdbc 整合測試**；`JdbcLegacyAccountRepository` **0/21 → `100%`**、`JdbcMigrationWaveRepository` **0/15 → `100%`**、`JdbcMigrationJobRepository` 36/37；`migration-service` 行覆蓋 **60.5% → 92.1%**（★ **+31.6 pp，單服務最大跳幅**）。★★ **全服務行覆蓋 77.5% → `78.6%`**（本批 **+134 行**）；**1,249 測／0 失敗／2 略過**；掃描器全 0 命中。★★ **動查才知**：表名為 `legacy_account_mappings`（**非** `LEGACY_ACCOUNTS`）。★ **真機反駁（第六次）**：三個列舉皆為**巢狀**（非獨立類別） |
| 2.5 | 2026-10-06 | ★★ **新增 §6.11.14 批次 8（`#A-200`）——大戶 jdbc：`report-service`**：★★ **本類別用「兩個容器」**（Oracle ＋ PostgreSQL，因本服務 jdbc **橫跨兩個資料源**）；`JdbcAuthSummaryReadModel` **0/21 → `100%`**、`JdbcReportDefinitionRepository` **0/14 → `100%`**、`JdbcReportJobRepository` 35/36；`report-service` 行覆蓋 **47.8% → 72.2%**。★★ **全服務行覆蓋 76.6% → `77.5%`**（本批 **+117 行**）；**1,239 測／0 失敗／2 略過**；掃描器全 0 命中。⚠️ **`JdbcAuthorizationSource` 跨 schema 測試因容器 app user 無 DBA 而誠實略過**（10 行未覆蓋，列後續）。★★ **真機反駁（第五次）**：`ReportJob` 狀態機為 **DAG**（`complete`／`fail` 皆要求 `RUNNING`；`COMPLETED` 不得再 `start`） |
| 2.4 | 2026-10-05 | ★★ **新增 §6.11.13 批次 7（`#A-199`）——大戶 jdbc：`notification-service`**：5 個 jdbc 類別中 **4 個達 100%**；`JdbcNotificationRepository` 37/39；`notification-service` 行覆蓋 **71.0% → 83.1%**。★★ **全服務行覆蓋 75.7% → `76.6%`**（本批 **+118 行**）；**1,226 測／0 失敗／1 略過**；掃描器全 0 命中。★★★ **真機反駁（第四次）**：`OracleRawId.toUuid` 對壞長度**拋 `IllegalStateException`（fail-closed）**，非回 null |
| 2.3 | 2026-10-05 | ★★★ **新增 §6.11.11 批次 6（`#A-198`）——大戶 jdbc：`limit-service`，★ 併結案 `WBS 5.13`（B1-b2）**：兩個付款投影 repository **各 0/17 → `100%`**；`limit-service` 行覆蓋 **73.1% → 78.5%**。★★ **全服務行覆蓋 75.2% → `75.7%`**（本批 **+53 行**）；**1,215 測／0 失敗／1 略過**；掃描器全 0 命中。★★★ **新增 §6.11.12：核心不變式「已記＝已套用」**——套用失敗時**不得**寫 `consumed_events`（否則重放永久漏套用）。★ 以**真實 `JdbcLimitRepository`** 為協作者（無 mock） |
| 2.2 | 2026-10-05 | ★★ **新增 §6.11.10 批次 5（`#A-197`）——大戶 jdbc：`clearing-service`**：`JdbcClearingRunRepository` **0/33 → `100%`**、`JdbcSettlementRepository` **0/33 → `100%`**；`clearing-service` 行覆蓋 **65.7% → 81.5%**。★★ **全服務行覆蓋 74.4% → `75.2%`**（本批 **+99 行**）；**1,202 測／0 失敗／1 略過**；掃描器全 0 命中。★ 動查發現 `SETTLEMENTS.DIRECTION` 係 **V2 新增**（V1 無） |
| 2.1 | 2026-10-05 | ★★ **新增 §6.11.9 批次 4（`#A-196`）——大戶 jdbc：`party-service`**：`JdbcAccountRepository` **0/32 → `100%`**、`JdbcPartyRepository` **37/41**（餘 4 為 `parseFlags`／`serializeFlags` 之**不可達 catch**）；`party-service` 行覆蓋 **44.0% → 59.6%**。★★ **全服務行覆蓋 73.7% → `74.4%`**（本批 **+92 行**）；**1,194 測／0 失敗／1 略過**；掃描器全 0 命中。★ 驗證 `flags` JSON 往返、兩處樂觀鎖、`BLOCK_CODE_HISTORY` append-only、他歸戶排除 |
| 2.0 | 2026-10-05 | ★★★ **新增 §6.11.7 批次 3（`#A-195`）——大戶 jdbc 之首發**：`authorization-service` 之 `JdbcAuthorizationRepository`（**162 行，全庫最大單一 jdbc 缺口**）→ ★★ **162/162 ＝ `100%`**；`authorization-service` 行覆蓋 **43.5% → 59.4%**。★★ **全服務行覆蓋 72.1% → `73.7%`**——★ **本批 +195 行（單批最大增益）**；**1,187 測／0 失敗／1 略過**。★★★ **新增 §6.11.8：實測揭露三項我方語意錯誤**（★ 真機反駁：DECLINED 不佔額度／`PendingStandIn.authorizationId` 非 holdId／`IS NOT NULL` 條件）。★ **協同效應兌現**（併同覆蓋多個 model 之 `reconstitute`／存取子） |
| 1.9 | 2026-10-05 | ★★ **新增 §6.11.6 批次 2（`#A-194`）**：**`B3-b-1` 之 scheduler 層全數完成**——**8 個排程類別 88/88 → `100%`**（8 個測試類別、17 測）。★★ 核心測試點：**例外不得外溢**（Spring `@Scheduled` 拋例外將**取消後續排程**）；併測 limit 之**兩個 try/catch 各自獨立**與 report 之 **`daysBack` 下限 1**。★ **全服務行覆蓋 71.4% → 72.1%**；**1,170 測／0 失敗／1 略過**。 |
| 1.8 | 2026-10-05 | ★★ **新增 §6.11 續推計畫（`B4`＋`B3-b`＋大戶）**（`#A-192`）：★ **先更正前輪「jdbc 層幾已見底」之錯誤敘述**（實為仍有 **935 行**）；★ **精確盤點**（`model` 393／`application` 814／`scheduler` 88／`storage` 118／`messaging` 217／`client` 251／`jdbc` 935）；★ **批次排序依「每行覆蓋之成本」**；★ **達 80% 尚需 +1,219 行**。★★ **新增 §6.11.5 批次 1 結果**（`#A-193`）：`party.Party`＋`rewards.PointsAccount` 之純領域分支——`party` **23.7% → 44.0%**、`rewards` **79.8% → 82.1%**；★ **全服務行覆蓋 70.3% → 71.4%**。★ **併載「jdbc 與 model 之協同效應」洞察**。⚠️ **如實揭露本輪僅完成 `B4-a` 之 2 個類別（96／393 行）** |
| 1.7 | 2026-10-05 | ★★ **新增 §6.10 W4：`rewards`／`card` 之 `jdbc` 層真機整合測試**（`#A-190`／`#A-191`）——**2 類別／39 測**；★ 9 個 jdbc 類別中 **6 個達 100%**；`rewards` 行覆蓋 **58.7% → 79.8%**、`card` **70.6% → 89.4%**；★★ **全服務行覆蓋 67.0% → 70.3%**。★★★ 併載 **§6.10.3 實測揭露之三項真實行為**（★ **修正我三個錯誤假設**：`points > 0` 之 CHECK、`save` 不寫 `card_id`、`Timestamp.from` 遺失 offset）與一項**一致性瑕疵**（`findAvailable` 未修剪 `CHAR(8)`，惟**無功能影響**）。⚠️ **併如實揭露本文初稿之預填失誤**（§6.10.5）。 |
| 1.6 | 2026-10-05 | ★★ **新增 §6.9 W3：`statement`／`posting` 之 `jdbc` 層真機整合測試**（`#A-188`／`#A-189`）——**2 類別／21 測**；★ **四個 jdbc 類別全數 100%**；`statement` 行覆蓋 **51.7% → 73.7%**、`posting` **64.8% → 83.5%**；★★ **全服務行覆蓋 64.1% → 67.0%**。★ 併載 **§6.9.3 防假綠設計**（6 個查詢之近似情境）與 **§6.9.4 實測揭露兩項事實**（`idempotency_key` **全域 UNIQUE** 使 `findDuplicates` 至多 1 筆——★ **初版測試因此被真機反駁**；`value_date` 為 `NOT NULL`）。 |
| 1.5 | 2026-10-05 | ★★★ **新增 §6.8 B2 規模化結果**（`#A-182`／`#A-183`）：**回頭補 `AuthorizationController` 76 行 → 0**（行覆蓋 **100%**；服務 **36.3% → 43.5%**）＋ **橫向複製至 16 服務、25 個 `@WebMvcTest`、130 條測試**（★ **範圍由 11 更正為 16**）。★★★ **實測揭露三個真缺陷**：**`#A-184`**（`ResponseStatusException` 未處理 → 查無卡片應 404 實回 **500**）、**`#A-185`**（★ **Jackson 3 預設 `FAIL_ON_NULL_FOR_PRIMITIVES=true`** → 省略 primitive 欄位即 400）、**`#A-186`**（部分 `@RequestBody` 缺 `@Valid` → 巢狀驗證不生效）。 |
| 1.4 | 2026-10-05 | ★ **新增 §5.5 W1-a（`installment-service`）實測**（`#A-170`）：**行覆蓋 47.8% → 73.3%**、`JdbcInstallmentRepository` **0/73 → 60/73**；★ **測試發現 `update()` 具樂觀鎖**；⚠️ **其 version 約定未查明 → 成功路徑測試暫緩並誠實註記**（列 W1-a2）。★ **`pricing-service` 因 `PRICING_RULES` 建表位置與巢狀列舉未查明 → 列 W1-b**。 |
| 1.3 | 2026-10-05 | ★★ **新增 §7 B1-c 執行計畫**（`#A-169`）：**10 服務盤點**（未覆蓋 1,675 行／jdbc public 方法 4～41）；★★ **排序原則「先低複雜度，不先大缺口」**（W1 最低複雜度 → W5 `authorization` 最後）；★★ **誠實推估「只做完 `jdbc` 層仍到不了 80%」**（約 67%，**必須 B2＋B3 一併**）；**可複製配方**。 |
| 1.2 | 2026-10-05 | ★ **新增 §5.1 B1-b 實測增益**（`#A-168`）與 **§5.3 兩個實測收穫**、**§5.4 未完成項**。★ 修訂**誠實推估**為「**一個服務 ≈ +2.0 pp**」→ 達 80% 約需 **20～22 個服務**同等規模。 |
| 1.1 | 2026-10-05 | ★ **新增 §5.1 B1-a 實測增益**（`#A-167`）與 **§5.2 實作要點**（含 **Spring Boot 4 測試切片已模組化**、**Testcontainers Oracle 套件名**、**映像須用 gvenzl** 三個實測發現）。★ 併載**誠實推估**：**一個測試類別 ＝ +1.0 pp**，達 80% 需 **約 25～27 個**同規模類別。 |
