From 3ec052a86d04e55d0be97aa21d1cb1f38757ab48 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Fri, 28 Nov 2025 12:35:18 +1000 Subject: [PATCH] =?UTF-8?q?fix(dal):=20EsppScheduleTransformService=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B2=D1=85=D0=BE=D0=B4=D0=B5=20=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D0=B5=D0=BC=20=D1=81=D0=BC=D0=B5=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=20=D0=B7=D0=BE=D0=BD=D1=83=20=D0=9C=D0=A1?= =?UTF-8?q?=D0=9A,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE=D0=B2,=20=D1=83?= =?UTF-8?q?=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D1=85=20=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D1=87?= =?UTF-8?q?=D0=B8=D1=82=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B4=D0=BD=D1=8F=D0=BC=20=D0=BD=D0=B5=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D1=82=D0=B0=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=B2=20=D0=BF=D1=80=D0=BE=D0=BC?= =?UTF-8?q?=D0=B5=D0=B6=D1=83=D1=82=D0=BA=D0=B5=2021-0=20=D0=B3=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D0=B2=D0=B8=D1=87=D0=B0,=20=D0=B2=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BE=D0=B5=20=D0=B5=D1=89=D1=91=20=D0=B2?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B0=20=D0=BE=D1=82=D0=BD=D0=BE=D1=81=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=20=D0=BD=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D1=87=D0=B0=D1=81=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D1=85=20=D0=BF=D0=BE=D1=8F=D1=81=D0=BE=D0=B2,=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D1=87=D1=91=D1=82=20=D0=BF=D1=80=D0=BE=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=B8=D1=82=20=D0=BD=D0=B5=D0=B2=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.Common/DateResolver.cs | 6 + .../EsppScheduleTransformServiceTests.cs | 214 +++++++----------- .../EsppScheduleTransformService.cs | 28 ++- 3 files changed, 105 insertions(+), 143 deletions(-) diff --git a/PARR.Common/DateResolver.cs b/PARR.Common/DateResolver.cs index 222f3326..a93c38dc 100644 --- a/PARR.Common/DateResolver.cs +++ b/PARR.Common/DateResolver.cs @@ -7,11 +7,14 @@ { /// /// Это текущий день относительно часовой зоны МСК? Да - текущий, нет - следующий день. + /// Так делаьб фуфуфу!!! + /// смотри IsCurrentDayRelativeMskTime(int hour) /// /// Дата ЮТС /// public static bool IsCurrentDayRelativeMskTime(DateTimeOffset date) { + // смотри IsCurrentDayRelativeMskTime(int hour) return IsCurrentDayRelativeMskTime(date.Hour); } @@ -25,6 +28,9 @@ { //Так как в базе всё храним в UTC то время с 21 часа уже соответствует следующими суткам в календаре Москвы + //TODO: Не надо так делать!!! НИКАДА!!! + // нужно добавить к таймзоне ЮТС +3 часа, посчитать в МСК и потом обратно отнять 3 часа + if (hour >= 21) return false; diff --git a/PARR.DAL.Tests/TransformServices/EsppScheduleTransformServiceTests.cs b/PARR.DAL.Tests/TransformServices/EsppScheduleTransformServiceTests.cs index e720889b..b270167e 100644 --- a/PARR.DAL.Tests/TransformServices/EsppScheduleTransformServiceTests.cs +++ b/PARR.DAL.Tests/TransformServices/EsppScheduleTransformServiceTests.cs @@ -32,6 +32,7 @@ namespace PARR.DAL.Tests.TransformServices ); } + [Fact] public async Task GetNextDate_Monthly2_ReturnsSameDateForAllReferenceDates() { @@ -95,7 +96,7 @@ namespace PARR.DAL.Tests.TransformServices esppSchTypeConfigServiceMock .Setup(x => x.GetEsppScheduleDtoAsync(It.IsAny())) .ReturnsAsync(scheduleDto); - + //Act var resultFromPast = await service.GetNextDateAsync(Guid.NewGuid(), pastDate); var resultFromFutureNonMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureNonMatchingDate); @@ -107,6 +108,7 @@ namespace PARR.DAL.Tests.TransformServices Assert.Equal(expectedDate, resultFromFutureMatching); } + [Fact] public async Task GetNextDate_Monthly_ReturnsSameDateForAllReferenceDates() { @@ -161,6 +163,7 @@ namespace PARR.DAL.Tests.TransformServices Assert.Equal(expectedDate, resultFromFutureMatching); } + [Fact] public async Task GetNextDate_Annually_ReturnsSameDateForAllReferenceDates() { @@ -236,6 +239,7 @@ namespace PARR.DAL.Tests.TransformServices Assert.Equal(expectedDate, resultFromFutureMatching); } + [Fact] public async Task GetNextDate_Annually2_ReturnsSameDateForAllReferenceDates() { @@ -392,9 +396,9 @@ namespace PARR.DAL.Tests.TransformServices public async Task GetNextDate_Regularly_EveryDay() { // 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 matchingDate = new DateTimeOffset(2025, 11, 27, 10, 0, 0, TimeSpan.Zero); // Совпадает время + var matchingDate = DateTimeOffset.UtcNow; // Совпадает время var scheduleDto = new EsppScheduleDto { @@ -434,25 +438,85 @@ namespace PARR.DAL.Tests.TransformServices // 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 resultFromFuture = await service.GetNextDateAsync(Guid.NewGuid(), futureDate); + var resultFromMatching = await service.GetNextDateAsync(Guid.NewGuid(), matchingDate); // Assert - Assert.Equal(matchingDate, resultFromPast); - Assert.Equal(futureDate, resultFromFutureMatching); - Assert.Equal(matchingDate, resultFromSimpleMatching); + Assert.Equal(new DateTimeOffset( + DateTimeOffset.UtcNow.Year, + DateTimeOffset.UtcNow.Month, + DateTimeOffset.UtcNow.Day, + 20, 0, 0, 0, 0, TimeSpan.Zero), resultFromPast); + Assert.Equal(futureDate, resultFromFuture); + Assert.Equal(matchingDate.AddDays(1), resultFromMatching); } - /* - /// - /// дата в будущем, но не по расписанию - /// - /// + [Fact] - public async Task GetNextDate_Weekly_FutureDateNotMatchingSchedule_ReturnsNextScheduledDate() + public async Task GetNextDate_Regularly_Every3Year() { // 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 + { + 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())) + .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 { @@ -491,124 +555,12 @@ namespace PARR.DAL.Tests.TransformServices .ReturnsAsync(scheduleDto); // 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.Equal(DayOfWeek.Monday, result.DayOfWeek); - Assert.True(result >= DateTimeOffset.UtcNow, "Дата должна быть в будущем"); + Assert.Equal(new DateTimeOffset(2025, 12, 1, 3, 0, 0, TimeSpan.Zero), resultFrombeforeMSKTZ); + Assert.Equal(new DateTimeOffset(2025, 12, 1, 3, 0, 0, TimeSpan.Zero), resultFromafterMSKTZ); } - - /// - /// несуществующая дата (31 февраля) - /// - /// - [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 - { - 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())) - .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); - } - - - /// - /// корректная рекурсия (без зацикливания) - /// - /// - [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 - { - 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())) - .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 часам"); - }*/ } } diff --git a/PARR.DAL/TransformServices/EsppScheduleTransformService.cs b/PARR.DAL/TransformServices/EsppScheduleTransformService.cs index 06ed46b6..bdca7091 100644 --- a/PARR.DAL/TransformServices/EsppScheduleTransformService.cs +++ b/PARR.DAL/TransformServices/EsppScheduleTransformService.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using PARR.Common; using PARR.Constants; using PARR.DAL.Contracts; using PARR.DAL.DomainModels; @@ -73,33 +72,40 @@ namespace PARR.DAL.TransformServices { var nextRun = referenceDate; + //--------- + // Для рассчета по МСК времени, потому что в ЮТС может быть еще ВСК, а по МСК это уже ПНД + var offsetReferenceDate = referenceDate.ToOffset(new TimeSpan(3, 0, 0)); + //--------- + switch (esppSchedule.TypeSchedule.Id) { case (int)EsppSchTypeScheduleEnum.Regularly: - nextRun = GetNextDateRegularly(esppSchedule.Values, referenceDate); + nextRun = GetNextDateRegularly(esppSchedule.Values, offsetReferenceDate); break; case (int)EsppSchTypeScheduleEnum.Weekly: - nextRun = GetNextDateWeekly(esppSchedule.Values, referenceDate); + nextRun = GetNextDateWeekly(esppSchedule.Values, offsetReferenceDate); break; case (int)EsppSchTypeScheduleEnum.Monthly: - nextRun = GetNextDateMonthly(esppSchedule.Values, referenceDate); + nextRun = GetNextDateMonthly(esppSchedule.Values, offsetReferenceDate); break; case (int)EsppSchTypeScheduleEnum.Monthly2: - nextRun = GetNextDateMonthly2(esppSchedule.Values, referenceDate); + nextRun = GetNextDateMonthly2(esppSchedule.Values, offsetReferenceDate); break; case (int)EsppSchTypeScheduleEnum.Annually: - nextRun = GetNextDateAnnually(esppSchedule.Values, referenceDate); + nextRun = GetNextDateAnnually(esppSchedule.Values, offsetReferenceDate); break; case (int)EsppSchTypeScheduleEnum.Annually2: - nextRun = GetNextDateAnnually2(esppSchedule.Values, referenceDate); + nextRun = GetNextDateAnnually2(esppSchedule.Values, offsetReferenceDate); break; } + // Возвращаем обратно в ЮТС + nextRun = nextRun.ToOffset(TimeSpan.Zero); //возможно для автораспределения нужно nextRun = nextRunModifierService.GetWorkDayAsync(nextRun).GetAwaiter().GetResult(); // тут может быть 21 час по МСК, то это предыдущие сутки, нужно отнять 1 день - if (!DateResolver.IsCurrentDayRelativeMskTime(nextRun.Hour)) - nextRun = nextRun.AddDays(-1); + //if (!DateResolver.IsCurrentDayRelativeMskTime(nextRun.Hour)) + // nextRun = nextRun.AddDays(-1); return nextRun; } @@ -196,7 +202,7 @@ namespace PARR.DAL.TransformServices {"Каждые 1,5 года", 13140}, {"Каждые 3 года", 26280}, - }; + }; if (!regDict.TryGetValue(values[0].Value.Value, out int regNum)) @@ -206,8 +212,6 @@ namespace PARR.DAL.TransformServices } var calcDay = referenceDate; - if (calcDay < DateTimeOffset.UtcNow) - calcDay = calcDay.AddHours(regNum); //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и уходим в рекурсию