From 8479190ec7fb21a30b790570eee0147375b585a5 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Mon, 27 Jul 2026 17:10:45 +0500 Subject: [PATCH] =?UTF-8?q?=D1=81=D1=85=D0=BB=D0=BE=D0=BF=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TemplateRoutes/TemplateRouteService.cs | 20 +++++++- .../TemplateRouteServiceTests.cs | 50 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs index 60a56e9..3acf480 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs @@ -177,7 +177,9 @@ public sealed class TemplateRouteService( foreach (var step in matchingSteps) steps.Add(await ResolveStepAsync(step, email, employee.Company, projectExecutors, cancellationToken)); - return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RouteType, route.RequiresControl, steps)); + var collapsedSteps = CollapseByExecutorEmail(steps); + + return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RouteType, route.RequiresControl, collapsedSteps)); } private Task HasDuplicateAsync( @@ -223,6 +225,22 @@ public sealed class TemplateRouteService( return new ResolvedRouteStepDto(step.Id, step.OrderNo, resolvedEmail, resolvedPosition, step.AllowReassignToOtherApprovers); } + private static IReadOnlyList CollapseByExecutorEmail(IReadOnlyList steps) + { + var seenEmails = new HashSet(StringComparer.OrdinalIgnoreCase); + var result = new List(); + + foreach (var step in steps) + { + if (!string.IsNullOrWhiteSpace(step.ExecutorEmail) && !seenEmails.Add(step.ExecutorEmail)) + continue; + + result.Add(step); + } + + return result; + } + private static TemplateRouteDto ToDto(TemplateRoute e) => new( e.Id, e.RouteType, e.RequestTypeId, e.DepartmentId, e.CompanyId, e.ByProject, e.RequiresControl); } diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs index e6ba8fe..c20f332 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs @@ -178,6 +178,56 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); + portalClient + .Setup(x => x.GetEmployeeByEmailAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность")); + portalClient + .Setup(x => x.GetManagerAsync(It.IsAny(), "chief", It.IsAny())) + .ReturnsAsync("dup@tnsu.kz"); + + var services = TestServiceProviderFactory.Build(_connectionString, portalClient: portalClient.Object); + var routeService = services.GetRequiredService(); + var requestTypeService = services.GetRequiredService(); + var stepService = services.GetRequiredService(); + + var requestTypeId = (await requestTypeService.CreateAsync( + new CreateRequestTypeRequest("Collapse duplicates request", "/collapse-duplicates"), CancellationToken.None)).Value.Id; + var route = await routeService.CreateAsync( + new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), + CancellationToken.None); + + await stepService.CreateAsync( + route.Value.Id, + new CreateTemplateRouteStepRequest(1, "dup@tnsu.kz", null, null, null, RouteStepStatus.Approval, false), + CancellationToken.None); + await stepService.CreateAsync( + route.Value.Id, + new CreateTemplateRouteStepRequest(2, "other@tnsu.kz", null, null, null, RouteStepStatus.Approval, false), + CancellationToken.None); + await stepService.CreateAsync( + route.Value.Id, + new CreateTemplateRouteStepRequest(3, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false), + CancellationToken.None); + + // Action + var resolved = await routeService.ResolveAsync( + new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval), + CancellationToken.None); + + // Assert + resolved.IsSuccess.ShouldBeTrue(); + resolved.Value.Steps.Count.ShouldBe(2); + resolved.Value.Steps[0].ExecutorEmail.ShouldBe("dup@tnsu.kz"); + resolved.Value.Steps[0].OrderNo.ShouldBe(1); + resolved.Value.Steps[1].ExecutorEmail.ShouldBe("other@tnsu.kz"); + resolved.Value.Steps[1].OrderNo.ShouldBe(2); + } + [Fact(DisplayName = "Резолв маршрута с ролью в проекте подставляет исполнителя из проекта")] public async Task ResolveAsync_ByProjectRole_UsesProjectExecutorEmail() {