# CR-047　活動回饋上限（cap）用量模型與回饋價值換算參數化

| 項目 | 內容 |
|---|---|
| 文件編號 | CR-047 |
| 版本 | **1.0** |
| 狀態 | **施工中**（BEAM PMO 2026-10-17 裁定：① 需開 CR 並**回訪既有架構**；② 點↔元換算之「點」與「元」**都要是參數**，現防呆為 1 點＝1 元） |
| 提出日期 | 2026-10-17 |
| 提出者 | 開發團隊（WorkBuddy AI） |
| 觸發 | `CR-046` §7.9 之 cap 兩落差（`#A-262`）——BEAM PMO 裁定「**若需要 CR 才能執行，請開立 CR，並審慎執行，尤其必須回訪既有架構是否需要調整**。參數極大化是這次微服務化的宗旨之一，不能用寫死或 workaround 解決」 |
| 需求依據 | `BRD-005` §11（cap 六單位各自可配）、BR-03（回饋計算與上限截斷）、BR-14（預算每戶上限）；例 1（每戶每期上限 500 元）、例 3（月上限 50 元） |
| 規格依據 | `SCS-026 §7.9`（提案）→ 本 CR 定案後改為 §7.10；`SCS-010 §7`（累積）、`SCS-002 §7.14`（商品參數消費閉環慣例） |
| 影響模組 | `campaign-service`（cap 用量＋回填）、`rewards-service`（活動歸屬＋事件欄位）、`CC_CPG`／`CC_RW` 遷移 |

---

## 1. 問題陳述（兩落差，實查）

| # | 落差 | 實查證據 |
|---|---|---|
| ① | `campaign_caps` 僅有活動級單一 `consumed_minor`，**無任何 scope／period 維度** | `V1__baseline.sql`：`(id, campaign_code, cap_unit, cap_value_minor, consumed_minor, …)`；`cap_unit` CHECK 含六值（`PER_CARD`／`PER_HOUSEHOLD`／`PER_STATEMENT`／`PER_MONTH`／`PER_YEAR`／`PER_HOUSEHOLD_MEMBER`） |
| ② | `rewards.points.accrued.v1` payload **無活動歸屬** | `SCS-010 §7.1` payload＝`accountId`／`pointsType`／`bucket`／`points`／`afterBalance`——campaign 收到回流無法識別屬哪個活動 |

**兩者皆非加一欄可解**：BRD §11 明訂 cap「每卡/每戶/每期/每月/每年 **各自可配**」——同一活動可**並存多組** cap（例：每戶 500 元 **且** 每月 200 元）；且同一 cap 需對**多個 scope 實例**分別記帳（戶 A／戶 B／2026-10 月／2026 年…）。單表單計數器無法表達。

---

## 2. ★ 架構回訪（BEAM PMO 要求；逐項結論）

| # | 回訪問題 | 結論 | 依據 |
|---|---|---|---|
| R1 | `campaign_caps` 是否應直接加維度欄？ | **否——改為「定義／用量」兩表分工**：`campaign_caps` 答「**上限是多少**」（定義，不變）；新表 `campaign_cap_usages` 答「**已用多少**」（可變用量，逐 scope 實例一列） | 專案既有原則「兩張表分工要能一句話講清」（`CR-030` P1 單一真實來源） |
| R2 | 用量維度如何參數化（不 hardcode）？ | **通用 `scope_key`＋`period_key`**：由 `cap_unit` 決定填入語意——`PER_CARD`→`instrumentId`；`PER_HOUSEHOLD`→`householdId`；`PER_HOUSEHOLD_MEMBER`→`partyId`；`PER_STATEMENT`→帳單期識別；`PER_MONTH`→`YYYYMM`；`PER_YEAR`→`YYYY`。**新增單位不需改表結構**（參數極大化） | BEAM PMO 裁定「不能用寫死或 workaround」 |
| R3 | 上限與用量的**單位**？ | 統一以**回饋價值（分）** 記帳；點數→價值以**商品參數**換算（見 §3），**兩側皆參數** | BEAM PMO 裁定 ② |
| R4 | rewards 側是否需調整？ | **是**——`AccrualRequest` 加可選 `campaignCode`（來源＝campaign 指令）、`POINTS_LEDGER` 加 `campaign_code` 可空、`points.accrued.v1` payload 加可選 `campaignCode`（依 `SAD-004 §5.3` **不升版**） | 落差 ②；不臆造反查 |
| R5 | 是否需改 `SCS-026 §3 #17`？ | **是**——原列五種單位，**漏 `PER_HOUSEHOLD_MEMBER`**（DDL 有）；本 CR 更正 | 文件漂移（preflight 紀律） |
| R6 | 本期可完整實作之單位？ | `PER_CARD`／`PER_HOUSEHOLD`／`PER_MONTH`／`PER_YEAR`（來源齊）；**`PER_STATEMENT`**（帳單期需 statement 事件）／**`PER_HOUSEHOLD_MEMBER`**（附卡人識別需 party 歸屬）→ **fail-closed**，記 WARN 不臆造 | 不臆造原則 |

---

## 3. 設計

### 3.1 用量模型（`V6__cap_usages.sql`，campaign）

```sql
CREATE TABLE campaign_cap_usages (
  id            RAW(16) PRIMARY KEY,
  campaign_code VARCHAR2(40) NOT NULL,
  cap_id        RAW(16) NOT NULL,          -- → campaign_caps.id（定義列）
  scope_key     VARCHAR2(64) NOT NULL,     -- 由 cap_unit 決定之實例鍵（戶／卡／YYYYMM／YYYY）
  period_key    VARCHAR2(16),              -- 期間維度（月／年／帳單期）；非期間型為 NULL
  consumed_minor NUMBER(18) DEFAULT 0 NOT NULL,
  created_at / updated_at / version,
  CONSTRAINT uq_cap_usage UNIQUE (cap_id, scope_key, period_key)
);
```

- **原子遞增**：`UPDATE … SET consumed_minor = consumed_minor + ? WHERE cap_id=? AND scope_key=? AND period_key=?`（單語句；同 `campaign_quotas` 慣例）。
- **事前截斷**（指令下發前）：以 `campaign_caps.cap_value_minor` 為界，檢查 `consumed_minor + 本次價值 > cap_value_minor` → **不發指令**（截斷）。
- **事後記帳**（`points.accrued` 回流）：以實際入點價值遞增用量（rewards 實際入點可能因層級倍率與指令推定不同——**以回流為準**，記 WARN 差異）。

### 3.2 回饋價值換算（商品參數，兩側皆參數）

| 參數 | 命名空間／鍵 | 語意 | 預設（防呆） |
|---|---|---|---|
| 點值 | `rewards` / `point-value-minor` | **1 點 ＝ N 分**（回饋價值） | **`1`（＝1 點 1 元，現行防呆）** |

- 取值：`GET /api/v1/parameters/{ns}/{key}`（帶 `businessDate`；`SCS-002 §7.14` 已驗證之消費慣例）。
- **「點」與「元」皆為參數**：`points` 為實際值（資料）、`point-value-minor` 為換算參數（政策）；換算後以**分**記入用量。改變參數值 → cap 截斷行為隨之改變（可驗收）。
- 取參失敗 → **fail-closed**（不截斷亦不臆造換算：記 WARN 並**不發指令**，沿用 `SCS-002 §7.14 A4` 降級模式）。

### 3.3 rewards 側調整（活動歸屬）

| 變更 | 內容 |
|---|---|
| `AccrualRequest` | 加可選 `campaignCode`（campaign 指令已帶；HTTP 契約同步加可選欄位） |
| `POINTS_LEDGER` | `V6__ledger_campaign.sql` 加 `campaign_code VARCHAR2(40)` 可空 |
| `points.accrued.v1` | payload 加可選 `campaignCode`（不升版；`SDD-006 §4.2` 同步） |

---

## 4. 驗收（真機）

1. **事前截斷**：cap 每戶 500 元（＝50,000 分）、點值 1 → 第 3 筆指令使推定價值超限 → **不發**；
2. **事後回填**：`points.accrued` 回流 → 對應 `(cap_id, scope_key, period_key)` 用量遞增；
3. **期間邊界**：`PER_MONTH` 跨月重置（10 月用量不計入 11 月）；
4. **換算參數**：`point-value-minor` 由 1 改 2 → 同一筆交易之推定價值加倍 → 截斷點前移（**改參數→行為改變**，非快取）；
5. **fail-closed**：取參失敗／`PER_STATEMENT`／`PER_HOUSEHOLD_MEMBER` → 不發指令並記 WARN；
6. 不變式：`consumed_minor` 不得超過 `cap_value_minor`（應用層截斷＋CHECK 為防線）。

---

## 5. 風險與邊界

1. **推定價值 vs 實際入點**：campaign 事前以推定價值截斷、事後以實際回流記帳——兩者可能有差（層級倍率）；本期**以回流為準**並記差異 WARN，不回溯撤銷已入點（撤銷屬 clawback 範圍，BR-15）。
2. **`PER_STATEMENT`／`PER_HOUSEHOLD_MEMBER`**：fail-closed，待帳單期事件／party 歸屬來源齊備後另立增量。
3. **疊加（D9）**：多活動並存之截斷次序（`priority`／`exclusion_group`）**未實作**——本期 cap 僅單活動維度，不得宣稱疊加已正確。

---

## 6. 修訂記錄

| 版本 | 說明 |
|---|---|
| 1.0 | 2026-10-17：初版。依 BEAM PMO 裁定開立；含**架構回訪 R1–R6**（定義／用量兩表分工、通用 scope/period 參數化、單位以回饋價值記帳、rewards 側活動歸屬調整、`PER_HOUSEHOLD_MEMBER` 文件更正、兩種單位 fail-closed）；設計、驗收、風險。 |
| 1.2 | 2026-10-17：★ **`POINTS_LEDGER.CAMPAIGN_CODE` 寫入完成**（1.1 之待辦結案）——`PointsLedgerEntry` 加 `campaignCode`（末尾）＋`JdbcPointsRepository`（INSERT 欄位／綁定、`LEDGER_COLUMNS` 加欄、mapRow）＋`AccrualService` 帶 `request.campaignCode()`（到期／兌換分錄為 null）；稽核可追溯「此筆點數屬哪個活動」。 |
| 1.1 | 2026-10-17：★ **實作完成（真機通過）**。① 遷移：`CC_CPG` `V6__cap_usages.sql`（`campaign_cap_usages`，通用 `scope_key`／`period_key`＋原子遞增）、`CC_RW` `V6__ledger_campaign.sql`（`POINTS_LEDGER.campaign_code` 可空）；② campaign：`CapUsageRepository`／`JdbcCapUsageRepository`、`CapGuardService`（事前截斷／事後回填）、`PointAccruedListener`（`cc.rewards.points.accrued`）、`TransactionRecognitionService` 下發前經守衛；③ rewards：`AccrualRequest` 加 3 可選欄位、`accrued` payload 加 4 可選欄位（不升版）、`PointValuePort`／`ProductPointValueHttpClient`（點值商品參數；**預設 1＝既有防呆**）；④ 契約：`gen_openapi.py` 同步（106 endpoints／0 errors）。**驗收**：未達上限可發／回填達上限停發／`PER_MONTH` 跨月獨立／`PER_STATEMENT` fail-closed 皆真機通過；campaign **43 例**、rewards **158 例** 全綠；SQL 30 檔 0 違規；掃描器 0 命中；契約↔DTO 0 不符。★ **過程由關卡攔下兩次真實漂移**：`verify_contract_dto`（DTO 有、契約無之 3 欄）與 rewards 既有「payload 恰 5 欄」斷言（契約擴充之判定力）。**仍缺**：精確事前截斷（需 `rate_schedule`）、`POINTS_LEDGER.campaign_code` 之寫入（欄位已備、寫入列下一增量）、`PER_STATEMENT`／`PER_HOUSEHOLD_MEMBER`（來源未備）。 |
