# SCS-004　當事人服務（party-service）程式設計規格

| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-004 |
| 版本 | **1.3**（`CR-046` B2-6：投影讀模型接入可選 `instrumentId`；`V4` 遷移、JDBC `COALESCE` 舊事件相容與 `CardStatusView` 映射）。前版 **1.2**（`CR-031 §4.1.1`：**真機驗證通過**——§9 補 `UT-PTY-008`～`012` 與驗證註記；**更正 §7.5 編號**（v1.1 誤植為 `§7.1`）；更正本表格式；補種子對齊說明）。前版 **1.1**（`CR-031 §2`：**新增 §7.5 卡片狀態投影**——跨服務讀模型；補上 `SAD-002 §3.4` 載明卻從未實作之訂閱）。前版 **1.0** |
| 狀態 | **已核定（規格先行之第一個示範）** |
| 對應需求 | `ISS-2.1`（信用卡持卡人、歸戶管理）、POC-10（市區停車） |
| 服務埠號 | 8084 |
| 套件根 | `com.beam.cardcore.party` |
| 權威 schema | `CC_PARTY` |
| 關聯文件 | `SDD-001 §5.1`、`BRD-002`、`BRD-004 POC-10`、`GOV-005` |

---

## 1. 對應需求

| FR 編號 | 需求名稱 | 來源 |
|---|---|---|
| `ISS-2.1` | 信用卡持卡人、歸戶管理 | 建議書發卡章節 |
| POC-10 | 市區停車（身分參數設定 → 符合資格名單產出 → 停車 → 資格判斷） | `BRD-004 §3.10` |

### 1.1 上層設計依據（本規格**不重複**這些圖，僅引用）

| 圖種 | 文件 | 數量（實測） | 本服務相關 |
|---|---|---|---|
| **ER-Diagram** | `SDD-001_領域模型與ER-Diagram` | 16 個 `erDiagram` | `PARTIES`／`ACCOUNTS`／`BLOCK_CODE_HISTORY` 之實體關係 |
| **DFD** | `SDD-002_DFD與流程分解` | 10 個 flowchart（L0/L1/L2） | 持卡人資料流（`ISS-2.1`） |
| **Sequence** | `SDD-003_Sequence-Diagrams` | 15 個 `sequenceDiagram` | 歸戶／持卡人查詢時序 |
| **狀態機** | `SDD-005_狀態機設計` | 10 個 `stateDiagram` | ⚠️ 當事人狀態機**未定義**（見 §3.2、§10） |
| **API Contract** | `SDD-006` + `contracts/openapi/party-service.yaml` | 17 份 | 本服務 4 支端點 |

> **規範**：SCS 為**程式／服務層級**，設計圖一律引用 `SDD-*` 系列，
> 不在 SCS 內重複繪製（避免兩套圖不同步）。若需新增圖，應先更新 `SDD-*`，再於 SCS 引用。
> 這也是「規格先行」與「單一真實來源」的體現。


**POC-10 對本服務之要求**：須能設定並查詢持卡人之**身分參數**（貼標 `flags`）：
- 每日／每月優惠停車總次數與總時數
- 達月限次簡訊通知註記
- 限定停車場註記

---

## 2. 服務職責與邊界

**承作**：
- 持卡人（Party）基本資料與身分參數（貼標 `flags`）之維護與查詢
- 歸戶（Account）基本資料查詢
- 歸戶管制碼（`block_codes`）設定

**不做（屬他服務）**：
- 卡片核發與生命週期 → `card-service`
- 授權決策 → `authorization-service`
- 額度 → `limit-service`
- 帳務入帳 → `posting-service`
- 實際停車交易與優惠計算 → 屬收單／行銷範疇，本服務**僅提供資格參數（flags）供其判斷

---

## 3. 領域模型

### 3.1 聚合與值物件

| 型別 | 職責 |
|---|---|
| `Party` | 持卡人聚合（不可變）。含身分別、姓名、身分證遮罩、生日、`flags`、狀態 |
| `PartyType` | 身分別列舉（`PRIMARY`／`SUPPLEMENTARY`／`COMPANY`） |
| `Account` | 歸戶聚合（帳號、戶別、狀態） |
| `PartyFlags` | 身分參數貼標（POC-10 之優惠停車參數 |

**設計要點**：
1. **不可變**：所有轉換回傳新實例（`CSP-001 §4.2`）。
2. **個資不落地明碼：身分證／手機／Email 皆以 `*_enc`（BLOB）儲存，對外僅提供 `*_masked`。

### 3.2 狀態機：當事人／歸戶狀態

> ⚠️ **SDD-005 未定義當事人狀態機**。本服務**不自創狀態機**（避免無依據設計）：
> - `PARTIES.STATUS`／`ACCOUNTS.STATUS` 僅攜帶與檢核，不做轉換邏輯。
> - 若未來需要，應先於 `SDD-005` 增訂狀態機後再實作。此缺口已記錄於 §10。

| 維度 | 欄位 | 維護者 | 用途 |
|---|---|---|---|
| 當事人狀態 | `PARTIES.status` | 本服務（僅設定，無轉換邏輯） | 顯示／稽核 |

| 歸戶狀態 | `ACCOUNTS.status` | 本服務（僅設定） | 顯示／稽核 |

### 3.3 身分參數（flags）— POC-10

`FLAGS` 為 `JSON NOT NULL`。POC-10 之參數結構（建議，須與行銷／收單確認）：

```json
{
  "parking": {
    "dailyLimitCount": 2,
    "monthlyLimitCount": 20,
    "dailyLimitHours": 4,
    "monthlyLimitHours": 60,
    "smsOnMonthlyLimit": true,
    "restrictedLots": ["LOT-A", "LOT-B"]
  }
}
```

**設計原則**：
- `flags` 採 **JSON 欄位存放**（DDL 已定義 `FLAGS JSON NOT NULL`），不額外開表（避免過度設計；參數異動頻率低）

- 領域層以 `PartyFlags` record 承載，由基礎設施層做 JSON ↔ record 轉換（領域層不碰 JSON 序列化框架）

### 3.4 管制碼（block_codes）

`ACCOUNTS.BLOCK_CODE` ＋ `BLOCK_CODE_HISTORY`（歷程表）。

- 管制碼設定時**須寫入歷程（稽核留痕）— 銀行案必要。同一交易內完成。

---

## 4. 程式結構

| 套件／類別 | 職責 | 對應需求 |
|---|---|---|
| `domain/model/Party` | 持卡人聚合 | ISS-2.1 |
| `domain/model/PartyType` | 身分別列舉 | ISS-2.1 |
| `domain/model/PartyFlags` | 身分參數貼標 | POC-10 |
| `domain/model/Account` | 歸戶聚合 | ISS-2.1 |
| `domain/port/PartyRepository` | 持卡人持久化埠 | ISS-2.1 |
| `domain/port/AccountRepository` | 歸戶持久化埠 | ISS-2.1 |
| `application/PartyQueryService` | 持卡人查詢 | ISS-2.1 |
| `application/PartyFlagService` | 貼標設定（POC-10 參數） | POC-10 |
| `application/AccountService` | 歸戶查詢 + 管制碼設定（含歷程） | ISS-2.1 |
| `infrastructure/jdbc/JdbcPartyRepository` | JDBC 實作 | ISS-2.1 |
| `infrastructure/jdbc/JdbcAccountRepository` | JDBC 實作 | ISS-2.1 |
| `interfaces/rest/PartyController` | 持卡人／貼標端點 | ISS-2.1, POC-10 |
| `interfaces/rest/AccountController` | 歸戶／管制碼端點 | ISS-2.1 |
| `infrastructure/crypto/PartyPiiCipher` | 身分證／手機／Email 加解密（`common-security`） | `SAD-005 §5` |

---

## 5. 資料模型（**實測值**，2026-09-19）

| 表 | 關鍵欄位 | 實測約束 |
|---|---|---|
| `parties` | `id`(RAW,PK)、`account_id`(RAW,NOT NULL)、`party_no`、`party_type`、`name`、`id_no_enc`(BLOB)、`id_no_masked`(NOT NULL)、`birth_date`、`mobile_enc`(BLOB)、`email_enc`(BLOB)、`relation_type`、`flags`(JSON,**NOT NULL**)、`status`(NOT NULL)、`created_at`、`updated_at`、`version` | `party_type IN ('PRIMARY','SUPPLEMENTARY','COMPANY')`；`ACCOUNT_ID`/`PARTY_NO`/`PARTY_TYPE`/`NAME`/`ID_NO_MASKED`/`FLAGS`/`STATUS`/`CREATED_AT`/`UPDATED_AT`/`VERSION` 皆 NOT NULL（PII 明文欄 `id_no_enc`/`mobile_enc`/`email_enc` 可為 NULL（未收集時）。`birth_date`、`relation_type` 可為 NULL。

| `accounts` | `id`(RAW,PK)、`account_no`、`cif_no`、`account_type`、`block_code`、`statement_cycle`、`statement_day`、`status`、`created_at`、`updated_at`、`version` | （既有 V1 基線；`BLOCK_CODE` 可空 |

| `block_code_history` | `id`、`account_id`、`block_code`、`reason`、`operator_id`、`created_at` | 歷程表（稽核） |

| `card_status_caches` | `id`(RAW,PK)、`card_no_hash`(**CHAR(256)**,`UNIQUE`)、`card_id`、`instrument_id`（RAW，可空）、`account_id`、`card_no_masked`、`status`、`block_code`、`expiry_date`、`last_sequence_no`、`created_at`、`updated_at`、`version` | **`V3` 新增、`V4` 補載體鍵**（`CR-031 §2`／`CR-046 B2-6`）。**跨服務投影**——由 `card.card.status-changed.v1` 餵養，**非**本服務之權威資料；舊列／舊事件可為 null |

| `consumed_events` | `id`(RAW,PK)、`event_id`(`UNIQUE`)、`event_type`、`aggregate_id`、`sequence_no`、`created_at` | **`V3` 新增**。消費冪等表——形狀與其他服務一致（`SAD-004 §5.4`） |

> ⚠️ **`card_no_hash` 為 `CHAR(256)`**：綁定參數為 `VARCHAR2` → 比對**一律 `TRIM`**（同 `authorization-service` 之同名表之註記）。
> ⚠️ 種子資料見 §9；`accounts` 之歸戶主檔須與 `card`／`limit` 之種子**對齊**（`CR-031 §4.4`）。

---

## 6. API 設計（依 `contracts/openapi/party-service.yaml`）

| 端點 | 說明 | 錯誤碼 |
|---|---|---|
| `GET /api/v1/parties/{partyId}` | 查詢持卡人（含 flags） | `CC-PTY-1001` |
| `POST /api/v1/parties/{partyId}/flags` | 貼標設定（POC-10 身分參數） | `CC-PTY-1001` |
| `GET /api/v1/accounts/{accountId}` | 查詢歸戶 | `CC-PTY-1001` |
| `GET /api/v1/accounts/{accountId}/cards` | 查詢該歸戶之**卡片狀態清單**（**跨服務讀模型**；`CR-031 §2`、§7.5） | `CC-PTY-4040`（歸戶不存在） |
| `POST /api/v1/accounts/{accountId}/block-codes` | 管制碼設定（寫歷程） | `CC-PTY-1001` |

**回應要點**：
- 持卡人／歸戶查詢回傳**僅含遮罩個資**（`id_no_masked`），不含明文或密文。

- 貼標設定回傳更新後之完整 `flags`。

---

## 7. 關鍵邏輯

### 7.1 個資處理（`SAD-005 §5`）**
- 寫入：`id_no_enc`／`mobile_enc`／`email_enc` 以 `AES-GCM` 加密（`common-security` 之 `AesGcmFieldEncryptor`）。
- 讀取：僅回傳遮罩欄；**明文不進日誌、不進例外訊息。

### 7.2 貼標設定（POC-10）
- 採**整包覆寫（flags 整體更新）。理由：參數結構單純、避免部分更新之合併複雜度。
- 須驗證 `dailyLimitCount`／`monthlyLimitCount` 為非負整數；`flags` 不得為 null（DDL NOT NULL）。

### 7.3 管制碼設定
- 更新 `accounts.block_code` ＋ 新增 `block_code_history`（同一交易）。
- 記錄 `operator_id`、`reason`（稽核留痕）。

### 7.4 交易邊界**
- 貼標更新：單表更新，交易包住 `UPDATE parties`。
- 管制碼：`UPDATE accounts` + `INSERT block_code_history` 同一交易。

---

### 7.5 卡片狀態投影（跨服務讀模型）——2026-10-01 新增（`CR-031 §2`）

> ⚠️ **編號更正**：本節於 `SCS-004` v1.1 初稿誤植為 **§7.1**，與既有「§7.1 個資處理」**衝突**；
> v1.2 更正為 **§7.5**（`CR-031 §4.1` 之引用同步更正）。

> **緣由**：`SAD-002 §3.4` 明載本服務**訂閱** `card.card.status-changed.v1`，
> 惟**從未實作**（同 `V2` 載明卻無餵養者之型）。本節補上。

**★ 為何是投影，而非同步查詢（架構上的唯一解）**

`cardcore-service-acl.yml` 之 `card-service` **只允許 `cc-authorization-service`** 呼叫
→ **本服務（及任何非授權場景：客服／風控／報表）在架構上無法同步取得卡片狀態**。
故**事件投影是唯一途徑**（`CR-031 §2.1`）。

> **與 `CR-030 §2 #16`（同步為主、事件為輔）之關係**：該通則禁止「**重做同步已做之寫入**」。
> 本項**不存在**可重做的同步路徑（ACL 即擋死）→ 屬「**做沒人做的事**」→ **應做**。

**訂閱主題**：`cc.card.card.status-changed`（`SAD-004 §5.4` 命名規則）。

| 項目 | 值 |
|---|---|
| 事件型別 | `card.card.status-changed.v1` |
| 分區鍵 | `cardId`（信封 `aggregateId`） |
| 存放地 | **`CC_PARTY.card_status_caches`** |
| 業務鍵 | `card_no_hash`（**`UNIQUE`**；與 `authorization` 之同名表同一慣例） |
| 更新語意 | **upsert by `card_no_hash`**；`last_sequence_no` 供**亂序防護** |
| 讀取端點 | **`GET /api/v1/accounts/{accountId}/cards`** → `CardStatusView[]` |

**處理**（複用既有消費端之三步，皆於**同一交易**）：
① 冪等（`consumed_events.event_id`）→ ② 亂序（`sequence_no` 落後於投影列之 `last_sequence_no` → 略過、**不覆寫較新狀態**）
→ ③ upsert ＋ 寫 `consumed_events`。
⚠️ **未知事件型別一律略過並記 WARN**（**不拋錯**——否則新增事件會毒化消費者）。

**讀取語意**：

| 情境 | 回應 |
|---|---|
| 歸戶**不存在** | **404 `CC-PTY-4040`**（與 `GET /accounts/{id}` 一致） |
| 歸戶存在但**尚無卡片投影** | **200 ＋ 空陣列**（**非** 404——「尚無卡片」是合法狀態） |

> ⚠️ **與 `authorization-service` 之同名表之關係（誠實揭露）**：二者**同形狀、不同語意**——
> `authorization` 之投影供**授權熱路徑**（免同步查卡況，`<100ms`）；本服務之投影供**歸戶視圖**。
> **刻意不共用**：跨服務共用讀模型＝**跨庫耦合**（違反「一服務一 schema」`SAD-003 §1 DP-01`）。
> 兩份各自投影是 CQRS 之標準取捨（**以儲存換解耦**）。

> ⚠️ **投影為唯讀且最終一致**（`SAD-003 §4.2`）：**卡片狀態之權威仍在 `card-service`**；
> 本服務僅提供歸戶視角之查詢，**不得**作為寫入來源。
> ⚠️ **不含 PAN**：`cardNoMasked` 為**遮罩值**（先例見 `card.card.status-changed.v1`）——符合 PCI DSS。
> **BR-16 補充**：投影同步保留事件之 `instrumentId`（交易載體最小識別），回應 `CardStatusView` 一併揭露；舊事件／舊列無值時回 `null`。`instrumentId` 是內部識別，不以 `cardNoMasked` 作比較鍵。

## 8. 外部依賴與降級**

| 依賴 | 用途 | 失敗處理 |
|---|---|---|
| Oracle `CC_PARTY` | 權威儲存 | 服務不可用（無降級 — 當事人資料無本地快取，查無即回 `CC-PTY-1001`。

> **無 Stand-in 降級**：當事人資料非即時交易路徑，授權服務未直接呼叫本服務（授權僅用卡片／額度）。故不設計降級，避免不必要的複雜度。若未來授權需查當事人，再另行評估（含快取一致性）。

---

## 9. 測試案例（對應 `TRP-001`）**

| 編號 | 情境 | 預期 |
|---|---|---|
| `UT-PTY-001` | 查詢持卡人（存在） | 200，回傳遮罩個資 + flags |
| `UT-PTY-002` | 查詢持卡人（不存在） | 404（`CC-PTY-1001`） |
| `UT-PTY-003` | 貼標設定（POC-10 參數） | 200，flags 更新 |
| `UT-PTY-004` | 貼標設定（flags 為 null） | 400（`CC-PTY-1001`） |
| `UT-PTY-005` | 查詢歸戶 | 200 |
| `UT-PTY-006` | 管制碼設定 | 200，`accounts.block_code` 更新 + 歷程新增 1 筆 |
| `UT-PTY-007` | 管制碼設定（無 operator/reason） | 400（稽核欄位必填） |
| `UT-PTY-008` | 卡片狀態投影（已知事件型別） | 透傳至埠；序號與完整狀態原樣傳遞（`CardStatusProjectionServiceTest`） |
| `UT-PTY-009` | 卡片狀態投影（**未知事件型別**） | **略過且不拋錯**、**不落到埠**（避免毒化消費者；`SAD-004 §5.4`） |
| `UT-PTY-010` | 卡片狀態投影（埠回 `false`＝重複／亂序） | 如實回 `false`，不吞不轉 |
| `UT-PTY-011` | 查該歸戶之卡片（歸戶不存在） | 404（`CC-PTY-4040`） |
| `UT-PTY-012` | 查該歸戶之卡片（歸戶存在、無投影） | 200、**空陣列** |

**測試資料（種子）**：需種入至少 1 個歸戶 + 2 個持卡人（PRIMARY／SUPPLEMENTARY），`flags` 含 POC-10 參數。種子檔：`ops/local/init/oracle/06-dev-party-seed.sql`。
⚠️ **`UT-PTY-011`／`012` 需歸戶主檔與其他服務之種子**對齊**——見 `CR-031 §4.4`（`party` 種子原僅有 `…0010`，與 `card`／`limit` 之 `…0001`～`…0003` 對不上，已補齊）。

> **真機驗證（2026-10-01）**：`CR-031 §4.1.1`——8 項樣本全數通過（含**兩項負樣本**：重複 `eventId`、較舊 `sequenceNo`）。
> ⚠️ 投影列由**事件餵養**，**非種子種入**；種子僅提供歸戶主檔。

---

## 10. 已知限制（誠實揭露）**

1. **當事人／歸戶狀態機未定義（SDD-005 無）。本服務不做狀態轉換邏輯，僅設定／攜帶 `status`。
2. **POC-10 完整流程未實作**：本服務僅提供「身分參數（flags）設定與查詢」；「符合資格名單產出」與「停車資格判斷」屬行銷／收單範疇，不在本服務。
3. **歸戶（accounts）僅查詢與管制碼設定**；未含開戶／結清等生命週期（屬其他流程）**
4. **`flags` 結構未與行銷／收單確認**（本文件為建議結構，須跨服務確認後凍結。
5. **監控未接**（`CSP-004` 僅追蹤碼）**
6. **無 Stand-in 降級**（見 §8）**

---

## 11. 修訂記錄**

| 版本 | 日期 | 作者 | 說明 |
|---|---|---|---|
| **1.3** | **2026-10-16** | WorkBuddy AI | **`CR-046` B2-6（`#A-249`）**：新增 `instrumentId` 至 `CardStatusSnapshot`／`card_status_caches` 投影／`CardStatusView`；新增 `V4__card_status_instrument_key.sql`（nullable＋索引）；舊事件相容（INSERT 可空、UPDATE `COALESCE` 不清除既有值）；契約由產生器重生。 |
| 1.2 | 2026-10-01 | WorkBuddy AI | **`CR-031 §4.1.1` 真機驗證通過**：§9 補 `UT-PTY-008`～`012`（含**未知事件型別略過**、**亂序／重複略過**、**404 vs 空陣列**）＋ 真機驗證註記；★ **更正 §7.5 之編號**（v1.1 誤植為 `§7.1`，與既有「§7.1 個資處理」衝突）；★ **更正本表格式**（v1.1 之列多一欄，與表頭不符）；補 §9 種子與其他服務**對齊**之說明（`CR-031 §4.4`） |
| 1.1 | 2026-10-01 | WorkBuddy AI | **`CR-031 §2`（`#A-143`）：新增卡片狀態投影（跨服務讀模型）**——補上 `SAD-002 §3.4` 載明卻**從未實作**之訂閱。★ **架構上之唯一解**：`card-service` 之 ACL 只允許 `cc-authorization-service` → 本服務**無法同步**取得卡片狀態。新增 `V3__card_status_projection.sql`／`CardStatusProjectionPort`／`CardStatusProjectionService`／`CardStatusChangedListener`／`GET /api/v1/accounts/{accountId}/cards`。契約端點 92 → **93**（0 錯誤）。⚠️ 本版之章節編號誤植為 §7.1（見 1.2 更正） |
| 1.0 | 2026-09-19 | WorkBuddy AI | 初版：**規格先行**（實作前完成）。依 `GOV-005` 與 `SCS-000` 模板撰寫；資料模型約束為 Oracle 實測值。 |
