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>> 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>(items); } public async Task> 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(new NotFoundError("Шаг маршрута", id)) : Result.Ok(ToDto(entity)); } public async Task> 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(new NotFoundError("Маршрут", routeId)); if (!route.ByProject && r.ProjectRole is not (null or ProjectRole.None)) return Result.Fail(new ValidationError( "Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false).")); if (!TemplateRouteStepFields.IsConditionValid(r.FieldName, r.Operation, r.Value)) return Result.Fail(new ValidationError( "Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе.")); if (RequiresListName(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListName)) return Result.Fail(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(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> 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(new NotFoundError("Маршрут", routeId)); var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken); if (entity is null) return Result.Fail(new NotFoundError("Шаг маршрута", id)); if (!route.ByProject && r.ProjectRole is not (null or ProjectRole.None)) return Result.Fail(new ValidationError( "Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false).")); if (!TemplateRouteStepFields.IsConditionValid(r.FieldName, r.Operation, r.Value)) return Result.Fail(new ValidationError( "Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе.")); if (RequiresListName(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListName)) return Result.Fail(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(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> 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(new NotFoundError("Маршрут", routeId)); var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken); if (entity is null) return Result.Fail(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(new ValidationError("Статус обязателен: Approval или Execution.")); if (!route.ByProject && projectRole is not (null or ProjectRole.None)) return Result.Fail(new ValidationError( "Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false).")); if (TemplateRouteStepFields.CountFilled(executorEmail, position, projectRole, headType) != 1) return Result.Fail(new ValidationError( "Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя.")); if (!TemplateRouteStepFields.IsConditionValid(fieldName, operation, value)) return Result.Fail(new ValidationError( "Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе.")); if (RequiresListName(fieldName, operation, value) && string.IsNullOrWhiteSpace(route.RequestType?.ListName)) return Result.Fail(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(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 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 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 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); }