правки

This commit is contained in:
gorbenko.pavel
2026-07-24 17:54:34 +05:00
parent 3ae1965c29
commit 5928377465
4 changed files with 88 additions and 12 deletions
@@ -16,8 +16,8 @@ public sealed record TemplateRouteStepDto(
public sealed record CreateTemplateRouteStepRequest(
int OrderNo,
string ExecutorEmail,
string Position,
string? ExecutorEmail,
string? Position,
ProjectRole ProjectRole,
HeadType HeadType,
RouteStepStatus Status,
@@ -25,8 +25,8 @@ public sealed record CreateTemplateRouteStepRequest(
public sealed record UpdateTemplateRouteStepRequest(
int OrderNo,
string ExecutorEmail,
string Position,
string? ExecutorEmail,
string? Position,
ProjectRole ProjectRole,
HeadType HeadType,
RouteStepStatus Status,
@@ -41,12 +41,21 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
var executorEmail = r.ExecutorEmail ?? string.Empty;
var position = r.Position ?? string.Empty;
var duplicate = await HasDuplicateAsync(
route.Id, executorEmail, position, r.ProjectRole, r.HeadType, r.Status, excludeId: null, cancellationToken);
if (duplicate)
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
var entity = new TemplateRouteStep
{
RouteId = route.Id,
OrderNo = r.OrderNo,
ExecutorEmail = r.ExecutorEmail,
Position = r.Position,
ExecutorEmail = executorEmail,
Position = position,
ProjectRole = r.ProjectRole,
HeadType = r.HeadType,
Status = r.Status,
@@ -73,9 +82,18 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
var executorEmail = r.ExecutorEmail ?? string.Empty;
var position = r.Position ?? string.Empty;
var duplicate = await HasDuplicateAsync(
route.Id, executorEmail, position, r.ProjectRole, r.HeadType, r.Status, excludeId: entity.Id, cancellationToken);
if (duplicate)
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
entity.OrderNo = r.OrderNo;
entity.ExecutorEmail = r.ExecutorEmail;
entity.Position = r.Position;
entity.ExecutorEmail = executorEmail;
entity.Position = position;
entity.ProjectRole = r.ProjectRole;
entity.HeadType = r.HeadType;
entity.Status = r.Status;
@@ -97,6 +115,19 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
return Result.Ok();
}
private Task<bool> HasDuplicateAsync(
int routeId, string executorEmail, string position, ProjectRole projectRole, HeadType headType,
RouteStepStatus status, int? excludeId, CancellationToken cancellationToken) =>
db.TemplateRouteSteps.AnyAsync(x =>
x.Id != excludeId &&
x.RouteId == routeId &&
x.ExecutorEmail == executorEmail &&
x.Position == position &&
x.ProjectRole == projectRole &&
x.HeadType == headType &&
x.Status == status,
cancellationToken);
// Для маршрута «по проекту» без единого шага с ролью в проекте DTO несёт предупреждение в поле Warning.
private async Task<TemplateRouteStepDto> ToDtoWithWarningAsync(
TemplateRouteStep entity, TemplateRoute route, CancellationToken cancellationToken)
@@ -8,8 +8,12 @@ public sealed class CreateTemplateRouteStepRequestValidator : AbstractValidator<
public CreateTemplateRouteStepRequestValidator()
{
RuleFor(x => x)
.Must(x => !(string.IsNullOrWhiteSpace(x.ExecutorEmail) && string.IsNullOrWhiteSpace(x.Position) && x.ProjectRole == ProjectRole.None))
.WithMessage("Исполнитель, должность и роль в проекте не могут быть пустыми одновременно.");
.Must(x =>
!string.IsNullOrWhiteSpace(x.ExecutorEmail) ||
!string.IsNullOrWhiteSpace(x.Position) ||
x.ProjectRole != ProjectRole.None ||
x.HeadType != HeadType.None)
.WithMessage("Должно быть заполнено хотя бы одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
}
}
@@ -18,7 +22,11 @@ public sealed class UpdateTemplateRouteStepRequestValidator : AbstractValidator<
public UpdateTemplateRouteStepRequestValidator()
{
RuleFor(x => x)
.Must(x => !(string.IsNullOrWhiteSpace(x.ExecutorEmail) && string.IsNullOrWhiteSpace(x.Position) && x.ProjectRole == ProjectRole.None))
.WithMessage("Исполнитель, должность и роль в проекте не могут быть пустыми одновременно.");
.Must(x =>
!string.IsNullOrWhiteSpace(x.ExecutorEmail) ||
!string.IsNullOrWhiteSpace(x.Position) ||
x.ProjectRole != ProjectRole.None ||
x.HeadType != HeadType.None)
.WithMessage("Должно быть заполнено хотя бы одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
}
}
@@ -65,6 +65,43 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
result.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Повторное создание шага с теми же исполнителем, должностью, ролью, типом руководителя и статусом возвращает конфликт")]
public async Task CreateAsync_DuplicateStep_ReturnsFailedResult()
{
// Arrange
var routeId = await CreateRouteAsync("Route with duplicate step", byProject: false);
var request = new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.Chief, RouteStepStatus.Approval, false);
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", ProjectRole.None, HeadType.Chief, RouteStepStatus.Approval, true),
CancellationToken.None);
// Assert
duplicate.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Создание шага только с типом руководителя (без исполнителя, должности и роли) успешно")]
public async Task CreateAsync_HeadTypeOnly_ReturnsCreatedStep()
{
// Arrange
var routeId = await CreateRouteAsync("Head type only route", byProject: false);
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.None, HeadType.Chief, RouteStepStatus.Approval, false),
CancellationToken.None);
// Assert
created.IsSuccess.ShouldBeTrue();
created.Value.HeadType.ShouldBe(HeadType.Chief);
created.Value.ExecutorEmail.ShouldBe(string.Empty);
}
[Fact(DisplayName = "Создание шага маршрута и получение его по Id возвращает сохранённые данные")]
public async Task CreateAsync_ThenGetAsync_ReturnsCreatedStep()
{