компания необзятаельна
This commit is contained in:
@@ -4,20 +4,20 @@ public sealed record TemplateRouteDto(
|
||||
int Id,
|
||||
int RequestTypeId,
|
||||
Guid? DepartmentId,
|
||||
Guid CompanyId,
|
||||
Guid? CompanyId,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
public sealed record CreateTemplateRouteRequest(
|
||||
int RequestTypeId,
|
||||
Guid? DepartmentId,
|
||||
Guid CompanyId,
|
||||
Guid? CompanyId,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
public sealed record UpdateTemplateRouteRequest(
|
||||
Guid? DepartmentId,
|
||||
Guid CompanyId,
|
||||
Guid? CompanyId,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
|
||||
@@ -140,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(
|
||||
@@ -186,8 +175,33 @@ public sealed class TemplateRouteService(
|
||||
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 &&
|
||||
|
||||
+4
-24
@@ -3,30 +3,10 @@ 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>
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ public class TemplateRoute
|
||||
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; }
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApprovalRouteDbContext))]
|
||||
[Migration("20260731093159_AddRequestTypeListNameSectionAndStepConditionFields")]
|
||||
[Migration("20260731111928_AddRequestTypeListNameSectionAndStepConditionFields")]
|
||||
partial class AddRequestTypeListNameSectionAndStepConditionFields
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -81,7 +81,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");
|
||||
|
||||
+20
-1
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -14,6 +15,14 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
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",
|
||||
@@ -85,6 +94,16 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
|
||||
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",
|
||||
+1
-1
@@ -78,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");
|
||||
|
||||
|
||||
+42
@@ -583,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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user