CR #201 » SCS-002_額度服務程式設計規格.md
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)。
處理:
-
同交易寫 Outbox:
applyToMaster套用主檔成功後,於同一交易(@Transactional)寫入outbox(SDD-001 §3.15形狀);否則會出現「額度已改、事件未發」之落差。 -
relay 發布:
OutboxRelayScheduler週期性呼叫common-event之OutboxPublisher撈取published_at IS NULL者送 Kafka(SDD-004 §4.3)。 -
信封欄位固定(
SAD-004 §5.1);業務差異全在payload。
設計要點:
- ★ 只在「已生效」路徑發射:
applyToMaster僅由EFFECTIVE(直接生效/覆核通過)呼叫;PENDING_APPROVAL/REJECTED不發射(額度未變)。 - ⚠️ 序列化為
StringSerializer:Outboxpayload已是信封 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 |
— | ✅ | 本次釋放金額 | 最小單位(分);為本次釋放額,非累計 |
處理:
-
同交易寫 Outbox:
LimitApplicationService.hold()/release()於同一交易(@Transactional)寫入outbox;否則會出現「佔用已成立、事件未發」之落差。 -
released之accountId由 DB 取得:release(holdId, amount)之入參不含帳戶——故於釋放成功後以findHoldAccountId(holdId)取得(同一交易)。 -
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。
處理:
-
冪等:
eventId寫入consumed_events(與used_amount更新同交易);已存在 → 略過。 -
亂序防護:
sequenceNo落後於該aggregateId已消費之最大值 → 略過並 WARN。
⚠️ 本服務之亂序判準取自consumed_events之MAX(sequence_no) WHERE aggregate_id = ?
(不新增欄位至權威主檔account_limits——理由見下)。 -
恢復額度:
used_amount = GREATEST(0, used_amount − amount),version + 1(僅此一欄)。 -
未知
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 寫入路徑
-
apply()之基準額度:SPECIAL之before取自「現行有效之同(歸戶, 幣別, 用途)專用額度」(無則 0),
不是主檔之permanent_amount。
⚠️ 否則覆核門檻(§7.2,以「增幅」判定)會以錯誤基準計算 → 該送覆核者可能直接生效。 -
applyToMaster():SPECIAL→SpecialLimitRepository.upsertActive(...)(同一交易),
不走applyAdjustedAmount。-
effectiveFrom= 請求之tempStartDate,未帶則取今日(本表該欄NOT NULL)。 - 主檔(
account_limits)之存在仍為前提(§7.2之既有檢核不變)——
8-4「未設定則用卡人額度」之前提是卡人額度存在。
-
-
revoke():SPECIAL→cancelActive(...)(ACTIVE→CANCELLED);
⚠️ 不得走applyAdjustedAmount(該表無 SPECIAL 分支 → 會誤報「回復額度失敗」)。 -
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(同一交易、批次處理)。
為何必要(非「錦上添花」):若不轉態,
-
status='ACTIVE'說謊——查詢端點將回傳已過期之專用額度; -
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-045P2 指出本服務(額度)未消費任何商品參數——product-service之GET /api/v1/parameters/{ns}/{key}(SCS-005 §6)生產端已備,且product.parameter.changed.v1事件已發布(#A-130),但全庫無下游消費者。依CR-045 §8D1–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——baselinethreshold=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 項負樣本(重疊 422CC-PRD-1002/查無 404CC-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)+ JDKHttpClient打真 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.2BEAM PMO 裁定 C-1 由 (i) 修正為 (ii)「卡片有獨立主額度」(客戶要求,
RFPFR.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-053D-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-053D-4)——卡片主額度初始值須由客戶提供。 -
不改既有帳戶層行為:
restoreUsedAmount/reapply/調額等寫入一律限定
subject_type='ACCOUNT'(否則會污染卡片列,且「恰 1 列才更新」之保守規則會被
多維度列誤判為多幣別而永久停止更新)。 -
卡片級調額(寫入)為後續批次(
CR-052C-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 §4BEAM PMO 裁定 C-3 含額度寫入(非僅查詢);CR-053D-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-053D-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. 已知限制
- 覆核門檻為金額模擬,非角色/權限系統。 |
-
→ ✅ 已解除(2026-10-02,SPECIAL調整類型未支援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。 | -
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現行實務上只減不增,需靠人工/種子資料建立初始值。 | - 監控未接。 |
-
limit.limit.adjusted.v1之發射僅涵蓋「已生效」路徑(§7.5):撤銷(§7.3回復額度)與臨時額度到期(§7.4歸零)尚未發射事件——惟此兩者同樣改變額度,故下游讀模型於該兩情境不會更新(已知落差,待後續補)。 | -
sequenceNo為同 JVM 內遞增之替代序號(§7.5):多實例部署可能重複;正式應改帳戶層級持久化序號。 | -
eventId為隨機 UUID,非SAD-004 §5.1所載之 ULID(同#A-118之既有實作);冪等語意不受影響(僅需唯一),惟格式與規格文字不一致,待統一。 | -
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)。 | -
→ ✅ 已化解(2026-10-01,limit.hold.created.v1payload 缺authorizationIdCR-028 §3 #4):補為可選欄位(依SAD-004 §5.3不升版);證據:CC_LIM.LIMIT_HOLDS與CC_AUTH.LIMIT_HOLDS皆有AUTHORIZATION_ID。實作補入屬批次 B。 | -
payment.payment.posted.v1payload 缺currency(§7.7,#A-124):消費端之額度主檔鍵為 (account_id,currency),無法精確定位——現行保守規則為「該帳戶恰一列才更新;多幣別則告警不更新」(不臆測幣別)。已列CR-027 §5 #10待裁定(建議補為可選欄位)。 | -
UNMATCHED(重新佔額)/REVERSED之事件未實作(§7.7,#A-124):SCS-008 §8.2之轉換 6 會將LIMIT_RESTORED設回false(撤銷額度恢復)、沖正亦然,惟兩者皆不發射事件 → 「繳款後又轉未匹配/沖正」時,額度不會被重新佔用(已知落差)。已列CR-027 §5 #9待裁定。 | -
專用額度之交易時扣抵未實作→ ✅ 已解除(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)。 | -
→ ✅ 已實作(2026-10-02,scope_ref(用途範圍,如特定 MCC)未實作CR-035 §2.1):special_limits.scope_ref(MCC 白名單,逗號分隔)+limit_adjustments.scope_ref(軌跡);NULL= 不限定。V7遷移。真機驗證見§7.10.4之 ①~③。 | -
→ ✅ 已實作(2026-10-02,8-2(角色/客層上限)未實作CR-035 §2.3/§7.11):
新增special_limit_policies(V8)+CallerIdentityPort(★ 角色自權杖)+ 政策檢核(fail-closed)+ 2 個維護端點。
★ 折衷之處理:角色之折衷已移除(改自已驗證之權杖,不接受請求帶入);
⚠️ 客層仍由請求帶入(附四項證據,見§7.11.3)。
⚠️ 政策維護端點之權限未收斂(任何已驗證呼叫者皆可設定上限)——列後續。 | -
limit_adjustments與special_limits之間無外鍵(§7.9):對應關係為業務鍵(account_id, currency, purpose);「哪一筆調整產生了現行定義」須由兩表時序推導,非精確可稽核。 | -
→ ✅ 已收斂(2026-10-02,used_amount之語意為「佔用中」而非「已入帳未清償」CR-035 §2.4/§7.12):請款入帳(posted.v1)會釋放該專用佔用 → 佔用可回落。⚠️ 仍未實作:請款後之「已入帳未清償」不再以專用池表達(改由 8-6 之臨時額度調高表達),故專用池之used_amount於請款後即歸零——與「已清償」無關,此為設計選擇(專用池管「額度」、臨時額度管「週轉」)。 -
8-6 之繳款截止日為「推算」值→ ✅ 已精確化(2026-10-02,CR-036/§7.13):於帳單出帳時以權威paymentDueDate校正。⚠️ 仍為推算值之情形:該歸戶長期不出帳時,迄日將停留於DERIVED(可由due_date_source辨識並人工處理)。 -
部分請款之精確回沖未實作(
§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 | 初版:回溯補建,與既有程式一致 |