31a0a8e9e3
Напоминания: - ProbationEndingReminderDto: ManagerEmail убран — это был унаследованный ReceiverEmail под вторым именем. - Ссылка для ProbationEnding больше не ведёт на саму заявку на приём (она уже закрыта, решать по ней нечего) — берётся из новой настройки RemindersOptions.ProbationReminderUrl и ведёт в раздел, где заводят новую заявку. Подстановок в ней нет: адрес конкретной формы в портал не передать, rptype без rpid/rpsource игнорируется (см. parseRoute в request-portal). Заодно ушли BuildProbationUrl и ненужные больше параметры type/listId. Стайлгайд (CLAUDE.md) — у enum первое значение None: - ReminderFeed: None=0 трактуется как ApprovalOverdue, чтобы вызов без явного выбора работал как раньше. - EmploymentStatus: заодно инвертирован switch в ReminderService — раньше «продолжить» было веткой по умолчанию, и новый None молча означал бы «сотрудник работает». Теперь по умолчанию пропуск с предупреждением. - ConditionsLogic: None — НЕвалидное значение, отвергается валидаторами шага (в том числе новым PatchTemplateRouteStepRequestValidator, его не было вовсе). Поэтому у ConditionsLogic в Create/Update-запросах убрано значение по умолчанию: System.Text.Json в .NET 10 уважает дефолты параметров конструктора, и пропущенное поле молча означало «И». Теперь пропущенное поле даёт None и упирается в валидатор. ЛОМАЮЩЕЕ ИЗМЕНЕНИЕ КОНТРАКТА: клиент, не присылающий conditionsLogic в POST/PUT шага, получит 400 вместо прежнего молчаливого «И». Админка не затронута — она это поле всегда отправляет.
100 lines
6.1 KiB
C#
100 lines
6.1 KiB
C#
using FluentValidation;
|
|
using Tnsu.ApprovalRoute.Domain.Enums;
|
|
|
|
namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
|
|
|
|
public sealed class CreateTemplateRouteStepRequestValidator : AbstractValidator<CreateTemplateRouteStepRequest>
|
|
{
|
|
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<UpdateTemplateRouteStepRequest>
|
|
{
|
|
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 в часах не может быть отрицательным.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// У PATCH-запроса шага валидатора раньше не было вовсе — здесь он ровно про одно:
|
|
/// не дать протащить невалидные значения-заглушки в поля, которые запрос ЯВНО
|
|
/// задаёт. null остаётся «не менять» и не проверяется (см. PatchTemplateRouteStepRequest).
|
|
/// </summary>
|
|
public sealed class PatchTemplateRouteStepRequestValidator : AbstractValidator<PatchTemplateRouteStepRequest>
|
|
{
|
|
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);
|
|
|
|
/// <summary>
|
|
/// Условие с пустым field_name/value или operation=None — половинчатое, а не
|
|
/// «выключенное» (выключается вовсе не добавляя условие в список), поэтому
|
|
/// такое отдельное условие недопустимо, даже если остальные условия шага корректны.
|
|
/// </summary>
|
|
public static bool AreConditionsValid(IReadOnlyList<RouteStepConditionDto>? conditions) =>
|
|
conditions is null || conditions.All(c =>
|
|
!string.IsNullOrWhiteSpace(c.FieldName) &&
|
|
c.Operation != ComparisonOperator.None &&
|
|
!string.IsNullOrWhiteSpace(c.Value));
|
|
}
|