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

Merged
admin merged 38 commits from initial-api into main 2026-07-27 13:14:32 +00:00
18 changed files with 666 additions and 1 deletions
Showing only changes of commit 1a5ed34182 - Show all commits
+1
View File
@@ -5,6 +5,7 @@
<ItemGroup>
<PackageVersion Include="EFCore.NamingConventions" Version="10.0.1" />
<PackageVersion Include="Extensions.Hosting.AsyncInitialization" Version="3.0.1" />
<PackageVersion Include="FluentResults" Version="4.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
@@ -0,0 +1,33 @@
using FluentResults;
using Microsoft.AspNetCore.Mvc;
using Tnsu.ApprovalRoute.Application.Common.Errors;
namespace Tnsu.ApprovalRoute.Api.Common;
public static class ResultExtensions
{
public static IActionResult ToActionResult<T>(this Result<T> result)
{
if (result.IsFailed)
return result.Errors[0].ToActionResult();
var warnings = result.Reasons.OfType<Warning>().Select(w => w.Message).ToList();
return new OkObjectResult(new { data = result.Value, warnings });
}
public static IActionResult ToActionResult(this Result result)
{
if (result.IsFailed)
return result.Errors[0].ToActionResult();
return new NoContentResult();
}
private static IActionResult ToActionResult(this IError error) => error switch
{
NotFoundError => new NotFoundObjectResult(new { message = error.Message }),
ValidationError => new BadRequestObjectResult(new { message = error.Message }),
ConflictError => new ConflictObjectResult(new { message = error.Message }),
_ => new ObjectResult(new { message = error.Message }) { StatusCode = StatusCodes.Status500InternalServerError }
};
}
@@ -0,0 +1,31 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Tnsu.ApprovalRoute.Api.Common;
using Tnsu.ApprovalRoute.Application.RequestTypes;
namespace Tnsu.ApprovalRoute.Api.Controllers;
[ApiController]
[Route("api/request-types")]
public sealed class RequestTypesController(ISender mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> List(CancellationToken ct) =>
(await mediator.Send(new ListRequestTypesQuery(), ct)).ToActionResult();
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id, CancellationToken ct) =>
(await mediator.Send(new GetRequestTypeQuery(id), ct)).ToActionResult();
[HttpPost]
public async Task<IActionResult> Create(CreateRequestTypeRequest body, CancellationToken ct) =>
(await mediator.Send(new CreateRequestTypeCommand(body.Name, body.PageUrl), ct)).ToActionResult();
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, UpdateRequestTypeRequest body, CancellationToken ct) =>
(await mediator.Send(new UpdateRequestTypeCommand(id, body.Name, body.PageUrl), ct)).ToActionResult();
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) =>
(await mediator.Send(new DeleteRequestTypeCommand(id), ct)).ToActionResult();
}
@@ -0,0 +1,31 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Tnsu.ApprovalRoute.Api.Common;
using Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
namespace Tnsu.ApprovalRoute.Api.Controllers;
[ApiController]
[Route("api/template-routes/{routeId:guid}/steps")]
public sealed class TemplateRouteStepsController(ISender mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> List(Guid routeId, CancellationToken ct) =>
(await mediator.Send(new ListTemplateRouteStepsQuery(routeId), ct)).ToActionResult();
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid routeId, Guid id, CancellationToken ct) =>
(await mediator.Send(new GetTemplateRouteStepQuery(routeId, id), ct)).ToActionResult();
[HttpPost]
public async Task<IActionResult> Create(Guid routeId, CreateTemplateRouteStepRequest body, CancellationToken ct) =>
(await mediator.Send(new CreateTemplateRouteStepCommand(routeId, body), ct)).ToActionResult();
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid routeId, Guid id, UpdateTemplateRouteStepRequest body, CancellationToken ct) =>
(await mediator.Send(new UpdateTemplateRouteStepCommand(routeId, id, body), ct)).ToActionResult();
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid routeId, Guid id, CancellationToken ct) =>
(await mediator.Send(new DeleteTemplateRouteStepCommand(routeId, id), ct)).ToActionResult();
}
@@ -0,0 +1,31 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Tnsu.ApprovalRoute.Api.Common;
using Tnsu.ApprovalRoute.Application.TemplateRoutes;
namespace Tnsu.ApprovalRoute.Api.Controllers;
[ApiController]
[Route("api/template-routes")]
public sealed class TemplateRoutesController(ISender mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> List([FromQuery] Guid? requestTypeId, CancellationToken ct) =>
(await mediator.Send(new ListTemplateRoutesQuery(requestTypeId), ct)).ToActionResult();
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id, CancellationToken ct) =>
(await mediator.Send(new GetTemplateRouteQuery(id), ct)).ToActionResult();
[HttpPost]
public async Task<IActionResult> Create(CreateTemplateRouteRequest body, CancellationToken ct) =>
(await mediator.Send(new CreateTemplateRouteCommand(body), ct)).ToActionResult();
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, UpdateTemplateRouteRequest body, CancellationToken ct) =>
(await mediator.Send(new UpdateTemplateRouteCommand(id, body), ct)).ToActionResult();
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) =>
(await mediator.Send(new DeleteTemplateRouteCommand(id), ct)).ToActionResult();
}
@@ -1,3 +1,4 @@
using System.Text.Json.Serialization;
using Extensions.Hosting.AsyncInitialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@@ -11,6 +12,8 @@ builder.Services.AddApprovalRouteInfrastructure(builder.Configuration);
builder.Services.AddApprovalRouteApplication();
builder.Services.AddOpenApi();
builder.Services.AddHealthChecks();
builder.Services.AddControllers()
.AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
var app = builder.Build();
@@ -18,6 +21,7 @@ if (app.Environment.IsDevelopment())
app.MapOpenApi();
app.MapHealthChecks("/health");
app.MapControllers();
await app.InitAndRunAsync();
@@ -0,0 +1,12 @@
using FluentResults;
namespace Tnsu.ApprovalRoute.Application.Common.Errors;
public sealed class NotFoundError(string entity, Guid id)
: Error($"{entity} с id {id} не найден.");
public sealed class ValidationError(string message) : Error(message);
public sealed class ConflictError(string message) : Error(message);
public sealed class Warning(string message) : Success(message);
@@ -0,0 +1,75 @@
using FluentResults;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Tnsu.ApprovalRoute.Application.Common.Errors;
using Tnsu.ApprovalRoute.Domain.Entities;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Application.RequestTypes;
public sealed record CreateRequestTypeCommand(string Name, string PageUrl) : IRequest<Result<RequestTypeDto>>;
public sealed record UpdateRequestTypeCommand(Guid Id, string Name, string PageUrl) : IRequest<Result<RequestTypeDto>>;
public sealed record DeleteRequestTypeCommand(Guid Id) : IRequest<Result>;
public sealed class CreateRequestTypeHandler(ApprovalRouteDbContext db)
: IRequestHandler<CreateRequestTypeCommand, Result<RequestTypeDto>>
{
public async Task<Result<RequestTypeDto>> Handle(CreateRequestTypeCommand request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Name))
return Result.Fail<RequestTypeDto>(new ValidationError("Имя типа заявки обязательно."));
if (string.IsNullOrWhiteSpace(request.PageUrl))
return Result.Fail<RequestTypeDto>(new ValidationError("Url страницы обязателен."));
var entity = new RequestType { Name = request.Name, PageUrl = request.PageUrl };
db.RequestTypes.Add(entity);
await db.SaveChangesAsync(cancellationToken);
return Result.Ok(RequestTypeMapper.ToDto(entity));
}
}
public sealed class UpdateRequestTypeHandler(ApprovalRouteDbContext db)
: IRequestHandler<UpdateRequestTypeCommand, Result<RequestTypeDto>>
{
public async Task<Result<RequestTypeDto>> Handle(UpdateRequestTypeCommand request, CancellationToken cancellationToken)
{
var entity = await db.RequestTypes.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
if (entity is null)
return Result.Fail<RequestTypeDto>(new NotFoundError("Тип заявки", request.Id));
if (string.IsNullOrWhiteSpace(request.Name))
return Result.Fail<RequestTypeDto>(new ValidationError("Имя типа заявки обязательно."));
if (string.IsNullOrWhiteSpace(request.PageUrl))
return Result.Fail<RequestTypeDto>(new ValidationError("Url страницы обязателен."));
entity.Name = request.Name;
entity.PageUrl = request.PageUrl;
await db.SaveChangesAsync(cancellationToken);
return Result.Ok(RequestTypeMapper.ToDto(entity));
}
}
public sealed class DeleteRequestTypeHandler(ApprovalRouteDbContext db)
: IRequestHandler<DeleteRequestTypeCommand, Result>
{
public async Task<Result> Handle(DeleteRequestTypeCommand request, CancellationToken cancellationToken)
{
var entity = await db.RequestTypes.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
if (entity is null)
return Result.Fail(new NotFoundError("Тип заявки", request.Id));
db.RequestTypes.Remove(entity);
await db.SaveChangesAsync(cancellationToken);
return Result.Ok();
}
}
internal static class RequestTypeMapper
{
public static RequestTypeDto ToDto(RequestType e) => new(e.Id, e.Name, e.PageUrl);
}
@@ -0,0 +1,7 @@
namespace Tnsu.ApprovalRoute.Application.RequestTypes;
public sealed record RequestTypeDto(Guid Id, string Name, string PageUrl);
public sealed record CreateRequestTypeRequest(string Name, string PageUrl);
public sealed record UpdateRequestTypeRequest(string Name, string PageUrl);
@@ -0,0 +1,39 @@
using FluentResults;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Tnsu.ApprovalRoute.Application.Common.Errors;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Application.RequestTypes;
public sealed record GetRequestTypeQuery(Guid Id) : IRequest<Result<RequestTypeDto>>;
public sealed record ListRequestTypesQuery : IRequest<Result<IReadOnlyList<RequestTypeDto>>>;
public sealed class GetRequestTypeHandler(ApprovalRouteDbContext db)
: IRequestHandler<GetRequestTypeQuery, Result<RequestTypeDto>>
{
public async Task<Result<RequestTypeDto>> Handle(GetRequestTypeQuery request, CancellationToken cancellationToken)
{
var entity = await db.RequestTypes.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
return entity is null
? Result.Fail<RequestTypeDto>(new NotFoundError("Тип заявки", request.Id))
: Result.Ok(RequestTypeMapper.ToDto(entity));
}
}
public sealed class ListRequestTypesHandler(ApprovalRouteDbContext db)
: IRequestHandler<ListRequestTypesQuery, Result<IReadOnlyList<RequestTypeDto>>>
{
public async Task<Result<IReadOnlyList<RequestTypeDto>>> Handle(ListRequestTypesQuery request, CancellationToken cancellationToken)
{
var items = await db.RequestTypes.AsNoTracking()
.OrderBy(x => x.Name)
.Select(x => new RequestTypeDto(x.Id, x.Name, x.PageUrl))
.ToListAsync(cancellationToken);
return Result.Ok<IReadOnlyList<RequestTypeDto>>(items);
}
}
@@ -0,0 +1,134 @@
using FluentResults;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Tnsu.ApprovalRoute.Application.Common.Errors;
using Tnsu.ApprovalRoute.Domain.Entities;
using Tnsu.ApprovalRoute.Domain.Enums;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
public sealed record CreateTemplateRouteStepCommand(Guid RouteId, CreateTemplateRouteStepRequest Request)
: IRequest<Result<TemplateRouteStepDto>>;
public sealed record UpdateTemplateRouteStepCommand(Guid RouteId, Guid Id, UpdateTemplateRouteStepRequest Request)
: IRequest<Result<TemplateRouteStepDto>>;
public sealed record DeleteTemplateRouteStepCommand(Guid RouteId, Guid Id) : IRequest<Result>;
public sealed class CreateTemplateRouteStepHandler(ApprovalRouteDbContext db)
: IRequestHandler<CreateTemplateRouteStepCommand, Result<TemplateRouteStepDto>>
{
public async Task<Result<TemplateRouteStepDto>> Handle(CreateTemplateRouteStepCommand command, CancellationToken cancellationToken)
{
var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == command.RouteId, cancellationToken);
if (route is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", command.RouteId));
var r = command.Request;
var fieldsError = TemplateRouteStepValidation.ValidateFields(r.ExecutorEmail, r.Position, r.ProjectRole, route.ByProject);
if (fieldsError is not null)
return Result.Fail<TemplateRouteStepDto>(fieldsError);
var entity = new TemplateRouteStep
{
RouteId = route.Id,
OrderNo = r.OrderNo,
ExecutorEmail = r.ExecutorEmail,
Position = r.Position,
ProjectRole = r.ProjectRole,
HeadType = r.HeadType,
Status = r.Status,
AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers
};
db.TemplateRouteSteps.Add(entity);
await db.SaveChangesAsync(cancellationToken);
var result = Result.Ok(TemplateRouteStepMapper.ToDto(entity));
return await TemplateRouteStepValidation.AppendMissingProjectRoleWarningAsync(db, route, result, cancellationToken);
}
}
public sealed class UpdateTemplateRouteStepHandler(ApprovalRouteDbContext db)
: IRequestHandler<UpdateTemplateRouteStepCommand, Result<TemplateRouteStepDto>>
{
public async Task<Result<TemplateRouteStepDto>> Handle(UpdateTemplateRouteStepCommand command, CancellationToken cancellationToken)
{
var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == command.RouteId, cancellationToken);
if (route is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", command.RouteId));
var entity = await db.TemplateRouteSteps
.FirstOrDefaultAsync(x => x.Id == command.Id && x.RouteId == command.RouteId, cancellationToken);
if (entity is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", command.Id));
var r = command.Request;
var fieldsError = TemplateRouteStepValidation.ValidateFields(r.ExecutorEmail, r.Position, r.ProjectRole, route.ByProject);
if (fieldsError is not null)
return Result.Fail<TemplateRouteStepDto>(fieldsError);
entity.OrderNo = r.OrderNo;
entity.ExecutorEmail = r.ExecutorEmail;
entity.Position = r.Position;
entity.ProjectRole = r.ProjectRole;
entity.HeadType = r.HeadType;
entity.Status = r.Status;
entity.AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers;
await db.SaveChangesAsync(cancellationToken);
var result = Result.Ok(TemplateRouteStepMapper.ToDto(entity));
return await TemplateRouteStepValidation.AppendMissingProjectRoleWarningAsync(db, route, result, cancellationToken);
}
}
public sealed class DeleteTemplateRouteStepHandler(ApprovalRouteDbContext db)
: IRequestHandler<DeleteTemplateRouteStepCommand, Result>
{
public async Task<Result> Handle(DeleteTemplateRouteStepCommand request, CancellationToken cancellationToken)
{
var entity = await db.TemplateRouteSteps
.FirstOrDefaultAsync(x => x.Id == request.Id && x.RouteId == request.RouteId, cancellationToken);
if (entity is null)
return Result.Fail(new NotFoundError("Шаг маршрута", request.Id));
db.TemplateRouteSteps.Remove(entity);
await db.SaveChangesAsync(cancellationToken);
return Result.Ok();
}
}
internal static class TemplateRouteStepValidation
{
public static IError? ValidateFields(string executorEmail, string position, ProjectRole projectRole, bool routeByProject)
{
if (string.IsNullOrWhiteSpace(executorEmail) && string.IsNullOrWhiteSpace(position) && projectRole == ProjectRole.None)
return new ValidationError("Исполнитель, должность и роль в проекте не могут быть пустыми одновременно.");
if (!routeByProject && projectRole != ProjectRole.None)
return new ValidationError("Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false).");
return null;
}
public static async Task<Result<TemplateRouteStepDto>> AppendMissingProjectRoleWarningAsync(
ApprovalRouteDbContext db, TemplateRoute route, Result<TemplateRouteStepDto> result, CancellationToken cancellationToken)
{
if (!route.ByProject)
return result;
var hasProjectRoleStep = await db.TemplateRouteSteps
.AnyAsync(x => x.RouteId == route.Id && x.ProjectRole != ProjectRole.None, cancellationToken);
return hasProjectRoleStep
? result
: result.WithReason(new Warning("Должен быть хотя бы один шаг с ролью в проекте."));
}
}
internal static class TemplateRouteStepMapper
{
public static TemplateRouteStepDto ToDto(TemplateRouteStep e) => new(
e.Id, e.OrderNo, e.ExecutorEmail, e.Position, e.ProjectRole, e.HeadType, e.Status, e.RouteId, e.AllowReassignToOtherApprovers);
}
@@ -0,0 +1,32 @@
using Tnsu.ApprovalRoute.Domain.Enums;
namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
public sealed record TemplateRouteStepDto(
Guid Id,
int OrderNo,
string ExecutorEmail,
string Position,
ProjectRole ProjectRole,
HeadType HeadType,
RouteStepStatus Status,
Guid RouteId,
bool AllowReassignToOtherApprovers);
public sealed record CreateTemplateRouteStepRequest(
int OrderNo,
string ExecutorEmail,
string Position,
ProjectRole ProjectRole,
HeadType HeadType,
RouteStepStatus Status,
bool AllowReassignToOtherApprovers);
public sealed record UpdateTemplateRouteStepRequest(
int OrderNo,
string ExecutorEmail,
string Position,
ProjectRole ProjectRole,
HeadType HeadType,
RouteStepStatus Status,
bool AllowReassignToOtherApprovers);
@@ -0,0 +1,41 @@
using FluentResults;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Tnsu.ApprovalRoute.Application.Common.Errors;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
public sealed record GetTemplateRouteStepQuery(Guid RouteId, Guid Id) : IRequest<Result<TemplateRouteStepDto>>;
public sealed record ListTemplateRouteStepsQuery(Guid RouteId) : IRequest<Result<IReadOnlyList<TemplateRouteStepDto>>>;
public sealed class GetTemplateRouteStepHandler(ApprovalRouteDbContext db)
: IRequestHandler<GetTemplateRouteStepQuery, Result<TemplateRouteStepDto>>
{
public async Task<Result<TemplateRouteStepDto>> Handle(GetTemplateRouteStepQuery request, CancellationToken cancellationToken)
{
var entity = await db.TemplateRouteSteps.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == request.Id && x.RouteId == request.RouteId, cancellationToken);
return entity is null
? Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", request.Id))
: Result.Ok(TemplateRouteStepMapper.ToDto(entity));
}
}
public sealed class ListTemplateRouteStepsHandler(ApprovalRouteDbContext db)
: IRequestHandler<ListTemplateRouteStepsQuery, Result<IReadOnlyList<TemplateRouteStepDto>>>
{
public async Task<Result<IReadOnlyList<TemplateRouteStepDto>>> Handle(ListTemplateRouteStepsQuery request, CancellationToken cancellationToken)
{
var items = await db.TemplateRouteSteps.AsNoTracking()
.Where(x => x.RouteId == request.RouteId)
.OrderBy(x => x.OrderNo)
.Select(x => new TemplateRouteStepDto(
x.Id, x.OrderNo, x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType, x.Status, x.RouteId, x.AllowReassignToOtherApprovers))
.ToListAsync(cancellationToken);
return Result.Ok<IReadOnlyList<TemplateRouteStepDto>>(items);
}
}
@@ -0,0 +1,122 @@
using FluentResults;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Tnsu.ApprovalRoute.Application.Common.Errors;
using Tnsu.ApprovalRoute.Domain.Entities;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
public sealed record CreateTemplateRouteCommand(CreateTemplateRouteRequest Request) : IRequest<Result<TemplateRouteDto>>;
public sealed record UpdateTemplateRouteCommand(Guid Id, UpdateTemplateRouteRequest Request) : IRequest<Result<TemplateRouteDto>>;
public sealed record DeleteTemplateRouteCommand(Guid Id) : IRequest<Result>;
public sealed class CreateTemplateRouteHandler(ApprovalRouteDbContext db)
: IRequestHandler<CreateTemplateRouteCommand, Result<TemplateRouteDto>>
{
public async Task<Result<TemplateRouteDto>> Handle(CreateTemplateRouteCommand command, CancellationToken cancellationToken)
{
var r = command.Request;
if (r.CompanyId == Guid.Empty)
return Result.Fail<TemplateRouteDto>(new ValidationError("Компания обязательна."));
var requestTypeExists = await db.RequestTypes.AnyAsync(x => x.Id == r.RequestTypeId, cancellationToken);
if (!requestTypeExists)
return Result.Fail<TemplateRouteDto>(new NotFoundError("Тип заявки", r.RequestTypeId));
var duplicate = await TemplateRouteUniqueness.HasDuplicateAsync(
db, r.RequestTypeId, r.DepartmentId, r.CompanyId, r.ByProject, excludeId: null, cancellationToken);
if (duplicate)
return Result.Fail<TemplateRouteDto>(new ConflictError(
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
var entity = new TemplateRoute
{
RouteType = r.RouteType,
RequestTypeId = r.RequestTypeId,
DepartmentId = r.DepartmentId,
CompanyId = r.CompanyId,
ByProject = r.ByProject,
RequiresControl = r.RequiresControl
};
db.TemplateRoutes.Add(entity);
await db.SaveChangesAsync(cancellationToken);
return Result.Ok(TemplateRouteMapper.ToDto(entity));
}
}
public sealed class UpdateTemplateRouteHandler(ApprovalRouteDbContext db)
: IRequestHandler<UpdateTemplateRouteCommand, Result<TemplateRouteDto>>
{
public async Task<Result<TemplateRouteDto>> Handle(UpdateTemplateRouteCommand command, CancellationToken cancellationToken)
{
var entity = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken);
if (entity is null)
return Result.Fail<TemplateRouteDto>(new NotFoundError("Маршрут", command.Id));
var r = command.Request;
if (r.CompanyId == Guid.Empty)
return Result.Fail<TemplateRouteDto>(new ValidationError("Компания обязательна."));
var duplicate = await TemplateRouteUniqueness.HasDuplicateAsync(
db, entity.RequestTypeId, r.DepartmentId, r.CompanyId, r.ByProject, excludeId: entity.Id, cancellationToken);
if (duplicate)
return Result.Fail<TemplateRouteDto>(new ConflictError(
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
entity.RouteType = r.RouteType;
entity.DepartmentId = r.DepartmentId;
entity.CompanyId = r.CompanyId;
entity.ByProject = r.ByProject;
entity.RequiresControl = r.RequiresControl;
await db.SaveChangesAsync(cancellationToken);
return Result.Ok(TemplateRouteMapper.ToDto(entity));
}
}
public sealed class DeleteTemplateRouteHandler(ApprovalRouteDbContext db)
: IRequestHandler<DeleteTemplateRouteCommand, Result>
{
public async Task<Result> Handle(DeleteTemplateRouteCommand request, CancellationToken cancellationToken)
{
var entity = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
if (entity is null)
return Result.Fail(new NotFoundError("Маршрут", request.Id));
db.TemplateRoutes.Remove(entity);
await db.SaveChangesAsync(cancellationToken);
return Result.Ok();
}
}
internal static class TemplateRouteUniqueness
{
public static Task<bool> HasDuplicateAsync(
ApprovalRouteDbContext db,
Guid requestTypeId,
Guid? departmentId,
Guid companyId,
bool byProject,
Guid? excludeId,
CancellationToken cancellationToken) =>
db.TemplateRoutes.AnyAsync(x =>
x.Id != excludeId &&
x.RequestTypeId == requestTypeId &&
x.DepartmentId == departmentId &&
x.CompanyId == companyId &&
x.ByProject == byProject,
cancellationToken);
}
internal static class TemplateRouteMapper
{
public static TemplateRouteDto ToDto(TemplateRoute e) => new(
e.Id, e.RouteType, e.RequestTypeId, e.DepartmentId, e.CompanyId, e.ByProject, e.RequiresControl);
}
@@ -0,0 +1,27 @@
using Tnsu.ApprovalRoute.Domain.Enums;
namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
public sealed record TemplateRouteDto(
Guid Id,
RouteType RouteType,
Guid RequestTypeId,
Guid? DepartmentId,
Guid CompanyId,
bool ByProject,
bool RequiresControl);
public sealed record CreateTemplateRouteRequest(
RouteType RouteType,
Guid RequestTypeId,
Guid? DepartmentId,
Guid CompanyId,
bool ByProject,
bool RequiresControl);
public sealed record UpdateTemplateRouteRequest(
RouteType RouteType,
Guid? DepartmentId,
Guid CompanyId,
bool ByProject,
bool RequiresControl);
@@ -0,0 +1,43 @@
using FluentResults;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Tnsu.ApprovalRoute.Application.Common.Errors;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
public sealed record GetTemplateRouteQuery(Guid Id) : IRequest<Result<TemplateRouteDto>>;
public sealed record ListTemplateRoutesQuery(Guid? RequestTypeId) : IRequest<Result<IReadOnlyList<TemplateRouteDto>>>;
public sealed class GetTemplateRouteHandler(ApprovalRouteDbContext db)
: IRequestHandler<GetTemplateRouteQuery, Result<TemplateRouteDto>>
{
public async Task<Result<TemplateRouteDto>> Handle(GetTemplateRouteQuery request, CancellationToken cancellationToken)
{
var entity = await db.TemplateRoutes.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
return entity is null
? Result.Fail<TemplateRouteDto>(new NotFoundError("Маршрут", request.Id))
: Result.Ok(TemplateRouteMapper.ToDto(entity));
}
}
public sealed class ListTemplateRoutesHandler(ApprovalRouteDbContext db)
: IRequestHandler<ListTemplateRoutesQuery, Result<IReadOnlyList<TemplateRouteDto>>>
{
public async Task<Result<IReadOnlyList<TemplateRouteDto>>> Handle(ListTemplateRoutesQuery request, CancellationToken cancellationToken)
{
var query = db.TemplateRoutes.AsNoTracking().AsQueryable();
if (request.RequestTypeId is { } requestTypeId)
query = query.Where(x => x.RequestTypeId == requestTypeId);
var items = await query
.Select(x => new TemplateRouteDto(
x.Id, x.RouteType, x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject, x.RequiresControl))
.ToListAsync(cancellationToken);
return Result.Ok<IReadOnlyList<TemplateRouteDto>>(items);
}
}
@@ -1,8 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Tnsu.ApprovalRoute.Domain\Tnsu.ApprovalRoute.Domain.csproj" />
<ProjectReference Include="..\Tnsu.ApprovalRoute.Infrastructure\Tnsu.ApprovalRoute.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentResults" />
<PackageReference Include="MediatR" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
</ItemGroup>
</Project>
@@ -1,6 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Tnsu.ApprovalRoute.Application\Tnsu.ApprovalRoute.Application.csproj" />
<ProjectReference Include="..\Tnsu.ApprovalRoute.Domain\Tnsu.ApprovalRoute.Domain.csproj" />
</ItemGroup>
<ItemGroup>