готово под локальное время
This commit is contained in:
@@ -312,6 +312,19 @@ public sealed class ReminderService(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Часовой пояс, в котором считаются КАЛЕНДАРНЫЕ даты испытательного срока.
|
||||||
|
///
|
||||||
|
/// Нужен потому, что портал пишет дату приёма как местную полночь, переведённую
|
||||||
|
/// в UTC: в списке она лежит как 19:00Z ПРЕДЫДУЩЕГО дня. Сравнивая по UTC-датам,
|
||||||
|
/// сервис видел бы дату приёма на сутки раньше той, что видит кадровик в
|
||||||
|
/// портале, и напоминание уходило бы на день раньше срока.
|
||||||
|
///
|
||||||
|
/// Константой, а не настройкой: смещение фиксировано для всей организации и не
|
||||||
|
/// меняется от окружения (перехода на летнее время нет).
|
||||||
|
/// </summary>
|
||||||
|
private static readonly TimeSpan LocalOffset = TimeSpan.FromHours(6);
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<string, int> ProbationMonthsByOption =
|
private static readonly IReadOnlyDictionary<string, int> ProbationMonthsByOption =
|
||||||
new Dictionary<string, int>
|
new Dictionary<string, int>
|
||||||
{
|
{
|
||||||
@@ -424,8 +437,11 @@ public sealed class ReminderService(
|
|||||||
if (!ProbationMonthsByOption.TryGetValue(row.ProbationPeriod ?? string.Empty, out var months))
|
if (!ProbationMonthsByOption.TryGetValue(row.ProbationPeriod ?? string.Empty, out var months))
|
||||||
return null; // "Без испытательного срока", пусто или значение вне справочника — отсчитывать нечего.
|
return null; // "Без испытательного срока", пусто или значение вне справочника — отсчитывать нечего.
|
||||||
|
|
||||||
var probationEndsAt = hireDate.AddMonths(months);
|
// Обе даты — в местном поясе (см. LocalOffset): и дату приёма, и «сегодня»
|
||||||
var daysLeft = (probationEndsAt.Date - now.Date).Days;
|
// надо брать по тому же календарю, который видит человек в портале.
|
||||||
|
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;
|
if (daysLeft != options.Value.ProbationReminderDaysBeforeEnd) return null;
|
||||||
|
|
||||||
var candidateName = row.CandidateName.Trim();
|
var candidateName = row.CandidateName.Trim();
|
||||||
@@ -470,7 +486,9 @@ public sealed class ReminderService(
|
|||||||
CandidateEmail: candidateEmail,
|
CandidateEmail: candidateEmail,
|
||||||
CandidateName: candidateName,
|
CandidateName: candidateName,
|
||||||
ProbationPeriod: row.ProbationPeriod,
|
ProbationPeriod: row.ProbationPeriod,
|
||||||
HireDate: hireDate,
|
// Тоже в местном поясе — иначе в письме дата приёма была бы на сутки
|
||||||
|
// раньше той, от которой посчитан ProbationEndsAt рядом.
|
||||||
|
HireDate: hireDateLocal,
|
||||||
ProbationEndsAt: probationEndsAt,
|
ProbationEndsAt: probationEndsAt,
|
||||||
DaysUntilProbationEnds: daysLeft,
|
DaysUntilProbationEnds: daysLeft,
|
||||||
// Не ссылка на саму заявку на приём (она уже закрыта, решать по ней
|
// Не ссылка на саму заявку на приём (она уже закрыта, решать по ней
|
||||||
|
|||||||
+24
@@ -522,6 +522,30 @@ public sealed class ReminderServiceTests : IClassFixture<PostgresDatabaseFixture
|
|||||||
result.Value.ShouldBeEmpty();
|
result.Value.ShouldBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Портал пишет дату приёма как МЕСТНУЮ полночь, переведённую в UTC, поэтому в
|
||||||
|
// списке она лежит как 19:00Z предыдущего дня. Здесь 2026-06-14T19:00Z — это
|
||||||
|
// 15 июня по местному календарю (см. LocalOffset в ReminderService): срок
|
||||||
|
// истекает 15 августа, до него от Now (12 августа) ровно 3 дня.
|
||||||
|
// Если считать по UTC, дата приёма превратилась бы в 14 июня, срок — в 14
|
||||||
|
// августа, и осталось бы 2 дня: напоминание не построилось бы вовсе.
|
||||||
|
[Fact(DisplayName = "Дата приёма считается по местному календарю, а не по UTC")]
|
||||||
|
public async Task HireDate_IsReadInLocalTimeZone()
|
||||||
|
{
|
||||||
|
var (listUrl, pageUrl) = await CreateHireTypeAsync("Probation timezone");
|
||||||
|
var rows = new[] { HireDataRow(1, "Закрыта", "Тимуров Тимур Тимурович", "2026-06-14T19:00:00Z", "2 месяца") };
|
||||||
|
var portalClient = BuildPortalClientMock(
|
||||||
|
"Тимуров Тимур Тимурович", dismissalDate: null, candidateEmail: "timurov@tnsu.kz", managerEmail: "manager@tnsu.kz");
|
||||||
|
|
||||||
|
var service = BuildProbationService(pageUrl, listUrl, rows, portalClient);
|
||||||
|
var result = await service.GetProbationEndingAsync(Now, CancellationToken.None);
|
||||||
|
|
||||||
|
var reminder = result.Value.ShouldHaveSingleItem().ShouldBeOfType<ProbationEndingReminderDto>();
|
||||||
|
reminder.DaysUntilProbationEnds.ShouldBe(3);
|
||||||
|
// И дата приёма, и конец срока отдаются в том же местном календаре.
|
||||||
|
reminder.HireDate.Date.ShouldBe(new DateTime(2026, 6, 15));
|
||||||
|
reminder.ProbationEndsAt.Date.ShouldBe(new DateTime(2026, 8, 15));
|
||||||
|
}
|
||||||
|
|
||||||
// Испытательный срок не бывает длиннее 6 месяцев (см. ProbationMonthsByOption
|
// Испытательный срок не бывает длиннее 6 месяцев (см. ProbationMonthsByOption
|
||||||
// в ReminderService), поэтому заявка, способная дать напоминание сегодня,
|
// в ReminderService), поэтому заявка, способная дать напоминание сегодня,
|
||||||
// физически не может лежать раньше предпоследней партиции. Три партиции:
|
// физически не может лежать раньше предпоследней партиции. Три партиции:
|
||||||
|
|||||||
Reference in New Issue
Block a user