diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props
index 1d529ff..55b277f 100644
--- a/backend/Directory.Packages.props
+++ b/backend/Directory.Packages.props
@@ -13,7 +13,6 @@
-
diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/RequestTypesController.cs b/backend/src/Tnsu.ApprovalRoute.Api/Controllers/RequestTypesController.cs
index fda5461..9c0644a 100644
--- a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/RequestTypesController.cs
+++ b/backend/src/Tnsu.ApprovalRoute.Api/Controllers/RequestTypesController.cs
@@ -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 List(CancellationToken ct) =>
- (await mediator.Send(new ListRequestTypesQuery(), ct)).ToActionResult();
+ (await service.ListAsync(ct)).ToActionResult();
[HttpGet("{id:guid}")]
public async Task Get(Guid id, CancellationToken ct) =>
- (await mediator.Send(new GetRequestTypeQuery(id), ct)).ToActionResult();
+ (await service.GetAsync(id, ct)).ToActionResult();
[HttpPost]
public async Task 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 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 Delete(Guid id, CancellationToken ct) =>
- (await mediator.Send(new DeleteRequestTypeCommand(id), ct)).ToActionResult();
+ (await service.DeleteAsync(id, ct)).ToActionResult();
}
diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRouteStepsController.cs b/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRouteStepsController.cs
index f069f64..57d9e79 100644
--- a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRouteStepsController.cs
+++ b/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRouteStepsController.cs
@@ -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 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 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 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 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 Delete(Guid routeId, Guid id, CancellationToken ct) =>
- (await mediator.Send(new DeleteTemplateRouteStepCommand(routeId, id), ct)).ToActionResult();
+ (await service.DeleteAsync(routeId, id, ct)).ToActionResult();
}
diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs b/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs
index 7946471..473034f 100644
--- a/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs
+++ b/backend/src/Tnsu.ApprovalRoute.Api/Controllers/TemplateRoutesController.cs
@@ -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 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 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 Get(Guid id, CancellationToken ct) =>
- (await mediator.Send(new GetTemplateRouteQuery(id), ct)).ToActionResult();
+ (await service.GetAsync(id, ct)).ToActionResult();
[HttpPost]
public async Task 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 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 Delete(Guid id, CancellationToken ct) =>
- (await mediator.Send(new DeleteTemplateRouteCommand(id), ct)).ToActionResult();
+ (await service.DeleteAsync(id, ct)).ToActionResult();
}
diff --git a/backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj b/backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj
index 72b635e..a941843 100644
--- a/backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj
+++ b/backend/src/Tnsu.ApprovalRoute.Api/Tnsu.ApprovalRoute.Api.csproj
@@ -7,7 +7,6 @@
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs b/backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs
index 51291b7..63a6f1b 100644
--- a/backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs
+++ b/backend/src/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs
@@ -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();
+ services.AddScoped();
+ services.AddScoped();
+
return services;
}
}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeCommands.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeCommands.cs
deleted file mode 100644
index a2449cb..0000000
--- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeCommands.cs
+++ /dev/null
@@ -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>;
-
-public sealed record UpdateRequestTypeCommand(Guid Id, string Name, string PageUrl) : IRequest>;
-
-public sealed record DeleteRequestTypeCommand(Guid Id) : IRequest;
-
-public sealed class CreateRequestTypeHandler(ApprovalRouteDbContext db)
- : IRequestHandler>
-{
- public async Task> Handle(CreateRequestTypeCommand request, CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(request.Name))
- return Result.Fail(new ValidationError("Имя типа заявки обязательно."));
- if (string.IsNullOrWhiteSpace(request.PageUrl))
- return Result.Fail(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>
-{
- public async Task> Handle(UpdateRequestTypeCommand 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));
-
- if (string.IsNullOrWhiteSpace(request.Name))
- return Result.Fail(new ValidationError("Имя типа заявки обязательно."));
- if (string.IsNullOrWhiteSpace(request.PageUrl))
- return Result.Fail(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
-{
- public async Task 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);
-}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeQueries.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeQueries.cs
deleted file mode 100644
index 93b75cd..0000000
--- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeQueries.cs
+++ /dev/null
@@ -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>;
-
-public sealed record ListRequestTypesQuery : IRequest>>;
-
-public sealed class GetRequestTypeHandler(ApprovalRouteDbContext db)
- : IRequestHandler>
-{
- public async Task> Handle(GetRequestTypeQuery request, CancellationToken cancellationToken)
- {
- var entity = await db.RequestTypes.AsNoTracking()
- .FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
-
- return entity is null
- ? Result.Fail(new NotFoundError("Тип заявки", request.Id))
- : Result.Ok(RequestTypeMapper.ToDto(entity));
- }
-}
-
-public sealed class ListRequestTypesHandler(ApprovalRouteDbContext db)
- : IRequestHandler>>
-{
- public async Task>> 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>(items);
- }
-}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs
new file mode 100644
index 0000000..6be017c
--- /dev/null
+++ b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs
@@ -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>> 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>(items);
+ }
+
+ public async Task> GetAsync(Guid id, CancellationToken cancellationToken)
+ {
+ var entity = await db.RequestTypes.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
+
+ return entity is null
+ ? Result.Fail(new NotFoundError("Тип заявки", id))
+ : Result.Ok(ToDto(entity));
+ }
+
+ public async Task> CreateAsync(string name, string pageUrl, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return Result.Fail(new ValidationError("Имя типа заявки обязательно."));
+ if (string.IsNullOrWhiteSpace(pageUrl))
+ return Result.Fail(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> 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(new NotFoundError("Тип заявки", id));
+
+ if (string.IsNullOrWhiteSpace(name))
+ return Result.Fail(new ValidationError("Имя типа заявки обязательно."));
+ if (string.IsNullOrWhiteSpace(pageUrl))
+ return Result.Fail(new ValidationError("Url страницы обязателен."));
+
+ entity.Name = name;
+ entity.PageUrl = pageUrl;
+ await db.SaveChangesAsync(cancellationToken);
+
+ return Result.Ok(ToDto(entity));
+ }
+
+ public async Task 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);
+}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepQueries.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepQueries.cs
deleted file mode 100644
index 2ddce86..0000000
--- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepQueries.cs
+++ /dev/null
@@ -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>;
-
-public sealed record ListTemplateRouteStepsQuery(Guid RouteId) : IRequest>>;
-
-public sealed class GetTemplateRouteStepHandler(ApprovalRouteDbContext db)
- : IRequestHandler>
-{
- public async Task> 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(new NotFoundError("Шаг маршрута", request.Id))
- : Result.Ok(TemplateRouteStepMapper.ToDto(entity));
- }
-}
-
-public sealed class ListTemplateRouteStepsHandler(ApprovalRouteDbContext db)
- : IRequestHandler>>
-{
- public async Task>> 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>(items);
- }
-}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepCommands.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs
similarity index 50%
rename from backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepCommands.cs
rename to backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs
index fa858a8..ae8c55f 100644
--- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepCommands.cs
+++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs
@@ -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>;
-
-public sealed record UpdateTemplateRouteStepCommand(Guid RouteId, Guid Id, UpdateTemplateRouteStepRequest Request)
- : IRequest>;
-
-public sealed record DeleteTemplateRouteStepCommand(Guid RouteId, Guid Id) : IRequest;
-
-public sealed class CreateTemplateRouteStepHandler(ApprovalRouteDbContext db)
- : IRequestHandler>
+public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
{
- public async Task> Handle(CreateTemplateRouteStepCommand command, CancellationToken cancellationToken)
+ public async Task>> ListAsync(Guid routeId, CancellationToken cancellationToken)
{
- var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == command.RouteId, cancellationToken);
- if (route is null)
- return Result.Fail(new NotFoundError("Маршрут", command.RouteId));
+ 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);
- var r = command.Request;
- var fieldsError = TemplateRouteStepValidation.ValidateFields(r.ExecutorEmail, r.Position, r.ProjectRole, route.ByProject);
+ return Result.Ok>(items);
+ }
+
+ public async Task> 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(new NotFoundError("Шаг маршрута", id))
+ : Result.Ok(ToDto(entity));
+ }
+
+ public async Task> 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(new NotFoundError("Маршрут", routeId));
+
+ var fieldsError = ValidateFields(r.ExecutorEmail, r.Position, r.ProjectRole, route.ByProject);
if (fieldsError is not null)
return Result.Fail(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>
-{
- public async Task> Handle(UpdateTemplateRouteStepCommand command, CancellationToken cancellationToken)
+ public async Task> UpdateAsync(
+ Guid routeId, Guid id, UpdateTemplateRouteStepRequest r, 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(new NotFoundError("Маршрут", command.RouteId));
+ return Result.Fail(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(new NotFoundError("Шаг маршрута", command.Id));
+ return Result.Fail(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(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
-{
- public async Task Handle(DeleteTemplateRouteStepCommand request, CancellationToken cancellationToken)
+ public async Task DeleteAsync(Guid routeId, Guid id, 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> AppendMissingProjectRoleWarningAsync(
- ApprovalRouteDbContext db, TemplateRoute route, Result result, CancellationToken cancellationToken)
+ private async Task> AppendMissingProjectRoleWarningAsync(
+ TemplateRoute route, Result 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);
}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteQuery.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteQuery.cs
deleted file mode 100644
index 5e83386..0000000
--- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/ResolveTemplateRouteQuery.cs
+++ /dev/null
@@ -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>;
-
-public sealed class ResolveTemplateRouteHandler(
- ApprovalRouteDbContext db,
- HttpTnsuPortalClient portalClient,
- ICurrentUser currentUser)
- : IRequestHandler>
-{
- public async Task> Handle(ResolveTemplateRouteQuery request, CancellationToken cancellationToken)
- {
- if (request.Status == RouteStepStatus.None)
- return Result.Fail(
- new ValidationError("Статус обязателен: Approval или Execution."));
-
- var email = string.IsNullOrWhiteSpace(request.UserEmail) ? currentUser.Email : request.UserEmail;
- if (string.IsNullOrWhiteSpace(email))
- return Result.Fail(
- new ValidationError("Не удалось определить email инициатора: не передан явно и отсутствует в токене."));
-
- var requestTypeExists = await db.RequestTypes.AnyAsync(x => x.Id == request.RequestTypeId, cancellationToken);
- if (!requestTypeExists)
- return Result.Fail(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(new NotFoundError(
- "Маршрут для этого типа заявки, отдела/компании инициатора и признака «по проекту»",
- request.RequestTypeId));
-
- IReadOnlyList 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 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);
- }
-}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteCommands.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteCommands.cs
deleted file mode 100644
index 19c54a0..0000000
--- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteCommands.cs
+++ /dev/null
@@ -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>;
-
-public sealed record UpdateTemplateRouteCommand(Guid Id, UpdateTemplateRouteRequest Request) : IRequest>;
-
-public sealed record DeleteTemplateRouteCommand(Guid Id) : IRequest;
-
-public sealed class CreateTemplateRouteHandler(ApprovalRouteDbContext db)
- : IRequestHandler>
-{
- public async Task> Handle(CreateTemplateRouteCommand command, CancellationToken cancellationToken)
- {
- var r = command.Request;
-
- if (r.CompanyId == Guid.Empty)
- return Result.Fail(new ValidationError("Компания обязательна."));
-
- var requestTypeExists = await db.RequestTypes.AnyAsync(x => x.Id == r.RequestTypeId, cancellationToken);
- if (!requestTypeExists)
- return Result.Fail(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(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>
-{
- public async Task> Handle(UpdateTemplateRouteCommand command, CancellationToken cancellationToken)
- {
- var entity = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == command.Id, cancellationToken);
- if (entity is null)
- return Result.Fail(new NotFoundError("Маршрут", command.Id));
-
- var r = command.Request;
-
- if (r.CompanyId == Guid.Empty)
- return Result.Fail(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(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
-{
- public async Task 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 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);
-}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteQueries.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteQueries.cs
deleted file mode 100644
index 54e417a..0000000
--- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteQueries.cs
+++ /dev/null
@@ -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>;
-
-public sealed record ListTemplateRoutesQuery(Guid? RequestTypeId) : IRequest>>;
-
-public sealed class GetTemplateRouteHandler(ApprovalRouteDbContext db)
- : IRequestHandler>
-{
- public async Task> Handle(GetTemplateRouteQuery request, CancellationToken cancellationToken)
- {
- var entity = await db.TemplateRoutes.AsNoTracking()
- .FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
-
- return entity is null
- ? Result.Fail(new NotFoundError("Маршрут", request.Id))
- : Result.Ok(TemplateRouteMapper.ToDto(entity));
- }
-}
-
-public sealed class ListTemplateRoutesHandler(ApprovalRouteDbContext db)
- : IRequestHandler>>
-{
- public async Task>> 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>(items);
- }
-}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs
new file mode 100644
index 0000000..4fa96d8
--- /dev/null
+++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRoutes/TemplateRouteService.cs
@@ -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>> 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>(items);
+ }
+
+ public async Task> GetAsync(Guid id, CancellationToken cancellationToken)
+ {
+ var entity = await db.TemplateRoutes.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
+
+ return entity is null
+ ? Result.Fail(new NotFoundError("Маршрут", id))
+ : Result.Ok(ToDto(entity));
+ }
+
+ public async Task> CreateAsync(CreateTemplateRouteRequest r, CancellationToken cancellationToken)
+ {
+ if (r.CompanyId == Guid.Empty)
+ return Result.Fail(new ValidationError("Компания обязательна."));
+
+ var requestTypeExists = await db.RequestTypes.AnyAsync(x => x.Id == r.RequestTypeId, cancellationToken);
+ if (!requestTypeExists)
+ return Result.Fail(new NotFoundError("Тип заявки", r.RequestTypeId));
+
+ var duplicate = await HasDuplicateAsync(r.RequestTypeId, r.DepartmentId, r.CompanyId, r.ByProject, excludeId: null, cancellationToken);
+ if (duplicate)
+ return Result.Fail(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> 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(new NotFoundError("Маршрут", id));
+
+ if (r.CompanyId == Guid.Empty)
+ return Result.Fail(new ValidationError("Компания обязательна."));
+
+ var duplicate = await HasDuplicateAsync(entity.RequestTypeId, r.DepartmentId, r.CompanyId, r.ByProject, excludeId: entity.Id, cancellationToken);
+ if (duplicate)
+ return Result.Fail(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 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> ResolveAsync(
+ Guid requestTypeId, string? userEmail, Guid? projectId, RouteStepStatus status, CancellationToken cancellationToken)
+ {
+ if (status == RouteStepStatus.None)
+ return Result.Fail(
+ new ValidationError("Статус обязателен: Approval или Execution."));
+
+ var email = string.IsNullOrWhiteSpace(userEmail) ? currentUser.Email : userEmail;
+ if (string.IsNullOrWhiteSpace(email))
+ return Result.Fail(
+ new ValidationError("Не удалось определить email инициатора: не передан явно и отсутствует в токене."));
+
+ var requestTypeExists = await db.RequestTypes.AnyAsync(x => x.Id == requestTypeId, cancellationToken);
+ if (!requestTypeExists)
+ return Result.Fail(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(new NotFoundError(
+ "Маршрут для этого типа заявки, отдела/компании инициатора и признака «по проекту»",
+ requestTypeId));
+
+ IReadOnlyList 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 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 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);
+}
diff --git a/backend/src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj b/backend/src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj
index 409b6fe..9988106 100644
--- a/backend/src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj
+++ b/backend/src/Tnsu.ApprovalRoute.Application/Tnsu.ApprovalRoute.Application.csproj
@@ -5,7 +5,6 @@
-