From 509f26de7b3b85b9f646b1abc637d72b21fe7502 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Tue, 11 Aug 2026 10:07:41 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8=20=D1=88?= =?UTF-8?q?=D0=B0=D0=B3=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TemplateRouteStepService.cs | 55 ++++++++++------ .../TemplateRouteStepServiceTests.cs | 64 +++++++++++++++++++ 2 files changed, 101 insertions(+), 18 deletions(-) diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs index 62afa2f..8d6c11a 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs @@ -51,14 +51,15 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) "Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id).")); var duplicate = await HasDuplicateAsync( - route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, excludeId: null, cancellationToken); + route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, + r.FieldName, r.Operation, r.Value, excludeId: null, cancellationToken); if (duplicate) return Result.Fail(new ConflictError( - "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте.")); + "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя, статусом и условием появления уже существует в этом маршруте.")); - if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, excludeId: null, cancellationToken)) + if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.FieldName, r.Operation, r.Value, excludeId: null, cancellationToken)) return Result.Fail(new ConflictError( - $"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» — второй такой шаг недопустим.")); + $"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим.")); var entity = new TemplateRouteStep { @@ -104,14 +105,15 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) "Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id).")); var duplicate = await HasDuplicateAsync( - route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, excludeId: entity.Id, cancellationToken); + route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, + r.FieldName, r.Operation, r.Value, excludeId: entity.Id, cancellationToken); if (duplicate) return Result.Fail(new ConflictError( - "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте.")); + "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя, статусом и условием появления уже существует в этом маршруте.")); - if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, excludeId: entity.Id, cancellationToken)) + if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.FieldName, r.Operation, r.Value, excludeId: entity.Id, cancellationToken)) return Result.Fail(new ConflictError( - $"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» — второй такой шаг недопустим.")); + $"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим.")); entity.OrderNo = r.OrderNo; entity.ExecutorEmail = r.ExecutorEmail; @@ -168,14 +170,15 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) "Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id).")); var duplicate = await HasDuplicateAsync( - route.Id, executorEmail, position, projectRole, headType, status, excludeId: entity.Id, cancellationToken); + route.Id, executorEmail, position, projectRole, headType, status, + fieldName, operation, value, excludeId: entity.Id, cancellationToken); if (duplicate) return Result.Fail(new ConflictError( - "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте.")); + "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя, статусом и условием появления уже существует в этом маршруте.")); - if (await HasDuplicateHeadTypeAsync(route.Id, headType, excludeId: entity.Id, cancellationToken)) + if (await HasDuplicateHeadTypeAsync(route.Id, headType, fieldName, operation, value, excludeId: entity.Id, cancellationToken)) return Result.Fail(new ConflictError( - $"В маршруте уже есть шаг с типом руководителя «{headType}» — второй такой шаг недопустим.")); + $"В маршруте уже есть шаг с типом руководителя «{headType}» и таким же условием появления — второй такой шаг недопустим.")); entity.OrderNo = r.OrderNo ?? entity.OrderNo; entity.ExecutorEmail = executorEmail; @@ -207,9 +210,16 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) private static bool RequiresList(string? fieldName, ComparisonOperator? operation, string? value) => TemplateRouteStepFields.CountConditionFilled(fieldName, operation, value) > 0; + /// + /// Шаги с одинаковым исполнителем считаются дубликатами только при одинаковом + /// условии появления (field_name/operation/value): шаги с разными условиями + /// взаимоисключающи по данным заявки, поэтому в один маршрут они попасть могут, + /// а в один резолв — нет. + /// private Task HasDuplicateAsync( int routeId, string? executorEmail, string? position, ProjectRole? projectRole, HeadType? headType, - RouteStepStatus status, int? excludeId, CancellationToken cancellationToken) => + RouteStepStatus status, string? fieldName, ComparisonOperator? operation, string? value, + int? excludeId, CancellationToken cancellationToken) => db.TemplateRouteSteps.AnyAsync(x => x.Id != excludeId && x.RouteId == routeId && @@ -217,7 +227,10 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) x.Position == position && x.ProjectRole == projectRole && x. HeadType == headType && - x.Status == status, + x.Status == status && + x.FieldName == fieldName && + x.Operation == operation && + x.Value == value, cancellationToken); /// @@ -229,11 +242,14 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) /// в одного и того же (или неотличимо какого) руководителя — например, «два /// разных директора одной и той же компании» просто не существуют. В отличие /// от HasDuplicateAsync (точное совпадение ВСЕХ полей включая статус), здесь - /// достаточно самого факта совпадения HeadType — вне зависимости от статуса - /// или остальных полей шага. + /// достаточно совпадения HeadType — вне зависимости от статуса или остальных + /// полей шага. Исключение — условие появления шага: при разных условиях + /// в конкретный резолв попадёт только один из шагов, так что одного и того же + /// руководителя дважды в маршруте не окажется. /// private Task HasDuplicateHeadTypeAsync( - int routeId, HeadType? headType, int? excludeId, CancellationToken cancellationToken) + int routeId, HeadType? headType, string? fieldName, ComparisonOperator? operation, string? value, + int? excludeId, CancellationToken cancellationToken) { if (headType is null or HeadType.None) return Task.FromResult(false); @@ -241,7 +257,10 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) return db.TemplateRouteSteps.AnyAsync(x => x.Id != excludeId && x.RouteId == routeId && - x.HeadType == headType, + x.HeadType == headType && + x.FieldName == fieldName && + x.Operation == operation && + x.Value == value, cancellationToken); } diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs index c65ed41..05547da 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs @@ -150,6 +150,70 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture