From 5476c1bab5dcbb572dccbadc034e6d7ec11a7b01 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Wed, 29 Jul 2026 14:53:45 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TemplateRoutes/ResolveTemplateRouteDto.cs | 3 +- .../TemplateRoutes/TemplateRouteService.cs | 20 ++++- .../TemplateRouteServiceTests.cs | 84 +++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs index 7947af8..bf5b484 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteDto.cs @@ -7,7 +7,8 @@ public sealed record ResolvedRouteStepDto( int OrderNo, string? ExecutorEmail, string? Position, - bool AllowReassignToOtherApprovers); + bool AllowReassignToOtherApprovers, + string? Warning = null); public sealed record ResolveTemplateRouteResultDto( int RouteId, diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs index 3acf480..a513f06 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs @@ -197,14 +197,26 @@ public sealed class TemplateRouteService( IReadOnlyList projectExecutors, CancellationToken cancellationToken) { string? resolvedEmail; + string? warning = null; if (!string.IsNullOrWhiteSpace(step.ExecutorEmail)) + { resolvedEmail = step.ExecutorEmail; + } else if (!string.IsNullOrWhiteSpace(step.Position)) + { resolvedEmail = await portalClient.GetEmployeeByPositionAsync(step.Position, initiatorCompany, cancellationToken); + if (string.IsNullOrWhiteSpace(resolvedEmail)) + warning = $"Не найден сотрудник на должности «{step.Position}» в компании «{initiatorCompany}»."; + } else if (step.ProjectRole is not (null or ProjectRole.None)) + { resolvedEmail = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole)?.Email; + if (string.IsNullOrWhiteSpace(resolvedEmail)) + warning = $"Не найден исполнитель с ролью в проекте «{step.ProjectRole}»."; + } else if (step.HeadType is not (null or HeadType.None)) + { resolvedEmail = step.HeadType switch { HeadType.Chief => await portalClient.GetManagerAsync(initiatorEmail, "chief", cancellationToken), @@ -212,8 +224,14 @@ public sealed class TemplateRouteService( HeadType.DirectorTansu => portalOptions.Value.DirectorTansuEmail, _ => null }; + if (string.IsNullOrWhiteSpace(resolvedEmail)) + warning = $"Не найден руководитель типа «{step.HeadType}»."; + } else + { resolvedEmail = null; + warning = "Не удалось определить исполнителя шага: не заполнено ни одно из полей."; + } string? resolvedPosition = null; if (!string.IsNullOrWhiteSpace(resolvedEmail)) @@ -222,7 +240,7 @@ public sealed class TemplateRouteService( resolvedPosition = resolvedEmployee?.Position; } - return new ResolvedRouteStepDto(step.Id, step.OrderNo, resolvedEmail, resolvedPosition, step.AllowReassignToOtherApprovers); + return new ResolvedRouteStepDto(step.Id, step.OrderNo, resolvedEmail, resolvedPosition, step.AllowReassignToOtherApprovers, warning); } private static IReadOnlyList CollapseByExecutorEmail(IReadOnlyList steps) diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs index c20f332..7bf9c42 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs @@ -367,6 +367,90 @@ public sealed class TemplateRouteServiceTests : IClassFixture x.GetManagerAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + [Fact(DisplayName = "Резолв шага с типом руководителя Chief возвращает warning, если руководитель не найден")] + public async Task ResolveAsync_ByHeadTypeChief_NotFound_ReturnsWarning() + { + // Arrange + var requestTypeId = await CreateRequestTypeAsync("Chief not found request"); + var route = await _service.CreateAsync( + new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), + CancellationToken.None); + + var stepService = _services.GetRequiredService(); + await stepService.CreateAsync( + route.Value.Id, + new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false), + CancellationToken.None); + + // Action + var resolved = await _service.ResolveAsync( + new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval), + CancellationToken.None); + + // Assert + resolved.IsSuccess.ShouldBeTrue(); + resolved.Value.Steps.ShouldHaveSingleItem(); + resolved.Value.Steps[0].ExecutorEmail.ShouldBeNull(); + resolved.Value.Steps[0].Warning.ShouldNotBeNull(); + resolved.Value.Steps[0].Warning.ShouldContain("руководитель"); + } + + [Fact(DisplayName = "Резолв шага с должностью возвращает warning, если сотрудник по должности не найден")] + public async Task ResolveAsync_ByPosition_NotFound_ReturnsWarning() + { + // Arrange + var requestTypeId = await CreateRequestTypeAsync("Position not found request"); + var route = await _service.CreateAsync( + new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), + CancellationToken.None); + + var stepService = _services.GetRequiredService(); + await stepService.CreateAsync( + route.Value.Id, + new CreateTemplateRouteStepRequest(1, null, "Несуществующая должность", null, null, RouteStepStatus.Approval, false), + CancellationToken.None); + + // Action + var resolved = await _service.ResolveAsync( + new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval), + CancellationToken.None); + + // Assert + resolved.IsSuccess.ShouldBeTrue(); + resolved.Value.Steps.ShouldHaveSingleItem(); + resolved.Value.Steps[0].ExecutorEmail.ShouldBeNull(); + resolved.Value.Steps[0].Warning.ShouldNotBeNull(); + resolved.Value.Steps[0].Warning.ShouldContain("должности"); + } + + [Fact(DisplayName = "Резолв шага с ролью в проекте возвращает warning, если исполнитель с такой ролью не найден")] + public async Task ResolveAsync_ByProjectRole_NotFound_ReturnsWarning() + { + // Arrange + var requestTypeId = await CreateRequestTypeAsync("Project role not found request"); + var route = await _service.CreateAsync( + new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false), + CancellationToken.None); + + var stepService = _services.GetRequiredService(); + await stepService.CreateAsync( + route.Value.Id, + new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.Security, null, RouteStepStatus.Approval, false), + CancellationToken.None); + + // Action + var resolved = await _service.ResolveAsync( + new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", Guid.NewGuid(), RouteStepStatus.Approval), + CancellationToken.None); + + // Assert + resolved.IsSuccess.ShouldBeTrue(); + resolved.Value.Steps.ShouldHaveSingleItem(); + resolved.Value.Steps[0].ExecutorEmail.ShouldBeNull(); + resolved.Value.Steps[0].Warning.ShouldNotBeNull(); + resolved.Value.Steps[0].Warning.ShouldContain("роль"); + } + [Fact(DisplayName = "Резолв маршрута откатывается на маршрут без отдела, если точного совпадения по отделу нет")] public async Task ResolveAsync_NoDepartmentRoute_FallsBackToCompanyWideRoute() {