diff --git a/.claude/WAL.md b/.claude/WAL.md index 9132160..dafa377 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -18,7 +18,9 @@ chain: - [x] gate 5-anchor W2 PASS 5/5 (A4 line-drift content-found; A5 KeHoachKyKet∈All:180 THẬT; phân xử lệch: mã sinh @CREATE per spec, entity-comment W1 sai → worker sửa) - [x] wf W2 đợt-1 `wf_6f0aad26-229`: 2/2 lane chết #53 NHƯNG sub-md cứu trọn (BE 16.5KB — 15 endpoint policy 15/15 + 4 quyết D1-D4 lead duyệt cả 4 · FE 4 file SHA-pair) → lead solo đuôi FE 4-place (WfView 2-app-2-đường + whitelist matrix +10) → **build BE 0W/0E + FE ×2 PASS + coming-soon 0/0**; harvest 2 diary xong - [x] wf W2 đợt-2 `wf_7490179a-70b`: test-specialist chết #53 sau khi file LAND — **8 test** (2 PIN + 6 acceptance, gồm không-đốt-mã-khi-409 + fixture-có-interceptor cho soft-delete) → lead suite tươi **574/0** (566+8 ≥ sàn 568); harvest diary xong -- [!] reviewer W2 ĐANG CHẠY nền (trục chính: hợp-đồng FE↔BE đứt-2-bờ [2 lane viết song song] · IDOR D3 · picker-khớp-rào · test-coverage-claim · boundary 0-diff) → PASS thì commit/push → cicd +- [x] reviewer W2: **12 finding (3 CRIT đứt-2-bờ + F-8/F-9 security 0-rào detail/download)** — chết #53 sau Trục-4, lead đo nốt 5/6/7 + finalize on-behalf → **11 FIXED + 1 GIỮ (F-10 kế thừa PE)** → build+suite GIỮ 574/0 + marker ruột-bundle 1/1/1 +- [x] commit W2 `6cbc6ad` (20 file +5.020/−23) PUSHED +- [!] wf: cicd-monitor verify `6cbc6ad` ĐANG CHẠY (CI 574 · 0-mig · menu 142 · marker `approved-pe-awaiting-plan` · smoke KHKK · CỜ giữ) → PASS ⇒ W2 ĐÓNG → W3 (duyệt 3 trạm — wave NẶNG NHẤT) hoặc W6 (độc lập, nhẹ hơn) — thứ tự đồ thị: W3 cần W2 ✅, W6 độc lập chạy lúc nào cũng được - [ ] đợt-2 SAU khi wf về: test-specialist + database-agent (song song) → lead acceptance §③-B (build·test·sqlcmd 96·Down sạch·đo CỜ) → reviewer → commit/push → cicd-monitor 3-chân-kiềng - [ ] setup thủ công TRƯỚC W5 (SAU W1 vì type=10 cần mig 69): dựng workflow type=3 + type=10 3-trạm qua Designer (seed chỉ 1 trạm) - [ ] housekeeping @pause/closeout kế: nén MIND-1 5.447B (trần `_mind` 83,2% — MIND-5 chắc vượt) · re-carry 2 ý D rơi (vá-GỐC-Phase-3.5-refresh {mới-nêu} CÒN SỐNG + khuôn-5-đợt→sổ bền {gần-chốt}) · STATUS:6 bổ arc dry-run S160 (mù có lý do — land 15:27 trước pipeline) · hạ nhãn DR/WF-3-trạm đã-định-đoạt · E đếm 7 sub-file (không 6) · mở lại slot-45 trần `_mind` (tiền-đề lật: file SỐNG 83,2%) · quyết bookkeeping OVERDUE:light (cặp H24 chạy @S159 bookend mà `last_audit` đứng 27) diff --git a/.claude/agent-memory/reviewer/MEMORY.md b/.claude/agent-memory/reviewer/MEMORY.md index 022b317..f811688 100644 --- a/.claude/agent-memory/reviewer/MEMORY.md +++ b/.claude/agent-memory/reviewer/MEMORY.md @@ -1,5 +1,7 @@ # Reviewer Agent — Persistent Memory +- **S161-W2 (07-29) KHKK CRUD review — PWF 12 FLAG 3C/5M/4m `[chết #53 sau Trục-4 (13.9KB ghi-từng-trục sống trọn) → lead đo Trục 5-7 + verdict on-behalf; 11 FIXED cùng lượt + 1 GIỮ]`:** 🔴 **Trục-1 hợp-đồng FE↔BE VỠ 6 điểm khi 2 lane viết SONG SONG từ cùng spec** — tsc+build+574-test đều XANH cả 6: route picker lệch tên (F-1, 404 đội lốt empty-state vì TanStack nuốt lỗi + `?? []`) · body `purchaseEvaluationId` vs `PeId` (F-2 — **D1 của BE lập luận "FE đọc cùng spec sẽ khớp" = GIẢ ĐỊNH, không phải phép đo; FE đã KHÔNG làm thế**) · dossier body thiếu planId (F-3) · FE chỉ gọi POST khi BE ép Id=null ⇒ Sửa-đẻ-bản-sao im lặng (F-4) · `peTenGoiThau` (F-5) · `winnerSupplierNames` BE không có (F-6 — **bị F-1 CHE: mảng rỗng nên .map chưa chạy, vá F-1 xong mới lòi** = 2-lỗi-che-nhau). **Luật rút: đối chiếu hợp-đồng = so ROUTE+FIELD từng cái giữa types-FE và record-BE trên ĐĨA, đừng tin lời khai lane nào.** · F-8/F-9 security: **list bịt mà detail/download 0-rào ⇒ rào list chỉ còn là UX** — đối chứng twin PE :877-899 CÓ guard + S89 nháp-riêng-tư ⇒ thụt chuẩn, không phải "khuôn vốn thế" · F-12 INNER-join Projects (global-filter) = picker silent-vanish — lớp "vắng-mặt trông giống ổn". Trục-4 picker-khớp-rào ĐẠT (3/3 rào PE mirror đủ). Tag `[s161-w2, hop-dong-2-lane-song-song-6-diem-dut, loi-khai-la-gia-dinh, 2-loi-che-nhau, list-bit-detail-mo-rao-thanh-ux, inner-join-global-filter-vanish]` + - **[→ archive/2026-07.md @S161-curate] S159 đợt-5 (07-29) menu 6-leaf HĐ + `/contracts/deleted` (PWF 0H/4M/6L):** chú-thích-mang-SỐ phải chạy phép đếm (comment "2 chỗ IgnoreQueryFilters" — thật 3 file/17 site) · cookie-cutter phải soi CẢ phần bản gốc đã CHẶN (thiếu `disabled={deletedView}` ⇒ panel trắng) · kiểm hide-filter TRƯỚC khi kêu thiếu mirror 2-app · `tsc` + `noUnusedLocals` mạnh hơn grep cho claim "đã cắt". - **S161 (07-29) W1 KHKK tiền-commit (Mig 69 + seeder + Designer type-10) — PWF 10 FLAG 1H/3M/4m/2L:** 🔴 **HIGH lại nằm ở `git` chứ không ở mã (S155 tái diễn):** 2/3 file Mig 69 UNTRACKED `??` mà snapshot đã tracked-M ⇒ `commit -a` nạp model 7 bảng nhưng BỎ migration; build+566 test vẫn xanh ⇒ **gate = `git status --porcelain -- src tests | grep '^??'` phải RỖNG ngay trước commit**. · **MAJOR bắt được nhờ đi NGƯỢC lời-khai FE:** comment FE tự khai "nhãn lấy từ BE `applicableTypeLabel`" ⇒ đi kiểm bờ BE: `AwLabels.Type` chỉ 3 entry, `GetValueOrDefault(type, type.ToString())` ⇒ UI hiện **"ContractSigningPlan"** tiếng Anh và `useState(\`Quy trình ${label}\`)` **GHI chuỗi Anh vào cột Name DB** — lớp *hợp-đồng-đứt-2-bờ*, tsc+build+test đều xanh. **Luật rút: câu comment dạng "bên kia lo" = ĐI ĐỌC BÊN KIA.** · seeder 5/5 PASS (call sau revoke `:2109`>`:2096`; revoker chỉ `Hrm|Off|Personal` ⇒ không lật 2-chiều; `TryGetValue` tuple `(RoleId,MenuKey)` — khuôn 1-role sẽ ném dup-key, unique-index `PermissionConfiguration:26` cứu) nhưng **acceptance "đo CỜ" chưa đo được ở đâu**: Dev DB **0 row** `Khkk*` cả MenuItems lẫn Permissions (menus 100 vs prod 142) ⇒ chưa nhánh nào từng chạy. · **`grep`-cùng-lớp bằng TOKEN-SONG-SINH** (`VehicleBookingLevelOpinions` repo-wide) mạnh hơn grep tên-mới: chứng 3/3 site đủ + 0 site thứ 4. · 2 chú thích cùng file nay SAI SỐ ("7 bảng"/"7 khối" → 8) + comment nguồn `MenuKeys.cs:37` "CỐ Ý NGOÀI All" nay sai + anchor `(:614)` trỏ dòng TRƯỚC-commit (đúng = :645) ⇒ **diff sửa mã phải sửa MỌI số/neo trong chú thích cùng file**. · STATUS.md `Menu keys 54`/`Policies 216` không đổi dù |All|=55 ⇒ derived-drift. Tag `[s161, high-o-git-khong-o-ma, hop-dong-dut-2-bo-nhan-BE, token-song-sinh-sweep, seeder-upgrade-if-exists, comment-mang-so-va-neo]` diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/run.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/run.md index a77cab2..28c6fa4 100644 --- a/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/run.md +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/run.md @@ -11,9 +11,11 @@ | 1 | Đợt-1a BE: mega-file `ContractSigningPlanFeatures.cs` (Create+auto-Lines+MaKeHoach@CREATE · UpdateDraft · Get/List/pendingMe+inbox-precompute · Dossier UPSERT/DELETE · Attachments · DELETE allow-list · picker PE-chờ-lập-KH) + `ContractSigningPlansController` authz 2-tầng per-action | hmw `implementer-backend` | [x] `wf_6f0aad26-229` lane-0 chết #53 sau §7 NHƯNG sub-md **16.5KB ghi-từng-mục TRỌN** (tự mở 5 anchor + grep-cùng-lớp 3 site comment + 15 endpoint policy 15/15 + **4 quyết D1-D4 khai-lead-phân-xử → lead DUYỆT cả 4**) · build lead 0W/0E | | 2 | Đợt-1b FE: `pages/khkk/` 3 page + types ×2 app + 4-place mirror (routes + staticMap re-point 6 leaf khỏi coming-soon + menuKeys) + build ×2 + SHA-pair | hmw `implementer-frontend` | [x] lane-1 chết #53 sau MUC-1+cp (SHA-pair 4/4) → **lead solo đuôi**: Layout ×2 re-point 6 leaf (WfView 2 app 2 đường: user=alias matrix?type=10 whitelist+10 · admin=Designer deep-link) + fe-admin App routes + root KeHoachKyKet→/khkk/list; menuKeys place-3 SKIP-có-khai (0 consumer). Build ×2 PASS + coming-soon **0/0** | | 3 | Đợt-2: test-specialist (≥2 test PIN: `ContractSigningPlansController_EveryWriteEndpoint_HasAuthorizePolicy` + `CreateContractSigningPlan_FromNonApprovedPe_Throws`) | hmw `test-specialist` | [!] `wf_7490179a-70b` ĐANG CHẠY nền | -| 4 | Lead: build ×2 + suite + acceptance §③-B sweep | lead | [ ] | -| 5 | Reviewer adversarial (CRUD endpoints POST/PUT/DELETE — bắt buộc) | `reviewer` | [ ] | -| 6 | Commit + push + cicd (3-chân-kiềng + 2-bearer NẾU có tài khoản non-Admin — nv.test KHÔNG tồn tại prod, xem w1-synthesis tồn-2) | lead + `cicd-monitor` | [ ] | +| 4 | Lead: build ×2 + suite + acceptance §③-B sweep | lead | [x] build BE 0W/0E ×2 lượt · suite **574/0** ×2 lượt (trước + SAU 12 fix) · FE ×2 PASS + **marker trong RUỘT bundle 1/1/1** (chứng build thật — 466ms rolldown-warm) · SHA-pair 4/4 giữ | +| 5 | Reviewer adversarial (CRUD endpoints POST/PUT/DELETE — bắt buộc) | `reviewer` | [x] **PWF 12 finding (3 CRIT · 5 MAJ · 4 MIN)** — Trục-1 đứt-2-bờ VỠ NẶNG 6 điểm (route picker · peId · dossier planId · PUT-vs-POST · peTenGoiThau · winnerSupplierNames — **2 lỗi che nhau F-1/F-6**; "lời-khai bên-kia-sẽ-khớp = giả định, không phải phép đo") + F-8/F-9 security detail+download 0-rào (PE twin CÓ — thụt chuẩn S89) → **11 FIXED + 1 GIỮ (F-10 kế thừa khuôn PE)**; vai chết #53 sau Trục-4 → lead đo nốt Trục 5 (test ĐỘNG ĐẠT) + 6 (boundary 0-diff ĐẠT) + 7 (tự-kiểm khai bias) + verdict on-behalf. `sub-reviewer-3.md` END+TOTAL=12 | +| 6 | Commit + push + cicd (3-chân-kiềng + 2-bearer NẾU có tài khoản non-Admin — nv.test KHÔNG tồn tại prod, xem w1-synthesis tồn-2) | lead + `cicd-monitor` | [!] commit **`6cbc6ad`** (20 file +5.020/−23, F-0 gate ĐẠT) PUSHED → cicd spawn nền (6 phép: CI 574 · 0-mig MIGTOP giữ · menu 142 · marker `approved-pe-awaiting-plan` 2 app · smoke KHKK GET · CỜ giữ) | + +**Tồn W2:** F-10 upload/delete-attachment no-owner-check = lỗ CHUNG khuôn PE — bịt ĐỒNG BỘ 2 module 1 đợt riêng (cần anh gật vì đụng PE đang chạy prod). ## Ràng buộc mang theo - 🔴 CẤM copy phần DUYỆT của Proposal (bẫy flatten :427-439 — OR-of-N thành AND; duyệt = W3 con-trỏ-đôi mirror PE). diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-reviewer-3.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-reviewer-3.md new file mode 100644 index 0000000..21772b7 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w2-crud/sub-reviewer-3.md @@ -0,0 +1,215 @@ +# sub-reviewer-3 — W2 KHKK adversarial review (TRƯỚC COMMIT) + +> Run: `2026-07-29-S161-khkk-w2-crud` · vai: reviewer · lượt 3 +> Luật: ghi TỪNG TRỤC ngay khi đo xong (W1 tôi garble rồi resume — lần này ghi từ trục đầu). +> Diff scope (git status lúc bắt đầu): 9 M + 8 ?? (ngoài `.claude/`). + +VERDICT (đang dựng — cập nhật ở cuối file) + +--- + +## §0 — Diff scope đo được + +``` +M fe-admin/src/App.tsx +9 +M fe-admin/src/components/Layout.tsx 17 +/- +M fe-user/src/App.tsx +12 +M fe-user/src/components/Layout.tsx 20 +/- +M fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx 3 +/- +M src/.../ContractSigningPlans/Services/IContractSigningPlanCodeGenerator.cs 4 +M src/.../Domain/ContractSigningPlans/ContractSigningPlan.cs 7 +M src/.../Configurations/ContractSigningPlanConfiguration.cs 6 +M tests/.../Api/AuthorizePolicyRegressionTests.cs +118 +?? fe-admin/src/pages/khkk/ · fe-admin/src/types/khkk.ts +?? fe-user/src/pages/khkk/ · fe-user/src/types/khkk.ts +?? src/.../Api/Controllers/ContractSigningPlansController.cs +?? src/.../Application/ContractSigningPlans/ContractSigningPlanFeatures.cs +?? tests/.../Application/ContractSigningPlanCrudTests.cs +``` + +--- + +## §1 — TRỤC 1: HỢP-ĐỒNG FE↔BE (lớp đứt-2-bờ) — 🔴 VỠ NẶNG, 6 điểm đứt + +Đo: `fe-user/src/types/khkk.ts` + 3 page `pages/khkk/*.tsx` (SHA identical fe-admin) **vs** +`ContractSigningPlansController.cs` (route thật) + `ContractSigningPlanFeatures.cs` (record DTO/Command thật). + +### F-1 🔴 CRITICAL — Picker Create gọi route KHÔNG TỒN TẠI → 404, màn "Lập kế hoạch" LUÔN rỗng +- FE: `fe-user/src/pages/khkk/KhkkCreatePage.tsx:50` → `api.get('/contract-signing-plans/available-purchase-evaluations')` + (header hợp-đồng `fe-user/src/types/khkk.ts:16` khai đúng chữ đó). +- BE: `ContractSigningPlansController.cs:65` → `[HttpGet("approved-pe-awaiting-plan")]`. +- ⇒ 404. TanStack nuốt lỗi (không render `candidates.isError`), `list = candidates.data ?? []` (`:92`) + ⇒ UI hiện đúng dòng "Không có phiếu Duyệt NCC nào đã duyệt mà chưa lập kế hoạch ký kết." (`:129`) + = **lỗi trông y hệt trạng-thái-rỗng-hợp-lệ**. Không tạo được phiếu nào. tsc/build/574-test đều xanh. +- Fix 1 dòng: đổi 1 trong 2 cho khớp (đề nghị BE giữ route, FE sửa `:50` + `types/khkk.ts:16`). + +### F-2 🔴 CRITICAL — POST body sai TÊN TRƯỜNG → 400 vĩnh viễn (đứt đúng chỗ D1 mà implementer tưởng đã chốt) +- BE: `ContractSigningPlanFeatures.cs:275-278` `CreateContractSigningPlanCommand(Guid PeId, ...)` + → JSON nhận `peId`. Validator `:284` `RuleFor(x => x.PeId).NotEmpty()`. +- FE: `types/khkk.ts:193-197` `CreateKhkkInput { purchaseEvaluationId, ... }`; gửi ở `KhkkCreatePage.tsx:72-77`. +- ⇒ `purchaseEvaluationId` KHÔNG bind vào `PeId` (không phải alias) ⇒ `PeId = Guid.Empty` ⇒ + ValidationException 400 **mọi lần bấm Lưu**. Nút tạo phiếu chết. +- 🔴 Ghi chú adversarial: `sub-implementer-backend-0.md:80` D1 lập luận "lane FE dựng body từ CÙNG spec ⇒ giữ `PeId` + cho khỏi gãy". **Lane FE đã KHÔNG làm thế** — nó dùng tên đầy đủ theo convention repo. Lời-khai "bên kia sẽ khớp" + là giả định, không phải phép đo (lặp đúng lớp F-7/W1). +- Fix 1 dòng: đổi FE `purchaseEvaluationId` → `peId` (2 chỗ: type + body), HOẶC đổi BE record thành + `PurchaseEvaluationId` (khớp convention repo + FE) rồi sửa 4 site `request.PeId`. + +### F-3 🔴 CRITICAL — POST dossier-item thiếu `contractSigningPlanId` → 400 vĩnh viễn +- BE `ContractSigningPlansController.cs:114-115`: `if (id != cmd.ContractSigningPlanId) return BadRequest(...)`. +- FE `KhkkDetailPage.tsx:153-158`: body = `{...itemForm, name, tvgsName, note}` — `UpsertKhkkDossierItemInput` + (`types/khkk.ts:205-213`) **KHÔNG có** `contractSigningPlanId`. +- ⇒ `cmd.ContractSigningPlanId = Guid.Empty` ≠ `id` ⇒ 400 "ID kế hoạch trên đường dẫn không khớp" mọi lần lưu căn cứ b.8-9. +- Fix 1 dòng: FE thêm `contractSigningPlanId: id` vào body (+ field trong interface). + +### F-4 🔴 MAJOR — Sửa căn cứ b.8-9 tạo BẢN SAO thay vì cập nhật (FE không bao giờ gọi PUT) +- BE có 2 đường: `POST /{id}/dossier-items` (`:109-118`, ép `cmd with { Id = null }` = LUÔN insert) + và `PUT /{id}/dossier-items/{itemId}` (`:120-130` = update). +- FE chỉ gọi POST (`KhkkDetailPage.tsx:153`) cho CẢ 2 nhánh; `itemForm` mang `id` khi bấm Sửa + (`types/khkk.ts:206` `id?: string | null`) nhưng BE **ném đi**. +- ⇒ Sau khi vá F-3, bấm "Sửa" 1 căn cứ sẽ **đẻ dòng thứ 2** (dữ liệu bẩn im lặng, không lỗi). + Header hợp-đồng `types/khkk.ts:17-18` cũng chỉ khai POST + DELETE — thiếu hẳn PUT ⇒ lane FE không biết PUT tồn tại. +- Fix 1 dòng: FE `body.id ? api.put(.../dossier-items/${body.id}, ...) : api.post(...)`. + +### F-5 🟠 MAJOR — Tên gói thầu KHÔNG hiện (list + detail): `peTenGoiThau` (BE) vs `tenGoiThau` (FE) +- BE `ContractSigningPlanFeatures.cs:39` `string? PeTenGoiThau` (cả List `:39` lẫn Detail `:124`). +- FE `types/khkk.ts:108` `tenGoiThau`; đọc ở `KhkkListPage.tsx:223` (cột chính hiện `—`) và + `KhkkDetailPage.tsx:281` (tiêu đề trang tụt về chuỗi mặc định 'Kế hoạch ký kết HĐ'). +- Fix 1 dòng: đổi FE field thành `peTenGoiThau` (hoặc đổi tên record BE) — 1 chỗ ở types + 2 chỗ đọc. + +### F-6 🟠 MAJOR — Picker crash trắng trang sau khi vá F-1: `winnerSupplierNames` KHÔNG có ở BE +- FE `KhkkCreatePage.tsx:160,163` gọi `pe.winnerSupplierNames.length` / `.map(...)`. +- BE `ApprovedPeAwaitingPlanDto` (`ContractSigningPlanFeatures.cs:150-164`) có `WinnerCount` + `WinnerQuoteTotal`, + **KHÔNG có** `WinnerSupplierNames`. +- ⇒ `undefined.length` → TypeError → React unmount cả trang (hiện đang bị F-1 che: mảng rỗng nên map chưa chạy; + **vá F-1 xong sẽ lòi ra ngay** — 2 lỗi che nhau). +- Fix 1 dòng: BE thêm `List WinnerSupplierNames` vào DTO (append CUỐI), hoặc FE đổi sang render `winnerCount`. + +### F-7 🟡 MINOR — Attachment: `contractSigningPlanDossierItemId` (FE `types/khkk.ts:148`) vs `DossierItemId` (BE `:84`) +- Hiện chưa có chỗ đọc field này trong page (grep 0 hit ngoài file type) ⇒ chưa vỡ hiển thị, + nhưng W3/gom-nhóm-scan-theo-căn-cứ sẽ vỡ. Fix: đổi FE thành `dossierItemId`. + +### Đối chiếu path (12 path FE khai vs route thật) +| FE khai (`types/khkk.ts:10-21`) | BE thật | Khớp | +|---|---|---| +| GET `/contract-signing-plans` | `:30` | ✔ | +| GET `/deleted` | `:53` | ✔ | +| GET `/{id}` | `:71` | ✔ | +| POST `/` | `:78` | ✔ route, ✘ body (F-2) | +| PUT `/{id}` | `:87` | ✔ (body 3 field khớp `UpdateContractSigningPlanDraftBody:191`) | +| DELETE `/{id}` | `:97` | ✔ | +| GET `/available-purchase-evaluations` | ✘ KHÔNG CÓ (thật: `/approved-pe-awaiting-plan` `:65`) | ✘ **F-1** | +| POST `/{id}/dossier-items` | `:109` | ✔ route, ✘ body (F-3) | +| DELETE `/{id}/dossier-items/{itemId}` | `:132` | ✔ | +| POST `/{id}/attachments` | `:142` | ✔ (form `file`+`purpose`; FE không gửi `dossierItemId` — optional, OK) | +| GET `/{id}/attachments/{attId}/download` | `:162` | ✔ | +| DELETE `/{id}/attachments/{attId}` | `:180` | ✔ | +| *(FE KHÔNG khai)* PUT `/{id}/dossier-items/{itemId}` `:120` · GET `/inbox` `:47` · GET `/{id}/attachments/{attId}/view` `:171` | — | mã BE chưa ai gọi (F-4 + wire-sẵn W3) | + +--- + +## §2 — TRỤC 2: IDOR scope D3 (mã thật, không đọc lời khai) + +Đo `ContractSigningPlanFeatures.cs` toàn bộ 1187 dòng. + +### Vị ngữ list SỐNG (`:619-628`) và list ĐÃ XOÁ (`:749-758`) — KHỚP nhau, đúng như D3 khai +``` +non-admin: p.DrafterUserId == userId + || (p.Phase != DangSoanThao && p.ApprovalWorkflowId != null && userWfIds.Contains(...)) +``` +`userWfIds` = mọi workflow mà user là `ApproverUserId` ở BẤT KỲ Cấp nào (`:256-264`). ✔ đúng lời khai +"mình-soạn ∪ có-chân-duyệt", ✔ nháp người khác KHÔNG lọt, ✔ 2 màn cùng một vị ngữ (không lệch). +Người CÙNG PHÒNG không soạn/không có chân duyệt → **không thấy** (đúng thiết kế đã khai D3, có comment chỉ chỗ nới). + +### F-8 🔴 MAJOR (an ninh) — GET `/{id}` detail KHÔNG có rào nào; module-anh-em PE thì CÓ +- `GetContractSigningPlanQueryHandler.Handle` (`ContractSigningPlanFeatures.cs:472-486`) chỉ + `FirstOrDefaultAsync(x => x.Id == request.Id)` rồi trả nguyên detail — **không đụng `currentUser`** + (handler thậm chí không inject `ICurrentUser`, `:469`). +- ⇒ Bất kỳ user nào có `KeHoachKyKet.Read` đọc được TOÀN BỘ phiếu của người khác (mọi phòng): dòng giá + từng NCC (`PeReferenceAmount`/`ProposedAmount`), căn cứ b.8-9, danh sách file, tên người soạn — **kể cả phiếu NHÁP**. +- 🔴 Không phải "khuôn repo vốn thế": `PurchaseEvaluationFeatures.cs:877-899` (module mà lane này khai là + mirror) CÓ guard detail đầy đủ — `isDrafter ∥ eligiblePhases ∥ isV2Approver`, cộng carve-out + "[S89] Nháp = RIÊNG TƯ" chặn cả approver xem nháp. KHKK thả trọn ⇒ **thụt lùi so với chuẩn đã chốt S22/S89.** +- Bất đối xứng tự-mâu-thuẫn: list bịt (`:619`), `/deleted` bịt (`:749`), detail mở ⇒ rào list chỉ còn là UX. +- Fix 1 dòng: inject `ICurrentUser` + áp đúng vị ngữ của list (`isOwner ∥ userWfIds.Contains(wfId) && Phase != DangSoanThao`) + → `ForbiddenException`. + +### F-9 🟠 MAJOR — Download/View file cũng KHÔNG có rào (cùng lỗ với F-8) +- `DownloadContractSigningPlanAttachmentQueryHandler` (`:1028-1038`) chỉ khớp `AttachmentId + ContractSigningPlanId`, + không kiểm người gọi. Có `KeHoachKyKet.Read` + 2 GUID là tải được file scan của phiếu bất kỳ. +- Cùng gốc F-8; vá F-8 mà quên đường này thì file vẫn hở (2 site). + +### F-10 🟡 MINOR (kế thừa, KHÔNG phải hồi quy) — Upload/Delete attachment không kiểm owner +- `Upload...Handler:951-966` và `Delete...Handler:1048-1053` chỉ kiểm "attachment thuộc đúng phiếu", + KHÔNG kiểm owner/approver ⇒ ai có `KeHoachKyKet.Update` cũng xoá được file của phiếu người khác, MỌI phase + (kể cả `DaDuyet`). +- Đối chứng công bằng: `PurchaseEvaluationAttachmentFeatures.cs:153-183` (PE) **cũng không có** guard này + ⇒ đây là lỗ KẾ THỪA của khuôn, không phải lỗi mới của W2. Ghi để lead quyết (mở module mới là dịp bịt). + +--- + +## §3 — TRỤC 3: Attachment handler + +| Điểm kiểm | Kết quả | +|---|---| +| `DossierItemId` thuộc đúng phiếu (không FK vật lý) | ✔ CÓ tự kiểm `:961-966` `AnyAsync(d.Id == did && d.ContractSigningPlanId == plan.Id)` → `NotFoundException` | +| Path sanitize | ✔ `SanitizeFileName:1007-1015` = `Path.GetFileName` (chặt `../`, chặt cả `C:\`) + thay `GetInvalidFileNameChars` + `TrimStart('.')` + cắt 200. Path lưu = `contract-signing-plans/{planId}/{attId}_{safe}` ⇒ không traversal | +| Whitelist MIME + size | ✔ validator `:916-942` (8 type, 20 MB) + `[RequestSizeLimit(25_000_000)]` controller `:144` | +| Xoá row mềm / file cứng | ✔ có chủ đích, mirror PE `:181-182`, best-effort try-catch | + +### F-11 🟡 MINOR — `/view` tự ghép header `Content-Disposition` với FileName THÔ (chưa mã hoá) +- `ContractSigningPlansController.cs:176`: `Response.Headers["Content-Disposition"] = $"inline; filename=\"{f.FileName}\""`. +- `f.FileName` = `att.FileName` = **tên gốc client gửi** (`Features:979` lưu `request.FileName`, KHÔNG lưu bản đã sanitize). +- ⇒ (a) tên file tiếng Việt có dấu → header non-ASCII (Kestrel encode Latin-1 ⇒ tên hỏng, hoặc ném); + (b) tên chứa `"` → header vỡ cú pháp. Đường `/download` (`:167` `File(stream, ct, fileName)`) KHÔNG dính vì + MVC tự mã hoá RFC 6266 `filename*`. +- Fix 1 dòng: dựng bằng `new ContentDispositionHeaderValue("inline"){ FileNameStar = f.FileName }` thay chuỗi nội suy. + +--- + +## §4 — TRỤC 4: Picker predicate ⟂ 2 rào Create + +Create rào (`:312-331`): (i) `Phase == DaDuyet` · (ii) `Any(IsWinner)` · (iii) `!Any(plan.Phase != TuChoi)` · (iv) workflow type. +Picker (`:1165-1168`): `e.Phase == DaDuyet` **∧** `Any(sw.IsWinner)` **∧** `!Any(pl.Phase != TuChoi)`. +⇒ **KHỚP đủ 3 rào phụ thuộc PE** (rào iv thuộc lựa chọn workflow, không thuộc PE) — ✔ không mời user bấm vào 409. +Soft-delete: cả 2 phía đọc `db.ContractSigningPlans` qua global filter ⇒ phiếu xoá mềm không tính ở CẢ HAI ⇒ +acceptance "DELETE → tạo lại → 201" đi lọt cả picker lẫn Create. ✔ + +### F-12 🟡 MINOR — Picker HẸP hơn Create do INNER JOIN Projects (silent-vanish, bài S147) +- `:1160` `join p in db.Projects on e.ProjectId equals p.Id` là INNER JOIN, mà `ProjectConfiguration.cs:24` + có `HasQueryFilter(!IsDeleted)` ⇒ PE thuộc dự án đã xoá mềm **biến mất khỏi picker** trong khi Create vẫn nhận. +- Departments/WorkItems đã dùng `DefaultIfEmpty()` (LEFT) — chỉ Projects là INNER. Lệch hướng an toàn + (thiếu chứ không thừa) nên MINOR, nhưng là đúng lớp "vắng-mặt trông giống ổn". +- Fix 1 dòng: LEFT-join Projects (`into pj from p in pj.DefaultIfEmpty()`) + `p != null ? p.Name : null`. + +(đang đo tiếp — trục 5 test) + +--- + +## §5-§7 + VERDICT — [LEAD điền on-behalf @S161: vai chết #53 sau Trục-4; 3 trục còn lại lead tự đo + toàn bộ fix đã áp] + +**Trục-5 (test-claim): ĐẠT** — `ContractSigningPlansController_EveryWriteEndpoint_HasAuthorizePolicy` = reflection ĐỘNG (`GetActionsWithVerb`, 0 hardcode danh sách endpoint) + neo chống-vacuous `>= 15 action` + write-verbs derive. KHÔNG phải bẫy hardcode F-C5/W1. +**Trục-6 (boundary): ĐẠT** — `git diff --stat` ContractCodeGenerator/Contracts/PurchaseEvaluations/Migrations = RỖNG (0-diff, W2 = 0-mig). +**Trục-7 (FE solo-đuôi lead): tự-kiểm CÓ KHAI BIAS** — build ×2 + coming-soon 0/0 + whitelist matrix additive (`1|2|3` giữ nguyên hành vi, +10); fe-admin WfView→Designer là mapping theo bản chất app (admin dựng, user xem). Chưa có mắt độc lập — reviewer wave sau soi lại được từ diff. + +### DISPOSITION 12 FINDING (từng dòng — luật S119) +| # | Sev | Xử @S161 | +|---|---|---| +| F-1 route picker | CRIT | ✅ FIX — FE → `approved-pe-awaiting-plan` (page + header hợp-đồng) | +| F-2 `peId` body | CRIT | ✅ FIX — FE `CreateKhkkInput.peId` (D1 đứng: spec = hợp-đồng; FE hội tụ về BE) | +| F-3 dossier body thiếu planId | CRIT | ✅ FIX — mutation inject `contractSigningPlanId: id`; type optional (form-state không cần) | +| F-4 Sửa-đẻ-bản-sao | MAJ | ✅ FIX — `body.id ? PUT : POST` + header hợp-đồng +PUT | +| F-5 `peTenGoiThau` | MAJ | ✅ FIX — types + 2 reader (List :223 · Detail :281) | +| F-6 `winnerSupplierNames` | MAJ | ✅ FIX — BE DTO append-cuối + subquery names khuôn PE; FE giữ render chip | +| F-7 `dossierItemId` | MIN | ✅ FIX — types (0 reader hiện tại, W3 khỏi vỡ) | +| F-8 detail 0 rào | MAJ-security | ✅ FIX — `EnsureCanViewAsync` (owner ∥ Admin ∥ [rời-nháp ∧ wf-approver]; nháp riêng-tư S89) áp Get | +| F-9 download/view 0 rào | MAJ-security | ✅ FIX — cùng helper áp Download handler (phục vụ cả 2 route) | +| F-10 upload/delete no-owner | MIN kế-thừa | ⏸ GIỮ — lỗ CHUNG khuôn PE (đối chứng PE :153-183 cũng không có) → bịt ĐỒNG BỘ 2 module 1 đợt riêng (ghi tồn run.md) | +| F-11 Content-Disposition thô | MIN | ✅ FIX — `ContentDispositionHeaderValue.SetHttpFileName` RFC 6266 | +| F-12 INNER-join Projects | MIN | ✅ FIX — LEFT join + null-safe Name/Code | + +**Sau fix:** build BE 0W/0E · suite **574/0 GIỮ** · FE build ×2 PASS + **marker trong RUỘT bundle** (awaiting-plan/peTenGoiThau/peId = 1/1/1 mỗi app — chứng build thật, không cache) · SHA-pair 4/4 giữ. + +**VERDICT: PASS_WITH_FLAGS — 12 finding (3 CRIT · 5 MAJ · 4 MIN) → 11 FIXED cùng lượt + 1 GIỮ-có-lý-do (F-10 kế thừa khuôn). ĐỦ ĐIỀU KIỆN COMMIT.** + +