# CR-030　事件流治理：`CR-027 §5` 十二項待裁定之**架構裁定**（Best Practice）

| 項目 | 內容 |
|---|---|
| 文件編號 | CR-030 |
| 版本 | **1.0** |
| 提出日 | 2026-10-01 |
| 提出者 | WorkBuddy AI（開發團隊） |
| **裁定者** | **架構師（本專案）**——BEAM PMO 2026-10-01 指示：「以 Best Practice（Payment Industry 20 年金融科技／架構師）**直接將這些問題解決**」 |
| 緣起 | `CR-027 §5` 累積 12 項待裁定。BEAM PMO 指出「多為 Day 1 立規格時未深思熟慮所致」——本 CR **不再逐項回問**，改以**業界實務**直接裁定，並**載明理由、業界依據、影響面與可逆性**，供日後覆核。 |
| 範圍 | `CR-027 §5` 之 **#5～#16**（#1～#4 已由 `CR-028` 化解） |
| 影響範圍 | `SAD-002 §3`、`SAD-003 §4`、`SAD-004 §5.2`／`§5.4`、`SDD-006 §2.5`／`§4.2`；`SCS-001`／`SCS-002`／`SCS-008`／`SCS-011`；`CR-027`；程式：`authorization`／`payment`／`limit`／`card`／`product`／`clearing`／`dispute`／`posting` |
| 狀態 | **12 項全數裁定**；**文件面已執行**；**程式面分 A／B 兩批執行**（見 §3） |

---

## 0. 裁定原則（本 CR 之方法論）

作為支付產業架構師，本案的裁定一律依循以下四條**業界通則**；後續每一項裁定皆註明所依據之原則：

| 原則 | 內容 | 出處／理由 |
|---|---|---|
| **P1　單一真實來源** | 同一事實**只能有一份權威記載**；重複記載必然漂移 | 本專案鐵律；已實證漂移（`card.card.issued` 在 `SAD-002` 有、`SAD-004` 無） |
| **P2　事件＝已發生的狀態變更** | 事件流**只承載狀態變更之事實**，不承載「請你做某事」（命令）與「統計指標」 | Event-Driven Architecture 基本原則；命令走 API、指標走 metrics |
| **P3　同步 vs 非同步之分界** | **需即時回應者走同步**；**解耦的通知／投影／分析走事件**。**同一寫入不得兩條路徑都做** | 支付授權有 <100ms 之硬性延遲要求；重複路徑必然重複套用 |
| **P4　契約演進向後相容** | 新增**可選**欄位不升版；移除／改型別／新增必填才升版 | `SAD-004 §5.3`（與業界 schema-evolution 慣例一致） |

> **兩類事項本 CR 不代為決定**（須外部輸入，**不得由開發臆造**）：
> **(a) 客戶業務文案**（通知模板文字）；**(b) 卡組織規範值**（各組織拒付時限、回應碼對映）。
> 此二類僅能「先立骨架、留待接入」。

---

## 1. 十二項裁定總表

| # | 爭點 | **裁定** | 依據 | 執行 |
|---:|---|---|---|---|
| **5** | 事件清單以哪份為權威 | **(c) `SAD-004 §5.2` 為唯一權威清單**；`SAD-002 §3.x` 之「事件（發布／訂閱）」欄**降為指標**並指向 §5.2 | **P1** | 文件（已執行） |
| **6** | `SAD-003 §4.1` 之 7 個 `q_*` 語意 | **廢除全部 7 個自投影**；改立「**跨服務投影清單**」，由**實際需求**驅動 | **P1／P2** | 文件（已執行） |
| **7** | `declined.v1` 是否納入 | **不納入事件流**；改以 **metrics（Micrometer counter）** 涵蓋 | **P2** | 文件 ＋ 程式（A 批） |
| **8** | `REFERRAL` 事件缺漏 | **`approved.v1` 之語意重新界定為「授權**佔用額度**之決策事件」**，並補 `decision`（可選）；發射條件由 `isApproved()` 改為 **`holdsLimit()`**（APPROVED／STANDIN／**REFERRAL**） | **P2** | 文件 ＋ 程式（**A 批**，**真缺陷**） |
| **9** | `UNMATCHED`／`REVERSED` 未實作 | **補發射 ＋ 補消費**（`limit` **重新佔用**）——**此為超額風險之真缺陷** | **P3** | 文件 ＋ 程式（**A 批**，**真缺陷**） |
| **10** | `posted.v1` 缺 `currency` | **確認為「可選」欄位**（已執行）；並**提升為通則**：**凡涉金額之事件 payload 一律帶 `currency`** | **P4** | 文件（已執行）＋ 通則入規 |
| **11** | `approved.v1` 擴充 2 可選欄位 | **追認**（`cardNoMasked` 為**遮罩**非明文，符合 PCI 原則） | **P4** | 文件（追認） |
| **12** | `sequenceNo` 跨事件型別交錯 | **明訂不變式**：「**同一 `aggregateId` 之所有事件型別，須共用同一單調序號來源**」；並**修正 `card-service`**（唯一違反者：改用單一序號來源） | **P2** | 文件 ＋ 程式（**A 批**） |
| **13** | `SDD-006 §4.2` 之 `card.status-changed` payload 更正 | **追認以實作為準**（實作已真機驗證且有消費端；原契約表為唯一錯誤記載） | **P1** | 文件（追認） |
| **14** | `limit.hold.created.v1` 補 `currency` | **追認**（可選、不升版） | **P4** | 文件（追認） |
| **15** | Kafka 主題命名不一致 | **(a) 對齊文件慣例**（`cc.<domain>.<event-short>`）；修正 `product`／`clearing`／`dispute`／`posting` 之 `event_type` 常數 | **P1** | 文件 ＋ 程式（**B 批**） |
| **16** | 事件 vs 同步呼叫之重複 | **(b)「同步為主、事件為輔」**——事件**僅供跨服務投影與通知**，**不重做**同步已做之寫入 | **P3** | 文件（已執行）＋ `SAD-004 §5.2` 用途欄改寫 |

---

## 2. 逐項裁定理由（含業界依據）

### #5　事件清單權威 → **(c) 合併為單一清單**

**裁定**：`SAD-004 §5.2` 為**唯一權威**（含生產者／消費者／用途）。`SAD-002 §3.x` 之「事件（發布）」／「事件（訂閱）」**降為服務視角之指標**，並在表頭明示「**權威清單見 `SAD-004 §5.2`**」。

**理由（P1）**：兩份清單並存**已經漂移**——`card.card.issued.v1`／`card.token.bound.v1` 見於 `SAD-002 §3.3` 而**不在** `SAD-004 §5.2`（見該節之揭露註記）。支付系統的事件契約若無單一權威，下游訂閱將依「看哪份文件」而異，屬**治理缺陷**。

**業界依據**：金融機構之 event catalog 通常以**中央 registry**（Schema Registry／Event Catalog）為單一權威；服務文件僅引用。

### #6　`q_*` 讀模型 → **廢除 7 個自投影，改立「跨服務投影清單」**

**裁定**：`SAD-003 §4.1` 之 7 列**全數刪除**（已逐列比對確認 **7／7 為自投影**，來源事件與存放服務同一）。改立**跨服務投影清單**，內容為**實際已實作或已裁定要做**之投影（如 `card_status_caches`／`account_limit_caches`／`limit_holds`），並由 `CR-029` 之盤點決定後續。

**理由（P1／P2）**：CQRS 讀模型之**存在價值**在「**跨服務查詢**」——避免同步呼叫扇出（fan-out）與跨庫 join。**自投影**只是把權威表複製一份到讀側，帶來**雙寫、延遲、一致性**三重成本而**零收益**（違反 CQRS 初衷），且與權威表同名同義易誤用（`q_transaction_history` vs `posting.transactions`）。

**業界依據**：CQRS 之 read model 是「**為查詢而生的另一種視圖**」；同構複製不構成 read model。支付核心常見之投影為「跨域客戶視圖」（卡＋額度＋點數＋帳單）而非自我複製。

### #7　`declined.v1` → **不納入事件流；改以 metrics 涵蓋**

**裁定**：`authorization.authorization.declined.v1` **不發射**。拒絕率／拒絕原因分佈改以 **Micrometer counter**（如 `cardcore_authorization_declined_total{reason=…}`）涵蓋，供 Prometheus／Grafana。

**理由（P2）**：**拒絕不改變任何下游狀態**——不佔額度、不產生待請款、不需通知（不應對每筆拒絕都發簡訊）。依 **P2**，事件流只承載**狀態變更之事實**；「拒絕率統計」是**觀測性**需求，屬 metrics 之職責。

**業界依據**：卡組織與發卡行之實務——**decline 是授權主機的即時回應碼**（ISO 8583 DE39），其統計走 **APM／BI**；domain event 通常僅於「需要跨域副作用」時發射。將高頻拒絕灌入事件流會造成 topic 爆量與無謂消費者負擔。

**⚠️ 可逆性**：`SDD-006 §4.2` 之 payload **保留**並標註「**刻意未發射**」；若日後客戶要求「拒絕率即時看板」，僅需新增生產端發射 ＋ 看板消費者，**無契約破壞**。

### #8　`REFERRAL` → **重新界定 `approved.v1` 為「佔用額度之決策事件」＋ 補 `decision`**

**裁定**：
1. **語意重新界定**：`authorization.authorization.approved.v1` ＝ 「**授權已佔用額度**」之事件（**非**「決策為核准」）。
   - 三者**皆佔用額度**：`APPROVED`（正常核准）／`STANDIN`（代授權）／**`REFERRAL`（轉人工）**。
   - `DECLINED` **不佔用** → 不屬本事件（亦不另立，見 #7）。
2. **`AuthDecision` 新增 `holdsLimit()`**（`APPROVED`／`STANDIN`／`REFERRAL`），**發射條件由 `isApproved()` 改為 `holdsLimit()`**。
3. **payload 補 `decision`（可選）**——下游得以分辨「核准」與「轉人工待覆核」。
4. **`notification` 端**：`decision = REFERRAL` → **不建立通知**（無對應模板，**不臆造文案**），記 INFO。

**理由（P2 ＋ 實證缺陷）**：實測 `AuthorizeUseCase:110` 之 `willApprove = approved || referralOnly`，`:113` 據此**呼叫 `limitPort.hold(...)`**——即 **REFERRAL 確實凍結額度**；惟 `:148` 之發射條件 `authorization.isApproved()` **排除 REFERRAL** → **額度被佔用而下游完全不知**。此為**真缺陷**，非「要不要做」之選擇。

> ⚠️ **併更正程式註解之不實**：`:145-147` 原註「REFERRAL 亦已佔用額度，**屬核准路徑**（與 `Authorization.isApproved()` 之定義一致）」——**前半正確、後半與程式碼矛盾**（`isApproved()` 明確排除 REFERRAL）。已於本 CR 一併更正。

**業界依據**：Card authorization 之 hold 生命週期——`REFER`（轉人工）在 VISA／Mastercard 實務上**仍為 authorization hold**，商戶已取得授權碼之占位；發卡行必須讓**額度、待請款、通知**三側同步得知，否則會出現「客戶額度被凍結但客服查不到原因」。

### #9　`UNMATCHED`／`REVERSED` → **補發射 ＋ 補消費（重新佔用額度）**

**裁定**：
1. **`payment.payment.unmatched.v1` 發射**：payload 依 `SDD-006 §4.2`（`paymentId`／`accountId`／`amount`／`daysUnmatched`）**＋ `currency`（可選，依 #10 通則）**；於 `PaymentUnmatchedService` 轉換成功後**同交易**寫 outbox。
2. **`payment.payment.reversed.v1` 發射**：**payload 本 CR 定義**為 `paymentId`／`accountId`／`amount`／`reason`／**`currency`（可選）**；於 `PaymentService.reverse` 同交易寫 outbox。
3. **`limit-service` 補消費**：訂閱兩者，**重新佔用**額度（`used_amount += amount`）——與 #A-124 之「恢復」**反向同構**。

**理由（P3 ＋ 實證缺陷）**：`SCS-008 §8.2` 明載 `POSTED → UNMATCHED` 會把 `LIMIT_RESTORED` 設回 **`false`**（**撤銷額度恢復＝重新佔額**）、`§7` 沖正亦然；惟 `§7.1` **只發射 `posted.v1`** →
**「繳款入帳 → 額度已恢復 → 三日後轉未匹配」時，額度不會被重新佔用** → **可用額度永久高估**。
以支付核心之標準，這屬 **P0 級帳務／風控缺陷**（超額放行風險），**必須補**。

> ⚠️ **與 #16 之區別（重要，避免誤解）**：`limit` 之「**重新佔用**」**不是**重做同步已做之寫入——
> 該行為**目前無任何路徑涵蓋**（`markUnmatched` 為批次、`reverse` 為 API，皆未通知 `limit`）。
> **#16 禁止的是「重複做同步已做的事」；不禁止「做沒人做的事」。**

**業界依據**：Payment reconciliation 之「**沖銷／退回必回沖額度**」為鐵律；發卡行若在未匹配期間仍讓客戶可用額度含該筆繳款，即形成**信用暴露**。

### #10／#11／#13／#14　→ **追認 ＋ 提升為通則**

**裁定**：
- **#10／#14 追認**：`currency` 為**可選**欄位（依 **P4** 不升版）。
- **#11 追認**：`cardNoMasked`／`merchantId` 為可選；★ **`cardNoMasked` 為遮罩值**（先例見 `card.card.status-changed.v1`），**不含 PAN**，符合 PCI DSS 對「不得儲存／傳遞明文 PAN」之要求。
- **#13 追認**：以**實作**為準（真機驗證過且有消費端）。
- **★ 提升為通則（新增）**：**凡事件 payload 涉及金額者，一律帶 `currency`（可選）**——理由：金額**無幣別即無語意**（多幣別帳戶下不可解讀）；列為 `SDD-006 §2.5` 之設計通則。

**業界依據（P4）**：ISO 20022 之 `Amount` 一律為「**Currency + Amount**」複合型別；金融訊息中**裸金額**被視為建模錯誤。

### #12　`sequenceNo` → **明訂不變式 ＋ 修正 `card-service`**

**裁定**：
1. **不變式入規**（`SAD-004 §5.4`）：「**同一 `aggregateId` 之所有事件型別，須共用同一單調序號來源**」。
2. **修正唯一違反者 `card-service`**：`cardIssuedEnvelope`／`tokenBoundEnvelope` 原用**時間戳基底序號**，`status-changed` 用**卡片版本** → 改為**全部使用同一來源**（`CardEventFactory.nextSequenceNo()`）。
3. **已知限制保留並升級為待辦**：JVM 內計數器**多實例部署不安全**；正解為「**聚合層級持久化序號**」（每 `aggregateId` 一計數列，同交易遞增）——記為後續強化項（**非本批**）。

**理由（P2）**：消費者之亂序防護以「`sequenceNo <= MAX(aggregateId)`」判定——若同一聚合之兩個事件型別序號**不同源**，後發之事件可能因序號較小而**被誤判為亂序而略過**（**資料遺失**，且**無錯誤訊息**）。此為**靜默型缺陷**，最難察覺。

**為何選 (b) 精神而非 (c)**：(c)「明訂序號僅在 (aggregateId, eventType) 內有意義」是**把問題下推給消費者**——每個消費者都要記得按型別分開判斷；而 (b) 讓**生產端保證不變式**，消費者之現行邏輯即正確。**生產端修一處 ＜ 消費者各修一處**，且不變式更強。

**業界依據**：Event Sourcing 之 `sequence_number` 慣例為**per-aggregate 嚴格遞增**（Axon／EventStoreDB 皆然），不因事件型別而分段。

### #15　主題命名 → **(a) 對齊文件慣例**

**裁定**：主題一律為 **`cc.<aggregate_type>.<event_type>`**，其中 `event_type` ＝ **事件全名去 `.vN` 後，再移除一層與 `aggregate_type` 相同之首段**。
修正 `product`／`clearing`／`dispute`／`posting` 四個服務之常數：

| 服務 | 現值 | **改為** | 主題（前 → 後） |
|---|---|---|---|
| `product` | `product.parameter.changed` | **`parameter.changed`** | `cc.product.product.parameter.changed` → **`cc.product.parameter.changed`** |
| `clearing` | `clearing.batch.matched` | **`batch.matched`** | `cc.clearing.clearing.batch.matched` → **`cc.clearing.batch.matched`** |
| `dispute` | `dispute.case.opened` | **`case.opened`** | `cc.dispute.dispute.case.opened` → **`cc.dispute.case.opened`** |
| `posting` | `posting.transaction.posted`／`.reversed` | **`transaction.posted`／`.reversed`** | `cc.posting.posting.transaction.*` → **`cc.posting.transaction.*`** |

**理由（P1）**：① 文件（`SAD-004 §5.4` 示例 ＋ `SCS-002 §7.5`／`SCS-003 §7.11`）為**多數且較早**之慣例；② 方案 (b) 會使主題退化為**三層**（`cc.card.card.card.issued`），明顯非設計意圖；③ **現階段零消費者**（`CR-029` 盤點確認），變更成本 ＝ 每服務 **1 個常數**。

> ⚠️ **正式環境之遷移注意**（本批為本地開發，無消費者故直接改）：若已有消費者，需**雙軌發布**（同時寫新舊 topic）≥1 release。**已記入 `SAD-004 §5.4`**。

### #16　事件 vs 同步呼叫 → **(b)「同步為主、事件為輔」**

**裁定**：事件**僅供跨服務投影與通知**；**不得重做同步呼叫已完成之寫入**。
`SAD-004 §5.2` 之「用途」欄**改寫**為「**投影／通知**」語意，並移除「佔用額度」「繳款入帳」等**命令式**描述。

**理由（P3）**：三條重複路徑（auth→limit 佔用／payment→posting 入帳／statement→posting 查詢）若同時走同步與事件，**必然重複套用**（重複佔用＝額度被扣兩次；重複入帳＝帳務翻倍）。且**授權有 <100ms 延遲要求**，同步不可去。

**業界依據**：CQRS／EDA 之標準分工——**write model 之間以同步（強一致）或 saga 協作；read model 與通知以事件（最終一致）餵養**。「同一事實兩條寫入路徑」在支付系統中屬**明確反模式**。

**連帶效果**（`CR-029` 之批次範圍收斂）：
| `CR-029` 條目 | 原判定 | **裁定後** |
|---|---|---|
| #2 `approved.v1` → limit | 需裁定 | **不做**（同步已佔用） |
| #11 `payment.posted.v1` → posting | 需裁定 | **不做**（同步已入帳） |
| #5／#9 `posting.posted/reversed` → statement | 需裁定 | **不做**（結帳時同步查詢） |
| #1 `approved.v1` → posting | 需裁定 | **不做**（無「待請款」模型；且屬命令） |
| **#8 `posting.posted` → limit** | payload 缺口 | **降級為不做**（同步已佔用） |
| **#6／#10 `posting.*` → rewards** | payload 缺口 | **改為「以事件驅動累點」之獨立議題**（見 §4） |
| **#7 `posting.posted` → clearing** | 語意未定 | **不做**（清算以批次讀取，非事件） |

---

## 3. 執行分批

| 批 | 內容 | 性質 | 真機驗證 |
|---|---|---|---|
| **文件批** | `SAD-002 §3`／`SAD-003 §4`／`SAD-004 §5.2`／`§5.4`／`SDD-006 §2.5`／`§4.2`／`SCS-001`／`CR-027`（12 項標記結案） | 治理 | — |
| **A 批（真缺陷）** | **#8** REFERRAL（`auth`）／**#9** unmatched ＋ reversed（`payment` ＋ `limit`）／**#12** `card` 序號統一／**#7** decline metrics（`auth`） | 程式 | **是**（auth／payment／limit／card） |
| **B 批（命名）** | **#15** 四服務常數 ＋ 主題 | 程式 | **是**（product／clearing／dispute／posting） |

---

## 4. 明確**不做**與**留待外部輸入**之事項

### 4.1 本 CR 裁定「不做」（並載明理由，避免日後重提）

| 事項 | 理由 |
|---|---|
| `approved.v1` → limit／posting 之消費端 | **#16**：同步已做 |
| `payment.posted.v1` → posting 之消費端 | **#16**：同步已做 |
| `posting.posted/reversed` → statement 之消費端 | **#16**：結帳時同步查詢即為正解（批次一致性優於事件最終一致） |
| `posting.posted` → clearing | 清算為**批次對帳**作業，以批次讀取為正解；事件驅動反而引入順序與完整性風險 |
| `declined.v1` 之發射 | **#7**：改以 metrics 涵蓋 |

### 4.2 **留待外部輸入**（**不得由開發臆造**）

| 事項 | 需何輸入 |
|---|---|
| `rewards.points.*`／`dispute.case.opened` 之通知 | **業務文案**（模板文字）——由業務單位提供 |
| `posting.*` → rewards 之**自動累點** | **累點規則與 MCC 對映**（且需 `posting.posted.v1` 補 `mcc`／`merchantId`）——屬**新功能**，非事件推廣 |
| 卡組織拒付時限與回應碼對映 | **卡組織規範文件**（真阻塞） |
| `notification` 之**收件人**（手機／Email） | `party-service` 之**聯絡資訊端點**（目前不存在）——需產品決策 |

> **原則**：**「沒人做的事」由架構師裁定；「需要業務定義的事」留給業務。** 兩者不得混淆。

---

## 5. 修訂記錄

| 版本 | 日期 | 說明 |
|---|---|---|
| 1.0 | 2026-10-01 | 初版：依 BEAM PMO 指示「以 Best Practice 直接解決」，**裁定 `CR-027 §5` 全部 12 項**（#5～#16）——先立 **4 條業界通則（P1～P4）**作為方法論，再逐項裁定並載明「理由／業界依據／影響面／可逆性」；★ **其中 #8／#9 為實證之真缺陷**（REFERRAL 佔額不發事件、未匹配／沖正不回沖額度）；★ **#16 之裁定連帶收斂 `CR-029` 之批次範圍**（4 條改判「不做」）；★ 明列**留待外部輸入**之 4 項（業務文案／卡組織規範／聯絡資訊／累點規則） |
