Project

General

Profile

CR #201 » SCS-002_額度服務程式設計規格.md

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

 

SCS-002 額度服務(limit-service)程式設計規格

項目 內容
文件編號 SCS-002
版本 1.20(CR-055 P4-2/P4-3:新增 §7.18 預借現金額度(上限=總額度×ratio、可用=min(上限−已預借, 有效可用);null≠0 fail-closed;★ V16 擴充 hold_type 值域——真機攔下 ORA-02290 之真實缺口)+ §7.19 企業子額度與組織樹(V17;上層可用扣減所有下層佔用;檢核走上層鏈並指出不足之層)。前版 1.19(CR-055 P4-1:新增 §7.17 共享池權利與卡片上限檢核——三概念分離(總額度/卡片名目額度/共享池權利)、可刷上限公式、查詢分開呈現;★ 修正「不享共享池之卡可刷到總額度」之超放缺陷。前版 1.18(CR-054 P1:§7.6 佔用事件 payload 補可選 instrumentId——權威表早有值、事件原本不帶(「權威表有、事件沒有」),補此欄為投影唯一來源;released 由同交易 findHoldInstrumentId 回查。前版 1.17(CR-052 C-3:新增 §7.16 卡片級額度調額寫入——V12 調額單維度化/帶 instrumentId 者只改 CARD 列/無 CARD 列時核准後建立(客戶提供初始值之唯一途徑)/★ 卡片級不發 limit.limit.adjusted.v1(避免覆蓋帳戶總額度)。前版 1.16(CR-053 D-1:新增 §7.15 載體(卡)級主額度——額度主體維度化 V11/subject_type+subject_id/trigger 補值/唯一鍵改主體界定;契約 GET /instruments/{instrumentId}/limits;★ 可用額度為保守下界並以 holdAttribution 揭露歷史佔用不可歸屬。前版 1.15(CR-045 D4:§7.14 閉環改採真 socket(#A-242:嵌入式 Tomcat + 真 HTTP,位址用 hostname);★ 更正 #A-241 之「沙箱代理」錯誤解釋,真因為本機 Tomcat 僅綁 IPv6、IPv4 迴路掛住。前版 1.14(CR-045 D4:§7.14 驗收 ②/④/⑤ 已閉合(#A-241)——product-service 新增 ParameterClosedLoopIntegrationTest(真 Oracle + 真 web 層)證明 PUT 落版/businessDate 版控/直改即反映(非快取)/同交易 Outbox/4 項負樣本;★ 更正前版「待寫入路徑俱備」之過度保守註記。前版 1.13(CR-045 D4:§7.14 商品參數消費閉環首案已實作並真機驗證閉環(#A-239)。前版 1.12(CR-045 D4:新增 §7.14 商品參數消費閉環首案)。前版 1.11(CR-036:新增 §7.13 8-6 迄日之精確化——改以帳單出帳事件(statement.statement.issued.v1)之權威 paymentDueDate 校正推算值;★ 契約無需變更,僅新增一組訂閱。真機驗證 3 項含負樣本)。前版 1.10(CR-035 §2.4:新增 §7.12 臨時額度串動——POC-8 之 8-6/8-7)
狀態 已核定(回溯補建)
服務埠號 8083
套件根 com.beam.cardcore.lim
權威 schema CC_LIM
對應需求 ISS-2.4(專用額度)、SYS-2.1(CQRS)

1. 對應需求

FR 編號 需求名稱
ISS-2.4 專用額度(永久/臨時/彈性)
SYS-2.1 讀寫分離(CQRS)

2. 服務職責與邊界

承作:

  • 額度主檔(account_limits)維護
  • 佔用/釋放(limit_holds)
  • 額度調整申請(limit_adjustments)與狀態機(§8)
  • 專用額度(special_limits)之現行定義維護(POC-8;§7.9)

不做:

  • 授權決策 → authorization-service(含專用額度之交易時扣抵,§10 #12)
  • 帳務入帳 → posting-service

3. 領域模型

3.1 聚合

型別 職責
AccountLimit 額度主檔聚合(永久/臨時/彈性 + 已用)。不可變
LimitAdjustment 調整申請聚合
LimitAdjustmentStatus 調整申請狀態機(14 條轉換)
SpecialLimit 專用額度之現行定義(用途 × 金額 × 效期 × 狀態;§7.9)。不可變

3.2 狀態機:額度調整申請(SDD-005 §8)

來源 事件 目標
— 建立 DRAFT
DRAFT 送出(增幅 > 門檻) PENDING_APPROVAL
DRAFT 送出(增幅 ≤ 門檻) EFFECTIVE
PENDING_APPROVAL 覆核通過 EFFECTIVE
PENDING_APPROVAL 覆核駁回 REJECTED
EFFECTIVE 到期 EXPIRED
EFFECTIVE 撤銷 REVOKED

終態:REJECTED/EXPIRED/REVOKED。非法 → CC-LIM-3001;終態再轉換 → CC-LIM-3002。

3.3 狀態機:佔用(SDD-005 §3)

HELD → RELEASED/EXPIRED;釋放與部分釋放。


4. 程式結構

套件/類別 職責
domain/model/AccountLimit 額度主檔聚合
domain/model/LimitAdjustment 調整申請聚合
domain/model/LimitAdjustmentStatus 調整狀態機(serviceCode LIM)
domain/model/SpecialLimit 專用額度現行定義(§7.9;含 scope_ref 之 MCC 涵蓋判定,§7.10)
domain/port/LimitRepository 主檔與佔用埠(createHold 含池歸屬,§7.10)
domain/port/LimitAdjustmentRepository 調整申請埠
domain/port/SpecialLimitRepository 專用額度埠(§7.9;含 findApplicable/increaseUsed,§7.10)
domain/port/SpecialLimitPolicyRepository 政策埠(§7.11;角色 × 客層 → 上限)
domain/port/CallerIdentityPort ★ 呼叫者身分(§7.11.2;角色自權杖,不接受請求帶入)
domain/port/SpecialLimitTempAdjustmentRepository 串動紀錄埠(§7.12;8-6/8-7)
domain/port/TempAdjustmentRecoveryPort ★ 窄埠(§7.12.3)——使繳款消費端不依賴串動服務之完整介面
application/SpecialLimitTempAdjustmentService 8-6 調高/8-7 FIFO 回收(§7.12)
infrastructure/messaging/PostingTransactionPostedListener 入帳事件消費者(8-6 之觸發;§7.12)
infrastructure/messaging/StatementIssuedListener ★ 帳單出帳事件消費者(8-6 迄日之校正;§7.13、CR-036)
application/LimitApplicationService 佔用/釋放/查詢(含 SPECIAL 池路徑與適用性查詢,§7.10)
application/LimitAdjustmentService 申請/覆核/撤銷/到期/專用額度
infrastructure/jdbc/JdbcLimitRepository JDBC 實作
infrastructure/jdbc/JdbcLimitAdjustmentRepository JDBC 實作
infrastructure/jdbc/JdbcSpecialLimitRepository JDBC 實作(§7.9)
infrastructure/scheduler/TemporaryLimitExpiryScheduler 臨時額度與專用額度到期排程
interfaces/rest/LimitController 佔用/釋放/歸戶額度
interfaces/rest/LimitAdjustmentController 申請/覆核/撤銷

5. 資料模型

表 關鍵欄位 實測約束
account_limits account_id、currency、permanent_amount、temporary_amount、temp_start_date、temp_end_date、shadow_amount、used_amount、status、version UNIQUE(account_id,currency)
limit_holds id、account_id、authorization_id、hold_amount、released_amount、hold_type、status、special_limit_id status IN ('HELD','RELEASED','EXPIRED');hold_type IN ('GENERAL','SPECIAL','INSTALLMENT');★ special_limit_id 非 NULL = 屬專用額度池(CR-035 §2.2)
limit_adjustments id、account_id、adjust_type、before_amount、after_amount、status、temp_start_date、temp_end_date、applicant_id、approver_id、effective_at、version adjust_type IN ('PERMANENT','TEMPORARY','SPECIAL','SHADOW');status IN (...)
special_limits id、account_id、currency、purpose、scope_ref、amount、used_amount、effective_from、effective_to、status、version ★ 見 §7.9:purpose IN ('TAX','INSURANCE','TUITION','MEDICAL','OTHER')、amount >= 0、status IN ('ACTIVE','EXPIRED','CANCELLED');UNIQUE INDEX uk_special_limits_active
special_limit_policies id、role_code、segment_code、currency、max_amount、status、version ★ 見 §7.11:status IN ('ACTIVE','INACTIVE')、max_amount >= 0;UNIQUE (role_code, segment_code, currency)(V8)
special_limit_temp_adjustments id、account_id、currency、special_limit_id、authorization_id、posting_id、raised_amount、recovered_amount、raised_on、raise_until、due_date_source、status、version ★ 見 §7.12:due_date_source IN ('DERIVED','STATEMENT')、status IN ('OPEN','CLOSED');UNIQUE INDEX uk_special_temp_posting(V9)

6. API 設計

端點 說明 錯誤碼
GET /api/v1/accounts/{accountId}/limits 歸戶額度 CC-LIM-1001
POST /api/v1/limit-holds 建立佔用 CC-LIM-1001
POST /api/v1/limit-holds/{holdId}/releases 釋放 CC-LIM-1001
POST /api/v1/limit-adjustments 提出申請 CC-LIM-1001
POST /api/v1/limit-adjustments/{id}/approval 覆核(APPROVE/REJECT) CC-LIM-3001/3002
POST /api/v1/limit-adjustments/{id}/revocation 撤銷 CC-LIM-3001
GET /api/v1/accounts/{accountId}/special-limits 歸戶專用額度(現行定義,§7.9) —
GET /api/v1/accounts/{accountId}/special-limits/applicable 交易適用之專用額度(池選擇,§7.10) —
GET /api/v1/special-limit-policies 列出政策(§7.11) —
PUT /api/v1/special-limit-policies/{roleCode}/{segmentCode} 新增/更新政策(冪等,§7.11) CC-LIM-1002

7. 關鍵邏輯

7.1 可用額度

可用 = 永久 + 有效之臨時 + 彈性 − 已用 − 未釋放之佔用
  • 臨時額度有效性:temp_start_date ≤ today ≤ temp_end_date(逾期不計入) |
  • 可用為負時歸零。

⚠️ 專用額度(special_limits)不計入本式——BRD-004 POC-8 之 8-3 明載
「使用專用額度不佔卡人額度」。專用額度為獨立額度池,由授權端於交易時併查(§10 #12,尚未實作)。

7.2 覆核門檻

  • 增幅(after − before)超過 cardcore.limit.approval-threshold-minor(預設 500,000 分 = 5,000 元)→ 送覆核 |
  • 未超過 → 直接 EFFECTIVE 並套用至主檔。

⚠️ 簡化模擬:正式應由角色/權限系統決定申請者可核准額度。已於程式與文件標註,避免誤認已完成權限整合。

7.3 撤銷

EFFECTIVE → REVOKED 必須回復額度至 before_amount——只改狀態不動主檔會留下「狀態已撤銷、額度卻是調整後數值」之兩套真相。

7.4 到期排程

TemporaryLimitExpiryScheduler 每 60 秒掃描「EFFECTIVE + TEMPORARY + temp_end_date < 今日」→ 轉 EXPIRED 且 temporary_amount 歸零(同一交易)。

7.5 額度事件輸出(limit.limit.adjusted.v1)——2026-10-01 新增(GOV-006 4.8 W-2 推廣・事件 #2)

緣由:SAD-004 §5.2 載明本事件(生產者 limit、主要消費者 authorization、用途「額度同步」),
惟從未有人發射(同 #A-117/#A-118 之卡片狀態事件,屬「文件已載、程式全無」型缺口)。
本節為該生產端之規格;消費端見 SCS-001 §7.13。

主題:cc.limit.limit.adjusted(命名規則 SAD-004 §5.4:cc.<aggregateType>.<eventType 去版本>)。

項目 值
eventType limit.limit.adjusted.v1
Outbox aggregate_type limit
Outbox event_type limit.adjusted(不含版本)
Kafka 分區鍵 accountId(同帳戶之事件落同一分區 → 聚合內有序)

事件 payload(v1):

欄位 來源(limit-service account_limits) 說明
accountId account_id 歸戶識別碼(亦為信封 aggregateId)
currency currency 幣別(ISO 4217,3 碼)
totalAmountMinor 套用後之總額度 = 永久 + 有效之臨時 + 彈性 金額以**最小單位(分)**表示

⚠️ totalAmountMinor 不是「本次調整之目標額度」:adjustment.afterAmount() 僅為該調整類型之目標值
(例:TEMPORARY 調整時它只是臨時額度)——若直接當總額度,會低估可用額度。
故 applyToMaster 於套用成功後重讀主檔,以 AccountLimit.totalLimit(today) 為 payload
(對應讀模型 account_limit_caches.total_amount 之語意,見 JdbcLimitPort)。

處理:

  1. 同交易寫 Outbox:applyToMaster 套用主檔成功後,於同一交易(@Transactional)寫入 outbox(SDD-001 §3.15 形狀);否則會出現「額度已改、事件未發」之落差。
  2. relay 發布:OutboxRelayScheduler 週期性呼叫 common-event 之 OutboxPublisher 撈取 published_at IS NULL 者送 Kafka(SDD-004 §4.3)。
  3. 信封欄位固定(SAD-004 §5.1);業務差異全在 payload。

設計要點:

  • ★ 只在「已生效」路徑發射:applyToMaster 僅由 EFFECTIVE(直接生效/覆核通過)呼叫;PENDING_APPROVAL/REJECTED 不發射(額度未變)。
  • ⚠️ 序列化為 StringSerializer:Outbox payload 已是信封 JSON 字串,若經 JsonSerializer 會多包一層引號並轉義 → 消費端解析失敗(SAD-004 §5.4)。
  • ⚠️ sequenceNo 為「同 JVM 內嚴格遞增」之替代序號(LimitEventFactory.nextSequenceNo():以毫秒時間戳為基底、max(now, last+1),故同毫秒多次調整不碰撞);多實例部署時仍可能重複,正式環境應改為帳戶層級之持久化序號(SAD-004 §5.4 要求「同一聚合內依 sequenceNo 遞增」)。此為已知簡化,如實揭露(見 §10)。
  • ⚠️ 撤銷(§7.3)與到期(§7.4)路徑尚未發射事件(額度亦隨之變動)→ 見 §10 已知限制。

7.6 佔用事件輸出(limit.hold.created.v1/limit.hold.released.v1)——2026-10-01 新增(GOV-006 4.8 W-2 推廣・事件 #4/#5)

緣由:SAD-002 §3.2 明列本服務發布 limit.hold.created.v1/limit.hold.released.v1,
SDD-006 §4.2 亦已定義 payload,惟從未有人發射(同 §7.5 之型)。
權威歸屬:limit_holds 之權威在本服務(CC_LIM)——故由本服務發布佔用事件為無爭議。
✅ 已化解(2026-10-01,CR-028):SAD-002 §3.1 原另稱 authorization-service 亦發布
authorization.hold.*——經查證(全庫程式碼零引用、payload 逐欄相同、權威在 CC_LIM)
確認為「同一事實兩個生產者」→ 已廢除 authorization.hold.*,hold 事件一律由本服務發布。

主題:cc.limit.limit.hold.created/cc.limit.limit.hold.released(命名規則 SAD-004 §5.4)。

項目 …hold.created.v1 …hold.released.v1
eventType limit.hold.created.v1 limit.hold.released.v1
Outbox aggregate_type limit limit
Outbox event_type limit.hold.created limit.hold.released
Kafka 分區鍵 holdId(同一佔用之事件落同一分區) holdId

payload(v1;欄位名依 SDD-006 §4.2):

欄位 created released 來源 說明
holdId ✅ ✅ limit_holds.id 佔用識別碼(亦為信封 aggregateId)
accountId ✅ ✅ limit_holds.account_id 歸戶識別碼
holdAmount ✅ — limit_holds.hold_amount 最小單位(分)
holdType ✅ — limit_holds.hold_type 佔用類型(如 GENERAL)
authorizationId ✅ — limit_holds.authorization_id 可選欄位(2026-10-01 補,CR-028)——供 authorization-service 之本地投影(CC_AUTH.LIMIT_HOLDS 有同名列)對應回授權。⚠️ 依 SAD-004 §5.3「新增可選欄位不升版」;無對應授權時為 null
currency ✅ — 請求參數 currencyCode 可選欄位(2026-10-01 補,批次 B)——CC_AUTH.LIMIT_HOLDS.currency 為 NOT NULL,消費端需要此值方能寫入本地投影;原 payload 未帶(契約缺口,見 SCS-001 §7.15)。同樣依 SAD-004 §5.3 不升版
instrumentId ✅ ✅ limit_holds.instrument_id(created 取入參;released 以 findHoldInstrumentId 回查) ★ 可選欄位(CR-054 P1,2026-10-17)——交易載體識別,供投影把佔用歸到正確的卡。★ 補此欄之根因:權威表早於 V10 即有值,但事件原本不帶 → 投影永遠拿不到(「權威表有、事件沒有」)。V10 前之歷史佔用為 null(不得回填、不得以 cardId 猜)。依 SAD-004 §5.3 不升版
releasedAmount — ✅ 本次釋放金額 最小單位(分);為本次釋放額,非累計

處理:

  1. 同交易寫 Outbox:LimitApplicationService.hold()/release() 於同一交易(@Transactional)寫入 outbox;否則會出現「佔用已成立、事件未發」之落差。
  2. released 之 accountId 由 DB 取得:release(holdId, amount) 之入參不含帳戶——故於釋放成功後以 findHoldAccountId(holdId) 取得(同一交易)。
  3. relay 發布:OutboxRelayScheduler(同 §7.5)。

設計要點:

  • ⚠️ holdAmount 為 null 之處理:createHold 必然有金額,故 created 之 holdAmount 恆非 null;不設 null 分支(避免死碼)。
  • ✅ authorizationId 已納入為「可選」欄位(2026-10-01,CR-028 化解):
    原載「刻意不納入(現行契約未定義)」——經查證 CC_LIM.LIMIT_HOLDS 本就有 AUTHORIZATION_ID 欄位,
    且 CC_AUTH.LIMIT_HOLDS(本地投影)亦有同名列 → 消費端需要它才能對應回授權。
    依 SAD-004 §5.3「新增可選欄位不升版」→ 不升 v2。
    ⚠️ 實作(補入該欄)屬批次 B(CR-028 §6),本輪未動程式。
  • ⚠️ 部分釋放會發射多次:每次成功釋放各發一則(releasedAmount 為該次金額);消費者若需累計須自行加總(冪等仍由 eventId 保證)。
  • ⚠️ 序列化為 StringSerializer(同 §7.5)。

真機驗證(2026-10-01;真實 Oracle + Kafka):

驗證 結果
生產(created) ✅ POST /api/v1/limit-holds(30,000 分、GENERAL)→ outbox 落列(limit.hold.created、aggregate_id=holdId)→ relay 標記已發布
生產(released) ✅ POST /api/v1/limit-holds/{holdId}/releases(10,000 分、部分釋放)→ outbox 落列(limit.hold.released)→ relay 標記已發布
Kafka ✅ 主題 cc.limit.limit.hold.created/cc.limit.limit.hold.released 皆自動建立;消費得信封,aggregateId=holdId(分區鍵正確)、sequenceNo 遞增
payload 契約 ✅ created={holdId, accountId, holdAmount:30000, holdType:"GENERAL"}、released={holdId, accountId, releasedAmount:10000}——與 SDD-006 §4.2 逐欄一致;⚠️ releasedAmount 為本次釋放額(非累計)
單元測試 ✅ LimitEventFactoryTest 4 例(含 payload 欄位數恰為契約值、不含 authorizationId 之守門)

7.7 繳款入帳事件消費(payment.payment.posted.v1)——2026-10-01 新增(GOV-006 4.8 W-2 推廣・CR-027 批次 A-1)

緣由:SAD-002 §3.2 明列本服務訂閱 payment.payment.posted.v1(恢復額度);
SCS-008 §8 亦載「limit-service(額度回復)經 Transactional Outbox 事件非同步通知」。
惟消費端從未實作——used_amount 因此永遠不會因繳款而下降(SCS-002 §10 #3 之「已知限制」)。
本節即補上;同時解除 §10 #3 之部分限制。

訂閱:主題 cc.payment.payment.posted;群組 limit-service。

處理:

  1. 冪等:eventId 寫入 consumed_events(與 used_amount 更新同交易);已存在 → 略過。
  2. 亂序防護:sequenceNo 落後於該 aggregateId 已消費之最大值 → 略過並 WARN。
    ⚠️ 本服務之亂序判準取自 consumed_events 之 MAX(sequence_no) WHERE aggregate_id = ?
    (不新增欄位至權威主檔 account_limits——理由見下)。
  3. 恢復額度:used_amount = GREATEST(0, used_amount − amount),version + 1(僅此一欄)。
  4. 未知 eventType:略過並 WARN(不拋錯,SAD-004 §5.4)。

★ 設計要點(每一項皆為「不臆測」之選擇):

決策 理由
恢復金額 = amount(繳款總額),不依 allocations[] 細分 used_amount 之語意為「已用額度總額」(§7.1)。繳款使未清償餘額下降 amount——子帳(稅/費/息/本金)之細分屬帳務(posting)之職責,額度讀模型不需也不應複製該細分
GREATEST(0, …) 下限 0 溢繳(繳款 > 欠款)時 used_amount 不得為負;超出部分屬帳務之貸方餘額,非額度概念
不使用 payload 之 availableLimitAfter 該欄位恆為 null(SCS-008 §10 #5 之既有設計);且額度之權威在本服務——採用外部系統算出之額度值=權威外移。本服務自行由 used_amount 推算,權威不出門
查無 account_limits 主檔 → 記 WARN、略過、且「不寫 consumed_events」 account_limits 為權威主檔,不得因一則繳款事件而憑空建列。不寫 consumed_events 係為維持「已記=已套用」之不變式(否則重放時會漏套用)
亂序判準置於 consumed_events 而非 account_limits account_limits 為權威主檔,不應為事件消費而增設技術欄位(與 authorization-service 之 card_status_caches——純讀模型——情況不同)
不快取 —

真機驗證(2026-10-01;真實 Oracle + Kafka;生產端與消費端同一輪):

驗證 結果
遷移 ✅ CC_LIM.consumed_events(V5)建立成功
生產端(payment-service) ✅ POST /api/v1/payments(200,000 分)→ status=POSTED、LIMIT_RESTORED=1 → outbox 落列 → relay 標記已發布
Kafka ✅ 主題 cc.payment.payment.posted 自動建立;消費得信封,aggregateId=paymentId
payload 契約 ✅ paymentId/accountId/amount=200000/allocations[]/availableLimitAfter=null(如實,非臆造)
消費端(★ 權威變更) ✅ account_limits.used_amount 500,000 → 300,000(version + 1);consumed_events 落列
冪等 ✅ 重送同 eventId → used_amount 仍 300,000、consumed_events 總數不變
亂序防護 ✅ 送 sequenceNo=1(落後)→ used_amount 仍 300,000(未被重複扣減)
溢繳下限 ✅ 送 amount 大於 used_amount → used_amount 收斂至 0(不為負)
單元測試 ✅ PaymentPostedProjectionServiceTest 4 例

⚠️ 未竟(誠實揭露):(i) UNMATCHED(重新佔額)與 REVERSED 之事件未實作——
故「繳款後又轉未匹配」時,額度不會被重新佔用(SCS-008 §8.2 之 LIMIT_RESTORED=0 語意未傳遞到本服務);
(ii) 亂序判準為 per-aggregate(paymentId)——每筆繳款僅發一次事件,實務上鮮少觸發,仍依 SAD-004 §5.4 實作以求一致。


7.8 繳款「重新佔用」事件消費(payment.payment.unmatched.v1/.reversed.v1)——2026-10-01 新增(CR-030 §2 #9)

緣由(★ 真缺陷之修補):SCS-008 §7/§8.2 明載繳款轉 UNMATCHED(逾三日未收銷帳檔)與沖正
皆會撤銷額度恢復(LIMIT_RESTORED = false),惟原僅發射 posted.v1 →
額度不會被重新佔用 → 可用額度永久高估(超額放行風險)。本節即補上該消費端。

訂閱主題:cc.payment.payment.unmatched 與 cc.payment.payment.reversed(SAD-004 §5.4 命名規則)。

項目 值
事件型別 payment.payment.unmatched.v1/payment.payment.reversed.v1
分區鍵 paymentId(信封 aggregateId)
消費效果 重新佔用額度:account_limits.used_amount += amount(restoreUsedAmount 之反向)

⚠️ 為何本消費「不是」重做同步已做之寫入(CR-030 §2 #16 之界限):
「撤銷額度恢復」目前無任何路徑涵蓋(markUnmatched 為批次、reverse 為 API,皆未通知 limit)。
#16 禁止的是「重複做同步已做的事」;不禁止「做沒人做的事」。

與 §7.7(恢復)之關鍵差異:本事件之 payload 帶 currency(CR-030 §2 #10 通則)
→ 可精確定位額度主檔(WHERE account_id = ? AND currency = ?),
無須 §7.7 之「該帳戶恰 1 列才更新」保守規則;未帶 currency 時仍走保守 fallback(容忍舊事件)。

處理(複用 §7.7 之三步,皆於同一交易):
① 冪等(consumed_events.event_id)→ ② 亂序(sequence_no <= MAX(aggregate_id))→
③ reoccupyUsedAmount + 寫 consumed_events。
⚠️ 查無主檔時「不寫 consumed_events」——維持「已記=已套用」之不變式。

7.9 專用額度(POC-8)之寫入與查詢——2026-10-02 新增(CR-014 v1.1)

緣由:applyAdjustedAmount 之 switch 無 SPECIAL 分支(原註解「SPECIAL 屬另一張表,
本階段不處理」)→ POST /limit-adjustments(adjustType=SPECIAL)必然失敗;
且查詢端點(C-11)當時讀 LIMIT_ADJUSTMENTS(調整視角),非專用額度之現行定義。

★ 前置查證(CR-014 v1.1 §0):CC_LIM.SPECIAL_LIMITS 早已存在——
由 V1__baseline.sql 建立,該檔係 tools/gen_flyway_baseline.py 依 SDD-001 §3.4 自動產生。
CR-014 v1.0 曾誤稱之為「新表」(根因:「實測」只掃 Java、未掃 db/migration 之 DDL)。
故本節之工作為「接線既有表」,非建表。

7.9.1 權威分工(CR-030 P1:單一真實來源)

表 職責 生命週期
limit_adjustments 申請/覆核/撤銷之稽核軌跡(誰申請、誰覆核、何時、為何) 不可變異動紀錄(狀態機 §3.2)
special_limits 專用額度之現行定義(用途、金額、效期、狀態) 可變現行狀態(8-1 之增/改/刪)

→ 兩者不是重複:前者答「這筆額度是怎麼來的」,後者答「現在這筆額度是多少」。

7.9.2 值域對映(special_limits 之 CHECK 為權威)

本服務概念 limit_adjustments.status(軌跡) special_limits.status(定義)
待覆核 DRAFT/PENDING_APPROVAL 不入本表(未生效者無現行定義)
已生效 EFFECTIVE ACTIVE
已撤銷 REVOKED CANCELLED
已到期 EXPIRED EXPIRED

⚠️ PENDING_APPROVAL 刻意不寫入 special_limits——本表之語意為「現行有效之定義」,
未生效之申請若寫入,將使 status='ACTIVE' 說謊(授權端可能據以放行)。

purpose 值域(不自由化,沿用既有 CHECK):
TAX/INSURANCE/TUITION/MEDICAL/OTHER。
不合法之 purpose → CC-LIM-1003(「專用額度不可用/未設定或已過期」——既有錯誤碼即為此設計)。

7.9.3 寫入路徑

  1. apply() 之基準額度:SPECIAL 之 before 取自「現行有效之同(歸戶, 幣別, 用途)專用額度」(無則 0),
    不是主檔之 permanent_amount。
    ⚠️ 否則覆核門檻(§7.2,以「增幅」判定)會以錯誤基準計算 → 該送覆核者可能直接生效。
  2. applyToMaster():SPECIAL → SpecialLimitRepository.upsertActive(...)(同一交易),
    不走 applyAdjustedAmount。
    • effectiveFrom = 請求之 tempStartDate,未帶則取今日(本表該欄 NOT NULL)。
    • 主檔(account_limits)之存在仍為前提(§7.2 之既有檢核不變)——
      8-4「未設定則用卡人額度」之前提是卡人額度存在。
  3. revoke():SPECIAL → cancelActive(...)(ACTIVE → CANCELLED);
    ⚠️ 不得走 applyAdjustedAmount(該表無 SPECIAL 分支 → 會誤報「回復額度失敗」)。
  4. upsertActive 之語意:先 UPDATE ... WHERE account_id=? AND currency=? AND purpose=? AND status='ACTIVE';
    影響 0 列 → INSERT。歷史列(CANCELLED/EXPIRED)保留,不覆寫。

7.9.4 唯一性不變式(★ 防「重複計算專用額度」)

CREATE UNIQUE INDEX uk_special_limits_active
    ON special_limits (account_id, currency, purpose,
                       CASE WHEN status = 'ACTIVE' THEN 'ACTIVE' END);
  • 語意:一歸戶 × 一幣別 × 一用途,至多一筆 ACTIVE。
  • Oracle B-tree 唯一索引不索引全 NULL 鍵 → 非 ACTIVE 之歷史列可留多筆。
  • 為何必要:若同(歸戶, 幣別, 用途)存在兩筆 ACTIVE,授權端之專用額度檢核
    將重複計算(可用額度被高估)→ 屬超額放行風險。
  • 併發:兩請求同時對同一鍵 upsert 時,後者可能因唯一鍵衝突而失敗(fail-closed),
    不會產生重複列——寧可失敗,不可多算。

7.9.5 查詢端點(GET /api/v1/accounts/{accountId}/special-limits)

  • 來源:special_limits(現行定義),條件 account_id = ?、status = 'ACTIVE',
    依 purpose 排序。不再讀 LIMIT_ADJUSTMENTS。
  • 回應:SpecialLimitsView{ accountId, count, items[] };
    SpecialLimitItem{ specialLimitId, accountId, currency, purpose, amountMinor, usedAmountMinor, status, effectiveFrom, effectiveTo }。
  • ★ 契約破壞性變更(CR-014 v1.1 §7):欄位由「調整視角」改為「定義視角」
    (移除 adjustmentId/applicantId、值域改 ACTIVE/EXPIRED/CANCELLED、
    tempStartDate/tempEndDate → effectiveFrom/effectiveTo、新增 usedAmountMinor)。
    經查證消費端為零(前端無引用)→ 直接修正 schema,不另立 v2。
  • 無資料 → 空清單(count=0),非 null。

7.9.6 到期排程(併入 §7.4 之排程)

ACTIVE 且 effective_to < 今日 → EXPIRED(同一交易、批次處理)。

為何必要(非「錦上添花」):若不轉態,

  1. status='ACTIVE' 說謊——查詢端點將回傳已過期之專用額度;
  2. uk_special_limits_active 會擋住同(歸戶, 幣別, 用途)之新額度(新申請之 INSERT 撞唯一鍵)。

7.9.7 真機驗證(2026-10-02;真實 Oracle 23.26 + Keycloak M2M + 服務 8083)

前置:V6 遷移經 Flyway 套用(真機複查:special_limits 新增 CURRENCY CHAR NOT NULL;
索引 IDX_SPECIAL_LIMITS_ACCOUNT(account_id, status)與
UK_SPECIAL_LIMITS_ACTIVE(account_id, currency, purpose, SYS_NC00013$←函式索引)皆已建立)。

# 驗證 結果
① 基線查詢 ✅ GET /accounts/{id}/special-limits → count=0(表原為 0 列)
② SPECIAL 申請(未超限) ✅ POST /limit-adjustments(TAX/200,000 分)→ 201、status=EFFECTIVE、beforeAmount=0(基準取自專用額度池,非主檔 10,000,000)
③ 查詢反映 ✅ count=1、purpose=TAX、amountMinor=200000、status=ACTIVE、effectiveFrom=2026-10-02
④ upsert(同用途再次申請) ✅ 再送 TAX/300,000 → beforeAmount=200000(讀到既有專用額度)→ special_limits 仍為 1 列(UPDATE,非新增)
⑤ 負樣本:purpose=CREDIT ✅ 422 CC-LIM-1003(訊息含允許值),未落庫
⑥ 負樣本:purpose 未帶 ✅ 422 CC-LIM-1003(不得以 null 落庫觸發 ORA-02290)
⑦ 超門檻 → 待覆核 ✅ MEDICAL/600,000(增幅 > 500,000)→ PENDING_APPROVAL;★ special_limits 無 MEDICAL 列(未生效者不寫入)
⑧ 覆核通過 ✅ POST …/{id}/approval(APPROVE)→ EFFECTIVE;special_limits 出現 MEDICAL/600,000/ACTIVE
⑨ 撤銷 ✅ POST …/{id}/revocation → REVOKED;special_limits 該列轉 CANCELLED;查詢清單僅剩 MEDICAL
⑩ ★ 權威來源(非快取)證明 ✅ 繞過服務直接 UPDATE special_limits SET amount=777777 → GET 立即反映 777777(若為快取則仍為 600,000)
⑪ ★ 到期(排程 + 讀側日期條件) ✅ 直接將 effective_to 設為昨日:排程執行前查詢即已 count=0(日期條件把關,非只靠 status);約 40 秒後排程將其轉 EXPIRED
⑫ ★ 不佔卡人額度(8-3) ✅ 全程 account_limits 之 permanent_amount/version 完全未變
⑬ 軌跡 vs 定義之分離 ✅ limit_adjustments 保有 5 筆 SPECIAL 申請/覆核紀錄(含 before_amount/after_amount/applicant_id),與 special_limits 之現行定義各自為政、互不取代
單元測試 ✅ LimitAdjustmentServiceSpecialTest 15 例(0 失敗);LayeringTest 通過
掃描器 ✅ scan_scaffold/scan_mockdata/scan_faketest 各 0 命中;validate_sql 0 違規;verify_contract_dto(limit-service)0 不符

⚠️ 併同修正之契約缺陷(真機測試揭露):POST /limit-adjustments 之實作原以
targetAmount 為 JSON 欄位名,而契約宣告 amount → 依契約送件者一律 400
(CC-PLT-4002 請求主體格式錯誤,實測);且實作之 applicantId 為必填而契約漏列
(送件後回 CC-PLT-4003 請求參數驗證失敗:applicantId 不得空白)。
已一併修正:實作欄位改名為 amount、契約補 applicantId(必填)與 currency。
根因:verify_contract_dto 僅核對回應型別,請求 DTO(巢狀 record)不在關卡內 → 靜默漂移。

7.10 專用額度池之佔用與適用性(POC-8 之 8-3/8-5)——2026-10-02 新增(CR-035 §2.1/§2.2)

緣由:CR-014 v1.1 完成後,專用額度可設定但不能動用——授權端完全不查本表,
故 BR-04-01(不佔卡人額度)與 8-5(超額拒絕)皆不成立。本節補上「池」這一半。

7.10.1 用途範圍(scope_ref;CR-035 §2.1)

值 語意
NULL/空白 不限定——該用途之任何 MCC 皆可動用
非空 逗號分隔之 MCC 白名單(如 9311,9399);交易之 MCC 須恰在其中

選擇順序(多筆命中時;保證決定性):
① scope_ref 非空且命中者優先(較特定)→ ② scope_ref 為 NULL 者 → ③ 依 purpose、id 升冪。

⚠️ 交易 MCC 為空且 scope_ref 非空 → 不命中(無法證明,不臆測)。

7.10.2 池之佔用與釋放

動作 效果
hold(specialLimitId 非空) special_limits.used_amount += 金額(條件更新:used_amount + ? <= amount)+ limit_holds 寫入 hold_type='SPECIAL'、special_limit_id
hold(specialLimitId 為空) 既有卡人路徑不變(hold_type='GENERAL'、special_limit_id 為 NULL)
release 若該佔用有 special_limit_id → 同交易 used_amount -= 釋放額(GREATEST(0, …))

★ used_amount 之語意為「佔用中」(非「已入帳未清償」)——專用池之請款比對訊號即為「釋放」。
(8-6/8-7 之臨時額度串動為另一條路徑,見 CR-035 §2.4/Phase 3。)

★ 為何是條件更新而非「先查後改」:兩筆併發授權可能同時通過檢查 → 超額佔用專用池。
以單一 UPDATE … WHERE used_amount + ? <= amount 原子完成「檢查+佔用」,
影響 0 列即如實失敗(CC-LIM-1003)。

7.10.3 適用性端點(GET /accounts/{accountId}/special-limits/applicable)

參數 說明
currency 幣別(預設 TWD)
mcc 交易之 MCC(可選)
amount 交易金額(分)

回應 SpecialLimitApplicabilityView:applicable/specialLimitId/purpose/scopeRef/
availableAmountMinor/reason。

★★ applicable=false 回 200,非 4xx——「無適用」與「池內不足」皆為正常結果。
如此授權端才能區分:

  • applicable=false → 退回卡人額度(8-4)或拒絕(8-5);
  • 5xx/連線失敗 → Stand-in 降級(SCS-001 §7.16)。

若兩者同以 4xx 表達,授權端將無法區分「客戶沒有專用額度」與「額度服務壞了」。

7.10.4 真機驗證(2026-10-02;真實 Oracle + 真實授權鏈路 8081 → 8083)

環境:V7 遷移套用;歸戶 …0002(卡 4222222222222222,account_limits.permanent=410,000);
TAX 專用額度 amount=20,000、scope_ref='9311,9399';cardcore.auth.manual-review 門檻 20,000 分。

# 驗證 結果
① scopeRef 可經 API 設定 ✅ POST /limit-adjustments(scopeRef="9311,9399")→ 201;DB scope_ref 落地
② 適用性:MCC 命中 ✅ mcc=9311 → applicable=true、availableAmountMinor=20000
③ 適用性:MCC 不命中 ✅ mcc=5411 → applicable=false、reason=查無適用…(無命中)
④ ★★ 8-3:走專用池 ✅ POST /authorizations(9311/15,000 分)→ APPROVED;DB:limit_holds.hold_type='SPECIAL' 且 special_limit_id 已設;special_limits.used_amount=15000
⑤ ★★ 不佔卡人額度(BR-04-01) ✅ ④ 之 availableLimit 為 310,000(=410,000 − 既有 GENERAL 佔用 100,000;SPECIAL 佔用不計入);account_limits 之 permanent/used/version 全程未變
⑥ 8-4:MCC 不符 → 回歸卡人池 ✅ 同卡同額、mcc=5411 → APPROVED;hold_type='GENERAL'、special_limit_id 為 NULL;availableLimit 310,000 → 295,000(確由卡人池扣減)
⑦ 專用池不足 → 適用性 false(仍回 200) ✅ reason=適用之專用額度不足:可用 5000、請求 15000;specialLimitId 仍回報(供判讀)
⑧ 池不足時回退卡人池 ✅ 同筆 → APPROVED、availableLimit 295,000 → 280,000(走卡人池)
⑨ ★ 8-5:兩池皆不足 → 拒絕 ✅ 將 permanent 暫降為 5,000 → DECLINED/respCode 51、holdId=null;checkResults 之 LIMIT=false、其餘 true
⑩ 釋放 → 專用池對稱回沖 ✅ POST /limit-holds/{id}/releases(15,000)→ 200;used_amount 15000 → 0
⑪ 併發防護(設計) ✅ 以 UPDATE … WHERE used_amount + ? <= amount 原子檢查;影響 0 列即拒絕(單元測試覆蓋)
單元測試 ✅ limit-service 41 測 0 失敗(新增 SpecialLimitPoolTest 13);authorization-service 65 測 0 失敗(新增 5)
掃描器 ✅ scan_scaffold/scan_mockdata/scan_faketest 各 0 命中;validate_sql 0 違規;verify_contract_dto(limit-service)0 不符

⚠️ 驗證方式之揭露:授權端(8081)之 ACL 不收任何服務(SAD-002 §5:authorization-service: []),
故 e2e 期間暫時以 CARDCORE_SECURITY_ENABLED=false 啟動 authorization-service 與 limit-service
(否則出站憑證為 NoOp → limit-service 回 401 → 全數 Stand-in)。
驗證後已還原為 SSO 開啟。此為環境操作,非程式變更。

7.11 專用額度之「可設定上限」政策(POC-8 之 8-2)——2026-10-02 新增(CR-035 §2.3)

緣由:BRD-001 §5.4 BR-04-03「額度異動之可設定上限依角色 + 客層參數化決定」。
在此之前專用額度無任何上限機制(僅受 purpose 值域與覆核門檻約束)。

7.11.1 政策表(special_limit_policies,V8)

欄位 說明
role_code 角色——★ 權威(見 §7.11.2)
segment_code 客層;* = 該角色不限客層
currency 幣別
max_amount 可設定上限(分)
status ACTIVE/INACTIVE(★ 停用以此表達,不做 DELETE——政策屬稽核相關設定)

UNIQUE (role_code, segment_code, currency)。

7.11.2 ★★ 角色之來源:自「已驗證之權杖」決定,不接受請求帶入

序 條件 role_code verified
① 服務權杖:azp 以 cc- 開頭(與 ServiceCallerAclManager 同一判準) azp(如 cc-payment-service) true
② 使用者權杖:realm_access.roles(去 ROLE_ 前綴、排除預設/雜訊角色) 字典序最小者 true
③ 皆無(認證停用/匿名) UNVERIFIED false(併記 WARN)

★★ 序 ① 必須先於序 ②——實測:M2M 權杖之 realm_access.roles 為
["default-roles-cardcore", "offline_access", "uma_authorization"],皆為無區辨力之預設角色;
若先取 realm roles,角色會被解析為 offline_access(荒謬),政策永遠查不到。
此為真機驗證時發現(第一版實作即踩此坑),已修正並補單元測試釘住。

★ 為何「取字典序最小」而非「取最寬鬆」:多角色使用者之語意本就不明;
取最小者決定性且不因角色組合而放寬。
★ 序 ③ 為 fail-closed 之樞紐:認證停用時角色為 UNVERIFIED,
必須顯式設定 role_code='UNVERIFIED' 之政策才會通過——不會「因為沒認證所以放行」。

7.11.3 客層之來源(★ 非權威,如實揭露)

segment_code 由請求帶入(可選,未帶 = *)。為何無法自動取得(真機查證):

# 障礙 證據
1 CC_LIM 無任何產品/卡等欄位 all_tab_columns 實查
2 card-service 之 ACL 只接受 cc-authorization-service cardcore-service-acl.yml
3 party-service 之 ACL 不含 cc-limit-service 同上
4 CC_PARTY.ACCOUNTS 無客層欄位(且全庫無 SEGMENT/LEVEL) 實查

→ 自動取得需同時開放兩個 ACL + 補讀模型,與 8-2 之價值不成比例。
★ 故 segment_code 定位為「政策查詢鍵」而非授權判斷(真正的權限在角色,而角色已不可偽造)。
後續:若客戶要求自動取得,需三項變更(見 CR-035 §5 #2)。

7.11.4 檢核與查詢

檢核時點:apply(),adjustType='SPECIAL' 時——afterAmount ≤ 適用政策之 max_amount。
僅 SPECIAL 適用(永久/臨時/彈性之權限上限屬 §7.2 既有範疇)。

查詢優先序(決定性):(role, segment) → (role, '*') → 皆無 → 拒絕
(fail-closed,CC-LIM-1002)。

⚠️ 不設 ('*','*') 萬用列:那會使「忘了設定政策」與「刻意全域放行」無法區分。

7.11.5 政策維護端點

端點 說明
GET /api/v1/special-limit-policies 列出全部政策(維運/DEMO)
PUT /api/v1/special-limit-policies/{roleCode}/{segmentCode} 新增或更新(冪等);停用 = status=INACTIVE

⚠️ 端點本身之權限(誠實揭露):目前僅受本服務之一般認證保護——
任何已驗證之呼叫者皆可設定政策上限。正式應限定特定角色,
惟平台無 RBAC 資料源可資判斷 → 列後續(CR-035 §5 #3)。

7.11.6 真機驗證(2026-10-02;真實 Oracle + Keycloak M2M)

# 驗證 結果
① ★ 角色解析 ✅ 角色為權杖之 azp=cc-payment-service(非 offline_access/default-roles-cardcore)
② 申請 ≤ 上限 ✅ 200,000 ≤ 300,000 → 201(EFFECTIVE)
③ ★ 申請 > 上限 → 拒絕 ✅ 400,000 > 300,000 → 422 CC-LIM-1002「超過可設定上限:申請 400000、上限 300000(角色 cc-payment-service/客層 *)」
④ ★ 上限為活值(非寫死) ✅ PUT 上限調為 500,000 → 同一請求 → 201
⑤ 角色間互不影響 ✅ 另設 cardcore-csr 上限 100,000 → 本次呼叫不受影響
⑥ 查無政策 → 拒絕(fail-closed) ✅ 見 #A-155 之首次驗證(未設政策時 422「查無適用之可設定上限政策」)
單元測試 ✅ limit-service 55 測 0 失敗(新增 SecurityContextCallerIdentityTest 9 + 政策檢核 5)

⚠️ 序 ③(認證停用 → UNVERIFIED)以單元測試覆蓋(SecurityContextCallerIdentityTest),
未於真機重跑——真機驗證該路徑需暫時關閉認證(連帶影響其他驗證)。

7.12 臨時額度串動(POC-8 之 8-6/8-7)——2026-10-02 新增(CR-035 §2.4)

緣由:BR-04-02「專用額度請款後,須調高卡人臨時額度至繳款截止日 + 7 日;繳款後調降」。

7.12.1 8-6:請款 → 調高(消費 posting.transaction.posted.v1)

★ 本服務原已列為該事件之消費者(SAD-002 §3.2),惟從未實作。

處理(同一交易):
① 依 payload 之 authorizationId 查 limit_holds(hold_type='SPECIAL')——
查無即不處理(該筆未用專用額度,正常略過);
② 釋放該佔用(授權請款比對,BR-03-02)→ 專用池 used_amount 對稱回沖;
③ 調高臨時額度:temporary_amount += 請款金額(★ 相對增量,非 applyAdjustedAmount 之絕對值);
④ 寫入串動紀錄(供 8-7 精確扣回)。

契約缺口之補正:posted.v1 原 payload 不含 authorizationId → 補為可選欄位
(來源 CC_POSTING.TRANSACTIONS.AUTHORIZATION_ID 早已存在,不需臆造;
依 SAD-004 §5.3 不升版)。⚠️ 缺該欄之舊事件 → 正常略過(不臆測)。

7.12.2 ★★ 繳款截止日之來源(如實揭露)

本服務之 schema 為 CC_LIM,不得讀 CC_STMT(跨 schema,SAD-003/SQL-07)。
故迄日以「入帳日 + 帳單週期(預設 25)+ 7 日」推算,並於紀錄標記
due_date_source = DERIVED——不臆造為「帳單之繳款截止日」。

⚠️ CR-035 §2.4 之原設計在此更正:原載「以 CC_STMT.STATEMENTS.payment_due_date
之最近一期為準」——該讀取為跨 schema,不可行。
精確值需 statement 整合(或於 posted.v1 補 paymentDueDate),列後續。
可配置:cardcore.limit.special-temp-raise.statement-cycle-days/.grace-days。

7.12.3 8-7:繳款 → 調降(併入既有之繳款事件消費端)

由 PaymentPostedProjectionService(§7.7)於恢復額度之同一交易內呼叫:
依串動紀錄先進先出回收 → temporary_amount -= 回收額(GREATEST(0, …)),
回收完畢轉 CLOSED。

★★ 為何必須逐筆紀錄、不得直接歸零:account_limits.temporary_amount 可能同時承載
「人工核准之臨時額度」(§7.2 之申請流程)——直接歸零會誤砍客戶經核准之額度。

★ 多幣別 → 不臆測:繳款事件之 payload 無 currency(§10 #10)→
未回收紀錄橫跨多幣別時略過並 WARN(寧可不回收,不可錯扣)。

7.12.4 真機驗證(2026-10-02;真實 Oracle + 真實 Kafka)

# 驗證 結果
⓪ V9 遷移 ✅ CC_LIM.special_limit_temp_adjustments 建立
① 前置:建立專用額度佔用 ✅ hold_type='SPECIAL';used_amount=15000;temporary_amount=0
② ★ 8-6:發佈 posted.v1(帶 authorizationId) ✅ temporary_amount 0 → 15000;temp_start=2026-10-02、temp_end=2026-11-03(=入帳日+25+7);串動紀錄 1 筆(raised=15000、due_date_source=DERIVED、OPEN)
③ ★ 請款比對釋放佔用 ✅ 專用池 used_amount 15000 → 0
④ ★ 8-7:繳款 10,000(部分回收) ✅ temporary_amount 15000 → 5000;紀錄 recovered=10000、仍 OPEN
⑤ ★ 8-7:再繳 5,000(回收完畢) ✅ temporary_amount → 0;紀錄 recovered=15000、CLOSED
單元測試 ✅ limit-service 64 測 0 失敗(新增 SpecialLimitTempAdjustmentServiceTest 9);posting-service 35 測 0 失敗(新增 2)

⚠️ 驗證方式之揭露:②/④/⑤ 以真實 Kafka 發佈事件驗證消費端(真實 Kafka + 真實 Oracle);
生產端(posting-service 之 payload 補 authorizationId)以單元測試驗證
(posted.v1 恰 9 欄、authorizationId 存在且可為 null)——
未以真實 POST /postings 端到端重跑(該路徑需 posting 重啟與 pricing 相依)。

7.13 8-6 迄日之精確化(CR-036)——2026-10-02 新增

緣由:§7.12.2 之迄日為推算值(入帳日+帳單週期+7)。而真正的繳款截止日
=「該筆稅款所屬帳單之帳單日 + 25 天」,取決於「入帳日是否已過帳單日」
→ 推算式最長可高估近一個帳單週期。
為何不能直接算:帳單日(ACCOUNTS.STATEMENT_DAY)在 CC_PARTY,
而本服務之 schema 為 CC_LIM——跨 schema 讀取為架構級禁止(SAD-003/SQL-07)。

7.13.1 ★★ 關鍵洞察:精確值在原理上只有帳單出帳後才存在

在帳單出帳之前,「該筆稅款屬於哪一期帳單」尚未確定——任何在請款當下算出的值都是猜測。
→ 正解不是「算得更準」,而是「在精確值出現時校正」。

7.13.2 兩階段(調高 → 校正)

階段 觸發 動作 due_date_source
① 調高 posting.transaction.posted.v1 temporary_amount += 金額;迄日先給推算值 DERIVED
② 校正 statement.statement.issued.v1 以帳單之權威 paymentDueDate 覆寫 raise_until = paymentDueDate + 7;同步 temp_end_date STATEMENT

★ 校正之歸屬條件(避免套錯期別):只有「在該筆調高之後才出帳」的帳單才承載該筆稅款
→ 僅校正 status='OPEN' 且 due_date_source='DERIVED' 且 raised_on ≤ 出帳日 之紀錄。
校正後標記 STATEMENT → 不重複校正(冪等)。

「出帳日」之來源:事件信封之 occurredAt(該事件即「出帳」之事實)——
不由 paymentDueDate − 25 反推(那會把 25 天慣例硬編進程式)。

temp_end_date 之同步:= 該歸戶所有 OPEN 紀錄之最晚迄日——
★ 取最晚:不得因校正某一筆而縮短另一筆之窗口。

7.13.3 契約與架構:契約無需變更,僅新增一組訂閱

statement.statement.issued.v1 之 payload 早已包含 accountId/paymentDueDate
(SCS-011 §7.2)→ 不升版、不增欄。
架構變更:SAD-004 §5.2 之消費者 notification → notification + limit;
SAD-002 §3.2 之訂閱清單同步。不新增同步呼叫、不開放任何 ACL。

7.13.4 真機驗證(2026-10-02;真實 Kafka + 真實 Oracle)

# 驗證 結果
① 8-6 調高(推算) ✅ temp_end_date=2026-11-03、紀錄 raise_until=2026-11-03、due_date_source=DERIVED
② ★ 負樣本:出帳日 2026-09-30(早於調高日 10-02) ✅ 不校正(仍 2026-11-03/DERIVED)——該期帳單不承載此筆
③ ★★ 正樣本:出帳日 2026-10-02 + 截止日 2026-11-10 ✅ raise_until → 2026-11-17、due_date_source → STATEMENT;★ account_limits.temp_end_date 同步為 2026-11-17
單元測試 ✅ limit-service 69 測 0 失敗(新增 correctDueDate 相關 5)

7.14 商品參數消費閉環(CR-045 D4 首案)——2026-10-15 新增(CR-045 D4/#A-238)

緣由:CR-045 P2 指出本服務(額度)未消費任何商品參數——product-service 之 GET /api/v1/parameters/{ns}/{key}(SCS-005 §6)生產端已備,且 product.parameter.changed.v1 事件已發布(#A-130),但全庫無下游消費者。依 CR-045 §8 D1–D6 已採納之立場(2026-10-15 用戶授權 go):首案以「商品額度策略」參數驅動本服務某一既有多維度行為,建立「改權威參數值 → 額度行為改變」之閉環。
★ 本節已實作並真機驗證(#A-239,2026-10-15):先補規格與驗收案例(spec-first),再實作 limit-service 之商品參數消費閉環,並以真機測試證明「改權威參數值 → 行為改變」。

首案收斂(★ 不選 limit.model/shared_scope):該二鍵之多維度分層(account/card/family/corporate)為 OPEN 待裁定(A1),行為語意尚未凍結,不宜做為第一條閉環(否則會「邊寫邊猜」)。故首案改選已在 §7.2 完整定義、且可直接對應商品參數之政策輸入:

首案鍵:namespace=product、paramKey=limit.approval-threshold-minor(覆核門檻,分)。
對應既有行為:§7.2 之「增幅超過門檻 → 送 PENDING_APPROVAL,否則直接 EFFECTIVE」。現門檻為服務屬性 cardcore.limit.approval-threshold-minor(預設 500,000 分)→ 改由商品參數驅動,即成「商品組合 × 額度」之最小化閉環。

消費機制:

  • 新增 domain/port/ProductParameterPort(或 ProductCapabilityPort):getAsOf(namespace, paramKey, businessDate) -> ParameterValue。
  • 實作經 GET /api/v1/parameters/{ns}/{key}(帶 businessDate 取版本)同步取得(CR-045 §7.1:domain-owned synchronous API)。
  • ★ 首案不引入快取(CR-045 §5.3):快取與事件失效僅於「快取實際存在且失效需求經量測」後落地;先以「改權威值 → 行為即變」證明閉環,避免「讀快取=沒做」歧義。

真機驗收(★ 沿用 §7.7/§7.9.7 之「改權威值 → 行為隨變」範式):

# 驗證 預期
① 基線:現值 threshold=500,000 分;申請增幅 600,000 分 → PENDING_APPROVAL(現行行為)
② 改權威:PUT /api/v1/parameters/product/limit.approval-threshold-minor(value=1,000,000,生效日=今日) 200;CC_PRODUCT.PARAMETERS 落新版本
③ 同一申請(增幅 600,000) → 直接 EFFECTIVE(行為改變:門檻提高後未超過)
④ ★ 非快取證明:繞過服務 UPDATE CC_PRODUCT.PARAMETERS SET param_value=... WHERE param_key='limit.approval-threshold-minor' 下次取參即反映新值(若為快取則仍為舊值 → 證偽)
⑤ 版本:businessDate 取 effective_from 對應版本;舊版本區間不影響 行為隨生效日版本改變

★ 實作狀態(2026-10-15,#A-239):本首案已實作並真機驗證閉環(real Oracle via Testcontainers)。

  • 已自動化證明(驗收 ①+③):LimitAdjustmentServiceIntegrationTest$ProductParameterClosedLoop.paramValueChangeFlipsApprovalBehavior——baseline threshold=500,000 → 增幅 600,000 → PENDING_APPROVAL;改權威值 threshold=1,000,000 → 同增幅 → EFFECTIVE(行為隨權威值翻轉)。證明「改權威參數值 → 額度行為改變」閉環成立。
  • 未取得自動化證明(誠實揭露,不虛報全閉環):驗收 ②(PUT /api/v1/parameters/... 改 product-service)、④(繞過服務直改 CC_PRODUCT.PARAMETERS 即反映=非快取證明)、⑤(businessDate 版控)需 product-service 參數寫入路徑與 CC_PRODUCT.PARAMETERS 表俱備;本輪未建跨服務真機實驗(首案不引入快取、只讀取,且避免 mock/假造)。★ 這正是 CR-045 §5.3「不快取」之紀律:即便未做 ④,同步每次取參亦保證「下游改值 → 本服務下次取參即反映」。
  • A4 fail-closed 已落地:取參失敗(連線/4xx/5xx)轉 CC-LIM-5001,LimitAdjustmentService.resolveApprovalThresholdMinor 依 fail-closed 降級至預設門檻(cardcore.limit.approval-threshold-minor,預設 500,000 分)並 log.warn,不略過覆核檢核、不假造放行。
  • 驗收 ②/④/⑤ 已閉合(2026-10-16,#A-241):於 product-service 新增 ParameterClosedLoopIntegrationTest(真 Oracle via Testcontainers + 真 web 層)——② PUT /api/v1/parameters/product/limit.approval-threshold-minor 落新版本且現行版本自動封閉於 newFrom − 1;⑤ businessDate 取昨日得舊值、今日得新值(生效日版控);④ 繞過服務直改 CC_PRODUCT.PARAMETERS 後再次取參即反映新值(非快取證明);併同交易 Outbox 事件與 4 項負樣本(重疊 422 CC-PRD-1002/查無 404 CC-PLT-4040/空 body 400/缺 header 400)。★ 動工前查證更正前版註記:「待 product-service 參數寫入路徑與 CC_PRODUCT.PARAMETERS 表俱備」實為過度保守——三者本地早已具備(ParameterController 之 PUT、ParameterService 版本鏈、V1__baseline.sql 之 parameters 表),②/④/⑤ 並非被外部阻塞,僅缺跨服務真機實驗。
    ⚠️ 根因更正(2026-10-16,#A-241 之原解釋有誤,現予修正):#A-241 當時將「嵌入式 Tomcat 無法以 HttpClient 連上」歸因於「沙箱 HTTP 攔截代理攔截迴路埠」——該解釋不成立。實測定位:Tomcat 僅綁 IPv6 萬用字元,而 IPv4 迴路 127.0.0.1(localhost 優先解析至 IPv4)之連線會「掛住」而非「拒絕」;改用 hostname 或 [::1] 立即 HTTP 200。→ 測試位址須用 hostname(或 ::1),不可用 localhost/127.0.0.1。
  • ★ 已改為真 socket 閉環(2026-10-16,#A-242):根因既明後,ParameterClosedLoopIntegrationTest 改採嵌入式 Tomcat(server.port=0)+ JDK HttpClient 打真 socket,路徑對映/@RequestHeader 綁定/@Valid/Jackson/GlobalExceptionHandler 錯誤碼→HTTP 映射全部經由真實 HTTP 完成,六例全通過。⚠️ 位址鐵則:必須用 hostname(或 [::1]),不可 localhost/127.0.0.1(見上方根因)。
  • ③之真 HTTP 段仍未閉合:limit-service 之 ProductParameterHttpClient 經 socket 打到 product-service 之驗證需另立跨上下文測試;根因既明(改用 hostname 即可),此項已無技術阻礙,列為後續交付單元。

OPEN(待裁定/待客戶輸入,不臆測):

  • A1:limit.model/shared_scope 之 account/card/family/corporate 分層語意(D4 未決)→ 多維度額度閉環之先決。
  • A2:同步取參 vs 事件消費(product.parameter.changed.v1)——首案採同步;事件消費僅於快取需失效時引入。
  • A3:參數寫入授權(D6)——本首案只讀取,不開放本服務寫入;寫入 API 之 RBAC 待 D6 落地。
  • A4:取參失敗之降級(product-service 不可達)→ fail-closed——已落地(#A-239):依 CR-045 §7.14 A4 選「回預設門檻」(cardcore.limit.approval-threshold-minor,預設 500,000 分),log.warn 後降級、不略過覆核檢核、不假造放行;取參失敗拋 CC-LIM-5001(見 SDD-006 §3.4)。

⚠️ 不逾越邊界:不恢復 stale endpoint(D2)、不臆測行銷活動政策(D3)、不把 product.rule_sets 升格為活動模型(D3)。

7.15 載體(卡)級主額度——額度主體維度化(CR-053 D-1)——2026-10-17 新增(#A-285)

緣由:CR-052 §4.2 BEAM PMO 裁定 C-1 由 (i) 修正為 (ii)「卡片有獨立主額度」(客戶要求,
RFP FR.md §2.4(1)「歸戶、持卡人、卡片調額/查詢」)。原模型 account_limits 為
帳戶層(V10 明示「額度非以卡為單位」),與客戶要求衝突 → 開立 CR-053 承載資料模型。
BEAM PMO 2026-10-17 授權「架構自主推進」→ 本節依 CR-053 建議立場(D-1~D-5)落地第一批。

7.15.1 資料模型(V11__limit_subject_dimension.sql)

欄位/約束 內容
subject_type 額度主體維度:HOUSEHOLD/ACCOUNT/PARTY/CARD/CORP_SUB;既有列=ACCOUNT
subject_id 主體識別:ACCOUNT → account_id;CARD → instrument_id(V10 載體識別)
account_id 保留——卡片列仍記錄所屬帳戶(供歸戶彙總與稽核)
唯一鍵 由 (account_id, currency) 改為 (subject_type, subject_id, currency);原鍵在「同帳戶多卡」下必然衝突
自動補值 BEFORE INSERT trigger:subject_type='ACCOUNT' 且 subject_id IS NULL → 補為 account_id;其他維度不推導(缺值即失敗)

★ 為何用 trigger 而非直接 NOT NULL(實測教訓):既有 51 處寫入(事件處理與測試)
只帶 account_id;直接設 NOT NULL 會使既有功能全數寫入失敗(實測 51 個測試轉紅)。
trigger 使「ACCOUNT 維度之主體即帳戶」之語意單一且明確,相容既有路徑且不產生 NULL 維度。

7.15.2 查詢語意(GET /api/v1/instruments/{instrumentId}/limits)

★ 路徑用 instruments 而非 cards:本服務只認交易載體(V10:實體卡/虛擬卡號/
Token 同構);card→instrument 對映不在本服務(CR-052 C-5 實查:無直呼路徑),
以 cardId 猜 instrumentId 即臆造。對外 UI 之「卡片」由 BFF 對映。

可用額度口徑(★ 保守下界,非精確值):

可用 = 本卡(永久 + 有效臨時 + 彈性)− 帳戶已用 − 本卡可歸屬佔用      (下限 0)
分量 來源 說明
本卡額度 CARD 維度列 永久/臨時(有效期內)/彈性
帳戶已用 ACCOUNT 維度列 used_amount 權威;非本卡專屬
本卡佔用 limit_holds where instrument_id=? and status='HELD' 僅可歸屬者
holdAttribution 該帳戶是否存在 instrument_id IS NULL 之 HELD 佔用 FULL/PARTIAL

★ 為何不採「本卡已用」:已入帳交易不帶 instrument_id,歷史佔用亦不得回填
(V10/CR-053 D-4)→ 卡片級已用無權威來源。以帳戶已用扣減屬保守
(不會超額放行),但非精確;精確分攤待 CR-052 C-2 投影落地。
回應帶 holdAttribution=PARTIAL 如實揭露,不得據此宣稱精確可用額度。

★ 查無即拒絕(不回退帳戶額度):未設 CARD 維度主額度時拋 CC-LIM-1001,
不以歸戶額度冒充卡片額度(CR-050 §2.1 同類原則)。

7.15.3 守界

  • 不臆造維度歸屬:HOUSEHOLD/PARTY/CORP_SUB 為模型預留,現行無寫入路徑;
    指定時查無主檔即 fail-closed(企業子額度之隸屬與分攤待客戶口徑,CR-043 §10.5)。
  • 不由既有帳戶額度複製卡片額度(CR-053 D-4)——卡片主額度初始值須由客戶提供。
  • 不改既有帳戶層行為:restoreUsedAmount/reapply/調額等寫入一律限定
    subject_type='ACCOUNT'(否則會污染卡片列,且「恰 1 列才更新」之保守規則會被
    多維度列誤判為多幣別而永久停止更新)。
  • 卡片級調額(寫入)為後續批次(CR-052 C-3):LIMIT_ADJUSTMENTS 現為帳戶維度,
    擴充需另立遷移與流程,本批不放置半套端點。

7.15.4 驗收(真機)

# 驗證 結果
① V11 回填:既有列 subject_type='ACCOUNT'、subject_id=account_id,無 NULL 維度 ✅
② 同帳戶多卡並存(原唯一鍵會衝突);同(維度, 主體, 幣別)重複則拒 ✅
③ 可用額度=手算值(含臨時額度有效期、負值截斷至 0) ✅
④ 查無 CARD 主額度 → CC-LIM-1001,不回退帳戶額度 ✅
⑤ holdAttribution:全帶 instrument_id → FULL;存在歷史佔用 → PARTIAL ✅
⑥ 回歸:同帳戶有 CARD 列時,restoreUsedAmount/applyAdjustedAmount 僅動 ACCOUNT 列 ✅

7.16 卡片級額度調額寫入(CR-052 C-3)——2026-10-17 新增(#A-286)

緣由:CR-052 §4 BEAM PMO 裁定 C-3 含額度寫入(非僅查詢);CR-053 D-1 之維度化
(§7.15)已備資料基礎,本節補寫入路徑。
沿用既有 §7.2 覆核門檻與 SDD-005 §8 狀態機——不另立流程(避免兩套語意)。

7.16.1 請求維度之決定

請求 維度 主檔
帶 instrumentId CARD(subject_id=instrumentId) 該卡之 CARD 維度列
未帶 ACCOUNT(既有行為,完全不變) 該帳戶之 ACCOUNT 維度列
  • beforeAmount 取該維度之主額度(PERMANENT/TEMPORARY/SHADOW 對應欄位);
    SPECIAL 仍走專用額度池(§7.9.3),不因本節改變。
  • ★ 不得混用維度:卡片級調額只改 CARD 列,不得連動帳戶列(反之亦然)。
    (維度化後同帳戶多列,任何以 account_id 定位的寫入都會跨維度污染——§7.15.3 之教訓。)

7.16.2 無既有 CARD 列時:核准後建立(客戶提供初始值之唯一途徑)

情境 處理
該卡尚無 CARD 列 beforeAmount = 0;核准(或未達門檻直接生效)時建立 CARD 列,permanent_amount = afterAmount
已有 CARD 列 照常調整(UPDATE ... WHERE subject_type='CARD' AND subject_id=?)

★ 這不是「臆造」:值由呼叫方明確提供(targetAmount),且 CR-053 D-4 所禁者為
「由既有帳戶額度複製出卡片額度」。若無此路徑,新卡將永遠無法設定卡片額度(死路)。
★ 建立時之 account_id 由請求帶入(與帳戶級調額相同),本服務無 card→instrument
對映能力(CR-052 C-5),故歸屬未經驗證——須由呼叫端(BFF/card-service)保證,
並於 API 文件與本節如實揭露。

7.16.3 ★ 事件輸出之守界(重要)

limit.limit.adjusted.v1 之 payload 為「帳戶總額度」(§7.5),授權端據以更新讀模型
account_limit_caches。故:

  • ACCOUNT 維度 → 照舊發射(行為不變)。
  • CARD 維度 → 不發射 該事件。理由:卡片額度若以帳戶事件送出,將使授權讀模型之
    「帳戶總額度」被卡片額度覆蓋 → 授權可用額度嚴重失真(超額放行或誤拒)。
  • 卡片額度之授權取值走即時查詢(§7.15.2),不經該讀模型。
  • ⚠️ 若日後需事件化,應另立 limit.instrument.adjusted.v1 並先凍結 payload
    (SAD-004 §5.3)——本節不臆造事件。

7.16.4 驗收(真機)

# 驗證 預期
① 卡片級調額(無 CARD 列)→ 核准後 建立 CARD 列、permanent = after;ACCOUNT 列不變
② 卡片級調額(已有 CARD 列)→ 直接生效(未達門檻) 僅 CARD 列更新
③ 帳戶級調額(既有路徑回歸) 僅 ACCOUNT 列更新;CARD 列不變
④ 卡片級調額是否發 limit.limit.adjusted.v1 不發(CC_LIM.OUTBOX 無新增列)
⑤ 卡片級調額超過覆核門檻 PENDING_APPROVAL(與帳戶級同門檻)

7.17 共享池(共用額度)權利與卡片上限檢核(CR-055 P4-1)——2026-10-17 新增

口徑來源:BEAM PMO 2026-10-17(CR-055 §1 逐字保留)。
★ 一句話:共享池=總額度的另一面(共用額度),不是額外池。

7.17.1 三個概念(★ 不可混用)

概念 定義 承載
總額度 客戶在本行之共用上限;≠ 各卡額度之和 account_limits(ACCOUNT 維度)
卡片名目額度 核給該卡的金額 account_limits(CARD 維度)
共享池權利 該卡是否可動用總額度 account_limits.shared_pool_eligible(V13)

★ 「總額度 5 萬、三張卡 5/2/3 萬」→ 共用額度仍為 5 萬(不是 10 萬)。
2 萬那張若享共享池,最多可刷到 5 萬(不是 2+5=7 萬)。

7.17.2 可刷上限(授權 hold() 之檢核,風控底線)

享共享池:可刷上限 = 總額度 − 帳戶已用 − 帳戶全部未釋放佔用   (=帳戶可用)
不享共享池:可刷上限 = 卡片名目額度 − 該卡未釋放佔用          (硬上限)

★ 不享共享池者,卡額度與帳戶可用取較嚴者(兩者都要過)。
★ 無 CARD 維度列時不檢核(沿用帳戶額度)——向後相容:既有卡未設定卡片額度前行為不變。

★ 修正之真實缺陷:原 hold() 完全不看卡片名目額度(CARD 維度僅供查詢)→
「不享共享池」的卡會用到總額度(例:總額度 5 萬、卡額度 2 萬 → 可刷到 5 萬=超放 3 萬)。

7.17.3 查詢呈現(★ 兩個數字,不可相加)

欄位 語意
availableAmount 卡片自身可用=卡名目額度 − 該卡佔用(不扣帳戶已用)
sharedPoolEligible 是否享共享池
sharedPoolAvailableAmount 可動用總額度之剩餘(=帳戶可用);★ 僅 eligible=true 有值,否則 null

★ BEAM PMO:「其卡片級的『可用』仍是他原本的額度」——故 availableAmount 不扣帳戶已用
(此為 CR-055 對 SCS-002 §7.15.2 舊「保守下界」口徑之修正)。

7.17.4 參數(商品級)與快照

參數鍵(namespace=product) 值域 說明
limit.shared-pool-eligible true/false 該商品之卡是否可動用總額度;缺值 fail-closed

★ 參數 → 卡片快照:設定卡片額度時由呼叫端帶入 sharedPoolEligible 並落地於 CARD 列;
事後改參數不改變已設定之卡片語意(留痕原則)。
⚠️ 本服務無 card→product 對映(CR-052 C-5),故不自行查商品參數。

7.17.5 驗收(真機)

# 驗證 預期
① 不享共享池、卡額度 200,000、帳戶 1,000,000 → 刷 150,000 後再刷 100,000 拒絕(CC-LIM-1001,訊息含「不享共享池」)
② 享共享池、卡額度 200,000 → 刷 800,000(≤ 總額度) 通過
③ 無 CARD 列(既有卡)→ 刷 900,000 通過(向後相容)
④ 不享共享池、卡額度足但帳戶只剩 50,000 → 刷 100,000 拒絕(帳戶可用不足)

7.18 預借現金額度(CR-055 P4-2)——2026-10-17 新增(#A-296)

口徑(權威:CR-055 §2.5):

預借上限 = 總額度(永久+有效臨時+彈性) × ratio / 100     (向下取整)
預借可用 = min(預借上限 − 已預借, 有效可用)                  (下限 0)

★ 不是獨立額度:總額度 5 萬、ratio 80% → 上限 4 萬,總額度仍 5 萬(非 9 萬)。
★ 與一般消費共用同一總額度 → 預借佔用亦計入帳戶佔用。

7.18.1 資料模型(V15/V16)

變更 內容
V15 account_limits.cash_advance_ratio NUMBER(3)(可空、CHECK 0–100)+limit_adjustments.cash_advance_ratio(留痕)
V16 limit_holds.hold_type 值域加入 CASH_ADVANCE——V1 原值域只有 GENERAL/SPECIAL/INSTALLMENT,真機第一次執行即 ORA-02290(見 §7.18.5)

7.18.2 null ≠ 0(fail-closed)

值 語意 行為
NULL 未設定 預借一律拒絕(CC-LIM-1001,訊息「預借現金未開放…未設定預借現金比例」);查詢四欄皆 null
0 明確禁止 上限 0、可用 0;預借走「額度不足」(訊息與「未開放」可區分)
1–100 已設定 依上式計算

★ 不臆造預設比例(80% 只是 CR-055 之舉例,非規範)。

7.18.3 「有效可用」取較嚴者

卡之共享池權利 有效可用
享共享池(shared_pool_eligible=1) 帳戶可用(總額度−帳戶已用−帳戶全部佔用)
不享共享池(=0) 卡片自身可用(卡名目額度−該卡佔用)

★ 若一律用帳戶可用,不享共享池之卡即可用預借繞過卡片硬上限(§7.17.2 之缺陷以另一形式復活)。

7.18.4 授權檢核(hold(holdType='CASH_ADVANCE'))

順序:① 卡片上限(不享共享池時)→ ② 預借子上限 → ③ 帳戶可用。
②③ 兩者皆須通過(共用同一總額度)。

★ holdType 由呼叫端帶入——本服務不判斷「這筆是不是預借現金」。

7.18.5 ★★ 實作期攔下之真實缺口(值域未涵蓋)

V1 之 ck_holds_hold_type 值域 ('GENERAL','SPECIAL','INSTALLMENT') 無 CASH_ADVANCE
→ 真機測試第一次執行即 ORA-02290: 違反檢查限制條件。
這不是測試寫錯,是語意與 DDL 不一致:「已預借」唯一可識別的位置就是 hold_type。

修法:V16 以 user_constraints(search_condition_vc LIKE '%hold_type%')動態定位
系統命名約束後卸除,再以具名 ck_holds_hold_type 重建。
★ 不改成「無 CHECK」——值域即行為契約,放寬會讓打錯字靜默變成新型別。

7.18.6 驗收(真機;Testcontainers 真 Oracle+真 Flyway)

CashAdvanceLimitIntegrationTest 7 例:未設定拒絕/80% 上限 160,000 與可用 60,000/
預借佔用計入帳戶佔用(holdAmount=100,000、available=900,000)/超上限拒絕(訊息帶上限)/
0 與 null 可區分/不享共享池取卡片可用(40,000 vs 30,000 兩例)/調額帶入比例落地。


7.19 企業子額度與組織樹(CR-055 P4-3)——2026-10-17 新增(#A-296)

口徑(權威:CR-055 §2.6):

上層可用 = 上層總額度 − 上層已用 − 上層自身佔用 − **所有下層佔用之和**(下限 0)

7.19.1 資料模型(V17__corp_units.sql)

表/欄 內容
corp_units id/unit_no(唯一)/unit_name/parent_unit_id(根為 NULL)/root_unit_id/depth/settlement_account_id/status
limit_holds.corp_unit_id 佔用歸屬企業單位(可空);與 instrument_id 並存不互斥
額度列 account_limits:subject_type='CORP_SUB'、subject_id = corp_units.id

★ 為何獨立一張表:① 組織樹是主體結構、額度是掛在結構上的數字,生命週期不同;
② 同一單位可能有多幣別額度列(唯一鍵含 currency),樹只能有一份。

★ settlement_account_id 為何必要:account_limits.account_id 為 NOT NULL,
企業額度歸屬企業戶結算帳戶(由呼叫端提供、子單位繼承、不臆造)。

7.19.2 三個容易做錯的點

陷阱 後果 做法
只檢核自身可用 N 個子單位各花光 → 上層超放 N 倍 檢核走上層鏈(CONNECT BY PRIOR parent_unit_id = id),任一層不足即拒絕,並指出不足之層
把自身佔用算進「下層之和」 同一筆被扣兩次 → 可用低估一倍 corpUnitHolds 與 descendantCorpHolds 分開;下層不含自身
遞迴方向寫反 CONNECT BY 回空集合且不報錯 → 靜默低估 以真機三層樹測試鎖住(根=750,000)

7.19.3 端點

端點 說明
POST /api/v1/corp-units 建根/子單位(unitNo 冪等;已存在回既有、不覆寫)
GET /api/v1/corp-units/{unitId}/limit 額度查詢:同時回自身佔用與下層佔用(只回總數無法解釋「上層為何變少」)
PUT /api/v1/corp-units/{unitId}/limit 設定總額度(風控核定端點,非客戶申請路徑)
POST /api/v1/corp-units/{unitId}/holds 建立佔用(上層鏈檢核)

7.19.4 守界

  • 不發射 limit.hold.created.v1:該事件消費端語意為卡片授權佔用(payload 無企業單位維度),
    照發會讓投影出現「無載體佔用」→ 污染卡片級可用額度。企業額度以本服務即時查詢為權威。
  • 企業額度「申請→覆核」未接:LimitAdjustmentService 目前僅由 instrumentId 判斷 CARD 維度。
  • 停用端點未做(status='INACTIVE' 值域已在 DDL)。

7.19.5 驗收(真機;真遞迴 SQL)

CorpLimitIntegrationTest 6 例:三層樹 depth/root/歸屬帳戶繼承/
子佔用扣減上層(分公司 50,000、根 750,000)/上層不足即拒(訊息指出不足之層 B-003)/
未核定額度 fail-closed/設定額度後可佔用/重複 unitNo 不覆寫。

7.20 企業卡改綁(CR-058 C)——2026-10-18 新增(#A-301)

語意(權威:CR-058 §2):改綁改的是「未來歸屬」,不是「過去帳」。
既有未釋放佔用保留原 corp_unit_id(forward-only),新佔用歸新單位。

7.20.1 資料模型(V20__corp_unit_instrument_history.sql)

表 欄位 說明
corp_unit_instrument_history id/instrument_id/from_unit_id(可空)/to_unit_id/reason/changed_by/changed_at+created_at/updated_at/version 只記錄歸屬變更;不搬移 limit_holds

corp_unit_instruments 仍以一卡一單位(instrument_id PK)表達現行歸屬。

7.20.2 埠與服務

// CorpUnitRepository
Optional<UUID> rebindInstrument(UUID targetUnitId, UUID instrumentId,
                                String reason, String changedBy);

// CorpLimitService
RebindView rebindInstrument(UUID unitId, UUID instrumentId,
                            String reason, String changedBy, String currencyCode);
情境 行為
目標單位不存在/停用 CC-LIM-1003
reason 空 CC-LIM-1003(稽核必填)
instrument 未綁定 CC-LIM-1003(導引先 bind)
目標=現有單位 冪等,不更新、不寫歷史
目標≠現有單位 更新 corp_unit_instruments + 寫一列歷史(同一交易)

7.20.3 API

POST /api/v1/corp-units/{unitId}/instruments/{instrumentId}/rebind

{ "reason": "組織調整", "changedBy": "ops-1", "currency": "TWD" }

回應:unitId/instrumentId/fromUnitId/toUnitId/openHoldsAtOldUnit/changedAt。
openHoldsAtOldUnit 為原單位未釋放佔用(改綁不搬移,供維運判斷)。

7.20.4 驗收

CorpLimitIntegrationTest 新增 3 例:forward-only(舊佔用留原單位、新佔用歸新單位、歷史 1 列)/
同單位冪等(歷史 0 列)/未綁定拒絕。


7.21 企業額度申請→覆核(CR-058 D)——2026-10-18 新增(#A-301)

語意(權威:CR-058 §3):企業額度走同一狀態機(limit_adjustments 已支援 CORP_SUB,V12)。

7.21.1 主體與歸屬

欄位 值
subject_type CORP_SUB
subject_id corp_units.id
account_id corp_units.settlement_account_id(由單位解析,不臆造)
adjust_type PERMANENT

7.21.2 服務(CorpLimitAdjustmentService)

AdjustmentView apply(UUID unitId, String currency, long targetAmountMinor,
                     String reason, String applicantId);        // 申請(走門檻)
AdjustmentView applyDirect(UUID unitId, String currency, long targetAmountMinor,
                           String reason, String operatorId);   // 風控直接核定(留審計)
  • 門檻:product/limit.approval-threshold-minor;取參失敗降級組態預設 500,000(fail-closed)。
  • 直接核定以「極大門檻」送出 → 狀態 EFFECTIVE,但仍寫一筆 limit_adjustments。
  • CORP_SUB 不發射 limit.limit.adjusted.v1(payload 為帳戶總額度,照發會污染授權讀模型)。

7.21.3 API

端點 用途
POST /api/v1/corp-units/{unitId}/limit-adjustments 申請(targetAmount/currency/reason/applicantId)
POST /api/v1/limit-adjustments/{id}/approval 覆核(既有端點)
PUT /api/v1/corp-units/{unitId}/limit 風控直接核定(留 EFFECTIVE 軌跡)

7.21.4 職責分離

LimitAdjustmentService.approval:當 subjectType=CORP_SUB 且 APPROVE 且
approverId == applicantId → CC-LIM-3001(僅對 CORP_SUB 生效,不改既有維度行為)。

7.21.5 驗收

CorpLimitAdjustmentIntegrationTest 4 例:未超門檻直接生效/超門檻待覆核且核准後才套用/
申請人不得自核/直接核定留審計。


7.22 Stand-in 上傳載體與型態(CR-058 E)——2026-10-18 新增(#A-301)

語意(權威:CR-058 §4):授權當下已知 instrumentId/holdType,就必須保存並上送。

7.22.1 資料模型(V15__limit_holds_hold_type.sql)

變更 內容
V14(既有) limit_holds.instrument_id
V15(本批) limit_holds.hold_type VARCHAR2(32 CHAR)(可空)

兩者可空:舊資料不得臆造;上送時 instrumentId=null 照送、holdType 預設 GENERAL。

7.22.2 模型與埠

record PendingStandIn(String authNo, UUID authorizationId, UUID accountId,
                      UUID instrumentId, long amountMinor, String currency, String holdType) {}

record StandInAdviceState(String authNo, UUID authorizationId, UUID accountId,
                          UUID instrumentId, long amountMinor, String currency,
                          String holdType, Instant uploadedAt) {}

// LimitUploadPort
UUID uploadHold(UUID accountId, UUID instrumentId, UUID authorizationId,
                Money amount, String holdType);

7.22.3 查詢與寫入

  • JdbcLimitPort.HOLD_SQL 寫入 instrument_id/hold_type(Stand-in 代佔即保留)。
  • PENDING_STANDIN_SQL/STANDIN_ADVICE_SQL 以純量子查詢由 limit_holds 取最近一筆
    instrument_id/hold_type(一授權一佔用;避免 MAX(RAW) 不支援)。
  • RemoteLimitUploadAdapter 以 HoldRequest.of(...) 帶載體與型態(與即時授權一致)。

7.22.4 相容

情境 行為
instrumentId 有值 上送帶載體;權威端 INSTRUMENT_ID 落地
instrumentId=null(舊資料) 上送 null;僅歸帳戶;holdAttribution=PARTIAL
holdType=CASH_ADVANCE 觸發預借子上限檢核
holdType 缺值 正規化 GENERAL

7.22.5 驗收

StandInUploadServiceTest:後送帶 instrumentId/holdType 之斷言;
AuthorizationRepositoryIntegrationTest:findPendingStandIn 由 limit_holds 取載體/型態。


8. 外部依賴

| 依賴 | 用途 |

| Oracle CC_LIM | 權威儲存 |
| common-saga(僅用其狀態機引擎) | 狀態轉換檢核 |


9. 測試案例

編號 情境 結果
UT-LIM-01 歸戶額度查詢(永久) 10,000,000
UT-LIM-02 含臨時額度查詢 150,000
UT-LIM-03 佔用 300,000 可用 9,700,000
UT-LIM-04 釋放 可用回到 10,000,000
UT-LIM-05 佔用超過可用 拒絕,CC-LIM-1001,不寫入佔用
UT-LIM-06 調整申請(未超限) EFFECTIVE + 主檔更新
UT-LIM-07 調整申請(超限) PENDING_APPROVAL
UT-LIM-08 覆核駁回 REJECTED
UT-LIM-09 覆核通過 EFFECTIVE + 主檔更新
UT-LIM-10 對終態再覆核 409 / CC-LIM-3002
UT-LIM-11 撤銷 REVOKED + 主檔回復
UT-LIM-12 臨時額度到期排程 EXPIRED + 歸零
UT-LIM-13 專用額度申請(未超限,purpose=TAX) EFFECTIVE + special_limits 新增 ACTIVE
UT-LIM-14 專用額度同用途再次申請 更新既有 ACTIVE(不新增列)
UT-LIM-15 專用額度 purpose 不在值域 422 / CC-LIM-1003,不落庫
UT-LIM-16 專用額度撤銷 REVOKED + special_limits.status = CANCELLED
UT-LIM-17 專用額度到期排程 ACTIVE 且 effective_to < 今日 → EXPIRED
UT-LIM-18 查詢歸戶專用額度 僅回 ACTIVE;不含 PENDING_APPROVAL
UT-LIM-19 適用性:MCC 命中白名單 applicable=true;不命中 → false(§7.10.1)
UT-LIM-20 適用性:多筆命中 有明確 scopeRef 者優先(決定性)
UT-LIM-21 專用池佔用 不觸碰卡人額度;池內不足 → CC-LIM-1003 且不留佔用
UT-LIM-22 專用池釋放 used_amount 對稱回沖;一般佔用不動專用池
UT-LIM-23 8-6:非專用額度之請款 不觸發(不調高、不落紀錄)
UT-LIM-24 8-6:專用額度之請款 釋放佔用 + 調高 + 落紀錄(迄日=入帳日+32)
UT-LIM-25 8-7:FIFO 回收 先調高者先回收;回收完畢轉 CLOSED
UT-LIM-26 8-7:只扣回收額 不得歸零(保留人工核准之臨時額度)

10. 已知限制

  1. 覆核門檻為金額模擬,非角色/權限系統。 |
  2. SPECIAL 調整類型未支援 → ✅ 已解除(2026-10-02,CR-014 v1.1/§7.9):
    SPECIAL 之寫入(upsertActive)、撤銷(cancelActive)、到期(EXPIRED)與查詢(改讀 special_limits)皆已實作。
    ⚠️ 仍未實作者:8-2(角色/客層上限)、8-3/8-5 之交易時扣抵與超額拒絕(屬 authorization-service)、
    8-6/8-7(臨時額度串動)、scope_ref(MCC 用途範圍)——見 #12~#15。 |
  3. used_amount 之維護:原載「不由此服務維護(應由帳務服務更新)」——✅ 部分解除(#A-124):
    現由本服務消費 payment.payment.posted.v1 遞減之(§7.7,SAD-002 §3.2 之明文設計)。
    ⚠️ 仍未由帳務事件維護——posting.transaction.posted.v1(SAD-002 §3.2 之「釋放佔用」)尚未實作,
    故「帳務入帳使 used_amount 增加」之路徑不存在(目前僅有繳款使之減少)。
    亦即 used_amount 現行實務上只減不增,需靠人工/種子資料建立初始值。 |
  4. 監控未接。 |
  5. limit.limit.adjusted.v1 之發射僅涵蓋「已生效」路徑(§7.5):撤銷(§7.3 回復額度)與臨時額度到期(§7.4 歸零)尚未發射事件——惟此兩者同樣改變額度,故下游讀模型於該兩情境不會更新(已知落差,待後續補)。 |
  6. sequenceNo 為同 JVM 內遞增之替代序號(§7.5):多實例部署可能重複;正式應改帳戶層級持久化序號。 |
  7. eventId 為隨機 UUID,非 SAD-004 §5.1 所載之 ULID(同 #A-118 之既有實作);冪等語意不受影響(僅需唯一),惟格式與規格文字不一致,待統一。 |
  8. limit.hold.* 之消費者未實作(§7.6,#A-122):✅ 訂閱關係已化解(2026-10-01,CR-028 §3 #2/#3)——已確認 authorization-service 同時訂閱 limit.hold.created.v1 與 limit.hold.released.v1(並補登至 SAD-004 §5.2);惟消費端程式仍未實作(屬批次 B,CR-028 §6)。 |
  9. limit.hold.created.v1 payload 缺 authorizationId → ✅ 已化解(2026-10-01,CR-028 §3 #4):補為可選欄位(依 SAD-004 §5.3 不升版);證據:CC_LIM.LIMIT_HOLDS 與 CC_AUTH.LIMIT_HOLDS 皆有 AUTHORIZATION_ID。實作補入屬批次 B。 |
  10. payment.payment.posted.v1 payload 缺 currency(§7.7,#A-124):消費端之額度主檔鍵為 (account_id, currency),無法精確定位——現行保守規則為「該帳戶恰一列才更新;多幣別則告警不更新」(不臆測幣別)。已列 CR-027 §5 #10 待裁定(建議補為可選欄位)。 |
  11. UNMATCHED(重新佔額)/REVERSED 之事件未實作(§7.7,#A-124):SCS-008 §8.2 之轉換 6 會將 LIMIT_RESTORED 設回 false(撤銷額度恢復)、沖正亦然,惟兩者皆不發射事件 → 「繳款後又轉未匹配/沖正」時,額度不會被重新佔用(已知落差)。已列 CR-027 §5 #9 待裁定。 |
  12. 專用額度之交易時扣抵未實作 → ✅ 已解除(2026-10-02,CR-035 §2.2/§7.10):8-3(走專用池、不佔卡人額度)、8-4(無適用則回歸卡人額度)、8-5(兩池皆不足 → 拒絕)皆已實作並真機驗證(§7.10.4)。授權端經 SpecialLimitPort 即時查詢本服務之適用性端點。⚠️ 仍未實作:8-6/8-7(臨時額度串動;Phase 3)。 |
  13. scope_ref(用途範圍,如特定 MCC)未實作 → ✅ 已實作(2026-10-02,CR-035 §2.1):special_limits.scope_ref(MCC 白名單,逗號分隔)+ limit_adjustments.scope_ref(軌跡);NULL = 不限定。V7 遷移。真機驗證見 §7.10.4 之 ①~③。 |
  14. 8-2(角色/客層上限)未實作 → ✅ 已實作(2026-10-02,CR-035 §2.3/§7.11):
    新增 special_limit_policies(V8)+ CallerIdentityPort(★ 角色自權杖)+ 政策檢核(fail-closed)+ 2 個維護端點。
    ★ 折衷之處理:角色之折衷已移除(改自已驗證之權杖,不接受請求帶入);
    ⚠️ 客層仍由請求帶入(附四項證據,見 §7.11.3)。
    ⚠️ 政策維護端點之權限未收斂(任何已驗證呼叫者皆可設定上限)——列後續。 |
  15. limit_adjustments 與 special_limits 之間無外鍵(§7.9):對應關係為業務鍵 (account_id, currency, purpose);「哪一筆調整產生了現行定義」須由兩表時序推導,非精確可稽核。 |
  16. used_amount 之語意為「佔用中」而非「已入帳未清償」 → ✅ 已收斂(2026-10-02,CR-035 §2.4/§7.12):請款入帳(posted.v1)會釋放該專用佔用 → 佔用可回落。⚠️ 仍未實作:請款後之「已入帳未清償」不再以專用池表達(改由 8-6 之臨時額度調高表達),故專用池之 used_amount 於請款後即歸零——與「已清償」無關,此為設計選擇(專用池管「額度」、臨時額度管「週轉」)。
  17. 8-6 之繳款截止日為「推算」值 → ✅ 已精確化(2026-10-02,CR-036/§7.13):於帳單出帳時以權威 paymentDueDate 校正。⚠️ 仍為推算值之情形:該歸戶長期不出帳時,迄日將停留於 DERIVED(可由 due_date_source 辨識並人工處理)。
  18. 部分請款之精確回沖未實作(§7.12.1):8-6 以請款金額釋放佔用(POC-8 為全額請款);部分請款時之精確回沖待後續。 |

11. 修訂記錄

版本 說明
1.20 CR-055 P4-2/P4-3(2026-10-17,#A-296):新增 §7.18 預借現金額度——V15 比例欄(account_limits/limit_adjustments)+V16 擴充 limit_holds.hold_type 值域加入 CASH_ADVANCE(★ 真機攔下之真實缺口:原值域只有 GENERAL/SPECIAL/INSTALLMENT,第一次執行即 ORA-02290);上限=總額度×ratio、可用=min(上限−已預借, 有效可用);null(未設定→拒絕)≠0(明確禁止);不享共享池之卡以卡片自身可用為有效可用(否則可繞過卡片硬上限)。新增 §7.19 企業子額度與組織樹——V17 corp_units+limit_holds.corp_unit_id;上層可用=總額度−已用−自身佔用−所有下層佔用之和;檢核走上層鏈並在錯誤訊息指出不足之層。★ 揭露:授權端尚未送 CASH_ADVANCE;企業額度覆蓋之「申請→覆核」與卡片綁定未接
1.19 ⚠️ 本列為回填(原漏列):CR-055 P4-1(#A-289):新增 §7.17 共享池權利與卡片上限檢核
1.18 ⚠️ 本列為回填(原漏列):CR-054 P1(#A-288):§7.6 佔用事件 payload 補可選 instrumentId
1.17 CR-052 C-3(2026-10-17,#A-286):新增 §7.16 卡片級額度調額寫入——① V12__limit_adjustment_subject.sql:limit_adjustments 加 subject_type/subject_id(既有單回填 ACCOUNT、BEFORE INSERT trigger 補值——與 V11 同一教訓:直接 NOT NULL 會使既有寫入全數失敗)。② 契約 LimitAdjustmentRequest 加 instrumentId(非 null=CARD 維度)、LimitAdjustmentView 揭露 subjectType/instrumentId。③ ★ 無 CARD 列時核准後建立(before=0):此為「客戶提供卡片主額度初始值」之唯一途徑,且不複製帳戶額度(CR-053 D-4)。④ ★★ 卡片級不發射 limit.limit.adjusted.v1:該事件 payload 為「帳戶總額度」,若以卡片額度送出將使授權讀模型 account_limit_caches 之帳戶總額度被覆蓋(超額放行或誤拒);卡片額度之授權取值走即時查詢。⑤ ★ 卡片級不支援 SPECIAL(專用額度池為帳戶層,回 CC-LIM-1003)。⑥ ★ 實作期修正:LimitAdjustment.copy() 原用帳戶建構子 → 狀態轉換(submit/approve)會悄悄把維度退回 ACCOUNT,導致核准時套到帳戶主額度;已改帶維度並以「待覆核→核准」測試鎖住。⑦ 驗證:194 測 0 失敗(新增 8 例真 Oracle);真機 E2E:卡片調額 EFFECTIVE、CARD 列 300,000、ACCOUNT 列不變、outbox 未增、查詢可用 100,000。⚠️ 未達:卡→帳戶歸屬未經驗證(本服務無對映能力);C-2 投影分攤未做
1.16 CR-053 D-1(2026-10-17,#A-285):新增 §7.15 載體(卡)級主額度——額度主體維度化——① V11__limit_subject_dimension.sql:account_limits 加 subject_type/subject_id、既有列明確回填 ACCOUNT、唯一鍵改 (subject_type, subject_id, currency)、BEFORE INSERT trigger 自動補值(subject_type='ACCOUNT' 時 subject_id := account_id,其他維度不推導)。② 契約 GET /api/v1/instruments/{instrumentId}/limits+InstrumentLimitView(用 instruments 非 cards:本服務無 card→instrument 對映,不得以 cardId 猜 instrumentId)。③ ★ 可用額度為保守下界:本卡(永久+有效臨時+彈性) − 帳戶已用 − 本卡可歸屬佔用(下限 0),並以 holdAttribution=FULL/PARTIAL 如實揭露歷史佔用不可歸屬(V10 不得回填);不得據此宣稱精確可用額度。④ ★ 查無即拒絕(CC-LIM-1001),不以歸戶額度冒充卡片額度。⑤ ★ 實作期修正兩項真缺陷:(i) 7 處帳戶層寫入未限定 subject_type → 同帳戶多列會使「未帶 currency 時恰 1 列才更新」誤判為多幣別(繳款恢復額度永久失效)且污染卡片列;(ii) subject_id 直接 NOT NULL 使既有 51 處寫入失敗(實測 51 測試轉紅)→ 改 trigger。⑥ 驗證:186 測 0 失敗(新增 11 例真 Oracle);真機 E2E 300,000 − 200,000 = 100,000、PARTIAL 揭露、查無回 CC-LIM-1001。⚠️ 未達(誠實揭露):商品參數驅動維度(D-2)與卡片級調額寫入(C-3)為後續批次;HOUSEHOLD/PARTY/CORP_SUB 僅值域預留
1.15 CR-045 D4(2026-10-16,#A-242):§7.14 閉環改採真 socket,並更正 #A-241 之錯誤解釋——① ParameterClosedLoopIntegrationTest 由 @WebMvcTest+MockMvc 改為嵌入式 Tomcat(server.port=0)+ JDK HttpClient 真 socket,六例全通過(② PUT 落新版+現行版封閉、⑤ businessDate 版控、④ 直改即反映、同交易 Outbox、4 項負樣本 422/404/400/400)。② ★ 根因定位:#A-241 原將失敗歸因「沙箱 HTTP 攔截代理攔走迴路埠」不成立;實測同埠四路競賽——localhost/127.0.0.1 → HttpTimeoutException,[::1]/hostname → HTTP 200。真因:本機嵌入式 Tomcat 僅綁 IPv6 萬用字元,IPv4 迴路連線「掛住」而非「拒絕」。③ 可複用判準:本機任何「嵌入式 Tomcat + HttpClient 逾時」→ 先改 hostname 或 [::1]。④ 品質:scan_scaffold/scan_mockdata/scan_faketest 各 0 命中。⑤ 仍誠實揭露:③之跨進程段(limit-service 之 ProductParameterHttpClient 打到本服務)尚未驗證——現僅證明本服務自身 web 層閉環。
1.14 CR-045 D4(2026-10-16,#A-241):§7.14 驗收 ②/④/⑤ 閉合——於 product-service 新增 ParameterClosedLoopIntegrationTest(真 Oracle via Testcontainers + @WebMvcTest 真 web 層,@Import 生產之 ParameterService/JdbcParameterRepository/JdbcOutboxRepository/GlobalExceptionHandler,零替身)。★ 動工前查證(MEMORY-RULES #17)更正前版註記:「待 product-service 參數寫入路徑與 CC_PRODUCT.PARAMETERS 表俱備」實為過度保守——ParameterController 之 PUT、ParameterService 之版本鏈與生效日重疊檢查(CC-PRD-1002)、V1__baseline.sql 之 parameters 表三者本地早已具備,②/④/⑤ 並非被外部阻塞,僅缺跨服務真機實驗。★ 六例全數通過:② PUT 落新版+現行版封閉於 newFrom−1、⑤ businessDate 取昨日/今日得舊值/新值、④ 直改 CC_PRODUCT.PARAMETERS 後再取即反映(非快取)、同交易 Outbox 事件 2 筆、4 項負樣本(422 CC-PRD-1002/404 CC-PLT-4040/空 body 400/缺 header 400)。⚠️ 根因更正(同日續,原解釋有誤):當時將失敗歸因「沙箱 HTTP 攔截代理攔走迴路埠」不成立;實測根因為 Tomcat 僅綁 IPv6 萬用字元、IPv4 迴路 127.0.0.1(localhost 優先解析 IPv4)之連線會掛住而非拒絕,改用 hostname 或 [::1] 即 HTTP 200 → 已改採 MockMvc 為保守選擇(根因未明時),真 socket 版本並無技術阻礙。③之真 HTTP 段(limit-service → product-service)仍未閉合,根因既明,列為後續交付單元。★ 品質:scan_scaffold/scan_mockdata/scan_faketest 各 0 命中
1.12 CR-045 D4(2026-10-15,#A-238):新增 §7.14 商品參數消費閉環(首案)——spec-first,不直接改程式。★ 緣由:CR-045 P2 指出本服務未消費商品參數(product-service 之 GET /api/v1/parameters/{ns}/{key} 生產端已備、無消費者)。★ 首案收斂:不選 limit.model/shared_scope(其 account/card/family/corporate 分層為 OPEN 待裁定 A1),改選 已在 §7.2 完整定義之覆核門檻,由商品參數 limit.approval-threshold-minor 驅動 → 證明「改權威參數值 → 額度行為改變」閉環。★ 新增 ProductParameterPort(同步取參、依 businessDate 取版本;快取僅於量測後引入)。★ 真機驗收範式沿用 §7.7/§7.9.7:改 CC_PRODUCT.PARAMETERS 值 → 同一申請之覆核路徑改變 → 繞過服務改 DB 即反映(證非快取)。★ OPEN:A1 多維度分層、A2 同步 vs 事件消費、A3 參數寫入 RBAC(D6)、A4 取參失敗降級。★ 不恢復 stale endpoint(D2)、不臆測活動政策(D3)
1.11 CR-036(2026-10-02,#A-157):新增 §7.13 8-6 迄日之精確化——診斷推算式之誤差來源(最長高估近一個帳單週期)與「帳單日在 CC_PARTY、跨 schema 不可讀」之限制。★★ 關鍵洞察:精確值在原理上只有帳單出帳後才存在 → 裁定採兩階段(① 請款時調高並給推算值 DERIVED;② 帳單出帳時以權威 paymentDueDate 校正為 paymentDueDate + 7,標記 STATEMENT)。★ 校正歸屬條件:僅 raised_on ≤ 出帳日(出帳日取信封之 occurredAt,不由 paymentDueDate − 25 反推)。★ temp_end_date 同步為所有 OPEN 紀錄之最晚迄日。★ 契約無需變更(payload 早已含 accountId/paymentDueDate);架構變更僅 SAD-004 §5.2 之消費者 notification → notification + limit。真機 3 項(含負樣本:出帳日早於調高日 → 不校正)。測試:69 測 0 失敗
1.10 CR-035 §2.4(2026-10-02,#A-156):新增 §7.12 臨時額度串動(POC-8 之 8-6/8-7)——BR-04-02「請款後調高至繳款截止日+7 日;繳款後調降」。新增:V9__special_limit_temp_adjustments.sql;SpecialLimitTempAdjustment + 埠 + JDBC;SpecialLimitTempAdjustmentService(8-6 調高/8-7 FIFO 回收);PostingTransactionPostedListener(本服務原已列為該事件之消費者,惟從未實作);LimitRepository#raiseTemporary(★ 相對增量,非 applyAdjustedAmount 之絕對值)/lowerTemporary/findSpecialHoldIdByAuthorization;PaymentPostedProjectionService 併入 8-7(窄埠 TempAdjustmentRecoveryPort)。★ 生產端:posting-service 之 posted.v1 payload 補可選 authorizationId(來源欄位早已存在,不升版)。★★ 兩項關鍵設計:(i) 8-7 必須逐筆扣回,不得直接將 temporary_amount 歸零(該欄可能同時承載人工核准之臨時額度);(ii) 繳款截止日以「入帳日+帳單週期(25)+7」推算並標記 DERIVED——★ 更正 CR-035 §2.4 原設計(原載讀 CC_STMT 為跨 schema、不可行)。真機 6 項(真實 Kafka + Oracle)。測試:limit 64 測/posting 35 測,皆 0 失敗
1.9 CR-035 §2.3(2026-10-02,#A-155):新增 §7.11 專用額度之「可設定上限」政策(POC-8 之 8-2)——BR-04-03「依角色 + 客層參數化」。新增:V8__special_limit_policies.sql;SpecialLimitPolicy + SpecialLimitPolicyRepository + JdbcSpecialLimitPolicyRepository;★ CallerIdentityPort + SecurityContextCallerIdentity(角色自已驗證之權杖:服務權杖取 azp、使用者權杖取 realm roles);apply() 之政策檢核(fail-closed);GET/PUT 兩個維護端點。★★ 折衷之處理:角色之折衷移除(不接受請求帶入,防提權);⚠️ 客層仍由請求帶入(四項證據,§7.11.3)。★★ 真機揭露之陷阱:M2M 權杖之 realm roles 實為 default-roles-cardcore/offline_access/uma_authorization(皆無區辨力)→ 若先取 realm roles,角色會被解析為 offline_access(荒謬)→ 改以 azp 以 cc- 開頭(與 ServiceCallerAclManager 同一判準)優先判定服務權杖。真機驗證 6 項(§7.11.6)。測試:55 測 0 失敗(新增 14)
1.8 CR-035 §2.1/§2.2(2026-10-02,#A-154):新增 §7.10 專用額度池之佔用與適用性——POC-8 之 8-3(走專用池、不佔卡人額度)、8-4(無適用則回歸卡人額度)、8-5(兩池皆不足 → 拒絕)。新增:special_limits.scope_ref + limit_adjustments.scope_ref(MCC 白名單;NULL = 不限定)+ limit_holds.special_limit_id(V7__special_limit_pool.sql);SpecialLimit#coversMcc/hasExplicitScope/availableMinor;SpecialLimitRepository#findApplicable(決定性選擇順序)/findById/increaseUsed(條件更新,防併發超額佔用)/decreaseUsed;LimitApplicationService#hold 之 SPECIAL 池路徑與 release 之對稱回沖;新端點 GET /accounts/{id}/special-limits/applicable(★ applicable=false 回 200——「無適用」與「服務故障」必須可區分)。★ used_amount 之語意為「佔用中」(§7.10.2)。真機驗證 11 項(§7.10.4):含 8-3 之 availableLimit 不因專用佔用而減少、account_limits 全程未變、8-5 之 DECLINED 51、釋放之對稱回沖。測試:limit-service 41 測 0 失敗(新增 SpecialLimitPoolTest 13)
1.7 CR-014 v1.1(2026-10-02,#A-153):新增 §7.9 專用額度(POC-8)之寫入與查詢——接線既有 SPECIAL_LIMITS(非建表;該表由 V1__baseline.sql ← SDD-001 §3.4 早已存在,CR-014 v1.0 之「新表」誤述已更正)。新增:SpecialLimit + SpecialLimitRepository + JdbcSpecialLimitRepository;V6__special_limits_currency_and_index.sql(補 currency、索引、唯一索引 uk_special_limits_active);apply() 之 SPECIAL 基準額度改取專用額度池、applyToMaster() 之 SPECIAL 分支(upsertActive)、revoke()(cancelActive)、查詢端點改讀 special_limits、到期排程併入 §7.4、purpose 值域檢核(CC-LIM-1003)。★ 契約破壞性變更(§7.9.5):SpecialLimitItem 由「調整視角」改為「定義視角」(消費端為零,不另立 v2)。併補 §7.1 之「專用額度不計入可用額度」、§10 #2 解除與 #12~#15 之誠實揭露、§9 之 UT-LIM-13~18。真機驗證(§7.9.7):13 項全通過(含「繞過服務改 DB → 查詢立即反映」之非快取證明、「不佔卡人額度」之 account_limits 全程未變、到期排程約 40 秒生效)。★ 併同修正真機揭露之契約缺陷:POST /limit-adjustments 實作欄位 targetAmount ↔ 契約 amount 不符(依契約送件一律 400)、契約漏列必填之 applicantId —— 已一併修正(實作改名 amount;契約補 applicantId/currency)
1.6 2026-10-01
1.5 GOV-006 4.8 W-2 推廣・CR-027 批次 B(2026-10-01,#A-128)——§7.6 payload 補 currency 為可選欄位:原 payload 未帶,惟 CC_AUTH.LIMIT_HOLDS.currency 為 NOT NULL → 消費端(authorization-service 之本地投影)無法寫入。limit-service 本來就有該值(hold() 之入參),依 SAD-004 §5.3 不升版。已列 CR-027 §5 #14 待裁定。併補 §10 之說明
1.4 CR-028 hold 事件群四項矛盾之化解(2026-10-01)——①**§7.6**:authorizationId 由「刻意不納入」改為可選欄位(依 SAD-004 §5.3 不升版;證據:CC_LIM.LIMIT_HOLDS 與 CC_AUTH.LIMIT_HOLDS 皆有 AUTHORIZATION_ID);②**§7.6 緣由**:移除「authorization.hold.* 語意矛盾待裁定」之註記,改為已化解(該二事件已廢除);③**§10 #8/#9** 由「待裁定」改為已化解。本輪不動程式(實作屬批次 B)
1.3 GOV-006 4.8 W-2 推廣・CR-027 批次 A-1:§7.7 繳款入帳事件消費(2026-10-01,#A-124)——新增 payment.payment.posted.v1 → account_limits.used_amount(恢復額度) 之消費端規格(SAD-002 §3.2 明列本服務訂閱、SCS-008 §8 亦載「經 Transactional Outbox 事件非同步通知」,惟消費端從未實作)。實作:V5__event_consumption.sql(consumed_events)+ PaymentPostedProjectionPort/Service/JdbcPaymentPostedProjectionRepository/PaymentPostedListener。★ 恢復金額=amount(GREATEST(0, …));★ 亂序判準置於 consumed_events(不在權威主檔增設技術欄位);★ 查無主檔不建列、不寫 consumed_events(維持「已記=已套用」不變式)。同時解除 §10 #3 之部分限制(used_amount 現會因繳款而下降)。真機:used_amount 500,000 → 300,000;冪等/亂序/溢繳下限/查無主檔皆通過。併補 §10 #10/#11(UNMATCHED/REVERSED 事件未實作;posted.v1 缺 currency)
1.2 GOV-006 4.8 W-2 推廣・事件 #4/#5:§7.6 佔用事件輸出(2026-10-01,#A-122)——新增 limit.hold.created.v1/limit.hold.released.v1 之生產端規格(SAD-002 §3.2 明列本服務發布、SDD-006 §4.2 已定義 payload,卻從未有人發射)。權威歸屬無爭議(limit_holds 之權威在本服務)。實作:LimitEventFactory 新增兩個信封產生器 + LimitApplicationService.hold()/release() 同交易寫 outbox;併補 LimitRepository.findHoldAccountId(釋放入參不含帳戶,事件需回查)。併補 §10 #8/#9 之誠實揭露(消費端未實作、payload 缺 authorizationId)
1.1 GOV-006 4.8 W-2 推廣・事件 #2:§7.5 額度事件輸出(2026-10-01)——新增 limit.limit.adjusted.v1 之生產端規格(SAD-004 §5.2 載明卻從未實作)。主題 cc.limit.limit.adjusted;同交易寫 Outbox + relay 發布;分區鍵 accountId。併補 §10 之五項誠實揭露(撤銷/到期未發射、序號簡化、eventId 非 ULID 等)。
1.0 初版:回溯補建,與既有程式一致
(39-39/67)