diff --git a/backend/Tnsu.ApprovalRoute.Application/Reminders/ApprovalOverdueReminderDto.cs b/backend/Tnsu.ApprovalRoute.Application/Reminders/ApprovalOverdueReminderDto.cs
new file mode 100644
index 0000000..5b738fd
--- /dev/null
+++ b/backend/Tnsu.ApprovalRoute.Application/Reminders/ApprovalOverdueReminderDto.cs
@@ -0,0 +1,23 @@
+namespace Tnsu.ApprovalRoute.Application.Reminders;
+
+/// Согласующий/исполнитель из строки списка — кому напоминать.
+///
+/// На сколько часов просрочен шаг: сейчас минус (активация шага + SLA).
+/// Выходные не учитываются — считаются календарные часы.
+///
+///
+/// Спрашивать ли у пользователя причину просрочки. Значение уже с применённым
+/// наследованием (шаг перекрывает тип заявки).
+///
+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);
diff --git a/backend/Tnsu.ApprovalRoute.Application/Reminders/ProbationEndingReminderDto.cs b/backend/Tnsu.ApprovalRoute.Application/Reminders/ProbationEndingReminderDto.cs
new file mode 100644
index 0000000..d46580f
--- /dev/null
+++ b/backend/Tnsu.ApprovalRoute.Application/Reminders/ProbationEndingReminderDto.cs
@@ -0,0 +1,25 @@
+namespace Tnsu.ApprovalRoute.Application.Reminders;
+
+/// Email руководителя принимаемого сотрудника — кому напоминать.
+/// Email принимаемого сотрудника (см. ReminderService.GetProbationEndingAsync).
+/// ФИО принимаемого сотрудника (колонка CandidateName списка данных).
+/// Текст испытательного срока (колонка ProbationPeriod).
+/// Дата приёма (колонка HireDate).
+/// Дата окончания испытательного срока (HireDate + срок).
+/// Сколько дней осталось до конца испытательного срока.
+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);
diff --git a/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderDto.cs b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderDto.cs
index b4ecc66..b3c5ad2 100644
--- a/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderDto.cs
+++ b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderDto.cs
@@ -1,52 +1,41 @@
+using System.Text.Json.Serialization;
+
namespace Tnsu.ApprovalRoute.Application.Reminders;
-/// Согласующий/исполнитель из строки списка — кому напоминать.
-///
-/// На сколько часов просрочен шаг: сейчас минус (активация шага + SLA).
-/// Выходные не учитываются — считаются календарные часы.
-///
-/// Ссылка на заявку, требующую решения.
-///
-/// Спрашивать ли у пользователя причину просрочки. Значение уже с применённым
-/// наследованием (шаг перекрывает тип заявки).
-///
-/// Статус просроченного шага (ApprovalStatus строки списка «… — согласование»).
+/// Вид напоминания — один на конкретный (sealed) наследник, см. их конструкторы.
+/// Ссылка на заявку.
+/// Id типа заявки (RequestType).
+/// Название типа заявки — для читаемости в письме/таблице, без похода в БД.
+/// Id заявки (TicketId строки списка согласования / Id строки списка данных).
+/// Статус заявки/шага — из того списка, который читает конкретный вид напоминания.
///
/// Уникальный код заявки (колонка RequestUniqueCode списка данных) — null, если у
/// заявки его нет или строка данных не найдена.
///
-///
-/// Вид напоминания. Необязательный — параметр добавлен позже остальных, поэтому по
-/// умолчанию («не указана»): старые вызовы
-/// конструктора и потребители, ничего не знающие о категориях, продолжают работать
-/// как раньше.
-///
-///
-/// Только для — email руководителя
-/// принимаемого сотрудника (см. ReminderService.GetProbationEndingAsync). Для
-/// остальных категорий — null.
-///
-/// Только для ProbationEnding — email принимаемого сотрудника.
-/// Только для ProbationEnding — ФИО принимаемого сотрудника (колонка CandidateName).
-/// Только для ProbationEnding — текст испытательного срока (колонка ProbationPeriod).
-/// Только для ProbationEnding — дата приёма (колонка HireDate).
-/// Только для ProbationEnding — дата окончания испытательного срока (HireDate + срок).
-/// Только для ProbationEnding — сколько дней осталось до конца испытательного срока.
-public sealed record ReminderDto(
- string Email,
- int HoursOverdue,
+///
+/// Общая часть двух видов напоминаний — и
+/// . У них разные поля-триггеры и разные
+/// адресаты, общее — только то, что описано здесь (к какой заявке относится строка
+/// и как её открыть).
+///
+/// [JsonPolymorphic]/[JsonDerivedType] — вместо того чтобы полагаться на
+/// compile-time тип элемента (ReminderService всегда работает со списком именно
+/// этого базового типа, см. IReadOnlyList<ReminderDto>), System.Text.Json без
+/// этих атрибутов сериализовал бы только поля базового класса, теряя специфичные
+/// для наследника (Email/HoursOverdue или ManagerEmail/CandidateEmail и т.д.).
+/// Дискриминатор — отдельное системное поле "$type", а не Category: значение
+/// Category читаемое (совпадает с именем enum-константы через уже подключенный
+/// глобально JsonStringEnumConverter, см. Program.cs) и не привязано к деталям
+/// сериализации.
+///
+[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);
\ No newline at end of file
diff --git a/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs
index 032f864..5cfcf3d 100644
--- a/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs
+++ b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs
@@ -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);
}
///
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs
index 683d268..b9ac9f3 100644
--- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs
+++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs
@@ -138,7 +138,7 @@ public sealed class ReminderServiceTests : IClassFixture();
reminder.Email.ShouldBe("approver@tnsu.kz");
reminder.HoursOverdue.ShouldBe(24);
reminder.RequestId.ShouldBe(1);
@@ -173,7 +173,7 @@ public sealed class ReminderServiceTests : IClassFixture().HoursOverdue.ShouldBe(2);
}
[Fact(DisplayName = "Активная строка без даты (Date пуста) — пропускается, а не падает")]
@@ -199,7 +199,7 @@ public sealed class ReminderServiceTests : IClassFixture().HoursOverdue.ShouldBe(5);
}
// SharePoint отдаёт Number-колонки ДРОБНЫМИ («1.0», а не «1»), и на этом
@@ -219,7 +219,7 @@ public sealed class ReminderServiceTests : IClassFixture().HoursOverdue.ShouldBe(5);
}
[Fact(DisplayName = "Флаг опроса причины наследуется от типа заявки, если в строке не задан")]
@@ -231,7 +231,7 @@ public sealed class ReminderServiceTests : IClassFixture().AskOverdueReason.ShouldBeTrue();
}
// Портал — одностраничный и живёт в корне сайта; вид ссылки подтверждён
@@ -276,7 +276,7 @@ public sealed class ReminderServiceTests : IClassFixture().Email.ShouldBe("approver@tnsu.kz");
}
private static object HireDataRow(
@@ -355,11 +355,10 @@ public sealed class ReminderServiceTests : IClassFixture();
reminder.Category.ShouldBe(ReminderCategory.ProbationEnding);
reminder.ManagerEmail.ShouldBe("manager@tnsu.kz");
reminder.CandidateEmail.ShouldBe("candidate@tnsu.kz");
- reminder.Email.ShouldBe("manager@tnsu.kz");
reminder.CandidateName.ShouldBe("Иванов Иван Иванович");
reminder.DaysUntilProbationEnds.ShouldBe(3);
}
@@ -407,7 +406,7 @@ public sealed class ReminderServiceTests : IClassFixture().CandidateEmail.ShouldBe("romanov@tnsu.kz");
}
// ФИО в заявке набирают руками — лишние пробелы и другой регистр не должны
@@ -423,7 +422,7 @@ public sealed class ReminderServiceTests : IClassFixture().CandidateEmail.ShouldBe("volkov@tnsu.kz");
}
[Fact(DisplayName = "Кандидат не найден в /Employee/FullBase — напоминания нет")]