апи для работы с маршрутами согласования #2

Merged
admin merged 38 commits from initial-api into main 2026-07-27 13:14:32 +00:00
12 changed files with 113 additions and 84 deletions
Showing only changes of commit 13f557e1f4 - Show all commits
@@ -5,10 +5,10 @@ namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
public sealed record TemplateRouteStepDto( public sealed record TemplateRouteStepDto(
int Id, int Id,
int OrderNo, int OrderNo,
string ExecutorEmail, string? ExecutorEmail,
string Position, string? Position,
ProjectRole ProjectRole, ProjectRole? ProjectRole,
HeadType HeadType, HeadType? HeadType,
RouteStepStatus Status, RouteStepStatus Status,
int RouteId, int RouteId,
bool AllowReassignToOtherApprovers, bool AllowReassignToOtherApprovers,
@@ -18,8 +18,8 @@ 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,
bool AllowReassignToOtherApprovers); bool AllowReassignToOtherApprovers);
@@ -27,8 +27,8 @@ 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,
bool AllowReassignToOtherApprovers); bool AllowReassignToOtherApprovers);
@@ -37,15 +37,12 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
if (route is null) if (route is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId)); return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
if (!route.ByProject && r.ProjectRole != ProjectRole.None) if (!route.ByProject && r.ProjectRole is not (null or ProjectRole.None))
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( var duplicate = await HasDuplicateAsync(
route.Id, executorEmail, position, r.ProjectRole, r.HeadType, r.Status, excludeId: null, cancellationToken); route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, excludeId: null, cancellationToken);
if (duplicate) if (duplicate)
return Result.Fail<TemplateRouteStepDto>(new ConflictError( return Result.Fail<TemplateRouteStepDto>(new ConflictError(
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте.")); "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
@@ -54,8 +51,8 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
{ {
RouteId = route.Id, RouteId = route.Id,
OrderNo = r.OrderNo, OrderNo = r.OrderNo,
ExecutorEmail = executorEmail, ExecutorEmail = r.ExecutorEmail,
Position = position, Position = r.Position,
ProjectRole = r.ProjectRole, ProjectRole = r.ProjectRole,
HeadType = r.HeadType, HeadType = r.HeadType,
Status = r.Status, Status = r.Status,
@@ -78,22 +75,19 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
if (entity is null) if (entity is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", id)); return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", id));
if (!route.ByProject && r.ProjectRole != ProjectRole.None) if (!route.ByProject && r.ProjectRole is not (null or ProjectRole.None))
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( var duplicate = await HasDuplicateAsync(
route.Id, executorEmail, position, r.ProjectRole, r.HeadType, r.Status, excludeId: entity.Id, cancellationToken); route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, excludeId: entity.Id, cancellationToken);
if (duplicate) if (duplicate)
return Result.Fail<TemplateRouteStepDto>(new ConflictError( return Result.Fail<TemplateRouteStepDto>(new ConflictError(
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте.")); "Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
entity.OrderNo = r.OrderNo; entity.OrderNo = r.OrderNo;
entity.ExecutorEmail = executorEmail; entity.ExecutorEmail = r.ExecutorEmail;
entity.Position = position; entity.Position = r.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;
@@ -120,14 +114,13 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
var headType = r.HeadType ?? entity.HeadType; var headType = r.HeadType ?? entity.HeadType;
var status = r.Status ?? entity.Status; var status = r.Status ?? entity.Status;
if (!route.ByProject && projectRole != ProjectRole.None) if (!route.ByProject && projectRole is not (null or ProjectRole.None))
return Result.Fail<TemplateRouteStepDto>(new ValidationError( return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false).")); "Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
if (string.IsNullOrWhiteSpace(executorEmail) && string.IsNullOrWhiteSpace(position) && if (TemplateRouteStepFields.CountFilled(executorEmail, position, projectRole, headType) != 1)
projectRole == ProjectRole.None && headType == HeadType.None)
return Result.Fail<TemplateRouteStepDto>(new ValidationError( return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Должно быть заполнено хотя бы одно из полей: исполнитель, должность, роль в проекте или тип руководителя.")); "Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя."));
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, excludeId: entity.Id, cancellationToken);
@@ -160,7 +153,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
} }
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, int? excludeId, CancellationToken cancellationToken) =>
db.TemplateRouteSteps.AnyAsync(x => db.TemplateRouteSteps.AnyAsync(x =>
x.Id != excludeId && x.Id != excludeId &&
@@ -179,7 +172,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
if (route.ByProject) if (route.ByProject)
{ {
var hasProjectRoleStep = await db.TemplateRouteSteps var hasProjectRoleStep = await db.TemplateRouteSteps
.AnyAsync(x => x.RouteId == route.Id && x.ProjectRole != ProjectRole.None, cancellationToken); .AnyAsync(x => x.RouteId == route.Id && x.ProjectRole != null && x.ProjectRole != ProjectRole.None, cancellationToken);
if (!hasProjectRoleStep) if (!hasProjectRoleStep)
warning = "Должен быть хотя бы один шаг с ролью в проекте."; warning = "Должен быть хотя бы один шаг с ролью в проекте.";
} }
@@ -8,12 +8,8 @@ public sealed class CreateTemplateRouteStepRequestValidator : AbstractValidator<
public CreateTemplateRouteStepRequestValidator() public CreateTemplateRouteStepRequestValidator()
{ {
RuleFor(x => x) RuleFor(x => x)
.Must(x => .Must(x => TemplateRouteStepFields.CountFilled(x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType) == 1)
!string.IsNullOrWhiteSpace(x.ExecutorEmail) || .WithMessage("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
!string.IsNullOrWhiteSpace(x.Position) ||
x.ProjectRole != ProjectRole.None ||
x.HeadType != HeadType.None)
.WithMessage("Должно быть заполнено хотя бы одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
} }
} }
@@ -22,11 +18,16 @@ public sealed class UpdateTemplateRouteStepRequestValidator : AbstractValidator<
public UpdateTemplateRouteStepRequestValidator() public UpdateTemplateRouteStepRequestValidator()
{ {
RuleFor(x => x) RuleFor(x => x)
.Must(x => .Must(x => TemplateRouteStepFields.CountFilled(x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType) == 1)
!string.IsNullOrWhiteSpace(x.ExecutorEmail) || .WithMessage("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
!string.IsNullOrWhiteSpace(x.Position) ||
x.ProjectRole != ProjectRole.None ||
x.HeadType != HeadType.None)
.WithMessage("Должно быть заполнено хотя бы одно из полей: исполнитель, должность, роль в проекте или тип руководителя.");
} }
} }
internal static class TemplateRouteStepFields
{
public static int CountFilled(string? executorEmail, string? position, ProjectRole? projectRole, HeadType? headType) =>
(string.IsNullOrWhiteSpace(executorEmail) ? 0 : 1) +
(string.IsNullOrWhiteSpace(position) ? 0 : 1) +
(projectRole is null or ProjectRole.None ? 0 : 1) +
(headType is null or HeadType.None ? 0 : 1);
}
@@ -5,10 +5,10 @@ namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
public sealed record ResolvedRouteStepDto( public sealed record ResolvedRouteStepDto(
int Id, int Id,
int OrderNo, int OrderNo,
string ExecutorEmail, string? ExecutorEmail,
string Position, string? Position,
ProjectRole ProjectRole, ProjectRole? ProjectRole,
HeadType HeadType, HeadType? HeadType,
RouteStepStatus Status, RouteStepStatus Status,
bool AllowReassignToOtherApprovers); bool AllowReassignToOtherApprovers);
@@ -187,7 +187,7 @@ public sealed class TemplateRouteService(
private static ResolvedRouteStepDto ResolveStep(TemplateRouteStep step, IReadOnlyList<ProjectExecutorInfo> projectExecutors) private static ResolvedRouteStepDto ResolveStep(TemplateRouteStep step, IReadOnlyList<ProjectExecutorInfo> projectExecutors)
{ {
if (step.ProjectRole != ProjectRole.None) if (step.ProjectRole is not (null or ProjectRole.None))
{ {
var projectExecutor = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole); var projectExecutor = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole);
if (projectExecutor is not null) if (projectExecutor is not null)
@@ -7,10 +7,10 @@ public class TemplateRouteStep
public int Id { get; set; } public int Id { get; set; }
public int OrderNo { get; set; } public int OrderNo { get; set; }
public string ExecutorEmail { get; set; } = string.Empty; public string? ExecutorEmail { get; set; }
public string Position { get; set; } = string.Empty; public string? Position { get; set; }
public ProjectRole ProjectRole { get; set; } = ProjectRole.None; public ProjectRole? ProjectRole { get; set; }
public HeadType HeadType { get; set; } = HeadType.None; public HeadType? HeadType { get; set; }
public RouteStepStatus Status { get; set; } = RouteStepStatus.Approval; public RouteStepStatus Status { get; set; } = RouteStepStatus.Approval;
public int RouteId { get; set; } public int RouteId { get; set; }
@@ -29,10 +29,10 @@ public class ApprovalRouteDbContext(DbContextOptions<ApprovalRouteDbContext> opt
modelBuilder.Entity<TemplateRouteStep>(e => modelBuilder.Entity<TemplateRouteStep>(e =>
{ {
e.HasKey(x => x.Id); e.HasKey(x => x.Id);
e.Property(x => x.ExecutorEmail).HasMaxLength(256).IsRequired(); e.Property(x => x.ExecutorEmail).HasMaxLength(256);
e.Property(x => x.Position).HasMaxLength(128).IsRequired(); e.Property(x => x.Position).HasMaxLength(128);
e.Property(x => x.ProjectRole).HasConversion<string>().HasMaxLength(32).IsRequired(); e.Property(x => x.ProjectRole).HasConversion<string>().HasMaxLength(32);
e.Property(x => x.HeadType).HasConversion<string>().HasMaxLength(32).IsRequired(); e.Property(x => x.HeadType).HasConversion<string>().HasMaxLength(32);
e.Property(x => x.Status).HasConversion<string>().HasMaxLength(32).IsRequired(); e.Property(x => x.Status).HasConversion<string>().HasMaxLength(32).IsRequired();
e.HasOne(x => x.Route).WithMany(x => x.Steps).HasForeignKey(x => x.RouteId); e.HasOne(x => x.Route).WithMany(x => x.Steps).HasForeignKey(x => x.RouteId);
e.HasIndex(x => new { x.RouteId, x.OrderNo }); e.HasIndex(x => new { x.RouteId, x.OrderNo });
@@ -12,7 +12,7 @@ using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
{ {
[DbContext(typeof(ApprovalRouteDbContext))] [DbContext(typeof(ApprovalRouteDbContext))]
[Migration("20260724114550_InitialCreate")] [Migration("20260727103117_InitialCreate")]
partial class InitialCreate partial class InitialCreate
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -111,13 +111,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnName("allow_reassign_to_other_approvers"); .HasColumnName("allow_reassign_to_other_approvers");
b.Property<string>("ExecutorEmail") b.Property<string>("ExecutorEmail")
.IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)") .HasColumnType("character varying(256)")
.HasColumnName("executor_email"); .HasColumnName("executor_email");
b.Property<string>("HeadType") b.Property<string>("HeadType")
.IsRequired()
.HasMaxLength(32) .HasMaxLength(32)
.HasColumnType("character varying(32)") .HasColumnType("character varying(32)")
.HasColumnName("head_type"); .HasColumnName("head_type");
@@ -127,13 +125,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnName("order_no"); .HasColumnName("order_no");
b.Property<string>("Position") b.Property<string>("Position")
.IsRequired()
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("character varying(128)") .HasColumnType("character varying(128)")
.HasColumnName("position"); .HasColumnName("position");
b.Property<string>("ProjectRole") b.Property<string>("ProjectRole")
.IsRequired()
.HasMaxLength(32) .HasMaxLength(32)
.HasColumnType("character varying(32)") .HasColumnType("character varying(32)")
.HasColumnName("project_role"); .HasColumnName("project_role");
@@ -57,10 +57,10 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
id = table.Column<int>(type: "integer", nullable: false) id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
order_no = table.Column<int>(type: "integer", nullable: false), order_no = table.Column<int>(type: "integer", nullable: false),
executor_email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), executor_email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
position = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), position = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
project_role = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false), project_role = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
head_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false), head_type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false), status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
route_id = table.Column<int>(type: "integer", nullable: false), route_id = table.Column<int>(type: "integer", nullable: false),
allow_reassign_to_other_approvers = table.Column<bool>(type: "boolean", nullable: false) allow_reassign_to_other_approvers = table.Column<bool>(type: "boolean", nullable: false)
@@ -108,13 +108,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnName("allow_reassign_to_other_approvers"); .HasColumnName("allow_reassign_to_other_approvers");
b.Property<string>("ExecutorEmail") b.Property<string>("ExecutorEmail")
.IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("character varying(256)") .HasColumnType("character varying(256)")
.HasColumnName("executor_email"); .HasColumnName("executor_email");
b.Property<string>("HeadType") b.Property<string>("HeadType")
.IsRequired()
.HasMaxLength(32) .HasMaxLength(32)
.HasColumnType("character varying(32)") .HasColumnType("character varying(32)")
.HasColumnName("head_type"); .HasColumnName("head_type");
@@ -124,13 +122,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnName("order_no"); .HasColumnName("order_no");
b.Property<string>("Position") b.Property<string>("Position")
.IsRequired()
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("character varying(128)") .HasColumnType("character varying(128)")
.HasColumnName("position"); .HasColumnName("position");
b.Property<string>("ProjectRole") b.Property<string>("ProjectRole")
.IsRequired()
.HasMaxLength(32) .HasMaxLength(32)
.HasColumnType("character varying(32)") .HasColumnType("character varying(32)")
.HasColumnName("project_role"); .HasColumnName("project_role");
@@ -160,7 +160,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>(); var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync( await stepService.CreateAsync(
route.Value.Id, route.Value.Id,
new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", "Lead", ProjectRole.None, HeadType.Chief, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", "Lead", null, HeadType.Chief, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Action // Action
@@ -187,7 +187,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = _services.GetRequiredService<TemplateRouteStepService>(); var stepService = _services.GetRequiredService<TemplateRouteStepService>();
await stepService.CreateAsync( await stepService.CreateAsync(
route.Value.Id, route.Value.Id,
new CreateTemplateRouteStepRequest(1, "", "Lead", ProjectRole.ProjectManager, HeadType.None, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "", "Lead", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Action // Action
@@ -42,7 +42,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action // Action
var result = await _service.CreateAsync( var result = await _service.CreateAsync(
999999, 999999,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
@@ -58,7 +58,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action // Action
var result = await _service.CreateAsync( var result = await _service.CreateAsync(
routeId, routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.ProjectManager, HeadType.None, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
@@ -70,14 +70,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
{ {
// Arrange // Arrange
var routeId = await CreateRouteAsync("Route with duplicate step", byProject: false); 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 request = new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, false);
var first = await _service.CreateAsync(routeId, request, CancellationToken.None); var first = await _service.CreateAsync(routeId, request, CancellationToken.None);
first.IsSuccess.ShouldBeTrue(); first.IsSuccess.ShouldBeTrue();
// Action // Action
var duplicate = await _service.CreateAsync( var duplicate = await _service.CreateAsync(
routeId, routeId,
new CreateTemplateRouteStepRequest(2, "a@b.kz", "Engineer", ProjectRole.None, HeadType.Chief, RouteStepStatus.Approval, true), new CreateTemplateRouteStepRequest(2, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, true),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
@@ -93,13 +93,13 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action // Action
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
routeId, routeId,
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.None, HeadType.Chief, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
created.IsSuccess.ShouldBeTrue(); created.IsSuccess.ShouldBeTrue();
created.Value.HeadType.ShouldBe(HeadType.Chief); created.Value.HeadType.ShouldBe(HeadType.Chief);
created.Value.ExecutorEmail.ShouldBe(string.Empty); created.Value.ExecutorEmail.ShouldBeNull();
} }
[Fact(DisplayName = "Создание шага маршрута и получение его по Id возвращает сохранённые данные")] [Fact(DisplayName = "Создание шага маршрута и получение его по Id возвращает сохранённые данные")]
@@ -111,7 +111,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action // Action
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
routeId, routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
@@ -131,7 +131,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action // Action
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
routeId, routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
@@ -149,7 +149,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Action // Action
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
routeId, routeId,
new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.ProjectManager, HeadType.None, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
@@ -164,14 +164,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to update step", byProject: false); var routeId = await CreateRouteAsync("Route to update step", byProject: false);
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
routeId, routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Action // Action
var updated = await _service.UpdateAsync( var updated = await _service.UpdateAsync(
routeId, routeId,
created.Value.Id, created.Value.Id,
new UpdateTemplateRouteStepRequest(2, "b@b.kz", "Senior Engineer", ProjectRole.None, HeadType.Chief, RouteStepStatus.Execution, true), new UpdateTemplateRouteStepRequest(2, "b@b.kz", "Senior Engineer", null, HeadType.Chief, RouteStepStatus.Execution, true),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
@@ -188,23 +188,66 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to patch step", byProject: false); var routeId = await CreateRouteAsync("Route to patch step", byProject: false);
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
routeId, routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Action // Action
var patched = await _service.PatchAsync( var patched = await _service.PatchAsync(
routeId, routeId,
created.Value.Id, created.Value.Id,
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: "c@b.kz", Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.Execution, AllowReassignToOtherApprovers: null), new PatchTemplateRouteStepRequest(OrderNo: 2, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.Execution, AllowReassignToOtherApprovers: null),
CancellationToken.None); CancellationToken.None);
// Assert // Assert
patched.IsSuccess.ShouldBeTrue(); patched.IsSuccess.ShouldBeTrue();
patched.Value.ExecutorEmail.ShouldBe("c@b.kz"); patched.Value.ExecutorEmail.ShouldBe("a@b.kz");
patched.Value.Position.ShouldBe("Engineer"); patched.Value.OrderNo.ShouldBe(2);
patched.Value.Status.ShouldBe(RouteStepStatus.Execution); patched.Value.Status.ShouldBe(RouteStepStatus.Execution);
} }
[Fact(DisplayName = "Патч, после которого заполнено больше одного из полей исполнителя, возвращает неуспешный результат")]
public async Task PatchAsync_ResultsInMoreThanOneFilledField_ReturnsFailedResult()
{
// Arrange
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),
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),
CancellationToken.None);
// Assert
patched.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Явные ProjectRole.None и HeadType.None при патче считаются незаполненными, как и null")]
public async Task PatchAsync_ExplicitNoneValues_TreatedAsUnfilled()
{
// Arrange
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),
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),
CancellationToken.None);
// Assert
patched.IsSuccess.ShouldBeTrue();
patched.Value.ExecutorEmail.ShouldBe("a@b.kz");
}
[Fact(DisplayName = "Удаление шага маршрута убирает его из базы")] [Fact(DisplayName = "Удаление шага маршрута убирает его из базы")]
public async Task DeleteAsync_RemovesStep() public async Task DeleteAsync_RemovesStep()
{ {
@@ -212,7 +255,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
var routeId = await CreateRouteAsync("Route to delete step", byProject: false); var routeId = await CreateRouteAsync("Route to delete step", byProject: false);
var created = await _service.CreateAsync( var created = await _service.CreateAsync(
routeId, routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
CancellationToken.None); CancellationToken.None);
// Action // Action