# run — PE: nút XÓA phiếu ở màn DUYỆT (approver-side) - **run-id:** `2026-07-27-S155-pe-delete-approver` - **phiên:** S155 (phiên-LOGIC L7, window 2) - **pipeline anh lệnh:** 2 Invest → file-chi-tiết + checklist → review spec → hmw Opus 5 MAX - **mode:** RUN-TRACE (≥3 task) — sub ghi full-detail vào CHỈ `sub--.md` của mình ## Nguồn yêu cầu (ảnh chat anh gửi — UAT thật, không phải giả định) Phiếu mẫu: `PE/2026/A/046` · Duyệt NCC · dự án **FLOCK03** · hạng mục **MAT-16 "16 Mat Khác"** · gói thầu **"16 Mat - Băng cản nước"** · trạng thái **Đã gửi duyệt** · có cờ **GẤP (PRO)**. Workflow đang chạy: Bước 1 (Phòng Cung ứng) — Cấp 2, NV duyệt **Bùi Lê Thủy Trà**, "Đến lượt bạn duyệt". Bước 2 có Cấp 2 "Duyệt thay CEO". Bước 3 Ban Giám đốc, kết thúc tại Cấp 2. `Lịch sử duyệt (1)`. Khối **HÀNH ĐỘNG** hiện chỉ có: `✓ Duyệt` · `← Trả lại` — **thiếu Xóa**. Banner đang hiện: *"Bạn được phép chỉnh sửa Hạng mục / NCC / Báo giá (workflow bật mode Approver edit)"*. **Lời user verbatim (Tra Sol):** - "@Kenny Chỗ duyệt - tạo giúp em nút xóa với" - "Vì mấy bạn bắt sai cái gói thầu" → "nên phải cho quyền xóa cái phiếu đó luôn" - "chứ lỡ bấm sai cái gói thầu / quay lại không được / **phải xóa thì nó mới ko có lũy kế lên**" **Lời anh (owner) verbatim:** "Hic hôm trước có cái chỗ cho huy / mọi người nói là ko cần cái đó / giờ lại cần" ⇒ 🔴 giả-thuyết cần KIỂM: đường xóa/thu-hồi **đã từng tồn tại rồi bị gỡ**. Nếu đúng → khôi phục rẻ hơn dựng mới, và phải tìm ra **lý do gỡ** để không tái phạm. ## Vì sao đây là lỗi SỐ LIỆU, không phải UX CLAUDE.md S134: `ComputePendingAsync` complementary — **phiếu ChoDuyet có winner được tính vào lũy kế TẠM TÍNH**. Phiếu bấm sai gói thầu đang treo ở bước duyệt ⇒ **đang ăn ngân sách hạng mục sai**. "Trả lại" đẩy về `TraLai=98` chứ không triệt tiêu ⇒ số tạm tính vẫn lệch. Đó chính là điều Tra Sol mô tả bằng chữ "lũy kế lên". ## Stages - [ ] **S1 — 2 Invest song song** (BE-slice + FE/authz-slice) → `sub-invest-be-1.md` · `sub-invest-fe-2.md` - [ ] **S2 — file chi tiết + checklist** (lead tổng hợp) → `spec-pe-delete-approver.md` - [ ] **S3 — review spec** (reviewer, adversarial) - [ ] **S4 — hmw Opus 5 MAX** thực thi ## Ràng buộc mang theo mọi stage - Governance L7 **vẫn treo** 22 FLAG chờ (42)(43)(44) — việc này **song song**, KHÔNG đóng thay. - Soft delete là quy ước sẵn có (`AuditableEntity`: `IsDeleted`/`DeletedAt`/`DeletedBy`) — spec phải nói rõ dùng cái này hay thu-hồi-về-Nháp. - Quyền = **2 tầng độc lập** (bài học S118 / gotcha #82): display-layer menu `CanRead` ⟂ API-authz `[Authorize(Policy)]`. Ẩn nút ≠ đóng API. - Xóa phải có **vết** (changelog/audit) — phiếu đã có `Lịch sử duyệt (1)`. ## taskList snapshot 1. invest BE: entity/state-machine/endpoint-xóa/authz/lũy-kế/changelog + git-khảo-cổ "chỗ cho huy" 2. invest FE: màn duyệt (component khối HÀNH ĐỘNG) + permission matrix `Pe_*` + nơi PE đang xóa được (nếu có) + dấu vết UI đã gỡ --- ## 🔴 OWNER CHỐT (2026-07-27, anh trả lời 3 câu + ảnh menu) **(1) AI được xóa:** *"Người đứng đầu phòng đc xóa khi đến lượt họ."* → 2 điều kiện **AND**: là người đứng đầu phòng ∧ đang tới lượt duyệt của họ. CHẶT HƠN "ai tới lượt cũng xóa được". **(2) Kiểu xóa:** *"Xóa mềm."* → dùng đúng cơ chế `AuditableEntity.IsDeleted` sẵn có (qua `AuditingInterceptor`), KHÔNG dựng mới. **(3) Ngữ nghĩa + phiếu đã có lượt duyệt:** *"Tính năng xóa mềm này sinh ra để phục vụ chuyện đó mà, vì nó đang trong quá trình duyệt và đang ăn lũy kế nên phải có nút xóa -> nhưng xóa là chuyển trạng thái đưa xuống mục đã xóa và ko ăn lũy kế nữa."* ⇒ **KHÔNG thêm rào cho phiếu đã có `Lịch sử duyệt (n)`** — đang-duyệt CHÍNH LÀ ca cần cứu. Câu hỏi "đã duyệt rồi có xóa được không" = **ĐÃ TRẢ LỜI: CÓ**. ⇒ Xóa = chuyển trạng thái + xuất hiện ở **mục "Đã xóa"** + **rớt khỏi lũy kế**. **(4) 🆕 RESTRUCTURE MENU (ảnh, ghi chú đỏ chỉ vào mục menu "Duyệt"):** ``` Chỗ này: Duyệt -> Đang duyệt. Thêm : Đã duyệt. Đã xóa. ``` Menu hiện tại dưới `1. Duyệt Nhà Cung Cấp - Thầu phụ (NCC-TP)`: `Luồng duyệt` · `Danh sách` · `Thao tác` · `Duyệt`. Menu sau khi sửa: `Luồng duyệt` · `Danh sách` · `Thao tác` · **`Đang duyệt`** (đổi tên từ `Duyệt`) · **`Đã duyệt`** (MỚI) · **`Đã xóa`** (MỚI). URL màn hiện tại: `/purchase-evaluations?type=1&pendingMe=1` · tiêu đề "Duyệt NCC — Chờ duyệt (8)" · lọc cố định `Đã gửi duyệt`. ### 🔴 Nghịch lý phải xử ở (4) — nối thẳng vào F9/F10 của sub BE Global filter `HasQueryFilter(x => !x.IsDeleted)` (`PurchaseEvaluationConfiguration.cs:84`) + `IgnoreQueryFilters` **0 hit toàn backend** là thứ khiến phiếu xóa **tự rớt khỏi 4 phép cộng** ⇒ cho không ý (3). NHƯNG cùng filter đó làm phiếu xóa **vô hình với MỌI query** ⇒ mục "Đã xóa" **không thể** dựng nếu không mở `IgnoreQueryFilters()`. ⇒ Ràng buộc spec: mở `IgnoreQueryFilters()` cho **ĐÚNG 1 endpoint list "Đã xóa"**, KHÔNG đụng accumulator, KHÔNG mở diện rộng. Đây sẽ là **chỗ đầu tiên trong toàn backend** dùng `IgnoreQueryFilters` ⇒ đáng 1 dòng checklist riêng + test. ### ❓ Chưa giải: "người đứng đầu phòng" map vào đâu trong schema? Ảnh cho thấy 1 Cấp có NHIỀU người (Phòng Cung ứng — Cấp 2: Bùi Lê Thủy Trà, Trần Xuân Lưu, Lê Trần Đăng Trường). 3 cách hiểu khả dĩ, ra 3 đoạn code KHÁC nhau: (a) Cấp cao nhất trong Bước (Level order) · (b) field trưởng phòng trên `Department` (Mig 51 có `ParentId` org-tree) · (c) role/permission riêng. ⇒ giao sub invest #3 tìm, CẤM đoán. ## Stages (cập nhật) - [~] S1 — 2 Invest BE/FE: đã cứu từ đĩa Q1-Q3 (BE) + Q1,Q2,Q4 (FE); **cả 2 dính #53**, đã SendMessage-resume làm nốt - [ ] S1-bis — Invest #3 lát cắt MỚI: menu restructure + "Đã xóa" list + "người đứng đầu phòng" - [ ] S2 — spec + checklist · [ ] S3 — reviewer · [ ] S4 — hmw Opus 5 MAX --- ## 🔴 OWNER CHỐT — BỔ SUNG (2026-07-27, lượt 2) **(5) Quyền xóa** = cờ **`AllowApproverDelete` per-Cấp-duyệt** (AskUser). Owner bác phương án `Department.ManagerUserId`. 🔴 KHAI: đổi nghĩa so với chữ đầu *"người đứng đầu phòng"* → thành *"cấp nào admin tick"*. Owner chọn với trade-off ghi rõ trên bàn. KHÔNG phải hiểu nhầm. **(6) Màn "Đã xóa" = CHỈ XEM**, chưa làm khôi phục (AskUser). **(7) 🆕 QUY TRÌNH DUYỆT — sửa TẠI CHỖ vs bắt buộc TẠO MỚI** *(anh, verbatim)*: > *"Chỗ quy trình duyệt -> Cho thêm tính năng thêm người/điều chỉnh quyền -> Chứ ko cần phải tạo mới. Khi nào cần thay đổi quy trình duyệt thì mới bắt buộc tạo mới."* ⇒ Bài toán = **PHÂN LOẠI THAY ĐỔI** thành 2 lớp: - **Lớp AN TOÀN (sửa tại chỗ, giữ nguyên Version):** thêm/bớt NGƯỜI trong một Cấp · điều chỉnh QUYỀN (các cờ F1-F6) - **Lớp PHÁ VỠ (bắt buộc Version mới):** đổi CẤU TRÚC quy trình — thêm/bớt/đổi thứ tự Bước hoặc Cấp 🔗 **Đụng thẳng spec đang viết:** tick cờ **F6 `AllowApproverDelete`** = "điều chỉnh quyền" ⇒ thuộc **lớp AN TOÀN** ⇒ phải sửa được tại chỗ trên workflow đang có phiếu chạy. Nếu không thì mọi lần bật quyền xóa lại đẻ 1 version mới. **Nền đã có (lead peek):** `ApprovalWorkflow.Version` (int, monotonic per `Code`) · `IsActive` · `ActivatedAt` · `IsUserSelectable`; note `ApprovalWorkflowV2AdminFeatures.cs:402` *"Sau UAT khi link với PE/Contract thật cần check usage trước khi delete"* ⇒ usage-check là việc còn treo. - [x] S1-ter — Invest #4 XONG (25 phát hiện) — `sub-invest-wfver-4.md` - [x] S2 — spec XONG 37.490 B — `spec-pe-delete-approver.md` (2 phần, ~50 mục checklist) - [x] S3 — reviewer XONG `PASS-WITH-FLAGS 17 FLAG (4H/8M/5L)` — `reviewer-spec-review.md`; **F.0 KHÔNG bác được** sau 6 đường tấn công; lead đã vá **4H + 8M** vào spec --- ## 🔴 OWNER CHỐT — LƯỢT 3 (sau review) | # | Câu | Anh chốt | Vá FLAG | |---|---|---|---| | 8 | Quyền xóa nháp vs xóa-khi-duyệt | **TÁCH 2 quyền riêng** | H1 | | 9 | Cờ F6 per-người hay per-cấp | **PER-NGƯỜI** (`matchingLevel`, 1 người) | H2 | | 10 | Cờ F5 `AllowApproverFinalize` | **PHÁ VỠ** (nhất quán ngưỡng CEO) | M1 | | 11 | `CeoApprovalThreshold` | **PHÁ VỠ** | W3#12 | | 12 | `Step.DepartmentId` | **AN TOÀN** | W3#13 | | 13 | Cách chạy hmw | **CHIA 2 ĐỢT — F trước, A-E sau** | — | 🎁 **Lời giải gọn cho (8):** đường xóa **nháp GIỮ NGUYÊN không policy** (Drafter không mất gì) · đường xóa-khi-duyệt = **endpoint RIÊNG** mang policy **root có sẵn** `PurchaseEvaluations.Delete` ⇒ **0 key mới** ⇒ `Menu keys=54` / `Policies=216` **không đổi** (khớp M6). ## ĐỢT 1 (hmw lượt này) — hạng mục F + schema F6 **Phạm vi:** checklist mục **1-2** (schema F6 + migration) → **F-1…F-4 + F-1a/b/c** (lệnh Update + validator 2 phép thử + authz) → **F-5/F-5a** (hạng NỬA) → **F-6** (message 403) → **F-10** (vá comment sai) → **F-11** (5 dây tick F6) → test **F-12…F-16**. **KHÔNG làm đợt này:** nút xóa FE · menu restructure · màn "Đã xóa" (mục 3-28) — đợt 2. **Lý do phân đợt:** F.0 chứng minh A-E không chạy nếu thiếu F ⇒ đợt 1 xong là UAT được ngay, biết chắc nền đúng trước khi xây tiếp; blast radius ≥6 module nên tách để truy lỗi dễ. - [ ] S4-đợt1 — hmw fan-out Opus 5 MAX