ReminderDto: разделить на ApprovalOverdueReminderDto и ProbationEndingReminderDto
Абстрактный базовый класс + два sealed-наследника вместо одного DTO с опциональными полями под обе категории — так специфичные для каждого вида напоминания поля не путаются с полями другого вида. Полиморфная сериализация System.Text.Json ([JsonPolymorphic]/[JsonDerivedType]) нужна, иначе список IReadOnlyList<ReminderDto> сериализовался бы только по общим полям базового класса.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
namespace Tnsu.ApprovalRoute.Application.Reminders;
|
||||
|
||||
/// <param name="Email">Согласующий/исполнитель из строки списка — кому напоминать.</param>
|
||||
/// <param name="HoursOverdue">
|
||||
/// На сколько часов просрочен шаг: сейчас минус (активация шага + SLA).
|
||||
/// Выходные не учитываются — считаются календарные часы.
|
||||
/// </param>
|
||||
/// <param name="AskOverdueReason">
|
||||
/// Спрашивать ли у пользователя причину просрочки. Значение уже с применённым
|
||||
/// наследованием (шаг перекрывает тип заявки).
|
||||
/// </param>
|
||||
public sealed record ApprovalOverdueReminderDto(
|
||||
string Email,
|
||||
int HoursOverdue,
|
||||
bool AskOverdueReason,
|
||||
string Url,
|
||||
int RequestTypeId,
|
||||
string RequestTypeName,
|
||||
int RequestId,
|
||||
string Status,
|
||||
string? RequestUniqueCode)
|
||||
: ReminderDto(
|
||||
ReminderCategory.ApprovalOverdue, Url, RequestTypeId, RequestTypeName, RequestId, Status, RequestUniqueCode);
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Tnsu.ApprovalRoute.Application.Reminders;
|
||||
|
||||
/// <param name="ManagerEmail">Email руководителя принимаемого сотрудника — кому напоминать.</param>
|
||||
/// <param name="CandidateEmail">Email принимаемого сотрудника (см. ReminderService.GetProbationEndingAsync).</param>
|
||||
/// <param name="CandidateName">ФИО принимаемого сотрудника (колонка CandidateName списка данных).</param>
|
||||
/// <param name="ProbationPeriod">Текст испытательного срока (колонка ProbationPeriod).</param>
|
||||
/// <param name="HireDate">Дата приёма (колонка HireDate).</param>
|
||||
/// <param name="ProbationEndsAt">Дата окончания испытательного срока (HireDate + срок).</param>
|
||||
/// <param name="DaysUntilProbationEnds">Сколько дней осталось до конца испытательного срока.</param>
|
||||
public sealed record ProbationEndingReminderDto(
|
||||
string ManagerEmail,
|
||||
string CandidateEmail,
|
||||
string CandidateName,
|
||||
string? ProbationPeriod,
|
||||
DateTimeOffset HireDate,
|
||||
DateTimeOffset ProbationEndsAt,
|
||||
int DaysUntilProbationEnds,
|
||||
string Url,
|
||||
int RequestTypeId,
|
||||
string RequestTypeName,
|
||||
int RequestId,
|
||||
string Status,
|
||||
string? RequestUniqueCode)
|
||||
: ReminderDto(
|
||||
ReminderCategory.ProbationEnding, Url, RequestTypeId, RequestTypeName, RequestId, Status, RequestUniqueCode);
|
||||
@@ -1,52 +1,41 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Application.Reminders;
|
||||
|
||||
/// <param name="Email">Согласующий/исполнитель из строки списка — кому напоминать.</param>
|
||||
/// <param name="HoursOverdue">
|
||||
/// На сколько часов просрочен шаг: сейчас минус (активация шага + SLA).
|
||||
/// Выходные не учитываются — считаются календарные часы.
|
||||
/// </param>
|
||||
/// <param name="Url">Ссылка на заявку, требующую решения.</param>
|
||||
/// <param name="AskOverdueReason">
|
||||
/// Спрашивать ли у пользователя причину просрочки. Значение уже с применённым
|
||||
/// наследованием (шаг перекрывает тип заявки).
|
||||
/// </param>
|
||||
/// <param name="Status">Статус просроченного шага (ApprovalStatus строки списка «… — согласование»).</param>
|
||||
/// <param name="Category">Вид напоминания — один на конкретный (sealed) наследник, см. их конструкторы.</param>
|
||||
/// <param name="Url">Ссылка на заявку.</param>
|
||||
/// <param name="RequestTypeId">Id типа заявки (RequestType).</param>
|
||||
/// <param name="RequestTypeName">Название типа заявки — для читаемости в письме/таблице, без похода в БД.</param>
|
||||
/// <param name="RequestId">Id заявки (TicketId строки списка согласования / Id строки списка данных).</param>
|
||||
/// <param name="Status">Статус заявки/шага — из того списка, который читает конкретный вид напоминания.</param>
|
||||
/// <param name="RequestUniqueCode">
|
||||
/// Уникальный код заявки (колонка RequestUniqueCode списка данных) — null, если у
|
||||
/// заявки его нет или строка данных не найдена.
|
||||
/// </param>
|
||||
/// <param name="Category">
|
||||
/// Вид напоминания. Необязательный — параметр добавлен позже остальных, поэтому по
|
||||
/// умолчанию <see cref="ReminderCategory.None"/> («не указана»): старые вызовы
|
||||
/// конструктора и потребители, ничего не знающие о категориях, продолжают работать
|
||||
/// как раньше.
|
||||
/// </param>
|
||||
/// <param name="ManagerEmail">
|
||||
/// Только для <see cref="ReminderCategory.ProbationEnding"/> — email руководителя
|
||||
/// принимаемого сотрудника (см. ReminderService.GetProbationEndingAsync). Для
|
||||
/// остальных категорий — null.
|
||||
/// </param>
|
||||
/// <param name="CandidateEmail">Только для ProbationEnding — email принимаемого сотрудника.</param>
|
||||
/// <param name="CandidateName">Только для ProbationEnding — ФИО принимаемого сотрудника (колонка CandidateName).</param>
|
||||
/// <param name="ProbationPeriod">Только для ProbationEnding — текст испытательного срока (колонка ProbationPeriod).</param>
|
||||
/// <param name="HireDate">Только для ProbationEnding — дата приёма (колонка HireDate).</param>
|
||||
/// <param name="ProbationEndsAt">Только для ProbationEnding — дата окончания испытательного срока (HireDate + срок).</param>
|
||||
/// <param name="DaysUntilProbationEnds">Только для ProbationEnding — сколько дней осталось до конца испытательного срока.</param>
|
||||
public sealed record ReminderDto(
|
||||
string Email,
|
||||
int HoursOverdue,
|
||||
/// <remarks>
|
||||
/// Общая часть двух видов напоминаний — <see cref="ApprovalOverdueReminderDto"/> и
|
||||
/// <see cref="ProbationEndingReminderDto"/>. У них разные поля-триггеры и разные
|
||||
/// адресаты, общее — только то, что описано здесь (к какой заявке относится строка
|
||||
/// и как её открыть).
|
||||
///
|
||||
/// [JsonPolymorphic]/[JsonDerivedType] — вместо того чтобы полагаться на
|
||||
/// compile-time тип элемента (ReminderService всегда работает со списком именно
|
||||
/// этого базового типа, см. IReadOnlyList<ReminderDto>), System.Text.Json без
|
||||
/// этих атрибутов сериализовал бы только поля базового класса, теряя специфичные
|
||||
/// для наследника (Email/HoursOverdue или ManagerEmail/CandidateEmail и т.д.).
|
||||
/// Дискриминатор — отдельное системное поле "$type", а не Category: значение
|
||||
/// Category читаемое (совпадает с именем enum-константы через уже подключенный
|
||||
/// глобально JsonStringEnumConverter, см. Program.cs) и не привязано к деталям
|
||||
/// сериализации.
|
||||
/// </remarks>
|
||||
[JsonPolymorphic]
|
||||
[JsonDerivedType(typeof(ApprovalOverdueReminderDto), nameof(ReminderCategory.ApprovalOverdue))]
|
||||
[JsonDerivedType(typeof(ProbationEndingReminderDto), nameof(ReminderCategory.ProbationEnding))]
|
||||
public abstract record ReminderDto(
|
||||
ReminderCategory Category,
|
||||
string Url,
|
||||
bool AskOverdueReason,
|
||||
int RequestTypeId,
|
||||
string RequestTypeName,
|
||||
int RequestId,
|
||||
string Status,
|
||||
string? RequestUniqueCode,
|
||||
ReminderCategory Category = ReminderCategory.None,
|
||||
string? ManagerEmail = null,
|
||||
string? CandidateEmail = null,
|
||||
string? CandidateName = null,
|
||||
string? ProbationPeriod = null,
|
||||
DateTimeOffset? HireDate = null,
|
||||
DateTimeOffset? ProbationEndsAt = null,
|
||||
int? DaysUntilProbationEnds = null);
|
||||
string? RequestUniqueCode);
|
||||
@@ -203,17 +203,16 @@ public sealed class ReminderService(
|
||||
var deadline = activatedAt.AddHours(slaHours);
|
||||
if (deadline >= now) continue;
|
||||
|
||||
reminders.Add(new ReminderDto(
|
||||
reminders.Add(new ApprovalOverdueReminderDto(
|
||||
row.ApproverEmail!,
|
||||
(int)Math.Floor((now - deadline).TotalHours),
|
||||
BuildUrl(type, row, listId),
|
||||
row.AskOverdueReason ?? type.AskOverdueReason ?? false,
|
||||
BuildUrl(type, row, listId),
|
||||
type.Id,
|
||||
type.Name,
|
||||
row.TicketId,
|
||||
row.Status,
|
||||
uniqueCodes.GetValueOrDefault(row.TicketId),
|
||||
ReminderCategory.ApprovalOverdue));
|
||||
uniqueCodes.GetValueOrDefault(row.TicketId)));
|
||||
}
|
||||
|
||||
return reminders;
|
||||
@@ -417,24 +416,20 @@ public sealed class ReminderService(
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ReminderDto(
|
||||
Email: managerEmail,
|
||||
HoursOverdue: 0,
|
||||
Url: BuildProbationUrl(type, row, listId),
|
||||
AskOverdueReason: false,
|
||||
RequestTypeId: type.Id,
|
||||
RequestTypeName: type.Name,
|
||||
RequestId: row.Id,
|
||||
Status: row.Status,
|
||||
RequestUniqueCode: row.RequestUniqueCode,
|
||||
Category: ReminderCategory.ProbationEnding,
|
||||
return new ProbationEndingReminderDto(
|
||||
ManagerEmail: managerEmail,
|
||||
CandidateEmail: candidateEmail,
|
||||
CandidateName: candidateName,
|
||||
ProbationPeriod: row.ProbationPeriod,
|
||||
HireDate: row.HireDate,
|
||||
HireDate: hireDate,
|
||||
ProbationEndsAt: probationEndsAt,
|
||||
DaysUntilProbationEnds: daysLeft);
|
||||
DaysUntilProbationEnds: daysLeft,
|
||||
Url: BuildProbationUrl(type, row, listId),
|
||||
RequestTypeId: type.Id,
|
||||
RequestTypeName: type.Name,
|
||||
RequestId: row.Id,
|
||||
Status: row.Status,
|
||||
RequestUniqueCode: row.RequestUniqueCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user