CR #201 » SCS-009_分期服務程式設計規格.md
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.3SDD-0051.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_ATTIMESTAMP(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同值
(DDLAMOUNT/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.9ER/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皆回
400CC-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 才查得到 |