апи для работы с маршрутами согласования #2
@@ -13,7 +13,6 @@
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
<PackageVersion Include="MediatR" Version="14.2.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tnsu.ApprovalRoute.Api.Common;
|
||||
@@ -9,25 +8,25 @@ namespace Tnsu.ApprovalRoute.Api.Controllers;
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/request-types")]
|
||||
public sealed class RequestTypesController(ISender mediator) : ControllerBase
|
||||
public sealed class RequestTypesController(RequestTypeService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List(CancellationToken ct) =>
|
||||
(await mediator.Send(new ListRequestTypesQuery(), ct)).ToActionResult();
|
||||
(await service.ListAsync(ct)).ToActionResult();
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> Get(Guid id, CancellationToken ct) =>
|
||||
(await mediator.Send(new GetRequestTypeQuery(id), ct)).ToActionResult();
|
||||
(await service.GetAsync(id, ct)).ToActionResult();
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(CreateRequestTypeRequest body, CancellationToken ct) =>
|
||||
(await mediator.Send(new CreateRequestTypeCommand(body.Name, body.PageUrl), ct)).ToActionResult();
|
||||
(await service.CreateAsync(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();
|
||||
(await service.UpdateAsync(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();
|
||||
(await service.DeleteAsync(id, ct)).ToActionResult();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tnsu.ApprovalRoute.Api.Common;
|
||||
@@ -9,25 +8,25 @@ namespace Tnsu.ApprovalRoute.Api.Controllers;
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/template-routes/{routeId:guid}/steps")]
|
||||
public sealed class TemplateRouteStepsController(ISender mediator) : ControllerBase
|
||||
public sealed class TemplateRouteStepsController(TemplateRouteStepService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List(Guid routeId, CancellationToken ct) =>
|
||||
(await mediator.Send(new ListTemplateRouteStepsQuery(routeId), ct)).ToActionResult();
|
||||
(await service.ListAsync(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();
|
||||
(await service.GetAsync(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();
|
||||
(await service.CreateAsync(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();
|
||||
(await service.UpdateAsync(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();
|
||||
(await service.DeleteAsync(routeId, id, ct)).ToActionResult();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tnsu.ApprovalRoute.Api.Common;
|
||||
@@ -10,7 +9,7 @@ namespace Tnsu.ApprovalRoute.Api.Controllers;
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/template-routes")]
|
||||
public sealed class TemplateRoutesController(ISender mediator) : ControllerBase
|
||||
public sealed class TemplateRoutesController(TemplateRouteService service) : ControllerBase
|
||||
{
|
||||
[HttpGet("resolve")]
|
||||
public async Task<IActionResult> Resolve(
|
||||
@@ -19,25 +18,25 @@ public sealed class TemplateRoutesController(ISender mediator) : ControllerBase
|
||||
[FromQuery] Guid? projectId,
|
||||
[FromQuery] RouteStepStatus status,
|
||||
CancellationToken ct) =>
|
||||
(await mediator.Send(new ResolveTemplateRouteQuery(requestTypeId, userEmail, projectId, status), ct)).ToActionResult();
|
||||
(await service.ResolveAsync(requestTypeId, userEmail, projectId, status, ct)).ToActionResult();
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] Guid? requestTypeId, CancellationToken ct) =>
|
||||
(await mediator.Send(new ListTemplateRoutesQuery(requestTypeId), ct)).ToActionResult();
|
||||
(await service.ListAsync(requestTypeId, ct)).ToActionResult();
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> Get(Guid id, CancellationToken ct) =>
|
||||
(await mediator.Send(new GetTemplateRouteQuery(id), ct)).ToActionResult();
|
||||
(await service.GetAsync(id, ct)).ToActionResult();
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(CreateTemplateRouteRequest body, CancellationToken ct) =>
|
||||
(await mediator.Send(new CreateTemplateRouteCommand(body), ct)).ToActionResult();
|
||||
(await service.CreateAsync(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();
|
||||
(await service.UpdateAsync(id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) =>
|
||||
(await mediator.Send(new DeleteTemplateRouteCommand(id), ct)).ToActionResult();
|
||||
(await service.DeleteAsync(id, ct)).ToActionResult();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<PackageReference Include="Extensions.Hosting.AsyncInitialization" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="MediatR" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
+7
-3
@@ -1,6 +1,7 @@
|
||||
using System.Reflection;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Tnsu.ApprovalRoute.Application.RequestTypes;
|
||||
using Tnsu.ApprovalRoute.Application.TemplateRoutes;
|
||||
using Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Application;
|
||||
|
||||
@@ -8,7 +9,10 @@ public static class ApplicationServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddApprovalRouteApplication(this IServiceCollection services)
|
||||
{
|
||||
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
|
||||
services.AddScoped<RequestTypeService>();
|
||||
services.AddScoped<TemplateRouteService>();
|
||||
services.AddScoped<TemplateRouteStepService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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,75 @@
|
||||
using FluentResults;
|
||||
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 class RequestTypeService(ApprovalRouteDbContext db)
|
||||
{
|
||||
public async Task<Result<IReadOnlyList<RequestTypeDto>>> ListAsync(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);
|
||||
}
|
||||
|
||||
public async Task<Result<RequestTypeDto>> GetAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.RequestTypes.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
return entity is null
|
||||
? Result.Fail<RequestTypeDto>(new NotFoundError("Тип заявки", id))
|
||||
: Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result<RequestTypeDto>> CreateAsync(string name, string pageUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return Result.Fail<RequestTypeDto>(new ValidationError("Имя типа заявки обязательно."));
|
||||
if (string.IsNullOrWhiteSpace(pageUrl))
|
||||
return Result.Fail<RequestTypeDto>(new ValidationError("Url страницы обязателен."));
|
||||
|
||||
var entity = new RequestType { Name = name, PageUrl = pageUrl };
|
||||
db.RequestTypes.Add(entity);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result<RequestTypeDto>> UpdateAsync(Guid id, string name, string pageUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.RequestTypes.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail<RequestTypeDto>(new NotFoundError("Тип заявки", id));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return Result.Fail<RequestTypeDto>(new ValidationError("Имя типа заявки обязательно."));
|
||||
if (string.IsNullOrWhiteSpace(pageUrl))
|
||||
return Result.Fail<RequestTypeDto>(new ValidationError("Url страницы обязателен."));
|
||||
|
||||
entity.Name = name;
|
||||
entity.PageUrl = pageUrl;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.RequestTypes.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail(new NotFoundError("Тип заявки", id));
|
||||
|
||||
db.RequestTypes.Remove(entity);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
private static RequestTypeDto ToDto(RequestType e) => new(e.Id, e.Name, e.PageUrl);
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
+45
-50
@@ -1,5 +1,4 @@
|
||||
using FluentResults;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||
@@ -8,25 +7,37 @@ 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 sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
{
|
||||
public async Task<Result<TemplateRouteStepDto>> Handle(CreateTemplateRouteStepCommand command, CancellationToken cancellationToken)
|
||||
public async Task<Result<IReadOnlyList<TemplateRouteStepDto>>> ListAsync(Guid routeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == command.RouteId, cancellationToken);
|
||||
var items = await db.TemplateRouteSteps.AsNoTracking()
|
||||
.Where(x => x.RouteId == 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);
|
||||
}
|
||||
|
||||
public async Task<Result<TemplateRouteStepDto>> GetAsync(Guid routeId, Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.TemplateRouteSteps.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
|
||||
return entity is null
|
||||
? Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", id))
|
||||
: Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result<TemplateRouteStepDto>> CreateAsync(Guid routeId, CreateTemplateRouteStepRequest r, CancellationToken cancellationToken)
|
||||
{
|
||||
var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == routeId, cancellationToken);
|
||||
if (route is null)
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", command.RouteId));
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
|
||||
|
||||
var r = command.Request;
|
||||
var fieldsError = TemplateRouteStepValidation.ValidateFields(r.ExecutorEmail, r.Position, r.ProjectRole, route.ByProject);
|
||||
var fieldsError = ValidateFields(r.ExecutorEmail, r.Position, r.ProjectRole, route.ByProject);
|
||||
if (fieldsError is not null)
|
||||
return Result.Fail<TemplateRouteStepDto>(fieldsError);
|
||||
|
||||
@@ -44,27 +55,22 @@ public sealed class CreateTemplateRouteStepHandler(ApprovalRouteDbContext db)
|
||||
db.TemplateRouteSteps.Add(entity);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var result = Result.Ok(TemplateRouteStepMapper.ToDto(entity));
|
||||
return await TemplateRouteStepValidation.AppendMissingProjectRoleWarningAsync(db, route, result, cancellationToken);
|
||||
}
|
||||
var result = Result.Ok(ToDto(entity));
|
||||
return await AppendMissingProjectRoleWarningAsync(route, result, cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class UpdateTemplateRouteStepHandler(ApprovalRouteDbContext db)
|
||||
: IRequestHandler<UpdateTemplateRouteStepCommand, Result<TemplateRouteStepDto>>
|
||||
public async Task<Result<TemplateRouteStepDto>> UpdateAsync(
|
||||
Guid routeId, Guid id, UpdateTemplateRouteStepRequest r, CancellationToken cancellationToken)
|
||||
{
|
||||
public async Task<Result<TemplateRouteStepDto>> Handle(UpdateTemplateRouteStepCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == command.RouteId, cancellationToken);
|
||||
var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == routeId, cancellationToken);
|
||||
if (route is null)
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", command.RouteId));
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
|
||||
|
||||
var entity = await db.TemplateRouteSteps
|
||||
.FirstOrDefaultAsync(x => x.Id == command.Id && x.RouteId == command.RouteId, cancellationToken);
|
||||
var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", command.Id));
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", id));
|
||||
|
||||
var r = command.Request;
|
||||
var fieldsError = TemplateRouteStepValidation.ValidateFields(r.ExecutorEmail, r.Position, r.ProjectRole, route.ByProject);
|
||||
var fieldsError = ValidateFields(r.ExecutorEmail, r.Position, r.ProjectRole, route.ByProject);
|
||||
if (fieldsError is not null)
|
||||
return Result.Fail<TemplateRouteStepDto>(fieldsError);
|
||||
|
||||
@@ -77,31 +83,23 @@ public sealed class UpdateTemplateRouteStepHandler(ApprovalRouteDbContext db)
|
||||
entity.AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var result = Result.Ok(TemplateRouteStepMapper.ToDto(entity));
|
||||
return await TemplateRouteStepValidation.AppendMissingProjectRoleWarningAsync(db, route, result, cancellationToken);
|
||||
}
|
||||
var result = Result.Ok(ToDto(entity));
|
||||
return await AppendMissingProjectRoleWarningAsync(route, result, cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class DeleteTemplateRouteStepHandler(ApprovalRouteDbContext db)
|
||||
: IRequestHandler<DeleteTemplateRouteStepCommand, Result>
|
||||
public async Task<Result> DeleteAsync(Guid routeId, Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
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);
|
||||
var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail(new NotFoundError("Шаг маршрута", request.Id));
|
||||
return Result.Fail(new NotFoundError("Шаг маршрута", 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)
|
||||
private static IError? ValidateFields(string executorEmail, string position, ProjectRole projectRole, bool routeByProject)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(executorEmail) && string.IsNullOrWhiteSpace(position) && projectRole == ProjectRole.None)
|
||||
return new ValidationError("Исполнитель, должность и роль в проекте не могут быть пустыми одновременно.");
|
||||
@@ -112,8 +110,8 @@ internal static class TemplateRouteStepValidation
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async Task<Result<TemplateRouteStepDto>> AppendMissingProjectRoleWarningAsync(
|
||||
ApprovalRouteDbContext db, TemplateRoute route, Result<TemplateRouteStepDto> result, CancellationToken cancellationToken)
|
||||
private async Task<Result<TemplateRouteStepDto>> AppendMissingProjectRoleWarningAsync(
|
||||
TemplateRoute route, Result<TemplateRouteStepDto> result, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!route.ByProject)
|
||||
return result;
|
||||
@@ -125,10 +123,7 @@ internal static class TemplateRouteStepValidation
|
||||
? result
|
||||
: result.WithReason(new Warning("Должен быть хотя бы один шаг с ролью в проекте."));
|
||||
}
|
||||
}
|
||||
|
||||
internal static class TemplateRouteStepMapper
|
||||
{
|
||||
public static TemplateRouteStepDto ToDto(TemplateRouteStep e) => new(
|
||||
private static TemplateRouteStepDto ToDto(TemplateRouteStep e) => new(
|
||||
e.Id, e.OrderNo, e.ExecutorEmail, e.Position, e.ProjectRole, e.HeadType, e.Status, e.RouteId, e.AllowReassignToOtherApprovers);
|
||||
}
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
using FluentResults;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
||||
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||
using Tnsu.ApprovalRoute.Domain.Enums;
|
||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||
using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
|
||||
|
||||
public sealed record ResolveTemplateRouteQuery(
|
||||
Guid RequestTypeId,
|
||||
string? UserEmail,
|
||||
Guid? ProjectId,
|
||||
RouteStepStatus Status) : IRequest<Result<ResolveTemplateRouteResultDto>>;
|
||||
|
||||
public sealed class ResolveTemplateRouteHandler(
|
||||
ApprovalRouteDbContext db,
|
||||
HttpTnsuPortalClient portalClient,
|
||||
ICurrentUser currentUser)
|
||||
: IRequestHandler<ResolveTemplateRouteQuery, Result<ResolveTemplateRouteResultDto>>
|
||||
{
|
||||
public async Task<Result<ResolveTemplateRouteResultDto>> Handle(ResolveTemplateRouteQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Status == RouteStepStatus.None)
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(
|
||||
new ValidationError("Статус обязателен: Approval или Execution."));
|
||||
|
||||
var email = string.IsNullOrWhiteSpace(request.UserEmail) ? currentUser.Email : request.UserEmail;
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(
|
||||
new ValidationError("Не удалось определить email инициатора: не передан явно и отсутствует в токене."));
|
||||
|
||||
var requestTypeExists = await db.RequestTypes.AnyAsync(x => x.Id == request.RequestTypeId, cancellationToken);
|
||||
if (!requestTypeExists)
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(new NotFoundError("Тип заявки", request.RequestTypeId));
|
||||
|
||||
var employee = await portalClient.GetEmployeeByEmailAsync(email, cancellationToken);
|
||||
var byProject = request.ProjectId is not null;
|
||||
|
||||
var route = await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == request.RequestTypeId &&
|
||||
x.CompanyId == employee.CompanyId &&
|
||||
x.ByProject == byProject &&
|
||||
x.DepartmentId == employee.DepartmentId)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == request.RequestTypeId &&
|
||||
x.CompanyId == employee.CompanyId &&
|
||||
x.ByProject == byProject &&
|
||||
x.DepartmentId == null)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (route is null)
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(new NotFoundError(
|
||||
"Маршрут для этого типа заявки, отдела/компании инициатора и признака «по проекту»",
|
||||
request.RequestTypeId));
|
||||
|
||||
IReadOnlyList<ProjectExecutorInfo> projectExecutors = [];
|
||||
if (request.ProjectId is { } projectId)
|
||||
projectExecutors = await portalClient.GetProjectExecutorsAsync(projectId, cancellationToken);
|
||||
|
||||
var steps = route.Steps
|
||||
.Where(s => s.Status == request.Status)
|
||||
.OrderBy(s => s.OrderNo)
|
||||
.Select(s => ResolveStep(s, projectExecutors))
|
||||
.ToList();
|
||||
|
||||
return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RouteType, route.RequiresControl, steps));
|
||||
}
|
||||
|
||||
private static ResolvedRouteStepDto ResolveStep(TemplateRouteStep step, IReadOnlyList<ProjectExecutorInfo> projectExecutors)
|
||||
{
|
||||
// Для шагов с ролью в проекте фактический исполнитель приходит из данных проекта (api.tnsu.kz),
|
||||
// а не из статичного ExecutorEmail шаблона — тот используется только для шагов без привязки к проекту.
|
||||
if (step.ProjectRole != ProjectRole.None)
|
||||
{
|
||||
var projectExecutor = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole);
|
||||
if (projectExecutor is not null)
|
||||
return new ResolvedRouteStepDto(
|
||||
step.Id, step.OrderNo, projectExecutor.Email, projectExecutor.Position,
|
||||
step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers);
|
||||
}
|
||||
|
||||
return new ResolvedRouteStepDto(
|
||||
step.Id, step.OrderNo, step.ExecutorEmail, step.Position,
|
||||
step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using FluentResults;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
||||
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||
using Tnsu.ApprovalRoute.Domain.Enums;
|
||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||
using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
|
||||
|
||||
public sealed class TemplateRouteService(
|
||||
ApprovalRouteDbContext db,
|
||||
HttpTnsuPortalClient portalClient,
|
||||
ICurrentUser currentUser)
|
||||
{
|
||||
public async Task<Result<IReadOnlyList<TemplateRouteDto>>> ListAsync(Guid? requestTypeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = db.TemplateRoutes.AsNoTracking().AsQueryable();
|
||||
if (requestTypeId is { } id)
|
||||
query = query.Where(x => x.RequestTypeId == id);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public async Task<Result<TemplateRouteDto>> GetAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.TemplateRoutes.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
return entity is null
|
||||
? Result.Fail<TemplateRouteDto>(new NotFoundError("Маршрут", id))
|
||||
: Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result<TemplateRouteDto>> CreateAsync(CreateTemplateRouteRequest r, CancellationToken cancellationToken)
|
||||
{
|
||||
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 HasDuplicateAsync(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(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result<TemplateRouteDto>> UpdateAsync(Guid id, UpdateTemplateRouteRequest r, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail<TemplateRouteDto>(new NotFoundError("Маршрут", id));
|
||||
|
||||
if (r.CompanyId == Guid.Empty)
|
||||
return Result.Fail<TemplateRouteDto>(new ValidationError("Компания обязательна."));
|
||||
|
||||
var duplicate = await HasDuplicateAsync(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(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail(new NotFoundError("Маршрут", id));
|
||||
|
||||
db.TemplateRoutes.Remove(entity);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
public async Task<Result<ResolveTemplateRouteResultDto>> ResolveAsync(
|
||||
Guid requestTypeId, string? userEmail, Guid? projectId, RouteStepStatus status, CancellationToken cancellationToken)
|
||||
{
|
||||
if (status == RouteStepStatus.None)
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(
|
||||
new ValidationError("Статус обязателен: Approval или Execution."));
|
||||
|
||||
var email = string.IsNullOrWhiteSpace(userEmail) ? currentUser.Email : userEmail;
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(
|
||||
new ValidationError("Не удалось определить email инициатора: не передан явно и отсутствует в токене."));
|
||||
|
||||
var requestTypeExists = await db.RequestTypes.AnyAsync(x => x.Id == requestTypeId, cancellationToken);
|
||||
if (!requestTypeExists)
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(new NotFoundError("Тип заявки", requestTypeId));
|
||||
|
||||
var employee = await portalClient.GetEmployeeByEmailAsync(email, cancellationToken);
|
||||
var byProject = projectId is not null;
|
||||
|
||||
var route = await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == requestTypeId &&
|
||||
x.CompanyId == employee.CompanyId &&
|
||||
x.ByProject == byProject &&
|
||||
x.DepartmentId == employee.DepartmentId)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == requestTypeId &&
|
||||
x.CompanyId == employee.CompanyId &&
|
||||
x.ByProject == byProject &&
|
||||
x.DepartmentId == null)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (route is null)
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(new NotFoundError(
|
||||
"Маршрут для этого типа заявки, отдела/компании инициатора и признака «по проекту»",
|
||||
requestTypeId));
|
||||
|
||||
IReadOnlyList<ProjectExecutorInfo> projectExecutors = [];
|
||||
if (projectId is { } id)
|
||||
projectExecutors = await portalClient.GetProjectExecutorsAsync(id, cancellationToken);
|
||||
|
||||
var steps = route.Steps
|
||||
.Where(s => s.Status == status)
|
||||
.OrderBy(s => s.OrderNo)
|
||||
.Select(s => ResolveStep(s, projectExecutors))
|
||||
.ToList();
|
||||
|
||||
return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RouteType, route.RequiresControl, steps));
|
||||
}
|
||||
|
||||
private Task<bool> HasDuplicateAsync(
|
||||
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);
|
||||
|
||||
private static ResolvedRouteStepDto ResolveStep(TemplateRouteStep step, IReadOnlyList<ProjectExecutorInfo> projectExecutors)
|
||||
{
|
||||
// Для шагов с ролью в проекте фактический исполнитель приходит из данных проекта (api.tnsu.kz),
|
||||
// а не из статичного ExecutorEmail шаблона — тот используется только для шагов без привязки к проекту.
|
||||
if (step.ProjectRole != ProjectRole.None)
|
||||
{
|
||||
var projectExecutor = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole);
|
||||
if (projectExecutor is not null)
|
||||
return new ResolvedRouteStepDto(
|
||||
step.Id, step.OrderNo, projectExecutor.Email, projectExecutor.Position,
|
||||
step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers);
|
||||
}
|
||||
|
||||
return new ResolvedRouteStepDto(
|
||||
step.Id, step.OrderNo, step.ExecutorEmail, step.Position,
|
||||
step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers);
|
||||
}
|
||||
|
||||
private static TemplateRouteDto ToDto(TemplateRoute e) => new(
|
||||
e.Id, e.RouteType, e.RequestTypeId, e.DepartmentId, e.CompanyId, e.ByProject, e.RequiresControl);
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentResults" />
|
||||
<PackageReference Include="MediatR" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user