# W5a — test-before ContractDeleteGuardTests (YC-029)

> Vai: test-specialist · run `2026-08-12-S190-soltuon-dieu-chinh-hd` · GHI TỪNG MỤC, không dựng dàn bài rồi điền.

## Baseline

Đo TƯƠI 2026-08-12, TRƯỚC khi chạm bất kỳ file nào (Đ-9):

```
dotnet test SolutionErp.slnx --nologo -v q -p:BuildInParallel=false -maxcpucount:1
Passed! - Failed: 0, Passed:  45, Total:  45  — SolutionErp.Domain.Tests.dll
Passed! - Failed: 0, Passed: 635, Total: 635  — SolutionErp.Infrastructure.Tests.dll
```

**45 + 635 = 680 PASS / 0 FAIL** ⇒ KHỚP mốc B4 (680). Đ-9 KHÔNG kích hoạt, đi tiếp.
(Cờ `-p:BuildInParallel=false -maxcpucount:1` là nếp chống MSBuild OOM của repo, không đổi tập test.)

## 13 ca đã viết (map T-n → tên Fact)

File: `tests/SolutionErp.Infrastructure.Tests/Application/ContractDeleteGuardTests.cs` (MỚI, untracked).
Đếm đo tươi: `grep -c "^    \[Fact\]"` = **13** · 13 dòng comment `// T-n:` khớp 1-1.

| T-n | Tên `[Fact]` | Kỳ vọng đã mã hoá |
|---|---|---|
| T-1 | `DeleteContract_DraftByOwner_SoftDeletes_AndWritesOneChangelogRow` :170 | 3-trong-1: `Count==0` (khuất) · `IgnoreQueryFilters` IsDeleted=true + DeletedBy=owner · changelog **2 dòng** (Insert seed + Delete) với EntityType=Contract, EntityId=null, PhaseAtChange=2, UserId, UserName, ContextNote |
| T-2 | `DeleteContract_TraLai_ByOwner_Succeeds` :210 | OK + `PhaseAtChange == TraLai(98)` |
| T-3 | `DeleteContract_TuChoi_ByOwner_Succeeds` :232 | OK + `PhaseAtChange == TuChoi(99)` |
| T-4 | `DeleteContract_LegacyPhaseBelowDangInKy_ByOwner_Succeeds` :255 | OK cho **1, 3, 4** (`DangChon`/`DangGopY`/`DangDamPhan`) |
| T-5 | `DeleteContract_ChoDuyet_ByOwner_ThrowsConflict` :281 | `ConflictException` + HĐ còn nguyên + **0 vết xoá ma** |
| T-6 | `DeleteContract_DaPhatHanh_ByAdmin_ThrowsConflict` :308 | `ConflictException` (9, Admin) + **biên `DangInKy(5)` cũng chặn** + cả 2 còn nguyên |
| T-7 | `DeleteContract_DraftOfAnotherUser_ByStranger_ThrowsForbidden` :336 | `ForbiddenException` + HĐ còn nguyên |
| T-8 | `DeleteContract_DraftOfAnotherUser_ByAdmin_Succeeds` :357 | OK + `DeletedBy == admin` + changelog `UserId == admin` |
| T-9 | `DeleteContract_NullDrafter_ByAnonymousActor_ThrowsUnauthorized` :385 | `UnauthorizedException` + HĐ còn nguyên |
| T-10 | `DeleteContract_UnknownId_ByStranger_ThrowsNotFoundBeforeForbidden` :408 | `NotFoundException` (KHÔNG Forbidden) |
| T-11 | `DeleteContract_ChoDuyetOfAnotherUser_ByStranger_ThrowsForbiddenBeforeConflict` :427 | `ForbiddenException` (KHÔNG Conflict) |
| T-12 | `DeleteContract_ReasonOver2000Chars_FailsValidation` :451 | `ValidationException` (app, không phải FluentValidation) + `Errors` có key `Reason` + `next` KHÔNG chạy + **control dương đúng 2000 ký tự thì chạy** |
| T-13 | `DeleteContract_WhitespaceReason_Succeeds_AndStoresNullContextNote` :481 | OK + `ContextNote == null` |
| T-14 | — **KHÔNG VIẾT** (phản-ca tôn trọng) | 0 test reflection-authz cho DELETE; lý do ghi trong header file |

### 🔴 3 TÊN BỊ PIN — hợp đồng API mà BE (W5 việc 2) phải nở ĐÚNG
1. `DeleteContractCommand(Guid Id, string? Reason = null)`
2. `DeleteContractCommandHandler(IApplicationDbContext db, ICurrentUser currentUser)`
3. `DeleteContractCommandValidator : AbstractValidator<DeleteContractCommand>` (khuôn `ContractSigningPlanFeatures.cs:1743`)

Sai tên/sai arity ⇒ vẫn đỏ compile. Đây là chủ đích của test-before (chốt hợp đồng trước khi gõ prod).

### Quyết định hạ tầng (điều kiện để phép đo CÓ NGHĨA)
- Fixture private-per-file `ContractDeleteFixture` (cái thứ 6 cùng nếp họ Contract — be2-R14) **wire `AuditingInterceptor`**. Không có nó thì `Remove()` = xoá CỨNG ⇒ mọi `IgnoreQueryFilters().Single(...)` nổ, còn `Count()==0` thì xanh cả khi soft-delete chưa từng tồn tại (bài S155).
- Mỗi lệnh = 1 `DbContext` mới, dùng chung 1 connection (mô phỏng scoped-per-request) — đúng điều kiện đẻ ra bug T26; nếu tái dùng context thì changelog Added bị cascade client-side ngay tại `Remove()` mà test lại không thấy.
- Seed kèm **1 dòng changelog `Insert` có sẵn** ⇒ assert "đúng 1 dòng Delete" thành phép đo PHÂN BIỆT (đọc theo `Action`, không đếm bừa) + canh vết cũ không bị lượt xoá cuốn theo.
- `Contract.SupplierId/ProjectId` là Guid LOOSE (`ContractConfiguration.cs:29-31` chỉ có index, không FK) ⇒ KHÔNG cần seed master, không rủi ro SQLite lỗi 19.
- T-12 đo qua `ValidationBehavior<DeleteContractCommand, Unit>` THẬT (đường request đi trong prod) + alias `using ValidationException = SolutionErp.Application.Common.Exceptions.ValidationException` giống chính `ValidationBehavior.cs` ⇒ bắt đúng exception map ra HTTP 400, không nhầm bản FluentValidation.

## Kết quả chạy ĐỎ

*(worker #53 chết trước bước này — LEAD chạy on-behalf @S190)*

`dotnet build tests/SolutionErp.Infrastructure.Tests` → **Build FAILED, ĐỎ COMPILE đúng kỳ vọng
test-before**, lỗi khớp ĐÚNG 3 tên bị pin: CS1729 `DeleteContractCommandHandler` ctor 2-args
(:155) · CS1729 `DeleteContractCommand` ctor 2-args (:156, :463, :472) · CS0246
`DeleteContractCommandValidator` chưa tồn tại (:458) · CS0117 `Reason` chưa có (:468).
⇒ RED-READY — hợp đồng API đã chốt, W5b (implementer-backend) làm xanh.

## DỪNG/GHI CHÚ

- Worker chết #53 SAU khi viết trọn file test + bảng map, TRƯỚC bước chạy-đỏ + return.
  Lead xác nhận đỏ on-behalf. 0 điều kiện Đ kích hoạt.
