Project

General

Profile

CR #201 » SCS-009_分期服務程式設計規格.md

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

 

SCS-009 分期服務(installment-service)程式設計規格

項目 內容
文件編號 SCS-009
版本 1.6(CR-043 §2.4:InstallmentPeriod.principalAmount 補契約與回應——原僅存於領域模型(回應 PeriodView 未暴露、契約未宣告);現補契約 schema + PeriodView + InstallmentViewAssembler;契約重生 0 錯誤;verify_contract_dto 不符 0 處)。前版 1.5(§9.4 WebUI 端到端實測重跑:真實瀏覽器+Keycloak 登入,7 情境全數成立,含雙軸下拉與「未記錄」;原 1.4「CR-008:通路/業務型態雙軸拆分完成,並經真實 Oracle 端到端實測)
狀態 已核定
對應需求 ISS-2.7(信用卡分期)
服務埠號 8089(CSP-006 §4;領域服務 8081–8089)
套件根 com.beam.cardcore.inst
權威 schema CC_INST(Oracle 26ai Free/PDB FREEPDB1)
關聯文件 SDD-001 §3.9(1.5)、SDD-003 §12、SDD-005 §6(1.2)、SDD-006 §3.4、SDD-007 §5.4.8(1.2)、GOV-005、CR-002、CR-008

本版差異:1.0 為「規格先行」版(埠號與類別名為規劃值,且 §1.1 之章節引用有誤)。
1.1 依實作結果與真實 Oracle 實測回填,並修正章節引用。
1.2 依 CR-002 修正契約值域(installmentType 原列舉 6 值實測全部不可用),
補本地種子檔,並更正 §10.1 之來源計數錯誤。
1.3 SDD-005 1.2 已完成 §6 狀態機更正(移除無法持久化之 PENDING/REJECTED、
SETTLED/EARLY_SETTLED 更名),值域四方(BRD/ER-DDL/狀態機/契約)全部對齊;
本節 §10.1 隨之改為「已全部對齊」。
1.4 依 CR-008(BEAM PMO 2026-09-20 核定)完成雙軸拆分:
installment_type → channel_type(來源通路,值域擴充為 7 值),
新增 business_type(業務型態,BRD §5.6 之 3 值,允許 NULL)。
領域列舉 InstallmentType → ChannelType + 新增 BusinessType;契約、種子、
前端同步;並以 V3__installment_channel_business_type.sql 遷移。
§10.2 之建模缺陷與 §10.7 #16「業務型態無處儲存」皆已解除。


1. 對應需求

FR 編號 需求名稱 來源
ISS-2.7 信用卡分期 BRD-002

1.1 上層設計依據(本規格不重複這些圖,僅引用)

圖種 文件 本服務相關
ER-Diagram SDD-001_領域模型與ER-Diagram §3.9 installment——INSTALLMENT_PLANS/PERIODS/PRODUCTS
DFD SDD-002_DFD與流程分解 分期處理流
Sequence SDD-003_Sequence-Diagrams §12 分期建立與佔用
狀態機 SDD-005_狀態機設計 ✅ §6 分期計畫狀態機(8 條轉換,7 個狀態)
API Contract SDD-006 + contracts/openapi/installment-service.yaml 本服務 3 支端點

⚠️ 1.0 之章節引用有誤,已更正:原引用 SDD-001 §5.5(SDD-001 只有 §3.x,無 §5.5)
與 SDD-003 §7(實為「繳款即時恢復額度」,非分期)。此為懸空引用,
與 rewards 域(SDD-001 §5.7)同類,屬章節重編後未同步之歷史遺留。


2. 服務職責與邊界

承作:

  • 分期計畫(INSTALLMENT_PLANS:商戶/銀行/循環/帳單分期)
  • 分期期數攤還(INSTALLMENT_PERIODS:每期本金/手續費/到期日/剩餘本金)
  • 分期產品參數(INSTALLMENT_PRODUCTS:期數上下限、費率、適用商戶)
  • 分期結清(提前清償/違約處理)

不做(屬他服務):

  • 實際入帳(每期扣款)→ posting-service
  • 費率試算 → pricing-service
  • 產品主檔 → product-service

3. 領域模型(實作檔案為準)

類別 職責 不變式
InstallmentPlan(聚合根) 分期計畫;含攤還試算 各期本金加總恰等於原始金額;originalAmount > 0;periodCount ∈ {3,6,12,24};僅 ACTIVE 可結清
InstallmentPeriod 單期攤還 periodNo ≥ 1;amount ≥ 0;remainingPrincipal 遞減至末期恰為 0。★ 欄位:periodNo/amount/principalAmount/feeAmount/dueDate/status/remainingPrincipal(CR-043 §2.4:principalAmount 原僅存於本領域模型,契約漏宣告,本次併補契約與回應)
ChannelType(CR-008 由 InstallmentType 改名) 來源通路(誰提供,DDL 7 值) MERCHANT/BANK/REVOLVING/BILL/ACQUIRER/NCCC/QRCODE;必填
BusinessType(CR-008 新增) 業務型態(怎麼發起,BRD-001 §5.6 3 值) SINGLE/RECURRING/STATEMENT;可為 null(當初未記錄,合法狀態)
PlanStatus 計畫狀態(DDL 5 值) ACTIVE/COMPLETED/PRE_CLOSED/DEFAULTED/CANCELLED;僅 ACTIVE 可結清
PeriodStatus 期數狀態(DDL 5 值) DUE/PAID/OVERDUE/WAIVED/REVERSED
SettlementType 結清類型(契約列舉) EARLY/DEFAULT;未定義值一律拒絕

⚠️ PlanStatus 之值域與 SDD-005 §6 狀態機、契約 InstallmentView.status 皆不同,
三方不一致見 §10.1。


4. 程式結構(實作檔案為準)

套件/類別 職責 對應需求
domain/model/InstallmentPlan InstallmentPeriod 聚合根與期數 ISS-2.7
domain/model/ChannelType BusinessType PlanStatus PeriodStatus SettlementType 值物件/列舉 —
domain/port/InstallmentRepository 持久化埠 —
application/InstallmentService 成立/查詢/結清 ISS-2.7
application/InstallmentViewAssembler 檢視組裝 —
infrastructure/jdbc/JdbcInstallmentRepository OracleRawId JDBC 實作(含樂觀鎖) —
interfaces/rest/InstallmentController REST 端點(3 支) ISS-2.7

5. 資料模型(約束為 Oracle 實測值)

5.1 V1 基線(3 表,V1__baseline.sql)

表 關鍵欄位 約束(實測)
INSTALLMENT_PLANS PLAN_NO VARCHAR2(24) UNIQUE;ORIGINAL_AMOUNT NUMBER(19);PERIOD_COUNT NUMBER(5);PER_PERIOD_AMOUNT/FIRST_PERIOD_AMOUNT/LAST_PERIOD_AMOUNT;FEE_RATE/ANNUAL_RATE NUMBER(9,6);STATUS DEFAULT 'ACTIVE';PAID_PERIODS/REMAINING_PERIODS;PRINCIPAL_OUTSTANDING ⚠️ installment_type 已於 V3 改名為 channel_type(值域擴充為 7 值);original_amount > 0;period_count BETWEEN 1 AND 60;per_period_amount > 0;status IN ('ACTIVE','COMPLETED','PRE_CLOSED','DEFAULTED','CANCELLED')
INSTALLMENT_PERIODS PLAN_ID(FK→PLANS.ID);PERIOD_NO NUMBER(5);AMOUNT;PRINCIPAL_AMOUNT;FEE_AMOUNT;DUE_DATE;REMAINING_PRINCIPAL;STATUS DEFAULT 'DUE' period_no >= 1;amount >= 0;status IN ('DUE','PAID','OVERDUE','WAIVED','REVERSED');FK(PLAN_ID)
INSTALLMENT_PRODUCTS PRODUCT_CODE VARCHAR2(16) UNIQUE;MIN_PERIODS/MAX_PERIODS;FEE_RATE/ANNUAL_RATE;MERCHANT_ELIGIBLE;STATUS ⚠️ installment_type 已於 V3 改名為 channel_type(7 值);status IN ('ACTIVE','INACTIVE')

含 CREATED_AT/UPDATED_AT TIMESTAMP(6) WITH TIME ZONE、VERSION 樂觀鎖。

5.2 V2(V2__installment_plan_no_seq.sql)

CREATE SEQUENCE INSTALLMENT_PLAN_NO_SEQ——分期編號須連號可稽核
(同 posting/statement/payment/rewards 之理由),不得以隨機值替代。

5.3 V3(V3__installment_channel_business_type.sql)——CR-008 雙軸拆分

表 變更 遷移後(Oracle 實測)
INSTALLMENT_PRODUCTS installment_type → channel_type;新增 business_type CHANNEL_TYPE VARCHAR2(16 CHAR) NOT NULL;BUSINESS_TYPE VARCHAR2(16 CHAR) NULL
INSTALLMENT_PLANS 同上 同上

實測約束(具名,非系統自動命名):

約束 條件
CK_INST_PROD_CHANNEL / CK_INST_PLAN_CHANNEL channel_type IN ('MERCHANT','BANK','REVOLVING','BILL','ACQUIRER','NCCC','QRCODE')
CK_INST_PROD_BUSINESS / CK_INST_PLAN_BUSINESS business_type IS NULL OR business_type IN ('SINGLE','RECURRING','STATEMENT')

⚠️ 為何以動態 PL/SQL 卸除舊約束:V1 以行內 CHECK 建表,Oracle 給了
系統自動命名之約束(實測 SYS_C0010885/SYS_C0010896)。若硬寫這些名字,
遷移會綁死在特定環境。且該欄實測有 2 個 constraint_type='C' 約束
(NOT NULL + 值域 CHECK),只刪一個會留下舊值域——故依
user_constraints ⋈ user_cons_columns 動態定位並全部卸除,再顯式重建。
約束名刻意 ≤30 字元,相容 12.2 以前之上限。

5.4 實測物件清單(CC_INST)

  • 表 4:INSTALLMENT_PLANS、INSTALLMENT_PERIODS、INSTALLMENT_PRODUCTS、flyway_schema_history
  • 序列 1:INSTALLMENT_PLAN_NO_SEQ
  • ⚠️ 本服務無本地種子檔(ops/local/init/oracle/ 僅 02/04–06/08/09),
    CC_INST 之資料係測試過程建立(見 §9.3)

6. API 設計(來自 contracts/openapi/installment-service.yaml)

端點 方法 說明 對應 FR
/api/v1/installments POST 分期計畫成立(含攤還表) ISS-2.7
/api/v1/installments/{planId} GET 計畫查詢(含期數) ISS-2.7
/api/v1/installments/{planId}/settlements POST 分期結清(提前清償/違約) ISS-2.7

必填標頭(兩個 POST):X-Channel、X-Idempotency-Key(長度 8–120)。
回應信封:{traceId, data, error, success};成功之 POST 回 201。

★ 期數欄位(CR-043 §2.4):InstallmentView.periods[] 之每期含
periodNo/amount/principalAmount/feeAmount/dueDate/status/remainingPrincipal。
★ principalAmount 原僅存在於領域模型(回應 PeriodView 未暴露、契約未宣告)→ 本次併補契約與回應
(PeriodView + InstallmentViewAssembler)。★ 現行實作之 amount 與 principalAmount 同值
(DDL AMOUNT/PRINCIPAL_AMOUNT 分列;生成器 InstallmentPlan 以同一 principal 填入兩者)。

6.1 錯誤碼(SDD-006 §3.4:本服務服務代碼為 INS)

錯誤碼 HTTP 觸發情境 實測
CC-INS-1001 422 期數不支援(非 3/6/12/24) ✅
CC-INS-1002 422 總約未簽署 ✅
CC-INS-3001 409 計畫非 ACTIVE 仍嘗試結清 ✅
CC-INS-4000 400 請求驗證錯誤(含未定義之來源通路/業務型態/結清類型;channelType 為 null) ✅
CC-INS-4040 404 查無分期計畫 ✅
CC-INS-3002 409 終態不可轉換(分期已結清仍嘗試攤還) — 未實作(無攤還端點)

⚠️ 缺必填標頭或 Bean Validation 失敗時回 500(契約要求 400)——平台缺口,見 §10.6。


7. 關鍵邏輯

  • 攤還試算(成立時一次算完):每期本金=originalAmount ÷ periodCount(整數除),
    尾差由末期吸收(如 100,000 ÷ 3 → 33,333/33,333/33,334);
    手續費依費率計算後平均分攤,尾差同樣由末期吸收。
    • 手續費率本期為固定值 0.005(InstallmentService.DEFAULT_FEE_RATE),
      待 pricing-service 費率引擎就緒後改為參數化(見 §10.5)。
    • 剩餘本金=原始金額 −(至本期為止累計已攤本金),故末期恰為 0。
      不可寫成 originalAmount − perPeriodAmount × i(未計入末期尾差 → 末期殘留尾差金額)。
  • 期數值域:僅 3/6/12/24(契約列舉),非 DDL 之 BETWEEN 1 AND 60(見 §10.3)。
  • 分期類型:以 DDL 4 值為準(MERCHANT/BANK/REVOLVING/BILL),
    非契約之 SINGLE/RECURRING/…(見 §10.2)。
  • 結清:settlementType 僅 EARLY/DEFAULT;未定義值一律以 CC-INS-4000 拒絕。
    僅 ACTIVE 可結清(CC-INS-3001)。
    • EARLY → PRE_CLOSED、earlySettlementFlag=true、剩餘期數與剩餘本金歸零
    • DEFAULT → DEFAULTED
    • 本期僅改變狀態,未計算結清金額(剩餘本金+違約金)亦未減免後續手續費(見 §10.5)。
  • 冪等:由 X-Idempotency-Key 承載(契約要求);⚠️ 本服務尚未落庫冪等鍵表
    (見 §10.5)。
  • 樂觀鎖:UPDATE … WHERE ID = ? AND VERSION = ?。

8. 外部依賴與降級

依賴 失敗處理(Stand-in)
pricing-service(費率) 未接;本期費率固定 0.005
posting-service(每期入帳) 未接;每期扣款排程未實作(見 §10.5)

⚠️ 1.0 所述之「經事件協調、入帳失敗重試」目前無實作可驗證。


9. 測試與驗收(TRP-001)

9.1 單元測試(./gradlew :services:installment-service:test)

33 tests, 0 failures(實測;1.3 版為 22)

測試類別 案例數 對應
InstallmentPlanTest 13 攤還試算、尾差、剩餘本金遞減至零、期數驗證、結清、+ CR-008 雙軸 3 例(正交可組合/businessType 未記錄應為 null/channelType 不得為空)
ChannelTypeBusinessTypeTest(1.4 新增) 8 通路軸 7 值、業務型態軸 3 值、業務型態值不得被通路軸接受(CR-002 根因回歸)、fromCodeOrNull 之 NULL 與未知值分流
SettlementTypeTest 8 結清類型值域(含 2 項缺陷回歸測試)
LayeringTest(ArchUnit) 4 CSP-002 §6.3

負樣本實測(1.4):刻意把 SINGLE 加回 ChannelType、並讓
BusinessType.fromCodeOrNull 對未知值靜默回 null,結果精準轉紅 3 例
(業務型態值不得被通路軸接受、未知業務型態碼不得靜默視為未記錄、通路軸應為七值),
其餘 30 例不受影響——證明新測試有判定力,非「怎麼改都綠」。

9.2 品質關卡(正規範圍:逐服務 + 只掃 db/migration/)

掃描器 範圍 結果
scan_scaffold.py backend/services/installment-service 0 命中
scan_mockdata.py 同上 0 命中
scan_faketest.py 同上 0 命中
validate_sql.py …/db/migration 0 違規(2 檔)
validate_mermaid.py docs 0 錯誤

⚠️ 1.0 交付時未執行品質關卡。本版補跑時 scan_scaffold 命中 1 處
(InstallmentPlan.Builder() 空無參建構子)——與本輪修改無關,屬既有遺留,
已依專案既有模式修正(預設值移入建構子本體)。此為第 5 次遇到同一模式。

9.3 真實 Oracle 實測(8089,FREEPDB1)

# 情境 期望 實測結果
1 POST 建立分期(120,000 分/12 期/MERCHANT) 攤還表 12 期 201,INST2026090007,每期 10,000、總手續費 600 ✅
2 各期本金加總不變式 =原始金額 Σ = 120000 ✅
3 尾差由末期吸收(100,000 分/3 期) 333.33/333.33/333.34 ✅
4 末期剩餘本金(同上) 0 0.00 ✅(修正前為 0.01,見 §10.4)
5 GET 查詢分期 200,含 12 期 ✅
6 POST 結清 EARLY PRE_CLOSED ✅
7 已結清再結清 409 CC-INS-3001 ✅
8 期數 7 422 CC-INS-1001 ✅
9 總約未簽署 422 CC-INS-1002 ✅
10 查無計畫 404 CC-INS-4040 ✅
11 結清類型未定義 FOO 400 CC-INS-4000,且不得結清 ✅ 400;計畫仍為 ACTIVE(修正前為 201 且靜默結清,見 §10.4)
12 結清類型小寫 early 容忍大小寫 → PRE_CLOSED ✅
13 分期類型用契約值 SINGLE 400 CC-INS-4000 ✅
14 V3 遷移(Flyway 於 8089 啟動時套用) channel_type NOT NULL、business_type 可 NULL,四個具名 CHECK 成立 ✅ Flyway Successfully applied 1 migration … now at version v3;CHANNEL_TYPE N/BUSINESS_TYPE Y;CK_INST_{PLAN,PROD}_{CHANNEL,BUSINESS} 皆存在
15 雙軸寫入:MERCHANT + STATEMENT 201,且落庫為兩欄 ✅ INST2026090006 → DB MERCHANT/STATEMENT,攤還 12 期
16 業務型態省略(channelType=QRCODE,不帶 businessType) 201,DB business_type 為 NULL ✅ INST2026090007 → DB QRCODE/<NULL>,攤還 6 期
17 查詢既有計畫(種子 0003,業務型態未記錄) businessType 回傳 null ✅ channelType=BANK、businessType=null(非空字串、非預設值)
18 誤將業務型態值填入通路軸(channelType=SINGLE) 400 CC-INS-4000 ✅ 未知分期來源通路:SINGLE —— CR-002 根因仍被正確擋下

9.4 WebUI 端到端實測(SDD-007 §5.4.8)

實測路徑:瀏覽器 → Vite 5173 → /installment-api 代理(rewrite 去前置)→ 8089 → Oracle CC_INST。

9.4.1 情境與結果(2026-09-21 14:12–14:22 重跑,真實瀏覽器 + Keycloak 登入)

# 情境 實測結果
1 建立分期(1,000.00 元/3 期,MERCHANT+SINGLE) INST2026090008;攤還表 3 期:本金 333.33/333.33/333.34,剩餘本金 666.67/333.34/0.00 ✅(手續費 1.66/1.66/1.68=5.00,尾差亦由末期吸收)
2 執行結清 EARLY 狀態轉 PRE_CLOSED(畫面「提前結清」),提示「已完成提前結清(PRE_CLOSED)」;DB 複核 remaining_periods=0 ✅
3 總約未簽署 畫面顯示 CC-INS-1002「總約未簽署,不得成立分期」 ✅
4 雙軸下拉(CR-008) 「來源通路」7 值(MERCHANT/BANK/REVOLVING/BILL/ACQUIRER/NCCC/QRCODE);「業務型態」3 值 +「(不指定 → 記錄為「未記錄」)」 ✅ 與 SDD-007 §5.4.8 一致
5 業務型態選「不指定」→ 顯示「未記錄」 建立 INST2026090009;畫面業務型態顯示 badge badge--muted :: 未記錄(灰色標記,非 SINGLE)✅
6 重新查詢(GET)後仍為「未記錄」 INST2026090009 重新查詢 → 業務型態仍顯示「未記錄」✅ —— 證明後端確實存 NULL,非畫面暫時狀態
7 DB 直接複核(sqlplus) select plan_no, channel_type, nvl(business_type,'<NULL>'):
INST2026090009 MERCHANT <NULL> ✅
INST2026090008 MERCHANT SINGLE ✅
狀態:INST2026090008=PRE_CLOSED/remaining_periods=0、INST2026090009=ACTIVE/3 ✅

9.4.2 本次實測之姿勢(誠實揭露)

項目 本次 說明
認證 ✅ 啟用(SSO) 以 Keycloak demo.csr(cardcore-csr)經 PKCE 登入後操作;後端 installment-service 亦為啟用態(8089 無憑證回 401)
使用者 token 與服務間 ACL 放行 使用者 token 之 azp=cardcore-web(非 cc-),依 CR-011/SCS-022 §7.10 不受服務間 ACL 管制 ✅ 與設計一致
前端建置 ✅ npm run typecheck 0 錯誤;npm run build 成功(101 modules,289.74 kB/gzip 83.98 kB)

⚠️ 1.4 版「尚未重跑瀏覽器實測」之待辦,本次解除——#1–3 既有情境已於真實瀏覽器重跑並仍成立,
且「雙軸下拉與空值顯示」(#4–6)亦已實際操作驗證,並以 DB 直接查詢佐證(#7)。

9.4.3 截圖存證(docs/08-evidence/)

檔案 內容
webui-installments-01-dual-axis-form.png 雙軸下拉:通路 7 值、業務型態 3 值 +「不指定」
webui-installments-02-amortization-1000x3.png 1,000.00 元/3 期之攤還表(尾差 333.34、剩餘本金 0.00)
webui-installments-03-early-settlement.png 提前結清 → PRE_CLOSED + 提示訊息
webui-installments-04-agreement-not-signed.png 總約未簽署 → CC-INS-1002
webui-installments-05-business-type-unrecorded.png 業務型態「不指定」→ 畫面顯示未記錄(灰色標記)

9.4.4 本次實測抓到之環境/工具事項(非程式缺陷)

# 事項 處置
1 Keycloak 回 Invalid parameter: redirect_uri cardcore-web 之 redirectUris 僅 http://localhost:5173/*——以 127.0.0.1 開啟會被拒。改用 localhost 即正常
2 agent-browser click @ref 對畫面下方按鈕無反應(回 Done 卻未觸發) 須先 scrollintoview 再 click;未捲入視窗時點擊不落地(本次「執行結清」即因此卡住)
3 docker exec … sqlplus 直連查不到 CC_INST 表 資料在 PDB FREEPDB1;CDB root 查不到。須 alter session set container=FREEPDB1(另:欄位名為 remaining_periods,無 remaining_principal)

10. 已知限制(誠實揭露)

10.1 ✅ 計畫狀態值域已全部對齊(原多方不一致,CR-002 修正完成)

修正後之單一值域:ACTIVE/COMPLETED/PRE_CLOSED/DEFAULTED/CANCELLED(5)

來源 修正前 修正後
SDD-005 §6 狀態機(設計) PENDING/ACTIVE/SETTLED/EARLY_SETTLED/DEFAULTED/REJECTED/CANCELLED(7) ✅ 已於 SDD-005 1.2 更正為 5 值
契約 InstallmentView.status 同 SDD-005 §6 之 7 值 ✅ 已於 CR-002 更正為 5 值
SDD-001 §3.9 ER/DDL(設計來源) 5 值(權威) 不變
實體 DDL INSTALLMENT_PLANS.status(實測) 5 值(權威) 不變
實作列舉 PlanStatus 5 值(權威) 不變

⚠️ 本規格 1.1 版原寫「2 比 1,DDL 是少數」——該敘述有誤,已更正。
錯誤原因:只計入「SDD-005 §6 + 契約」而漏計 SDD-001 §3.9 ER/DDL。
正確為 4 源(BRD+ER/DDL+實體 DDL+實作)vs 1 源(SDD-005 §6)。
教訓:值域衝突要數全部來源,不能只數「文件 vs 程式」兩類。

權威判定(CR-002 §3.1):以 ER/DDL 為權威。理由——
BRD-001 §5.6 之分期流程為「申請 → 檢核 → {拒絕-額度不足 / 總約簽署 → 首期入帳}」,
其中「拒絕」為同步交易拒絕,不存在「已申請、待核准」之持久狀態;
故 PENDING/REJECTED 無 ER/DDL 落點,且 view 只能回傳存得下的值。

# 項目 狀態
1 值域不一致 ✅ 已解決。SDD-005 1.2 + CR-002 皆已對齊 ER/DDL;並新增 SDD-005 通用規則 SM-08「狀態值域以可持久化之 DDL CHECK 為權威」
2 語意對照(歷史) SETTLED → COMPLETED;EARLY_SETTLED → PRE_CLOSED;PENDING/REJECTED 移除(無法持久化)
3 SDD-005 §6 原轉換 1~4(申請 → 核准;拒絕;補償) 未實作。本服務之 create 直接建立 ACTIVE 並產生期數,無核准流程(見 §10.5 #4)。SDD-005 1.2 已將狀態機更正為「建立即 ACTIVE」,與實作一致
4 COMPLETED 之產生者 未實作(每期扣款排程未實作,見 §10.5 #7)。SDD-005 1.2 已將此列為保留事項
5 若日後需「申請—核准」流程 須另提 CR 於 SDD-001 §3.9 ER/DDL 補狀態並附遷移腳本,不得僅改文件(SDD-005 1.2 §6 已明訂)

⚠️ 1.0 之 §10 原寫「狀態轉換未建模為 SDD-005 正式狀態機」——此為錯誤敘述:
SDD-005 §6 即「分期計畫狀態機」。已更正為本表。

10.2 ✅ 分期分類雙軸已拆分(原建模缺陷,CR-008 解除)

原缺陷:兩個正交分類軸被塞進同一欄。

來源 分類軸 值域
BRD-001 §5.6(需求源頭) 業務型態 單筆消費分期/約定分期/帳單分期
契約(1.1 版以前) 業務型態 SINGLE/RECURRING/STATEMENT/ACQUIRER/NCCC/QRCODE
SDD-001 §3.9 敘述 來源通路 商家分期(MERCHANT)/銀行分期(BANK)
SDD-001 §3.9 DDL + 實體 DDL + 實作列舉 來源通路 MERCHANT/BANK/REVOLVING/BILL

嚴重性(當時):契約 6 值全部不可用——實測 SINGLE/RECURRING/QRCODE 皆回
400 CC-INS-4000,僅 MERCHANT/BANK 回 201(§9.3 #13),
亦即照契約開發之呼叫端 100% 無法建立分期(阻斷性缺陷)。

兩階段處理:

階段 處置 狀態
CR-002 契約先對齊 DDL 以解除阻斷(MERCHANT/BANK/REVOLVING/BILL) ✅ 已完成
CR-008 雙軸拆分:installment_type→channel_type(改名,7 值)+ 新增 business_type(3 值,允許 NULL) ✅ 已完成(本版)

拆解後之權威值域:

欄位 軸 值域 NULL
channel_type 來源通路(誰提供) MERCHANT/BANK/REVOLVING/BILL/ACQUIRER/NCCC/QRCODE ❌ NOT NULL
business_type 業務型態(怎麼發起) SINGLE/RECURRING/STATEMENT(BRD-001 §5.6) ✅ 允許,語意為「當初未記錄」

⚠️ 殘留揭露:ACQUIRER/NCCC/QRCODE 歸入通路軸,係依字面語意判定,
此 3 值既不見於 BRD-001 §5.6 亦不見於既有 DDL,非客戶確認之規格。
客戶規格確認時應覆核(見 CR-008 §4.2)。

⚠️ 報表與篩選必須處理 business_type 空值——不得以 COALESCE
默認成某一型態;「未記錄」與「單筆」是不同事實。

10.3 期數值域不一致(契約 vs DDL)

來源 值域
契約 periodCount enum: [3, 6, 12, 24]
DDL period_count(實測) BETWEEN 1 AND 60

以契約為準(業務上分期期數須為約定之級距);DDL 之 1–60 為較寬之實體約束。
兩者不衝突但範圍不同(非值域衝突,屬約束層級差異)。

另:期數狀態 InstallmentPeriod.status 於 1.1 版以前未定義列舉且範例誤植為
PENDING;已於 CR-002 補上列舉 DUE/PAID/OVERDUE/WAIVED/REVERSED 並更正範例。

10.4 ★ 本輪修復之 2 項缺陷(皆有實測證據與回歸測試)

# 缺陷 嚴重性 修法
1 結清類型靜默降級:InstallmentService.settle 以 !"DEFAULT".equalsIgnoreCase(type) 判定,任何非 DEFAULT 之字串(含錯字、未定義值)都被靜默視為提前結清並實際執行。實測 settlementType="FOO" → 201 且計畫轉 PRE_CLOSED 高:金融操作在非法輸入下照做。整合端欄位錯置會把「違約處理」執行成「提前結清」,兩者之帳務與催收後果完全不同 新增 SettlementType 列舉(值域=契約 enum),未定義值以 CC-INS-4000 拒絕;InstallmentServiceTest 迴歸測試
2 末期剩餘本金殘留尾差:remainingPrincipal 以 originalAmount − perPeriodAmount × i 計算,未計入末期吸收之尾差 → 100,000 ÷ 3 之末期剩 1 分(0.01 元) 中:客戶繳完所有期數後帳上仍顯示欠款 0.01 元,屬對帳缺口 改為累計已攤本金相減(末期恰為 0);加 2 項回歸測試

缺陷 2 係由 WebUI 實測發現——後端單測與 E2E 皆未察覺,因兩者皆未斷言末期剩餘本金。
教訓:畫面會把資料攤開給人看,比端點回 201 更能暴露語意錯誤。

10.5 未實作 / 部分實作

# 項目 現況
4 SDD-005 §6 轉換 1~4:PENDING 申請 → 核准 ACTIVE/拒絕 REJECTED/補償 CANCELLED ✅ 已釐清(非「未實作」而是「設計未採納」)。SDD-005 1.2 已移除 PENDING/REJECTED——DDL 無法持久化該二值,且 BRD-001 §5.6 之拒絕為同步交易拒絕。狀態機現為「建立即 ACTIVE」,與實作一致。惟 ACTIVE → CANCELLED(Saga 補償)仍未實作
5 結清金額計算(剩餘本金+違約金) 未實作。僅改狀態;SCS-009 §7 原述「經 pricing-service 計算」尚無實作
6 提前結清之後續手續費減免(SDD-005 §6 轉換 3) 未實作。期數之手續費不因結清而調整
7 每期自動扣款/攤還排程(與 payment-service/common-batch 協調) 未實作。期數狀態恆為 DUE;OVERDUE/PAID 無觸發來源
8 冪等鍵落庫 未實作。X-Idempotency-Key 收了但未儲存、未比對;同鍵重送會再建一筆計畫。
✅ 已實測(2026-09-19):以同一把 X-Idempotency-Key 連送兩次 POST /v1/installments,回應為兩筆不同計畫——第 1 次 INST2026090012(planId=e8d39768-c53a-4219-92bb-83710e795126)、第 2 次 INST2026090013(planId=b746aded-8858-47af-8372-ee9f0a8bdb38),CC_INST 計畫總數由 8 → 10。冪等鍵確實未落庫,重送即重複建檔
9 pricing-service 費率引擎 未接。費率固定 0.005
10 CC-INS-3002(終態不可轉換) 無對應端點可觸發
11 樂觀鎖 CC-INS-2001 等衝突碼 契約未定義;update 之版本比對失敗處理未實測

10.6 平台缺口(非本服務特有)

# 缺口 影響與對策
12 GlobalExceptionHandler 未攔截 MethodArgumentNotValidException/MissingRequestHeaderException/HttpMessageNotReadableException 實測:缺 X-Channel → 500 CC-PLT-5000;缺 X-Idempotency-Key → 500;@Valid 欄位缺漏 → 500。契約一律要求 400。本服務之 controller 仍用 @RequestHeader(required=true)+ @Valid,故全部落入此缺口
↳ 本服務之權宜處理 本輪未修(屬平台層議題,影響全部 17 個服務,須另立 CR)。rewards-service 係以「不用 Bean Validation + 標頭 required=false + 程式內明確檢核」規避;本服務尚未跟進。建議於平台層統一修復(新增 3 個 @ExceptionHandler 對應 400),一次解決全部服務

10.7 其他

# 項目 說明
13 無本地種子檔 ✅ 已補:ops/local/init/oracle/10-dev-installment-seed.sql(4 產品/5 計畫/57 期數)。涵蓋 ACTIVE(12/3/24 期)、PRE_CLOSED、DEFAULTED;刻意不種 COMPLETED——該狀態無程式路徑可產生,種入會讓畫面看似「功能已存在」。期數以 CONNECT BY 依與 InstallmentPlan.create 同一公式生成,並重設編號序列為 6
14 1.0 之埠號欄位為「待骨架產生時分配」 已回填 8089
15 INSTALLMENT_PRODUCTS 無維護端點 契約未定義;MIN_PERIODS/MAX_PERIODS 未被 InstallmentService 讀取(期數以契約 enum 檢核)。已於 CR-002 契約對齊後維持揭露
16 installment_type 之業務型態分類無處儲存 ✅ 已解除(CR-008):新增 business_type 欄承載業務型態,SDD-001 1.5 已完成建模、V3 已遷移、程式/契約/種子/前端皆已同步,並經真實 Oracle 端到端實測(§9.3 #14–18)。殘留限制見 §10.2(空值處理、3 值歸類待客戶覆核)

11. 修訂記錄

版本 日期 修訂內容
1.0 — 規格先行版(埠號與類別名為規劃值)
1.1 2026-09-19 ① 埠號回填 8089;② 修正 §1.1 懸空章節引用(SDD-001 §5.5→§3.9、SDD-003 §7→§12、補 SDD-005 §6);③ §3/§4 改為實作檔案;④ §6 補錯誤碼實測表;⑤ §7 補攤還與剩餘本金不變式;⑥ §9 補 22 單測/5 關卡/13 項 Oracle 實測/3 項 WebUI 端到端;⑦ §10.1 更正原「未建模狀態機」之錯誤敘述,改為「值域三方不一致,且 DDL 為少數」;⑧ §10.4 新增本輪修復之 2 項缺陷(結清類型靜默降級、末期剩餘本金殘額);⑨ §10.6 新增平台缺口 500-vs-400 實測證據;⑩ §10.5 #8 補冪等鍵未落庫之實測證據(同鍵重送產生兩筆計畫);⑪ 補 §11 修訂記錄
1.2 2026-09-19 ① 依 CR-002 執行契約修正:installmentType 對齊 DDL(原列舉 6 值實測全部回 400,屬阻斷性缺陷)、InstallmentView.status 對齊 ER/DDL、InstallmentPeriod.status 補列舉並更正範例;② 更正 §10.1 原「2 比 1,DDL 是少數」之錯誤敘述——漏計 SDD-001 §3.9 ER/DDL,正確為 4 源 vs 1 源;③ §10.2 改述為建模缺陷(兩正交分類軸共用一欄);④ §10.7 新增 #16 業務型態無處儲存;⑤ #13 補種子檔完成;⑥ 新增 10-dev-installment-seed.sql
1.3 2026-09-19 ① SDD-005 1.2 完成 §6 狀態機更正(移除無法持久化之 PENDING/REJECTED;SETTLED/EARLY_SETTLED → COMPLETED/PRE_CLOSED;轉換 8→6;新增通用規則 SM-08「狀態值域以可持久化之 DDL CHECK 為權威」;§12.2 矩陣連動更正為 80/103);② §10.1 改為「值域已全部對齊」——四方(BRD/ER-DDL/狀態機/契約)一致,殘留項僅剩「COMPLETED 無產生者」與「ACTIVE→CANCELLED 未實作」;③ SDD-007 1.1 同步更新 §5.4.8 值域說明;④ 契約描述與 InstallmentView javadoc 同步更正
1.4 2026-09-20 ① 依 CR-008(BEAM PMO 核定)完成雙軸拆分:installment_type→channel_type(7 值)+ 新增 business_type(3 值、允許 NULL);② 領域列舉 InstallmentType→ChannelType + 新增 BusinessType;③ InstallmentPlan/JdbcInstallmentRepository/InstallmentService/CreateInstallmentRequest/InstallmentView 同步(InstallmentView 新增 2 欄供讀回雙軸);④ 新增 V3__installment_channel_business_type.sql(動態 PL/SQL 卸除系統命名約束,避免綁死環境);⑤ 契約由 gen_openapi.py 重產(17 服務/72 端點/0 錯誤);⑥ 種子補雙軸值,刻意保留 0003 為 NULL 以驗證空值路徑;⑦ 前端 installments.ts/InstallmentConsole.tsx 同步(空值顯示「未記錄」,不得默認成單筆);⑧ 單測 22→33,含 CR-002 根因回歸測試,並以負樣本實測證明判定力(精準轉紅 3 例);⑨ 真實 Oracle 端到端實測 #14–18(V3 遷移、雙軸落庫、NULL 還原、誤填業務型態值仍被拒);⑩ §10.2 建模缺陷與 §10.7 #16 皆標為已解除
1.6 2026-10-15 CR-043 §2.4:InstallmentPeriod.principalAmount 補契約與回應——★ 緣由:verify_contract_dto 回報「DTO 有、契約無」(實作領域模型 InstallmentPeriod 有 principalAmount,契約 schema 漏宣告)。★ 盤點(不照清單字面做):該欄原僅在領域模型;回應 InstallmentView.PeriodView 未暴露 → 若僅補契約,將宣告「回應不存在之欄位」=新的不一致。★ 處置:① 契約 InstallmentPeriod schema 補 principalAmount;② 回應 PeriodView 併補暴露(InstallmentViewAssembler 傳 period.principalAmount())——使契約=回應=領域模型一致。★ 契約重生 0 錯誤;verify_contract_dto 不符 0 處。★ 如實揭露:現行實作之 amount 與 principalAmount 同值(DDL 分列 AMOUNT/PRINCIPAL_AMOUNT,生成器 InstallmentPlan 以同一 principal 填入兩者)——兩欄是否應有語意差異(如 amount=本金+手續費)屬業務語意,未於本次認定
1.5 2026-09-21 §9.4 WebUI 端到端實測重跑(真實瀏覽器 + Keycloak 登入)——解除 1.4 版「本次僅 typecheck+build、尚未重跑瀏覽器實測」之待辦。★ 7 情境全數成立:①建立分期 1,000.00/3 期 → INST2026090008,攤還表 333.33/333.33/333.34、剩餘本金 666.67/333.34/0.00(手續費 1.66/1.66/1.68=5.00,尾差由末期吸收);②結清 EARLY → PRE_CLOSED + 提示「已完成提前結清」,DB 複核 remaining_periods=0;③總約未簽署 → CC-INS-1002;④雙軸下拉:通路 7 值、業務型態 3 值 +「不指定」,與 SDD-007 §5.4.8 一致;⑤業務型態「不指定」→ 畫面顯示 badge--muted :: 未記錄(灰色標記,非 SINGLE);⑥重新查詢後仍為「未記錄」——證明後端存 NULL 非畫面暫態;⑦DB 直接複核:INST2026090009 MERCHANT <NULL>/INST2026090008 MERCHANT SINGLE。⚠️ 姿勢揭露:本次為認證啟用(Keycloak demo.csr 經 PKCE 登入;8089 無憑證回 401),使用者 token 之 azp=cardcore-web 不受服務間 ACL 管制,與 CR-011 設計一致。截圖存證 5 張(docs/08-evidence/webui-installments-01~05)。另記錄三項環境/工具事項(非程式缺陷):redirect_uri 僅允許 localhost 不能用 127.0.0.1、agent-browser 對下方按鈕須先 scrollintoview 再 click、Oracle 資料在 PDB FREEPDB1 須切 container 才查得到
(42-42/67)