промежуточный коммит
Tests / frontend-test (pull_request) Successful in 47s
Tests / backend-test (pull_request) Successful in 1m34s

This commit is contained in:
gorbenko.pavel
2026-08-26 17:17:47 +05:00
parent 19a46b0df8
commit 874e41990c
12 changed files with 640 additions and 3 deletions
@@ -0,0 +1,74 @@
using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
namespace Tnsu.ApprovalRoute.Application.Reminders;
internal enum EmploymentStatus
{
/// <summary>Сотрудника с таким ФИО в справочнике нет вовсе — трудоустройство не подтверждено.</summary>
NotFound,
/// <summary>Работает хотя бы в одной компании.</summary>
Employed,
/// <summary>Есть в справочнике, но уволен из всех компаний, где числился.</summary>
Dismissed,
}
/// <summary>
/// Оба справочника api.tnsu.kz разом, разобранные по ФИО: дата увольнения есть
/// только в /Employee/FullBase, а email — только в /Employee/All, поэтому нужны
/// оба. Ищем ПО ФИО, потому что точечного поиска по нему у api.tnsu.kz нет
/// (эндпоинта /Employee/ByFio не существует), а в заявке на приём кандидат
/// записан именно ФИО (колонка CandidateName) — временное решение, см.
/// <see cref="ITnsuPortalClient.GetAllEmployeesAsync"/>.
///
/// ФИО не уникально: один человек может числиться в нескольких компаниях (тогда
/// это несколько записей одного и того же человека), но с тем же успехом могут
/// совпасть ФИО двух разных людей — различить эти два случая по одному ФИО нельзя,
/// поэтому все совпадения трактуются как записи одного человека.
/// </summary>
internal sealed class EmployeeDirectorySnapshot
{
private readonly ILookup<string, EmployeeFullBaseInfo> _fullBaseByFio;
private readonly ILookup<string, EmployeeContactInfo> _contactsByFio;
public EmployeeDirectorySnapshot(
IReadOnlyList<EmployeeFullBaseInfo> fullBase, IReadOnlyList<EmployeeContactInfo> contacts)
{
_fullBaseByFio = fullBase.ToLookup(e => Normalize(e.Fio));
_contactsByFio = contacts.ToLookup(e => Normalize(e.Fio));
}
/// <summary>
/// Уволенным считаем только того, у кого дата увольнения стоит во ВСЕХ его
/// записях: сотрудник, уволенный из одной компании холдинга, но оставшийся в
/// другой, продолжает работать — и напоминание ему по-прежнему нужно.
/// </summary>
public EmploymentStatus EmploymentOf(string fio)
{
var entries = _fullBaseByFio[Normalize(fio)].ToList();
if (entries.Count == 0) return EmploymentStatus.NotFound;
return entries.All(e => e.DismissalDate is not null)
? EmploymentStatus.Dismissed
: EmploymentStatus.Employed;
}
/// <summary>
/// Первый непустой email среди записей этого ФИО. У одного человека записей
/// несколько (по компании на каждую), а email у него один и тот же — но
/// заполнен он может быть не в каждой записи.
/// </summary>
public string? EmailOf(string fio) =>
_contactsByFio[Normalize(fio)]
.Select(e => e.Email)
.FirstOrDefault(email => !string.IsNullOrWhiteSpace(email));
/// <summary>
/// В справочнике и в заявке ФИО набирают руками, поэтому сравниваем без учёта
/// регистра и лишних пробелов между словами (в живых данных встречаются и
/// двойные пробелы, и неразрывный пробел — см. поле department в /Employee/All).
/// </summary>
private static string Normalize(string fio) =>
string.Join(' ', fio.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)).ToLowerInvariant();
}