diff --git a/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs index c418d4f..321d0ca 100644 --- a/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs @@ -221,11 +221,8 @@ public sealed class TemplateRouteService( HasUnevaluatableManagerCondition(step, positionMissing) ? PositionMissingWarning : null, cancellationToken))); - var flattenedSteps = stepResults.SelectMany(x => x).ToList(); - var (toCollapse, directorTansuAfterCompanyDirectors) = SplitDirectorTansuAfterCompanyDirectors(flattenedSteps); - var collapsedSteps = CollapseByExecutorEmail(toCollapse) - .Concat(directorTansuAfterCompanyDirectors) - .ToList(); + var flattenedSteps = RemoveCompanyDirectorsDuplicatedByDirectorTansu(stepResults.SelectMany(x => x).ToList()); + var collapsedSteps = CollapseByExecutorEmail(flattenedSteps); return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RequiresControl, collapsedSteps)); } @@ -233,53 +230,29 @@ public sealed class TemplateRouteService( /// /// Директор Tansu (HeadType.DirectorTansu) сплошь и рядом совпадает с директором /// ОДНОЙ из компаний инициатора (HeadType.CompanyDirectors) — обычно с ТОО TANSU - /// Construction. Оба шага в маршруте законны сами по себе, и оба должны попасть в - /// результат — это НЕ дубликат одного согласования, а два разных: сначала блок - /// директоров компаний параллельно, а затем — отдельно — Директор Tansu, даже если - /// физически это тот же человек. Поэтому строку DirectorTansu с таким совпадением - /// НЕ пускаем через обычное схлопывание по email (см. CollapseByExecutorEmail) — - /// оно бы решило, что это конфликт PositionCode одного согласующего, и либо - /// смержило бы её с строкой CompanyDirectors, либо просто выбрало одну из них. - /// Вместо этого явно переставляем её OrderNo на «после всего блока - /// CompanyDirectors» — не параллельно ему, а следом. + /// Construction. Оба шага в маршруте законны сами по себе, но если так, из + /// CompanyDirectors нужно убрать именно ЭТУ строку — не смешивать её с шагом + /// DirectorTansu через обычное схлопывание по email (см. CollapseByExecutorEmail): + /// тот считает разные PositionCode у одного email конфликтом и пишет + /// предупреждение, а здесь это не конфликт, а один и тот же человек — оставляем + /// только шаг DirectorTansu, а дубликат из CompanyDirectors удаляем. /// - /// - /// (toCollapse, directorTansuAfterCompanyDirectors): первый список идёт через - /// обычное схлопывание по email, второй — строки DirectorTansu, которые нужно - /// просто дописать в конец результата без схлопывания. - /// - private static (IReadOnlyList ToCollapse, IReadOnlyList DirectorTansuAfterCompanyDirectors) - SplitDirectorTansuAfterCompanyDirectors(IReadOnlyList steps) + private static IReadOnlyList RemoveCompanyDirectorsDuplicatedByDirectorTansu( + IReadOnlyList steps) { - var companyDirectorSteps = steps.Where(s => s.PositionCode == HeadType.CompanyDirectors).ToList(); - var companyDirectorEmails = companyDirectorSteps - .Where(s => !string.IsNullOrWhiteSpace(s.ExecutorEmail)) + var directorTansuEmails = steps + .Where(s => s.PositionCode == HeadType.DirectorTansu && !string.IsNullOrWhiteSpace(s.ExecutorEmail)) .Select(s => s.ExecutorEmail!) .ToHashSet(StringComparer.OrdinalIgnoreCase); - if (companyDirectorEmails.Count == 0) - return (steps, []); + if (directorTansuEmails.Count == 0) + return steps; - var maxCompanyDirectorsOrderNo = companyDirectorSteps.Max(s => s.OrderNo); - - var toCollapse = new List(); - var afterBlock = new List(); - - foreach (var step in steps) - { - if (step.PositionCode == HeadType.DirectorTansu && - !string.IsNullOrWhiteSpace(step.ExecutorEmail) && - companyDirectorEmails.Contains(step.ExecutorEmail)) - { - afterBlock.Add(step with { OrderNo = maxCompanyDirectorsOrderNo + 1 }); - } - else - { - toCollapse.Add(step); - } - } - - return (toCollapse, afterBlock); + return steps + .Where(s => s.PositionCode != HeadType.CompanyDirectors + || string.IsNullOrWhiteSpace(s.ExecutorEmail) + || !directorTansuEmails.Contains(s.ExecutorEmail)) + .ToList(); } /// diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs index 47b0af1..7e3d092 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs @@ -900,12 +900,12 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); portalClient .Setup(x => x.GetEmployeeByEmailAsync(It.IsAny(), It.IsAny())) @@ -949,21 +949,15 @@ public sealed class TemplateRouteServiceTests : IClassFixture s.PositionCode == HeadType.DirectorTansu); - tansuStep.ExecutorEmail.ShouldBe("director.tansu@tnsu.kz"); - tansuStep.OrderNo.ShouldBe(6); + resolved.Value.Steps.Count.ShouldBe(2); + var tansuStep = resolved.Value.Steps.Single(s => s.ExecutorEmail == "director.tansu@tnsu.kz"); + tansuStep.PositionCode.ShouldBe(HeadType.DirectorTansu); + tansuStep.OrderNo.ShouldBe(10); tansuStep.Warning.ShouldBeNull(); - - var companyDirectorSteps = resolved.Value.Steps.Where(s => s.PositionCode == HeadType.CompanyDirectors).ToList(); - companyDirectorSteps.Count.ShouldBe(2); - companyDirectorSteps.ShouldAllBe(s => s.OrderNo == 5); - companyDirectorSteps.ShouldContain(s => s.ExecutorEmail == "director.tansu@tnsu.kz"); - companyDirectorSteps.ShouldContain(s => s.ExecutorEmail == "mechanic.kps@tnsu.kz"); + resolved.Value.Steps.ShouldContain(s => s.ExecutorEmail == "mechanic.kps@tnsu.kz" && s.PositionCode == HeadType.CompanyDirectors); } [Fact(DisplayName = "Резолв шага с типом руководителя Chief возвращает warning, если руководитель не найден")]