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

Merged
admin merged 38 commits from initial-api into main 2026-07-27 13:14:32 +00:00
6 changed files with 21 additions and 22 deletions
Showing only changes of commit 06cab46efc - Show all commits
@@ -1,5 +1,5 @@
namespace Tnsu.ApprovalRoute.Api.Common; namespace Tnsu.ApprovalRoute.Api.Common;
public sealed record ApiResponse<T>(T Data, IReadOnlyList<string> Warnings); public sealed record ApiResponse<T>(T Data);
public sealed record ApiError(string Message); public sealed record ApiError(string Message);
@@ -11,8 +11,7 @@ public static class ResultExtensions
if (result.IsFailed) if (result.IsFailed)
return result.Errors[0].ToActionResult(); return result.Errors[0].ToActionResult();
var warnings = result.Reasons.OfType<Warning>().Select(w => w.Message).ToList(); return new OkObjectResult(new ApiResponse<T>(result.Value));
return new OkObjectResult(new ApiResponse<T>(result.Value, warnings));
} }
public static IActionResult ToActionResult(this Result result) public static IActionResult ToActionResult(this Result result)
@@ -8,5 +8,3 @@ public sealed class NotFoundError(string entity, object id)
public sealed class ValidationError(string message) : Error(message); public sealed class ValidationError(string message) : Error(message);
public sealed class ConflictError(string message) : Error(message); public sealed class ConflictError(string message) : Error(message);
public sealed class Warning(string message) : Success(message);
@@ -11,7 +11,8 @@ public sealed record TemplateRouteStepDto(
HeadType HeadType, HeadType HeadType,
RouteStepStatus Status, RouteStepStatus Status,
int RouteId, int RouteId,
bool AllowReassignToOtherApprovers); bool AllowReassignToOtherApprovers,
string? Warning = null);
public sealed record CreateTemplateRouteStepRequest( public sealed record CreateTemplateRouteStepRequest(
int OrderNo, int OrderNo,
@@ -55,8 +55,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
db.TemplateRouteSteps.Add(entity); db.TemplateRouteSteps.Add(entity);
await db.SaveChangesAsync(cancellationToken); await db.SaveChangesAsync(cancellationToken);
var result = Result.Ok(ToDto(entity)); return Result.Ok(await ToDtoWithWarningAsync(entity, route, cancellationToken));
return await AppendMissingProjectRoleWarningAsync(route, result, cancellationToken);
} }
public async Task<Result<TemplateRouteStepDto>> UpdateAsync( public async Task<Result<TemplateRouteStepDto>> UpdateAsync(
@@ -83,8 +82,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
entity.AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers; entity.AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers;
await db.SaveChangesAsync(cancellationToken); await db.SaveChangesAsync(cancellationToken);
var result = Result.Ok(ToDto(entity)); return Result.Ok(await ToDtoWithWarningAsync(entity, route, cancellationToken));
return await AppendMissingProjectRoleWarningAsync(route, result, cancellationToken);
} }
public async Task<Result> DeleteAsync(int routeId, int id, CancellationToken cancellationToken) public async Task<Result> DeleteAsync(int routeId, int id, CancellationToken cancellationToken)
@@ -99,18 +97,20 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
return Result.Ok(); return Result.Ok();
} }
private async Task<Result<TemplateRouteStepDto>> AppendMissingProjectRoleWarningAsync( // Для маршрута «по проекту» без единого шага с ролью в проекте DTO несёт предупреждение в поле Warning.
TemplateRoute route, Result<TemplateRouteStepDto> result, CancellationToken cancellationToken) private async Task<TemplateRouteStepDto> ToDtoWithWarningAsync(
TemplateRouteStep entity, TemplateRoute route, CancellationToken cancellationToken)
{
string? warning = null;
if (route.ByProject)
{ {
if (!route.ByProject)
return result;
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 != ProjectRole.None, cancellationToken);
if (!hasProjectRoleStep)
warning = "Должен быть хотя бы один шаг с ролью в проекте.";
}
return hasProjectRoleStep return ToDto(entity) with { Warning = warning };
? result
: result.WithReason(new Warning("Должен быть хотя бы один шаг с ролью в проекте."));
} }
private static TemplateRouteStepDto ToDto(TemplateRouteStep e) => new( private static TemplateRouteStepDto ToDto(TemplateRouteStep e) => new(
@@ -99,7 +99,8 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Assert // Assert
created.IsSuccess.ShouldBeTrue(); created.IsSuccess.ShouldBeTrue();
created.Reasons.ShouldContain(r => r.Message.Contains("ролью в проекте")); created.Value.Warning.ShouldNotBeNull();
created.Value.Warning.ShouldContain("ролью в проекте");
} }
[Fact(DisplayName = "Создание шага с ролью в проекте на маршруте «по проекту» не возвращает предупреждение")] [Fact(DisplayName = "Создание шага с ролью в проекте на маршруте «по проекту» не возвращает предупреждение")]
@@ -116,7 +117,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
// Assert // Assert
created.IsSuccess.ShouldBeTrue(); created.IsSuccess.ShouldBeTrue();
created.Reasons.ShouldBeEmpty(); created.Value.Warning.ShouldBeNull();
} }
[Fact(DisplayName = "Обновление шага маршрута изменяет исполнителя, статус и разрешение на переназначение")] [Fact(DisplayName = "Обновление шага маршрута изменяет исполнителя, статус и разрешение на переназначение")]