Напоминания: убрать дубль ManagerEmail, ссылка из настроек; None первым во всех enum

Напоминания:
- 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 вместо прежнего молчаливого «И». Админка не
затронута — она это поле всегда отправляет.
This commit is contained in:
gorbenko.pavel
2026-08-27 15:53:31 +05:00
parent 81ed85d6ce
commit 31a0a8e9e3
11 changed files with 192 additions and 102 deletions
@@ -8,6 +8,9 @@ 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("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
@@ -28,6 +31,9 @@ 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("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
@@ -43,6 +49,35 @@ public sealed class UpdateTemplateRouteStepRequestValidator : AbstractValidator<
}
}
/// <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) =>