# CR-014　專用額度（POC-8）**資料模型裁定**——接線既有 `SPECIAL_LIMITS`

| 項目 | 內容 |
|---|---|
| 文件編號 | CR-014 |
| 版本 | **1.1**（✅ 裁定：採 **選項 A′**＝**接線既有** `SPECIAL_LIMITS`；併**更正 v1.0 之「新表」誤述**） |
| 狀態 | ✅ **已裁定**（2026-10-02）——依 BEAM PMO 之授權「以 Best Practice 直接將這些問題解決」，由架構師裁定並逕行實作 |
| 提出日期 | 2026-09-22（v1.0）／裁定 2026-10-02（v1.1） |
| 提出者 | WorkBuddy AI（開發團隊） |
| 緣起 | `#A-75` ③：SPECIAL 之 apply 回 422「查無主檔」——`applyAdjustedAmount` 無 SPECIAL 分支 |
| 關聯 | `BRD-004 POC-8`、`BRD-001 §5.4 BF-04`、`CR-001 §2.3`、`SDD-001 §3.4`、`SAD-002 §6`、`ISS-2.4`、`GOV-006 2.2.3b`、`SCS-002` |
| 嚴重度 | 🟡 中——POC-8（R1）之完整驗收受阻 |

---

## 0. ★ 對 v1.0 之重大更正（先讀本節）

> **v1.0 標題與 §2 現況皆誤**：v1.0 稱本案為「**`SPECIAL_LIMITS` 新表**」，
> 並於 §2「現況（實測）」列了三點而**獨漏該表**。
>
> **事實（2026-10-02 真機複查）**：`CC_LIM.SPECIAL_LIMITS` **早已存在**——
> - **來源**：`limit-service/src/main/resources/db/migration/V1__baseline.sql`，
>   該檔由 `tools/gen_flyway_baseline.py` 依 **`SDD-001 §3.4`** 自動產生（非人工手寫）。
> - **真機實測**：`SELECT table_name FROM all_tables WHERE owner='CC_LIM'` →
>   `ACCOUNT_LIMITS / CONSUMED_EVENTS / LIMIT_ADJUSTMENTS / LIMIT_HOLDS / OUTBOX / SPECIAL_LIMITS`；
>   `SPECIAL_LIMITS` 11 欄、**0 列**、`Flyway` 已於 2026-09-19 基線套用。
> - **文件早已載明**：`CR-001 §2.3`（2026-09-19）已將 `SPECIAL_LIMITS` 列為
>   「**特定用途**額度（非一般信用額度）」，§2.4 並實測其 `PURPOSE` 之 `CHECK` 值域。
>
> **根因（本專案第 N 次「實測範圍不足」型缺陷）**：v1.0 之「實測」**只掃了 Java 程式碼**
> （`grep` 全庫無 `special_limits` 之 Java 引用），**未掃 `db/migration` 之 DDL**，
> 亦未回頭核對 `CR-001` 之既有記載 → 誤判為「表不存在」。
> 此與 `CR-033` 之「幽靈待辦」同型：**權威清單／既有事實未被回查**。
>
> **結論**：本案**不是「新建表」**，而是「**把既有表接上寫入與查詢路徑**」——
> 這使實作**更小、風險更低**（**不動既有表結構**，除 `currency` 一欄之增補，見 §5）。

---

## 1. 語意查證（設計依據）

| 來源 | 語意 |
|---|---|
| `BRD-001 §5.4 BF-04` | 專用額度＝**依用途設定**（繳稅／保費／特定 MCC），與永久／臨時並列為第三種額度異動類型；流程含權限檢核（依角色與客層之上限）→ 覆核 → 生效 → **同步至授權服務（含快取更新）** → 稽核 |
| `BRD-004 POC-8` | 8-1 參數設定（新增／修改／刪除／**查詢**、依人員權限控管）；8-3 使用專用額度**不佔卡人額度**；8-4 未設定則用卡人額度；8-5 超額拒絕；8-6/8-7 綜所稅請款→**調高**臨時額度（截止日＋7 日）／繳款→**調降** |
| `SAD-002 §6.2`（分流鐵律） | 同一歸戶不得跨波次——專用額度設定亦須可追蹤歸戶 |
| `CR-001 §2.3／§2.4` | `SPECIAL_LIMITS`＝**特定用途**額度；`PURPOSE` 值域實測為 `('TAX','INSURANCE','TUITION','MEDICAL','OTHER')` |
| `SDD-001 §3.4` | `lim.special_limits` 之權威 DDL（即 `V1__baseline.sql` 之來源） |

**關鍵推論**：一個歸戶可同時存在**多筆**專用額度（不同用途），
各有**獨立的金額、效期、申請／覆核軌跡與生命週期**——這是**一對多的常駐定義**，
不是主檔上的一個欄位，也不是一筆不可變的異動紀錄。

---

## 2. 現況（實測；★ 已更正）

| 對象 | 實測結果（2026-10-02，真實 Oracle 23.26 `FREEPDB1`／schema `CC_LIM`） |
|---|---|
| `ACCOUNT_LIMITS` | 僅 `permanent_amount`／`temporary_amount`／`shadow_amount`／`used_amount`——**無專用額度欄位**；3 列、皆 `TWD`、單帳戶單幣別 |
| `LIMIT_ADJUSTMENTS` | `adjust_type` 含 `SPECIAL`（`CHECK` 值域已備），但 `applyAdjustedAmount` 之 `switch` **無 `SPECIAL` 分支** → apply 回 422 |
| **`SPECIAL_LIMITS`** | **★ 早已存在**（`V1__baseline.sql` ← `SDD-001 §3.4`，Flyway 基線 2026-09-19 套用）。11 欄：`ID RAW(16)` PK／`ACCOUNT_ID RAW(16) NOT NULL`／`PURPOSE VARCHAR2(24 CHAR) NOT NULL`／`AMOUNT NUMBER(19,0) NOT NULL`／`USED_AMOUNT NUMBER(19,0) DEFAULT 0`／`EFFECTIVE_FROM DATE NOT NULL`／`EFFECTIVE_TO DATE`／`STATUS VARCHAR2(16 CHAR) DEFAULT 'ACTIVE' NOT NULL`／`CREATED_AT`／`UPDATED_AT`／`VERSION`。**0 列**。索引僅 PK（`SYS_C0010675`）。**無任何 Java 程式引用**（**表已存在、程式全無**型缺口，同 `#A-117`／`#A-118` 之卡片狀態事件） |
| 值域（`CHECK`，實測） | `purpose IN ('TAX','INSURANCE','TUITION','MEDICAL','OTHER')`；`amount >= 0`；`status IN ('ACTIVE','EXPIRED','CANCELLED')` |
| 查詢端點 | `GET /api/v1/accounts/{accountId}/special-limits`（`C-11`）已上線，**目前查 `LIMIT_ADJUSTMENTS` 之 SPECIAL 紀錄**（空清單）——即**讀錯表** |

---

## 3. 選項對照（v1.0 三案 ＋ 本次新增 A′）

| 選項 | 內容 | 判定 |
|---|---|---|
| **A′（✅ 裁定）** | **接線既有 `SPECIAL_LIMITS`**：寫入路徑補 SPECIAL 分支、查詢端點改讀本表；**僅增補 `currency` 一欄**（見 §5） | **採納**。單一真實來源（**不重複建表**）；沿用既有值域即權威；改動最小 |
| A（v1.0 之建議） | **新建** `SPECIAL_LIMITS` 表（自訂 `purpose_code`／`scope_ref`／`status` 值域） | **❌ 不可行**——表已存在（§0）；且會與 `SDD-001 §3.4`／`V1` **同名衝突** |
| B | 主檔欄位（`account_limits.special_amount`） | ❌ 一帳戶僅一筆一用途，違反 POC-8 多用途語意 |
| C | `LIMIT_ADJUSTMENTS` 擴充（加 `purpose`／`scope`） | ❌ 調整表是**不可變稽核紀錄**，而專用額度是**可變更之現行定義**（8-1 要能修改／刪除）——語意不合 |

---

## 4. 裁定（2026-10-02）

> **採選項 A′**。裁定者：架構師（依 BEAM PMO 之明確授權「以 Best Practice 直接將這些問題解決」）。

**理由**：

1. **單一真實來源（`CR-030` P1）**：權威表已存在，**再建一張同名表＝兩套真相**。
2. **既有值域即權威**：`purpose`／`status` 之 `CHECK` 為**真機既有事實**，
   文件（`CR-001 §2.4`）亦已記載；**改動已套用之約束＝無必要之破壞性變更**。
3. **「不佔卡人額度」自然成立**：`SPECIAL_LIMITS` 是**獨立額度池**（`8-3`），
   與 `account_limits` 無欄位重疊——`CR-001 §2.4` 早已據此拒絕以本表充當信用額度。
4. **生命週期語意對映完整**（見 §5）：申請／覆核之**軌跡**留在 `LIMIT_ADJUSTMENTS`，
   本表只存**現行定義**——兩者職責分明，非重複。
5. **改動最小**：除 `currency` 一欄與兩個索引外，**不動既有表結構**。

**業界依據**：信用卡核心之「專用額度／用途額度（purpose-based limit / dedicated limit）」
在業界（Vision Plus、Fiserv、Temenos 等）一律以**獨立於一般信用額度之定義表**實作，
每筆具備用途別、金額、效期、狀態，並由授權端於交易時**併查**；
申請與覆核之軌跡則留在**調整／簽核歷程表**。此與本裁定之分工一致。

---

## 5. 逐欄位裁定（CR-014 v1.0 選項 A 之設計 ↔ 既有表）

| v1.0 選項 A 欄位 | 既有表 | 裁定 | 說明 |
|---|---|---|---|
| `id` | ✅ | **沿用** | `RAW(16)` PK |
| `account_id` | ✅ | **沿用** | 歸戶 |
| `purpose_code` `VARCHAR2(20)` | `purpose` `VARCHAR2(24 CHAR)` ＋ `CHECK` | **沿用既有名與值域** | 值域 `TAX／INSURANCE／TUITION／MEDICAL／OTHER`——**不自由化**（自由化＝放寬既有約束，且無需求支撐） |
| `scope_ref` | ✗ | **❌ 不加** | POC-8 `8-1`～`8-7` 未要求 MCC 範圍檢核；**如實列為已知限制**（§7） |
| `amount` | ✅ | **沿用** | 分 |
| `currency` | ✗ | **★ 加（`V6`）** | 理由：`account_limits` 之鍵為 `(account_id, currency)`，**多幣別為既有設計**；無幣別之專用額度**無法歸屬**，且 upsert 鍵會跨幣別碰撞。契約本已有 `currency` 欄位 |
| `status` | ✅ `ACTIVE／EXPIRED／CANCELLED` | **沿用既有值域** | 對映：`EFFECTIVE`→`ACTIVE`、`REVOKED`→`CANCELLED`、到期→`EXPIRED`。**`PENDING_APPROVAL` 不入本表**——未生效者僅存於 `LIMIT_ADJUSTMENTS`（**本表只存現行定義**） |
| `effective_from` | ✅ `NOT NULL` | **沿用** | 未帶 `tempStart` 時取**今日**（本表 `NOT NULL`，不得為空） |
| `effective_to` | ✅ nullable | **沿用** | `NULL`＝無期限 |
| `applicant_id`／`approver_id` | ✗ | **❌ 不加** | 申請／覆核**軌跡之權威在 `LIMIT_ADJUSTMENTS`**（P1）。本表存現行定義，不複製簽核欄位 |
| — | `used_amount` | **沿用（既有）** | 供 `8-3`「不佔卡人額度」之用量追蹤（授權端扣抵屬後續增量，見 §6） |

**`V6__special_limits_currency_and_index.sql`（limit-service）**：

1. `ALTER TABLE special_limits ADD (currency CHAR(3 CHAR))` ＋ 由 `account_limits` 回填
   ＋ `MODIFY (currency NOT NULL)`——**不設預設值**，避免把「TWD」這個業務假設寫進 DDL；
   無法歸屬幣別之列將使遷移**失敗（fail-closed）**而非靜默填值。
   （本表於遷移時為 **0 列**，故實務上為空操作。）
2. `CREATE INDEX idx_special_limits_account ON special_limits (account_id, status)`
   ——查詢端點之存取路徑。
3. `CREATE UNIQUE INDEX uk_special_limits_active
   ON special_limits (account_id, currency, purpose, CASE WHEN status='ACTIVE' THEN 'ACTIVE' END)`
   ——**「一歸戶 × 一幣別 × 一用途，至多一筆生效中」**。
   Oracle B-tree 唯一索引**不索引全 `NULL` 鍵** → 非 `ACTIVE`（`EXPIRED`／`CANCELLED`）可留多筆歷史。
   此為 `8-1` 之核心不變式：若容許兩筆 `ACTIVE` 同用途，授權端將**重複計算**專用額度。

---

## 6. 實作範圍（裁定 A′ 後）

1. `V6__special_limits_currency_and_index.sql`（limit-service）：§5 之三項。
2. **領域層**：`SpecialLimit`（不可變）＋ `SpecialLimitRepository`（埠）。
3. **基礎設施層**：`JdbcSpecialLimitRepository`（`findActive`／`findActiveByAccount`／
   `upsertActive`／`cancelActive`／`expireActive`）。
4. **應用層** `LimitAdjustmentService`：
   - `apply()`：`SPECIAL` 之 `before` 取自**現行有效之同用途專用額度**（無則 0），
     而非主檔永久額度——否則覆核門檻（`§7.2`）會以錯誤基準計算。
   - `applyToMaster()`：`SPECIAL` → `upsertActive(...)`（**同交易**），
     不走 `applyAdjustedAmount`。
   - `revoke()`：`SPECIAL` → `cancelActive(...)`（對映 `REVOKED`→`CANCELLED`）。
   - `specialLimits()`：**改讀 `SPECIAL_LIMITS`**（現行定義）。
   - 新增 `expireSpecial(today)`：`ACTIVE` 且 `effective_to < 今日` → `EXPIRED`。
5. **排程**：`TemporaryLimitExpiryScheduler` 併同處理專用額度到期
   （否則 `status='ACTIVE'` 會**說謊**，且唯一索引會擋住同用途之新額度）。
6. **介面層**：`SPECIAL` 之 `purpose` 值域檢核（不合法 → `CC-LIM-1003`「專用額度不可用」，
   既有錯誤碼即為此設計：「未設定或已過期」）。
7. **契約**：`SpecialLimitItem` 改為**現行定義視角**（§7）。
8. **後續增量（本 CR 不含）**：`8-2`（角色／客層上限）、`8-6`／`8-7`（臨時額度串動）、
   授權端之專用額度扣抵與 `scope_ref`（MCC）。

---

## 7. 契約變更與向後相容（★ 破壞性）

`GET /api/v1/accounts/{accountId}/special-limits` 之回應 schema `SpecialLimitItem`
**由「調整視角」改為「現行定義視角」**：

| 欄位 | v1（原） | v1.1（新） |
|---|---|---|
| `adjustmentId` | ✅ | **移除**（改 `specialLimitId`） |
| `accountId` | ✅ | 保留 |
| `currency` | ✅ | 保留（本表已補欄） |
| `amountMinor` | ✅ | 保留 |
| `status` | `DRAFT／PENDING_APPROVAL／EFFECTIVE／REJECTED／EXPIRED／REVOKED` | **值域變更** → `ACTIVE／EXPIRED／CANCELLED`（本表 `CHECK`） |
| `tempStartDate`／`tempEndDate` | ✅ | **改名** → `effectiveFrom`／`effectiveTo` |
| `purpose` | `nullable` 自由字串 | **值域化** `TAX／INSURANCE／TUITION／MEDICAL／OTHER` |
| `applicantId` | ✅ | **移除**（軌跡在 `LIMIT_ADJUSTMENTS`） |
| `usedAmountMinor` | — | **新增** |

**向後相容性裁定**：依 `CR-030 P4`／`SAD-004 §5.3`，**移除欄位與變更值域屬破壞性變更**，
本應升版；惟經查證——

- **消費端為零**：全庫 `grep` 前端（`frontend/src`）**無任何 `special-limits` 引用**；
  `limit-service` 亦無其他服務呼叫此端點（`SAD-002` 之訂閱關係不含本端點）。
- **端點語意本來就是錯的**：`C-11` 上線時即以「查 `LIMIT_ADJUSTMENTS`」權宜實作
  （`CR-014 v1.0 §2` 自承），本次即為**修正該權宜**。

→ 故**不另立 `v2` 端點**，直接修正 schema，並於契約 description 標註語意變更。
（若日後有外部消費端，應改以新增 `v2` 路徑為之。）

### 7.1 併同修正之契約缺陷（★ 真機測試揭露）

`POST /api/v1/limit-adjustments` 為 POC-8 之 `8-1`（參數設定）入口，真機測試時發現
**契約與實作脫節**（`CR-019` 型，惟未被關卡攔下）：

| # | 症狀 | 證據 | 修正 |
|---|---|---|---|
| 1 | 契約宣告金額欄為 `amount`，**實作之 record 欄名為 `targetAmount`** | 依契約送 `amount` → **400 `CC-PLT-4002` 請求主體格式錯誤** | 實作欄名改為 `amount`（與契約一致） |
| 2 | 契約**漏列** `applicantId`，而實作為 `@NotBlank`（**必填**） | 送 `targetAmount` 後 → **400 `CC-PLT-4003`：`applicantId` 不得空白** | 契約補 `applicantId`（列為 `required`） |
| 3 | 契約**漏列** `currency`（實作既有，未帶時預設 `TWD`） | 讀 `AdjustmentRequest` 即得 | 契約補 `currency`（可選） |

**根因（工具關卡缺口，非個案）**：`tools/verify_contract_dto.py` 之檢查 A 係「**由 Controller 解析回應型別**」，
檢查 B 係「契約 schema ↔ `application/dto/<Name>.java`」——
**請求 DTO**（本服務之請求 record 巢狀於 Controller，且無 `application/dto` 目錄）**不在任一檢查範圍**。
故此類漂移**編譯不會錯、啟動不會錯、單測不會錯**，只有「真機照契約打一次」才會現形。
→ **建議**（列為後續）：擴充 `verify_contract_dto` 覆蓋請求 DTO，或至少對「契約有、Controller 無同名 record」發 WARN。
**本輪不改工具**（超出本 CR 範圍），如實揭露。

---

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

1. **`scope_ref`（MCC 等用途範圍）未實作**——`BRD-001 §5.4` 提及「特定 MCC」，
   惟 POC-8 之 `8-1`～`8-7` 未列驗收項；待需求確認後另提 CR。
2. **`8-2`（角色／客層上限）仍未實作**——目前覆核門檻仍為金額模擬（`SCS-002 §7.2`）。
3. **授權端尚未扣抵專用額度**——`8-3`／`8-4`／`8-5` 之**交易時檢核**屬 `authorization-service`
   之增量；本 CR 只備妥資料模型與 `used_amount` 欄位。
4. **`8-6`／`8-7`（綜所稅／繳款之臨時額度串動）未實作**。
5. **`LIMIT_ADJUSTMENTS` 與 `SPECIAL_LIMITS` 之間無外鍵**——
   對應關係為業務鍵 `(account_id, currency, purpose)`；
   「哪一筆調整產生了現行定義」須由兩表之時序推導，非精確可稽核。

---

## 9. 真機驗證結果（2026-10-02）

環境：真實 Oracle 23.26（`FREEPDB1`／schema `CC_LIM`）＋ Keycloak M2M（`cc-payment-service`，`azp` 通過 `limit-service` ACL）＋ limit-service 8083。
逐項結果見 **`SCS-002 §7.9.7`**（13 項，全數通過）。摘要：

| 面向 | 結論 |
|---|---|
| `V6` 遷移 | ✅ Flyway 套用成功；`currency` 欄與兩索引真機複查存在 |
| 寫入路徑 | ✅ 未超限 → `EFFECTIVE` ＋ `special_limits` 新增／更新；超門檻 → `PENDING_APPROVAL` **且不落庫**；覆核通過 → 落庫 |
| **基準額度** | ✅ `beforeAmount` 取自**專用額度池**（`0 → 200000 → 300000` 之軌跡證明），**非**主檔之 10,000,000 |
| **upsert** | ✅ 同業務鍵再次申請 → `special_limits` **仍為 1 列**（更新，非新增） |
| 撤銷 | ✅ `REVOKED` ＋ `special_limits` 該列轉 `CANCELLED`；**未觸碰額度主檔** |
| 負樣本 | ✅ `purpose` 不合法／未帶 → **422 `CC-LIM-1003`**，不落庫 |
| **權威來源（非快取）** | ✅ **繞過服務**直接改 DB → 查詢立即反映（777777） |
| 到期 | ✅ 讀側**日期條件**先行把關（排程前即 `count=0`）；排程約 40 秒轉 `EXPIRED` |
| **不佔卡人額度**（8-3） | ✅ 全程 `account_limits` 未變 |
| 單元測試 | ✅ 15 例（0 失敗）；`LayeringTest` 通過 |
| 掃描器 | ✅ `scan_scaffold`／`scan_mockdata`／`scan_faketest` 各 0 命中；`validate_sql` 0 違規；`verify_contract_dto`（limit-service）0 不符 |

---

## 10. 修訂記錄

| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 1.1 | 2026-10-02 | ★ **裁定採選項 A′（接線既有 `SPECIAL_LIMITS`）** 並逕行實作。**重大更正**：`SPECIAL_LIMITS` **早已存在**（`V1__baseline.sql` ← `SDD-001 §3.4`；`CR-001 §2.3` 亦已載明；真機實測 11 欄、0 列）——v1.0 之「新表」與 §2 現況之漏列**皆屬誤述**，根因＝「實測」只掃 Java 未掃 DDL（同 `CR-033` 之幽靈型）。**新增**：§5 逐欄位裁定（沿用既有值域、僅補 `currency`、加唯一索引 `uk_special_limits_active`）、§7 契約破壞性變更之向後相容性裁定（消費端為零，不另立 `v2`）、**§7.1 併同修正之契約缺陷**（`targetAmount`↔`amount` 不符、漏列 `applicantId`／`currency`；併揭露 `verify_contract_dto` **不涵蓋請求 DTO** 之關卡缺口）、§8 五項已知限制、**§9 真機驗證結果**（13 項全通過）。**實作範圍**：`V6` 遷移 ＋ 領域／埠／JDBC ＋ `LimitAdjustmentService` 之 SPECIAL 分支與查詢改讀 ＋ 到期排程 ＋ `purpose` 值域檢核 ＋ 15 支單元測試 |
| 1.0 | 2026-09-22 | 設計提案：語意查證（`BRD-001 §5.4`／POC-8）、三選項對照（A 新表／B 主檔欄位／C 調整表擴充）、建議 A 並附表結構與實作範圍；**待 BEAM PMO 裁定** |
