Files
solution-erp/.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/sub-d2-test-4.md
2026-07-27 13:12:40 +07:00

4.2 KiB

sub-d2-test-4 — ĐỢT 2: test đường xóa-khi-đang-duyệt + màn "Đã xóa"

Diary GHI TRONG LÚC LÀM (run này đã mất 1 diary 0-byte + 9 return). Flush từng phần, tự wc -c sau mỗi lần ghi.

0. Trạng thái nhận việc

  • Baseline lead đo: 551 PASS / 0 FAIL (Domain 45 + Infra 506). Code đợt 2 đã land, 0 test.
  • Mục tiêu: 10 test (T21, T22, T22b, T22c, T23, T24, T24b, T25, T25b, T26) ⇒ đích > 551.

1. Đọc vật cần test — ĐÃ XONG

PeSoftDeleteFeatures.cs (file mới, 249 dòng) — 2 vật

A. DeletePurchaseEvaluationByApproverCommandHandler (:50-145) — thứ tự rào:

  1. !IsAuthenticated || UserId is nullUnauthorizedException
  2. PE không tồn tại → NotFoundException
  3. Phase != ChoDuyetConflictException
  4. ApprovalWorkflowId is not GuidConflictException
  5. steps rỗng / CurrentWorkflowStepIndex ngoài biên → ConflictException
  6. pendingLevelGroup.Count == 0 (Bước không có Cấp đang chờ) → ConflictException
  7. KHÔNG match ApproverUserId == actorIdForbiddenException ("chưa tới lượt") ← T24
  8. !matchingLevel.AllowApproverDeleteForbiddenException ("chưa được bật quyền") ← T23, T24b
  9. Ghi PurchaseEvaluationChangelog (Action=ChangelogAction.Delete, EntityType=Workflow, EntityId=matchingLevel.Id, PhaseAtChange=ChoDuyet, ContextNote=Reason.Trim() hoặc null) ← T26
  10. db.PurchaseEvaluations.Remove(pe) = XÓA MỀM qua AuditingInterceptor (Deleted→Modified + IsDeleted=true)

🔴 Điểm sống-còn cho test: rào (7) đọc PER-ROW pendingLevelGroup.FirstOrDefault(l => l.ApproverUserId == actorId), rào (8) đọc matchingLevel.AllowApproverDeletekhông g.Any(...). Đây đúng là thứ T24b phải ghim.

B. ListDeletedPurchaseEvaluationsQueryHandler (:153-248):

  • IgnoreQueryFilters().Where(x => x.IsDeleted)T22b ghim vế .Where (gỡ nó ⇒ list toàn phiếu sống)
  • INNER join db.Projects ( phiếu phải có Project THẬT, không Guid ngẫu nhiên — nếu không row rơi khỏi kết quả và test xanh giả)
  • IDOR-scope khi !Roles.Contains(Admin): DrafterUserId == userIdeligiblePhases.Contains(Phase) ∥ (Phase != DangSoanThao ∧ pinned wf ∈ workflow mà user là approver) ← T22c

PeBudgetAccumulator.cs — số học cho T21 (🔴 KHÔNG phải "cả 4 cùng giảm")

Đại lượng Hàm Điều kiện phase Phiếu ChoDuyet có winner đang xét
PrevSubmitted* ComputeAsync :53-59 ChoDuyet ∥ DaDuyet CÓ vào ⇒ xóa thì
PrevSelected* ComputeAsync :61-62 DaDuyet ∧ có winner không (đòi DaDuyet) ⇒ KHÔNG đổi
PendingSubmitted* ComputePendingAsync :104-108 loại tường minh ChoDuyet không ⇒ KHÔNG đổi
PendingSelected* ComputePendingAsync :114-117 ∉{DangSoanThao,DaDuyet,TuChoi} ∧ winner CÓ vào ⇒ xóa thì
PriorPes :128-133 ∉{DaDuyet,TuChoi} CÓ ⇒ mất đúng 1 phần tử

Cơ chế triệt tiêu: RemoveIsDeleted=true ⇒ global filter PurchaseEvaluationConfiguration.cs:84 loại khỏi db.PurchaseEvaluations ⇒ CẢ 4 phép cộng đọc hụt. Accumulator KHÔNG sửa.

Controller (PurchaseEvaluationsController.cs)

  • :163-169 DELETE {id}/by-approver + [Authorize(Policy = "PurchaseEvaluations.Delete")]T25
  • :174-182 GET deleted + [Authorize(Policy = "PurchaseEvaluations.Read")]
  • :151-156 DELETE {id} (nháp) — KHÔNG policy, đừng nhầm ⇒ T25 phải assert cả vế này (nếu ai đó "sửa" bằng cách gắn policy lên method nháp thì T25 vẫn phải phân biệt được 2 method).

2. Kế hoạch file test

  • File MỚI tests/SolutionErp.Infrastructure.Tests/Application/PeDeleteByApproverTests.cs — T21..T24b, T25b, T26 (handler-level, SQLite thật).
  • T25 (403 policy) = Pattern 10 reflection, thêm vào file mới cùng chỗ (đọc attribute trên method, không phải class — 2 endpoint này gắn method-level).