fix(dal): EsppScheduleTransformService на входе делаем смещение в зону МСК, чтобы для шаблонов, у которых расписание считается по дням недели и время старта установлено в промежутке 21-0 гринвича, в которое ещё вчера относительно нормальных часовых поясов, расчёт проходит неверно

This commit is contained in:
Mikhail Kuznetsov
2025-11-28 12:35:18 +10:00
parent 5f84ccf4b8
commit 3ec052a86d
3 changed files with 105 additions and 143 deletions

View File

@@ -7,11 +7,14 @@
{ {
/// <summary> /// <summary>
/// Это текущий день относительно часовой зоны МСК? Да - текущий, нет - следующий день. /// Это текущий день относительно часовой зоны МСК? Да - текущий, нет - следующий день.
/// Так делаьб фуфуфу!!!
/// смотри IsCurrentDayRelativeMskTime(int hour)
/// </summary> /// </summary>
/// <param name="date">Дата ЮТС</param> /// <param name="date">Дата ЮТС</param>
/// <returns></returns> /// <returns></returns>
public static bool IsCurrentDayRelativeMskTime(DateTimeOffset date) public static bool IsCurrentDayRelativeMskTime(DateTimeOffset date)
{ {
// смотри IsCurrentDayRelativeMskTime(int hour)
return IsCurrentDayRelativeMskTime(date.Hour); return IsCurrentDayRelativeMskTime(date.Hour);
} }
@@ -25,6 +28,9 @@
{ {
//Так как в базе всё храним в UTC то время с 21 часа уже соответствует следующими суткам в календаре Москвы //Так как в базе всё храним в UTC то время с 21 часа уже соответствует следующими суткам в календаре Москвы
//TODO: Не надо так делать!!! НИКАДА!!!
// нужно добавить к таймзоне ЮТС +3 часа, посчитать в МСК и потом обратно отнять 3 часа
if (hour >= 21) if (hour >= 21)
return false; return false;

View File

@@ -32,6 +32,7 @@ namespace PARR.DAL.Tests.TransformServices
); );
} }
[Fact] [Fact]
public async Task GetNextDate_Monthly2_ReturnsSameDateForAllReferenceDates() public async Task GetNextDate_Monthly2_ReturnsSameDateForAllReferenceDates()
{ {
@@ -95,7 +96,7 @@ namespace PARR.DAL.Tests.TransformServices
esppSchTypeConfigServiceMock esppSchTypeConfigServiceMock
.Setup(x => x.GetEsppScheduleDtoAsync(It.IsAny<Guid>())) .Setup(x => x.GetEsppScheduleDtoAsync(It.IsAny<Guid>()))
.ReturnsAsync(scheduleDto); .ReturnsAsync(scheduleDto);
//Act //Act
var resultFromPast = await service.GetNextDateAsync(Guid.NewGuid(), pastDate); var resultFromPast = await service.GetNextDateAsync(Guid.NewGuid(), pastDate);
var resultFromFutureNonMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureNonMatchingDate); var resultFromFutureNonMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureNonMatchingDate);
@@ -107,6 +108,7 @@ namespace PARR.DAL.Tests.TransformServices
Assert.Equal(expectedDate, resultFromFutureMatching); Assert.Equal(expectedDate, resultFromFutureMatching);
} }
[Fact] [Fact]
public async Task GetNextDate_Monthly_ReturnsSameDateForAllReferenceDates() public async Task GetNextDate_Monthly_ReturnsSameDateForAllReferenceDates()
{ {
@@ -161,6 +163,7 @@ namespace PARR.DAL.Tests.TransformServices
Assert.Equal(expectedDate, resultFromFutureMatching); Assert.Equal(expectedDate, resultFromFutureMatching);
} }
[Fact] [Fact]
public async Task GetNextDate_Annually_ReturnsSameDateForAllReferenceDates() public async Task GetNextDate_Annually_ReturnsSameDateForAllReferenceDates()
{ {
@@ -236,6 +239,7 @@ namespace PARR.DAL.Tests.TransformServices
Assert.Equal(expectedDate, resultFromFutureMatching); Assert.Equal(expectedDate, resultFromFutureMatching);
} }
[Fact] [Fact]
public async Task GetNextDate_Annually2_ReturnsSameDateForAllReferenceDates() public async Task GetNextDate_Annually2_ReturnsSameDateForAllReferenceDates()
{ {
@@ -392,9 +396,9 @@ namespace PARR.DAL.Tests.TransformServices
public async Task GetNextDate_Regularly_EveryDay() public async Task GetNextDate_Regularly_EveryDay()
{ {
// Arrange // Arrange
var pastDate = new DateTimeOffset(2025, 11, 20, 10, 0, 0, TimeSpan.Zero); // Прошлое время var pastDate = new DateTimeOffset(2025, 11, 20, 20, 0, 0, TimeSpan.Zero); // Прошлое время
var futureDate = new DateTimeOffset(2025, 11, 30, 10, 0, 0, TimeSpan.Zero); // Будущее время var futureDate = new DateTimeOffset(2025, 11, 30, 10, 0, 0, TimeSpan.Zero); // Будущее время
var matchingDate = new DateTimeOffset(2025, 11, 27, 10, 0, 0, TimeSpan.Zero); // Совпадает время var matchingDate = DateTimeOffset.UtcNow; // Совпадает время
var scheduleDto = new EsppScheduleDto var scheduleDto = new EsppScheduleDto
{ {
@@ -434,25 +438,85 @@ namespace PARR.DAL.Tests.TransformServices
// Act // Act
var resultFromPast = await service.GetNextDateAsync(Guid.NewGuid(), pastDate); var resultFromPast = await service.GetNextDateAsync(Guid.NewGuid(), pastDate);
var resultFromFutureMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureDate); var resultFromFuture = await service.GetNextDateAsync(Guid.NewGuid(), futureDate);
var resultFromSimpleMatching = await service.GetNextDateAsync(Guid.NewGuid(), matchingDate); var resultFromMatching = await service.GetNextDateAsync(Guid.NewGuid(), matchingDate);
// Assert // Assert
Assert.Equal(matchingDate, resultFromPast); Assert.Equal(new DateTimeOffset(
Assert.Equal(futureDate, resultFromFutureMatching); DateTimeOffset.UtcNow.Year,
Assert.Equal(matchingDate, resultFromSimpleMatching); DateTimeOffset.UtcNow.Month,
DateTimeOffset.UtcNow.Day,
20, 0, 0, 0, 0, TimeSpan.Zero), resultFromPast);
Assert.Equal(futureDate, resultFromFuture);
Assert.Equal(matchingDate.AddDays(1), resultFromMatching);
} }
/*
/// <summary>
/// дата в будущем, но не по расписанию
/// </summary>
/// <returns></returns>
[Fact] [Fact]
public async Task GetNextDate_Weekly_FutureDateNotMatchingSchedule_ReturnsNextScheduledDate() public async Task GetNextDate_Regularly_Every3Year()
{ {
// Arrange // Arrange
var futureDate = new DateTimeOffset(2025, 12, 04, 10, 0, 0, TimeSpan.Zero); // Четверг var pastDate = new DateTimeOffset(2020, 11, 20, 10, 0, 0, TimeSpan.Zero); // Прошлое время
var futureDate = new DateTimeOffset(2026, 11, 30, 10, 0, 0, TimeSpan.Zero); // Будущее время
var matchingDate = DateTimeOffset.UtcNow; // Совпадает время
var failureDate = new DateTimeOffset(2027, 01, 01, 23, 59, 0, TimeSpan.Zero); // Дата с ошибкой в работе
var scheduleDto = new EsppScheduleDto
{
TypeSchedule = new EsppSchTypeSchedule
{
Id = (int)EsppSchTypeScheduleEnum.Regularly,
Name = "Regularly",
Description = "Регулярно"
},
Values = new List<EsppScheduleValDto>
{
new EsppScheduleValDto
{
Order = 1,
Type = new EsppSchType
{
Id = 1,
Name = "Интервал",
Description = "Интервал повторения (например, Ежедневно)"
},
Value = new EsppSchTypeValue
{
Id = Guid.NewGuid(),
Value = "Каждые 3 года",
EsppExportValue = "1095 00:00:00",
TypeId = 1,
DateCreated = DateTimeOffset.UtcNow,
Order = 1
}
}
}
};
esppSchTypeConfigServiceMock
.Setup(x => x.GetEsppScheduleDtoAsync(It.IsAny<Guid>()))
.ReturnsAsync(scheduleDto);
// Act
var resultFromPast = await service.GetNextDateAsync(Guid.NewGuid(), pastDate);
var resultFromFutureMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureDate);
var resultFromSimpleMatching = await service.GetNextDateAsync(Guid.NewGuid(), matchingDate);
var resultFromFailure = await service.GetNextDateAsync(Guid.NewGuid(), failureDate);
// Assert
Assert.Equal(pastDate.AddHours(26280 * 2), resultFromPast);
Assert.Equal(futureDate, resultFromFutureMatching);
Assert.Equal(matchingDate.AddHours(26280), resultFromSimpleMatching);
Assert.Equal(failureDate, resultFromFailure);
}
[Fact]
public async Task GetNextDate_Weekly_TestUTCToMsk()
{
// Arrange
var beforeMSKTZ = new DateTimeOffset(2025, 11, 25, 3, 0, 0, TimeSpan.Zero);
var afterMSKTZ = new DateTimeOffset(2025, 12, 01, 3, 0, 0, TimeSpan.Zero);
var scheduleDto = new EsppScheduleDto var scheduleDto = new EsppScheduleDto
{ {
@@ -491,124 +555,12 @@ namespace PARR.DAL.Tests.TransformServices
.ReturnsAsync(scheduleDto); .ReturnsAsync(scheduleDto);
// Act // Act
var result = await service.GetNextDateAsync(Guid.NewGuid(), futureDate); var resultFrombeforeMSKTZ = await service.GetNextDateAsync(Guid.NewGuid(), beforeMSKTZ);
var resultFromafterMSKTZ = await service.GetNextDateAsync(Guid.NewGuid(), afterMSKTZ);
// Assert // Assert
Assert.Equal(DayOfWeek.Monday, result.DayOfWeek); Assert.Equal(new DateTimeOffset(2025, 12, 1, 3, 0, 0, TimeSpan.Zero), resultFrombeforeMSKTZ);
Assert.True(result >= DateTimeOffset.UtcNow, "Дата должна быть в будущем"); Assert.Equal(new DateTimeOffset(2025, 12, 1, 3, 0, 0, TimeSpan.Zero), resultFromafterMSKTZ);
} }
/// <summary>
/// несуществующая дата (31 февраля)
/// </summary>
/// <returns></returns>
[Fact]
public async Task GetNextDate_Monthly_InvalidDay_FallsBackToLastDay()
{
// Arrange
var pastDate = new DateTimeOffset(2025, 1, 10, 10, 0, 0, TimeSpan.Zero); // Прошлое
var scheduleDto = new EsppScheduleDto
{
TypeSchedule = new EsppSchTypeSchedule
{
Id = (int)EsppSchTypeScheduleEnum.Monthly,
Name = "Ежемесячно",
Description = "Расписание по числу месяца"
},
Values = new List<EsppScheduleValDto>
{
new EsppScheduleValDto
{
Order = 1,
Type = new EsppSchType
{
Id = 1,
Name = "День месяца",
Description = "Число месяца"
},
Value = new EsppSchTypeValue
{
Id = Guid.NewGuid(),
Value = "31", // <-- 31 февраля не существует
EsppExportValue = "31-е число",
TypeId = 1,
DateCreated = DateTimeOffset.UtcNow,
Order = 1
}
}
}
};
esppSchTypeConfigServiceMock
.Setup(x => x.GetEsppScheduleDtoAsync(It.IsAny<Guid>()))
.ReturnsAsync(scheduleDto);
// Act
var result = await service.GetNextDateAsync(Guid.NewGuid(), pastDate);
// Assert
// Результат должен быть 28 февраля (или 29 в високосный год)
Assert.Equal(28, result.Day); // или 29, если високосный
Assert.Equal(2, result.Month);
}
/// <summary>
/// корректная рекурсия (без зацикливания)
/// </summary>
/// <returns></returns>
[Fact]
public async Task GetNextDate_Regularly_RecursionDoesNotLoop()
{
// Arrange
var pastDate = new DateTimeOffset(1925, 11, 20, 10, 0, 0, TimeSpan.Zero); // Давно прошло
var scheduleDto = new EsppScheduleDto
{
TypeSchedule = new EsppSchTypeSchedule
{
Id = (int)EsppSchTypeScheduleEnum.Regularly,
Name = "Регулярно",
Description = "Регулярное расписание"
},
Values = new List<EsppScheduleValDto>
{
new EsppScheduleValDto
{
Order = 1,
Type = new EsppSchType
{
Id = 1,
Name = "Интервал",
Description = "Интервал повторения (например, Ежедневно)"
},
Value = new EsppSchTypeValue
{
Id = Guid.NewGuid(),
Value = "Каждые 2 часа", // <-- Интервал 2 часа
EsppExportValue = "Каждые 2 часа",
TypeId = 1,
DateCreated = DateTimeOffset.UtcNow,
Order = 1
}
}
}
};
esppSchTypeConfigServiceMock
.Setup(x => x.GetEsppScheduleDtoAsync(It.IsAny<Guid>()))
.ReturnsAsync(scheduleDto);
// Act
var result = await service.GetNextDateAsync(Guid.NewGuid(), pastDate);
// Assert
// Результат должен быть >= DateTimeOffset.UtcNow
Assert.True(result >= DateTimeOffset.UtcNow, "Дата должна быть в будущем");
// Должно быть кратно интервалу
var diff = (result - pastDate).TotalHours;
Assert.True(diff % 2 == 0 || diff % 2 == 2, "Разница должна быть кратна 2 часам");
}*/
} }
} }

View File

@@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PARR.Common;
using PARR.Constants; using PARR.Constants;
using PARR.DAL.Contracts; using PARR.DAL.Contracts;
using PARR.DAL.DomainModels; using PARR.DAL.DomainModels;
@@ -73,33 +72,40 @@ namespace PARR.DAL.TransformServices
{ {
var nextRun = referenceDate; var nextRun = referenceDate;
//---------
// Для рассчета по МСК времени, потому что в ЮТС может быть еще ВСК, а по МСК это уже ПНД
var offsetReferenceDate = referenceDate.ToOffset(new TimeSpan(3, 0, 0));
//---------
switch (esppSchedule.TypeSchedule.Id) switch (esppSchedule.TypeSchedule.Id)
{ {
case (int)EsppSchTypeScheduleEnum.Regularly: case (int)EsppSchTypeScheduleEnum.Regularly:
nextRun = GetNextDateRegularly(esppSchedule.Values, referenceDate); nextRun = GetNextDateRegularly(esppSchedule.Values, offsetReferenceDate);
break; break;
case (int)EsppSchTypeScheduleEnum.Weekly: case (int)EsppSchTypeScheduleEnum.Weekly:
nextRun = GetNextDateWeekly(esppSchedule.Values, referenceDate); nextRun = GetNextDateWeekly(esppSchedule.Values, offsetReferenceDate);
break; break;
case (int)EsppSchTypeScheduleEnum.Monthly: case (int)EsppSchTypeScheduleEnum.Monthly:
nextRun = GetNextDateMonthly(esppSchedule.Values, referenceDate); nextRun = GetNextDateMonthly(esppSchedule.Values, offsetReferenceDate);
break; break;
case (int)EsppSchTypeScheduleEnum.Monthly2: case (int)EsppSchTypeScheduleEnum.Monthly2:
nextRun = GetNextDateMonthly2(esppSchedule.Values, referenceDate); nextRun = GetNextDateMonthly2(esppSchedule.Values, offsetReferenceDate);
break; break;
case (int)EsppSchTypeScheduleEnum.Annually: case (int)EsppSchTypeScheduleEnum.Annually:
nextRun = GetNextDateAnnually(esppSchedule.Values, referenceDate); nextRun = GetNextDateAnnually(esppSchedule.Values, offsetReferenceDate);
break; break;
case (int)EsppSchTypeScheduleEnum.Annually2: case (int)EsppSchTypeScheduleEnum.Annually2:
nextRun = GetNextDateAnnually2(esppSchedule.Values, referenceDate); nextRun = GetNextDateAnnually2(esppSchedule.Values, offsetReferenceDate);
break; break;
} }
// Возвращаем обратно в ЮТС
nextRun = nextRun.ToOffset(TimeSpan.Zero);
//возможно для автораспределения нужно nextRun = nextRunModifierService.GetWorkDayAsync(nextRun).GetAwaiter().GetResult(); //возможно для автораспределения нужно nextRun = nextRunModifierService.GetWorkDayAsync(nextRun).GetAwaiter().GetResult();
// тут может быть 21 час по МСК, то это предыдущие сутки, нужно отнять 1 день // тут может быть 21 час по МСК, то это предыдущие сутки, нужно отнять 1 день
if (!DateResolver.IsCurrentDayRelativeMskTime(nextRun.Hour)) //if (!DateResolver.IsCurrentDayRelativeMskTime(nextRun.Hour))
nextRun = nextRun.AddDays(-1); // nextRun = nextRun.AddDays(-1);
return nextRun; return nextRun;
} }
@@ -196,7 +202,7 @@ namespace PARR.DAL.TransformServices
{"Каждые 1,5 года", 13140}, {"Каждые 1,5 года", 13140},
{"Каждые 3 года", 26280}, {"Каждые 3 года", 26280},
}; };
if (!regDict.TryGetValue(values[0].Value.Value, out int regNum)) if (!regDict.TryGetValue(values[0].Value.Value, out int regNum))
@@ -206,8 +212,6 @@ namespace PARR.DAL.TransformServices
} }
var calcDay = referenceDate; var calcDay = referenceDate;
if (calcDay < DateTimeOffset.UtcNow)
calcDay = calcDay.AddHours(regNum);
//TODO: вот это повторяется от метода к методу //TODO: вот это повторяется от метода к методу
//Если итоговая дата указывает на прошлое, то повторяем расчёт и уходим в рекурсию //Если итоговая дата указывает на прошлое, то повторяем расчёт и уходим в рекурсию