теперь поля nullable

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