From 59283774656664c9db211eae502c3a4b29c8c9f6 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 24 Jul 2026 17:54:34 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TemplateRouteStepDto.cs | 8 ++-- .../TemplateRouteStepService.cs | 39 +++++++++++++++++-- .../TemplateRouteStepValidators.cs | 16 ++++++-- .../TemplateRouteStepServiceTests.cs | 37 ++++++++++++++++++ 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs index d24d5f0..73eef71 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs @@ -16,8 +16,8 @@ public sealed record TemplateRouteStepDto( public sealed record CreateTemplateRouteStepRequest( int OrderNo, - string ExecutorEmail, - string Position, + string? ExecutorEmail, + string? Position, ProjectRole ProjectRole, HeadType HeadType, RouteStepStatus Status, @@ -25,8 +25,8 @@ public sealed record CreateTemplateRouteStepRequest( public sealed record UpdateTemplateRouteStepRequest( int OrderNo, - string ExecutorEmail, - string Position, + string? ExecutorEmail, + string? Position, ProjectRole ProjectRole, HeadType HeadType, RouteStepStatus Status, diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs index 9e1f094..2bc5267 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs @@ -41,12 +41,21 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) return Result.Fail(new ValidationError( "Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false).")); + var executorEmail = r.ExecutorEmail ?? string.Empty; + var position = r.Position ?? string.Empty; + + var duplicate = await HasDuplicateAsync( + route.Id, executorEmail, position, r.ProjectRole, r.HeadType, r.Status, excludeId: null, cancellationToken); + if (duplicate) + return Result.Fail(new ConflictError( + "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте.")); + var entity = new TemplateRouteStep { RouteId = route.Id, OrderNo = r.OrderNo, - ExecutorEmail = r.ExecutorEmail, - Position = r.Position, + ExecutorEmail = executorEmail, + Position = position, ProjectRole = r.ProjectRole, HeadType = r.HeadType, Status = r.Status, @@ -73,9 +82,18 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) return Result.Fail(new ValidationError( "Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false).")); + var executorEmail = r.ExecutorEmail ?? string.Empty; + var position = r.Position ?? string.Empty; + + var duplicate = await HasDuplicateAsync( + route.Id, executorEmail, position, r.ProjectRole, r.HeadType, r.Status, excludeId: entity.Id, cancellationToken); + if (duplicate) + return Result.Fail(new ConflictError( + "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте.")); + entity.OrderNo = r.OrderNo; - entity.ExecutorEmail = r.ExecutorEmail; - entity.Position = r.Position; + entity.ExecutorEmail = executorEmail; + entity.Position = position; entity.ProjectRole = r.ProjectRole; entity.HeadType = r.HeadType; entity.Status = r.Status; @@ -97,6 +115,19 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) return Result.Ok(); } + private Task HasDuplicateAsync( + int routeId, string executorEmail, string position, ProjectRole projectRole, HeadType headType, + RouteStepStatus status, int? excludeId, CancellationToken cancellationToken) => + db.TemplateRouteSteps.AnyAsync(x => + x.Id != excludeId && + x.RouteId == routeId && + x.ExecutorEmail == executorEmail && + x.Position == position && + x.ProjectRole == projectRole && + x.HeadType == headType && + x.Status == status, + cancellationToken); + // Для маршрута «по проекту» без единого шага с ролью в проекте DTO несёт предупреждение в поле Warning. private async Task ToDtoWithWarningAsync( TemplateRouteStep entity, TemplateRoute route, CancellationToken cancellationToken) diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs index be1d48b..8f5e7a7 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs @@ -8,8 +8,12 @@ public sealed class CreateTemplateRouteStepRequestValidator : AbstractValidator< public CreateTemplateRouteStepRequestValidator() { RuleFor(x => x) - .Must(x => !(string.IsNullOrWhiteSpace(x.ExecutorEmail) && string.IsNullOrWhiteSpace(x.Position) && x.ProjectRole == ProjectRole.None)) - .WithMessage("Исполнитель, должность и роль в проекте не могут быть пустыми одновременно."); + .Must(x => + !string.IsNullOrWhiteSpace(x.ExecutorEmail) || + !string.IsNullOrWhiteSpace(x.Position) || + x.ProjectRole != ProjectRole.None || + x.HeadType != HeadType.None) + .WithMessage("Должно быть заполнено хотя бы одно из полей: исполнитель, должность, роль в проекте или тип руководителя."); } } @@ -18,7 +22,11 @@ public sealed class UpdateTemplateRouteStepRequestValidator : AbstractValidator< public UpdateTemplateRouteStepRequestValidator() { RuleFor(x => x) - .Must(x => !(string.IsNullOrWhiteSpace(x.ExecutorEmail) && string.IsNullOrWhiteSpace(x.Position) && x.ProjectRole == ProjectRole.None)) - .WithMessage("Исполнитель, должность и роль в проекте не могут быть пустыми одновременно."); + .Must(x => + !string.IsNullOrWhiteSpace(x.ExecutorEmail) || + !string.IsNullOrWhiteSpace(x.Position) || + x.ProjectRole != ProjectRole.None || + x.HeadType != HeadType.None) + .WithMessage("Должно быть заполнено хотя бы одно из полей: исполнитель, должность, роль в проекте или тип руководителя."); } } diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs index 2e4b4dc..12f6f0c 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs @@ -65,6 +65,43 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture