Files
tnsu-approval-route/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs
T
2026-07-31 11:54:16 +05:00

230 lines
13 KiB
C#

using FluentResults;
using Microsoft.EntityFrameworkCore;
using Tnsu.ApprovalRoute.Application.Common.Errors;
using Tnsu.ApprovalRoute.Domain.Entities;
using Tnsu.ApprovalRoute.Domain.Enums;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
{
public async Task<Result<IReadOnlyList<TemplateRouteStepDto>>> ListAsync(int routeId, CancellationToken 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,
x.FieldName, x.Operation, x.Value))
.ToListAsync(cancellationToken);
return Result.Ok<IReadOnlyList<TemplateRouteStepDto>>(items);
}
public async Task<Result<TemplateRouteStepDto>> GetAsync(int routeId, int 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(int routeId, CreateTemplateRouteStepRequest r, CancellationToken cancellationToken)
{
var route = await db.TemplateRoutes.Include(x => x.RequestType).FirstOrDefaultAsync(x => x.Id == routeId, cancellationToken);
if (route is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
if (!route.ByProject && r.ProjectRole is not (null or ProjectRole.None))
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
if (!TemplateRouteStepFields.IsConditionValid(r.FieldName, r.Operation, r.Value))
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."));
if (RequiresListName(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListName))
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано имя списка (list_name)."));
var duplicate = await HasDuplicateAsync(
route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, excludeId: null, cancellationToken);
if (duplicate)
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
var entity = new TemplateRouteStep
{
RouteId = route.Id,
OrderNo = r.OrderNo,
ExecutorEmail = r.ExecutorEmail,
Position = r.Position,
ProjectRole = r.ProjectRole,
HeadType = r.HeadType,
Status = r.Status,
AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers,
FieldName = r.FieldName,
Operation = r.Operation,
Value = r.Value
};
db.TemplateRouteSteps.Add(entity);
await db.SaveChangesAsync(cancellationToken);
return Result.Ok(await ToDtoWithWarningAsync(entity, route, cancellationToken));
}
public async Task<Result<TemplateRouteStepDto>> UpdateAsync(
int routeId, int id, UpdateTemplateRouteStepRequest r, CancellationToken cancellationToken)
{
var route = await db.TemplateRoutes.Include(x => x.RequestType).FirstOrDefaultAsync(x => x.Id == routeId, cancellationToken);
if (route is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
if (entity is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", id));
if (!route.ByProject && r.ProjectRole is not (null or ProjectRole.None))
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
if (!TemplateRouteStepFields.IsConditionValid(r.FieldName, r.Operation, r.Value))
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."));
if (RequiresListName(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListName))
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано имя списка (list_name)."));
var duplicate = await HasDuplicateAsync(
route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, excludeId: entity.Id, cancellationToken);
if (duplicate)
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
entity.OrderNo = r.OrderNo;
entity.ExecutorEmail = r.ExecutorEmail;
entity.Position = r.Position;
entity.ProjectRole = r.ProjectRole;
entity.HeadType = r.HeadType;
entity.Status = r.Status;
entity.AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers;
entity.FieldName = r.FieldName;
entity.Operation = r.Operation;
entity.Value = r.Value;
await db.SaveChangesAsync(cancellationToken);
return Result.Ok(await ToDtoWithWarningAsync(entity, route, cancellationToken));
}
public async Task<Result<TemplateRouteStepDto>> PatchAsync(
int routeId, int id, PatchTemplateRouteStepRequest r, CancellationToken cancellationToken)
{
var route = await db.TemplateRoutes.Include(x => x.RequestType).FirstOrDefaultAsync(x => x.Id == routeId, cancellationToken);
if (route is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
if (entity is null)
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", id));
var executorEmail = r.ExecutorEmail ?? entity.ExecutorEmail;
var position = r.Position ?? entity.Position;
var projectRole = r.ProjectRole ?? entity.ProjectRole;
var headType = r.HeadType ?? entity.HeadType;
var status = r.Status ?? entity.Status;
var fieldName = r.FieldName ?? entity.FieldName;
var operation = r.Operation ?? entity.Operation;
var value = r.Value ?? entity.Value;
if (status == RouteStepStatus.None)
return Result.Fail<TemplateRouteStepDto>(new ValidationError("Статус обязателен: Approval или Execution."));
if (!route.ByProject && projectRole is not (null or ProjectRole.None))
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
if (TemplateRouteStepFields.CountFilled(executorEmail, position, projectRole, headType) != 1)
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя."));
if (!TemplateRouteStepFields.IsConditionValid(fieldName, operation, value))
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."));
if (RequiresListName(fieldName, operation, value) && string.IsNullOrWhiteSpace(route.RequestType?.ListName))
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано имя списка (list_name)."));
var duplicate = await HasDuplicateAsync(
route.Id, executorEmail, position, projectRole, headType, status, excludeId: entity.Id, cancellationToken);
if (duplicate)
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
entity.OrderNo = r.OrderNo ?? entity.OrderNo;
entity.ExecutorEmail = executorEmail;
entity.Position = position;
entity.ProjectRole = projectRole;
entity.HeadType = headType;
entity.Status = status;
entity.AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers ?? entity.AllowReassignToOtherApprovers;
entity.FieldName = fieldName;
entity.Operation = operation;
entity.Value = value;
await db.SaveChangesAsync(cancellationToken);
return Result.Ok(await ToDtoWithWarningAsync(entity, route, cancellationToken));
}
public async Task<Result> DeleteAsync(int routeId, int id, CancellationToken cancellationToken)
{
var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
if (entity is null)
return Result.Fail(new NotFoundError("Шаг маршрута", id));
db.TemplateRouteSteps.Remove(entity);
await db.SaveChangesAsync(cancellationToken);
return Result.Ok();
}
private static bool RequiresListName(string? fieldName, ComparisonOperator? operation, string? value) =>
TemplateRouteStepFields.CountConditionFilled(fieldName, operation, value) > 0;
private Task<bool> HasDuplicateAsync(
int routeId, string? executorEmail, string? position, ProjectRole? projectRole, HeadType? headType,
RouteStepStatus status, int? excludeId, CancellationToken cancellationToken) =>
db.TemplateRouteSteps.AnyAsync(x =>
x.Id != excludeId &&
x.RouteId == routeId &&
x.ExecutorEmail == executorEmail &&
x.Position == position &&
x.ProjectRole == projectRole &&
x. HeadType == headType &&
x.Status == status,
cancellationToken);
private async Task<TemplateRouteStepDto> ToDtoWithWarningAsync(
TemplateRouteStep entity, TemplateRoute route, CancellationToken cancellationToken)
{
string? warning = null;
if (route.ByProject)
{
var hasProjectRoleStep = await db.TemplateRouteSteps
.AnyAsync(x => x.RouteId == route.Id && x.ProjectRole != null && x.ProjectRole != ProjectRole.None, cancellationToken);
if (!hasProjectRoleStep)
warning = "Должен быть хотя бы один шаг с ролью в проекте.";
}
return ToDto(entity) with { Warning = warning };
}
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,
e.FieldName, e.Operation, e.Value);
}