95 lines
7.2 KiB
Markdown
95 lines
7.2 KiB
Markdown
# 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.
|