CR #215 » CR-014_專用額度資料模型裁定.md
CR-014 專用額度(POC-8)資料模型裁定——接線既有 SPECIAL_LIMITS
| 項目 | 內容 |
|---|---|
| 文件編號 | CR-014 |
| 版本 |
1.1(✅ 裁定:採 選項 A′=接線既有 SPECIAL_LIMITS;併更正 v1.0 之「新表」誤述) |
| 狀態 | ✅ 已裁定(2026-10-02)——依 BEAM PMO 之授權「以 Best Practice 直接將這些問題解決」,由架構師裁定並逕行實作 |
| 提出日期 | 2026-09-22(v1.0)/裁定 2026-10-02(v1.1) |
| 提出者 | WorkBuddy AI(開發團隊) |
| 緣起 |
#A-75 ③:SPECIAL 之 apply 回 422「查無主檔」——applyAdjustedAmount 無 SPECIAL 分支 |
| 關聯 |
BRD-004 POC-8、BRD-001 §5.4 BF-04、CR-001 §2.3、SDD-001 §3.4、SAD-002 §6、ISS-2.4、GOV-006 2.2.3b、SCS-002
|
| 嚴重度 | 🟡 中——POC-8(R1)之完整驗收受阻 |
0. ★ 對 v1.0 之重大更正(先讀本節)
v1.0 標題與 §2 現況皆誤:v1.0 稱本案為「
SPECIAL_LIMITS新表」,
並於 §2「現況(實測)」列了三點而獨漏該表。事實(2026-10-02 真機複查):
CC_LIM.SPECIAL_LIMITS早已存在——
- 來源:
limit-service/src/main/resources/db/migration/V1__baseline.sql,
該檔由tools/gen_flyway_baseline.py依SDD-001 §3.4自動產生(非人工手寫)。- 真機實測:
SELECT table_name FROM all_tables WHERE owner='CC_LIM'→
ACCOUNT_LIMITS / CONSUMED_EVENTS / LIMIT_ADJUSTMENTS / LIMIT_HOLDS / OUTBOX / SPECIAL_LIMITS;
SPECIAL_LIMITS11 欄、0 列、Flyway已於 2026-09-19 基線套用。- 文件早已載明:
CR-001 §2.3(2026-09-19)已將SPECIAL_LIMITS列為
「特定用途額度(非一般信用額度)」,§2.4 並實測其PURPOSE之CHECK值域。根因(本專案第 N 次「實測範圍不足」型缺陷):v1.0 之「實測」只掃了 Java 程式碼
(grep全庫無special_limits之 Java 引用),未掃db/migration之 DDL,
亦未回頭核對CR-001之既有記載 → 誤判為「表不存在」。
此與CR-033之「幽靈待辦」同型:權威清單/既有事實未被回查。結論:本案不是「新建表」,而是「把既有表接上寫入與查詢路徑」——
這使實作更小、風險更低(不動既有表結構,除currency一欄之增補,見 §5)。
1. 語意查證(設計依據)
| 來源 | 語意 |
|---|---|
BRD-001 §5.4 BF-04 |
專用額度=依用途設定(繳稅/保費/特定 MCC),與永久/臨時並列為第三種額度異動類型;流程含權限檢核(依角色與客層之上限)→ 覆核 → 生效 → 同步至授權服務(含快取更新) → 稽核 |
BRD-004 POC-8 |
8-1 參數設定(新增/修改/刪除/查詢、依人員權限控管);8-3 使用專用額度不佔卡人額度;8-4 未設定則用卡人額度;8-5 超額拒絕;8-6/8-7 綜所稅請款→調高臨時額度(截止日+7 日)/繳款→調降 |
SAD-002 §6.2(分流鐵律) |
同一歸戶不得跨波次——專用額度設定亦須可追蹤歸戶 |
CR-001 §2.3/§2.4 |
SPECIAL_LIMITS=特定用途額度;PURPOSE 值域實測為 ('TAX','INSURANCE','TUITION','MEDICAL','OTHER')
|
SDD-001 §3.4 |
lim.special_limits 之權威 DDL(即 V1__baseline.sql 之來源) |
關鍵推論:一個歸戶可同時存在多筆專用額度(不同用途),
各有獨立的金額、效期、申請/覆核軌跡與生命週期——這是一對多的常駐定義,
不是主檔上的一個欄位,也不是一筆不可變的異動紀錄。
2. 現況(實測;★ 已更正)
| 對象 | 實測結果(2026-10-02,真實 Oracle 23.26 FREEPDB1/schema CC_LIM) |
|---|---|
ACCOUNT_LIMITS |
僅 permanent_amount/temporary_amount/shadow_amount/used_amount——無專用額度欄位;3 列、皆 TWD、單帳戶單幣別 |
LIMIT_ADJUSTMENTS |
adjust_type 含 SPECIAL(CHECK 值域已備),但 applyAdjustedAmount 之 switch 無 SPECIAL 分支 → apply 回 422 |
SPECIAL_LIMITS |
★ 早已存在(V1__baseline.sql ← SDD-001 §3.4,Flyway 基線 2026-09-19 套用)。11 欄:ID RAW(16) PK/ACCOUNT_ID RAW(16) NOT NULL/PURPOSE VARCHAR2(24 CHAR) NOT NULL/AMOUNT NUMBER(19,0) NOT NULL/USED_AMOUNT NUMBER(19,0) DEFAULT 0/EFFECTIVE_FROM DATE NOT NULL/EFFECTIVE_TO DATE/STATUS VARCHAR2(16 CHAR) DEFAULT 'ACTIVE' NOT NULL/CREATED_AT/UPDATED_AT/VERSION。0 列。索引僅 PK(SYS_C0010675)。無任何 Java 程式引用(表已存在、程式全無型缺口,同 #A-117/#A-118 之卡片狀態事件) |
值域(CHECK,實測) |
purpose IN ('TAX','INSURANCE','TUITION','MEDICAL','OTHER');amount >= 0;status IN ('ACTIVE','EXPIRED','CANCELLED')
|
| 查詢端點 |
GET /api/v1/accounts/{accountId}/special-limits(C-11)已上線,目前查 LIMIT_ADJUSTMENTS 之 SPECIAL 紀錄(空清單)——即讀錯表
|
3. 選項對照(v1.0 三案 + 本次新增 A′)
| 選項 | 內容 | 判定 |
|---|---|---|
| A′(✅ 裁定) |
接線既有 SPECIAL_LIMITS:寫入路徑補 SPECIAL 分支、查詢端點改讀本表;僅增補 currency 一欄(見 §5) |
採納。單一真實來源(不重複建表);沿用既有值域即權威;改動最小 |
| A(v1.0 之建議) |
新建 SPECIAL_LIMITS 表(自訂 purpose_code/scope_ref/status 值域) |
❌ 不可行——表已存在(§0);且會與 SDD-001 §3.4/V1 同名衝突
|
| B | 主檔欄位(account_limits.special_amount) |
❌ 一帳戶僅一筆一用途,違反 POC-8 多用途語意 |
| C |
LIMIT_ADJUSTMENTS 擴充(加 purpose/scope) |
❌ 調整表是不可變稽核紀錄,而專用額度是可變更之現行定義(8-1 要能修改/刪除)——語意不合 |
4. 裁定(2026-10-02)
採選項 A′。裁定者:架構師(依 BEAM PMO 之明確授權「以 Best Practice 直接將這些問題解決」)。
理由:
-
單一真實來源(
CR-030P1):權威表已存在,再建一張同名表=兩套真相。 -
既有值域即權威:
purpose/status之CHECK為真機既有事實,
文件(CR-001 §2.4)亦已記載;改動已套用之約束=無必要之破壞性變更。 -
「不佔卡人額度」自然成立:
SPECIAL_LIMITS是獨立額度池(8-3),
與account_limits無欄位重疊——CR-001 §2.4早已據此拒絕以本表充當信用額度。 -
生命週期語意對映完整(見 §5):申請/覆核之軌跡留在
LIMIT_ADJUSTMENTS,
本表只存現行定義——兩者職責分明,非重複。 -
改動最小:除
currency一欄與兩個索引外,不動既有表結構。
業界依據:信用卡核心之「專用額度/用途額度(purpose-based limit / dedicated limit)」
在業界(Vision Plus、Fiserv、Temenos 等)一律以獨立於一般信用額度之定義表實作,
每筆具備用途別、金額、效期、狀態,並由授權端於交易時併查;
申請與覆核之軌跡則留在調整/簽核歷程表。此與本裁定之分工一致。
5. 逐欄位裁定(CR-014 v1.0 選項 A 之設計 ↔ 既有表)
| v1.0 選項 A 欄位 | 既有表 | 裁定 | 說明 |
|---|---|---|---|
id |
✅ | 沿用 |
RAW(16) PK |
account_id |
✅ | 沿用 | 歸戶 |
purpose_code VARCHAR2(20)
|
purpose VARCHAR2(24 CHAR) + CHECK
|
沿用既有名與值域 | 值域 TAX/INSURANCE/TUITION/MEDICAL/OTHER——不自由化(自由化=放寬既有約束,且無需求支撐) |
scope_ref |
✗ | ❌ 不加 | POC-8 8-1~8-7 未要求 MCC 範圍檢核;如實列為已知限制(§7) |
amount |
✅ | 沿用 | 分 |
currency |
✗ | ★ 加(V6) |
理由:account_limits 之鍵為 (account_id, currency),多幣別為既有設計;無幣別之專用額度無法歸屬,且 upsert 鍵會跨幣別碰撞。契約本已有 currency 欄位 |
status |
✅ ACTIVE/EXPIRED/CANCELLED
|
沿用既有值域 | 對映:EFFECTIVE→ACTIVE、REVOKED→CANCELLED、到期→EXPIRED。PENDING_APPROVAL 不入本表——未生效者僅存於 LIMIT_ADJUSTMENTS(本表只存現行定義) |
effective_from |
✅ NOT NULL
|
沿用 | 未帶 tempStart 時取今日(本表 NOT NULL,不得為空) |
effective_to |
✅ nullable | 沿用 |
NULL=無期限 |
applicant_id/approver_id
|
✗ | ❌ 不加 | 申請/覆核軌跡之權威在 LIMIT_ADJUSTMENTS(P1)。本表存現行定義,不複製簽核欄位 |
| — | used_amount |
沿用(既有) | 供 8-3「不佔卡人額度」之用量追蹤(授權端扣抵屬後續增量,見 §6) |
V6__special_limits_currency_and_index.sql(limit-service):
-
ALTER TABLE special_limits ADD (currency CHAR(3 CHAR))+ 由account_limits回填
+MODIFY (currency NOT NULL)——不設預設值,避免把「TWD」這個業務假設寫進 DDL;
無法歸屬幣別之列將使遷移**失敗(fail-closed)**而非靜默填值。
(本表於遷移時為 0 列,故實務上為空操作。) -
CREATE INDEX idx_special_limits_account ON special_limits (account_id, status)
——查詢端點之存取路徑。 -
CREATE UNIQUE INDEX uk_special_limits_active ON special_limits (account_id, currency, purpose, CASE WHEN status='ACTIVE' THEN 'ACTIVE' END)
——「一歸戶 × 一幣別 × 一用途,至多一筆生效中」。
Oracle B-tree 唯一索引不索引全NULL鍵 → 非ACTIVE(EXPIRED/CANCELLED)可留多筆歷史。
此為8-1之核心不變式:若容許兩筆ACTIVE同用途,授權端將重複計算專用額度。
6. 實作範圍(裁定 A′ 後)
-
V6__special_limits_currency_and_index.sql(limit-service):§5 之三項。 -
領域層:
SpecialLimit(不可變)+SpecialLimitRepository(埠)。 -
基礎設施層:
JdbcSpecialLimitRepository(findActive/findActiveByAccount/
upsertActive/cancelActive/expireActive)。 -
應用層
LimitAdjustmentService:-
apply():SPECIAL之before取自現行有效之同用途專用額度(無則 0),
而非主檔永久額度——否則覆核門檻(§7.2)會以錯誤基準計算。 -
applyToMaster():SPECIAL→upsertActive(...)(同交易),
不走applyAdjustedAmount。 -
revoke():SPECIAL→cancelActive(...)(對映REVOKED→CANCELLED)。 -
specialLimits():改讀SPECIAL_LIMITS(現行定義)。 - 新增
expireSpecial(today):ACTIVE且effective_to < 今日→EXPIRED。
-
-
排程:
TemporaryLimitExpiryScheduler併同處理專用額度到期
(否則status='ACTIVE'會說謊,且唯一索引會擋住同用途之新額度)。 -
介面層:
SPECIAL之purpose值域檢核(不合法 →CC-LIM-1003「專用額度不可用」,
既有錯誤碼即為此設計:「未設定或已過期」)。 -
契約:
SpecialLimitItem改為現行定義視角(§7)。 -
後續增量(本 CR 不含):
8-2(角色/客層上限)、8-6/8-7(臨時額度串動)、
授權端之專用額度扣抵與scope_ref(MCC)。
7. 契約變更與向後相容(★ 破壞性)
GET /api/v1/accounts/{accountId}/special-limits 之回應 schema SpecialLimitItem
由「調整視角」改為「現行定義視角」:
| 欄位 | v1(原) | v1.1(新) |
|---|---|---|
adjustmentId |
✅ |
移除(改 specialLimitId) |
accountId |
✅ | 保留 |
currency |
✅ | 保留(本表已補欄) |
amountMinor |
✅ | 保留 |
status |
DRAFT/PENDING_APPROVAL/EFFECTIVE/REJECTED/EXPIRED/REVOKED |
值域變更 → ACTIVE/EXPIRED/CANCELLED(本表 CHECK) |
tempStartDate/tempEndDate
|
✅ |
改名 → effectiveFrom/effectiveTo
|
purpose |
nullable 自由字串 |
值域化 TAX/INSURANCE/TUITION/MEDICAL/OTHER
|
applicantId |
✅ |
移除(軌跡在 LIMIT_ADJUSTMENTS) |
usedAmountMinor |
— | 新增 |
向後相容性裁定:依 CR-030 P4/SAD-004 §5.3,移除欄位與變更值域屬破壞性變更,
本應升版;惟經查證——
-
消費端為零:全庫
grep前端(frontend/src)無任何special-limits引用;
limit-service亦無其他服務呼叫此端點(SAD-002之訂閱關係不含本端點)。 -
端點語意本來就是錯的:
C-11上線時即以「查LIMIT_ADJUSTMENTS」權宜實作
(CR-014 v1.0 §2自承),本次即為修正該權宜。
→ 故不另立 v2 端點,直接修正 schema,並於契約 description 標註語意變更。
(若日後有外部消費端,應改以新增 v2 路徑為之。)
7.1 併同修正之契約缺陷(★ 真機測試揭露)
POST /api/v1/limit-adjustments 為 POC-8 之 8-1(參數設定)入口,真機測試時發現
契約與實作脫節(CR-019 型,惟未被關卡攔下):
| # | 症狀 | 證據 | 修正 |
|---|---|---|---|
| 1 | 契約宣告金額欄為 amount,實作之 record 欄名為 targetAmount
|
依契約送 amount → 400 CC-PLT-4002 請求主體格式錯誤
|
實作欄名改為 amount(與契約一致) |
| 2 | 契約漏列 applicantId,而實作為 @NotBlank(必填) |
送 targetAmount 後 → 400 CC-PLT-4003:applicantId 不得空白
|
契約補 applicantId(列為 required) |
| 3 | 契約漏列 currency(實作既有,未帶時預設 TWD) |
讀 AdjustmentRequest 即得 |
契約補 currency(可選) |
根因(工具關卡缺口,非個案):tools/verify_contract_dto.py 之檢查 A 係「由 Controller 解析回應型別」,
檢查 B 係「契約 schema ↔ application/dto/<Name>.java」——
請求 DTO(本服務之請求 record 巢狀於 Controller,且無 application/dto 目錄)不在任一檢查範圍。
故此類漂移編譯不會錯、啟動不會錯、單測不會錯,只有「真機照契約打一次」才會現形。
→ 建議(列為後續):擴充 verify_contract_dto 覆蓋請求 DTO,或至少對「契約有、Controller 無同名 record」發 WARN。
本輪不改工具(超出本 CR 範圍),如實揭露。
8. 已知限制(誠實揭露)
-
scope_ref(MCC 等用途範圍)未實作——BRD-001 §5.4提及「特定 MCC」,
惟 POC-8 之8-1~8-7未列驗收項;待需求確認後另提 CR。 -
8-2(角色/客層上限)仍未實作——目前覆核門檻仍為金額模擬(SCS-002 §7.2)。 -
授權端尚未扣抵專用額度——
8-3/8-4/8-5之交易時檢核屬authorization-service
之增量;本 CR 只備妥資料模型與used_amount欄位。 -
8-6/8-7(綜所稅/繳款之臨時額度串動)未實作。 -
LIMIT_ADJUSTMENTS與SPECIAL_LIMITS之間無外鍵——
對應關係為業務鍵(account_id, currency, purpose);
「哪一筆調整產生了現行定義」須由兩表之時序推導,非精確可稽核。
9. 真機驗證結果(2026-10-02)
環境:真實 Oracle 23.26(FREEPDB1/schema CC_LIM)+ Keycloak M2M(cc-payment-service,azp 通過 limit-service ACL)+ limit-service 8083。
逐項結果見 SCS-002 §7.9.7(13 項,全數通過)。摘要:
| 面向 | 結論 |
|---|---|
V6 遷移 |
✅ Flyway 套用成功;currency 欄與兩索引真機複查存在 |
| 寫入路徑 | ✅ 未超限 → EFFECTIVE + special_limits 新增/更新;超門檻 → PENDING_APPROVAL 且不落庫;覆核通過 → 落庫 |
| 基準額度 | ✅ beforeAmount 取自專用額度池(0 → 200000 → 300000 之軌跡證明),非主檔之 10,000,000 |
| upsert | ✅ 同業務鍵再次申請 → special_limits 仍為 1 列(更新,非新增) |
| 撤銷 | ✅ REVOKED + special_limits 該列轉 CANCELLED;未觸碰額度主檔
|
| 負樣本 | ✅ purpose 不合法/未帶 → 422 CC-LIM-1003,不落庫 |
| 權威來源(非快取) | ✅ 繞過服務直接改 DB → 查詢立即反映(777777) |
| 到期 | ✅ 讀側日期條件先行把關(排程前即 count=0);排程約 40 秒轉 EXPIRED
|
| 不佔卡人額度(8-3) | ✅ 全程 account_limits 未變 |
| 單元測試 | ✅ 15 例(0 失敗);LayeringTest 通過 |
| 掃描器 | ✅ scan_scaffold/scan_mockdata/scan_faketest 各 0 命中;validate_sql 0 違規;verify_contract_dto(limit-service)0 不符 |
10. 修訂記錄
| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 1.1 | 2026-10-02 | ★ 裁定採選項 A′(接線既有 SPECIAL_LIMITS) 並逕行實作。重大更正:SPECIAL_LIMITS 早已存在(V1__baseline.sql ← SDD-001 §3.4;CR-001 §2.3 亦已載明;真機實測 11 欄、0 列)——v1.0 之「新表」與 §2 現況之漏列皆屬誤述,根因=「實測」只掃 Java 未掃 DDL(同 CR-033 之幽靈型)。新增:§5 逐欄位裁定(沿用既有值域、僅補 currency、加唯一索引 uk_special_limits_active)、§7 契約破壞性變更之向後相容性裁定(消費端為零,不另立 v2)、§7.1 併同修正之契約缺陷(targetAmount↔amount 不符、漏列 applicantId/currency;併揭露 verify_contract_dto 不涵蓋請求 DTO 之關卡缺口)、§8 五項已知限制、§9 真機驗證結果(13 項全通過)。實作範圍:V6 遷移 + 領域/埠/JDBC + LimitAdjustmentService 之 SPECIAL 分支與查詢改讀 + 到期排程 + purpose 值域檢核 + 15 支單元測試 |
| 1.0 | 2026-09-22 | 設計提案:語意查證(BRD-001 §5.4/POC-8)、三選項對照(A 新表/B 主檔欄位/C 調整表擴充)、建議 A 並附表結構與實作範圍;待 BEAM PMO 裁定
|