# Phase A5 Implementation Plan — 預覽 UI + Confirm 寫入

> **Phase**：A5（Track A：Excel 匯入收口）
> **依據**：design-A5.md
> **狀態**：plan draft（2026-05-20）
> **預估**：3 session / ~4d（Session A design+plan / B BE / C FE+收尾）

---

## 0. Pre-flight Verification（T0，開工 BE 前必驗）

落地偏差進 design-A5 §10。

### T0.1 — 既有 confirm schema 影響盤點

```bash
grep -rn "SspExcelImportConfirmRequestSchema\|decisions.*overrides" \
  api/oscal/ app/oscal/ tests/
```

驗證項目：
- 既有 A2/A4 test 是否依賴「decisions 是 List[Dict] / overrides 是 Dict」彈性 shape
- 改成結構化 schema 後哪些 test 需更新

### T0.2 — Inline 新建 domain service 簽章驗

| Domain Service | 期望 method | 簽章驗 |
|---------------|------------|--------|
| `DeviceDomainService` | `.add(entity, user_context)` | 看 `domain/device/service/device_domain_service.py` |
| `InformationSystemDomainService` | `.add(entity, user_context)` | 看 `domain/compliance/...` |
| `OrgUnitDomainService` (jedi-auth) | `.add(entity, user_context)` | 看 `~/Projects/Jedicogy/.../jedi-auth/` |
| `UserDomainService` (jedi-auth) | `.add(entity, send_invite_flag)` | 同上 |

特別檢查：4 個 add() 是否都接得起 `@transaction` scope、是否需要額外 session injection、回傳 entity 是否含 id。

### T0.3 — content_overrides 套用 path 驗

確認 A4 ship 的 `_dict_to_parsed_*` 5 個 static helper 是否能在 in-memory overlay 後仍跑通（A4 設計時應已支援，但 T0 確認）。

### T0.4 — DELETE pattern 驗

```bash
grep -A 20 "def discard_parse" app/oscal/service/ssp_docx_import_app_service.py
```

確認 docx discard pattern：soft delete flag / hard delete / cascade 行為。A5 對齊。

### T0.5 — Error code 序號驗

```bash
grep -E "GRC_(400|409|412)" common/code/grc_error_code.py | sort -t_ -k2 -n
```

抓 400 / 409 / 412 最後使用的序號，A5 新加從下一個序號開始。

### T0.6 — FE base patterns 驗

| 確認項目 | 路徑 |
|---------|------|
| BaseService extension | `compliance-manager-fe/src/service/BaseService.js` + 既有 service 範例 |
| Pinia store pattern | `compliance-manager-fe/src/store/modules/<既有>.js` |
| PrimeVue Dialog / Dropdown | 既有 component reference |
| i18n 用法 | `compliance-manager-fe/src/locales/zh-tw/` |

---

## 1. Session B — BE 實作（~1.5d）

### Task 1 — Confirm Request Schema 定稿（~30 min）

**改動**：`api/oscal/serializers/ssp/ssp_excel_import.py`

實作：
- 新增 `InlineCreateSchema`（row_idx / entity_type / payload）
- 新增 `DecisionSchema`（sheet / row_idx / action / matched_id_override / inline_create_row_idx）
- 改 `SspExcelImportConfirmRequestSchema`：`decisions = List(Nested(DecisionSchema))` / `inline_creates = List(Nested(InlineCreateSchema))` / `content_overrides = Dict`（保留沿 docx pattern）

**驗收**：
- 既有 A2/A4 test 全綠（A2 confirm test 應該都是空 decisions 直接傳 → 不破壞）
- 新 schema 驗錯誤 payload 回 400

**Commit**：`feat(oscal): A5 T1 confirm request schema 定稿`

### Task 2 — _apply_inline_creates helper（~1h）

**改動**：`app/oscal/service/ssp_excel_import_app_service.py`

實作 `_apply_inline_creates(inline_creates, user_context, tenant_id) -> dict[int, dict]`：

```python
def _apply_inline_creates(self, inline_creates, user_context, tenant_id):
    """
    Returns: {row_idx: {"entity_type": str, "new_id": int}}
    All writes in caller's @transaction; rollback if any fail.
    """
    created = {}
    for ic in inline_creates:
        if ic["entity_type"] == "device":
            entity = DeviceEntity(**ic["payload"], tenant_id=tenant_id)
            created_entity = self._device_domain.add(entity, user_context)
            created[ic["row_idx"]] = {"entity_type": "device", "new_id": created_entity.id}
        elif ic["entity_type"] == "information_system":
            # 同上 pattern
        elif ic["entity_type"] == "org_unit":
            # 透過 jedi-auth org_unit_service.add(...)
        elif ic["entity_type"] == "user":
            # 透過 jedi-auth user_service.add(...) + send_invite=True
    return created
```

注入 4 個新 domain service 到 `__init__`：
- `device_domain_service`
- `information_system_domain_service`
- `org_unit_domain_service`（jedi-auth）
- `user_domain_service`（jedi-auth）

**驗收**：
- 4 種 entity 都 wire 通
- DI Container `ssp_excel_import_app_service` Factory 加 4 個 wiring
- Inline create 失敗（duplicate / validation）整段 rollback

**Commit**：`feat(oscal): A5 T2 inline_creates handling + 4 domain service DI`

### Task 3 — _apply_decisions + _apply_content_overrides helper（~45 min）

**改動**：同 file

實作：
- `_apply_decisions(parsed_result, decisions, inline_create_map)` — 遍歷 decisions，對 `use_existing` / `use_inline_new` / `skip_as_text` 三種 action 蓋掉 `parsed_result` 對應位置的 `matched_*_id` / `match_method`
- `_apply_content_overrides(parsed_result, content_overrides)` — 蓋掉 `parsed_result["metadata"]` / `parsed_result["controls"][cid]["implementation_description"]` / `parsed_result["controls"][cid]["objectives"][obj_key]` 等欄位

**注意**：A4 Step 7 `_update_parsed_result_with_reconcile` 已有 by-key 對齊 helper `_iter_paired` — A5 `_apply_decisions` 沿用同 pattern，by (sheet, row_idx) lookup。

**驗收**：unit test 各 5+ 個 case

**Commit**：`feat(oscal): A5 T3 _apply_decisions + _apply_content_overrides`

### Task 4 — confirm_import 整合 + transaction 保證（~1h）

**改動**：`SspExcelImportAppService.confirm_import`

把 T1/T2/T3 串成完整 flow：

```python
@transaction
def confirm_import(self, parse_uid, payload, user_context):
    parse_job = self._parse_job_domain.get_by_uid(parse_uid)
    parsed_result = parse_job.parsed_result

    # Step 1: content_overrides
    self._apply_content_overrides(parsed_result, payload["content_overrides"])

    # Step 2: inline_creates
    tenant_id = user_context.tenant_id
    inline_create_map = self._apply_inline_creates(
        payload["inline_creates"], user_context, tenant_id
    )

    # Step 3: decisions
    self._apply_decisions(parsed_result, payload["decisions"], inline_create_map)

    # Step 4: A4 ship 的 _write_all_data 8-step pipeline
    write_result = self._write_all_data(
        parsed_result, is_superset_flow=..., fw_version=...
    )

    # Step 5: import_summary 補 inline_created_* 計數
    import_summary = {**write_result, **{
        "inline_created_devices": sum(...),
        "inline_created_info_systems": sum(...),
        "inline_created_org_units": sum(...),
        "inline_created_users": sum(...),
        "decision_applied_count": len(payload["decisions"]),
        "skipped_as_text_count": sum(1 for d in payload["decisions"] if d["action"] == "skip_as_text"),
    }}

    return {"parse_uid": parse_uid, "status": "success", "import_summary": import_summary, ...}
```

**驗收**：整個 confirm 在單一 `@transaction` scope；inline create 失敗整段 rollback

**Commit**：`feat(oscal): A5 T4 confirm_import 整合 + atomicity 保證`

### Task 5 — DELETE /ssp-excel-import/<parse_uid> endpoint（~30 min）

**改動**：
- `api/oscal/routes/ssp/ssp_excel_import_route.py` — 加 `delete()` method 到 `SspExcelImportRoute`
- `app/oscal/service/ssp_excel_import_app_service.py` — 加 `discard_parse(parse_uid, user_context)` method（沿 docx pattern：soft delete parse job status=discarded）

**驗收**：
- DELETE 後 GET 同 parse_uid 回 404
- discarded parse job 不能 confirm

**Commit**：`feat(oscal): A5 T5 DELETE /ssp-excel-import discard endpoint`

### Task 6 — Error code 新增（~15 min）

**改動**：`common/code/grc_error_code.py`

可能新增（依 T0.5 verify 結果 + 實作期間需要）：
- `GRC_INLINE_CREATE_VALIDATION_FAILED`（400xxx）
- `GRC_INLINE_CREATE_DUPLICATE_NAME`（409xxx）
- `GRC_PARSE_JOB_DISCARDED`（410xxx 或 404xxx；T0 拍板）

**Commit**：併入 T2 / T5 commit（同主題的小範圍 error code）

### Task 7 — BE Pytest 補齊（~2h）

**新增 test 檔**：

```
tests/test_a5_confirm_schema.py                  # ~8 case
tests/test_a5_inline_create_device.py            # ~5 case
tests/test_a5_inline_create_info_system.py       # ~4 case
tests/test_a5_inline_create_party.py             # ~4 case (org_unit + user)
tests/test_a5_content_overrides.py               # ~6 case
tests/test_a5_decisions.py                       # ~6 case (4 action types + skip_as_text)
tests/test_a5_inline_create_rollback.py          # ~3 case (duplicate / validation / cascade rollback)
tests/test_a5_discard_endpoint.py                # ~3 case
tests/test_a5_confirm_integration.py             # ~5 case (full payload happy path + 主要 error)
```

**累計**：~44 個 A5 新 test。

**規範**：
- 沿 jedi DBLogHandler patch pattern：`patch('app.oscal.service.ssp_excel_import_app_service.logger')`
- 用 `patch_session_scope` 開 `@transaction`
- Mock layered domain services（不接真實 DB）

**驗收**：a2+a3+a4+a5 ssp_excel test 全綠（A4 ship 後 282 個 + A5 新 ~44 個 = ~326 個）

**Commit**：`test(oscal): A5 T7 unit + integration tests`

### Task 8 — Session B 收尾（~30 min）

- Changelog：`docs/changelog/2026-05-21-feat-ssp-excel-import-phase2-a5-be.md`（type=feat）
- 跑全套 pytest 確認 0 regression
- 寫 Session B → C handoff prompt：`docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-21-a5-b-to-c.md`

**Commit**：`docs(ssp-import-export-phase2): A5 Session B 收尾 - changelog + handoff B→C`

---

## 2. Session C — FE 實作 + 收尾（~2d）

### Task 9 — Service + Pinia Store + Router（~1h）

**新增**：
- `src/service/oscal/SspExcelImportService.js`（BaseService extension）
  - `parse(formData)` → POST /ssp-excel-imports/parse
  - `getParseResult(parseUid)` → GET /ssp-excel-import/<uid>
  - `confirm(parseUid, payload)` → POST /ssp-excel-import/<uid>/confirm
  - `discard(parseUid)` → DELETE /ssp-excel-import/<uid>
- `src/store/modules/sspExcelImport.js`（Pinia）
  - state：parsedResult / decisions / inlineCreates / contentOverrides
  - actions：loadFromLocalStorage / saveToLocalStorage / clearLocalStorage / setDecision / addInlineCreate / setContentOverride
- `src/router/routes.js`：加 `/module-frame/import-excel` + `/module-frame/import-excel/:parseUid`

### Task 10 — 上傳頁 + ImportExcelPreview.vue 骨架（~1h）

- 上傳頁：framework + version 兩層 picker（沿 A1 既有 DownloadSspBlankTemplateDialog pattern）→ file upload → POST parse → redirect /:parseUid
- ImportExcelPreview.vue：9 sheet TabView + 動態 badge（unmatched 數量）

### Task 11 — Sheet preview 元件 × 7（~4h）

- `SheetPreviewBasic.vue` — 01_基本資料（DataTable 單列 + 編輯）
- `SheetPreviewParties.vue` — 02_單位 + 03_參與人員（共用，依 party_type 切換）
- `SheetPreviewDevices.vue` — 04_設備（DataTable + unmatched badge + UnmatchedRow component）
- `SheetPreviewInfoSystems.vue` — 05_資訊系統
- `SheetPreviewLeveraged.vue` — 06_外部利用服務
- `SheetPreviewControls.vue` — 07_控制項與 AO（巢狀 control + objectives 結構，較複雜）
- `SheetPreviewRefDocs.vue` — 08_程序書

### Task 12 — UnmatchedRow + InlineCreateDialog + FuzzyBadge 共用元件（~2h）

- `UnmatchedRow.vue` — 顯示 3 個按鈕（選現有 / 新建 / 純文字保留）+ 對應 dropdown / dialog
- `InlineCreateDialog.vue` — 通用對話框，依 entity_type 切 form schema（device / info_system / org_unit / user）
- `FuzzyBadge.vue` — confidence badge + override 按鈕（4 個 action 選項）

### Task 13 — Confirm flow + 確認 modal（~1h）

- 確認按鈕 → opens modal 顯示總計
- 點 "確定" → POST confirm with payload from Pinia → 成功 → clear localStorage + redirect MF 詳細頁 + success toast

### Task 14 — i18n + UX polish + Manual smoke（~1.5h）

- `src/locales/zh-tw/module-frame.js` + `src/locales/en/module-frame.js` 加 A5 字串
- 跑 manual smoke：上傳 → 預覽 → 編輯 → unmatched 三路徑 → fuzzy override → confirm → 驗 DB

### Task 15 — Session C 收尾 / A5 task arc final SUMMARY（~1h）

按 CLAUDE.md「做 summary 觸發完整收尾」段：
1. 跑全套 BE pytest 確認 0 regression（最後 sanity check）
2. 規範文件齊全度：BE changelog (Session B 已寫) + FE changelog 新寫
3. design §10 reconciliation 補 Session B / C 偏差
4. Tracker README A5 row 改 BE+FE shipped
5. 對話歷史 dump 到 `docs/conversation-history/<date>/ssp-import-export-phase2-A5/`
6. 產 SUMMARY → `docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-XX-a5-SUMMARY.md`

**Commits（Session C）**：
- `feat(fe): A5 T9-T13 import-excel preview page`
- `feat(fe): A5 T14 i18n + UX polish`
- `docs(ssp-import-export-phase2): A5 Session C 收尾 + SUMMARY`（含 BE/FE 跨 repo summary）

---

## 3. CLAUDE.md 規範對應 Checklist

| 規範 | A5 對應 |
|------|---------|
| DDD Route 不查 DB | ✅ DELETE / confirm 都走 app_service |
| App Service @transaction | ✅ confirm_import @transaction 包整個 inline_create + write |
| Repo session lazy | ✅ 不動 repo 層 |
| 寫入 API 必有角色檢查 | ✅ 沿用 A2 既有 jwt + role check |
| 前置條件驗證 | ✅ confirm 走 A4 既有檢查（缺 SSP raise GRC_412026 等）|
| Error code 命名 | ✅ T0.5 驗 + T6 新加沿 `GRC_<status><3 位序號>` |
| SQL Migration | ❌ A5 無 schema 異動 |
| 審計欄位 | ✅ inline create 走 domain service.add() 走既有 audit fields |
| 顯式 git add | ✅ 14 個 commits 都用 explicit list |
| jedi-* 不動 | ✅ Phase 2 完整完工才一次 bump |
| pyproject.toml dev-path 不 commit | ✅ 沿 A4 慣例 |
| 跨 repo 切換時提醒 user | ✅ Session C FE 切到 compliance-manager-fe repo |
| 階段性 commit 不用問 | ✅ |

---

## 4. Risk / Rollback

| 場景 | Rollback path |
|------|--------------|
| Session B BE 撞 jedi-auth 簽章不對 | T0.2 verify 應先抓到；若實作中撞 → §10 補偏差 + 改 helper 簽章 |
| Inline create 寫到一半 fail | `@transaction` 自動 rollback；不留髒資料 |
| FE localStorage 損毀 | 加 try/catch + 自動清除 + 提示 user 重新上傳 |
| Session C smoke 撞 BE bug | 回到 BE 補 hotfix（type=fix changelog）|
| A5 全 ship 後發現某 entity inline create 缺欄位 | 補 InlineCreateSchema.payload 子欄位 + FE dialog；不影響既有資料 |

---

## 5. Migration 順序（跨 repo / 跨 commit）

```
1. BE Session B：
   T1 schema → T2 inline_creates → T3 helpers → T4 confirm 整合 → T5 DELETE → T6 error code → T7 pytest
   ↓ ship
2. BE smoke（可用 curl / postman 驗 BE-only happy path）
   ↓
3. FE Session C：
   T9 service+store → T10 上傳頁 → T11 sheet 元件 → T12 unmatched UI → T13 confirm flow → T14 i18n+smoke
   ↓ ship
4. A5 task arc 收口 SUMMARY
```

每階段獨立可 ship — partial ship pattern OK（BE 先 ship → smoke → FE）。

---

## 6. Plan Verification（plan 寫完自查）

- [x] 規則 1：先 ship 這份 spec 能用嗎？— Session B 後 BE 端可獨立 smoke；Session C 後完整 user-facing 可用 ✅
- [x] 規則 2：mental walkthrough 在 design §5 ✅
- [x] 規則 3：本 spec 範圍 / 衍生 follow-up 邊界在 design §2.2 / §2.3 ✅
- [x] 規則 4：3-day cap — ⚠ ~4d，BE/FE 不能拆兩 spec；mitigation 已標 §11 ✅
- [x] DDD 層級規範對應 §3 ✅
- [x] Error code 命名 §0.5 T0.5 verify ✅
- [x] 跨 repo 標記每個 task ✅
- [x] CLAUDE.md compliance checklist §3 ✅
