# SPEC WAVE-7 — Bản cứng `Hdc_*` 7 leaf trang thật (GĐ4, b.19→21, upload-only)

> Nguồn: lane-2 §3 (option A derive 0-mig — grounding đã SỬA: Hdc = **7 leaf per-ContractType**, KHÔNG phải 6-leaf; KHÔNG có phase "KyCung") + lane-1 F-13/F-14/F-15 (nghiệp vụ ký/dấu/lưu/scan; ⚠️ bảng 6-leaf của F-15 xây trên tiền đề menu SAI — NỘI DUNG cột/field giữ, HÌNH menu theo lane-2) + F-11 (bản cứng sinh ở b.17-18 GĐ3 → GĐ4 hiển thị readonly phần đó). Build song song từ đầu; VERIFY cần HĐ `DaPhatHanh` (sau W5+W6).

## ① TÍNH NĂNG / ĐỀ BÀI

7 leaf `Hdc_<Code>` ("01. HĐ Thầu phụ - Bảng cứng" … "07.") hết ComingSoon → trang **theo dõi bản cứng** per loại HĐ: list HĐ `phase=9 DaPhatHanh` + **1 MỐC** hệ-thống. 🔄 **Anh chốt @S161 (verbatim, default #8 plan cha):** *"trình ký sẽ hiển thị full form doc có sẵn trong mục FORM -> điền đầy đủ vào luôn -> Duyệt qua (Nếu có) -> Upload file cứng lên -> Là xem như xong"* ⇒ ký GĐ (b.19) + đóng dấu (b.20) + thủ tục lưu (b.21) diễn ra **NGOÀI hệ thống trên giấy**; hệ thống chỉ ghi nhận bằng chứng cuối: **upload scan bộ HĐ cứng (đã ký + đóng dấu) → badge "Đã lưu bản cứng" = has(`SealedCopy`) → XONG**. **Option A-RÚT-GỌN: 0-migration + 0 enum-extend** — GĐ4 nằm SAU terminal nên KHÔNG thêm phase, KHÔNG hồi sinh `DangDongDau=8 [LEGACY]`.

Mô hình bằng chứng (1-mốc — supersede bảng 3-trạm bản trước @S161):
| Việc | Ai | Bằng chứng hệ thống | Derive badge |
|---|---|---|---|
| b.19+b.20+b.21 (ký GĐ · đóng dấu · phát hành/lưu — làm trên GIẤY) | BOD · HRA/ISO · PB/BCH+CCM | upload `SealedCopy` (scan bộ cứng đã ký+đóng dấu) + note **tùy chọn** {số bộ gốc · đã giao CCM/NTP · nơi lưu} (default #9 hạ mức @S161) — 🔴 KHÔNG dùng `ScannedSigned` (F-C6/gotcha #71: =2 là "scan chữ ký NCC ở DangInKy") | "Đã lưu bản cứng" = has(`SealedCopy`) |
Readonly từ GĐ3: **LevelOpinions HĐ** (chữ ký duyệt điện tử — khớp "Duyệt qua (nếu có)" @S161). Khối attachment ký nháy `InitialSigned` **BỎ** theo 1-mốc.

## ② CÁCH IMPLEMENT

1. **Gate 5-anchor** (§③-A).
2. **BE nhỏ**: (a) 🔄 **0 enum-extend (@S161 — supersede bản trước):** dùng `SealedCopy` CÓ SẴN (`ContractAttachment.cs:5-11`); ~~`InitialSigned=4` + `CoverChecklist=5`~~ **BỎ** theo 1-mốc (slot 4/5 vẫn trống cho tương lai; F-09 cover moot — cover FO-002.01 thuộc bộ hồ sơ giấy); (b) 🔴 **thêm param `type` vào `ListContractsQuery` + controller (review F-C1/F-05 — `GET /api/contracts` HIỆN 0 CÓ `type`, `ContractsController.cs:16-24` chỉ phase/supplierId/projectId):** thêm `[FromQuery] ContractType? type` + filter handler `ContractFeatures.cs:305-307` — KHÔNG lọc client-side `pageSize:100` (trần 100/loại). (c) `ContractListItemDto` **+1 cờ derive `hasSealedCopy`** trong projection (DTO hiện 12 field `:323-327` KHÔNG trả attachments; ~~hasInitialSigned~~ BỎ @S161 — 1-mốc; 🔴 vẫn KHÔNG derive từ `ScannedSigned` — review F-C6: =2 nghĩa "scan có chữ ký NCC ở DangInKy", gotcha #71).
3. **FE ×2 app**: page `HardCopiesPage` (khuôn `MyContractsPage.tsx` — ĐÃ đọc đủ `type`/`phase` params `:29,:34-35`): route `/hard-copies?type=N` → list `GET /api/contracts?type=N&phase=9` + cột mã HĐ/NCC/dự án/giá trị + **1 badge derive** (`hasSealedCopy`) + panel upload (**1 purpose `SealedCopy`** + note tùy chọn default #9) + khối LevelOpinions readonly (@S161 1-mốc). 4-place mirror ×2 app: page + `App.tsx` route + `Layout.tsx` staticMap **re-point 7 dòng `Hdc_`** (`fe-user Layout.tsx:63-69`, mirror fe-admin) khỏi `/coming-soon?stage=4&t=NN` + types. Empty-state khi loại chưa có HĐ phát hành.
4. **Authz**: 🔴 anh chốt **O-A (để mở)** ⇒ Contract giữ `[Authorize]` trần, **KHÔNG gắn policy mới** cho trang Hdc/upload — mọi user auth thao tác được (đúng "hiển thị hết để góp ý"; rủi ro attachment-IDOR disclose plan §4.7). *(Nếu sau này anh bật O-B thì mới dùng `Contracts.{Read,Update}` — Contracts ∈ All `:168`, policy sống sẵn.)*
5. Build ×2 + test + commit. cicd: byte-marker + menu KHÔNG đổi (142 giữ — chỉ re-point staticMap FE, seeder không đụng).

## ③ CHECKLIST

**A. Gate 5-anchor:** `ContractAttachment.cs:5-11` · `DbInitializer.cs:1862-1867` (7-leaf loop — chỉ ĐỌC, không sửa) · `fe-user/src/components/Layout.tsx:63-69` · `fe-user/src/pages/contracts/MyContractsPage.tsx:29-46` · `ContractsController.cs:16-24` (list params `type`/`phase`).
**B. Acceptance (lane-2 §7-W8 đổi số wave):**
- [ ] Test ĐÚNG TÊN (khuôn repo) trong `ContractHardCopyTests`: `ListHardCopies_ReturnsOnlyContracts_InDaPhatHanhPhase` · `ListHardCopies_FiltersByContractType` (🔴 review F-C1 — chứng param `type` LỌC thật, không hiện trùng 7 leaf) · `HardCopyBadge_DerivesFromSealedCopyAttachment`.
- [ ] `dotnet test` PASS 0 fail, số-sau ≥ baseline + 3.
- [ ] FE build ×2 PASS; `grep "Hdc_" fe-user/src/components/Layout.tsx | grep -c "coming-soon"` = **0** (cùng lệnh fe-admin).
- [ ] 🔴 Prod ③ — 2 loại KHÁC NHAU ra 2 danh sách KHÁC NHAU (chứng `type` lọc, review F-C1): `GET /api/contracts?type=1&phase=9` ≠ `?type=2&phase=9` (khi có ≥1 HĐ mỗi loại); mọi item `phase==9`. Byte-marker bundle #77.
- [ ] 🔴 Prod ③ — VAI THẬT thấy được (review F-C2/F-01, phụ thuộc W6): login actor bản-cứng non-admin (Director b.19 / HrAdmin b.20 / CostControl b.21) → trang Bảng cứng ≥1 dòng, KHÔNG rỗng/403.
- [ ] Ca ÂM: leaf loại 0 HĐ phát hành → empty-state, KHÔNG 500.
- [ ] Upload `SealedCopy` trên HĐ ZZTEST DaPhatHanh → badge "Đã lưu bản cứng" lật đúng (1 badge — @S161); note tùy chọn lưu + hiển thị lại được.
**C. Rủi ro:** [ ] mirror mù 6-leaf (grounding cũ) — menu Hdc là 7-leaf-per-type owner vẽ, KHÔNG tự đẻ key (gotcha #84) · [ ] upload không phase-guard là HÀNH VI HIỆN TRẠNG (up được mọi phase) — W7 không siết (đó là W8-authz nếu chọn) · [ ] UI ghi rõ ký GĐ/đóng dấu là bước NGOÀI hệ thống — chỉ nhận scan cuối (anh chốt 1-mốc @S161), tránh user tưởng thiếu trạm.
