Revert: Директор Tansu — снова удаляем дубликат из CompanyDirectors
Возврат к удалению совпавшей строки CompanyDirectors вместо
последовательной постановки после блока (см. 40d741c) — решили не
показывать одного и того же человека дважды в списке согласования.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -221,11 +221,8 @@ public sealed class TemplateRouteService(
|
|||||||
HasUnevaluatableManagerCondition(step, positionMissing) ? PositionMissingWarning : null,
|
HasUnevaluatableManagerCondition(step, positionMissing) ? PositionMissingWarning : null,
|
||||||
cancellationToken)));
|
cancellationToken)));
|
||||||
|
|
||||||
var flattenedSteps = stepResults.SelectMany(x => x).ToList();
|
var flattenedSteps = RemoveCompanyDirectorsDuplicatedByDirectorTansu(stepResults.SelectMany(x => x).ToList());
|
||||||
var (toCollapse, directorTansuAfterCompanyDirectors) = SplitDirectorTansuAfterCompanyDirectors(flattenedSteps);
|
var collapsedSteps = CollapseByExecutorEmail(flattenedSteps);
|
||||||
var collapsedSteps = CollapseByExecutorEmail(toCollapse)
|
|
||||||
.Concat(directorTansuAfterCompanyDirectors)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RequiresControl, collapsedSteps));
|
return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RequiresControl, collapsedSteps));
|
||||||
}
|
}
|
||||||
@@ -233,53 +230,29 @@ public sealed class TemplateRouteService(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Директор Tansu (HeadType.DirectorTansu) сплошь и рядом совпадает с директором
|
/// Директор Tansu (HeadType.DirectorTansu) сплошь и рядом совпадает с директором
|
||||||
/// ОДНОЙ из компаний инициатора (HeadType.CompanyDirectors) — обычно с ТОО TANSU
|
/// ОДНОЙ из компаний инициатора (HeadType.CompanyDirectors) — обычно с ТОО TANSU
|
||||||
/// Construction. Оба шага в маршруте законны сами по себе, и оба должны попасть в
|
/// Construction. Оба шага в маршруте законны сами по себе, но если так, из
|
||||||
/// результат — это НЕ дубликат одного согласования, а два разных: сначала блок
|
/// CompanyDirectors нужно убрать именно ЭТУ строку — не смешивать её с шагом
|
||||||
/// директоров компаний параллельно, а затем — отдельно — Директор Tansu, даже если
|
/// DirectorTansu через обычное схлопывание по email (см. CollapseByExecutorEmail):
|
||||||
/// физически это тот же человек. Поэтому строку DirectorTansu с таким совпадением
|
/// тот считает разные PositionCode у одного email конфликтом и пишет
|
||||||
/// НЕ пускаем через обычное схлопывание по email (см. CollapseByExecutorEmail) —
|
/// предупреждение, а здесь это не конфликт, а один и тот же человек — оставляем
|
||||||
/// оно бы решило, что это конфликт PositionCode одного согласующего, и либо
|
/// только шаг DirectorTansu, а дубликат из CompanyDirectors удаляем.
|
||||||
/// смержило бы её с строкой CompanyDirectors, либо просто выбрало одну из них.
|
|
||||||
/// Вместо этого явно переставляем её OrderNo на «после всего блока
|
|
||||||
/// CompanyDirectors» — не параллельно ему, а следом.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>
|
private static IReadOnlyList<ResolvedRouteStepDto> RemoveCompanyDirectorsDuplicatedByDirectorTansu(
|
||||||
/// (toCollapse, directorTansuAfterCompanyDirectors): первый список идёт через
|
IReadOnlyList<ResolvedRouteStepDto> steps)
|
||||||
/// обычное схлопывание по email, второй — строки DirectorTansu, которые нужно
|
|
||||||
/// просто дописать в конец результата без схлопывания.
|
|
||||||
/// </returns>
|
|
||||||
private static (IReadOnlyList<ResolvedRouteStepDto> ToCollapse, IReadOnlyList<ResolvedRouteStepDto> DirectorTansuAfterCompanyDirectors)
|
|
||||||
SplitDirectorTansuAfterCompanyDirectors(IReadOnlyList<ResolvedRouteStepDto> steps)
|
|
||||||
{
|
{
|
||||||
var companyDirectorSteps = steps.Where(s => s.PositionCode == HeadType.CompanyDirectors).ToList();
|
var directorTansuEmails = steps
|
||||||
var companyDirectorEmails = companyDirectorSteps
|
.Where(s => s.PositionCode == HeadType.DirectorTansu && !string.IsNullOrWhiteSpace(s.ExecutorEmail))
|
||||||
.Where(s => !string.IsNullOrWhiteSpace(s.ExecutorEmail))
|
|
||||||
.Select(s => s.ExecutorEmail!)
|
.Select(s => s.ExecutorEmail!)
|
||||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (companyDirectorEmails.Count == 0)
|
if (directorTansuEmails.Count == 0)
|
||||||
return (steps, []);
|
return steps;
|
||||||
|
|
||||||
var maxCompanyDirectorsOrderNo = companyDirectorSteps.Max(s => s.OrderNo);
|
return steps
|
||||||
|
.Where(s => s.PositionCode != HeadType.CompanyDirectors
|
||||||
var toCollapse = new List<ResolvedRouteStepDto>();
|
|| string.IsNullOrWhiteSpace(s.ExecutorEmail)
|
||||||
var afterBlock = new List<ResolvedRouteStepDto>();
|
|| !directorTansuEmails.Contains(s.ExecutorEmail))
|
||||||
|
.ToList();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+11
-17
@@ -900,12 +900,12 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
|||||||
resolved.Value.Steps[0].Warning.ShouldBeNull();
|
resolved.Value.Steps[0].Warning.ShouldBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(DisplayName = "Резолв: если Директор Tansu совпадает с одним из Директоров компаний, он идёт не параллельно блоку, а сразу после него")]
|
[Fact(DisplayName = "Резолв: если Директор Tansu совпадает с одним из Директоров компаний, эта строка убирается из CompanyDirectors")]
|
||||||
public async Task ResolveAsync_DirectorTansuMatchesOneOfCompanyDirectors_SequencedAfterCompanyDirectorsBlock()
|
public async Task ResolveAsync_DirectorTansuMatchesOneOfCompanyDirectors_RemovesDuplicateFromCompanyDirectors()
|
||||||
{
|
{
|
||||||
// Arrange — директор ТОО TANSU Construction (из шага CompanyDirectors, OrderNo 5)
|
// Arrange — директор ТОО TANSU Construction (из шага CompanyDirectors, OrderNo 5)
|
||||||
// и Директор Tansu (из шага DirectorTansu, сам сконфигурирован с OrderNo 10) —
|
// и Директор Tansu (из шага DirectorTansu, OrderNo 10) — один и тот же человек
|
||||||
// один и тот же человек с одним и тем же email.
|
// с одним и тем же email.
|
||||||
var portalClient = new Mock<ITnsuPortalClient>();
|
var portalClient = new Mock<ITnsuPortalClient>();
|
||||||
portalClient
|
portalClient
|
||||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
@@ -949,21 +949,15 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
|||||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
// Assert — все три строки остаются (директор не пропадает и не схлопывается
|
// Assert — вместо трёх строк (2 CompanyDirectors + 1 DirectorTansu) остаётся
|
||||||
// молча), но Директор Tansu не параллелен блоку CompanyDirectors (OrderNo 5),
|
// две: KazPromService-директор и Директор Tansu, без предупреждения о конфликте.
|
||||||
// а идёт сразу после него — OrderNo 6, а не свой сконфигурированный 10.
|
|
||||||
resolved.IsSuccess.ShouldBeTrue();
|
resolved.IsSuccess.ShouldBeTrue();
|
||||||
resolved.Value.Steps.Count.ShouldBe(3);
|
resolved.Value.Steps.Count.ShouldBe(2);
|
||||||
var tansuStep = resolved.Value.Steps.Single(s => s.PositionCode == HeadType.DirectorTansu);
|
var tansuStep = resolved.Value.Steps.Single(s => s.ExecutorEmail == "director.tansu@tnsu.kz");
|
||||||
tansuStep.ExecutorEmail.ShouldBe("director.tansu@tnsu.kz");
|
tansuStep.PositionCode.ShouldBe(HeadType.DirectorTansu);
|
||||||
tansuStep.OrderNo.ShouldBe(6);
|
tansuStep.OrderNo.ShouldBe(10);
|
||||||
tansuStep.Warning.ShouldBeNull();
|
tansuStep.Warning.ShouldBeNull();
|
||||||
|
resolved.Value.Steps.ShouldContain(s => s.ExecutorEmail == "mechanic.kps@tnsu.kz" && s.PositionCode == HeadType.CompanyDirectors);
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(DisplayName = "Резолв шага с типом руководителя Chief возвращает warning, если руководитель не найден")]
|
[Fact(DisplayName = "Резолв шага с типом руководителя Chief возвращает warning, если руководитель не найден")]
|
||||||
|
|||||||
Reference in New Issue
Block a user