Files
solution-erp/.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/run.md
2026-07-29 08:58:23 +07:00

177 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 **328** (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****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`**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.