Несколько условий шага, объединяемых через И/ИЛИ

Раньше шаг маршрута мог нести ровно одно условие (field_name/operation/
value). Теперь условий может быть сколько угодно, а их объединение задаётся
явно — ConditionsLogic.And (все условия) или .Or (любое из условий).

Бэкенд:
- Новая сущность TemplateRouteStepCondition (дочерняя таблица
  template_route_step_conditions, FK на шаг, cascade delete) вместо трёх
  колонок на TemplateRouteStep. Миграция AddMultipleStepConditions переносит
  существующие условия шагов в новую таблицу перед удалением старых колонок,
  чтобы не потерять уже настроенные в деве/проде условия.
- TemplateRouteService.MatchesConditions: несколько условий шага объединяются
  по ConditionsLogic (Any/All); HasUnevaluatableManagerCondition проверяет
  условие на IsManager в списке условий, а не единственное поле.
- TemplateRouteStepService: дубликаты шагов теперь сравниваются по всему
  набору условий (без учёта порядка) и, если условий два и более, по логике
  их объединения — сравнение в памяти, а не в SQL, кандидатов на дубликат
  в пределах маршрута всегда мало.
- Валидация условия: раньше «все три поля или ни одного», теперь — в каждом
  элементе списка все три поля обязательны.

Фронтенд:
- RouteStepFormModal: условие — динамический список строк (добавить/удалить),
  у каждой строки свой набор допустимых операторов по типу колонки; селектор
  И/ИЛИ появляется, когда условий два и больше.
- describeStepCondition в таблице шагов склеивает все условия через « И »/« ИЛИ ».

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-08-25 09:56:36 +05:00
parent e4a6cb8167
commit 10c3057eff
21 changed files with 1201 additions and 346 deletions
@@ -169,7 +169,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -204,8 +204,8 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(
1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null,
stepSla, stepAsk),
1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null,
SlaHours: stepSla, AskOverdueReason: stepAsk),
CancellationToken.None);
var resolved = await _service.ResolveAsync(
@@ -283,15 +283,15 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, "dup@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "dup@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(2, "other@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(2, "other@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(3, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(3, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -320,7 +320,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action — departmentId в запросе не передан
@@ -362,7 +362,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
CancellationToken.None);
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -402,7 +402,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
CancellationToken.None);
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -431,7 +431,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
CancellationToken.None);
// Action
@@ -458,11 +458,11 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
CancellationToken.None);
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(2, "always@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(2, "always@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -489,7 +489,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
CancellationToken.None);
// Action — fieldValues не переданы вовсе
@@ -514,7 +514,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, "urgent@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Priority", ComparisonOperator.Equal, "Urgent"),
new CreateTemplateRouteStepRequest(1, "urgent@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent") }),
CancellationToken.None);
// Action
@@ -548,7 +548,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, "text-condition@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Priority", operation, expected),
new CreateTemplateRouteStepRequest(1, "text-condition@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Priority", operation, expected) }),
CancellationToken.None);
// Action
@@ -562,6 +562,47 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
resolved.Value.Steps.Count.ShouldBe(shouldMatch ? 1 : 0);
}
[Theory(DisplayName = "Резолв маршрута с несколькими условиями шага, объединёнными через И/ИЛИ")]
// И: оба условия должны выполниться.
[InlineData(ConditionsLogic.And, "1500", "Urgent", true)]
[InlineData(ConditionsLogic.And, "500", "Urgent", false)]
[InlineData(ConditionsLogic.And, "1500", "Normal", false)]
// ИЛИ: достаточно одного.
[InlineData(ConditionsLogic.Or, "500", "Urgent", true)]
[InlineData(ConditionsLogic.Or, "1500", "Normal", true)]
[InlineData(ConditionsLogic.Or, "500", "Normal", false)]
public async Task ResolveAsync_MultipleConditions_CombinesWithConfiguredLogic(
ConditionsLogic logic, string amount, string priority, bool shouldMatch)
{
// Arrange
var requestTypeId = await CreateRequestTypeWithListAsync($"Multi-condition request {logic} {amount} {priority}");
var route = await _service.CreateAsync(
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None);
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, "multi-condition@tnsu.kz", null, null, null, RouteStepStatus.Approval, false,
new[]
{
new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000"),
new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent")
},
logic),
CancellationToken.None);
// Action
var resolved = await _service.ResolveAsync(
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval,
[new ResolveFieldValueDto("Amount", amount), new ResolveFieldValueDto("Priority", priority)]),
CancellationToken.None);
// Assert
resolved.IsSuccess.ShouldBeTrue();
resolved.Value.Steps.Count.ShouldBe(shouldMatch ? 1 : 0);
}
[Fact(DisplayName = "Резолв маршрута с ролью в проекте подставляет исполнителя из проекта")]
public async Task ResolveAsync_ByProjectRole_UsesProjectExecutorEmail()
{
@@ -574,7 +615,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -612,7 +653,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
CancellationToken.None);
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, "Lead", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, "Lead", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -651,7 +692,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
CancellationToken.None);
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -686,7 +727,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
CancellationToken.None);
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -713,7 +754,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -742,7 +783,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, "Несуществующая должность", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, "Несуществующая должность", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -771,7 +812,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync(
route.Value.Id,
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.Security, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.Security, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -43,7 +43,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var result = await _service.CreateAsync(
999999,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Assert
@@ -59,7 +59,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var result = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Assert
@@ -71,20 +71,79 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
{
// Arrange
var routeId = await CreateRouteAsync("Route with duplicate step", byProject: false);
var request = new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null);
var request = new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, false, null);
var first = await _service.CreateAsync(routeId, request, CancellationToken.None);
first.IsSuccess.ShouldBeTrue();
// Action
var duplicate = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, true, null, null, null),
new CreateTemplateRouteStepRequest(2, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, true, null),
CancellationToken.None);
// Assert
duplicate.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Порядок условий и логика их объединения не влияют на определение дубликата")]
public async Task CreateAsync_DuplicateStepWithSameConditionsInDifferentOrder_ReturnsFailedResult()
{
// Arrange
var routeId = await CreateRouteAsync("Route with duplicate conditions", byProject: false, listName: "Requests");
var first = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false,
new[]
{
new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000"),
new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent")
},
ConditionsLogic.And),
CancellationToken.None);
first.IsSuccess.ShouldBeTrue();
// Action — те же два условия, в обратном порядке, но с той же логикой И
var duplicate = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, "a@b.kz", null, null, null, RouteStepStatus.Approval, false,
new[]
{
new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent"),
new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000")
},
ConditionsLogic.And),
CancellationToken.None);
// Assert
duplicate.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Те же условия, но с другой логикой объединения (И/ИЛИ), — не дубликат")]
public async Task CreateAsync_SameConditionsWithDifferentLogic_DoesNotConflict()
{
// Arrange
var routeId = await CreateRouteAsync("Route with different conditions logic", byProject: false, listName: "Requests");
var conditions = new[]
{
new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000"),
new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent")
};
var first = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, conditions, ConditionsLogic.And),
CancellationToken.None);
first.IsSuccess.ShouldBeTrue();
// Action — те же условия, но объединены через ИЛИ, а не И
var second = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, conditions, ConditionsLogic.Or),
CancellationToken.None);
// Assert
second.IsSuccess.ShouldBeTrue();
}
[Fact(DisplayName = "Второй шаг с тем же типом руководителя в том же маршруте — конфликт, даже с другим статусом/переназначением")]
public async Task CreateAsync_SecondStepWithSameHeadType_ReturnsFailedResult()
{
@@ -94,14 +153,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route with duplicate head type", byProject: false);
var first = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
CancellationToken.None);
first.IsSuccess.ShouldBeTrue();
// Action — другой статус (Execution вместо Approval), но тот же HeadType.Chief
var second = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Execution, true, null, null, null),
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Execution, true, null),
CancellationToken.None);
// Assert
@@ -115,14 +174,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route with duplicate department head", byProject: false);
var first = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null),
CancellationToken.None);
first.IsSuccess.ShouldBeTrue();
// Action
var second = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Execution, false, null, null, null),
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Execution, false, null),
CancellationToken.None);
// Assert
@@ -138,11 +197,11 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var chief = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
CancellationToken.None);
var directorTansu = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Assert
@@ -160,11 +219,11 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var small = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.LessThan, "1000"),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.LessThan, "1000") }),
CancellationToken.None);
var big = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
CancellationToken.None);
// Assert
@@ -181,11 +240,11 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var small = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.LessThan, "1000"),
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.LessThan, "1000") }),
CancellationToken.None);
var big = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
new CreateTemplateRouteStepRequest(2, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
CancellationToken.None);
// Assert
@@ -200,14 +259,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route with head type same condition", byProject: false, listName: "Requests");
var first = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
CancellationToken.None);
first.IsSuccess.ShouldBeTrue();
// Action — то же условие, другой статус
var second = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Execution, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Execution, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
CancellationToken.None);
// Assert
@@ -221,14 +280,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to update head type on itself", byProject: false);
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action — HasDuplicateHeadTypeAsync должен исключать сам обновляемый шаг (excludeId)
var updated = await _service.UpdateAsync(
routeId,
created.Value.Id,
new UpdateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Execution, true, null, null, null),
new UpdateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Execution, true, null),
CancellationToken.None);
// Assert
@@ -244,7 +303,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Assert
@@ -262,7 +321,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Assert
@@ -282,7 +341,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Assert
@@ -300,7 +359,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Assert
@@ -315,14 +374,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to update step", byProject: false);
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
var updated = await _service.UpdateAsync(
routeId,
created.Value.Id,
new UpdateTemplateRouteStepRequest(2, "b@b.kz", "Senior Engineer", null, HeadType.Chief, RouteStepStatus.Execution, true, null, null, null),
new UpdateTemplateRouteStepRequest(2, "b@b.kz", "Senior Engineer", null, HeadType.Chief, RouteStepStatus.Execution, true, null),
CancellationToken.None);
// Assert
@@ -339,14 +398,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to patch step", byProject: false);
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
var patched = await _service.PatchAsync(
routeId,
created.Value.Id,
new PatchTemplateRouteStepRequest(OrderNo: 2, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.Execution, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
new PatchTemplateRouteStepRequest(OrderNo: 2, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.Execution, AllowReassignToOtherApprovers: null, Conditions: null),
CancellationToken.None);
// Assert
@@ -363,14 +422,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to patch step into conflict", byProject: false);
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
var patched = await _service.PatchAsync(
routeId,
created.Value.Id,
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: "a@b.kz", Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: "a@b.kz", Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, Conditions: null),
CancellationToken.None);
// Assert
@@ -384,14 +443,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to patch step status none", byProject: false);
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
var patched = await _service.PatchAsync(
routeId,
created.Value.Id,
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.None, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.None, AllowReassignToOtherApprovers: null, Conditions: null),
CancellationToken.None);
// Assert
@@ -405,14 +464,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route with explicit None values on patch", byProject: false);
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
var patched = await _service.PatchAsync(
routeId,
created.Value.Id,
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: ProjectRole.None, HeadType: HeadType.None, Status: null, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: ProjectRole.None, HeadType: HeadType.None, Status: null, AllowReassignToOtherApprovers: null, Conditions: null),
CancellationToken.None);
// Assert
@@ -420,16 +479,17 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
patched.Value.ExecutorEmail.ShouldBe("a@b.kz");
}
[Fact(DisplayName = "Создание шага с частично заполненными полями условия возвращает неуспешный результат")]
[Fact(DisplayName = "Создание шага с условием без значения возвращает неуспешный результат")]
public async Task CreateAsync_ConditionFieldsPartiallyFilled_ReturnsFailedResult()
{
// Arrange
var routeId = await CreateRouteAsync("Route with partial condition", byProject: false, listName: "Requests");
// Action
// Action — оператор задан, но value пуст: условие наполовину заполнено
var result = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, "Amount", null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false,
new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "") }),
CancellationToken.None);
// Assert
@@ -445,7 +505,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var result = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
CancellationToken.None);
// Assert
@@ -461,14 +521,15 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
CancellationToken.None);
// Assert
created.IsSuccess.ShouldBeTrue();
created.Value.FieldName.ShouldBe("Amount");
created.Value.Operation.ShouldBe(ComparisonOperator.GreaterThan);
created.Value.Value.ShouldBe("1000");
created.Value.Conditions.ShouldHaveSingleItem();
created.Value.Conditions[0].FieldName.ShouldBe("Amount");
created.Value.Conditions[0].Operation.ShouldBe(ComparisonOperator.GreaterThan);
created.Value.Conditions[0].Value.ShouldBe("1000");
}
[Fact(DisplayName = "Патч, после которого поля условия заполнены частично, возвращает неуспешный результат")]
@@ -478,14 +539,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to patch condition into conflict", byProject: false, listName: "Requests");
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
var patched = await _service.PatchAsync(
routeId,
created.Value.Id,
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, FieldName: "Amount", Operation: null, Value: null),
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, Conditions: new[] { new RouteStepConditionDto("Amount", ComparisonOperator.None, "") }),
CancellationToken.None);
// Assert
@@ -499,7 +560,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to delete step", byProject: false);
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action
@@ -520,16 +581,16 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeBId = await CreateRouteAsync("Route B for executor search", byProject: false);
await _service.CreateAsync(
routeAId,
new CreateTemplateRouteStepRequest(1, "Target@Example.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "Target@Example.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
await _service.CreateAsync(
routeBId,
new CreateTemplateRouteStepRequest(1, "target@example.kz", "Manager", null, null, RouteStepStatus.Execution, false, null, null, null),
new CreateTemplateRouteStepRequest(1, "target@example.kz", "Manager", null, null, RouteStepStatus.Execution, false, null),
CancellationToken.None);
// Другой исполнитель — не должен попасть в выдачу.
await _service.CreateAsync(
routeAId,
new CreateTemplateRouteStepRequest(2, "other@example.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
new CreateTemplateRouteStepRequest(2, "other@example.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
CancellationToken.None);
// Action