CR #201 » SCS-008_繳款服務程式設計規格.md
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.yamlaggregates) - 值物件:
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_ATTIMESTAMP(6) WITH TIME ZONE、VERSION樂觀鎖。
遷移沿革:V1基線 →V2自扣表與序列 →V3沖正原因與自扣冪等鍵 →
V4CK_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) |
處理:
-
同交易寫 Outbox:
PaymentService.accept()於paymentRepository.save(posted)之後、同一交易(@Transactional)寫入outbox;否則會出現「繳款已入帳、事件未發」之落差。 -
僅
LIMIT_RESTORED = true(即status = POSTED)時發射——與§7「繳款 POSTED 後標LIMIT_RESTORED,經事件通知」一致。
⚠️UNMATCHED(重新佔額,LIMIT_RESTORED = false)與REVERSED不發射本事件——其對應事件(payment.payment.unmatched.v1/payment.payment.reversed.v1)尚未實作(見 §10 #11)。 -
relay 發布:
OutboxRelayScheduler週期性呼叫common-event之OutboxPublisher(SDD-004 §4.3)。
設計要點:
- ★
availableLimitAfter發null而非臆造數字:本服務不知道額度(額度之權威在limit-service)——
填 0 或猜測值都是假綠。此與§10 #5之既有決策一致。 - ⚠️
allocations[]為資訊性:消費端不應依子帳細分自行推算額度(見SCS-002 §7.7之設計說明)。 - ⚠️ 序列化為
StringSerializer(Outboxpayload已為信封 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-004POC-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,誠實揭露):
-
契約與 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 之值域必然完備」)。 -
第一階段(同日稍早):
-
新增
AUTO_DEBIT_ENROLLMENTS表與payment_no_seq序列(V2 遷移):
V1 基線無自扣表,若僅存記憶體則重啟後約定消失(客戶自扣失效,銀行案屬重大事故)。 -
新增
REVERSAL_REASON與自扣IDEMPOTENCY_KEY(V3 遷移):
契約要求沖正帶原因、所有寫入帶冪等鍵,原表無處可存。 -
新增
CK_PAYMENTS_STATUS具名約束(V4 遷移,CR-003):
V1 之 CHECK 為內聯宣告,Oracle 自動命名為SYS_C*(各環境不同)。
V4 以資料字典動態尋得後重建為具名約束,使後續值域變更得以穩定引用。
V4 冪等,可重跑。 -
額度回復採事件非同步(依 §8):本期入帳(
POSTED)時標LIMIT_RESTORED,
availableLimitAfter回傳 null——不即時呼叫limit-service。
待 Transactional Outbox/事件基礎設施就緒後補即時值。
⚠️ 更正(C-19,2026-09-22):原文載「本期受理即標」,與實物不符
——受理(RECEIVED/ALLOCATED)階段不標,係於Payment#post()時標為true
(此即「三日後重新佔額」得以成立之前提:先恢復、後撤銷)。 -
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)。 -
✅
→ 已實作(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)——該狀態即現行受理流程之
常態結果,故批次確實可達(實測全程未用合成資料)。 -
✅
→ 已放行(reversible()尚未放行UNMATCHED(CR-003保留事項)C-19,2026-09-22):
PaymentStatus#reversible()現回true於POSTED與UNMATCHED,
與SDD-005 §5轉換 8 一致,消除「狀態機允許、程式拒絕」之缺陷。
實測:對UNMATCHED繳款呼叫沖正端點 → 200REVERSED、REVERSAL_REASON落 DB。 -
自扣帳號僅存遮蔽值:以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 暴露(個資)。
測試:AutoDebitServiceTest11 項(含「可還原完整帳號」「無密文須明確失敗」
「密文遭竄改須失敗」「對外視圖不含明文與密文」)。
仍待辦:自扣執行流程尚未實作;resolveDebitAccount之前置條件
(約定須為ACTIVE)由呼叫方負責,執行流程落地時須一併實作該檢核
(見CR-004 §6.2之同類原則:不為不存在的流程發明規則與錯誤碼)。 -
平台層請求層例外之狀態碼缺口:缺必要標頭、非法 JSON、不支援之方法等
會回 500CC-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。 -
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)。 -
payment.payment.posted.v1payload 缺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(可選)→ 消費端得精確定位額度主檔 |