апи для работы с маршрутами согласования #2
+4
-4
@@ -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,
|
||||
|
||||
+35
-4
@@ -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)
|
||||
|
||||
+12
-4
@@ -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("Должно быть заполнено хотя бы одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user