поправлены дубли шагов
This commit is contained in:
+37
-18
@@ -51,14 +51,15 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id)."));
|
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id)."));
|
||||||
|
|
||||||
var duplicate = await HasDuplicateAsync(
|
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)
|
if (duplicate)
|
||||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
return Result.Fail<TemplateRouteStepDto>(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<TemplateRouteStepDto>(new ConflictError(
|
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||||
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» — второй такой шаг недопустим."));
|
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим."));
|
||||||
|
|
||||||
var entity = new TemplateRouteStep
|
var entity = new TemplateRouteStep
|
||||||
{
|
{
|
||||||
@@ -104,14 +105,15 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id)."));
|
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id)."));
|
||||||
|
|
||||||
var duplicate = await HasDuplicateAsync(
|
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)
|
if (duplicate)
|
||||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
return Result.Fail<TemplateRouteStepDto>(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<TemplateRouteStepDto>(new ConflictError(
|
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||||
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» — второй такой шаг недопустим."));
|
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим."));
|
||||||
|
|
||||||
entity.OrderNo = r.OrderNo;
|
entity.OrderNo = r.OrderNo;
|
||||||
entity.ExecutorEmail = r.ExecutorEmail;
|
entity.ExecutorEmail = r.ExecutorEmail;
|
||||||
@@ -168,14 +170,15 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id)."));
|
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id)."));
|
||||||
|
|
||||||
var duplicate = await HasDuplicateAsync(
|
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)
|
if (duplicate)
|
||||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
return Result.Fail<TemplateRouteStepDto>(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<TemplateRouteStepDto>(new ConflictError(
|
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||||
$"В маршруте уже есть шаг с типом руководителя «{headType}» — второй такой шаг недопустим."));
|
$"В маршруте уже есть шаг с типом руководителя «{headType}» и таким же условием появления — второй такой шаг недопустим."));
|
||||||
|
|
||||||
entity.OrderNo = r.OrderNo ?? entity.OrderNo;
|
entity.OrderNo = r.OrderNo ?? entity.OrderNo;
|
||||||
entity.ExecutorEmail = executorEmail;
|
entity.ExecutorEmail = executorEmail;
|
||||||
@@ -207,9 +210,16 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
private static bool RequiresList(string? fieldName, ComparisonOperator? operation, string? value) =>
|
private static bool RequiresList(string? fieldName, ComparisonOperator? operation, string? value) =>
|
||||||
TemplateRouteStepFields.CountConditionFilled(fieldName, operation, value) > 0;
|
TemplateRouteStepFields.CountConditionFilled(fieldName, operation, value) > 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Шаги с одинаковым исполнителем считаются дубликатами только при одинаковом
|
||||||
|
/// условии появления (field_name/operation/value): шаги с разными условиями
|
||||||
|
/// взаимоисключающи по данным заявки, поэтому в один маршрут они попасть могут,
|
||||||
|
/// а в один резолв — нет.
|
||||||
|
/// </summary>
|
||||||
private Task<bool> HasDuplicateAsync(
|
private Task<bool> HasDuplicateAsync(
|
||||||
int routeId, string? executorEmail, string? position, ProjectRole? projectRole, HeadType? headType,
|
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 =>
|
db.TemplateRouteSteps.AnyAsync(x =>
|
||||||
x.Id != excludeId &&
|
x.Id != excludeId &&
|
||||||
x.RouteId == routeId &&
|
x.RouteId == routeId &&
|
||||||
@@ -217,7 +227,10 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
x.Position == position &&
|
x.Position == position &&
|
||||||
x.ProjectRole == projectRole &&
|
x.ProjectRole == projectRole &&
|
||||||
x. HeadType == headType &&
|
x. HeadType == headType &&
|
||||||
x.Status == status,
|
x.Status == status &&
|
||||||
|
x.FieldName == fieldName &&
|
||||||
|
x.Operation == operation &&
|
||||||
|
x.Value == value,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -229,11 +242,14 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
/// в одного и того же (или неотличимо какого) руководителя — например, «два
|
/// в одного и того же (или неотличимо какого) руководителя — например, «два
|
||||||
/// разных директора одной и той же компании» просто не существуют. В отличие
|
/// разных директора одной и той же компании» просто не существуют. В отличие
|
||||||
/// от HasDuplicateAsync (точное совпадение ВСЕХ полей включая статус), здесь
|
/// от HasDuplicateAsync (точное совпадение ВСЕХ полей включая статус), здесь
|
||||||
/// достаточно самого факта совпадения HeadType — вне зависимости от статуса
|
/// достаточно совпадения HeadType — вне зависимости от статуса или остальных
|
||||||
/// или остальных полей шага.
|
/// полей шага. Исключение — условие появления шага: при разных условиях
|
||||||
|
/// в конкретный резолв попадёт только один из шагов, так что одного и того же
|
||||||
|
/// руководителя дважды в маршруте не окажется.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Task<bool> HasDuplicateHeadTypeAsync(
|
private Task<bool> 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)
|
if (headType is null or HeadType.None)
|
||||||
return Task.FromResult(false);
|
return Task.FromResult(false);
|
||||||
@@ -241,7 +257,10 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
return db.TemplateRouteSteps.AnyAsync(x =>
|
return db.TemplateRouteSteps.AnyAsync(x =>
|
||||||
x.Id != excludeId &&
|
x.Id != excludeId &&
|
||||||
x.RouteId == routeId &&
|
x.RouteId == routeId &&
|
||||||
x.HeadType == headType,
|
x.HeadType == headType &&
|
||||||
|
x.FieldName == fieldName &&
|
||||||
|
x.Operation == operation &&
|
||||||
|
x.Value == value,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+64
@@ -150,6 +150,70 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
|||||||
directorTansu.IsSuccess.ShouldBeTrue();
|
directorTansu.IsSuccess.ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(DisplayName = "Два шага с одним типом руководителя, но разными условиями появления — допустимы")]
|
||||||
|
public async Task CreateAsync_SameHeadTypeDifferentConditions_BothSucceed()
|
||||||
|
{
|
||||||
|
// Arrange — условия появления взаимоисключающи по данным заявки, поэтому
|
||||||
|
// в конкретный резолв попадёт только один из двух шагов.
|
||||||
|
var routeId = await CreateRouteAsync("Route with head type per condition", byProject: false, listName: "Requests");
|
||||||
|
|
||||||
|
// Action
|
||||||
|
var small = await _service.CreateAsync(
|
||||||
|
routeId,
|
||||||
|
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, "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"),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
small.IsSuccess.ShouldBeTrue();
|
||||||
|
big.IsSuccess.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(DisplayName = "Два шага с одним конкретным исполнителем, но разными условиями появления — допустимы")]
|
||||||
|
public async Task CreateAsync_SameExecutorDifferentConditions_BothSucceed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var routeId = await CreateRouteAsync("Route with executor per condition", byProject: false, listName: "Requests");
|
||||||
|
|
||||||
|
// Action
|
||||||
|
var small = await _service.CreateAsync(
|
||||||
|
routeId,
|
||||||
|
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, "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"),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
small.IsSuccess.ShouldBeTrue();
|
||||||
|
big.IsSuccess.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(DisplayName = "Два шага с одним типом руководителя и одинаковым условием появления — конфликт")]
|
||||||
|
public async Task CreateAsync_SameHeadTypeSameCondition_ReturnsFailedResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
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"),
|
||||||
|
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"),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
second.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(DisplayName = "Обновление шага, при котором HeadType не меняется, не конфликтует само с собой")]
|
[Fact(DisplayName = "Обновление шага, при котором HeadType не меняется, не конфликтует само с собой")]
|
||||||
public async Task UpdateAsync_SameHeadTypeOnSameStep_DoesNotConflictWithItself()
|
public async Task UpdateAsync_SameHeadTypeOnSameStep_DoesNotConflictWithItself()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user