Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/2 Co-authored-by: gorbenko.pavel <gorbenko.pavel@tnsu.kz> Co-committed-by: gorbenko.pavel <gorbenko.pavel@tnsu.kz>
This commit was merged in pull request #2.
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
using FluentResults;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
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,
|
||||
ITnsuPortalClient portalClient,
|
||||
ICurrentUser currentUser,
|
||||
IOptions<TnsuPortalOptions> portalOptions)
|
||||
{
|
||||
public async Task<Result<IReadOnlyList<TemplateRouteDto>>> ListAsync(int? 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(int 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)
|
||||
{
|
||||
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(int 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));
|
||||
|
||||
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<TemplateRouteDto>> PatchAsync(int id, PatchTemplateRouteRequest 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));
|
||||
|
||||
var departmentId = r.DepartmentId ?? entity.DepartmentId;
|
||||
var companyId = r.CompanyId ?? entity.CompanyId;
|
||||
var byProject = r.ByProject ?? entity.ByProject;
|
||||
|
||||
var duplicate = await HasDuplicateAsync(entity.RequestTypeId, departmentId, companyId, byProject, excludeId: entity.Id, cancellationToken);
|
||||
if (duplicate)
|
||||
return Result.Fail<TemplateRouteDto>(new ConflictError(
|
||||
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
|
||||
|
||||
entity.RouteType = r.RouteType ?? entity.RouteType;
|
||||
entity.DepartmentId = departmentId;
|
||||
entity.CompanyId = companyId;
|
||||
entity.ByProject = byProject;
|
||||
entity.RequiresControl = r.RequiresControl ?? entity.RequiresControl;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(int 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(
|
||||
ResolveTemplateRouteRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
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);
|
||||
if (employee is null)
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(
|
||||
new NotFoundError("Сотрудник в справочнике api.tnsu.kz по email", email));
|
||||
|
||||
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 matchingSteps = route.Steps
|
||||
.Where(s => s.Status == request.Status)
|
||||
.OrderBy(s => s.OrderNo)
|
||||
.ToList();
|
||||
|
||||
var steps = new List<ResolvedRouteStepDto>();
|
||||
foreach (var step in matchingSteps)
|
||||
steps.Add(await ResolveStepAsync(step, email, employee.Company, projectExecutors, cancellationToken));
|
||||
|
||||
var collapsedSteps = CollapseByExecutorEmail(steps);
|
||||
|
||||
return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RouteType, route.RequiresControl, collapsedSteps));
|
||||
}
|
||||
|
||||
private Task<bool> HasDuplicateAsync(
|
||||
int requestTypeId, Guid? departmentId, Guid companyId, bool byProject, int? excludeId, CancellationToken cancellationToken) =>
|
||||
db.TemplateRoutes.AnyAsync(x =>
|
||||
x.Id != excludeId &&
|
||||
x.RequestTypeId == requestTypeId &&
|
||||
x.DepartmentId == departmentId &&
|
||||
x.CompanyId == companyId &&
|
||||
x.ByProject == byProject,
|
||||
cancellationToken);
|
||||
|
||||
private async Task<ResolvedRouteStepDto> ResolveStepAsync(
|
||||
TemplateRouteStep step, string initiatorEmail, string initiatorCompany,
|
||||
IReadOnlyList<ProjectExecutorInfo> projectExecutors, CancellationToken cancellationToken)
|
||||
{
|
||||
string? resolvedEmail;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(step.ExecutorEmail))
|
||||
resolvedEmail = step.ExecutorEmail;
|
||||
else if (!string.IsNullOrWhiteSpace(step.Position))
|
||||
resolvedEmail = await portalClient.GetEmployeeByPositionAsync(step.Position, initiatorCompany, cancellationToken);
|
||||
else if (step.ProjectRole is not (null or ProjectRole.None))
|
||||
resolvedEmail = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole)?.Email;
|
||||
else if (step.HeadType is not (null or HeadType.None))
|
||||
resolvedEmail = step.HeadType switch
|
||||
{
|
||||
HeadType.Chief => await portalClient.GetManagerAsync(initiatorEmail, "chief", cancellationToken),
|
||||
HeadType.DirectionHead => await portalClient.GetManagerAsync(initiatorEmail, "directionHead", cancellationToken),
|
||||
HeadType.DirectorTansu => portalOptions.Value.DirectorTansuEmail,
|
||||
_ => null
|
||||
};
|
||||
else
|
||||
resolvedEmail = null;
|
||||
|
||||
string? resolvedPosition = null;
|
||||
if (!string.IsNullOrWhiteSpace(resolvedEmail))
|
||||
{
|
||||
var resolvedEmployee = await portalClient.GetEmployeeByEmailAsync(resolvedEmail, cancellationToken);
|
||||
resolvedPosition = resolvedEmployee?.Position;
|
||||
}
|
||||
|
||||
return new ResolvedRouteStepDto(step.Id, step.OrderNo, resolvedEmail, resolvedPosition, step.AllowReassignToOtherApprovers);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ResolvedRouteStepDto> CollapseByExecutorEmail(IReadOnlyList<ResolvedRouteStepDto> steps)
|
||||
{
|
||||
var seenEmails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var result = new List<ResolvedRouteStepDto>();
|
||||
|
||||
foreach (var step in steps)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(step.ExecutorEmail) && !seenEmails.Add(step.ExecutorEmail))
|
||||
continue;
|
||||
|
||||
result.Add(step);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static TemplateRouteDto ToDto(TemplateRoute e) => new(
|
||||
e.Id, e.RouteType, e.RequestTypeId, e.DepartmentId, e.CompanyId, e.ByProject, e.RequiresControl);
|
||||
}
|
||||
Reference in New Issue
Block a user