# 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`（可選）→ 消費端得**精確定位**額度主檔 |
