# SCS-017　報表服務（report-service）程式設計規格

| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-017 |
| 版本 | **1.6**（`GOV-007 §4`：**§6.2 之下載「作業未就緒」錯誤碼定案**——由暫用之 `CC-PLT-3001`（非法狀態轉換）改為**另立之 `CC-PLT-3003`（409 資源尚未就緒）**，語意區隔；錯誤碼 123 → 124。前版 **1.5**（`HANDOVER #A-109`：新增 §6.2 **報表檔案下載端點（代理端點）**——BEAM PMO 裁定採方案 B；`§8.1.4` 定案） |
| 狀態 | **已核定（規格先行）** |
| 對應需求 | `SYS-1`（Report Generation Tool）、`ACQ-4.5`（商店報送與報表）、`ISS-2.11`（信用卡報送與報表）、`DEB-3.4`（DEBIT 卡報送與報表） |
| 服務埠號 | 待骨架產生時依 `CSP-006 §4` 分配（自 8081 起編） |
| 套件根 | `com.beam.cardcore.rpt` |
| 權威 schema | `CC_RPT`（**尚未建表**；報表以 `rpt` Postgres 讀模型為主） |
| 關聯文件 | `SDD-006 §7`、`BRD-002`、`GOV-005` |

---

## 1. 對應需求

| FR 編號 | 需求名稱 | 來源 |
|---|---|---|
| `SYS-1` | Report Generation Tool 能力 | `BRD-002` |
| `ACQ-4.5` | 商店報送與報表（收單） | `BRD-002` |
| `ISS-2.11` | 信用卡報送與報表 | `BRD-002` |
| `DEB-3.4` | DEBIT 卡報送與報表 | `BRD-002` |

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

| 圖種 | 文件 | 數量（實測） | 本服務相關 |
|---|---|---|---|
| **ER-Diagram** | `SDD-001_領域模型與ER-Diagram` | 16 個 `erDiagram` | ⚠️ 本服務 schema 尚未建表（`SDD-001 §3` 僅涵蓋 14 個 bounded context；報表主體位於 `rpt` Postgres 讀模型） |
| **DFD** | `SDD-002_DFD與流程分解` | 10 個 flowchart | 報表產生流 |
| **Sequence** | `SDD-003_Sequence-Diagrams` | 15 個 `sequenceDiagram` | 報表作業時序 |
| **狀態機** | `SDD-005_狀態機設計` | 10 個 `stateDiagram` | ⚠️ 報表作業狀態待確認 |
| **API Contract** | `SDD-006` + `contracts/openapi/report-service.yaml` | 17 份 | 本服務 2 支端點 |

---

## 2. 服務職責與邊界

**承作**：
- 報表定義（`ReportDefinition`：欄位、篩選、排程、格式）
- 報表作業（`ReportJob`：非同步產生、狀態、輸出）
- 報送（監管／商店報送，含 `ACQ-4.5`／`ISS-2.11`／`DEB-3.4`）

**不做（屬他服務）**：
- 交易處理 → 各領域服務
- 即時查詢 → 各領域服務 API

---

## 3. 領域模型

- 聚合（依 `services.yaml`）：`ReportDefinition`（報表定義）、`ReportJob`（報表作業）
- 值物件：`ReportCode`、`ReportStatus`、`OutputFormat`（PDF/CSV/EXCEL）
- 狀態（設計意圖）：`ReportJob.status ∈ {QUEUED, RUNNING, COMPLETED, FAILED}`（待 `SDD-005` 正式化）

---

## 4. 程式結構

| 套件／類別 | 職責 | 對應需求 |
|---|---|---|
| `domain/model/ReportDefinition` `ReportJob` | 聚合與值物件 | `SYS-1`/`ACQ-4.5`/`ISS-2.11`/`DEB-3.4` |
| `domain/port/ReportRepository` | 持久化埠（待建表） | — |
| `application/ReportService` | 定義／作業／報送 | `SYS-1` |
| `infrastructure/jdbc/*RepositoryImpl` | JDBC 實作（待建表） | — |
| `interfaces/rest/ReportController` | REST 端點 | `SYS-1` |

---

## 5. 資料模型

> **`CR-017` 裁定 D（BEAM PMO 2026-09-22）：C-7 範圍含 Postgres 讀模型**。本節為**設計權威**，
> 實體交付見 §5.4（`V2` 遷移 ＋ `db/migration-postgres/V1`）。

### 5.1 架構：作業指標（Oracle）＋ 讀模型（Postgres）

| 存放 | 內容 | 理由 |
|---|---|---|
| **Oracle `CC_RPT`** | `REPORT_DEFINITIONS`（報表定義）／`REPORT_JOBS`（作業指標：狀態、筆數、輸出參照） | 作業狀態為**權威事務資料**（契約直接對應）；與 `CSP-006 §2.4`「Oracle 為權威 OLTP」一致 |
| **Postgres `rpt`** | 讀模型表（**報表主體**） | CQRS 讀側（`SCS-017 §7`）：報表查詢不佔用權威 OLTP |

### 5.2 讀模型（Postgres `rpt`，實測 schema 已存在、**0 表**）

| 表 | 欄位 | 說明 |
|---|---|---|
| `rpt.daily_auth_summary` | `business_date` DATE／`channel`／`tx_type`／`decision`／`tx_count` BIGINT／`total_amount` NUMERIC(19,0)／`currency` CHAR(3)／`refreshed_at` TIMESTAMPTZ | **每日授權彙總**（`DAILY_AUTH_SUMMARY`）；主鍵＝（日 × 通路 × 交易型別 × 決策），**冪等 upsert** |

> **⚠️ CQRS 讀側首例**：實測 `evt`／`proj`／`rpt` 三 schema 由 `ops/local/init/postgres/01-init.sql`
> 建立，惟**迄今無任何服務使用**（`grep jdbc:postgresql` 全庫 0 命中）——本服務為**首個**接線者，
> 其 DataSource／Flyway 配置方式即為後續讀側之**慣例**（見 §5.4）。

### 5.3 投影（Oracle → Postgres）

`SCS-017 §7` 原載「讀模型與寫側最終一致」，惟**同步機制未實作**（`GOV-006` 4.8 `W-2` Kafka 消費端 ⬜）。
本期採 **批次投影**（排程自 Oracle 權威彙總後 upsert 讀模型）為 **stand-in**——
不阻塞、可實測、可重跑，且與 `#A-79` 之排程慣例一致；待 Kafka 消費端落地後可改為事件驅動。

### 5.4 交付

| 項 | 位置 |
|---|---|
| Oracle 作業指標 | `V2__report_definitions_and_jobs.sql`（＋`report_job_no_seq`＋內建定義 `DAILY_AUTH_SUMMARY`） |
| Postgres 讀模型 | `db/migration-postgres/V1__rpt_read_model.sql`（**獨立 Flyway 實例**，`schemas("rpt")`） |
| 輸出 | `${cardcore.report.output-dir}`（預設 `ops/local/logs/reports`）；`downloadUrl` 為 `file://` |
| ⚠️ 誠實揭露 | **物件儲存「基礎設施已就緒、程式尚未接線」**（2026-10-01 更新，`HANDOVER #A-106`）：本地物件儲存已建置並實測（**Garage `v2.4.1`**，S3 API 埠 3900、bucket `cardcore-artifacts`，見 `SAD-006 §3.2`）——惟**本服務尚未接線**，`downloadUrl` **仍為 `file://`**。⚠️ **併更正**：原載「實測 compose **無 MinIO**」**仍成立**，但**結論已變**——不是「無替代方案」，而是 **MinIO 開源版已封存（官方回 410）、改用 Garage**。`XLSX`／`PDF` 需額外函式庫，**未支援**（請求即回 `FAILED` 並載明原因，非靜默） |

---

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

| 端點 | 方法 | 說明 | 對應 FR |
|---|---|---|---|
| `/api/v1/report-jobs` | POST | 報表作業提交 | `SYS-1`/`ACQ-4.5`/`ISS-2.11`/`DEB-3.4` |
| `/api/v1/report-jobs/{jobId}` | GET | 報表作業查詢 | `SYS-1` |
| `/api/v1/report-jobs` | GET | **報表作業清單**（最近 N 筆；2026-09-30 補建，見 §6.1） | `SYS-1`/`ACQ-4.5`/`ISS-2.11`/`DEB-3.4` |
| `/api/v1/report-jobs/{jobId}/download` | GET | **報表檔案下載**（**代理端點**；2026-10-01 補建，見 §6.2） | `SYS-1`/`ISS-2.11` |

> 錯誤碼：`404` 找不到作業／定義、`400` 參數錯誤、`409` 重複作業。

### 6.2 報表檔案下載端點（**代理端點**；2026-10-01 補建，`HANDOVER #A-109`）

> **緣由**：`W-5` 階段二接線後，`DOWNLOAD_URL` 存的是**物件鍵**（`§8.1.4`），
> 但**如何下載**尚未實作。**BEAM PMO 2026-10-01 裁定採方案 B（代理端點）**——
> 理由：本案為**銀行核心系統**，報表可能含敏感資料；**presigned URL 會把授權交給物件儲存**
> （URL 一旦外流，有效期內人人可載），與全案「**一律經服務認證**」之架構（`SAD-005 §3`）不一致。

**`GET /api/v1/report-jobs/{jobId}/download`**（回應**檔案位元組**，`200`）

| 項目 | 規格 |
|---|---|
| 回應 | **非** `ApiResponse` 封套——直接回**檔案內容**（`application/octet-stream`） |
| `Content-Disposition` | `attachment; filename="<jobNo>.<ext>"`（供瀏覽器另存） |
| `Content-Type` | 依格式（`CSV` → `text/csv; charset=utf-8`；`JSON` → `application/json; charset=utf-8`） |
| 認證 | **受本服務認證與授權保護**（與其他端點一致）——**此即方案 B 之核心價值** |

**行為要求**：

1. **唯讀**——僅取回，不觸發產生。
2. ⚠️ **作業須為 `COMPLETED`**：`QUEUED`／`RUNNING` 無檔案可下載 → **`409` `CC-PLT-3003`（資源尚未就緒）**（**不得**回空檔假裝成功）。
3. ⚠️ **`FAILED` 亦無檔案** → 同樣 `409` `CC-PLT-3003`（並於訊息載明作業失敗原因）。
4. **作業不存在** → `404` `CC-PLT-4040`。
5. ⚠️ **物件不存在（鍵在但物件儲存查無）** → `404`——**如實回報，不回空檔**：
   這是「**DB 說有、儲存說沒有**」的不一致，須讓人看見（比照 `CSP-001 §8 B2` 不靜默）。
6. **不快取**。

> ✅ **`409` 錯誤碼已定案（BEAM PMO 2026-10-01 裁定；`GOV-007 §4`）**：
> 原暫用 `CC-PLT-3001`（**非法狀態轉換**）——因 `SDD-006 §3.4` 之既有 4 個 `409` 平台碼
> （`CC-PLT-2001`／`2002`／`3001`／`3002`）**皆為「狀態機轉換」語意**。
> 惟本情境「**資源尚未就緒**」（`QUEUED`／`RUNNING`／`FAILED`）與「狀態機非法轉換」**語意不同** →
> 已於 `SDD-006 §3.4` **另立 `CC-PLT-3003`（409 資源尚未就緒）** 並回填登錄表
> （錯誤碼 **123 → 124**）；本端點改用之。

> ⚠️ **`DOWNLOAD_URL` 仍存物件鍵**（不變）——下載**由此端點動態取回**，
> 故**資料庫欄位不必隨 URL 效期變動**，且**換物件儲存後端不影響下載路徑**。

> ⚠️ **未竟**：**(i)** **無範圍請求（Range）支援**——大檔續傳俟需求確認；
> **(ii)** **無下載稽核留痕**（誰下載了什麼）——報表可能含敏感資料，**建議另立 CR 補稽核**。

### 6.1 報表作業清單端點（2026-09-30 補建，`HANDOVER #A-104`）

> **緣由**：`SDD-007 §5.4.26` 之桶 C「**批次作業監控**」（`/system/batch`）——
> `bff-web` 依 `SCS-020 §9.9` 聚合四服務之批次執行紀錄，**需各服務提供「列出批次」端點**。
> ⚠️ **實測揭露**：`/api/v1/report-jobs` 原**僅有 `POST`**（送交）——`GET` 實測回 **404**。

**`GET /api/v1/report-jobs?limit=N`**（回應 `ReportJobSummary[]`，`200`）

| 參數 | 型別 | 說明 |
|---|---|---|
| `limit` | integer（可選） | 回傳筆數上限。預設 **20**，上限 **100** |

**回應欄位（`ReportJobSummary`）**：

| 欄位 | 說明 |
|---|---|
| `jobId` | 作業識別碼（UUID） |
| `jobNo` | 作業編號（`RPTJ` ＋ 年月 ＋ 連號，**可稽核**） |
| `reportCode` | 報表代碼 |
| `format` | 輸出格式（`CSV`／`JSON`／…） |
| `status` | `QUEUED`／`RUNNING`／`COMPLETED`／`FAILED` |
| `rowCount` | 產出列數（未完成時為 `0`） |
| `downloadUrl` | 輸出參照（本期為 `file://<絕對路徑>`，見 §10） |
| `errorDetail` | 失敗原因（非 `FAILED` 時為 `null`） |
| `startedAt`／`finishedAt` | 開始／完成時刻（未開始／未完成者為 `null`） |

> ⚠️ **為何不沿用既有 `ReportJobView`**：該 DTO 為**受理／單筆查詢回應**之形狀，
> **未含 `jobNo`／`reportCode`／`format`**——清單需可辨識「這是哪一支報表」與時間錨點
> （`SCS-020 §9.9.4 #3`）。故另立清單專用 DTO，**不動既有已驗證之契約**。

**行為要求**：

1. **唯讀**——僅查詢，**不觸發**產生（觸發為 `POST /report-jobs`）。
2. **依 `startedAt` 遞減**（最近送交者在前；`startedAt` 為 `null` 之 `QUEUED` 置後）。
3. **無資料回 `200` ＋ 空陣列**——**非** `404`。
4. **不快取**（`SCS-020 §9.9.4 #4`）。
5. ⚠️ **與 `POST /report-jobs` 同路徑、不同方法**——須明確 `@GetMapping`／`@PostMapping`。

> 錯誤碼：`400` 參數錯誤（`limit` 超出 `1..100`）。**無 `404` 情境**。

**未竟**：

- **不支援分頁游標**（僅取最近 N 筆）；**不含 `parameters` 原文**（清單僅需辨識與狀態；
  參數原文仍由單筆查詢取得）。

---

## 7. 關鍵邏輯

- **非同步產生**：`ReportJob` 入隊，`common-batch` 批次產出，避免阻塞。
- **讀模型**：報表資料源自 `rpt` Postgres（CQRS 讀側），與寫側（`CC_*`）最終一致。
- **報送**：監管／商店報送依定義排程，輸出於物件儲存。

**`CR-017`（BEAM PMO 2026-09-22 裁定 D）後定案之實作要點**

1. **非同步（實作方式）**：受理僅**入隊**（`QUEUED`）即回 **202**；實際產生由排程取件
   （`ReportJobScheduler`）→ `RUNNING` → `COMPLETED`／`FAILED`——**狀態由流程推進**。
2. **讀模型投影（stand-in）**：`ReportProjectionScheduler` 以**追趕視窗**「N 日前 ~ 昨日」
   自 Oracle 權威彙總後 upsert 至 `rpt.daily_auth_summary`（**冪等、可回補**）。
   ⚠️ 正規機制為**事件驅動**（`W-2` Kafka 消費端 ⬜）；本地以跨 schema 授權暫代（見 §10）。
3. **產生器**：`DAILY_AUTH_SUMMARY` → 讀讀模型 → 輸出 **CSV／JSON**（金額為**分**）；
   `XLSX`／`PDF` **未支援** → 作業 **`FAILED` 並載明原因**（不產空檔、不假裝成功）。
4. **冪等**：受理以冪等鍵**派生主鍵**（`UUID v3`，前綴 `rpt:job:`）——同鍵重送回同一作業。
5. **失敗可稽核**：`FAILED` 必載 `errorDetail`（領域層強制，未載即拒）。

---

## 8. 外部依賴與降級

| 依賴 | 失敗處理（Stand-in） |
|---|---|
| `rpt` Postgres 讀模型、`common-batch` | 報表為非同步；來源延遲僅影響報表即時性，不影響交易 |

### 8.1 物件儲存（輸出媒體）——2026-10-01 補建（`HANDOVER #A-107`）

> **緣由**：`W-5` 之本地替代方案已建置並實測（**Garage `v2.4.1`**，`SAD-006 §3.2`），
> 本節為**程式接線**之規格——**規格先行**，先立本節再動程式。

#### 8.1.1 契約：**S3 API**（不綁特定實作）

本服務**只依賴 S3 API**（endpoint ＋ region ＋ 靜態憑證），**不依賴任何 Garage 專屬特性**。
故本地後端（Garage）與正式環境（客戶之企業物件儲存）**可互換**，程式不動。

| 組態 | 值（本地） |
|---|---|
| endpoint | `http://localhost:3900` |
| region | **`cardcore`**（由 `config/garage.toml` 之 `s3_region` 決定，**非 AWS 固定值**） |
| bucket | `cardcore-artifacts` |
| 憑證 | `GARAGE_ACCESS_KEY`／`GARAGE_SECRET_KEY`（**環境變數，不寫入組態檔**） |
| 物件鍵前綴 | `reports/<jobNo>.<ext>`（**以 `jobNo` 為鍵**——可稽核、可追溯至作業） |

#### 8.1.2 分層：埠（`application`）＋ 實作（`infrastructure`）

| 角色 | 類別 | 說明 |
|---|---|---|
| 埠 | `application/ObjectStoragePort` | **本輪只有 `put(key, bytes, contentType)`** |
| 實作 | `infrastructure/storage/S3ObjectStorageAdapter`（＋`S3StorageConfig`） | AWS SDK v2 for Java；**沿用 `AccountOverviewPort` 之慣例** |

> ⚠️ **`get`／`delete` 本輪「不」納入埠**（規格與實作一致）：本輪範圍為「上傳 ＋ 存鍵」，
> 取回／刪除俟「下載呈現方式」（§8.1.4）定案後再補——
> 依「**不預先加未使用之介面方法**」之原則，避免死碼。

> ⚠️ **`S3StorageConfig` 之 fail-closed**：**憑證刻意不給預設值**——缺任一項即**啟動期失敗**
> （`IllegalStateException`），**不靜默略過、不退回本地檔案**（§8.1.5）。
> `endpoint`／`region`／`bucket` 則給本地預設值（比照 `POSTGRES_URL` 之慣例）。
> ⚠️ **`path-style` 預設 `true`**——本地 `localhost` **必須**用路徑式尋址
> （虛擬主機式會把 bucket 名當前綴，`cardcore-artifacts.localhost:3900` **無法解析**）；
> 正式環境若需虛擬主機式，**改組態即可，程式不動**。

> ⚠️ **為何要埠**：與 `KeyProvider`／`ServiceClientHttpClient` 同一思路——
> 換後端（Garage → 客戶企業儲存）、換 SDK、或改為測試替身時，**應用層不動**；
> 且應用層**得以手寫 fake 測試**（無須起 S3）。

#### 8.1.3 輸出流程（`RUNNING` → `COMPLETED`）

```
① 產生內容（CSV／JSON，記憶體）
② put(key, bytes, contentType)  → 上傳物件儲存
③ complete(rowCount, key, now)  → 將「物件鍵」寫入 REPORT_JOBS.DOWNLOAD_URL
```

> ⚠️ **順序不可顛倒**：**先上傳成功、才標 `COMPLETED`**。
> 若先標完成再上傳，上傳失敗將留下「**已完成但沒有檔案**」的作業——
> 那是**假成功**，比失敗更難查（比照 `#C-42`「零產出必須可區分」之教訓）。

#### 8.1.4 ⚠️ `downloadUrl` 之語意（**關鍵決策——本節之核心**）

`REPORT_JOBS.DOWNLOAD_URL` 是**寫入資料庫的欄位**，故：

> ⚠️ **不得存入 presigned URL**——presigned URL **有時效**，
> 存進 DB 後**一旦過期即失效**，而作業紀錄**不會再更新**（`COMPLETED` 為終態）。
> 這會產生「**資料庫裡有一條永遠打不開的連結**」的缺陷。

**故：`DOWNLOAD_URL` 存「物件鍵」**（如 `reports/RPTJ20261001000001.csv`），
**不存 URL**——物件鍵**永久有效**，且下載呈現方式（見下）**可日後更換而不動資料**。

**下載之呈現方式（✅ 2026-10-01 BEAM PMO 裁定採 B）**：

| 方案 | 做法 | 優點 | 缺點 |
|---|---|---|---|
| A. 動態 presigned URL | 查詢時由 `ObjectStoragePort` 產生短效 URL | 改動最小 | **URL 會過期**（前端顯示的字串有時效）；**授權交給物件儲存**——URL 一旦外流，有效期內人人可下載 |
| **B. 代理下載端點（✅ 採用）** | 新增 `GET /api/v1/report-jobs/{jobId}/download`，由服務向物件儲存取回後回傳 | **受本服務認證與授權保護**（與其他端點一致）；URL **永久有效**；不洩漏物件儲存憑證 | 需新增端點（契約＋前端＋測試）；檔案流量經服務 |

> **裁定理由**：本案為**銀行核心系統**，報表可能含敏感資料；把授權交給物件儲存的 presigned URL，
> **與全案「一律經 gateway 認證」之架構不一致**（`SAD-005 §3`）→ 採 **B**。
> **端點規格見 `§6.2`**。

#### 8.1.5 降級政策

| 情境 | 行為 |
|---|---|
| 物件儲存不可用（連線失敗／逾時） | 該作業標 **`FAILED`** 並載明原因——**不得**退回寫本地檔案（否則「已上傳」與「寫本機」兩種狀態混雜，日後無法辨識） |
| 未組態 `cardcore.storage.*`（endpoint 空白） | **啟動期即失敗**（fail-closed），不靜默略過——比照 `CR-010` 之出站憑證政策 |

> ⚠️ **不再有 `file://` fallback**：接線後**移除**本地檔案路徑，避免「兩種輸出媒體並存」。
> 此為**刻意的破壞性變更**——`SCS-017 §10.1` 之 stand-in 記載同時廢止。

#### 8.1.6 依賴（**已查證 ＋ 已 spike 驗證**）

| 項目 | 值 |
|---|---|
| SDK | **`software.amazon.awssdk:s3:2.55.9`**（Maven Central 實查 `:s3` 與 `:bom` **皆存在**；版本鎖於 `gradle/libs.versions.toml`） |
| 傳遞相依 | `:s3` 之 sync 客戶端**預設即 `url-connection-client`**（JDK 內建）→ **不需** Apache HttpClient／Netty，與 Spring 之 HTTP 疊層**無衝突** |
| 驗證 | ✅ **編譯 ＋ 執行期皆通過**（`version-compat-verification` skill 步驟 4／5）——見下 |

#### 8.1.7 ⚠️ ★ S3 相容性之兩個地雷（**實測踩到並修除，必讀**）

> 這兩項**編譯期不會報錯、對 AWS S3 也無感**，但在 S3 相容實作（Garage／MinIO 等）
> **一律失敗**——是典型的「靜默相容性地雷」。

**地雷 ①：SDK 2.30 起預設「盡量加校驗」＋ `aws-chunked` 串流簽章**

| 項目 | 內容 |
|---|---|
| 症狀 | 上傳回 **400**：`Bad request: Invalid payload signature (Service: S3, Status Code: 400)` |
| 根因 | SDK 2.30 起 `requestChecksumCalculation` 預設為 `WHEN_SUPPORTED`，會附加 `x-amz-checksum-*` trailer；搭配 `aws-chunked` 串流簽章，**S3 相容實作不支援** |
| 修法 | `S3Configuration.chunkedEncodingEnabled(false)` ＋ client 層 `requestChecksumCalculation(WHEN_REQUIRED)`／`responseChecksumValidation(WHEN_REQUIRED)`——即 **SDK 2.30 之前的預設值**，對 AWS S3 **亦完全有效** |

**地雷 ②：校驗設定「只能在一處」**

| 項目 | 內容 |
|---|---|
| 症狀 | **啟動期失敗**：`IllegalStateException: Checksum behavior has been configured on both S3Configuration and the client/global level. Please limit checksum behavior configuration to one location.` |
| 根因 | 於 `S3Configuration` 亦設 `checksumValidationEnabled`，與 client 層之校驗設定**衝突**（SDK 明文禁止） |
| 修法 | 校驗**只在 client 層**設；`S3Configuration` 只留**尋址方式**（`pathStyleAccessEnabled`）與 `chunkedEncodingEnabled` |

#### 8.1.8 ★ 真機驗證（2026-10-01，`HANDOVER #A-107`）

| 驗證項 | 結果 |
|---|---|
| 服務啟動 | ✅ `S3StorageConfig` 記錄「物件儲存（S3 API）已建立：endpoint=http://localhost:3900 region=cardcore pathStyle=true」 |
| 送出作業（`POST /report-jobs`，CSV） | ✅ **202**、`QUEUED` |
| 排程取件 | ✅ **`COMPLETED`**、`rowCount=7` |
| **`DOWNLOAD_URL` 之值** | ✅ **`reports/RPTJ202610000006.csv`**（**物件鍵**，**非** `file://`） |
| **物件確實進 Garage** | ✅ 以 S3 客戶端取回：`ContentType=text/csv; charset=utf-8`、**385 bytes**、內容為真實讀模型資料（表頭 ＋ 7 列） |
| **負樣本：本機不留檔** | ✅ 近 10 分鐘內**無**新增本地報表檔——證實**未**退回 `file://`（`§8.1.5`） |
| 失敗路徑（修正前） | ✅ 亦已驗證：上傳失敗時作業標 **`FAILED`** 並**載明原因**（`Invalid payload signature`），**無假成功、無本地 fallback** |

---

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

- 報表定義／作業／查詢（`SYS-1`）
- 商店／信用卡／DEBIT 報送（`ACQ-4.5`/`ISS-2.11`/`DEB-3.4`）、格式與排程

---

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

- ~~**`CC_RPT` 尚未建表**~~ → **已建**（`V2__report_definitions_and_jobs.sql`；`SDD-001 §3.16` 已補 DDL 章節）。
- 報表作業狀態機待 `SDD-005` 正式化（本期狀態轉換由領域層守衛，未建模為正式狀態機）。
- ~~`report-service` 骨架與埠號尚未產生（規格先行）~~ → **已產生，埠號 8098，已啟動實測**。

**`CR-017` 執行後新增之限制（2026-09-22）**

1. **物件儲存已接線，惟「下載呈現」尚未提供**（2026-10-01，`HANDOVER #A-107`）：
   輸出已改走**物件儲存（S3 API）**（本地 Garage `v2.4.1`，見 `SAD-006 §3.2`），
   `DOWNLOAD_URL` 存**物件鍵**（**非** URL，理由見 `§8.1.4`）；
   ⚠️ **已移除本地檔案（`file://`）fallback**（`§8.1.5`）。
   **仍待**：**下載呈現方式**（方案 A 動態 presigned／方案 B 代理端點，`§8.1.4`）**待 BEAM PMO 裁定**——
   故目前前端**只顯示物件鍵文字，尚無法直接下載**。
   正式環境為**客戶之企業物件儲存**（契約為 S3 API，換後端**只需改組態、程式不動**）。
2. **投影之來源以跨 schema 讀取暫代**（`GRANT SELECT ON CC_AUTH.AUTHORIZATIONS TO CC_RPT`，
   `ops/local/init/oracle/12-dev-report-projection-grant.sql`）——**本地 stand-in**：
   正規為事件驅動（`W-2` ⬜）；經 API 亦不可行（`authorization-service` 之 `allowed-callers`
   依 `SAD-002 §5` 為空集合）。**正式環境不得如此**（破壞服務邊界）。
3. **僅一支報表**：`DAILY_AUTH_SUMMARY`；其餘報表（監管／商店報送）未實作。
4. **僅兩種格式**：`CSV`／`JSON`；`XLSX`／`PDF` 未支援（需額外函式庫）。
5. **無排程報送**：`REPORT_DEFINITIONS` 之排程欄位（Cron）未納入本期（契約無排程端點）。
6. **無預設區間策略**：參數未提供時取讀模型**全部**資料（避免產出看似正確卻空無一列之報表）。
7. **`REPORT_DEFINITIONS` 無管理端點**：定義由遷移內建；契約未定義 CRUD。
8. **`common-batch` 未使用**：`SCS-017 §7` 原載「`common-batch` 批次產出」，
   本期以**服務內排程**實作（`common-batch` 尚未落地）。

---

## 11. 修訂記錄

| 版本 | 日期 | 修訂者 | 說明 |
|---|---|---|---|
| **1.6** | 2026-10-01 | WorkBuddy AI | **§6.2 之下載「作業未就緒」錯誤碼定案（`GOV-007 §4`；BEAM PMO 裁定）**——由暫用之 `CC-PLT-3001`（非法狀態轉換）改為**另立之 `CC-PLT-3003`（409 資源尚未就緒）**。**緣由**：「資源尚未就緒」（`QUEUED`／`RUNNING`／`FAILED`）與「狀態機非法轉換」**語意不同**；已於 `SDD-006 §3.4` 補登 `CC-PLT-3003`（錯誤碼 **123 → 124**）並重生 `ErrorCodes.java`（`--force`）；`ReportJobService.download` 與 `ReportJobServiceTest` 同步。★ **解除 `v1.5` 所載之「語意落差」** |
| **1.5** | 2026-10-01 | WorkBuddy AI | **新增 §6.2「報表檔案下載端點（代理端點）」（`HANDOVER #A-109`）**——**BEAM PMO 裁定採方案 B**（代理端點），`§8.1.4` 由「待裁定」改為**定案**。**規格先行**：立 §6.2 → 改 `gen_openapi.py`（新增 **`BINARY_RESP` 哨兵**以產生**非封套**之二進位回應）→ **契約 86 端點、0 錯誤** → 實作（`ObjectStoragePort.get` ＋ `S3ObjectStorageAdapter.get` ＋ `ReportJobService.download` ＋ controller）→ 前端 `downloadReportFile` ＋ 下載鈕 → **真機驗證**。★ **設計要點**：**(i)** 回應為**檔案位元組**（`application/octet-stream` ＋ `Content-Disposition`），**全案唯一非封套回應**；**(ii)** 追蹤碼改以 **`X-Trace-Id` 回應標頭**回傳（回應體已非封套，否則此路徑追蹤斷掉）；**(iii)** **三種「沒有檔案」分開講**：作業不存在→`404`、作業未完成／失敗→**`409`**、**物件查無→`404` 如實回報不一致**（**不回空檔**）。★ **併載錯誤碼語意落差之誠實揭露**：`409` 之既有平台碼皆為「狀態機轉換」語意，本情境以最接近之 **`CC-PLT-3001`** 表達，**不為單一端點擴充登錄表**；若此類情境增多**建議另立 `CC-PLT-3003`**。**真機驗證**：QUEUED→**409 `CC-PLT-3001`**；完成後→**200**、`Content-Disposition` 正確、**下載內容與物件儲存內之原始檔逐位元組一致（SHA-256 相同）**；作業不存在→404、非 UUID→400 |
| **1.4** | 2026-10-01 | WorkBuddy AI | **新增 §8.1「物件儲存（輸出媒體）」——程式接線（`HANDOVER #A-107`）**。**規格先行**：立 §8.1 → 加依賴（`awssdk:s3:2.55.9`，**Maven Central 實查**）→ 實作埠與 S3 實作 → 真機驗證。**設計要點**：**埠**（`application/ObjectStoragePort`）＋ **實作**（`infrastructure/.../S3ObjectStorageAdapter`）——應用層只認物件鍵，換後端**程式不動**；**只依賴 S3 API**。★ **`downloadUrl` 語意定案**：`DOWNLOAD_URL` 存**物件鍵**（**非 URL**）——**presigned URL 會過期**，存進 DB 即成死連結（`§8.1.4`）。⚠️ **移除 `file://` fallback**（避免兩種輸出媒體並存，`§8.1.5`）；憑證 **fail-closed**（缺則啟動期失敗）。★ **併載 S3 相容性兩個地雷（實測踩到並修除）**：**①** SDK 2.30 起預設校驗＋`aws-chunked` → S3 相容實作回 **400 `Invalid payload signature`**（修：關閉 chunked ＋ 校驗改 `WHEN_REQUIRED`）；**②** 校驗**只能在一處**設，兩處並設會**啟動期失敗**。**真機驗證**：作業 `COMPLETED`、`DOWNLOAD_URL=reports/RPTJ202610000006.csv`、**物件確實可自 Garage 取回**（385 bytes）、**本機未留檔** |
| **1.3** | 2026-10-01 | WorkBuddy AI | **物件儲存狀態更正（`HANDOVER #A-106`）**：§5.4／§10.1 原載「**物件儲存未部署**」→ 更正為「**基礎設施已就緒、程式尚未接線**」。本地物件儲存已建置並實測（**Garage `v2.4.1`**，S3 API 埠 3900、region `cardcore`、bucket `cardcore-artifacts`，見 `SAD-006 §3.2`）；**惟本服務尚未接線**，`downloadUrl` 仍為 `file://`。★ **併載選型更正**：原「無 MinIO」之觀察**仍成立**，但**結論已變**——MinIO 開源版**已封存不再維護**（官方下載站 410 Gone），故改採 Garage。接線所需 Java SDK 已查證 **`awssdk:s3:2.55.9`**（**未做 spike**，不得視為已驗證） |
| **1.2** | 2026-09-30 | WorkBuddy AI | **新增 §6.1「報表作業清單端點」**（`GET /api/v1/report-jobs?limit=N`）——解鎖 `SDD-007 §5.4.26` 桶 C「批次作業監控」（`SCS-020 §9.9`）。⚠️ **併載實測揭露**：該路徑原**僅有 `POST`**、`GET` 回 **404**。**新增 `ReportJobSummary`**（含 `jobNo`／`reportCode`／`format`／時間錨點）——**不沿用既有 `ReportJobView`**（受理／單筆查詢形狀），**不動既有已驗證之契約**。`limit` 預設 20／上限 100；無資料回空陣列非 404 |
| 1.0 | — | 開發團隊 | 初版（規格先行） |
| 1.1 | 2026-09-22 | WorkBuddy AI | **`CR-017` 裁定 D 執行完成（含 Postgres 讀模型）**——①**§5 改寫**為設計權威（Oracle 作業指標／Postgres 讀模型之分工、`rpt.daily_auth_summary` 定義、批次投影 stand-in、交付清單）；②**§7** 新增實作要點（非同步語意、投影追趕視窗、產生器支援矩陣、冪等、失敗可稽核）；③**§10** 移除兩項已解除限制、新增 8 項（物件儲存未部署／跨 schema stand-in／僅一支報表／僅兩種格式／無排程報送／無預設區間／無定義管理端點／`common-batch` 未使用）；④**補建本修訂記錄區**（原文件欠缺）。單測 16 例 0 失敗；真機 e2e 六項 |
