# SCS-005　產品服務（product-service）程式設計規格

| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-005 |
| 版本 | **1.4**（`CR-045` D4：於 §7.1／§10 `#A-130` 新增正向參照至 `SCS-002 §7.14` 首案消費端規格。前版 **1.3**（★ **修正 `paramValue` 之 JSON 序列化缺陷**——`STRING`／`JSON` 型別原產生非法 JSON → `ORA-40441` → HTTP 500）。前版 **1.2**（`GOV-006` 4.8 W-2 推廣・`CR-027` 批次 A-4：**新增 §7.1 參數變更事件輸出**（`product.parameter.changed.v1`）——`SAD-002 §3.5` 明列、`SDD-006 §4.2` 已定義 payload，惟**從未有人發射**。★ 分區鍵＝`namespace/paramKey`；★ **併補 `@Transactional`**（原方法未標交易）。真機驗證通過。前版 **1.1**（`C-2` 落地：5 端點實作＋版本化／熱更新語意定案＋真機 e2e；2026-09-22） |
| 狀態 | **已核定（規格先行）** |
| 對應需求 | `COM-6.1`（參數設定）、`COM-6.2`（共同類／產品主檔） |
| 服務埠號 | 待骨架產生時依 `CSP-006 §4` 分配（自 8081 起編，參 `spec/services.yaml` `port_start`） |
| 套件根 | `com.beam.cardcore.product` |
| 權威 schema | `CC_PRODUCT` |
| 關聯文件 | `SDD-001 §6`、`SDD-006 §4`、`BRD-002`、`GOV-005` |

---

## 1. 對應需求

| FR 編號 | 需求名稱 | 來源 |
|---|---|---|
| `COM-6.1` | 參數設定（參數化，P0–P3） | `BRD-002` |
| `COM-6.2` | 共同類／產品主檔查詢 | `BRD-002` |

> 產品服務亦承載卡片產品主檔（`PRODUCTS`）與外部組織碼對照（`CHARSET_MAPPINGS`），為授權、定價、分期等服務提供產品維度參數來源。

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

| 圖種 | 文件 | 數量（實測） | 本服務相關 |
|---|---|---|---|
| **ER-Diagram** | `SDD-001_領域模型與ER-Diagram` | 16 個 `erDiagram` | `PRODUCTS`／`PARAMETERS`／`CHARSET_MAPPINGS`／`RULE_SETS` 實體（`SDD-001 §6`） |
| **DFD** | `SDD-002_DFD與流程分解` | 10 個 flowchart（L0/L1/L2） | 參數設定資料流（`COM-6.1`） |
| **Sequence** | `SDD-003_Sequence-Diagrams` | 15 個 `sequenceDiagram` | 產品／參數查詢時序 |
| **狀態機** | `SDD-005_狀態機設計` | 10 個 `stateDiagram` | ⚠️ `PRODUCTS.status` 轉換（DRAFT→ACTIVE→SUSPENDED→RETIRED）以 CHECK 約束＋應用邏輯控管，**未定義為正式狀態機**（見 §3、§10） |
| **API Contract** | `SDD-006` + `contracts/openapi/product-service.yaml` | 17 份 | 本服務 3 支端點 |

---

## 2. 服務職責與邊界

**承作**：
- 卡片產品主檔（`PRODUCTS`：產品代碼、卡名、組織 VISA/MC/…、卡等、票種）
- 參數化（`PARAMETERS`：以 JSON 承載各類參數，依 `NAMESPACE`/`PARAM_KEY` 生效區間管理）
- 組織碼對照（`CHARSET_MAPPINGS`：內部碼↔外部組織碼）
- 規則集（`RULE_SETS`：資格／費用／紅利／利息等規則，供他服務引用）

**不做（屬他服務）**：
- 費率試算與利息計算 → `pricing-service`
- 授權決策 → `authorization-service`
- 紅利累兌 → `rewards-service`

---

## 3. 領域模型

- 聚合：`Product`（產品主檔）、`ProductParameter`（參數，含生效區間）、`CharsetMapping`（組織碼對照）、`RuleSet`（規則）
- 值物件：`ProductCode`、`OrgCode`（受限於 `PRODUCTS.org` CHECK）、`ParamKey`、`RuleType`
- 狀態：`PRODUCTS.status ∈ {DRAFT, ACTIVE, SUSPENDED, RETIRED}`（CHECK 約束，預設 DRAFT）；轉換由應用層控管，未建模為 `SDD-005` 狀態機

---

## 4. 程式結構

| 套件／類別 | 職責 | 對應需求 |
|---|---|---|
| `domain/model/Product` `ProductParameter` `CharsetMapping` `RuleSet` | 聚合與值物件、不變式 | `COM-6.2` |
| `domain/port/ProductRepository` `ParameterRepository` `RuleRepository` | 持久化埠 | — |
| `application/ProductQueryService` | 產品主檔查詢 | `COM-6.2` |
| `application/ParameterService` | 參數設定與生效區間管理 | `COM-6.1` |
| `application/RuleEngine` | 規則評估（依 `PRIORITY`） | （規則消費側，供他服務） |
| `infrastructure/jdbc/*RepositoryImpl` | JDBC 實作 | — |
| `interfaces/rest/ProductController` `ParameterController` | REST 端點 | `COM-6.1`/`6.2` |

---

## 5. 資料模型（約束為 Oracle 實測值）

| 表 | 關鍵欄位 | 約束（實測） |
|---|---|---|
| `PRODUCTS` | `ID` RAW(16) PK；`PRODUCT_CODE` VARCHAR2(16) **NOT NULL UNIQUE**；`CARD_NAME`；`ORG`；`CARD_GRADE`；`TICKET_TYPE`；`STATUS` VARCHAR2(16) DEFAULT `'DRAFT'` | `org IN ('VISA','MASTERCARD','JCB','AMEX','UNIONPAY','DINERS','DISCOVER')`；`status IN ('DRAFT','ACTIVE','SUSPENDED','RETIRED')`；`EFFECTIVE_FROM` NOT NULL、`EFFECTIVE_TO` 可空（生效區間） |
| `PARAMETERS` | `ID` PK；`NAMESPACE` VARCHAR2(40)；`PARAM_KEY` VARCHAR2(80)；`PARAM_VALUE` **JSON NOT NULL**；`VALUE_TYPE`；`EFFECTIVE_FROM`/`TO` DATE | `value_type IN ('STRING','NUMBER','BOOLEAN','JSON','AMOUNT','RATE')`；`UQ_PARAM` UNIQUE(`NAMESPACE`,`PARAM_KEY`,`EFFECTIVE_FROM`) |
| `CHARSET_MAPPINGS` | `INTERNAL_CHAR`；`EXTERNAL_CODE`；`ORG` | `UQ_CHARSET` UNIQUE(`ORG`,`INTERNAL_CHAR`) |
| `RULE_SETS` | `RULE_CODE` UNIQUE；`RULE_TYPE`；`CONDITION` JSON；`ACTION` JSON；`PRIORITY` DEFAULT 100；`STATUS` | `rule_type IN ('AUTH_CHECK','REWARD','FEE','INTEREST','DEDUCT','ELIGIBILITY')`；`status IN ('ACTIVE','INACTIVE')` |

> 所有表均含 `CREATED_AT`/`UPDATED_AT` TIMESTAMP(6) WITH TIME ZONE（預設 `SYSTIMESTAMP`）、`VERSION` NUMBER(10) 樂觀鎖。

---

## 6. API 設計（來自 `contracts/openapi/product-service.yaml`）

| 端點 | 方法 | 說明 | 對應 FR |
|---|---|---|---|
| `/api/v1/products` | POST | 建立產品（`CreateProductRequest`→201；代碼重複 `CC-PRD-1001`） | `COM-6.2` |
| `/api/v1/products/{productCode}` | GET | 單筆產品主檔（查無 `CC-PLT-4040`） | `COM-6.2` |
| `/api/v1/products/{productCode}` | PUT | 更新產品（`RETIRED` 終態不可更新 `CC-PRD-3002`） | `COM-6.2` |
| `/api/v1/parameters/{namespace}/{paramKey}` | GET | 查詢參數（選填 `businessDate` 依業務日取版本；查無 `CC-PLT-4040`） | `COM-6.1` |
| `/api/v1/parameters/{namespace}/{paramKey}` | PUT | 設定參數（熱更新；生效日重疊 `CC-PRD-1002`） | `COM-6.1` |

> 錯誤碼沿用 `common-domain` `ErrorCodes`；`org`／`valueType` 列舉於**應用層檢核**（400 `CC-PLT-4003`，不落 DDL CHECK 變 500）。⚠️ `copyFrom` 之完整複製語意（§10）與狀態機端點屬後續增量。

### 6.1 參數版本鏈語意（`C-2` 定案）

| 操作 | 行為 |
|---|---|
| PUT（新版本 `[from, to?]`） | **自動封閉**被覆蓋之現行版本（`effective_to = from - 1`）；**開放式版本被有尾端新版本切割 → 尾端自動延續**（同值補 `[to+1, ∞)`，避免參數憑空消失） |
| 真重疊 | 較晚版本起日落入新區間內（以封閉後狀態重算）→ 400 `CC-PRD-1002` |
| 查詢 | `businessDate`（預設當日）取 `covers(date)` 之版本；查無 → 404 `CC-PLT-4040` |

---

## 7. 關鍵邏輯

- **參數生效區間**：`PARAMETERS` 以 `EFFECTIVE_FROM`/`EFFECTIVE_TO` 管理版本，查詢須帶 `AS_OF` 時間取當時有效版本；`UQ_PARAM` 防止同鍵同起始日重複。
- **組織碼對照**：外部系統（如 VISA/MC 檔）交換前，經 `CHARSET_MAPPINGS` 轉內部碼，避免外部碼污染核心。
- **規則評估**：`RuleEngine` 依 `PRIORITY` 由小到大評估 `RULE_SETS`（條件 JSON 匹配 → 動作 JSON）。

### 7.1 參數變更事件輸出（`product.parameter.changed.v1`）——2026-10-01 新增（`GOV-006` 4.8 W-2 推廣・`CR-027` 批次 A-4）

> **緣由**：`SAD-002 §3.5` 明列本服務**發布** `product.parameter.changed.v1`（用途：**參數熱更新**），
> `SDD-006 §4.2` 亦已定義 payload，惟**從未有人發射**（同 `SCS-002 §7.5` 之型）。
> 本節補**生產端**。

**主題**：`cc.product.parameter.changed`（命名規則 `SAD-004 §5.4`）。

| 項目 | 值 |
|---|---|
| `eventType` | `product.parameter.changed.v1` |
| Outbox `aggregate_type` | `product` |
| Outbox `event_type` | `product.parameter.changed`（**不含**版本） |
| **Kafka 分區鍵** | **`namespace + "/" + paramKey`** |

**payload（`v1`；欄位名依 `SDD-006 §4.2`）**：`namespace`／`paramKey`／`effectiveFrom`。

**處理**：`ParameterService.set` 於 `repository.save(version)` 後、**同一交易**寫 outbox。

**設計要點**：

- ★ **分區鍵為 `namespace/paramKey`（而非版本列之 `id`）**：本事件之**聚合是「該參數」**，
  非某個版本——同一參數之**歷次變更須落同一分區**以保序（`SAD-004 §5.4`）。
- ★ **補上 `@Transactional`（2026-10-01，`#A-130`）**：原 `set()` **未標交易**——惟其步驟
  （封閉現行版本 ＋（必要時）補延續版本 ＋ 寫新版本）**本應為原子操作**；
  且 Outbox 寫入**必須與權威寫入同交易**，否則失去 Transactional Outbox 之意義（`SDD-001 §3.15`）。
- ⚠️ **消費者為「全體服務」**（`SAD-002 §3.5`）——**消費端尚未實作**（批次 C）。
  故現階段為「**有人發、沒人收**」（**如實揭露**）。
- ★ **首案消費端規格已立**：`limit-service` 之商品參數消費閉環見 **`SCS-002 §7.14`**（`CR-045` D4，2026-10-15 採納立場；**規格先行，程式尚未實作**）。本節（生產端）不變。

**真機驗證（2026-10-01；真實 Oracle ＋ Kafka）**：見 §10 #6。

---

## 8. 外部依賴與降級

| 依賴 | 失敗處理（Stand-in） |
|---|---|
| 僅依賴自身 `CC_PRODUCT` 權威庫；屬讀多寫少之配置型服務 | 無強即時外部依賴；快取（Redis 8）可承接高頻查詢，失效時回查 DB，不降級功能 |

---

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

**18 tests, 0 failures**（2026-09-22，`--rerun-tasks` 實測）

| 測試類別 | 案例數 | 對應 |
|---|---|---|
| `ProductServiceTest` | 6 | 建立 DRAFT／代碼重複 `CC-PRD-1001`／org 非法 400／查無 404／更新版本遞增 |
| `ParameterServiceTest` | 8 | 依業務日取版本／自動封閉／真重疊 `CC-PRD-1002`／同生效日／查無 404／valueType 檢核／切割尾端延續 |
| `LayeringTest`（ArchUnit） | 4 | `CSP-002 §6.3` |

**§9.3 真機 e2e（8085，使用者 token）**：產品 201→200→PUT 200（org 更新）；重複代碼 `CC-PRD-1001`；參數版本鏈 09-21→404（生效前）、09-22→3（v1）、09-26/10-14→9（v2 切割）、**10-16/11-01→3（尾端延續）**——DB 與 API 一致。

---

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

- `PRODUCTS.status` 轉換未建模為 `SDD-005` 正式狀態機，目前以 CHECK 約束＋應用層邏輯控管；若客戶要求正式狀態機，須提 CR 補 `SDD-005`。
- `RuleEngine` 之規則語意範圍（哪些 FR 實際消費 `RULE_SETS`）待各消費側服務（授權／紅利／費用）確認後凍結。
- ~~`product-service` 骨架與埠號尚未產生~~ → ✅ **已過時**：埠 8085／`CC_PRODUCT` schema／Flyway V1 皆已就緒，`C-2` 已實作（1.1）。
- `copyFrom` 之完整複製語意（來源產品參數複製範圍）本期僅驗證來源存在，未實作複製；產品狀態機端點（DRAFT→ACTIVE）屬後續增量。
- **`product.parameter.changed.v1` 之消費者尚未實作**（`§7.1`，`#A-130`）：`SAD-002 §3.5` 載其消費者為**全體服務**（參數熱更新）——**無任何服務訂閱** → 現階段為「**有人發、沒人收**」（**如實揭露**）。★ **首案（同步取參）消費端規格見 `SCS-002 §7.14`**（`CR-045` D4，規格中；程式待立 SCS 後實作）。
- **真機驗證（2026-10-01，`#A-130`；真實 Oracle ＋ Kafka）**：`PUT /api/v1/parameters/{ns}/{key}`（`X-Channel`／`X-Idempotency-Key` 必填）→ **HTTP 200** → `outbox` 落列（`product.parameter.changed`、`aggregate_id`＝**`e2e-ns/event-test`**、`published=YES`）→ Kafka 主題 `cc.product.parameter.changed` **自動建立**；消費得信封，**payload 與 `SDD-006 §4.2` 逐欄一致**（`namespace`／`paramKey`／`effectiveFrom`）。**併註**：實作時**補上 `@Transactional`**（原方法未標交易——版本鏈之「封閉現行／補延續／寫新版本」**本應原子**，且 Outbox 須與權威寫入同交易，`SDD-001 §3.15`）。

---

**★ 缺陷修正（2026-10-01，真機驗證時發現）**

> **`paramValue` 之 JSON 序列化錯誤 → `ORA-40441` → HTTP 500**
>
> `CC_PRODUCT.parameters.param_value` 為 **JSON 欄**。原實作以 `paramValue.toString()` **直接寫入**：
> - **`valueType=STRING`** ＋ `paramValue="v1"` → 寫出**裸字串** `v1` → **非法 JSON** → `ORA-40441` → **HTTP 500**
>   （實測：`PUT /api/v1/parameters/e2e-ns/topic-test` 回 **500 `CC-PLT-5000`**）
> - **`valueType=JSON`** ＋ `{"a":1}` → `Map.toString()` 產生 `{a=1}` → **非法 JSON**（同型缺陷）
>
> **修正**：一律以 **Jackson 序列化**（`ParameterService.toJson`）——
> `"v1"`／`100`／`true`／`{"a":1}`／`[1,2]` 皆為合法 JSON。
> **回歸測試**：`ParameterValueJsonTest` **6 例**（含特殊字元轉義）。
>
> ⚠️ **揭露**：本缺陷**非** `CR-030` 之 12 項之一，係於**驗證 `#15`（主題命名）時順帶發現**——
> 原請求以 `valueType=AMOUNT` 可繞過（整數 `toString()` 恰為合法 JSON），故長期未被察覺。

## 11. 修訂記錄

| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 1.4 | 2026-10-15 | **`CR-045` D4（首案消費閉環）**：於 `SCS-005 §7.1`／`§10 #A-130` 新增正向參照至 `SCS-002 §7.14`（首案商品參數消費端規格已立；**程式尚未實作**）。本服務（生產端）內容不變 |
| 1.3 | 2026-10-01 | WorkBuddy AI | **★ 修正 `paramValue` 之 JSON 序列化缺陷**——原以 `paramValue.toString()` 直接寫入 **JSON 欄**：`valueType=STRING` 產生裸字串（如 `v1`）→ **非法 JSON** → `ORA-40441` → **HTTP 500**；`valueType=JSON` 之 `Map.toString()`（`{a=1}`）同型。改以 **Jackson 序列化**（`ParameterService.toJson`）。**回歸測試** `ParameterValueJsonTest` 6 例。★ **本缺陷非 `CR-030` 之 12 項之一**，係驗證 `#15`（主題命名）時順帶發現——原以 `valueType=AMOUNT` 可繞過（整數恰為合法 JSON），故長期未察覺 |
| 1.0 | 2026-09-18 | 規格先行版（埠號與類別名為規劃值） |
| 1.2 | 2026-10-01 | **`GOV-006` 4.8 W-2 推廣・`CR-027` 批次 A-4：新增 §7.1 參數變更事件輸出（`product.parameter.changed.v1`）**——`SAD-002 §3.5` 明列本服務發布、`SDD-006 §4.2` 已定義 payload，惟**從未有人發射**。**實作**：`V2__outbox.sql` ＋ `OutboxPort`／`ParameterEventFactory`／`JdbcOutboxRepository`／`{KafkaEventPublisher,OutboxRelayConfig,OutboxRelayScheduler}`；`ParameterService.set` **同交易**寫 outbox。★ **分區鍵＝`namespace/paramKey`**（聚合為「該參數」，非版本列）。★ **併補 `@Transactional`**（原方法未標交易；版本鏈本應原子，且 Outbox 須同交易）。**真機驗證通過**（payload 與契約逐欄一致）。⚠️ **消費者（全體服務）尚未實作** |
| 1.1 | 2026-09-22 | 依 **`C-2`（BEAM PMO 指示，GOV-006 排序）** 實作 5 端點：產品 CRUD＋參數版本化（熱更新）。**版本鏈語意定案**（§6.1）：自動封閉被覆蓋版本／真重疊 `CC-PRD-1002`／開放式被切割 → 尾端自動延續（同值補 `[to+1, ∞)`，避免參數憑空消失）。單測 **18 例 0 失敗**；真機 e2e（產品 201/200/PUT、參數版本鏈多日期點、DB 一致）；三支掃描器 0 命中。⚠️ `copyFrom` 完整語意與狀態機端點屬後續 | 
