логика готова
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"TenantId": "",
|
||||
"ClientId": "",
|
||||
"ClientSecret": "",
|
||||
"Scope": ""
|
||||
"Scope": "",
|
||||
"DirectorTansuEmail": "director.tansu@tnsu.kz"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ public sealed record ResolvedRouteStepDto(
|
||||
int OrderNo,
|
||||
string? ExecutorEmail,
|
||||
string? Position,
|
||||
ProjectRole? ProjectRole,
|
||||
HeadType? HeadType,
|
||||
RouteStepStatus Status,
|
||||
bool AllowReassignToOtherApprovers);
|
||||
|
||||
public sealed record ResolveTemplateRouteResultDto(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentResults;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
||||
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||
@@ -12,7 +13,8 @@ namespace Tnsu.ApprovalRoute.Application.TemplateRoutes;
|
||||
public sealed class TemplateRouteService(
|
||||
ApprovalRouteDbContext db,
|
||||
ITnsuPortalClient portalClient,
|
||||
ICurrentUser currentUser)
|
||||
ICurrentUser currentUser,
|
||||
IOptions<TnsuPortalOptions> portalOptions)
|
||||
{
|
||||
public async Task<Result<IReadOnlyList<TemplateRouteDto>>> ListAsync(int? requestTypeId, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -166,12 +168,15 @@ public sealed class TemplateRouteService(
|
||||
if (request.ProjectId is { } projectId)
|
||||
projectExecutors = await portalClient.GetProjectExecutorsAsync(projectId, cancellationToken);
|
||||
|
||||
var steps = route.Steps
|
||||
var matchingSteps = route.Steps
|
||||
.Where(s => s.Status == request.Status)
|
||||
.OrderBy(s => s.OrderNo)
|
||||
.Select(s => ResolveStep(s, projectExecutors))
|
||||
.ToList();
|
||||
|
||||
var steps = new List<ResolvedRouteStepDto>();
|
||||
foreach (var step in matchingSteps)
|
||||
steps.Add(await ResolveStepAsync(step, email, employee.Company, projectExecutors, cancellationToken));
|
||||
|
||||
return Result.Ok(new ResolveTemplateRouteResultDto(route.Id, route.RouteType, route.RequiresControl, steps));
|
||||
}
|
||||
|
||||
@@ -185,20 +190,37 @@ public sealed class TemplateRouteService(
|
||||
x.ByProject == byProject,
|
||||
cancellationToken);
|
||||
|
||||
private static ResolvedRouteStepDto ResolveStep(TemplateRouteStep step, IReadOnlyList<ProjectExecutorInfo> projectExecutors)
|
||||
private async Task<ResolvedRouteStepDto> ResolveStepAsync(
|
||||
TemplateRouteStep step, string initiatorEmail, string initiatorCompany,
|
||||
IReadOnlyList<ProjectExecutorInfo> projectExecutors, CancellationToken cancellationToken)
|
||||
{
|
||||
if (step.ProjectRole is not (null or ProjectRole.None))
|
||||
string? resolvedEmail;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(step.ExecutorEmail))
|
||||
resolvedEmail = step.ExecutorEmail;
|
||||
else if (!string.IsNullOrWhiteSpace(step.Position))
|
||||
resolvedEmail = await portalClient.GetEmployeeByPositionAsync(step.Position, initiatorCompany, cancellationToken);
|
||||
else if (step.ProjectRole is not (null or ProjectRole.None))
|
||||
resolvedEmail = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole)?.Email;
|
||||
else if (step.HeadType is not (null or HeadType.None))
|
||||
resolvedEmail = step.HeadType switch
|
||||
{
|
||||
HeadType.Chief => await portalClient.GetManagerAsync(initiatorEmail, "chief", cancellationToken),
|
||||
HeadType.DirectionHead => await portalClient.GetManagerAsync(initiatorEmail, "directionHead", cancellationToken),
|
||||
HeadType.DirectorTansu => portalOptions.Value.DirectorTansuEmail,
|
||||
_ => null
|
||||
};
|
||||
else
|
||||
resolvedEmail = null;
|
||||
|
||||
string? resolvedPosition = null;
|
||||
if (!string.IsNullOrWhiteSpace(resolvedEmail))
|
||||
{
|
||||
var projectExecutor = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole);
|
||||
if (projectExecutor is not null)
|
||||
return new ResolvedRouteStepDto(
|
||||
step.Id, step.OrderNo, projectExecutor.Email, step.Position,
|
||||
step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers);
|
||||
var resolvedEmployee = await portalClient.GetEmployeeByEmailAsync(resolvedEmail, cancellationToken);
|
||||
resolvedPosition = resolvedEmployee?.Position;
|
||||
}
|
||||
|
||||
return new ResolvedRouteStepDto(
|
||||
step.Id, step.OrderNo, step.ExecutorEmail, step.Position,
|
||||
step.ProjectRole, step.HeadType, step.Status, step.AllowReassignToOtherApprovers);
|
||||
return new ResolvedRouteStepDto(step.Id, step.OrderNo, resolvedEmail, resolvedPosition, step.AllowReassignToOtherApprovers);
|
||||
}
|
||||
|
||||
private static TemplateRouteDto ToDto(TemplateRoute e) => new(
|
||||
|
||||
@@ -17,32 +17,33 @@ public sealed class HttpTnsuPortalClient(
|
||||
|
||||
public async Task<EmployeeDirectoryInfo?> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"/Employee/ByEmail?email={Uri.EscapeDataString(email)}");
|
||||
await AuthorizeAsync(request, cancellationToken);
|
||||
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var employee = await response.Content.ReadFromJsonAsync<EmployeeResponse>(JsonOptions, cancellationToken);
|
||||
var employee = await GetAsync<EmployeeResponse>(
|
||||
$"/Employee/ByEmail?email={Uri.EscapeDataString(email)}", cancellationToken);
|
||||
if (employee is null)
|
||||
return null;
|
||||
|
||||
return new EmployeeDirectoryInfo(employee.CompanyId ?? Guid.Empty, employee.DepartmentId);
|
||||
return new EmployeeDirectoryInfo(employee.CompanyId ?? Guid.Empty, employee.DepartmentId, employee.Company ?? string.Empty, employee.Position);
|
||||
}
|
||||
|
||||
public async Task<string?> GetEmployeeByPositionAsync(string position, string company, CancellationToken cancellationToken)
|
||||
{
|
||||
var employee = await GetAsync<EmployeeResponse>(
|
||||
$"/Employee/ByPosition?position={Uri.EscapeDataString(position)}&company={Uri.EscapeDataString(company)}", cancellationToken);
|
||||
|
||||
return employee?.Email;
|
||||
}
|
||||
|
||||
public async Task<string?> GetManagerAsync(string email, string managerType, CancellationToken cancellationToken)
|
||||
{
|
||||
var manager = await GetAsync<ManagerResponse>(
|
||||
$"/Employee/Manager?email={Uri.EscapeDataString(email)}&managerType={Uri.EscapeDataString(managerType)}", cancellationToken);
|
||||
|
||||
return manager?.ManagerEmail;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"/Project/{projectId}");
|
||||
await AuthorizeAsync(request, cancellationToken);
|
||||
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return [];
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var project = await response.Content.ReadFromJsonAsync<ProjectResponse>(JsonOptions, cancellationToken);
|
||||
var project = await GetAsync<ProjectResponse>($"/Project/{projectId}", cancellationToken);
|
||||
if (project is null)
|
||||
return [];
|
||||
|
||||
@@ -55,6 +56,19 @@ public sealed class HttpTnsuPortalClient(
|
||||
return executors;
|
||||
}
|
||||
|
||||
private async Task<T?> GetAsync<T>(string requestUri, CancellationToken cancellationToken) where T : class
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
await AuthorizeAsync(request, cancellationToken);
|
||||
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<T>(JsonOptions, cancellationToken);
|
||||
}
|
||||
|
||||
private static void AddIfPresent(List<ProjectExecutorInfo> executors, ProjectRole role, string? email)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
@@ -75,8 +89,14 @@ public sealed class HttpTnsuPortalClient(
|
||||
}
|
||||
|
||||
private sealed record EmployeeResponse(
|
||||
[property: JsonPropertyName("email")] string? Email,
|
||||
[property: JsonPropertyName("company")] string? Company,
|
||||
[property: JsonPropertyName("companyId")] Guid? CompanyId,
|
||||
[property: JsonPropertyName("departmentId")] Guid? DepartmentId);
|
||||
[property: JsonPropertyName("departmentId")] Guid? DepartmentId,
|
||||
[property: JsonPropertyName("position")] string? Position);
|
||||
|
||||
private sealed record ManagerResponse(
|
||||
[property: JsonPropertyName("managerEmail")] string? ManagerEmail);
|
||||
|
||||
private sealed record ProjectResponse(
|
||||
[property: JsonPropertyName("projectManager")] string? ProjectManager,
|
||||
|
||||
@@ -4,5 +4,9 @@ public interface ITnsuPortalClient
|
||||
{
|
||||
Task<EmployeeDirectoryInfo?> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken);
|
||||
|
||||
Task<string?> GetEmployeeByPositionAsync(string position, string company, CancellationToken cancellationToken);
|
||||
|
||||
Task<string?> GetManagerAsync(string email, string managerType, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ using Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
|
||||
public sealed record EmployeeDirectoryInfo(Guid CompanyId, Guid? DepartmentId);
|
||||
public sealed record EmployeeDirectoryInfo(Guid CompanyId, Guid? DepartmentId, string Company, string? Position);
|
||||
|
||||
public sealed record ProjectExecutorInfo(ProjectRole ProjectRole, string Email);
|
||||
|
||||
@@ -7,4 +7,5 @@ public sealed class TnsuPortalOptions
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
public string ClientSecret { get; set; } = string.Empty;
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
public string DirectorTansuEmail { get; set; } = "director.tansu@tnsu.kz";
|
||||
}
|
||||
|
||||
+120
-4
@@ -15,21 +15,24 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
private static readonly Guid CompanyId = Guid.Parse("00000000-0000-0000-0000-000000000002");
|
||||
private static readonly Guid DepartmentId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||
|
||||
private readonly string _connectionString;
|
||||
private readonly ServiceProvider _services;
|
||||
private readonly TemplateRouteService _service;
|
||||
private readonly RequestTypeService _requestTypeService;
|
||||
|
||||
public TemplateRouteServiceTests(PostgresDatabaseFixture fixture)
|
||||
{
|
||||
_connectionString = fixture.ConnectionString;
|
||||
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId));
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность"));
|
||||
portalClient
|
||||
.Setup(x => x.GetProjectExecutorsAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([new ProjectExecutorInfo(ProjectRole.ProjectManager, "pm@tnsu.kz")]);
|
||||
|
||||
_services = TestServiceProviderFactory.Build(fixture.ConnectionString, portalClient: portalClient.Object);
|
||||
_services = TestServiceProviderFactory.Build(_connectionString, portalClient: portalClient.Object);
|
||||
_service = _services.GetRequiredService<TemplateRouteService>();
|
||||
_requestTypeService = _services.GetRequiredService<RequestTypeService>();
|
||||
}
|
||||
@@ -160,7 +163,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", "Lead", null, HeadType.Chief, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -187,7 +190,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "", "Lead", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -201,6 +204,119 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("pm@tnsu.kz");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв шага с должностью подставляет email сотрудника, найденного по должности и компании инициатора")]
|
||||
public async Task ResolveAsync_ByPosition_UsesEmployeeFoundByPositionAndCompany()
|
||||
{
|
||||
// Arrange
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Lead Engineer"));
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByPositionAsync("Lead", "ТОО TANSU Construction", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("lead@tnsu.kz");
|
||||
|
||||
var services = TestServiceProviderFactory.Build(_connectionString, portalClient: portalClient.Object);
|
||||
var routeService = services.GetRequiredService<TemplateRouteService>();
|
||||
var requestTypeService = services.GetRequiredService<RequestTypeService>();
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("Position request", "/position-request"), CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, "Lead", null, null, RouteStepStatus.Approval, false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await routeService.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("lead@tnsu.kz");
|
||||
resolved.Value.Steps[0].Position.ShouldBe("Lead Engineer");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв шага с типом руководителя Chief подставляет email руководителя из api.tnsu.kz")]
|
||||
public async Task ResolveAsync_ByHeadTypeChief_UsesManagerEmail()
|
||||
{
|
||||
// Arrange
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Chief position"));
|
||||
portalClient
|
||||
.Setup(x => x.GetManagerAsync("someone@tnsu.kz", "chief", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("chief@tnsu.kz");
|
||||
|
||||
var services = TestServiceProviderFactory.Build(_connectionString, portalClient: portalClient.Object);
|
||||
var routeService = services.GetRequiredService<TemplateRouteService>();
|
||||
var requestTypeService = services.GetRequiredService<RequestTypeService>();
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("Chief request", "/chief-request"), CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await routeService.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("chief@tnsu.kz");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв шага с типом руководителя DirectorTansu подставляет захардкоженный email из настроек")]
|
||||
public async Task ResolveAsync_ByHeadTypeDirectorTansu_UsesConfiguredEmail()
|
||||
{
|
||||
// Arrange
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Director position"));
|
||||
|
||||
var services = TestServiceProviderFactory.Build(_connectionString, portalClient: portalClient.Object);
|
||||
var routeService = services.GetRequiredService<TemplateRouteService>();
|
||||
var requestTypeService = services.GetRequiredService<RequestTypeService>();
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("DirectorTansu request", "/director-tansu-request"), CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await routeService.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("director.tansu@tnsu.kz");
|
||||
portalClient.Verify(x => x.GetManagerAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута откатывается на маршрут без отдела, если точного совпадения по отделу нет")]
|
||||
public async Task ResolveAsync_NoDepartmentRoute_FallsBackToCompanyWideRoute()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user