# 批次匯入匯出功能設計規格書

## 1. 概述

提供 Excel 批次匯入匯出機制，讓使用者能高效維護專案內的 SSP 控制項現況說明與 Job 任務設置，取代逐筆 UI 操作。

### 1.1 功能範圍

| 模組 | 匯出內容 | 匯入可設定欄位 |
|------|---------|---------------|
| SSP 控制項現況 | 控制項 + 檢查項目的實作狀態與現況描述 | 實作狀態、現況描述 |
| Job 任務設置 | 所有 Job 的任務類型、指派人員、部門、設備 | 任務類型、指派人員、參與部門、設備 |

### 1.2 設計原則

- **匯出即範本**：匯出的 Excel 帶有現有資料，使用者在此基礎上修改後直接匯入
- **三步驟流程**：上傳 → 驗證預覽 → 確認寫入，降低誤操作風險
- **支援重新驗證**：前端可在表格中修正錯誤後，送 JSON 重新驗證，不需重新上傳 Excel
- **防禦性寫入**：confirm 階段也做基本驗證，防止未經 validate 直接送入錯誤資料

---

## 2. 共用流程

兩個模組共用相同的匯入匯出流程架構：

```
                    ┌──────────────┐
                    │   匯出 Excel  │
                    │  GET /export  │
                    └──────┬───────┘
                           │
                    瀏覽器下載 .xlsx
                  （灰底=唯讀，黃底=可編輯）
                           │
                    使用者離線編輯 Excel
                           │
                    ┌──────▼───────┐
                    │  上傳 Excel   │
                    │ POST /import  │
                    │(multipart)    │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │  驗證結果預覽  │
                    │ valid / error │
                    └──────┬───────┘
                           │
                  ┌────────┼────────┐
                  │有錯誤            │全部有效
                  ▼                  ▼
          前端表格編輯         ┌─────────────┐
                  │           │  確認匯入     │
                  ▼           │POST /confirm │
          ┌─────────────┐    └──────┬──────┘
          │  重新驗證     │           │
          │POST /validate│    批次寫入完成
          │  (JSON)      │    刷新列表
          └──────┬──────┘
                 │
          回到驗證結果預覽
          （可重複多次）
```

### 2.1 API 端點模式

每個模組提供 4 支 API：

| 端點 | Method | Content-Type | 用途 |
|------|--------|-------------|------|
| `/export` | GET | — | 下載 Excel |
| `/import` | POST | multipart/form-data | 上傳 Excel + 驗證 |
| `/import/validate` | POST | application/json | JSON 重新驗證 |
| `/import/confirm` | POST | application/json | 確認寫入 |

### 2.2 Excel 格式規範

| 項目 | 規格 |
|------|------|
| 格式 | `.xlsx`（OpenXML） |
| 大小限制 | 10 MB |
| Header 行 | 第 1 行，藍底白字，凍結 |
| 唯讀欄位 | 灰底（`#F2F2F2`），匯入時忽略修改 |
| 可編輯欄位 | 淡黃底（`#FFFFC0`，同 Excel 備註色） |
| 下拉選單 | openpyxl DataValidation，限制合法值 |
| 框線 | 全部資料格加細框線 |

### 2.3 驗證結果格式

```json
{
  "status": true,
  "data": {
    "valid_count": 45,
    "error_count": 3,
    "items": [
      {
        "row": 2,
        "...欄位資料...",
        "errors": []
      },
      {
        "row": 5,
        "...欄位資料...",
        "errors": [
          {"field": "欄位名", "message": "錯誤訊息"}
        ]
      }
    ]
  }
}
```

- `errors` 為空陣列 → 該行有效
- `errors` 有值 → 該行有錯誤，前端標紅顯示

### 2.4 Confirm 防護機制

即使未經 validate 直接呼叫 confirm，也會：
- 驗證必填欄位（跳過無效行）
- 驗證合法值（如 status、job_type）
- 回傳 `skipped_count` 告知跳過筆數

---

## 3. SSP 控制項現況匯入匯出

### 3.1 API 端點

| Method | URL | 說明 |
|--------|-----|------|
| GET | `/api/1.0/ssp/<ssp_uid>/control-implementations/export` | 匯出 Excel |
| POST | `/api/1.0/ssp/<ssp_uid>/control-implementations/import` | 上傳 Excel + 驗證 |
| POST | `/api/1.0/ssp/<ssp_uid>/control-implementations/import/validate` | JSON 重新驗證 |
| POST | `/api/1.0/ssp/<ssp_uid>/control-implementations/import/confirm` | 確認匯入 |

### 3.2 Excel 結構（7 欄）

| 欄 | 標題 | 控制項行 | 檢查項目行 | 說明 |
|----|------|---------|-----------|------|
| A | 控制項識別碼 | 灰底（有值） | 灰底（重複） | e.g. `AC.L1-3.1.1` |
| B | 控制項名稱 | 灰底（有值） | 灰底 | e.g. `授權存取控制` |
| C | 實作狀態 | **黃底（可填）** | 灰底 | 下拉選單 |
| D | 現況描述 | **黃底（可填）** | 灰底 | 自由文字 |
| E | 檢查項目名稱 | 灰底 | 灰底（有值） | e.g. `[a] authorized users...` |
| F | 檢查項目實作狀態 | 灰底 | **黃底（可填）** | 下拉選單 |
| G | 檢查項目現況描述 | 灰底 | **黃底（可填）** | 自由文字 |

### 3.3 實作狀態合法值

`implemented`, `partial`, `not_applicable`, `not_implemented`, `inherited`, `unknown`

### 3.4 行類型判斷

匯出時：
- 每個控制項先輸出一行（控制項行）
- 接著輸出該控制項下的所有檢查項目（每個一行）

匯入時：
- 有 C 或 D 欄值 → 控制項行
- 有 F 或 G 欄值（且 C/D 無值） → 檢查項目行
- 檢查項目行按控制項下的順序對應 `statement_identifier`

### 3.5 資料來源

| 資料 | 來源 |
|------|------|
| 控制項識別碼 | `system_security_plan_control_implementations.control_identifier` |
| 控制項名稱 | `assessment_plan_controls.control_title`（透過 SSP → AP → Controls） |
| 檢查項目名稱 | `assessment_plan_tasks.title`（透過 SSP → AP → Controls → TaskControls → Tasks） |
| 實作狀態/現況描述 | `system_security_plan_control_implementations` / `ssp_control_implementation_objectives` |

### 3.6 驗證結果欄位

| 欄位 | 說明 | 前端顯示 |
|------|------|---------|
| `control_identifier` | 控制項識別碼 | 顯示 |
| `control_name` | 控制項名稱 | 顯示 |
| `objective_id` | 檢查項目內部 ID | **不顯示**（原封帶回） |
| `objective_name` | 檢查項目名稱 | 顯示 |
| `implementation_status` | 控制項實作狀態 | 控制項行顯示 |
| `implementation_description` | 控制項現況描述 | 控制項行顯示 |
| `obj_implementation_status` | 檢查項目實作狀態 | 檢查項目行顯示 |
| `obj_implementation_description` | 檢查項目現況描述 | 檢查項目行顯示 |

### 3.7 Confirm Response

```json
{
  "updated_controls": 15,
  "updated_objectives": 30,
  "total": 45,
  "skipped_count": 0
}
```

---

## 4. Job 任務設置匯入匯出

### 4.1 API 端點

| Method | URL | 說明 |
|--------|-----|------|
| GET | `/api/1.0/grc/project/<pid>/ap/<apid>/jobs/export` | 匯出 Excel |
| POST | `/api/1.0/grc/project/<pid>/ap/<apid>/jobs/import` | 上傳 Excel + 驗證 |
| POST | `/api/1.0/grc/project/<pid>/ap/<apid>/jobs/import/validate` | JSON 重新驗證 |
| POST | `/api/1.0/grc/project/<pid>/ap/<apid>/jobs/import/confirm` | 確認匯入（需 manager 角色） |

### 4.2 Excel 結構（10 欄，A 欄隱藏）

| 欄 | 標題 | 底色 | 可見 | 必填 | 說明 |
|----|------|------|------|------|------|
| A | job_uid | — | **隱藏** | — | 程式用，匯入時精確匹配 Job |
| B | 控制項群組 | 灰底 | 可見 | — | group name |
| C | 控制項識別碼 | 灰底 | 可見 | — | e.g. `AC.L1-3.1.1` |
| D | 控制項名稱 | 灰底 | 可見 | — | control title |
| E | 檢查項目名稱 | 灰底 | 可見 | — | AO name |
| F | 任務名稱 | 灰底 | 可見 | — | job name |
| G | 任務類型 | **黃底** | 可見 | **必填** | 下拉：`general` / `survey` |
| H | 指派人員 | **黃底** | 可見 | **必填** | 逗號分隔 login_name |
| I | 參與部門 | **黃底** | 可見 | 選填 | 逗號分隔部門名稱 |
| J | 設備 | **黃底** | 可見 | 選填 | 逗號分隔設備名稱 |

### 4.3 排序

匯出資料按 **控制項群組 → 控制項識別碼 → 檢查項目名稱** 升序排列。

### 4.4 隱藏欄位設計

- A 欄（`job_uid`）使用 openpyxl `column_dimensions["A"].hidden = True`
- 使用者在 Excel 中看不到此欄
- openpyxl `iter_rows(values_only=True)` 仍能讀取隱藏欄的值
- 匯入時用 `job_uid` 精確匹配，解決同一 AO 下多個同名 Job 的問題

### 4.5 驗證規則

| 欄位 | 規則 | 錯誤訊息 |
|------|------|---------|
| job_uid（A 欄） | 必須存在且屬於此 AP | 「任務不屬於此稽核計畫」 |
| job_uid 重複 | 同一 Excel 不允許重複 | 「重複的任務識別碼」 |
| 任務類型 | 必填，`general` 或 `survey` | 「無效的任務類型」 |
| 指派人員 | 必填，每個 login_name 須存在（不分大小寫） | 「使用者不存在：xxx」 |
| 參與部門 | 選填，名稱須存在且不重名 | 「部門不存在」/「部門名稱重複，請改用 UI」 |
| 設備 | 選填，名稱須存在且不重名 | 「設備不存在」/「設備名稱重複，請改用 UI」 |

### 4.6 部門/設備重名處理

- 驗證階段：檢測到重名部門/設備時報錯，提示使用者改用 UI 設定
- Confirm 階段：重名的 name → uid mapping 已排除，不會指定到錯誤的部門/設備

### 4.7 指派人員更新機制

- `update_job` 需要 `control_uid` 才能更新 assignees（repo 層限制）
- Confirm 時透過 `JobImportLookupQuery.get_job_control_uid_map()` 批次查詢 job_uid → control_uid 的 mapping
- 查詢路徑：Job → WorkflowExecution → AP Task → TaskControl → Control → uid

### 4.8 權限控制

- Confirm 端點需要 **manager 角色**
- 透過 `_check_manager_role()` 在 service 層驗證
- 非 manager 回傳 `403 GRC_403002`

### 4.9 驗證結果欄位

| 欄位 | 說明 | 前端顯示 |
|------|------|---------|
| `job_uid` | Job 內部 ID | **不顯示**（原封帶回） |
| `job_name` | 任務名稱 | 顯示 |
| `control_name` | 控制項名稱 | 顯示 |
| `ao_name` | 檢查項目名稱 | 顯示 |
| `job_type` | 任務類型 | 顯示，可編輯 |
| `assignees` | 指派人員（逗號分隔） | 顯示，可編輯 |
| `departments` | 參與部門（逗號分隔） | 顯示，可編輯 |
| `devices` | 設備（逗號分隔） | 顯示，可編輯 |

### 4.10 Confirm Response

```json
{
  "updated_count": 30,
  "skipped_count": 0
}
```

---

## 5. Error Code

| 錯誤碼 | HTTP | 訊息 | 適用模組 |
|--------|------|------|---------|
| `GRC_400001` | 400 | 上傳檔案格式無效，請使用匯出的 Excel 範本 | SSP |
| `GRC_400002` | 400 | 上傳檔案超過大小限制（10MB） | SSP |
| `GRC_400003` | 400 | 上傳檔案格式無效，請使用匯出的 Excel 範本 | Job |
| `GRC_400004` | 400 | 上傳檔案超過大小限制（10MB） | Job |
| `GRC_403002` | 403 | 使用者不具備管理者角色 | Job confirm |
| `GRC_404012` | 404 | SSP 系統安全計畫不存在 | SSP |
| `GRC_404015` | 404 | 稽核計畫不存在 | Job |

---

## 6. 架構設計

### 6.1 DDD 層級分佈

```
api/                          ← Route 層（HTTP 邊界）
├── oscal/routes/ssp/
│   └── ssp_control_impl_import_route.py    ← SSP 匯入匯出 routes
├── oscal/serializers/ssp/
│   └── ssp_control_impl_import.py          ← SSP Marshmallow schemas
├── grc/routes/
│   └── job_import_route.py                 ← Job 匯入匯出 routes
└── grc/serializers/
    └── job_import.py                       ← Job Marshmallow schemas

app/                          ← App Service 層（業務邏輯編排）
├── oscal/service/
│   └── ssp_control_impl_import_service.py  ← SSP 匯入匯出 service
└── grc/service/
    └── job_import_service.py               ← Job 匯入匯出 service

infra/                        ← 基礎設施層（ORM 查詢）
├── oscal/repository/
│   └── ssp_catalog_title_query.py          ← SSP 控制項/AO 標題查詢
└── grc/repository/
    ├── job_export_query.py                 ← Job 匯出資料查詢
    └── job_import_lookup_query.py          ← Job 匯入驗證/確認 lookup
```

### 6.2 DI Container

| Container | Provider | 類別 |
|-----------|----------|------|
| `oscal_container` | `ssp_catalog_title_query` | `SspCatalogTitleQuery` (Singleton) |
| `oscal_container` | `ssp_control_impl_import_service` | `SspControlImplImportService` (Factory) |
| `grc_container` | `job_export_query` | `JobExportQuery` (Singleton) |
| `grc_container` | `job_import_lookup_query` | `JobImportLookupQuery` (Singleton) |
| `grc_container` | `job_import_service` | `JobImportService` (Factory) |

### 6.3 依賴關係

**SSP Import Service：**
```
SspControlImplImportService
├── SystemSecurityPlanDomainService     (SSP 查詢)
├── ControlImplementationDomainService  (控制項查詢)
├── ControlImplementationObjectiveDomainService (Objective 查詢)
├── SspControlImplementationService     (upsert 複用)
└── SspCatalogTitleQuery                (標題查詢，infra)
```

**Job Import Service：**
```
JobImportService
├── JobService                          (update_job 複用)
├── JobExportQuery                      (匯出資料查詢，infra)
├── JobImportLookupQuery                (驗證/確認 lookup，infra)
├── ProjectDomainService                (權限檢查用)
└── ProjectParticipantDomainService     (權限檢查用)
```

---

## 7. 變更檔案清單

### 7.1 新增檔案

| 檔案 | 職責 |
|------|------|
| `api/oscal/serializers/ssp/ssp_control_impl_import.py` | SSP Marshmallow schemas |
| `api/oscal/routes/ssp/ssp_control_impl_import_route.py` | SSP 4 支 API routes |
| `app/oscal/service/ssp_control_impl_import_service.py` | SSP 匯入匯出 service |
| `infra/oscal/repository/ssp_catalog_title_query.py` | SSP 標題查詢（infra） |
| `infra/oscal/__init__.py` | package init |
| `infra/oscal/repository/__init__.py` | package init |
| `api/grc/serializers/job_import.py` | Job Marshmallow schemas |
| `api/grc/routes/job_import_route.py` | Job 4 支 API routes |
| `app/grc/service/job_import_service.py` | Job 匯入匯出 service |
| `infra/grc/repository/job_export_query.py` | Job 匯出資料查詢（infra） |
| `infra/grc/repository/job_import_lookup_query.py` | Job 匯入驗證/確認 lookup（infra） |

### 7.2 修改檔案

| 檔案 | 變更內容 |
|------|---------|
| `api/oscal/__init__.py` | 註冊 SSP import/export routes |
| `api/grc/__init__.py` | 註冊 Job import/export routes |
| `di_containers/oscal/oscal_containers.py` | SSP DI wiring |
| `di_containers/grc/grc_containers.py` | Job DI wiring |
| `common/code/grc_error_code.py` | 新增 4 個 error codes |

---

## 8. 不在範圍內的功能

| 項目 | 原因 | 替代方案 |
|------|------|---------|
| 問卷配置匯入 | 問卷有類型、審核流程、per-user 權限，Excel 無法表達 | 保留在 UI 操作 |
| 指派人員審核者設定 | 增加 Excel 複雜度，使用頻率低 | 保留在 UI 操作 |
| SSP 備註欄位 | UI 暫無此功能，為預留欄位 | 未來需要時加回 |
| 新增 Job | 匯入只更新現有 Job，不支援新增 | 使用現有 Job create API |
