using FluentValidation; using Tnsu.ApprovalRoute.Domain.Enums; namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps; public sealed class CreateTemplateRouteStepRequestValidator : AbstractValidator { public CreateTemplateRouteStepRequestValidator() { RuleFor(x => x.Status).NotEqual(RouteStepStatus.None).WithMessage("Статус обязателен: Approval или Execution."); RuleFor(x => x.ConditionsLogic) .NotEqual(ConditionsLogic.None) .WithMessage("Логика объединения условий обязательна: And или Or."); RuleFor(x => x) .Must(x => TemplateRouteStepFields.CountFilled(x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType) == 1) .WithMessage("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя."); RuleFor(x => x) .Must(x => TemplateRouteStepFields.AreConditionsValid(x.Conditions)) .WithMessage("В каждом условии (field_name, operation, value) все три поля обязательны."); // Отрицательный SLA сделал бы шаг просроченным уже в момент активации; // ноль допустим — это осознанное «напоминать сразу». RuleFor(x => x.SlaHours) .GreaterThanOrEqualTo(0) .When(x => x.SlaHours is not null) .WithMessage("SLA в часах не может быть отрицательным."); } } public sealed class UpdateTemplateRouteStepRequestValidator : AbstractValidator { public UpdateTemplateRouteStepRequestValidator() { RuleFor(x => x.Status).NotEqual(RouteStepStatus.None).WithMessage("Статус обязателен: Approval или Execution."); RuleFor(x => x.ConditionsLogic) .NotEqual(ConditionsLogic.None) .WithMessage("Логика объединения условий обязательна: And или Or."); RuleFor(x => x) .Must(x => TemplateRouteStepFields.CountFilled(x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType) == 1) .WithMessage("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя."); RuleFor(x => x) .Must(x => TemplateRouteStepFields.AreConditionsValid(x.Conditions)) .WithMessage("В каждом условии (field_name, operation, value) все три поля обязательны."); // Отрицательный SLA сделал бы шаг просроченным уже в момент активации; // ноль допустим — это осознанное «напоминать сразу». RuleFor(x => x.SlaHours) .GreaterThanOrEqualTo(0) .When(x => x.SlaHours is not null) .WithMessage("SLA в часах не может быть отрицательным."); } } /// /// У PATCH-запроса шага валидатора раньше не было вовсе — здесь он ровно про одно: /// не дать протащить невалидные значения-заглушки в поля, которые запрос ЯВНО /// задаёт. null остаётся «не менять» и не проверяется (см. PatchTemplateRouteStepRequest). /// public sealed class PatchTemplateRouteStepRequestValidator : AbstractValidator { public PatchTemplateRouteStepRequestValidator() { When(x => x.Status is not null, () => RuleFor(x => x.Status) .NotEqual(RouteStepStatus.None) .WithMessage("Статус обязателен: Approval или Execution.")); When(x => x.ConditionsLogic is not null, () => RuleFor(x => x.ConditionsLogic) .NotEqual(ConditionsLogic.None) .WithMessage("Логика объединения условий обязательна: And или Or.")); RuleFor(x => x) .Must(x => TemplateRouteStepFields.AreConditionsValid(x.Conditions)) .WithMessage("В каждом условии (field_name, operation, value) все три поля обязательны."); // Отрицательный SLA сделал бы шаг просроченным уже в момент активации; // ноль допустим — это осознанное «напоминать сразу». RuleFor(x => x.SlaHours) .GreaterThanOrEqualTo(0) .When(x => x.SlaHours is not null) .WithMessage("SLA в часах не может быть отрицательным."); } } internal static class TemplateRouteStepFields { public static int CountFilled(string? executorEmail, string? position, ProjectRole? projectRole, HeadType? headType) => (string.IsNullOrWhiteSpace(executorEmail) ? 0 : 1) + (string.IsNullOrWhiteSpace(position) ? 0 : 1) + (projectRole is null or ProjectRole.None ? 0 : 1) + (headType is null or HeadType.None ? 0 : 1); /// /// Условие с пустым field_name/value или operation=None — половинчатое, а не /// «выключенное» (выключается вовсе не добавляя условие в список), поэтому /// такое отдельное условие недопустимо, даже если остальные условия шага корректны. /// public static bool AreConditionsValid(IReadOnlyList? conditions) => conditions is null || conditions.All(c => !string.IsNullOrWhiteSpace(c.FieldName) && c.Operation != ComparisonOperator.None && !string.IsNullOrWhiteSpace(c.Value)); }