Project

General

Profile

CR #215 » CR-014_專用額度資料模型裁定.md

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

 

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_LIMITS 11 欄、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 直接將這些問題解決」)。

理由:

  1. 單一真實來源(CR-030 P1):權威表已存在,再建一張同名表=兩套真相。
  2. 既有值域即權威:purpose/status 之 CHECK 為真機既有事實,
    文件(CR-001 §2.4)亦已記載;改動已套用之約束=無必要之破壞性變更。
  3. 「不佔卡人額度」自然成立:SPECIAL_LIMITS 是獨立額度池(8-3),
    與 account_limits 無欄位重疊——CR-001 §2.4 早已據此拒絕以本表充當信用額度。
  4. 生命週期語意對映完整(見 §5):申請/覆核之軌跡留在 LIMIT_ADJUSTMENTS,
    本表只存現行定義——兩者職責分明,非重複。
  5. 改動最小:除 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):

  1. ALTER TABLE special_limits ADD (currency CHAR(3 CHAR)) + 由 account_limits 回填
    + MODIFY (currency NOT NULL)——不設預設值,避免把「TWD」這個業務假設寫進 DDL;
    無法歸屬幣別之列將使遷移**失敗(fail-closed)**而非靜默填值。
    (本表於遷移時為 0 列,故實務上為空操作。)
  2. CREATE INDEX idx_special_limits_account ON special_limits (account_id, status)
    ——查詢端點之存取路徑。
  3. 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′ 後)

  1. V6__special_limits_currency_and_index.sql(limit-service):§5 之三項。
  2. 領域層:SpecialLimit(不可變)+ SpecialLimitRepository(埠)。
  3. 基礎設施層:JdbcSpecialLimitRepository(findActive/findActiveByAccount/
    upsertActive/cancelActive/expireActive)。
  4. 應用層 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。
  5. 排程:TemporaryLimitExpiryScheduler 併同處理專用額度到期
    (否則 status='ACTIVE' 會說謊,且唯一索引會擋住同用途之新額度)。
  6. 介面層:SPECIAL 之 purpose 值域檢核(不合法 → CC-LIM-1003「專用額度不可用」,
    既有錯誤碼即為此設計:「未設定或已過期」)。
  7. 契約:SpecialLimitItem 改為現行定義視角(§7)。
  8. 後續增量(本 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. 已知限制(誠實揭露)

  1. scope_ref(MCC 等用途範圍)未實作——BRD-001 §5.4 提及「特定 MCC」,
    惟 POC-8 之 8-1~8-7 未列驗收項;待需求確認後另提 CR。
  2. 8-2(角色/客層上限)仍未實作——目前覆核門檻仍為金額模擬(SCS-002 §7.2)。
  3. 授權端尚未扣抵專用額度——8-3/8-4/8-5 之交易時檢核屬 authorization-service
    之增量;本 CR 只備妥資料模型與 used_amount 欄位。
  4. 8-6/8-7(綜所稅/繳款之臨時額度串動)未實作。
  5. 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 裁定
    (1-1/1)