Files
tnsu-approval-route/backend/Tnsu.ApprovalRoute.Application/Reminders/EmployeeDirectorySnapshot.cs
T
gorbenko.pavel 31a0a8e9e3 Напоминания: убрать дубль ManagerEmail, ссылка из настроек; None первым во всех enum
Напоминания:
- ProbationEndingReminderDto: ManagerEmail убран — это был унаследованный
  ReceiverEmail под вторым именем.
- Ссылка для ProbationEnding больше не ведёт на саму заявку на приём (она уже
  закрыта, решать по ней нечего) — берётся из новой настройки
  RemindersOptions.ProbationReminderUrl и ведёт в раздел, где заводят новую
  заявку. Подстановок в ней нет: адрес конкретной формы в портал не передать,
  rptype без rpid/rpsource игнорируется (см. parseRoute в request-portal).
  Заодно ушли BuildProbationUrl и ненужные больше параметры type/listId.

Стайлгайд (CLAUDE.md) — у enum первое значение None:
- ReminderFeed: None=0 трактуется как ApprovalOverdue, чтобы вызов без явного
  выбора работал как раньше.
- EmploymentStatus: заодно инвертирован switch в ReminderService — раньше
  «продолжить» было веткой по умолчанию, и новый None молча означал бы
  «сотрудник работает». Теперь по умолчанию пропуск с предупреждением.
- ConditionsLogic: None — НЕвалидное значение, отвергается валидаторами шага
  (в том числе новым PatchTemplateRouteStepRequestValidator, его не было вовсе).
  Поэтому у ConditionsLogic в Create/Update-запросах убрано значение по
  умолчанию: System.Text.Json в .NET 10 уважает дефолты параметров
  конструктора, и пропущенное поле молча означало «И». Теперь пропущенное поле
  даёт None и упирается в валидатор.

ЛОМАЮЩЕЕ ИЗМЕНЕНИЕ КОНТРАКТА: клиент, не присылающий conditionsLogic в
POST/PUT шага, получит 400 вместо прежнего молчаливого «И». Админка не
затронута — она это поле всегда отправляет.
2026-08-27 15:53:31 +05:00

96 lines
5.7 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 Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
namespace Tnsu.ApprovalRoute.Application.Reminders;
internal enum EmploymentStatus
{
/// <summary>
/// Статус не определяли. EmploymentOf такого не возвращает — значение существует
/// только как значение по умолчанию (default(EmploymentStatus)); трактуется
/// наравне с <see cref="NotFound"/>, то есть «трудоустройство не подтверждено».
/// </summary>
None = 0,
/// <summary>Сотрудника с таким ФИО в справочнике нет вовсе — трудоустройство не подтверждено.</summary>
NotFound = 1,
/// <summary>Работает хотя бы в одной компании.</summary>
Employed = 2,
/// <summary>Есть в справочнике, но уволен из всех компаний, где числился.</summary>
Dismissed = 3,
}
/// <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;
private readonly IReadOnlyDictionary<string, string> _fioByEmail;
public EmployeeDirectorySnapshot(
IReadOnlyList<EmployeeFullBaseInfo> fullBase, IReadOnlyList<EmployeeContactInfo> contacts)
{
_fullBaseByFio = fullBase.ToLookup(e => Normalize(e.Fio));
_contactsByFio = contacts.ToLookup(e => Normalize(e.Fio));
// Обратный индекс email → ФИО — нужен, чтобы подписать напоминание именем
// получателя, когда известен только его email (руководитель кандидата,
// см. GetManagerAsync). При дублях email в справочнике побеждает первая
// запись — этого достаточно для отображения имени, не для идентификации.
_fioByEmail = contacts
.Where(e => !string.IsNullOrWhiteSpace(e.Email))
.GroupBy(e => e.Email!.Trim().ToLowerInvariant())
.ToDictionary(g => g.Key, g => g.First().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>ФИО по email — для ReceiverName, когда известен только email получателя (см. EmailOf).</summary>
public string? NameByEmail(string email) =>
_fioByEmail.GetValueOrDefault(email.Trim().ToLowerInvariant());
/// <summary>
/// В справочнике и в заявке ФИО набирают руками, поэтому сравниваем без учёта
/// регистра и лишних пробелов между словами (в живых данных встречаются и
/// двойные пробелы, и неразрывный пробел — см. поле department в /Employee/All).
/// </summary>
private static string Normalize(string fio) =>
string.Join(' ', fio.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)).ToLowerInvariant();
}