Project

General

Profile

CR #201 » SCS-008_繳款服務程式設計規格.md

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

 

SCS-008 繳款服務(payment-service)程式設計規格

項目 內容
文件編號 SCS-008
版本 1.7(CR-030 §2 #9:新增 §7.3 未匹配/沖正之事件輸出——修補「額度永久高估」之真缺陷)。前版 1.6(CR-029 批次 C-1:§7.2 payload 補可選 currency——供 notification-service 以既有種子模板 PAYMENT_RECEIVED 建立繳款通知;依 SAD-004 §5.3 不升版,比照 CR-027 §5 #11/#14 之先例)。前版 1.5(GOV-006 4.8 W-2 推廣・CR-027 批次 A-1:新增 §7.1 繳款入帳事件輸出(payment.payment.posted.v1)——補 §8/§10 #5 早已載明卻從未實作之事件;★ availableLimitAfter 恆為 null(非額度權威,不臆造))
狀態 已核定(規格先行)→ 已實作並實測通過(2026-09-19);1.2 依 W-1 補自扣帳號加密
對應需求 ISS-2.6(信用卡各類繳款、自扣)、ISS-2.9(信用卡代付)
服務埠號 8088(application.yml 實測)
套件根 com.beam.cardcore.payment
權威 schema CC_PAYMENT
關聯文件 SDD-001 §5.5、SDD-002 §3.5、BRD-002、GOV-005

1. 對應需求

FR 編號 需求名稱 來源
ISS-2.6 信用卡各類繳款、約定扣款(自扣) BRD-002
ISS-2.9 信用卡代付 BRD-002

1.1 上層設計依據(本規格不重複這些圖,僅引用)

圖種 文件 數量(實測) 本服務相關
ER-Diagram SDD-001_領域模型與ER-Diagram 16 個 erDiagram PAYMENTS/PAYMENT_ALLOCATIONS 實體(SDD-001 §5.5)
DFD SDD-002_DFD與流程分解 10 個 flowchart 繳款處理流(SDD-002 §3.5)
Sequence SDD-003_Sequence-Diagrams 15 個 sequenceDiagram 繳款→分配→入帳時序
狀態機 SDD-005_狀態機設計 10 個 stateDiagram ⚠️ PAYMENTS.status(RECEIVED→ALLOCATED→POSTED→REVERSED/FAILED)以 CHECK 控管,未定義正式狀態機
API Contract SDD-006 + contracts/openapi/payment-service.yaml 17 份 本服務 4 支端點

2. 服務職責與邊界

承作:

  • 各類繳款(PAYMENTS:ATM/櫃檯/超商/行動/網銀/自扣)
  • 約定扣款(Auto-Debit) enrollment
  • 繳款分配(PAYMENT_ALLOCATIONS:依費用/利息/本金/稅順序沖抵)
  • 代付(ISS-2.9)

不做(屬他服務):

  • 實際入帳與餘額更新 → posting-service(繳款核准後發事件)
  • 額度回復決策 → limit-service(經 LIMIT_RESTORED 標記協調)

3. 領域模型

  • 聚合:Payment(繳款)、AutoDebitAgreement(約定扣款,依 services.yaml aggregates)
  • 值物件:PaymentMethod、Channel、PaymentStatus、Money、Currency
  • 狀態:PAYMENTS.status ∈ {RECEIVED, ALLOCATED, POSTED, REVERSED, FAILED}(CHECK,預設 RECEIVED)

4. 程式結構

套件/類別 職責 對應需求
domain/model/Payment AutoDebitAgreement PaymentAllocation 聚合與值物件 ISS-2.6/ISS-2.9
domain/port/PaymentRepository 持久化埠 —
application/PaymentService 受理/分配/沖銷 ISS-2.6
application/AutoDebitService 約定扣款 ISS-2.6
infrastructure/jdbc/PaymentRepositoryImpl JDBC 實作 —
interfaces/rest/PaymentController AutoDebitController REST 端點 ISS-2.6

5. 資料模型(約束為 Oracle 實測值)

表 關鍵欄位 約束(實測)
PAYMENTS PAYMENT_NO VARCHAR2(24) UNIQUE;ACCOUNT_ID;AMOUNT NUMBER(19);CURRENCY CHAR(3) DEFAULT 'TWD';PAYMENT_METHOD;CHANNEL;IDEMPOTENCY_KEY UNIQUE;STATUS DEFAULT 'RECEIVED';LIMIT_RESTORED NUMBER(1);UNMATCHED_FLAG NUMBER(1);REVERSAL_REASON(V3) amount > 0;payment_method IN ('AUTO_DEBIT','ATM','COUNTER','CONVENIENCE_STORE','MOBILE','WEB');status IN ('RECEIVED','ALLOCATED','POSTED','REVERSED','FAILED','UNMATCHED')(V4 重建為具名約束 CK_PAYMENTS_STATUS);UQ(PAYMENT_NO)、UQ(IDEMPOTENCY_KEY)
PAYMENT_ALLOCATIONS PAYMENT_ID(FK→PAYMENTS.ID);SUB_ACCOUNT;AMOUNT;ALLOCATION_ORDER NUMBER(5) sub_account IN ('FEE','INTEREST','PRINCIPAL','TAX');amount >= 0;FK(PAYMENT_ID) REFERENCES PAYMENTS(ID)
AUTO_DEBIT_ENROLLMENTS(V2) ACCOUNT_ID;DEBIT_BANK;DEBIT_ACCOUNT_ENC;DEBIT_AMOUNT;EFFECTIVE_FROM;STATUS;IDEMPOTENCY_KEY(V3,UNIQUE) status IN ('ACTIVE','CANCELLED');UQ(IDEMPOTENCY_KEY)

含 CREATED_AT/UPDATED_AT TIMESTAMP(6) WITH TIME ZONE、VERSION 樂觀鎖。
遷移沿革:V1 基線 → V2 自扣表與序列 → V3 沖正原因與自扣冪等鍵 →
V4 CK_PAYMENTS_STATUS 值域增列 UNMATCHED(CR-003)。
V1 已凍結不得修改(SAD-003 §9);設計權威 SDD-001 §3.7 已同步。


6. API 設計(來自 contracts/openapi/payment-service.yaml)

端點 方法 說明 對應 FR
/api/v1/payments POST 繳款受理 ISS-2.6
/api/v1/payments/{paymentId} GET 繳款明細 ISS-2.6
/api/v1/payments/{paymentId}/reversals POST 繳款沖銷 ISS-2.6
/api/v1/auto-debit-enrollments POST 約定扣款 enrollment ISS-2.6

ISS-2.9 代付沿用繳款受理端點(method=代付渠道),以 PAYMENT_METHOD/CHANNEL 區分。錯誤碼:409 重複(冪等鍵)、400 金額/分配錯誤、404 找不到。


7. 關鍵邏輯

  • 分配順序:繳款依 ALLOCATION_ORDER 由稅→費用→利息→本金沖抵(與 posting-service 子帳一致)。
  • 冪等:IDEMPOTENCY_KEY 唯一,重送回原結果。
  • 額度回復:繳款 POSTED 後標 LIMIT_RESTORED,經事件通知 limit-service 回復可用額度。
  • 未匹配(兩個正交維度,CR-003 釐清):
    • UNMATCHED_FLAG——本次受理未取得沖抵來源時標記(受理時判定);
      現行作法為全額歸本金並標記,轉人工/對帳,不變更 status。
    • status = 'UNMATCHED'——已分配、逾三日未收銷帳檔(批次判定)。
      對應 SDD-005 §5 轉換 6;產生者尚未實作(見 §10 #7)。
  • 狀態機:繳款狀態轉換依 SDD-005 §5(8 條轉換);非法轉換回 CC-PAY-3001(409)。

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

緣由:SAD-002 §3.7 明列本服務發布 payment.payment.posted.v1,
SDD-006 §4.2 已定義 payload,§8 更白紙黑字寫「limit-service(額度回復)經 Transactional Outbox 事件非同步通知」、
§10 #5 亦載「待 Outbox/事件就緒後補」——惟從未有人發射(同 SCS-002 §7.5/SCS-001 §7.14 之型)。
本節即補上該片拼圖。

主題:cc.payment.payment.posted(命名規則 SAD-004 §5.4)。

項目 值
eventType payment.payment.posted.v1
Outbox aggregate_type payment
Outbox event_type payment.posted(不含版本)
Kafka 分區鍵 paymentId(同一繳款之事件落同一分區 → 聚合內有序)

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

欄位 來源 說明
paymentId payments.id 繳款識別碼(亦為信封 aggregateId)
accountId payments.account_id 歸戶識別碼
amount payments.amount 最小單位(分)(SDD-006 §2.5)
currency payments.currency 可選(§7.2,CR-029 批次 C-1)——供通知樣板 PAYMENT_RECEIVED 渲染
allocations[] payment_allocations 元素為 {subAccount, amount, allocationOrder}(對應 PaymentAllocation)
availableLimitAfter — ⚠️ 恆為 null——非缺陷、係既有設計(§10 #5:額度回復採事件非同步,受理時不即時查詢 limit-service;且本服務非額度權威)。額度之計算在消費端(limit-service)

處理:

  1. 同交易寫 Outbox:PaymentService.accept() 於 paymentRepository.save(posted) 之後、同一交易(@Transactional)寫入 outbox;否則會出現「繳款已入帳、事件未發」之落差。
  2. 僅 LIMIT_RESTORED = true(即 status = POSTED)時發射——與 §7「繳款 POSTED 後標 LIMIT_RESTORED,經事件通知」一致。
    ⚠️ UNMATCHED(重新佔額,LIMIT_RESTORED = false)與 REVERSED 不發射本事件——其對應事件(payment.payment.unmatched.v1/payment.payment.reversed.v1)尚未實作(見 §10 #11)。
  3. relay 發布:OutboxRelayScheduler 週期性呼叫 common-event 之 OutboxPublisher(SDD-004 §4.3)。

設計要點:

  • ★ availableLimitAfter 發 null 而非臆造數字:本服務不知道額度(額度之權威在 limit-service)——
    填 0 或猜測值都是假綠。此與 §10 #5 之既有決策一致。
  • ⚠️ allocations[] 為資訊性:消費端不應依子帳細分自行推算額度(見 SCS-002 §7.7 之設計說明)。
  • ⚠️ 序列化為 StringSerializer(Outbox payload 已為信封 JSON 字串;JsonSerializer 會多包引號,消費端解析失敗——SAD-004 §5.4)。

真機驗證:見 SCS-002 §7.7(生產端與消費端同一輪驗證)。

7.2 payload 補可選 currency——2026-10-01 新增(CR-029 批次 C-1)

緣由:notification-service 訂閱本事件並以既有種子模板 PAYMENT_RECEIVED 建立通知
(SAD-004 §5.2 載消費者含 notification)。惟該模板需要 {amount} 與 {currency},
而 §7.1 之 payload 無 currency → 變數不足即無法建立通知(CC-NOT-1003)。
此即 CR-027 §5 #10 之同族問題(該項即「posted.v1 缺 currency」,建議 (b) 補為可選)。

處置:依 SAD-004 §5.3「新增可選欄位 → 不升版」,於 v1 payload 增列 currency(可選)。

欄位 來源 必填 說明
currency payments.currency 否(可選) ISO 4217 三碼。本即存在於 payments 表(非新增資料);補入僅為使下游(通知)得渲染

相容性:不升版(SAD-004 §5.3)。

  • 舊事件(無 currency)之消費者須容忍缺欄——notification-service 之處理見 SCS-016 §7.11:
    缺 currency 時不建立通知並記 WARN(不臆造幣別,亦不產出「金額無幣別」之錯誤訊息)。
  • 既有消費者(limit-service,SCS-002 §7.7)不受影響——其以「該帳戶恰一列才更新」之保守規則處理,
    不讀 currency。

先例:CR-027 §5 #11(approved.v1 擴充 2 可選欄位,#A-125)/#14(limit.hold.created.v1 補 currency,#A-128)
——兩者皆為「依建議執行 + 登記待裁定」,本項比照辦理(見 CR-029 §8.4)。


7.3 未匹配/沖正之事件輸出(unmatched.v1/reversed.v1)——2026-10-01 新增(CR-030 §2 #9)

緣由(★ 真缺陷之修補):§7 明載沖正會撤銷額度恢復、§8.2 明載轉 UNMATCHED 亦同
(LIMIT_RESTORED = false),惟 §7.1 只發射 posted.v1 →
limit-service 無從得知 → 可用額度永久高估(超額放行風險)。本節補上兩個事件之發射。

項目 payment.payment.unmatched.v1 payment.payment.reversed.v1
主題 cc.payment.payment.unmatched cc.payment.payment.reversed
分區鍵 paymentId paymentId
Outbox event_type payment.unmatched payment.reversed
掛點 PaymentUnmatchedService.processUnmatched(逐筆轉換成功後) PaymentService.reverse(更新後)
payload paymentId/accountId/amount/daysUnmatched/currency* paymentId/accountId/amount/reason/currency*

* 可選欄位(SDD-006 §2.5 通則:凡涉金額一律帶 currency)——帶值使消費端得精確定位額度主檔。
★ daysUnmatched 由本服務自行計算(自 postedAt 起算之整日數)——不由呼叫端傳入,
避免同一事實兩個來源。
★ reversed.v1 之 payload 為 CR-030 §2 #9 首次定義。

發射與否:

情境 行為
轉 UNMATCHED 成功 寫 1 列 unmatched.v1(與權威寫入同交易)
轉換失敗(樂觀鎖衝突) 不發射(不留幽靈事件)
沖正成功 寫 1 列 reversed.v1
沖正查無繳款(CC-PAY-4040) 不發射(拋錯於寫入之前)

消費者:limit-service(重新佔用額度,見 SCS-002 §7.8)。

8. 外部依賴與降級

依賴 失敗處理(Stand-in)
posting-service(入帳)、limit-service(額度回復) 經 Transactional Outbox 事件非同步通知;若下游暫不可用,繳款先 POSTED 並重試,不阻塞受理
約定扣款排程 common-batch 批次;失敗可重跑(冪等)

8.2 未匹配繳款批次(逾三日未收銷帳檔,C-19)

對應:SDD-005 §5 轉換 6(ALLOCATED → UNMATCHED)、BRD-004 POC-11 情境 11-3、
SCS-011 §8.1 之同類慣例(結帳前觸發計息)。

項 內容
觸發 排程批次(@Scheduled,cardcore.payment.unmatched-delay-ms 預設 60000);逾期天數 unmatched-overdue-days 預設 3
候選 STATUS = 'POSTED' 且 LIMIT_RESTORED = 1 且 RECEIVED_AT < now - 3 日(PaymentRepository#findOverduePostedForUnmatched,ORDER BY RECEIVED_AT、單輪上限 unmatched-batch-limit 預設 500)
轉換 Payment#markUnmatched():狀態 → UNMATCHED;重新佔額 LIMIT_RESTORED = false(撤銷額度恢復);VERSION + 1(樂觀鎖)
不改動 UNMATCHED_FLAG——該旗標為正交維度(受理時「未取得沖抵來源」之資料品質訊號),非本批次之產出
失敗處理 單筆失敗不中斷整批(記 WARN、下一輪重試);批次例外由排程吞掉並記 ERROR(避免 Spring 取消後續排程)
日誌 有候選時記 INFO:逾 N 日、候選 X 筆、轉換 Y 筆、失敗 Z 筆(POC-11 11-3 驗收方式為 DEMO+LOG)

來源狀態之定案(BEAM PMO 2026-09-22 裁定採方案 B)

原依 SDD-005 §5 字面採 ALLOCATED,惟三方不相容:① 現行受理流程不停留
ALLOCATED(accept() 一次完成至 POSTED)→ 候選不可達;② BRD-004 POC-11 情境 11-3
之「三日後重新佔額」隱含額度已先恢復;③ 額度實於 post() 才標(原 §10 #5 誤載「受理即標」)。
裁定:來源狀態為 POSTED(額度已恢復者方有可撤銷之恢復)——
SDD-005 §5 轉換 6 已同步更正。

實測(2026-09-22,真實 Oracle,走真實受理流程)

步驟 資料 結果
① 受理 POST /api/v1/payments(10,000 分、ATM)→ PAY20260922000004 POSTED、LIMIT_RESTORED=1、VERSION 2
② 逾時 將 RECEIVED_AT 回推 4 日(模擬逾三日未收銷帳檔) 候選成立
③ 批次 等待排程週期 → UNMATCHED、LIMIT_RESTORED=0(重新佔額)、VERSION 2→3;日誌「逾 3 日、候選 1 筆、轉換 1 筆、失敗 0 筆」
負樣本(未逾時) 另一筆剛受理之 POSTED 繳款 經 2 個排程週期仍為 POSTED、VERSION 不變
轉換 8(沖正) 對上列 UNMATCHED 呼叫 POST /payments/{id}/reversals 200、REVERSED、LIMIT_RESTORED=0;REVERSAL_REASON 已落 DB(稽核留痕)

✅ 可達性已解除:本輪實測未使用任何合成資料——候選由真實受理流程產生
(受理即 POSTED 且額度已恢復),故批次在現行流程下確實可達(方案 B 之核心效益)。


9. 測試案例(對應 TRP-001)

  • 正常繳款受理+分配+額度回復
  • 冪等重複繳款、繳款沖銷、代付(ISS-2.9)
  • 跨幣別(Currency)與分配平衡檢核

10. 已知限制(誠實揭露)

  • 代付(ISS-2.9)與一般繳款共用受理端點,其專屬核驗(如代付對象白名單)須在實作時補強。

實作後補記(2026-09-19,誠實揭露):

  1. 契約與 DDL 之 UNMATCHED 差異(已二度修約,最終採 DDL 增列):

    • 第一階段(同日稍早):PaymentView.status 曾移除 UNMATCHED
      (理由:PAYMENTS.status 之 CHECK 無此值),改以 unmatchedFlag 布林欄位表示。
    • 第二階段(CR-003,最終):經核定改採 DDL 增列途徑——
      因 SDD-005 §5 之 UNMATCHED 為作業狀態(已分配、逾三日未收銷帳檔),
      與旗標語意不同,不應為遷就 DDL 而捨棄。故:
      • SDD-001 §3.7 之 CHECK 增列 UNMATCHED(設計權威);
      • V4__payments_unmatched_status.sql 交付實體變更(V1 已凍結);
      • PaymentView.status 枚舉增列 UNMATCHED;PaymentStatus 同步;
      • unmatchedFlag 不移除——二者為正交維度(見下 #6)。
    • POST /auto-debit-enrollments 之回應已改為 AutoDebitEnrollmentView
      (原誤用 PaymentView)。
    • POST /payments/{id}/reversals 之請求已改為 PaymentReversalRequest{reason}
      (原誤用 PaymentRequest),沖正原因並確實入庫(V3 新增 REVERSAL_REASON)。
    • 自扣端點已補 IdempotencyHeader 並實作冪等(V3 新增 IDEMPOTENCY_KEY+唯一索引)。

    修約途徑:改 tools/gen_openapi.py(契約之單一真實來源)後重生。
    重要:修約時曾試圖直接移除冪等鍵與 request body,遭驗證器擋下——
    專案規則為「所有 POST 寫入須帶冪等鍵、所有 POST 須有 request body」,
    故應讓實作跟上合約,而非刪合約將就實作。
    教訓:值域不一致時,先判定「是狀態還是旗標」再決定改哪一邊——
    本項一度以「DDL 是權威」為由移除契約值,實則 DDL 本身不足
    (SDD-005 §1 SM-08 之權威性是「不可寫入 DDL 不存在之值」,
    不等於「DDL 之值域必然完備」)。

  2. 新增 AUTO_DEBIT_ENROLLMENTS 表與 payment_no_seq 序列(V2 遷移):
    V1 基線無自扣表,若僅存記憶體則重啟後約定消失(客戶自扣失效,銀行案屬重大事故)。

  3. 新增 REVERSAL_REASON 與自扣 IDEMPOTENCY_KEY(V3 遷移):
    契約要求沖正帶原因、所有寫入帶冪等鍵,原表無處可存。

  4. 新增 CK_PAYMENTS_STATUS 具名約束(V4 遷移,CR-003):
    V1 之 CHECK 為內聯宣告,Oracle 自動命名為 SYS_C*(各環境不同)。
    V4 以資料字典動態尋得後重建為具名約束,使後續值域變更得以穩定引用。
    V4 冪等,可重跑。

  5. 額度回復採事件非同步(依 §8):本期入帳(POSTED)時標 LIMIT_RESTORED,
    availableLimitAfter 回傳 null——不即時呼叫 limit-service。
    待 Transactional Outbox/事件基礎設施就緒後補即時值。
    ⚠️ 更正(C-19,2026-09-22):原文載「本期受理即標」,與實物不符
    ——受理(RECEIVED/ALLOCATED)階段不標,係於 Payment#post() 時標為 true
    (此即「三日後重新佔額」得以成立之前提:先恢復、後撤銷)。

  6. UNMATCHED 與 UNMATCHED_FLAG 之區分(CR-003 釐清):

    欄位 語意 判定時機
    UNMATCHED_FLAG 本次受理未取得沖抵來源(資料品質訊號) 受理時
    status = 'UNMATCHED' 已入帳、逾三日未收銷帳檔(作業狀態) 批次

    二者可同時成立。分配來源不可用時,現行作法為全額歸本金並標
    UNMATCHED_FLAG
    ,該旗標之標記不變更 status。
    ⚠️ 更正(C-19,2026-09-22):原文載「不變更 status(仍為 ALLOCATED)」,
    與實物不符——受理流程於同一交易內完成 ALLOCATED → POSTED,落地狀態為 POSTED;
    且經 BEAM PMO 裁定,轉換 6 之來源狀態即為 POSTED(見 §8.2 與 SDD-005 §5)。

  7. ✅ UNMATCHED 之產生者尚未實作(CR-003 保留事項) → 已實作(C-19,2026-09-22):
    SDD-005 §5 轉換 6 所需之批次已交付——PaymentUnmatchedService#processUnmatched
    + PaymentUnmatchedScheduler(@Scheduled,@EnableScheduling 已啟用)、
    Payment#markUnmatched()(含重新佔額)、
    PaymentRepository#findOverduePostedForUnmatched。詳見 §8.2(含真機實測與負樣本)。
    ✅ 候選可達性已解除:來源狀態經 BEAM PMO 裁定為 POSTED(方案 B)——該狀態即現行受理流程之
    常態結果,故批次確實可達(實測全程未用合成資料)。

  8. ✅ reversible() 尚未放行 UNMATCHED(CR-003 保留事項) → 已放行(C-19,2026-09-22):
    PaymentStatus#reversible() 現回 true 於 POSTED 與 UNMATCHED,
    與 SDD-005 §5 轉換 8 一致,消除「狀態機允許、程式拒絕」之缺陷。
    實測:對 UNMATCHED 繳款呼叫沖正端點 → 200 REVERSED、REVERSAL_REASON 落 DB。

  9. 自扣帳號僅存遮蔽值:以 PiiMasker.maskAccount 遮蔽(僅留後 4 碼),
    尚未以 AesGcmFieldEncryptor 加密明文,故無法還原完整帳號供實際扣款;
    待金鑰基礎設施接入後補。

    → ✅ 已解決(2026-09-19,W-1)。新增 V5__autodebit_account_ciphertext.sql
    於 AUTO_DEBIT_ENROLLMENTS 加 DEBIT_ACCOUNT_ENC BLOB,與 DEBIT_ACCOUNT_MASKED
    兩欄並存、職責分明:遮蔽值供對外顯示,密文供內部扣款解密。
    金鑰由 common-security 之 CryptoAutoConfiguration 提供(W-1 之 KeyProvider 推廣,
    服務端無須自寫 @Configuration)。新增 AutoDebitService#resolveDebitAccount(UUID)
    為唯一解密入口,且不經任何 API 暴露(個資)。
    測試:AutoDebitServiceTest 11 項(含「可還原完整帳號」「無密文須明確失敗」
    「密文遭竄改須失敗」「對外視圖不含明文與密文」)。
    仍待辦:自扣執行流程尚未實作;resolveDebitAccount 之前置條件
    (約定須為 ACTIVE)由呼叫方負責
    ,執行流程落地時須一併實作該檢核
    (見 CR-004 §6.2 之同類原則:不為不存在的流程發明規則與錯誤碼)。

  10. 平台層請求層例外之狀態碼缺口:缺必要標頭、非法 JSON、不支援之方法等
    會回 500 CC-PLT-5000(契約要求 400/405)。屬全平台問題,
    已立 CR-004(待核定)。本服務目前未比照 rewards-service 全面繞道,
    故相關情境仍受影響。

    → ✅ 已解決(2026-09-19,CR-004 已核定/已執行)。common-spring 之
    GlobalExceptionHandler 由 2 個 handler 增為 7 個(涵蓋 9 種請求層例外型別),
    本服務之請求層錯誤現由平台統一映射為 CC-PLT-4001/4002/4003/4040/4050,
    且一律記 WARN(不再觸發告警)。本服務無須任何改動即受惠
    (缺陷位於共用元件)。實測證據見 CR-004 §4.1。

  11. UNMATCHED(重新佔額)與 REVERSED 之事件尚未實作(§7.1,#A-124):
    §7.1 僅發射 payment.payment.posted.v1(LIMIT_RESTORED = true)。
    惟 §8.2 之轉換 6 會將 LIMIT_RESTORED 設回 false(撤銷額度恢復=重新佔額)、
    §7 之沖正亦然——此二情境目前不會通知 limit-service,故「繳款後又轉未匹配/沖正」
    時,額度不會被重新佔用(已知落差)。
    對應事件 payment.payment.unmatched.v1(SDD-006 §4.2 有定義)與
    payment.payment.reversed.v1(SAD-002 §3.7 有列)均尚未實作;
    ⚠️ 前者不在 SAD-004 §5.2 之清單內(兩份清單不一致,CR-027 §5 #5)。

  12. payment.payment.posted.v1 payload 缺 currency(§7.1):
    SDD-006 §4.2 未定義該欄位,惟 payments.currency 存在。
    消費端(limit-service)因此無法以幣別精確定位額度主檔——現行處置為
    「該帳戶恰一列才更新;多列(多幣別)則告警不更新」(SCS-002 §7.7)。
    已列 CR-027 §5 #10 待裁定(建議補為可選欄位,依 SAD-004 §5.3 不升版)。


11. 修訂記錄

版本 日期 修訂者 說明
1.0 2026-09-19 開發團隊 初版(規格先行),並完成實作與實測
1.1 2026-09-19 開發團隊 依 CR-003 更新:①§5 資料模型補 REVERSAL_REASON(V3)、AUTO_DEBIT_ENROLLMENTS(V2)、status 值域增列 UNMATCHED 並註明 V4 具名約束 CK_PAYMENTS_STATUS,及遷移沿革;②§7 關鍵邏輯將「未匹配」拆為 UNMATCHED_FLAG(受理時)與 status='UNMATCHED'(批次)兩個正交維度,並補狀態機依據;③§10 改寫 #1(UNMATCHED 由「契約移除」改為「DDL 增列」,並記錄兩階段決策與教訓),新增 #4(V4 具名約束)、#6(兩維度對照)、#7(UNMATCHED 無產生者)、#8(reversible() 未放行)、#10(CR-004 平台缺口);④新增本修訂記錄區(原文件欠缺)
1.2 2026-09-19 開發團隊 依 W-1(KeyProvider 推廣) 更新:①**§10 #9 由「已知限制」改為「已解決」——新增 V5__autodebit_account_ciphertext.sql,於 AUTO_DEBIT_ENROLLMENTS 加 DEBIT_ACCOUNT_ENC BLOB,與 DEBIT_ACCOUNT_MASKED 兩欄並存(遮蔽值供顯示、密文供扣款);金鑰改由 common-security 之 CryptoAutoConfiguration 提供,服務端無須自寫 @Configuration;新增 AutoDebitService#resolveDebitAccount 為唯一解密入口且不經 API 暴露**。②**§10 #10 由「平台缺口」改為「已解決」——CR-004 已執行,GlobalExceptionHandler 由 2 增至 7 handler(涵蓋 9 種例外型別),本服務無須改動即受惠。③實測證據:AutoDebitServiceTest 11 項(含「可還原完整帳號」「無密文須明確失敗」「密文遭竄改須失敗」)+ Oracle 實機往返(明文 → API → 加密 → BLOB → 解密 → 完整明文)。④誠實揭露**:自扣執行流程仍未實作,resolveDebitAccount 之狀態前置條件由呼叫方負責(見 #9)
1.3 2026-09-22 WorkBuddy AI C-19:未匹配繳款批次(逾三日未收銷帳檔)——①新增 §8.2:觸發(排程)、候選(ALLOCATED 且 RECEIVED_AT < now-3日)、轉換(markUnmatched() 含重新佔額)、不改動 UNMATCHED_FLAG(正交)、失敗處理(單筆失敗不中斷整批)、日誌格式;並附真機實測表(逾時轉換/未逾時負樣本/轉換 8 沖正)。②**§10 #7、#8 由「保留事項」改為已實作/已放行**。③誠實揭露新增:現行 accept() 不停留 ALLOCATED,故候選可達性存疑(實測以合成資料為之),且與 BRD-004 POC-11 11-3 之「重新佔額」語意、§10 #5「受理即標」三者互相矛盾——待 BEAM PMO 裁定
1.4 2026-09-22 WorkBuddy AI BEAM PMO 裁定採方案 B → §8.2 候選狀態更正為 POSTED(LIMIT_RESTORED = 1):①§8.2 候選條件、實測表(改走真實受理流程、未用合成資料:受理 POSTED/LIMIT_RESTORED=1 → 逾期 → UNMATCHED/LIMIT_RESTORED=0,VERSION 2→3;負樣本未逾時不動;轉換 8 沖正 200)、並載明「可達性已解除」;②**§10 #5 更正**(額度係入帳時標記,非「受理即標」)、§10 #6 更正(未匹配者落地狀態為 POSTED,非 ALLOCATED);③§10 #7 補記可達性解除。SDD-005 升 1.6(轉換 6 來源同步更正)
1.5 2026-10-01 WorkBuddy AI GOV-006 4.8 W-2 推廣・CR-027 批次 A-1:新增 §7.1 繳款入帳事件輸出(payment.payment.posted.v1)——§8 早已載明「limit-service(額度回復)經 Transactional Outbox 事件非同步通知」、§10 #5 亦載「待 Outbox/事件就緒後補」,惟從未有人發射;本版補上該片拼圖(V6__outbox.sql + 同交易寫 outbox + relay)。★ availableLimitAfter 恆為 null(本服務非額度權威,填 0 或猜測值皆為假綠)——與 §10 #5 既有決策一致。併補 §10 #11/#12 之誠實揭露(UNMATCHED/REVERSED 事件未實作;payload 缺 currency)。真機驗證:見 SCS-002 §7.7(生產+消費同一輪)
1.6 2026-10-01 WorkBuddy AI CR-029 批次 C-1:§7.2 payload 補【可選】currency——notification-service 訂閱本事件並以既有種子模板 PAYMENT_RECEIVED(EMAIL/SMS,需 {amount} 與 {currency})建立繳款通知,惟 §7.1 之 payload 無 currency → 變數不足即無法建立通知(此即 CR-027 §5 #10 之同族問題)。依 SAD-004 §5.3「新增可選欄位 → 不升版」補入;payments.currency 本即存在(非新增資料)。相容性:舊事件(無 currency)之消費者須容忍缺欄(SCS-016 §7.11:缺則不建立並記 WARN,不臆造幣別);既有消費者 limit-service 不讀該欄故不受影響。先例:#11(#A-125)/#14(#A-128)——皆「依建議執行 + 登記待裁定」
1.7 2026-10-01 WorkBuddy AI CR-030 §2 #9(#A-142):新增 §7.3 未匹配/沖正之事件輸出——unmatched.v1(PaymentUnmatchedService,逐筆成功後同交易發射)+ reversed.v1(PaymentService.reverse);★ 真缺陷之修補:§7/§8.2 之「撤銷額度恢復」原無對應事件 → limit 無從得知 → 可用額度永久高估。★ reversed.v1 之 payload 為本 CR 首次定義(paymentId/accountId/amount/reason/currency*)。★ 兩個 payload 皆依 §2.5 通則帶 currency(可選)→ 消費端得精確定位額度主檔
(41-41/67)