Project

General

Profile

CR #201 » TRP-003_覆蓋率提升計畫.md

內容更新:內部稱謂去識別化(2026-10-18) - Redmine Admin, 10/18/2026 05:32 AM

 

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):

@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)皆為真實物件,
未以任何替身取代我方程式。

接線(實查):

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)
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 加密器串起應用鏈路:

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 個同規模類別。
(17-17/67)