правки
This commit is contained in:
+4
-4
@@ -16,8 +16,8 @@ public sealed record TemplateRouteStepDto(
|
|||||||
|
|
||||||
public sealed record CreateTemplateRouteStepRequest(
|
public sealed record CreateTemplateRouteStepRequest(
|
||||||
int OrderNo,
|
int OrderNo,
|
||||||
string ExecutorEmail,
|
string? ExecutorEmail,
|
||||||
string Position,
|
string? Position,
|
||||||
ProjectRole ProjectRole,
|
ProjectRole ProjectRole,
|
||||||
HeadType HeadType,
|
HeadType HeadType,
|
||||||
RouteStepStatus Status,
|
RouteStepStatus Status,
|
||||||
@@ -25,8 +25,8 @@ public sealed record CreateTemplateRouteStepRequest(
|
|||||||
|
|
||||||
public sealed record UpdateTemplateRouteStepRequest(
|
public sealed record UpdateTemplateRouteStepRequest(
|
||||||
int OrderNo,
|
int OrderNo,
|
||||||
string ExecutorEmail,
|
string? ExecutorEmail,
|
||||||
string Position,
|
string? Position,
|
||||||
ProjectRole ProjectRole,
|
ProjectRole ProjectRole,
|
||||||
HeadType HeadType,
|
HeadType HeadType,
|
||||||
RouteStepStatus Status,
|
RouteStepStatus Status,
|
||||||
|
|||||||
+35
-4
@@ -41,12 +41,21 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||||
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
|
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (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
|
var entity = new TemplateRouteStep
|
||||||
{
|
{
|
||||||
RouteId = route.Id,
|
RouteId = route.Id,
|
||||||
OrderNo = r.OrderNo,
|
OrderNo = r.OrderNo,
|
||||||
ExecutorEmail = r.ExecutorEmail,
|
ExecutorEmail = executorEmail,
|
||||||
Position = r.Position,
|
Position = position,
|
||||||
ProjectRole = r.ProjectRole,
|
ProjectRole = r.ProjectRole,
|
||||||
HeadType = r.HeadType,
|
HeadType = r.HeadType,
|
||||||
Status = r.Status,
|
Status = r.Status,
|
||||||
@@ -73,9 +82,18 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||||
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
|
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (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.OrderNo = r.OrderNo;
|
||||||
entity.ExecutorEmail = r.ExecutorEmail;
|
entity.ExecutorEmail = executorEmail;
|
||||||
entity.Position = r.Position;
|
entity.Position = position;
|
||||||
entity.ProjectRole = r.ProjectRole;
|
entity.ProjectRole = r.ProjectRole;
|
||||||
entity.HeadType = r.HeadType;
|
entity.HeadType = r.HeadType;
|
||||||
entity.Status = r.Status;
|
entity.Status = r.Status;
|
||||||
@@ -97,6 +115,19 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
|||||||
return Result.Ok();
|
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.
|
// Для маршрута «по проекту» без единого шага с ролью в проекте DTO несёт предупреждение в поле Warning.
|
||||||
private async Task<TemplateRouteStepDto> ToDtoWithWarningAsync(
|
private async Task<TemplateRouteStepDto> ToDtoWithWarningAsync(
|
||||||
TemplateRouteStep entity, TemplateRoute route, CancellationToken cancellationToken)
|
TemplateRouteStep entity, TemplateRoute route, CancellationToken cancellationToken)
|
||||||
|
|||||||
+12
-4
@@ -8,8 +8,12 @@ public sealed class CreateTemplateRouteStepRequestValidator : AbstractValidator<
|
|||||||
public CreateTemplateRouteStepRequestValidator()
|
public CreateTemplateRouteStepRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x)
|
RuleFor(x => x)
|
||||||
.Must(x => !(string.IsNullOrWhiteSpace(x.ExecutorEmail) && string.IsNullOrWhiteSpace(x.Position) && x.ProjectRole == ProjectRole.None))
|
.Must(x =>
|
||||||
.WithMessage("Исполнитель, должность и роль в проекте не могут быть пустыми одновременно.");
|
!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()
|
public UpdateTemplateRouteStepRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x)
|
RuleFor(x => x)
|
||||||
.Must(x => !(string.IsNullOrWhiteSpace(x.ExecutorEmail) && string.IsNullOrWhiteSpace(x.Position) && x.ProjectRole == ProjectRole.None))
|
.Must(x =>
|
||||||
.WithMessage("Исполнитель, должность и роль в проекте не могут быть пустыми одновременно.");
|
!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();
|
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 возвращает сохранённые данные")]
|
[Fact(DisplayName = "Создание шага маршрута и получение его по Id возвращает сохранённые данные")]
|
||||||
public async Task CreateAsync_ThenGetAsync_ReturnsCreatedStep()
|
public async Task CreateAsync_ThenGetAsync_ReturnsCreatedStep()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user