Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/6
This commit was merged in pull request #6.
This commit is contained in:
@@ -11,11 +11,14 @@ namespace Tnsu.ApprovalRoute.Api.Controllers;
|
||||
[Produces("application/json")]
|
||||
public sealed class TemplateRoutesController(TemplateRouteService service) : ControllerBase
|
||||
{
|
||||
[HttpGet("resolve")]
|
||||
// POST, а не GET: запрос принимает список условий (field_name/value) переменной длины,
|
||||
// который не ложится на плоскую query-строку без индексной эквилибристики. Единственный
|
||||
// потребитель — диагностический экран «Проверка маршрута» в админке, внешних вызовов нет.
|
||||
[HttpPost("resolve")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ResolveTemplateRouteResultDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Resolve([FromQuery] ResolveTemplateRouteRequest request, CancellationToken ct) =>
|
||||
public async Task<IActionResult> Resolve([FromBody] ResolveTemplateRouteRequest request, CancellationToken ct) =>
|
||||
(await service.ResolveAsync(request, ct)).ToActionResult();
|
||||
|
||||
[HttpGet]
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Postgres": "Host=db-master.tnsu.kz;Database=tnsu_approval_route;Username=dbuser;Password=p}{6{Pa]{JQwP1H"
|
||||
"Postgres": "Host=localhost;Port=5435;Database=tnsu_approval_route;Username=tnsu_approval_route;Password=tnsu_approval_route"
|
||||
},
|
||||
"Entra": {
|
||||
|
||||
"TenantId": "b4c2af35-bfd6-4cff-9af9-556efb53e65a",
|
||||
"Audience": "9232d9e2-9807-4753-808f-07250e5474b0"
|
||||
},
|
||||
|
||||
@@ -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? SiteUrl, string Section);
|
||||
|
||||
public sealed record CreateRequestTypeRequest(string Name, string PageUrl);
|
||||
public sealed record CreateRequestTypeRequest(string Name, string PageUrl, string? ListName, string? SiteUrl, string Section);
|
||||
|
||||
public sealed record UpdateRequestTypeRequest(string Name, string PageUrl);
|
||||
public sealed record UpdateRequestTypeRequest(string Name, string PageUrl, string? ListName, string? SiteUrl, string Section);
|
||||
|
||||
public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl);
|
||||
public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl, string? ListName, string? SiteUrl, string? Section);
|
||||
|
||||
@@ -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.SiteUrl, x.Section))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok<IReadOnlyList<RequestTypeDto>>(items);
|
||||
@@ -29,7 +29,18 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db)
|
||||
|
||||
public async Task<Result<RequestTypeDto>> CreateAsync(CreateRequestTypeRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = new RequestType { Name = request.Name, PageUrl = request.PageUrl };
|
||||
if (!RequestTypeFields.IsListReferenceValid(request.ListName, request.SiteUrl))
|
||||
return Result.Fail<RequestTypeDto>(new ValidationError(
|
||||
"Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."));
|
||||
|
||||
var entity = new RequestType
|
||||
{
|
||||
Name = request.Name,
|
||||
PageUrl = request.PageUrl,
|
||||
ListName = request.ListName,
|
||||
SiteUrl = request.SiteUrl,
|
||||
Section = request.Section
|
||||
};
|
||||
db.RequestTypes.Add(entity);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -42,8 +53,15 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db)
|
||||
if (entity is null)
|
||||
return Result.Fail<RequestTypeDto>(new NotFoundError("Тип заявки", id));
|
||||
|
||||
if (!RequestTypeFields.IsListReferenceValid(request.ListName, request.SiteUrl))
|
||||
return Result.Fail<RequestTypeDto>(new ValidationError(
|
||||
"Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."));
|
||||
|
||||
entity.Name = request.Name;
|
||||
entity.PageUrl = request.PageUrl;
|
||||
entity.ListName = request.ListName;
|
||||
entity.SiteUrl = request.SiteUrl;
|
||||
entity.Section = request.Section;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(ToDto(entity));
|
||||
@@ -55,8 +73,18 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db)
|
||||
if (entity is null)
|
||||
return Result.Fail<RequestTypeDto>(new NotFoundError("Тип заявки", id));
|
||||
|
||||
var listName = request.ListName ?? entity.ListName;
|
||||
var siteUrl = request.SiteUrl ?? entity.SiteUrl;
|
||||
|
||||
if (!RequestTypeFields.IsListReferenceValid(listName, siteUrl))
|
||||
return Result.Fail<RequestTypeDto>(new ValidationError(
|
||||
"Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."));
|
||||
|
||||
entity.Name = request.Name ?? entity.Name;
|
||||
entity.PageUrl = request.PageUrl ?? entity.PageUrl;
|
||||
entity.ListName = listName;
|
||||
entity.SiteUrl = siteUrl;
|
||||
entity.Section = request.Section ?? entity.Section;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(ToDto(entity));
|
||||
@@ -74,5 +102,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.SiteUrl, e.Section);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ public sealed class CreateRequestTypeRequestValidator : AbstractValidator<Create
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().WithMessage("Имя типа заявки обязательно.");
|
||||
RuleFor(x => x.PageUrl).NotEmpty().WithMessage("Url страницы обязателен.");
|
||||
RuleFor(x => x.Section).NotEmpty().WithMessage("Раздел обязателен.");
|
||||
RuleFor(x => x)
|
||||
.Must(x => RequestTypeFields.IsListReferenceValid(x.ListName, x.SiteUrl))
|
||||
.WithMessage("Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +21,10 @@ public sealed class UpdateRequestTypeRequestValidator : AbstractValidator<Update
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().WithMessage("Имя типа заявки обязательно.");
|
||||
RuleFor(x => x.PageUrl).NotEmpty().WithMessage("Url страницы обязателен.");
|
||||
RuleFor(x => x.Section).NotEmpty().WithMessage("Раздел обязателен.");
|
||||
RuleFor(x => x)
|
||||
.Must(x => RequestTypeFields.IsListReferenceValid(x.ListName, x.SiteUrl))
|
||||
.WithMessage("Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,5 +36,13 @@ public sealed class PatchRequestTypeRequestValidator : AbstractValidator<PatchRe
|
||||
RuleFor(x => 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("Раздел не может быть пустым."));
|
||||
}
|
||||
}
|
||||
|
||||
internal static class RequestTypeFields
|
||||
{
|
||||
public static bool IsListReferenceValid(string? listName, string? siteUrl) =>
|
||||
string.IsNullOrWhiteSpace(listName) == string.IsNullOrWhiteSpace(siteUrl);
|
||||
}
|
||||
|
||||
+15
-3
@@ -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);
|
||||
|
||||
+47
-6
@@ -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<IReadOnlyList<TemplateRouteStepDto>>(items);
|
||||
@@ -33,7 +34,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
|
||||
public async Task<Result<TemplateRouteStepDto>> 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<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
|
||||
|
||||
@@ -41,6 +42,14 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
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)
|
||||
@@ -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<Result<TemplateRouteStepDto>> 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<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
|
||||
|
||||
@@ -79,6 +91,14 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
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)
|
||||
@@ -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<Result<TemplateRouteStepDto>> 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<TemplateRouteStepDto>(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<TemplateRouteStepDto>(new ValidationError("Статус обязателен: Approval или Execution."));
|
||||
@@ -125,6 +151,14 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
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)
|
||||
@@ -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<bool> 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);
|
||||
}
|
||||
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -12,12 +12,15 @@ public sealed record ResolvedRouteStepDto(
|
||||
|
||||
public sealed record ResolveTemplateRouteResultDto(
|
||||
int RouteId,
|
||||
RouteType RouteType,
|
||||
bool RequiresControl,
|
||||
IReadOnlyList<ResolvedRouteStepDto> Steps);
|
||||
|
||||
/// <summary>Значение поля списка SharePoint (field_name шага) для проверки условий шагов при resolve.</summary>
|
||||
public sealed record ResolveFieldValueDto(string FieldName, string Value);
|
||||
|
||||
public sealed record ResolveTemplateRouteRequest(
|
||||
int RequestTypeId,
|
||||
string? UserEmail,
|
||||
Guid? ProjectId,
|
||||
RouteStepStatus Status);
|
||||
RouteStepStatus Status,
|
||||
IReadOnlyList<ResolveFieldValueDto>? FieldValues = null);
|
||||
|
||||
@@ -1,33 +1,27 @@
|
||||
using Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
|
||||
|
||||
public sealed record TemplateRouteDto(
|
||||
int Id,
|
||||
RouteType RouteType,
|
||||
int RequestTypeId,
|
||||
Guid? DepartmentId,
|
||||
Guid CompanyId,
|
||||
Guid? CompanyId,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
public sealed record CreateTemplateRouteRequest(
|
||||
RouteType RouteType,
|
||||
int RequestTypeId,
|
||||
Guid? DepartmentId,
|
||||
Guid CompanyId,
|
||||
Guid? CompanyId,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
public sealed record UpdateTemplateRouteRequest(
|
||||
RouteType RouteType,
|
||||
Guid? DepartmentId,
|
||||
Guid CompanyId,
|
||||
Guid? CompanyId,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
public sealed record PatchTemplateRouteRequest(
|
||||
RouteType? RouteType,
|
||||
Guid? DepartmentId,
|
||||
Guid? CompanyId,
|
||||
bool? ByProject,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using FluentResults;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -24,7 +25,7 @@ public sealed class TemplateRouteService(
|
||||
|
||||
var items = await query
|
||||
.Select(x => new TemplateRouteDto(
|
||||
x.Id, x.RouteType, x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject, x.RequiresControl))
|
||||
x.Id, x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject, x.RequiresControl))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok<IReadOnlyList<TemplateRouteDto>>(items);
|
||||
@@ -52,7 +53,6 @@ public sealed class TemplateRouteService(
|
||||
|
||||
var entity = new TemplateRoute
|
||||
{
|
||||
RouteType = r.RouteType,
|
||||
RequestTypeId = r.RequestTypeId,
|
||||
DepartmentId = r.DepartmentId,
|
||||
CompanyId = r.CompanyId,
|
||||
@@ -76,7 +76,6 @@ public sealed class TemplateRouteService(
|
||||
return Result.Fail<TemplateRouteDto>(new ConflictError(
|
||||
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
|
||||
|
||||
entity.RouteType = r.RouteType;
|
||||
entity.DepartmentId = r.DepartmentId;
|
||||
entity.CompanyId = r.CompanyId;
|
||||
entity.ByProject = r.ByProject;
|
||||
@@ -101,7 +100,6 @@ public sealed class TemplateRouteService(
|
||||
return Result.Fail<TemplateRouteDto>(new ConflictError(
|
||||
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
|
||||
|
||||
entity.RouteType = r.RouteType ?? entity.RouteType;
|
||||
entity.DepartmentId = departmentId;
|
||||
entity.CompanyId = companyId;
|
||||
entity.ByProject = byProject;
|
||||
@@ -142,22 +140,11 @@ public sealed class TemplateRouteService(
|
||||
|
||||
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);
|
||||
// Приоритет — маршрут с компанией инициатора; если для этой компании
|
||||
// своего маршрута нет, берём запасной маршрут без указанной компании
|
||||
// (тот же принцип, что уже применяется к отделу внутри FindRouteAsync).
|
||||
var route = await FindRouteAsync(request.RequestTypeId, employee.CompanyId, employee.DepartmentId, byProject, cancellationToken)
|
||||
?? await FindRouteAsync(request.RequestTypeId, null, employee.DepartmentId, byProject, cancellationToken);
|
||||
|
||||
if (route is null)
|
||||
return Result.Fail<ResolveTemplateRouteResultDto>(new NotFoundError(
|
||||
@@ -168,8 +155,14 @@ public sealed class TemplateRouteService(
|
||||
if (request.ProjectId is { } projectId)
|
||||
projectExecutors = await portalClient.GetProjectExecutorsAsync(projectId, cancellationToken);
|
||||
|
||||
var fieldValues = (request.FieldValues ?? [])
|
||||
.Where(f => !string.IsNullOrWhiteSpace(f.FieldName))
|
||||
.GroupBy(f => f.FieldName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.First().Value, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var matchingSteps = route.Steps
|
||||
.Where(s => s.Status == request.Status)
|
||||
.Where(s => MatchesCondition(s, fieldValues))
|
||||
.OrderBy(s => s.OrderNo)
|
||||
.ToList();
|
||||
|
||||
@@ -179,11 +172,36 @@ public sealed class TemplateRouteService(
|
||||
|
||||
var collapsedSteps = CollapseByExecutorEmail(steps);
|
||||
|
||||
return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RouteType, route.RequiresControl, collapsedSteps));
|
||||
return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RequiresControl, collapsedSteps));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Маршрут для компании/отдела инициатора с фолбэком по отделу: сначала точное
|
||||
/// совпадение отдела, если такого маршрута нет — запасной маршрут без отдела
|
||||
/// (для той же компании). Компания при этом фиксирована — фолбэк по ней делает
|
||||
/// вызывающий код (см. ResolveAsync: сначала с companyId сотрудника, затем null).
|
||||
/// </summary>
|
||||
private async Task<TemplateRoute?> FindRouteAsync(
|
||||
int requestTypeId, Guid? companyId, Guid? departmentId, bool byProject, CancellationToken cancellationToken) =>
|
||||
await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == requestTypeId &&
|
||||
x.CompanyId == companyId &&
|
||||
x.ByProject == byProject &&
|
||||
x.DepartmentId == departmentId)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? await db.TemplateRoutes.AsNoTracking()
|
||||
.Include(x => x.Steps)
|
||||
.Where(x =>
|
||||
x.RequestTypeId == requestTypeId &&
|
||||
x.CompanyId == companyId &&
|
||||
x.ByProject == byProject &&
|
||||
x.DepartmentId == null)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
private Task<bool> HasDuplicateAsync(
|
||||
int requestTypeId, Guid? departmentId, Guid companyId, bool byProject, int? excludeId, CancellationToken cancellationToken) =>
|
||||
int requestTypeId, Guid? departmentId, Guid? companyId, bool byProject, int? excludeId, CancellationToken cancellationToken) =>
|
||||
db.TemplateRoutes.AnyAsync(x =>
|
||||
x.Id != excludeId &&
|
||||
x.RequestTypeId == requestTypeId &&
|
||||
@@ -243,6 +261,37 @@ public sealed class TemplateRouteService(
|
||||
return new ResolvedRouteStepDto(step.Id, step.OrderNo, resolvedEmail, resolvedPosition, step.AllowReassignToOtherApprovers, warning);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Условие шага (field_name/operation/value) против значений полей, переданных в resolve-запросе.
|
||||
/// Шаг без условия применяется всегда. Шаг с условием, для которого не передано значение поля
|
||||
/// (fieldValues его не содержит), считается неприменимым — при resolve мы моделируем конкретный
|
||||
/// сценарий, и отсутствие значения означает, что условие не выполняется.
|
||||
/// </summary>
|
||||
private static bool MatchesCondition(TemplateRouteStep step, IReadOnlyDictionary<string, string> fieldValues)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(step.FieldName) || step.Operation is null or ComparisonOperator.None)
|
||||
return true;
|
||||
|
||||
if (!fieldValues.TryGetValue(step.FieldName, out var actual) || string.IsNullOrWhiteSpace(actual))
|
||||
return false;
|
||||
|
||||
var expected = step.Value;
|
||||
return step.Operation switch
|
||||
{
|
||||
ComparisonOperator.Equal =>
|
||||
string.Equals(actual.Trim(), expected?.Trim(), StringComparison.OrdinalIgnoreCase) ||
|
||||
(TryParseNumber(actual, out var eqActual) && TryParseNumber(expected, out var eqExpected) && eqActual == eqExpected),
|
||||
ComparisonOperator.GreaterThan =>
|
||||
TryParseNumber(actual, out var gtActual) && TryParseNumber(expected, out var gtExpected) && gtActual > gtExpected,
|
||||
ComparisonOperator.LessThan =>
|
||||
TryParseNumber(actual, out var ltActual) && TryParseNumber(expected, out var ltExpected) && ltActual < ltExpected,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryParseNumber(string? value, out double number) =>
|
||||
double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out number);
|
||||
|
||||
private static IReadOnlyList<ResolvedRouteStepDto> CollapseByExecutorEmail(IReadOnlyList<ResolvedRouteStepDto> steps)
|
||||
{
|
||||
var seenEmails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -260,5 +309,5 @@ public sealed class TemplateRouteService(
|
||||
}
|
||||
|
||||
private static TemplateRouteDto ToDto(TemplateRoute e) => new(
|
||||
e.Id, e.RouteType, e.RequestTypeId, e.DepartmentId, e.CompanyId, e.ByProject, e.RequiresControl);
|
||||
e.Id, e.RequestTypeId, e.DepartmentId, e.CompanyId, e.ByProject, e.RequiresControl);
|
||||
}
|
||||
|
||||
+9
-24
@@ -3,35 +3,20 @@ using Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
|
||||
|
||||
public sealed class CreateTemplateRouteRequestValidator : AbstractValidator<CreateTemplateRouteRequest>
|
||||
{
|
||||
public CreateTemplateRouteRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.CompanyId).NotEqual(Guid.Empty).WithMessage("Компания обязательна.");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpdateTemplateRouteRequestValidator : AbstractValidator<UpdateTemplateRouteRequest>
|
||||
{
|
||||
public UpdateTemplateRouteRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.CompanyId).NotEqual(Guid.Empty).WithMessage("Компания обязательна.");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PatchTemplateRouteRequestValidator : AbstractValidator<PatchTemplateRouteRequest>
|
||||
{
|
||||
public PatchTemplateRouteRequestValidator()
|
||||
{
|
||||
When(x => x.CompanyId is not null, () =>
|
||||
RuleFor(x => x.CompanyId!.Value).NotEqual(Guid.Empty).WithMessage("Компания обязательна."));
|
||||
}
|
||||
}
|
||||
// Компания и отдел маршрута — оба опциональны, поэтому CreateTemplateRouteRequest/
|
||||
// UpdateTemplateRouteRequest/PatchTemplateRouteRequest не нуждаются в отдельных
|
||||
// валидаторах: маршрут без компании служит запасным вариантом, когда для
|
||||
// конкретной компании инициатора своего маршрута нет (см. ResolveAsync).
|
||||
|
||||
public sealed class ResolveTemplateRouteRequestValidator : AbstractValidator<ResolveTemplateRouteRequest>
|
||||
{
|
||||
public ResolveTemplateRouteRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Status).NotEqual(RouteStepStatus.None).WithMessage("Статус обязателен: Approval или Execution.");
|
||||
RuleForEach(x => x.FieldValues).ChildRules(v =>
|
||||
{
|
||||
v.RuleFor(f => f.FieldName).NotEmpty().WithMessage("Имя поля обязательно.");
|
||||
v.RuleFor(f => f.Value).NotEmpty().WithMessage("Значение поля обязательно.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ 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? SiteUrl { get; set; }
|
||||
public string Section { get; set; } = string.Empty;
|
||||
|
||||
public ICollection<TemplateRoute> Routes { get; set; } = new List<TemplateRoute>();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
using Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Domain.Entities;
|
||||
|
||||
public class TemplateRoute
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public RouteType RouteType { get; set; } = RouteType.Sequential;
|
||||
public int RequestTypeId { get; set; }
|
||||
|
||||
public Guid? DepartmentId { get; set; }
|
||||
public Guid CompanyId { get; set; }
|
||||
public Guid? CompanyId { get; set; }
|
||||
public bool ByProject { get; set; }
|
||||
|
||||
public bool RequiresControl { get; set; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
public enum ComparisonOperator
|
||||
{
|
||||
None,
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
Equal
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
public enum RouteType
|
||||
{
|
||||
None,
|
||||
Parallel,
|
||||
Sequential
|
||||
}
|
||||
@@ -16,12 +16,14 @@ public class ApprovalRouteDbContext(DbContextOptions<ApprovalRouteDbContext> 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.SiteUrl).HasMaxLength(512);
|
||||
e.Property(x => x.Section).HasMaxLength(256).IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<TemplateRoute>(e =>
|
||||
{
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.RouteType).HasConversion<string>().HasMaxLength(32).IsRequired();
|
||||
e.HasOne(x => x.RequestType).WithMany(x => x.Routes).HasForeignKey(x => x.RequestTypeId);
|
||||
e.HasIndex(x => new { x.RequestTypeId, x.DepartmentId, x.CompanyId, x.ByProject }).IsUnique();
|
||||
});
|
||||
@@ -34,6 +36,9 @@ public class ApprovalRouteDbContext(DbContextOptions<ApprovalRouteDbContext> opt
|
||||
e.Property(x => x.ProjectRole).HasConversion<string>().HasMaxLength(32);
|
||||
e.Property(x => x.HeadType).HasConversion<string>().HasMaxLength(32);
|
||||
e.Property(x => x.Status).HasConversion<string>().HasMaxLength(32).IsRequired();
|
||||
e.Property(x => x.FieldName).HasMaxLength(128);
|
||||
e.Property(x => x.Operation).HasConversion<string>().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 });
|
||||
});
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// <auto-generated />
|
||||
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("20260731111928_AddRequestTypeListNameSectionAndStepConditionFields")]
|
||||
partial class AddRequestTypeListNameSectionAndStepConditionFields
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ListName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("list_name");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("PageUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasColumnName("page_url");
|
||||
|
||||
b.Property<string>("Section")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("section");
|
||||
|
||||
b.Property<string>("SiteUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasColumnName("site_url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_request_types");
|
||||
|
||||
b.ToTable("request_types", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("ByProject")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("by_project");
|
||||
|
||||
b.Property<Guid?>("CompanyId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("company_id");
|
||||
|
||||
b.Property<Guid?>("DepartmentId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("department_id");
|
||||
|
||||
b.Property<int>("RequestTypeId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("request_type_id");
|
||||
|
||||
b.Property<bool>("RequiresControl")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("requires_control");
|
||||
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllowReassignToOtherApprovers")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("allow_reassign_to_other_approvers");
|
||||
|
||||
b.Property<string>("ExecutorEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("executor_email");
|
||||
|
||||
b.Property<string>("FieldName")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("field_name");
|
||||
|
||||
b.Property<string>("HeadType")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("head_type");
|
||||
|
||||
b.Property<string>("Operation")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("operation");
|
||||
|
||||
b.Property<int>("OrderNo")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("order_no");
|
||||
|
||||
b.Property<string>("Position")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("position");
|
||||
|
||||
b.Property<string>("ProjectRole")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("project_role");
|
||||
|
||||
b.Property<int>("RouteId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("route_id");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRequestTypeListNameSectionAndStepConditionFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "route_type",
|
||||
table: "template_routes");
|
||||
|
||||
migrationBuilder.AlterColumn<Guid>(
|
||||
name: "company_id",
|
||||
table: "template_routes",
|
||||
type: "uuid",
|
||||
nullable: true,
|
||||
oldClrType: typeof(Guid),
|
||||
oldType: "uuid");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "field_name",
|
||||
table: "template_route_steps",
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "operation",
|
||||
table: "template_route_steps",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "value",
|
||||
table: "template_route_steps",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "list_name",
|
||||
table: "request_types",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "section",
|
||||
table: "request_types",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "site_url",
|
||||
table: "request_types",
|
||||
type: "character varying(512)",
|
||||
maxLength: 512,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "site_url",
|
||||
table: "request_types");
|
||||
|
||||
migrationBuilder.AlterColumn<Guid>(
|
||||
name: "company_id",
|
||||
table: "template_routes",
|
||||
type: "uuid",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
|
||||
oldClrType: typeof(Guid),
|
||||
oldType: "uuid",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "route_type",
|
||||
table: "template_routes",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-7
@@ -31,6 +31,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ListName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("list_name");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
@@ -43,6 +48,17 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasColumnName("page_url");
|
||||
|
||||
b.Property<string>("Section")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("section");
|
||||
|
||||
b.Property<string>("SiteUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasColumnName("site_url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_request_types");
|
||||
|
||||
@@ -62,7 +78,7 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("by_project");
|
||||
|
||||
b.Property<Guid>("CompanyId")
|
||||
b.Property<Guid?>("CompanyId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("company_id");
|
||||
|
||||
@@ -78,12 +94,6 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("requires_control");
|
||||
|
||||
b.Property<string>("RouteType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("route_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_template_routes");
|
||||
|
||||
@@ -112,11 +122,21 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("executor_email");
|
||||
|
||||
b.Property<string>("FieldName")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("field_name");
|
||||
|
||||
b.Property<string>("HeadType")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("head_type");
|
||||
|
||||
b.Property<string>("Operation")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("operation");
|
||||
|
||||
b.Property<int>("OrderNo")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("order_no");
|
||||
@@ -141,6 +161,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("value");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_template_route_steps");
|
||||
|
||||
|
||||
+35
-9
@@ -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, 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, 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, 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, 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, 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, SiteUrl: 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, SiteUrl: 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, null, "General"), CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var deleted = await _service.DeleteAsync(created.Value.Id, CancellationToken.None);
|
||||
@@ -109,11 +109,37 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I
|
||||
fetched.IsFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Создание типа заявки с list_name без site_url возвращает неуспешный результат")]
|
||||
public async Task CreateAsync_ListNameWithoutSiteUrl_ReturnsFailedResult()
|
||||
{
|
||||
// Action
|
||||
var result = await _service.CreateAsync(
|
||||
new CreateRequestTypeRequest("Missing site url", "/missing-site-url", "Requests", null, "General"), CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.IsFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Создание типа заявки с list_name и site_url успешно")]
|
||||
public async Task CreateAsync_ListNameWithSiteUrl_ReturnsCreatedEntity()
|
||||
{
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
new CreateRequestTypeRequest(
|
||||
"With list reference", "/with-list-reference", "Requests", "https://tnsukz.sharepoint.com/sites/hrm", "General"),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
created.IsSuccess.ShouldBeTrue();
|
||||
created.Value.ListName.ShouldBe("Requests");
|
||||
created.Value.SiteUrl.ShouldBe("https://tnsukz.sharepoint.com/sites/hrm");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Список типов заявок включает ранее созданный тип")]
|
||||
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, null, "General"), CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var list = await _service.ListAsync(CancellationToken.None);
|
||||
|
||||
+191
-36
@@ -38,7 +38,12 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
}
|
||||
|
||||
private async Task<int> CreateRequestTypeAsync(string name) =>
|
||||
(await _requestTypeService.CreateAsync(new CreateRequestTypeRequest(name, $"/{name}"), CancellationToken.None)).Value.Id;
|
||||
(await _requestTypeService.CreateAsync(new CreateRequestTypeRequest(name, $"/{name}", null, null, "General"), CancellationToken.None)).Value.Id;
|
||||
|
||||
private async Task<int> CreateRequestTypeWithListAsync(string name) =>
|
||||
(await _requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest(name, $"/{name}", "Requests", "https://tnsukz.sharepoint.com/sites/hrm", "General"),
|
||||
CancellationToken.None)).Value.Id;
|
||||
|
||||
[Fact(DisplayName = "Создание маршрута и получение его по Id возвращает сохранённые данные")]
|
||||
public async Task CreateAsync_ThenGetAsync_ReturnsCreatedEntity()
|
||||
@@ -48,7 +53,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -65,7 +70,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
{
|
||||
// Action
|
||||
var result = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, 999999, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(999999, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -77,7 +82,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Business trip");
|
||||
var request = new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false);
|
||||
var request = new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false);
|
||||
var first = await _service.CreateAsync(request, CancellationToken.None);
|
||||
first.IsSuccess.ShouldBeTrue();
|
||||
|
||||
@@ -94,18 +99,17 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Sick leave");
|
||||
var created = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var updated = await _service.UpdateAsync(
|
||||
created.Value.Id,
|
||||
new UpdateTemplateRouteRequest(RouteType.Parallel, DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
|
||||
new UpdateTemplateRouteRequest(DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
updated.IsSuccess.ShouldBeTrue();
|
||||
updated.Value.RouteType.ShouldBe(RouteType.Parallel);
|
||||
updated.Value.RequiresControl.ShouldBeTrue();
|
||||
}
|
||||
|
||||
@@ -115,18 +119,17 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Patch route request");
|
||||
var created = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteRequest(RouteType.Parallel, DepartmentId: null, CompanyId: null, ByProject: null, RequiresControl: true),
|
||||
new PatchTemplateRouteRequest(DepartmentId: null, CompanyId: null, ByProject: null, RequiresControl: true),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
patched.IsSuccess.ShouldBeTrue();
|
||||
patched.Value.RouteType.ShouldBe(RouteType.Parallel);
|
||||
patched.Value.RequiresControl.ShouldBeTrue();
|
||||
patched.Value.CompanyId.ShouldBe(CompanyId);
|
||||
patched.Value.DepartmentId.ShouldBe(DepartmentId);
|
||||
@@ -138,7 +141,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Remote work");
|
||||
var created = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -157,13 +160,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Exact match request");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
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 +199,22 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("Collapse duplicates request", "/collapse-duplicates"), CancellationToken.None)).Value.Id;
|
||||
new CreateRequestTypeRequest("Collapse duplicates request", "/collapse-duplicates", null, null, "General"), CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(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
|
||||
@@ -228,19 +231,129 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
resolved.Value.Steps[1].OrderNo.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута включает шаг с условием «больше», когда переданное значение поля его удовлетворяет")]
|
||||
public async Task ResolveAsync_ConditionGreaterThan_SatisfiedByFieldValue_IncludesStep()
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeWithListAsync("Condition request satisfied");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await _service.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval,
|
||||
[new ResolveFieldValueDto("Amount", "1500")]),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("big-amount@tnsu.kz");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута исключает шаг с условием, когда значение поля его не удовлетворяет")]
|
||||
public async Task ResolveAsync_ConditionGreaterThan_NotSatisfiedByFieldValue_ExcludesStep()
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeWithListAsync("Condition request not satisfied");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(2, "always@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await _service.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval,
|
||||
[new ResolveFieldValueDto("Amount", "500")]),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("always@tnsu.kz");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута исключает шаг с условием, если значение поля вообще не передано")]
|
||||
public async Task ResolveAsync_ConditionField_MissingFromRequest_ExcludesStep()
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeWithListAsync("Condition request missing value");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action — fieldValues не переданы вовсе
|
||||
var resolved = await _service.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута с условием «равно» сравнивает значения как строки без учёта регистра")]
|
||||
public async Task ResolveAsync_ConditionEqual_MatchesCaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeWithListAsync("Condition request equal");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "urgent@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Priority", ComparisonOperator.Equal, "Urgent"),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await _service.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval,
|
||||
[new ResolveFieldValueDto("Priority", "URGENT")]),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("urgent@tnsu.kz");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута с ролью в проекте подставляет исполнителя из проекта")]
|
||||
public async Task ResolveAsync_ByProjectRole_UsesProjectExecutorEmail()
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Project request");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
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 +385,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("Position request", "/position-request"), CancellationToken.None)).Value.Id;
|
||||
new CreateRequestTypeRequest("Position request", "/position-request", null, null, "General"), CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(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 +424,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("Chief request", "/chief-request"), CancellationToken.None)).Value.Id;
|
||||
new CreateRequestTypeRequest("Chief request", "/chief-request", null, null, "General"), CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(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 +459,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("DirectorTansu request", "/director-tansu-request"), CancellationToken.None)).Value.Id;
|
||||
new CreateRequestTypeRequest("DirectorTansu request", "/director-tansu-request", null, null, "General"), CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(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
|
||||
@@ -373,13 +486,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Chief not found request");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
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
|
||||
@@ -401,13 +514,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Position not found request");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
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
|
||||
@@ -429,13 +542,13 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Project role not found request");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
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
|
||||
@@ -457,7 +570,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Company-wide request");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId: null, CompanyId, ByProject: false, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId: null, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -470,6 +583,48 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
resolved.Value.RouteId.ShouldBe(route.Value.Id);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута без компании инициатора использует запасной маршрут без указанной компании")]
|
||||
public async Task ResolveAsync_NoCompanyRoute_FallsBackToRouteWithoutCompany()
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("No company route request");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId: null, CompanyId: null, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await _service.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Execution),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.RouteId.ShouldBe(route.Value.Id);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута предпочитает маршрут с компанией инициатора маршруту без компании")]
|
||||
public async Task ResolveAsync_RouteWithCompanyExists_PreferredOverRouteWithoutCompany()
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Company priority request");
|
||||
var companyWideRoute = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId: null, CompanyId: null, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
var companySpecificRoute = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId: null, CompanyId, ByProject: false, RequiresControl: true),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await _service.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Execution),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.RouteId.ShouldBe(companySpecificRoute.Value.Id);
|
||||
resolved.Value.RouteId.ShouldNotBe(companyWideRoute.Value.Id);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута с несуществующим типом заявки возвращает неуспешный результат")]
|
||||
public async Task ResolveAsync_UnknownRequestType_ReturnsFailedResult()
|
||||
{
|
||||
|
||||
+95
-22
@@ -24,13 +24,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
_requestTypeService = services.GetRequiredService<RequestTypeService>();
|
||||
}
|
||||
|
||||
private async Task<int> CreateRouteAsync(string requestTypeName, bool byProject)
|
||||
private async Task<int> CreateRouteAsync(string requestTypeName, bool byProject, string? listName = null)
|
||||
{
|
||||
var siteUrl = listName is null ? null : "https://example.sharepoint.com/sites/test";
|
||||
var requestTypeId = (await _requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}"), CancellationToken.None)).Value.Id;
|
||||
new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}", listName, siteUrl, "General"), CancellationToken.None)).Value.Id;
|
||||
|
||||
var route = await _routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, null, CompanyId, byProject, RequiresControl: false),
|
||||
new CreateTemplateRouteRequest(requestTypeId, null, CompanyId, byProject, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
return route.Value.Id;
|
||||
@@ -42,7 +43,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var result = await _service.CreateAsync(
|
||||
999999,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -58,7 +59,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var result = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -70,14 +71,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route with duplicate step", byProject: false);
|
||||
var request = new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, false);
|
||||
var request = new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null);
|
||||
var first = await _service.CreateAsync(routeId, request, CancellationToken.None);
|
||||
first.IsSuccess.ShouldBeTrue();
|
||||
|
||||
// Action
|
||||
var duplicate = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, true),
|
||||
new CreateTemplateRouteStepRequest(2, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, true, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -93,7 +94,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
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);
|
||||
|
||||
// Assert
|
||||
@@ -111,7 +112,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -131,7 +132,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -149,7 +150,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -164,14 +165,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to update step", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var updated = await _service.UpdateAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new UpdateTemplateRouteStepRequest(2, "b@b.kz", "Senior Engineer", null, HeadType.Chief, RouteStepStatus.Execution, true),
|
||||
new UpdateTemplateRouteStepRequest(2, "b@b.kz", "Senior Engineer", null, HeadType.Chief, RouteStepStatus.Execution, true, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -188,14 +189,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to patch step", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: 2, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.Execution, AllowReassignToOtherApprovers: null),
|
||||
new PatchTemplateRouteStepRequest(OrderNo: 2, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.Execution, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -212,14 +213,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to patch step into conflict", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
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
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: "a@b.kz", Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null),
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: "a@b.kz", Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -233,14 +234,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to patch step status none", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.None, AllowReassignToOtherApprovers: null),
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.None, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -254,14 +255,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route with explicit None values on patch", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: ProjectRole.None, HeadType: HeadType.None, Status: null, AllowReassignToOtherApprovers: null),
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: ProjectRole.None, HeadType: HeadType.None, Status: null, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -269,6 +270,78 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
patched.Value.ExecutorEmail.ShouldBe("a@b.kz");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Создание шага с частично заполненными полями условия возвращает неуспешный результат")]
|
||||
public async Task CreateAsync_ConditionFieldsPartiallyFilled_ReturnsFailedResult()
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route with partial condition", byProject: false, listName: "Requests");
|
||||
|
||||
// Action
|
||||
var result = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, "Amount", null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.IsFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Создание шага с условием на типе заявки без list_name возвращает неуспешный результат")]
|
||||
public async Task CreateAsync_ConditionFieldsWithoutListName_ReturnsFailedResult()
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route without list name", byProject: false);
|
||||
|
||||
// Action
|
||||
var result = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.IsFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Создание шага с условием на типе заявки со list_name успешно")]
|
||||
public async Task CreateAsync_ConditionFieldsWithListName_ReturnsCreatedStep()
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route with list name", byProject: false, listName: "Requests");
|
||||
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
created.IsSuccess.ShouldBeTrue();
|
||||
created.Value.FieldName.ShouldBe("Amount");
|
||||
created.Value.Operation.ShouldBe(ComparisonOperator.GreaterThan);
|
||||
created.Value.Value.ShouldBe("1000");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Патч, после которого поля условия заполнены частично, возвращает неуспешный результат")]
|
||||
public async Task PatchAsync_ConditionFieldsPartiallyFilled_ReturnsFailedResult()
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route to patch condition into conflict", byProject: false, listName: "Requests");
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, FieldName: "Amount", Operation: null, Value: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
patched.IsFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Удаление шага маршрута убирает его из базы")]
|
||||
public async Task DeleteAsync_RemovesStep()
|
||||
{
|
||||
@@ -276,7 +349,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to delete step", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
|
||||
Reference in New Issue
Block a user