Files
tnsu-approval-route/backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs
T
gorbenko.pavel c60726759e
Tests / frontend-test (pull_request) Successful in 46s
Tests / backend-test (pull_request) Successful in 5m31s
Дедуп исполнителей резолва — по Guid сотрудника, не по email
Реальная причина бага с дублем в списке согласования: один и тот же
человек может иметь несколько email в справочнике api.tnsu.kz, поэтому
схлопывание по email его не ловило.

EmployeeDirectoryInfo дополнен полем Id (Guid сотрудника из /Employee/
ByEmail). ResolveStepAsync теперь параллельно с email несёт этот Id
(внутренний ResolvedStepRow, наружу не отдаётся), а CollapseByExecutorIdentity
и RemoveCompanyDirectorsDuplicatedByDirectorTansu дедуплицируют по
identity-ключу: Id, если он резолвился, иначе email как фолбэк (на случай,
если сотрудник по итоговому email не нашёлся в справочнике).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 11:30:17 +05:00

110 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Tnsu.ApprovalRoute.Domain.Enums;
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
public sealed class HttpTnsuPortalClient(
HttpClient httpClient,
TnsuPortalAuthorizer authorizer) : ITnsuPortalClient
{
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
public async Task<IReadOnlyList<EmployeeDirectoryInfo>> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken)
{
var employees = await GetAsync<List<EmployeeResponse>>(
$"/Employee/ByEmail?email={Uri.EscapeDataString(email)}", cancellationToken);
if (employees is null)
return [];
return employees
.Select(e => new EmployeeDirectoryInfo(e.CompanyId ?? Guid.Empty, e.DepartmentId, e.Company ?? string.Empty, e.Position, e.Id))
.ToList();
}
public async Task<EmployeeLookupResult> GetEmployeeByPositionAsync(string position, string company, CancellationToken cancellationToken)
{
var employee = await GetAsync<EmployeeResponse>(
$"/Employee/ByPosition?position={Uri.EscapeDataString(position)}&company={Uri.EscapeDataString(company)}", cancellationToken);
return new EmployeeLookupResult(employee is not null, employee?.Email, employee?.Fio);
}
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)
{
var project = await GetAsync<ProjectResponse>($"/Project/{projectId}", cancellationToken);
if (project is null)
return [];
var executors = new List<ProjectExecutorInfo>();
AddIfPresent(executors, ProjectRole.ProjectManager, project.ProjectManager);
AddIfPresent(executors, ProjectRole.Planner, project.Planner);
AddIfPresent(executors, ProjectRole.ManagerHSE, project.ManagerHSE);
AddIfPresent(executors, ProjectRole.Security, project.Security);
AddIfPresent(executors, ProjectRole.Keeper, project.Keeper);
return executors;
}
// Ответ — {"managerEmail": "..."} (та же форма, что у /Employee/Manager),
// а НЕ форма сотрудника (EmployeeResponse) — проверено живым вызовом
// (GET /Employee/DepartmentHead?departmentId=... на отделе "АУП"/"ТОО
// Монтажник-Т" вернул именно managerEmail). 404 с телом {"error": "..."}
// на отделах без руководителя уже отфильтровывается общим GetAsync<T>.
public async Task<string?> GetDepartmentHeadEmailAsync(Guid departmentId, CancellationToken cancellationToken)
{
var manager = await GetAsync<ManagerResponse>(
$"/Employee/DepartmentHead?departmentId={Uri.EscapeDataString(departmentId.ToString())}", cancellationToken);
return manager?.ManagerEmail;
}
private async Task<T?> GetAsync<T>(string requestUri, CancellationToken cancellationToken) where T : class
{
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
await authorizer.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))
executors.Add(new ProjectExecutorInfo(role, email));
}
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("position")] string? Position,
[property: JsonPropertyName("fio")] string? Fio,
[property: JsonPropertyName("id")] Guid? Id = null);
private sealed record ManagerResponse(
[property: JsonPropertyName("managerEmail")] string? ManagerEmail);
private sealed record ProjectResponse(
[property: JsonPropertyName("projectManager")] string? ProjectManager,
[property: JsonPropertyName("planner")] string? Planner,
[property: JsonPropertyName("managerHSE")] string? ManagerHSE,
[property: JsonPropertyName("security")] string? Security,
[property: JsonPropertyName("keeper")] string? Keeper);
}