From 738e1671c6894d8d76237a6a1b1c3b765c734414 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Thu, 27 Aug 2026 16:02:50 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=B2=D1=80=D0=B5=D0=BC=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Reminders/ReminderService.cs | 24 ++++++++++++++++--- .../ReminderServiceTests.cs | 24 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs index 45cac5b..f38ffd4 100644 --- a/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/Reminders/ReminderService.cs @@ -312,6 +312,19 @@ public sealed class ReminderService( return result; } + /// + /// Часовой пояс, в котором считаются КАЛЕНДАРНЫЕ даты испытательного срока. + /// + /// Нужен потому, что портал пишет дату приёма как местную полночь, переведённую + /// в UTC: в списке она лежит как 19:00Z ПРЕДЫДУЩЕГО дня. Сравнивая по UTC-датам, + /// сервис видел бы дату приёма на сутки раньше той, что видит кадровик в + /// портале, и напоминание уходило бы на день раньше срока. + /// + /// Константой, а не настройкой: смещение фиксировано для всей организации и не + /// меняется от окружения (перехода на летнее время нет). + /// + private static readonly TimeSpan LocalOffset = TimeSpan.FromHours(6); + private static readonly IReadOnlyDictionary ProbationMonthsByOption = new Dictionary { @@ -424,8 +437,11 @@ public sealed class ReminderService( if (!ProbationMonthsByOption.TryGetValue(row.ProbationPeriod ?? string.Empty, out var months)) return null; // "Без испытательного срока", пусто или значение вне справочника — отсчитывать нечего. - var probationEndsAt = hireDate.AddMonths(months); - var daysLeft = (probationEndsAt.Date - now.Date).Days; + // Обе даты — в местном поясе (см. LocalOffset): и дату приёма, и «сегодня» + // надо брать по тому же календарю, который видит человек в портале. + var hireDateLocal = hireDate.ToOffset(LocalOffset); + var probationEndsAt = hireDateLocal.AddMonths(months); + var daysLeft = (probationEndsAt.Date - now.ToOffset(LocalOffset).Date).Days; if (daysLeft != options.Value.ProbationReminderDaysBeforeEnd) return null; var candidateName = row.CandidateName.Trim(); @@ -470,7 +486,9 @@ public sealed class ReminderService( CandidateEmail: candidateEmail, CandidateName: candidateName, ProbationPeriod: row.ProbationPeriod, - HireDate: hireDate, + // Тоже в местном поясе — иначе в письме дата приёма была бы на сутки + // раньше той, от которой посчитан ProbationEndsAt рядом. + HireDate: hireDateLocal, ProbationEndsAt: probationEndsAt, DaysUntilProbationEnds: daysLeft, // Не ссылка на саму заявку на приём (она уже закрыта, решать по ней diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs index 6787ab6..2f663a2 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/ReminderServiceTests.cs @@ -522,6 +522,30 @@ public sealed class ReminderServiceTests : IClassFixture(); + reminder.DaysUntilProbationEnds.ShouldBe(3); + // И дата приёма, и конец срока отдаются в том же местном календаре. + reminder.HireDate.Date.ShouldBe(new DateTime(2026, 6, 15)); + reminder.ProbationEndsAt.Date.ShouldBe(new DateTime(2026, 8, 15)); + } + // Испытательный срок не бывает длиннее 6 месяцев (см. ProbationMonthsByOption // в ReminderService), поэтому заявка, способная дать напоминание сегодня, // физически не может лежать раньше предпоследней партиции. Три партиции: