# 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-<role>-<i>.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ễ.

- [x] S4-đợt1 — hmw 5 wave + lead-vá · **DEPLOY `b1bae77` · CICD PASS 6/6** (mig ở TOP history prod, cột khớp, smoke 8/8, PUT 401, bundle rotate ×2)

---

# ĐỢT 2 — nút xóa + menu + màn "Đã xóa" (anh lệnh 2026-07-27)

**Phạm vi:** checklist PHẦN I mục **3–28** (trừ mục 3 đã gộp F-11 đợt 1).

## 🔴 HỢP-ĐỒNG LIÊN-LANE — chốt TRƯỚC khi phóng (bài học đợt 1: 2 nửa build sạch mà không nối được)

Mọi tên dưới đây là **CANONICAL**. BE phải hiện thực ĐÚNG tên này; FE phải gọi ĐÚNG tên này. Lệch một ký tự = silent drop, cả hai build vẫn xanh.

### API
| Việc | Method + Route | Authz |
|---|---|---|
| Xóa phiếu **khi đang duyệt** (đường MỚI) | `DELETE /api/purchase-evaluations/{id}/by-approver` | `[Authorize(Policy = "PurchaseEvaluations.Delete")]` |
| Xóa **nháp** (đường CŨ) | `DELETE /api/purchase-evaluations/{id}` — **GIỮ NGUYÊN, KHÔNG gắn policy** | chỉ `[Authorize]` trần |
| List **phiếu đã xóa** | `GET /api/purchase-evaluations/deleted?type=&page=&pageSize=` | `[Authorize(Policy = "PurchaseEvaluations.Read")]` |

### Menu key (factory trong `MenuKeys.cs`, KHÔNG vào `MenuKeys.All`)
- `PurchaseEvaluationApproved(code)` → `Pe_{code}_Approved` — nhãn **"Đã duyệt"**
- `PurchaseEvaluationDeleted(code)` → `Pe_{code}_Deleted` — nhãn **"Đã xóa"**
- Đổi nhãn `Pe_{code}_Pending`: `"Duyệt"` → **"Đang duyệt"** qua `labelBackfill`

### Route FE (KHÔNG page mới cho 2 mục đầu — tái dùng `PurchaseEvaluationsListPage`)
- Đang duyệt → `/purchase-evaluations?type={N}&pendingMe=1` *(y như cũ, chỉ đổi nhãn menu)*
- Đã duyệt → `/purchase-evaluations?type={N}&phase=7` *(0 route mới, `phase=` đã thông 3 tầng)*
- Đã xóa → `/purchase-evaluations?type={N}&deleted=1` *(view mới, gọi endpoint `/deleted`)*

## Ràng buộc CỨNG mang từ spec + review đợt 1
- **7a**: đường xóa **nháp** GIỮ NGUYÊN không policy — gắn vào là `Drafter` mất quyền xóa nháp của chính mình (đo thật: 11/13 role `CanDelete=0`)
- **7c**: phải cấp `CanDelete` cho role approver đích (vd `Procurement`) — seeder `:2120-2126` **skip-if-exists** và **không bao giờ chạm `CanDelete`** ⇒ sửa seeder KHÔNG vá được prod, phải tick tay hoặc viết upgrade-path
- **B2**: guard xóa-khi-duyệt = đúng lượt **∧** `matchingLevel.AllowApproverDelete` (**PER-ROW**, dùng khuôn `:859`, KHÔNG dùng `g.Any(...)` `:1182`)
- **8a**: `IgnoreQueryFilters()` **GỠ** filter chứ không ĐẢO ⇒ **BẮT BUỘC** kèm `.Where(x => x.IsDeleted)`
- **8b**: handler mới **mặc định 0 rào** ⇒ phải sao khối IDOR `PurchaseEvaluationFeatures.cs:596-617`
- **D3**: `IgnoreQueryFilters` chỉ ở **đúng 1 handler mới**; CẤM helper dùng chung trả `IQueryable`, CẤM sửa `Configuration:84`, CẤM cờ `includeDeleted` trên query list hiện có
- **12**: thêm key vào **CẢ HAI** danh sách permission seed `:2092-2096` **và** `:2492-2496`
- **11**: đổi nhãn phải qua `labelBackfill` — sửa `tree.Add` là **vô tác dụng** trên DB đang chạy
- **14a**: `PeWorkflowPanel` nhận đúng 3 prop `{evaluation, readOnly, onApproved}` — **KHÔNG có `onDelete`**; phải THÊM prop + mutation
- **19**: `PeWorkflowPanel.tsx` phải giữ **md5 giống nhau ×2 app** sau khi sửa
- Nhân bản: menu sinh trong `foreach PurchaseEvaluationTypeCodes` ⇒ mỗi mục mới **× số loại phiếu**

- [ ] Đ2-W1 — BE (xóa-by-approver + list deleted + menu seed + changelog)
- [ ] Đ2-W2 — FE (nút xóa + 3 mục menu + màn Đã xóa)
- [ ] Đ2-W3 — test + reviewer → deploy
