Files
solution-erp/.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/sub-implementer-backend-5.md
2026-07-27 12:24:07 +07:00

95 lines
7.2 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-5 — WAVE 5 BE: vá F1 + F2 (2 FLAG HIGH mất dữ liệu)
> Ghi TRONG LÚC LÀM, flush từng phần, tự `wc -c` kiểm byte thật.
- Ngày: 2026-07-27 · run `2026-07-27-S155-pe-delete-approver`
- Vật sửa: `src/Backend/SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs`
- Nền đo trước wave: `dotnet build` 0W/0E · `dotnet test` 549 PASS / 0 FAIL
---
## §0 — ĐỌC NGUYÊN VĂN F1 + F2 (reviewer-diff-dot1.md:33-80)
F1: `PurgeOrphanOpinionsAsync` (`:886-918`) dùng `ExecuteDeleteAsync` = auto-commit NGAY khi
không có transaction ambient. Gọi tại `:681-683`, `SaveChangesAsync` mãi `:822`. 4 nguồn ném
nằm GIỮA (`ResolveUserNamesAsync:686` round-trip · `Remove():771` severed-association ·
`db.PurchaseEvaluations…ToListAsync:794-800` round-trip · ghi changelog N phiếu trong SaveChanges).
Tiêu chí nghiệm thu: test giả lập SaveChanges ném SAU purge ⇒ số row opinion KHÔNG đổi.
F2: điều kiện purge `:889-891` (+6 khối lặp tới `:915`) rộng hơn nhãn "mồ côi" ở CẢ HAI vế —
(a) `!db.PurchaseEvaluations.Any(p => p.Id == … && !p.IsDeleted)` coi phiếu XOÁ MỀM ngang phiếu
không tồn tại; (b) `o.IsDeleted` — opinion xoá mềm trên phiếu CÒN SỐNG = bản ghi kiểm toán.
Phương án (a) của reviewer: chỉ purge khi phiếu cha thật sự KHÔNG CÒN ROW
(`IgnoreQueryFilters().Any(...)`), phiếu xoá mềm thì CHẶN admin như phiếu sống.
---
## §1 — ĐO NỀN TRƯỚC KHI SỬA (không tin doc, chạm đĩa)
| Sự thật | Bằng chứng |
|---|---|
| `IApplicationDbContext` KHÔNG expose `Database` | `grep -n "Database\|BeginTransaction" src/.../Common/Interfaces/IApplicationDbContext.cs`**0 hit** |
| Handler đã có tiền lệ ép kiểu `((DbContext)db)` | `ApprovalWorkflowV2AdminFeatures.cs:941` (trong `DetachTrackedOpinionsOn`) |
| Tiền lệ transaction repo | `ContractCodeGenerator.cs:35-59``var context = (DbContext)db; await using var tx = await context.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);` + try/Commit/catch/Rollback |
| `Microsoft.EntityFrameworkCore.Relational` CÓ trong Application | `SolutionErp.Application.csproj``<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.6" />` |
| 7 bảng `*LevelOpinion` KHÔNG có global query filter | `grep -rn "HasQueryFilter" Configurations/` → chỉ 1 hit `PurchaseEvaluationConfiguration.cs:84` (bảng CHA). ⇒ vế `o.IsDeleted` trong purge là THẬT (không phải dead code) |
| FK opinion→cha = **Cascade**, opinion→Level = **Restrict** | `PurchaseEvaluationLevelOpinionConfiguration.cs:20-28` |
| SQLite fixture BẬT FK enforcement thật | test cũ `UpdateAwDefinitionTests.cs:562-565` assert `DbUpdateException` khi `Remove(level)` có opinion ⇒ FK Restrict fire thật trong SQLite |
| Fixture SQLite hỗ trợ transaction | `SqliteDbFixture.cs:36-40` comment + `UseSqlite(shared connection)` |
### Hệ quả SUY RA TỪ ĐO (quan trọng cho thiết kế)
FK cha→opinion là **Cascade** ⇒ xoá cứng phiếu cha thì opinion cũng đi theo ⇒ **"mồ côi thật"
(cha KHÔNG CÒN ROW) gần như không sinh ra được qua đường ứng dụng**. Nghĩa là sau khi thu hẹp
theo F2, `PurgeOrphanOpinionsAsync` gần như là **lớp phòng-vệ-chiều-sâu** cho dữ liệu lệch sinh
ngoài luồng (sửa DB tay / import / FK tắt), KHÔNG còn là đường chạy thường ngày. Đúng ý F2:
guard chặn HẾT những gì còn cha, purge chỉ dọn thứ đã không thể khôi phục.
---
## §2 — VÁ F1 (rào giao dịch) — ĐÃ LAND
Kỹ thuật chọn: `await using var tx` + `CommitAsync` cuối, **KHÔNG try/catch** — đúng khuôn nhà
(`LeaveOtApprovalFeatures.cs:34-44`, `ProposalFeatures.cs:386`, `TravelVehicleApprovalFeatures.cs:822`).
Dispose của `IDbContextTransaction` rollback nếu chưa Commit ⇒ **diff là CHÈN-THÊM, 0 dòng thân
method bị thụt lề lại** (nếu bọc try/catch thì ~150 dòng phải re-indent = nhiễu review, khó soi).
- Mở rào: ngay TRƯỚC `PurgeOrphanOpinionsAsync` (thao tác không-hoàn-tác đầu tiên). Guard phía
trên chỉ ĐỌC `AsNoTracking` rồi ném, không cần nằm trong rào; mở sớm hơn chỉ kéo dài giữ khoá.
- KHÔNG dùng `IsolationLevel.Serializable` như 4 code-generator: ở đó Serializable chống ĐUA
cấp-số; ở đây yêu cầu là NGUYÊN TỬ, siết isolation chỉ làm màn quản trị dễ deadlock.
- `ExecuteDeleteAsync` tự enlist vào transaction đang mở của context ⇒ 7 câu DELETE nay cùng
sống/chết với `SaveChangesAsync`.
## §3 — VÁ F2 (thu hẹp purge + chặn hồ sơ xoá mềm) — ĐÃ LAND
Phân hoạch KÍN, không còn kẽ (đây là điểm thiết kế chính):
| Trạng thái chữ ký | Xử lý sau vá | Ai lo |
|---|---|---|
| cha SỐNG + ý kiến SỐNG | chặn 409 (thông điệp CŨ, giữ nguyên) | `CountLiveOpinionsAsync` (tầng 1) |
| cha XOÁ MỀM (row còn) | **chặn 409 + nêu TÊN hồ sơ** ← MỚI | `ListRetainedOpinionsAsync` (tầng 2) |
| ý kiến XOÁ MỀM, cha sống | **chặn 409 + nêu TÊN hồ sơ** ← MỚI | `ListRetainedOpinionsAsync` (tầng 2) |
| cha KHÔNG CÒN ROW | purge (xoá cứng) — đường duy nhất còn lại | `PurgeOrphanOpinionsAsync` |
- Áp CẢ 7 bảng chữ ký (PE · HĐ · Đề xuất · Nghỉ phép · Tăng ca · Công tác · Đơn xe), tầng 2
JOIN sang cha để lấy mã hồ sơ: `MaPhieu` · `MaHopDong` · `MaDeXuat` · `MaDonTu`×4.
- Guard tầng 2 áp cho **cả `toRemove` lẫn `approverChanged`** — reviewer viết "chặn admin **như
phiếu sống**", mà phiếu sống thì cả bỏ-người lẫn đổi-người đều bị chặn. Đổi người trên dòng
còn giữ chữ ký của hồ sơ xoá mềm = khi khôi phục, ý kiến người A hoá thành của người B.
- `IgnoreQueryFilters()` BẮT BUỘC ở tầng 2 và ở purge: PE/Contract có global filter `!IsDeleted`
(`PurchaseEvaluationConfiguration.cs:84`) — không tắt thì hồ sơ xoá mềm biến mất khỏi JOIN và
ta lại kết luận "mồ côi" = đúng cái sai đang chữa.
- Sửa luôn dòng changelog `"Dọn N chữ ký mồ côi (thuộc phiếu đã xoá)"` → `"(hồ sơ mang chữ ký đã
KHÔNG CÒN trong hệ thống)"` — nhãn cũ nay MÔ TẢ SAI việc đang làm.
### Đo ngay sau khi vá (trước khi đụng test)
`dotnet build SolutionErp.slnx` → **0 Warning / 0 Error**.
`dotnet test --filter UpdateAwDefinitionTests` → **14 PASS / 1 FAIL**. Đúng 1 fail, đúng cái phải
fail: `F14_F5a_OpinionBelongsToDeletedPe_RemovalAllowed_AndOrphanPurged` — test này khẳng định
CHÍNH hành vi mà F2 gọi là sai ("phiếu xoá mềm ⇒ cho gỡ + purge chữ ký"). Thông điệp thật nhận
được: `… còn 1 chữ ký/ý kiến đang LƯU, thuộc: Phiếu duyệt NCC PE-F14b-001 — đã xoá …`
⇒ chứng minh 3 việc cùng lúc: guard tầng 2 CHẠY · `IgnoreQueryFilters()` DỊCH ĐƯỢC trên SQLite ·
thông điệp NÊU ĐÚNG TÊN hồ sơ. 0 test nào khác đỏ ⇒ vá không lan.