177 lines
15 KiB
Markdown
177 lines
15 KiB
Markdown
# 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**
|
||
|
||
- [x] Đ2-W1 — BE (xóa-by-approver + list deleted + menu seed + changelog)
|
||
- [x] Đ2-W2 — FE (nút xóa + 3 mục menu + màn Đã xóa)
|
||
- [x] Đ2-W3 — test + reviewer → deploy — `reviewer-diff-dot2.md` FAIL 12 FLAG (H1 DTO thiếu field) → lead vá → deploy `4464a46`
|
||
|
||
> 🔧 Sau đợt 2, một fix-nhanh UAT nữa đã đẩy: **`f4494cf`** — click "Danh sách" lại highlight "Đã duyệt" (`phase` vừa là bộ-lọc vừa là danh-tính điều-hướng ⇒ `queryMatches` strip cả 2 vế). Vá 2 chỗ ×2 app. ~~**cicd verify cho commit này CHƯA chạy.**~~ 🔴 **ĐÍNH CHÍNH @S159 (đo đĩa):** dòng gạch trên **STALE** — `f4494cf` = Gitea **Run #418 success** (07-27 14:42:35, bảng `cicd-verify-s157-diadiem.md:38`), bundle `BK8w3QxU`/`DYnxSI0T` khớp baseline STATUS S156 `:58`; cộng phép đo **6/6 của lead** @S156 (session log `:17`). Khai giới hạn: KHÔNG có artifact cicd-verify **riêng** cho commit này — 2 vết gián tiếp, đủ lật ledger, không đủ nói "cicd-monitor đã chấm riêng".
|
||
|
||
- [x] **Harvest — `implement-synthesis.md`** (retro @S159, đóng Sàn-3 ① dạng-4)
|
||
> 🔧 Ledger này tụt hậu so WAL tới `/tiep` @S156 mới đối-chứng ra (`ctx-verifier` 🚩3) — 3 dòng trên đã `[x]` từ lúc `4464a46` lên prod.
|