diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs index 50cdce7..96e0e5e 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs @@ -1,9 +1,9 @@ namespace Tnsu.ApprovalRoute.Application.RequestTypes; -public sealed record RequestTypeDto(int Id, string Name, string PageUrl); +public sealed record RequestTypeDto(int Id, string Name, string PageUrl, string? ListName, string Section); -public sealed record CreateRequestTypeRequest(string Name, string PageUrl); +public sealed record CreateRequestTypeRequest(string Name, string PageUrl, string? ListName, string Section); -public sealed record UpdateRequestTypeRequest(string Name, string PageUrl); +public sealed record UpdateRequestTypeRequest(string Name, string PageUrl, string? ListName, string Section); -public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl); +public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl, string? ListName, string? Section); diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs index 481c5d9..999ccf0 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs @@ -12,7 +12,7 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) { var items = await db.RequestTypes.AsNoTracking() .OrderBy(x => x.Name) - .Select(x => new RequestTypeDto(x.Id, x.Name, x.PageUrl)) + .Select(x => new RequestTypeDto(x.Id, x.Name, x.PageUrl, x.ListName, x.Section)) .ToListAsync(cancellationToken); return Result.Ok>(items); @@ -29,7 +29,13 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) public async Task> CreateAsync(CreateRequestTypeRequest request, CancellationToken cancellationToken) { - var entity = new RequestType { Name = request.Name, PageUrl = request.PageUrl }; + var entity = new RequestType + { + Name = request.Name, + PageUrl = request.PageUrl, + ListName = request.ListName, + Section = request.Section + }; db.RequestTypes.Add(entity); await db.SaveChangesAsync(cancellationToken); @@ -44,6 +50,8 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) entity.Name = request.Name; entity.PageUrl = request.PageUrl; + entity.ListName = request.ListName; + entity.Section = request.Section; await db.SaveChangesAsync(cancellationToken); return Result.Ok(ToDto(entity)); @@ -57,6 +65,8 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) entity.Name = request.Name ?? entity.Name; entity.PageUrl = request.PageUrl ?? entity.PageUrl; + entity.ListName = request.ListName ?? entity.ListName; + entity.Section = request.Section ?? entity.Section; await db.SaveChangesAsync(cancellationToken); return Result.Ok(ToDto(entity)); @@ -74,5 +84,5 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) return Result.Ok(); } - private static RequestTypeDto ToDto(RequestType e) => new(e.Id, e.Name, e.PageUrl); + private static RequestTypeDto ToDto(RequestType e) => new(e.Id, e.Name, e.PageUrl, e.ListName, e.Section); } diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs index 51946db..5a14f9c 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs @@ -8,6 +8,7 @@ public sealed class CreateRequestTypeRequestValidator : AbstractValidator x.Name).NotEmpty().WithMessage("Имя типа заявки обязательно."); RuleFor(x => x.PageUrl).NotEmpty().WithMessage("Url страницы обязателен."); + RuleFor(x => x.Section).NotEmpty().WithMessage("Раздел обязателен."); } } @@ -17,6 +18,7 @@ public sealed class UpdateRequestTypeRequestValidator : AbstractValidator x.Name).NotEmpty().WithMessage("Имя типа заявки обязательно."); RuleFor(x => x.PageUrl).NotEmpty().WithMessage("Url страницы обязателен."); + RuleFor(x => x.Section).NotEmpty().WithMessage("Раздел обязателен."); } } @@ -28,5 +30,7 @@ public sealed class PatchRequestTypeRequestValidator : AbstractValidator x.Name).NotEmpty().WithMessage("Имя типа заявки не может быть пустым.")); When(x => x.PageUrl is not null, () => RuleFor(x => x.PageUrl).NotEmpty().WithMessage("Url страницы не может быть пустым.")); + When(x => x.Section is not null, () => + RuleFor(x => x.Section).NotEmpty().WithMessage("Раздел не может быть пустым.")); } } diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs index ca3074e..200c375 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepDto.cs @@ -12,6 +12,9 @@ public sealed record TemplateRouteStepDto( RouteStepStatus Status, int RouteId, bool AllowReassignToOtherApprovers, + string? FieldName, + ComparisonOperator? Operation, + string? Value, string? Warning = null); public sealed record CreateTemplateRouteStepRequest( @@ -21,7 +24,10 @@ public sealed record CreateTemplateRouteStepRequest( ProjectRole? ProjectRole, HeadType? HeadType, RouteStepStatus Status, - bool AllowReassignToOtherApprovers); + bool AllowReassignToOtherApprovers, + string? FieldName, + ComparisonOperator? Operation, + string? Value); public sealed record UpdateTemplateRouteStepRequest( int OrderNo, @@ -30,7 +36,10 @@ public sealed record UpdateTemplateRouteStepRequest( ProjectRole? ProjectRole, HeadType? HeadType, RouteStepStatus Status, - bool AllowReassignToOtherApprovers); + bool AllowReassignToOtherApprovers, + string? FieldName, + ComparisonOperator? Operation, + string? Value); public sealed record PatchTemplateRouteStepRequest( int? OrderNo, @@ -39,4 +48,7 @@ public sealed record PatchTemplateRouteStepRequest( ProjectRole? ProjectRole, HeadType? HeadType, RouteStepStatus? Status, - bool? AllowReassignToOtherApprovers); + bool? AllowReassignToOtherApprovers, + string? FieldName, + ComparisonOperator? Operation, + string? Value); diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs index 9e100bb..4525edf 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs @@ -15,7 +15,8 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) .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.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); @@ -33,7 +34,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) public async Task> CreateAsync(int routeId, CreateTemplateRouteStepRequest r, CancellationToken cancellationToken) { - var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == routeId, 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)); @@ -41,6 +42,14 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) 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) @@ -56,7 +65,10 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) ProjectRole = r.ProjectRole, HeadType = r.HeadType, Status = r.Status, - AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers + AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers, + FieldName = r.FieldName, + Operation = r.Operation, + Value = r.Value }; db.TemplateRouteSteps.Add(entity); await db.SaveChangesAsync(cancellationToken); @@ -67,7 +79,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) public async Task> UpdateAsync( int routeId, int id, UpdateTemplateRouteStepRequest r, CancellationToken cancellationToken) { - var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == routeId, 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)); @@ -79,6 +91,14 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) 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) @@ -92,6 +112,9 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) 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)); @@ -100,7 +123,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) public async Task> PatchAsync( int routeId, int id, PatchTemplateRouteStepRequest r, CancellationToken cancellationToken) { - var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == routeId, 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)); @@ -113,6 +136,9 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) 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.")); @@ -125,6 +151,14 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) 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) @@ -138,6 +172,9 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) 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)); @@ -155,6 +192,9 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) 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) => @@ -184,5 +224,6 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) } 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.Id, e.OrderNo, e.ExecutorEmail, e.Position, e.ProjectRole, e.HeadType, e.Status, e.RouteId, e.AllowReassignToOtherApprovers, + e.FieldName, e.Operation, e.Value); } diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs index 89357a5..1c4ac4b 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepValidators.cs @@ -11,6 +11,9 @@ public sealed class CreateTemplateRouteStepRequestValidator : AbstractValidator< RuleFor(x => x) .Must(x => TemplateRouteStepFields.CountFilled(x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType) == 1) .WithMessage("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя."); + RuleFor(x => x) + .Must(x => TemplateRouteStepFields.IsConditionValid(x.FieldName, x.Operation, x.Value)) + .WithMessage("Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."); } } @@ -22,6 +25,9 @@ public sealed class UpdateTemplateRouteStepRequestValidator : AbstractValidator< RuleFor(x => x) .Must(x => TemplateRouteStepFields.CountFilled(x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType) == 1) .WithMessage("Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя."); + RuleFor(x => x) + .Must(x => TemplateRouteStepFields.IsConditionValid(x.FieldName, x.Operation, x.Value)) + .WithMessage("Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."); } } @@ -32,4 +38,15 @@ internal static class TemplateRouteStepFields (string.IsNullOrWhiteSpace(position) ? 0 : 1) + (projectRole is null or ProjectRole.None ? 0 : 1) + (headType is null or HeadType.None ? 0 : 1); + + public static int CountConditionFilled(string? fieldName, ComparisonOperator? operation, string? value) => + (string.IsNullOrWhiteSpace(fieldName) ? 0 : 1) + + (operation is null or ComparisonOperator.None ? 0 : 1) + + (string.IsNullOrWhiteSpace(value) ? 0 : 1); + + public static bool IsConditionValid(string? fieldName, ComparisonOperator? operation, string? value) + { + var count = CountConditionFilled(fieldName, operation, value); + return count == 0 || count == 3; + } } diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs b/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs index d86af83..dfcb7d6 100644 --- a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs +++ b/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs @@ -5,6 +5,8 @@ public class RequestType public int Id { get; set; } public string Name { get; set; } = string.Empty; public string PageUrl { get; set; } = string.Empty; + public string? ListName { get; set; } + public string Section { get; set; } = string.Empty; public ICollection Routes { get; set; } = new List(); } diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs b/backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs index 9901b07..ce5a6be 100644 --- a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs +++ b/backend/src/Tnsu.ApprovalRoute.Domain/Entities/TemplateRouteStep.cs @@ -16,5 +16,9 @@ public class TemplateRouteStep public int RouteId { get; set; } public bool AllowReassignToOtherApprovers { get; set; } + public string? FieldName { get; set; } + public ComparisonOperator? Operation { get; set; } + public string? Value { get; set; } + public TemplateRoute? Route { get; set; } } diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Enums/ComparisonOperator.cs b/backend/src/Tnsu.ApprovalRoute.Domain/Enums/ComparisonOperator.cs new file mode 100644 index 0000000..7bfbdce --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Domain/Enums/ComparisonOperator.cs @@ -0,0 +1,9 @@ +namespace Tnsu.ApprovalRoute.Domain.Enums; + +public enum ComparisonOperator +{ + None, + GreaterThan, + LessThan, + Equal +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs index 2c84977..401fa69 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs @@ -16,6 +16,8 @@ public class ApprovalRouteDbContext(DbContextOptions opt e.HasKey(x => x.Id); e.Property(x => x.Name).HasMaxLength(256).IsRequired(); e.Property(x => x.PageUrl).HasMaxLength(512).IsRequired(); + e.Property(x => x.ListName).HasMaxLength(256); + e.Property(x => x.Section).HasMaxLength(256).IsRequired(); }); modelBuilder.Entity(e => @@ -34,6 +36,9 @@ public class ApprovalRouteDbContext(DbContextOptions opt e.Property(x => x.ProjectRole).HasConversion().HasMaxLength(32); e.Property(x => x.HeadType).HasConversion().HasMaxLength(32); e.Property(x => x.Status).HasConversion().HasMaxLength(32).IsRequired(); + e.Property(x => x.FieldName).HasMaxLength(128); + e.Property(x => x.Operation).HasConversion().HasMaxLength(32); + e.Property(x => x.Value).HasMaxLength(256); e.HasOne(x => x.Route).WithMany(x => x.Steps).HasForeignKey(x => x.RouteId); e.HasIndex(x => new { x.RouteId, x.OrderNo }); }); diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731062407_AddRequestTypeListNameSectionAndStepConditionFields.Designer.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731062407_AddRequestTypeListNameSectionAndStepConditionFields.Designer.cs new file mode 100644 index 0000000..d62004d --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731062407_AddRequestTypeListNameSectionAndStepConditionFields.Designer.cs @@ -0,0 +1,218 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tnsu.ApprovalRoute.Infrastructure.Persistence; + +#nullable disable + +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApprovalRouteDbContext))] + [Migration("20260731062407_AddRequestTypeListNameSectionAndStepConditionFields")] + partial class AddRequestTypeListNameSectionAndStepConditionFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ListName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("list_name"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("PageUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("page_url"); + + b.Property("Section") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("section"); + + b.HasKey("Id") + .HasName("pk_request_types"); + + b.ToTable("request_types", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ByProject") + .HasColumnType("boolean") + .HasColumnName("by_project"); + + b.Property("CompanyId") + .HasColumnType("uuid") + .HasColumnName("company_id"); + + b.Property("DepartmentId") + .HasColumnType("uuid") + .HasColumnName("department_id"); + + b.Property("RequestTypeId") + .HasColumnType("integer") + .HasColumnName("request_type_id"); + + b.Property("RequiresControl") + .HasColumnType("boolean") + .HasColumnName("requires_control"); + + b.Property("RouteType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("route_type"); + + b.HasKey("Id") + .HasName("pk_template_routes"); + + b.HasIndex("RequestTypeId", "DepartmentId", "CompanyId", "ByProject") + .IsUnique() + .HasDatabaseName("ix_template_routes_request_type_id_department_id_company_id_by"); + + b.ToTable("template_routes", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowReassignToOtherApprovers") + .HasColumnType("boolean") + .HasColumnName("allow_reassign_to_other_approvers"); + + b.Property("ExecutorEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("executor_email"); + + b.Property("FieldName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("field_name"); + + b.Property("HeadType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("head_type"); + + b.Property("Operation") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operation"); + + b.Property("OrderNo") + .HasColumnType("integer") + .HasColumnName("order_no"); + + b.Property("Position") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("position"); + + b.Property("ProjectRole") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("project_role"); + + b.Property("RouteId") + .HasColumnType("integer") + .HasColumnName("route_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Value") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_template_route_steps"); + + b.HasIndex("RouteId", "OrderNo") + .HasDatabaseName("ix_template_route_steps_route_id_order_no"); + + b.ToTable("template_route_steps", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.RequestType", "RequestType") + .WithMany("Routes") + .HasForeignKey("RequestTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_template_routes_request_types_request_type_id"); + + b.Navigation("RequestType"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", "Route") + .WithMany("Steps") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_template_route_steps_template_routes_route_id"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestType", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.Navigation("Steps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731062407_AddRequestTypeListNameSectionAndStepConditionFields.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731062407_AddRequestTypeListNameSectionAndStepConditionFields.cs new file mode 100644 index 0000000..f349663 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260731062407_AddRequestTypeListNameSectionAndStepConditionFields.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddRequestTypeListNameSectionAndStepConditionFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "field_name", + table: "template_route_steps", + type: "character varying(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.AddColumn( + name: "operation", + table: "template_route_steps", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "value", + table: "template_route_steps", + type: "character varying(256)", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "list_name", + table: "request_types", + type: "character varying(256)", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "section", + table: "request_types", + type: "character varying(256)", + maxLength: 256, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "field_name", + table: "template_route_steps"); + + migrationBuilder.DropColumn( + name: "operation", + table: "template_route_steps"); + + migrationBuilder.DropColumn( + name: "value", + table: "template_route_steps"); + + migrationBuilder.DropColumn( + name: "list_name", + table: "request_types"); + + migrationBuilder.DropColumn( + name: "section", + table: "request_types"); + } + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs index 5b2cea2..fed778a 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs @@ -31,6 +31,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("ListName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("list_name"); + b.Property("Name") .IsRequired() .HasMaxLength(256) @@ -43,6 +48,12 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations .HasColumnType("character varying(512)") .HasColumnName("page_url"); + b.Property("Section") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("section"); + b.HasKey("Id") .HasName("pk_request_types"); @@ -112,11 +123,21 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations .HasColumnType("character varying(256)") .HasColumnName("executor_email"); + b.Property("FieldName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("field_name"); + b.Property("HeadType") .HasMaxLength(32) .HasColumnType("character varying(32)") .HasColumnName("head_type"); + b.Property("Operation") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operation"); + b.Property("OrderNo") .HasColumnType("integer") .HasColumnName("order_no"); @@ -141,6 +162,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations .HasColumnType("character varying(32)") .HasColumnName("status"); + b.Property("Value") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("value"); + b.HasKey("Id") .HasName("pk_template_route_steps"); diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs index 30ff46f..e46fa47 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs @@ -13,7 +13,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I public async Task CreateAsync_Then_GetAsync_ReturnsCreatedEntity() { // Action - var created = await _service.CreateAsync(new CreateRequestTypeRequest("Vacation", "/vacation"), CancellationToken.None); + var created = await _service.CreateAsync(new CreateRequestTypeRequest("Vacation", "/vacation", null, "General"), CancellationToken.None); // Assert created.IsSuccess.ShouldBeTrue(); @@ -43,11 +43,11 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I public async Task UpdateAsync_ChangesFields() { // Arrange - var created = await _service.CreateAsync(new CreateRequestTypeRequest("Old name", "/old"), CancellationToken.None); + var created = await _service.CreateAsync(new CreateRequestTypeRequest("Old name", "/old", null, "General"), CancellationToken.None); // Action var updated = await _service.UpdateAsync( - created.Value.Id, new UpdateRequestTypeRequest("New name", "/new"), CancellationToken.None); + created.Value.Id, new UpdateRequestTypeRequest("New name", "/new", null, "General"), CancellationToken.None); // Assert updated.IsSuccess.ShouldBeTrue(); @@ -60,7 +60,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I { // Action var result = await _service.UpdateAsync( - 999999, new UpdateRequestTypeRequest("X", "/x"), CancellationToken.None); + 999999, new UpdateRequestTypeRequest("X", "/x", null, "General"), CancellationToken.None); // Assert result.IsFailed.ShouldBeTrue(); @@ -70,11 +70,11 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I public async Task PatchAsync_UpdatesOnlyProvidedFields() { // Arrange - var created = await _service.CreateAsync(new CreateRequestTypeRequest("Original name", "/original"), CancellationToken.None); + var created = await _service.CreateAsync(new CreateRequestTypeRequest("Original name", "/original", null, "General"), CancellationToken.None); // Action var patched = await _service.PatchAsync( - created.Value.Id, new PatchRequestTypeRequest(Name: "Patched name", PageUrl: null), CancellationToken.None); + created.Value.Id, new PatchRequestTypeRequest(Name: "Patched name", PageUrl: null, ListName: null, Section: null), CancellationToken.None); // Assert patched.IsSuccess.ShouldBeTrue(); @@ -87,7 +87,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I { // Action var result = await _service.PatchAsync( - 999999, new PatchRequestTypeRequest(Name: "X", PageUrl: null), CancellationToken.None); + 999999, new PatchRequestTypeRequest(Name: "X", PageUrl: null, ListName: null, Section: null), CancellationToken.None); // Assert result.IsFailed.ShouldBeTrue(); @@ -97,7 +97,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I public async Task DeleteAsync_RemovesEntity() { // Arrange - var created = await _service.CreateAsync(new CreateRequestTypeRequest("Temp", "/temp"), CancellationToken.None); + var created = await _service.CreateAsync(new CreateRequestTypeRequest("Temp", "/temp", null, "General"), CancellationToken.None); // Action var deleted = await _service.DeleteAsync(created.Value.Id, CancellationToken.None); @@ -113,7 +113,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I public async Task ListAsync_IncludesCreatedEntity() { // Arrange - var created = await _service.CreateAsync(new CreateRequestTypeRequest("Listed type", "/listed"), CancellationToken.None); + var created = await _service.CreateAsync(new CreateRequestTypeRequest("Listed type", "/listed", null, "General"), CancellationToken.None); // Action var list = await _service.ListAsync(CancellationToken.None); diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs index 7bf9c42..471ce96 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs @@ -38,7 +38,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture CreateRequestTypeAsync(string name) => - (await _requestTypeService.CreateAsync(new CreateRequestTypeRequest(name, $"/{name}"), CancellationToken.None)).Value.Id; + (await _requestTypeService.CreateAsync(new CreateRequestTypeRequest(name, $"/{name}", null, "General"), CancellationToken.None)).Value.Id; [Fact(DisplayName = "Создание маршрута и получение его по Id возвращает сохранённые данные")] public async Task CreateAsync_ThenGetAsync_ReturnsCreatedEntity() @@ -163,7 +163,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); // Action @@ -196,22 +196,22 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); var requestTypeId = (await requestTypeService.CreateAsync( - new CreateRequestTypeRequest("Collapse duplicates request", "/collapse-duplicates"), CancellationToken.None)).Value.Id; + new CreateRequestTypeRequest("Collapse duplicates request", "/collapse-duplicates", null, "General"), CancellationToken.None)).Value.Id; var route = await routeService.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), CancellationToken.None); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(1, "dup@tnsu.kz", null, null, null, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(1, "dup@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(2, "other@tnsu.kz", null, null, null, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(2, "other@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(3, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(3, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); // Action @@ -240,7 +240,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); // Action @@ -272,13 +272,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); var requestTypeId = (await requestTypeService.CreateAsync( - new CreateRequestTypeRequest("Position request", "/position-request"), CancellationToken.None)).Value.Id; + new CreateRequestTypeRequest("Position request", "/position-request", null, "General"), CancellationToken.None)).Value.Id; var route = await routeService.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), CancellationToken.None); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(1, null, "Lead", null, null, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(1, null, "Lead", null, null, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); // Action @@ -311,13 +311,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); var requestTypeId = (await requestTypeService.CreateAsync( - new CreateRequestTypeRequest("Chief request", "/chief-request"), CancellationToken.None)).Value.Id; + new CreateRequestTypeRequest("Chief request", "/chief-request", null, "General"), CancellationToken.None)).Value.Id; var route = await routeService.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), CancellationToken.None); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); // Action @@ -346,13 +346,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); var requestTypeId = (await requestTypeService.CreateAsync( - new CreateRequestTypeRequest("DirectorTansu request", "/director-tansu-request"), CancellationToken.None)).Value.Id; + new CreateRequestTypeRequest("DirectorTansu request", "/director-tansu-request", null, "General"), CancellationToken.None)).Value.Id; var route = await routeService.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), CancellationToken.None); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); // Action @@ -379,7 +379,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); // Action @@ -407,7 +407,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(1, null, "Несуществующая должность", null, null, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(1, null, "Несуществующая должность", null, null, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); // Action @@ -435,7 +435,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture(); await stepService.CreateAsync( route.Value.Id, - new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.Security, null, RouteStepStatus.Approval, false), + new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.Security, null, RouteStepStatus.Approval, false, null, null, null), CancellationToken.None); // Action diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs index 31daf65..8efbe4a 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs @@ -24,10 +24,10 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture(); } - private async Task CreateRouteAsync(string requestTypeName, bool byProject) + private async Task CreateRouteAsync(string requestTypeName, bool byProject, string? listName = null) { var requestTypeId = (await _requestTypeService.CreateAsync( - new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}"), CancellationToken.None)).Value.Id; + new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}", listName, "General"), CancellationToken.None)).Value.Id; var route = await _routeService.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, null, CompanyId, byProject, RequiresControl: false), @@ -42,7 +42,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture