Files
solution-erp/.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/sub-implementer-backend-4.md
2026-07-27 11:55:41 +07:00

131 lines
7.5 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.

# sub-implementer-backend-4 — WAVE 3: vá 6 test đỏ (hạng mục F, S155)
Run: `2026-07-27-S155-pe-delete-approver` · vai: implementer-backend · ngày 2026-07-27
---
## BƯỚC 1 — ĐO TRƯỚC KHI SỬA (không sửa mù)
Lệnh: `dotnet test SolutionErp.slnx --nologo -v minimal`
```
Failed! - Failed: 6, Passed: 499, Skipped: 0, Total: 505 - SolutionErp.Infrastructure.Tests.dll (net10.0)
```
(Domain: 45 PASS — không đỏ.)
6 đỏ ĐÚNG như bảng lead giao. Thông điệp lỗi THẬT (trích từ output):
| # | Test | Exception THẬT | Ném tại |
|---|---|---|---|
| 1 | `F1c_SafeUpdate_LogsChangelogWithReason_OnlyToUnfinishedPes` | `DbUpdateConcurrencyException: expected to affect 1 row(s), but actually affected 0 row(s)` | `ApprovalWorkflowV2AdminFeatures.cs:806` (`SaveChangesAsync`) |
| 2 | `F12_SafeUpdate_AddsPersonToExistingLevel_RunningPeAcceptsNewApproverImmediately` | y hệt #1 | `:806` |
| 3 | `F16_ProposalWorkflow_SafeUpdate_Succeeds_AndWritesNoPeChangelog` | y hệt #1 | `:806` |
| 4 | `F14_RemoveApproverWithLiveOpinion_BlockedPolitely_NotFkViolation` | `InvalidOperationException: association ... 'ApprovalWorkflowLevel' and 'PurchaseEvaluationLevelOpinion' ... has been severed, but the relationship is ... required` | **UpdateAwDefinitionTests.cs:580** (dòng CHỨNG-NHÂN của CHÍNH TEST, không phải handler) |
| 5 | `F14_F5a_OpinionBelongsToDeletedPe_RemovalAllowed_AndOrphanPurged` | y hệt #4 | handler `:755` `db.ApprovalWorkflowLevels.Remove(gone)` |
| 6 | `ZZ_DIAG_AddLevel` | giàn giáo chẩn-đoán, tự ném `Exception` để in tracker | — |
Dump quyết định (từ ZZ_DIAG):
```
ENTRIES=[ApprovalWorkflowLevel/Modified/Id=60537ebc-...]
TRACKER=[ApprovalWorkflow/Modified || ApprovalWorkflowLevel/Modified || User/Unchanged ×3
|| Role/Unchanged || IdentityUserRole/Unchanged || ApprovalWorkflowStep/Unchanged
|| ApprovalWorkflowLevel/Unchanged]
```
---
## BƯỚC 2 — ROOT CAUSE ĐO ĐƯỢC (không chép giả thuyết)
### LỚP B (3 test) — nguyên nhân KHÔNG phải `AsNoTracking`
Giả thuyết lead: "vùng ghi changelog dùng `db.PurchaseEvaluations.AsNoTracking()` rồi đem cập nhật ⇒ affected 0".
**BÁC BỎ bằng đọc mã:** `:778-783` chỉ `Select(p => new { p.Id, p.Phase })` — projection sang anonymous type, KHÔNG hề gán/ghi lại PE nào. Không có UPDATE nào phát cho `PurchaseEvaluations`. Và F16 (module Đề xuất) thậm chí KHÔNG vào nhánh changelog PE (`:774-776` lọc `DuyetNcc/DuyetNccPhuongAn`) mà vẫn đỏ y hệt ⇒ thủ phạm nằm NGOÀI vùng changelog.
**Root cause THẬT (khớp đúng dump `Added` → `Modified`):**
`src/Backend/SolutionErp.Domain/Common/BaseEntity.cs:5`
```csharp
public Guid Id { get; set; } = Guid.NewGuid(); // ⇐ khoá LUÔN có giá trị ngay khi `new`
```
Handler thêm dòng Cấp mới bằng **navigation-add** trên entity cha ĐANG ĐƯỢC THEO DÕI
(`:730-747`): `step.Levels.Add(new ApprovalWorkflowLevel { ... })`.
Khi `DetectChanges` phát hiện một entity LẠ trong collection của cha đã-tracked, EF quyết
`Added` hay `Modified` bằng heuristic **"khoá đã set ⇒ coi là đã tồn tại"**. Vì `BaseEntity`
gán sẵn `Id = Guid.NewGuid()`, khoá KHÔNG BAO GIỜ là default ⇒ EF chọn **Modified** ⇒ phát
`UPDATE ApprovalWorkflowLevels SET ... WHERE Id = @p` cho một dòng CHƯA TỒN TẠI ⇒ 0 row
`DbUpdateConcurrencyException`.
Bằng chứng đối chứng: `POST` tạo mới (`CreateAwDefinitionCommandHandler`) KHÔNG dính vì nó
`db.ApprovalWorkflows.Add(wf)` — add từ GỐC ⇒ cả đồ thị bị ép `Added`, heuristic không chạy.
**Fix:** đăng ký dòng mới TƯỜNG MINH qua `DbSet.Add` (ép state `Added`, bỏ qua heuristic).
### LỚP A (2 test) — `ExecuteDelete` không đụng change-tracker (đúng hướng lead) + 1 ca KHÁC
`PurgeOrphanOpinionsAsync` (`:870-902`) dùng `ExecuteDeleteAsync` — đi thẳng SQL, **không**
cập nhật change-tracker. Bản sao `PurchaseEvaluationLevelOpinion` mà test đã `Add`+`Save`
vẫn nằm trong tracker ở state `Unchanged`. Tới `db.ApprovalWorkflowLevels.Remove(gone)`
(`:755`), EF chạy cascade-check TỨC THÌ (`CascadeDeleteTiming.Immediate` = mặc định), thấy
dependent còn tracked + FK `Restrict` + non-nullable ⇒ ném `InvalidOperationException`
NGAY TẠI `Remove()` (stack: `InternalDbSet.Remove``StateManager.CascadeDelete`
`HandleConceptualNulls`) — chưa kịp tới SaveChanges.
🔴 **PHÂN BIỆT 2 CA (không được gộp):**
- **F14** (phiếu CÒN SỐNG): handler PHẢI chặn 409. Đo được: guard `:658-665` CHẠY ĐÚNG,
mọi assert phía handler đều xanh. Chỗ nổ là **dòng 580 của chính test** (khối CHỨNG-NHÂN
tự tay `Remove` để chứng minh FK là thật) — xem BƯỚC 5.
- **F14_F5a** (phiếu ĐÃ xoá mềm): handler PHẢI cho qua + purge. Đây mới là lỗi production thật.
---
## BƯỚC 3 — CÁC THAY ĐỔI
### (1) `ApprovalWorkflowV2AdminFeatures.cs` — vá LỚP B (dòng Cấp mới bị đoán nhầm là "đã tồn tại")
Vùng `toAdd` (`:730-759` sau sửa): tách `new ApprovalWorkflowLevel {...}` ra biến rồi
**đăng ký tường minh**:
```csharp
db.ApprovalWorkflowLevels.Add(newLevel); // ép state Added, không qua heuristic khoá-đã-set
step.Levels.Add(newLevel); // giữ nav in-memory nhất quán cho phần đọc phía sau
```
Đúng khuôn đã có sẵn trong repo: `PeWorkItemBudgetFeatures.cs:44` cũng `db.X.Add(rec)` tường minh.
### (2) `ApprovalWorkflowV2AdminFeatures.cs` — vá LỚP A (`ExecuteDelete` bỏ quên change-tracker)
Thêm helper `DetachTrackedOpinionsOn(IApplicationDbContext, HashSet<Guid>)` (đặt cạnh
`PurgeOrphanOpinionsAsync`) + gọi NGAY TRƯỚC vòng `db.ApprovalWorkflowLevels.Remove(gone)`.
Quét `((DbContext)db).ChangeTracker.Entries()` theo **tên cột** `ApprovalWorkflowLevelId`
⇒ phủ đủ **cả 7 bảng chữ ký** (PE + HĐ + Proposal/Leave/Ot/Travel/Vehicle — đã kiểm:
`grep -rl ApprovalWorkflowLevelId src/.../Domain/` ra ĐÚNG 7 file, không dư entity nào khác)
mà không phải liệt kê tay từng DbSet.
Cast `((DbContext)db)` là khuôn đã có trong repo (`PeWorkItemBudgetFeatures.cs:52`,
`PurchaseEvaluationFeatures.cs:125`) ⇒ **KHÔNG phải đổi `IApplicationDbContext`**.
An toàn: tới điểm gọi, guard F-5 (`:658-673`) đã đọc DB bằng `AsNoTracking` và xác nhận các
dòng đó có **0 chữ ký còn hiệu lực** ⇒ bản sao nào còn tracked cũng là rác ⇒ detach = làm
tracker khớp lại DB (không nuốt dữ liệu thật).
### (3) `UpdateAwDefinitionTests.cs` — GỠ HẲN `ZZ_DIAG_AddLevel`
Xoá trọn `[Fact] ZZ_DIAG_AddLevel` (26 dòng giàn giáo, ném `Exception` để in tracker).
KHÔNG đụng bất kỳ `[Fact]` nào khác.
---
## BƯỚC 4 — ĐO SAU KHI VÁ (chạy riêng lớp test)
`dotnet build SolutionErp.slnx`**0 Warning · 0 Error**.
`dotnet test --filter FullyQualifiedName~UpdateAwDefinitionTests`:
```
Failed! - Failed: 1, Passed: 14, Skipped: 0, Total: 15
```
- LỚP B: **F12 · F1c · F16 → XANH** (F12 xanh với assert NGUYÊN VẸN — kể cả nửa contrast
`ForbiddenException` trước sửa, `CurrentApprovalLevelOrder == 2` sau sửa, và chữ ký gắn
đúng dòng Cấp mới của bob).
- LỚP A: **F14_F5a → XANH**.
- `ZZ_DIAG` → biến mất (15 test còn lại, trước là 16).
- **F14 → VẪN ĐỎ**, xem BƯỚC 5.