diff --git a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs index 933c8ff..71336dc 100644 --- a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs +++ b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs @@ -70,6 +70,21 @@ public class CreateContractCommandHandler( .Select(w => (Guid?)w.Id) .FirstOrDefaultAsync(ct); + // [Plan B S29 2026-05-22 Hotfix Reviewer] Validate ApprovalWorkflowId V2 + // (Mig 32) — User chọn lúc create. Phải tồn tại + ApplicableType=Contract(3). + // Mirror PE pattern PurchaseEvaluationFeatures.cs:62-77. Defense-in-depth: + // FE Workspace dropdown đã filter ApplicableType=3 server-side; BE guard + // chặn attacker forge POST với PE workflow ID (ApplicableType=1/2). + if (request.ApprovalWorkflowId is Guid awId) + { + var aw = await db.ApprovalWorkflows.AsNoTracking() + .FirstOrDefaultAsync(w => w.Id == awId, ct) + ?? throw new NotFoundException("ApprovalWorkflow", awId); + if (aw.ApplicableType != Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.Contract) + throw new ConflictException( + $"Quy trình {aw.Code} áp dụng cho {aw.ApplicableType}, không khớp với HĐ (cần ApplicableType=Contract)."); + } + // Validate Budget link nếu có: cùng Project + Phase=DaDuyet. if (request.BudgetId is Guid bid) {