diff --git a/PARR.API.sln b/PARR.API.sln index efadc31f..e1b3da1d 100644 --- a/PARR.API.sln +++ b/PARR.API.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.4.33213.308 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11222.15 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PARR.API", "PARR.API\PARR.API.csproj", "{82C57299-DA04-4A99-9C04-614FDDCE7AA1}" EndProject @@ -100,6 +100,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateMatcher", "PAR EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateMatcherWorker", "PARR.TemplateMatcherWorker\PARR.TemplateMatcherWorker.csproj", "{DA6B2A17-6E64-4E2C-8D4C-E278BEE94E03}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.DAL.Tests", "PARR.DAL.Tests\PARR.DAL.Tests.csproj", "{D61A7AB6-D785-477A-95D1-FBED049D6CE2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -280,6 +282,10 @@ Global {DA6B2A17-6E64-4E2C-8D4C-E278BEE94E03}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA6B2A17-6E64-4E2C-8D4C-E278BEE94E03}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA6B2A17-6E64-4E2C-8D4C-E278BEE94E03}.Release|Any CPU.Build.0 = Release|Any CPU + {D61A7AB6-D785-477A-95D1-FBED049D6CE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D61A7AB6-D785-477A-95D1-FBED049D6CE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D61A7AB6-D785-477A-95D1-FBED049D6CE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D61A7AB6-D785-477A-95D1-FBED049D6CE2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PARR.DAL.Tests/PARR.DAL.Tests.csproj b/PARR.DAL.Tests/PARR.DAL.Tests.csproj new file mode 100644 index 00000000..d0843bba --- /dev/null +++ b/PARR.DAL.Tests/PARR.DAL.Tests.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/PARR.DAL.Tests/TransformServices/EsppScheduleTransformServiceTests.cs b/PARR.DAL.Tests/TransformServices/EsppScheduleTransformServiceTests.cs new file mode 100644 index 00000000..ffd260ec --- /dev/null +++ b/PARR.DAL.Tests/TransformServices/EsppScheduleTransformServiceTests.cs @@ -0,0 +1,614 @@ +using Microsoft.Extensions.Logging; +using Moq; +using PARR.DAL.Contracts; +using PARR.DAL.DomainModels; +using PARR.DAL.Models; +using PARR.DAL.Services.Interfaces; +using PARR.DAL.TransformServices; + +namespace PARR.DAL.Tests.TransformServices +{ + public class EsppScheduleTransformServiceTests + { + private Mock esppSchTypeConfigServiceMock; + private Mock nextRunModifierServiceMock; + + private ILogger logger; + private EsppScheduleTransformService service; + + public EsppScheduleTransformServiceTests() + { + esppSchTypeConfigServiceMock = new Mock(); + nextRunModifierServiceMock = new Mock(); + + var loggerFactory = new LoggerFactory(); + logger = loggerFactory.CreateLogger(); + + + service = new EsppScheduleTransformService( + logger, + esppSchTypeConfigServiceMock.Object, + nextRunModifierServiceMock.Object + ); + } + + [Fact] + public async Task GetNextDate_Monthly2_ReturnsSameDateForAllReferenceDates() + { + // Arrange + var pastDate = new DateTimeOffset(2024, 10, 10, 10, 0, 0, TimeSpan.Zero); // Прошлое время + var futureNonMatchingDate = new DateTimeOffset(2025, 12, 08, 10, 0, 0, TimeSpan.Zero); // Будущее, не среда и до ожидаемой даты + var futureMatchingDate = new DateTimeOffset(2025, 12, 10, 10, 0, 0, TimeSpan.Zero); // Вторая среда и совпадает с ожидаемой датой + var expectedDate = futureMatchingDate; + + var scheduleDto = new EsppScheduleDto + { + TypeSchedule = new EsppSchTypeSchedule + { + Id = (int)EsppSchTypeScheduleEnum.Monthly2, + Name = "Ежемесячно-2", + Description = "Расписание по дням недели в месяце" + }, + Values = new List + { + new EsppScheduleValDto + { + Order = 1, + Type = new EsppSchType + { + Id = 1, + Name = "Порядок", + Description = "Порядковый номер дня недели" + }, + Value = new EsppSchTypeValue + { + Id = Guid.NewGuid(), + Value = "Второй", + EsppExportValue = "Второй (экспорт)", + TypeId = 1, + DateCreated = DateTimeOffset.UtcNow, + Order = 2 + } + }, + new EsppScheduleValDto + { + Order = 2, + Type = new EsppSchType + { + Id = 2, + Name = "День недели", + Description = "День недели (например, понедельник)" + }, + Value = new EsppSchTypeValue + { + Id = Guid.NewGuid(), + Value = "Среда", + EsppExportValue = "Среда", + TypeId = 2, + DateCreated = DateTimeOffset.UtcNow, + Order = 3 + } + } + } + }; + + 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); + var resultFromFutureMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureMatchingDate); + + //Assert + Assert.Equal(expectedDate, resultFromPast); + Assert.Equal(expectedDate, resultFromFutureNonMatching); + Assert.Equal(expectedDate, resultFromFutureMatching); + } + + [Fact] + public async Task GetNextDate_Monthly_ReturnsSameDateForAllReferenceDates() + { + // Arrange + var pastDate = new DateTimeOffset(2024, 10, 10, 10, 0, 0, TimeSpan.Zero); // Прошлое время + var futureNonMatchingDate = new DateTimeOffset(2025, 12, 08, 10, 0, 0, TimeSpan.Zero); // Будущее, не 15-е + var futureMatchingDate = new DateTimeOffset(2025, 12, 15, 10, 0, 0, TimeSpan.Zero); // 15-е декабря — совпадает с ожидаемой датой + var expectedDate = futureMatchingDate; + + 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 = "15", + EsppExportValue = "15-е число", + TypeId = 1, + DateCreated = DateTimeOffset.UtcNow, + Order = 1 + } + } + } + }; + + esppSchTypeConfigServiceMock + .Setup(x => x.GetEsppScheduleDtoAsync(It.IsAny())) + .ReturnsAsync(scheduleDto); + + var resultFromPast = await service.GetNextDateAsync(Guid.NewGuid(), pastDate); + var resultFromFutureNonMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureNonMatchingDate); + var resultFromFutureMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureMatchingDate); + + Assert.Equal(expectedDate, resultFromPast); + Assert.Equal(expectedDate, resultFromFutureNonMatching); + Assert.Equal(expectedDate, resultFromFutureMatching); + } + + [Fact] + public async Task GetNextDate_Annually_ReturnsSameDateForAllReferenceDates() + { + // Arrange + var pastDate = new DateTimeOffset(2024, 10, 10, 10, 0, 0, TimeSpan.Zero); // Прошлое время + var futureNonMatchingDate = new DateTimeOffset(2025, 11, 20, 10, 0, 0, TimeSpan.Zero); // Будущее, не январь, не 1-е + var futureMatchingDate = new DateTimeOffset(2026, 1, 1, 10, 0, 0, TimeSpan.Zero); // 1 января 2026 — совпадает с ожидаемой датой + var expectedDate = futureMatchingDate; // 1 января 2026 + + var scheduleDto = new EsppScheduleDto + { + TypeSchedule = new EsppSchTypeSchedule + { + Id = (int)EsppSchTypeScheduleEnum.Annually, + Name = "Ежегодно", + Description = "Расписание по месяцу и дню года" + }, + Values = new List + { + new EsppScheduleValDto + { + Order = 1, + Type = new EsppSchType + { + Id = 1, + Name = "Месяц", + Description = "Месяц года" + }, + Value = new EsppSchTypeValue + { + Id = Guid.NewGuid(), + Value = "Январь", + EsppExportValue = "Январь", + TypeId = 1, + DateCreated = DateTimeOffset.UtcNow, + Order = 1 + } + }, + new EsppScheduleValDto + { + Order = 2, + Type = new EsppSchType + { + Id = 2, + Name = "День месяца", + Description = "Число месяца" + }, + Value = new EsppSchTypeValue + { + Id = Guid.NewGuid(), + Value = "1", + EsppExportValue = "1-е число", + TypeId = 2, + DateCreated = DateTimeOffset.UtcNow, + Order = 1 + } + } + } + }; + + 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); + var resultFromFutureMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureMatchingDate); + + // Assert + Assert.Equal(expectedDate, resultFromPast); + Assert.Equal(expectedDate, resultFromFutureNonMatching); + Assert.Equal(expectedDate, resultFromFutureMatching); + } + + [Fact] + public async Task GetNextDate_Annually2_ReturnsSameDateForAllReferenceDates() + { + // Arrange + var pastDate = new DateTimeOffset(2024, 10, 10, 10, 0, 0, TimeSpan.Zero); // Прошлое время + var futureNonMatchingDate = new DateTimeOffset(2026, 01, 02, 10, 0, 0, TimeSpan.Zero); // Будущее, не понедельник + var futureMatchingDate = new DateTimeOffset(2026, 1, 5, 10, 0, 0, TimeSpan.Zero); // Первый понедельник января 2026 — совпадает с ожидаемой датой + var expectedDate = futureMatchingDate; + + var scheduleDto = new EsppScheduleDto + { + TypeSchedule = new EsppSchTypeSchedule + { + Id = (int)EsppSchTypeScheduleEnum.Annually2, + Name = "Ежегодно-2", + Description = "Расписание по порядку дня недели в месяце (например, первый понедельник января)" + }, + Values = new List + { + new EsppScheduleValDto + { + Order = 1, + Type = new EsppSchType + { + Id = 1, + Name = "Порядок", + Description = "Порядковый номер дня недели (например, первый)" + }, + Value = new EsppSchTypeValue + { + Id = Guid.NewGuid(), + Value = "Первый", + EsppExportValue = "Первый", + TypeId = 1, + DateCreated = DateTimeOffset.UtcNow, + Order = 1 + } + }, + new EsppScheduleValDto + { + Order = 2, + Type = new EsppSchType + { + Id = 2, + Name = "День недели", + Description = "День недели (например, понедельник)" + }, + Value = new EsppSchTypeValue + { + Id = Guid.NewGuid(), + Value = "Понедельник", + EsppExportValue = "Понедельник", + TypeId = 2, + DateCreated = DateTimeOffset.UtcNow, + Order = 2 + } + }, + new EsppScheduleValDto + { + Order = 3, + Type = new EsppSchType + { + Id = 3, + Name = "Месяц", + Description = "Месяц года (например, Января)" + }, + Value = new EsppSchTypeValue + { + Id = Guid.NewGuid(), + Value = "Января", + EsppExportValue = "Января", + TypeId = 3, + DateCreated = DateTimeOffset.UtcNow, + Order = 3 + } + } + } + }; + + 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); + var resultFromFutureMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureMatchingDate); + + // Assert + Assert.Equal(expectedDate, resultFromPast); + Assert.Equal(expectedDate, resultFromFutureNonMatching); + Assert.Equal(expectedDate, resultFromFutureMatching); + } + + + [Fact] + public async Task GetNextDate_Weekly_ReturnsSameDateForAllReferenceDates() + { + // Arrange + var pastDate = new DateTimeOffset(2025, 11, 19, 10, 0, 0, TimeSpan.Zero); // Прошлое время + var futureNonMatchingDate = new DateTimeOffset(2025, 11, 27, 10, 0, 0, TimeSpan.Zero); // Будущее, четверг — не понедельник + var futureMatchingDate = new DateTimeOffset(2025, 12, 01, 10, 0, 0, TimeSpan.Zero); // четверг — совпадает с ожидаемой датой + var expectedDate = futureMatchingDate; + + var scheduleDto = new EsppScheduleDto + { + TypeSchedule = new EsppSchTypeSchedule + { + Id = (int)EsppSchTypeScheduleEnum.Weekly, + Name = "Еженедельно", + Description = "Расписание по дню недели" + }, + Values = new List + { + new EsppScheduleValDto + { + Order = 1, + Type = new EsppSchType + { + Id = 1, + Name = "День недели", + Description = "День недели" + }, + Value = new EsppSchTypeValue + { + Id = Guid.NewGuid(), + Value = "Понедельник", + EsppExportValue = "Понедельник", + 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 resultFromFutureNonMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureNonMatchingDate); + var resultFromFutureMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureMatchingDate); + + // Assert + Assert.Equal(expectedDate, resultFromPast); + Assert.Equal(expectedDate, resultFromFutureNonMatching); + Assert.Equal(expectedDate, resultFromFutureMatching); + } + + + [Fact] + public async Task GetNextDate_Regularly_EveryDay() + { + // Arrange + var pastDate = new DateTimeOffset(2025, 11, 20, 10, 0, 0, TimeSpan.Zero); // Прошлое время + var futureNonMatchingDate = new DateTimeOffset(2025, 11, 27, 10, 0, 0, TimeSpan.Zero); // Будущее время + var matchingDate = new DateTimeOffset(2025, 11, 26, 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 = "Ежедневно", + EsppExportValue = "Ежедневно", + 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 resultFromFutureNonMatching = await service.GetNextDateAsync(Guid.NewGuid(), futureNonMatchingDate); + var resultFromFutureMatching = await service.GetNextDateAsync(Guid.NewGuid(), matchingDate); + + // Assert + Assert.Equal(matchingDate.AddDays(1), resultFromPast); + Assert.Equal(futureNonMatchingDate.AddDays(1), resultFromFutureNonMatching); + Assert.Equal(matchingDate.AddDays(1), resultFromFutureMatching); + } + + /* + /// + /// дата в будущем, но не по расписанию + /// + /// + [Fact] + public async Task GetNextDate_Weekly_FutureDateNotMatchingSchedule_ReturnsNextScheduledDate() + { + // Arrange + var futureDate = new DateTimeOffset(2025, 12, 04, 10, 0, 0, TimeSpan.Zero); // Четверг + + var scheduleDto = new EsppScheduleDto + { + TypeSchedule = new EsppSchTypeSchedule + { + Id = (int)EsppSchTypeScheduleEnum.Weekly, + Name = "Еженедельно", + Description = "Расписание по дню недели" + }, + Values = new List + { + new EsppScheduleValDto + { + Order = 1, + Type = new EsppSchType + { + Id = 1, + Name = "День недели", + Description = "День недели" + }, + Value = new EsppSchTypeValue + { + Id = Guid.NewGuid(), + Value = "Понедельник", + EsppExportValue = "Понедельник", + TypeId = 1, + DateCreated = DateTimeOffset.UtcNow, + Order = 1 + } + } + } + }; + + esppSchTypeConfigServiceMock + .Setup(x => x.GetEsppScheduleDtoAsync(It.IsAny())) + .ReturnsAsync(scheduleDto); + + // Act + var result = await service.GetNextDateAsync(Guid.NewGuid(), futureDate); + + // Assert + Assert.Equal(DayOfWeek.Monday, result.DayOfWeek); + Assert.True(result >= DateTimeOffset.UtcNow, "Дата должна быть в будущем"); + } + + /// + /// несуществующая дата (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.Tests/Usings.cs b/PARR.DAL.Tests/Usings.cs new file mode 100644 index 00000000..a611174c --- /dev/null +++ b/PARR.DAL.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/PARR.DAL/PARR.DAL.csproj b/PARR.DAL/PARR.DAL.csproj index 5afba98e..b9a6e5a1 100644 --- a/PARR.DAL/PARR.DAL.csproj +++ b/PARR.DAL/PARR.DAL.csproj @@ -1,28 +1,34 @@  - - net7.0 - enable - enable - + + net7.0 + enable + enable + - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + - - - - - + + + + + + + + + <_Parameter1>PARR.DAL.Tests + + diff --git a/PARR.DAL/TransformServices/EsppScheduleTransformService.cs b/PARR.DAL/TransformServices/EsppScheduleTransformService.cs index b669950d..fc78ecb9 100644 --- a/PARR.DAL/TransformServices/EsppScheduleTransformService.cs +++ b/PARR.DAL/TransformServices/EsppScheduleTransformService.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using PARR.Common; using PARR.Constants; using PARR.DAL.Contracts; @@ -14,10 +13,9 @@ namespace PARR.DAL.TransformServices { private readonly IEsppSchTypeConfigService esppSchTypeConfigService; private readonly INextRunModifierService nextRunModifierService; - private readonly IJobGroupService jobGroupService; private readonly ILogger logger; - private readonly Dictionary monthDict = new Dictionary() + private static readonly Dictionary monthDict = new Dictionary() { { "Январь", 1 }, { "Февраль", 2 }, @@ -33,7 +31,7 @@ namespace PARR.DAL.TransformServices { "Декабрь", 12 } }; - private readonly Dictionary orderDict = new Dictionary() + private static readonly Dictionary orderDict = new Dictionary() { { "Первый", 1 }, { "Второй", 2 }, @@ -42,7 +40,7 @@ namespace PARR.DAL.TransformServices { "Последний", 5 } }; - private readonly Dictionary dwDict = new Dictionary() + private static readonly Dictionary dwDict = new Dictionary() { {"Понедельник", 1 }, {"Вторник", 2 }, @@ -57,56 +55,48 @@ namespace PARR.DAL.TransformServices public EsppScheduleTransformService( ILogger logger, IEsppSchTypeConfigService esppSchTypeConfigService, - IWeekendDayService weekendDayService, - INextRunModifierService nextRunModifierService, - IJobGroupService jobGroupService + INextRunModifierService nextRunModifierService ) { this.esppSchTypeConfigService = esppSchTypeConfigService; this.nextRunModifierService = nextRunModifierService; - this.jobGroupService = jobGroupService; this.logger = logger; } - - public DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset lastRun) + public async Task GetNextDateAsync(Guid jobGroupId, DateTimeOffset referenceDate) { - var nextRun = lastRun; + var esppSchedule = await GetEsppScheduleAsync(jobGroupId); - //если больше текущего вернуть lastRun. Это и есть следующая дата - if (lastRun > DateTimeOffset.UtcNow) - nextRun = lastRun; - //return lastRun; + return GetNextDate(esppSchedule, referenceDate); + } + + public DateTimeOffset GetNextDate(EsppScheduleDto esppSchedule, DateTimeOffset referenceDate) + { + var nextRun = referenceDate; switch (esppSchedule.TypeSchedule.Id) { case (int)EsppSchTypeScheduleEnum.Regularly: - //return GetNextDateRegularly(esppSchedule.Values, lastRun); - nextRun = GetNextDateRegularly(esppSchedule.Values, lastRun); + nextRun = GetNextDateRegularly(esppSchedule.Values, referenceDate); break; case (int)EsppSchTypeScheduleEnum.Weekly: - //return GetNextDateWeekly(esppSchedule.Values, lastRun); - nextRun = GetNextDateWeekly(esppSchedule.Values, lastRun); + nextRun = GetNextDateWeekly(esppSchedule.Values, referenceDate); break; case (int)EsppSchTypeScheduleEnum.Monthly: - //return GetNextDateMonthly(esppSchedule.Values, lastRun); - nextRun = GetNextDateMonthly(esppSchedule.Values, lastRun); + nextRun = GetNextDateMonthly(esppSchedule.Values, referenceDate); break; case (int)EsppSchTypeScheduleEnum.Monthly2: - //return GetNextDateMonthly2(esppSchedule.Values, lastRun); - nextRun = GetNextDateMonthly2(esppSchedule.Values, lastRun); + nextRun = GetNextDateMonthly2(esppSchedule.Values, referenceDate); break; case (int)EsppSchTypeScheduleEnum.Annually: - //return GetNextDateAnnually(esppSchedule.Values, lastRun); - nextRun = GetNextDateAnnually(esppSchedule.Values, lastRun); + nextRun = GetNextDateAnnually(esppSchedule.Values, referenceDate); break; case (int)EsppSchTypeScheduleEnum.Annually2: - //return GetNextDateAnnually2(esppSchedule.Values, lastRun); - nextRun = GetNextDateAnnually2(esppSchedule.Values, lastRun); + nextRun = GetNextDateAnnually2(esppSchedule.Values, referenceDate); break; } - nextRun = nextRunModifierService.GetWorkDayAsync(nextRun).GetAwaiter().GetResult(); + //возможно для автораспределения нужно nextRun = nextRunModifierService.GetWorkDayAsync(nextRun).GetAwaiter().GetResult(); // тут может быть 21 час по МСК, то это предыдущие сутки, нужно отнять 1 день if (!DateResolver.IsCurrentDayRelativeMskTime(nextRun.Hour)) @@ -116,17 +106,17 @@ namespace PARR.DAL.TransformServices } - private List GetNextSchedule(EsppScheduleDto esppSchedule, DateTimeOffset lastRun) + private List GetNextSchedule(EsppScheduleDto esppSchedule, DateTimeOffset referenceDate) { // расписание на оставшееся на сегодня время. его так мало... - lastRun = lastRun.UtcDateTime; + referenceDate = referenceDate.UtcDateTime; var schedules = new List(); - var curTime = lastRun; + var curTime = referenceDate; //Если ещё не произошло то добавляем расписание, возможно это новое AiW и он ещё ниразу не запускался. //lastRun это будущее - if (lastRun >= DateTimeOffset.UtcNow && lastRun <= DateTimeOffset.UtcNow.EndOfDay()) - schedules.Add(lastRun); + if (referenceDate >= DateTimeOffset.UtcNow && referenceDate <= DateTimeOffset.UtcNow.EndOfDay()) + schedules.Add(referenceDate); while (curTime < DateTimeOffset.UtcNow.EndOfDay()) { @@ -160,38 +150,15 @@ namespace PARR.DAL.TransformServices } - public async Task GetNextDateAsync(Guid jobGroupId, DateTimeOffset lastRun) - { - //если больше текущего вернуть lastRun. Это и есть следующая дата - if (lastRun > DateTimeOffset.UtcNow) - return await nextRunModifierService.GetWorkDayAsync(lastRun); - - var jobGroup = await jobGroupService.Get().AsTracking() - .Include(t => t.EsppSchValues) - .ThenInclude(esv => esv.EsppSchTypeValue) - .ThenInclude(etv => etv!.DistributionPeriod) - .FirstOrDefaultAsync(t => t.Id == jobGroupId); - - var esppSchedule = await GetEsppScheduleAsync(jobGroupId); - - //if (jobGroup!.IsAutoDistributionEnabled) - //{ - // var period = jobGroup.EsppSchValues.FirstOrDefault()?.EsppSchTypeValue?.DistributionPeriod; - // var periodType = ParseDistributionPeriodType(period!.Type); - - // return GetNextDateForDistributionRun(lastRun, periodType, period.Duration); - //} - - return GetNextDate(esppSchedule, lastRun); - } - public async Task> GetNextScheduleAsync(Guid jobGroupId, DateTimeOffset lastRun) + + public async Task> GetNextScheduleAsync(Guid jobGroupId, DateTimeOffset referenceDate) { //TODO: тут не проверен переход через выходные дни!!! Переход не используется, так как тут не рассчитываем NextRun. //В общем проверить, когда будем тестировать агента, что с датами все ок var esppSchedule = await GetEsppScheduleAsync(jobGroupId); - var nextSchedule = GetNextSchedule(esppSchedule, lastRun); + var nextSchedule = GetNextSchedule(esppSchedule, referenceDate); return nextSchedule; } @@ -209,8 +176,7 @@ namespace PARR.DAL.TransformServices return esppSchedule; } - - private DateTimeOffset GetNextDateRegularly(List values, DateTimeOffset lastRun) + private DateTimeOffset GetNextDateRegularly(List values, DateTimeOffset referenceDate) { var regDict = new Dictionary { @@ -233,93 +199,133 @@ namespace PARR.DAL.TransformServices }; - var regNum = regDict[values[0].Value.Value]; - var calcDay = lastRun.AddHours(regNum); + if (!regDict.TryGetValue(values[0].Value.Value, out int regNum)) + { + logger.LogError("Неизвестное значение: {Value}", values[0].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); + } + var calcDay = referenceDate.AddHours(regNum); //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и уходим в рекурсию if (calcDay < DateTimeOffset.UtcNow) - calcDay = GetNextDateRegularly(values, calcDay); - + calcDay = GetNextDateRegularly(values, calcDay.AddHours(regNum)); return calcDay; } - private DateTimeOffset GetNextDateWeekly(List values, DateTimeOffset lastRun) + private DateTimeOffset GetNextDateWeekly(List values, DateTimeOffset referenceDate) { - var dwNum = dwDict[values[0].Value.Value]; + if (!dwDict.TryGetValue(values[0].Value.Value, out int dwNum)) + { + logger.LogError("Неизвестное значение дня недели: {Value}", values[0].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); + } - var calcDay = new DateTimeOffset(lastRun.Year, lastRun.Month, lastRun.Day, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); - if (lastRun < DateTimeOffset.UtcNow) - calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, DateTimeOffset.UtcNow.Day, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); + var calcDay = referenceDate; + if (calcDay < DateTimeOffset.UtcNow) + calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, DateTimeOffset.UtcNow.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); if (((int)calcDay.DayOfWeek) != dwNum) - OffsetToDayOfWeek(dwNum, ref calcDay); + calcDay = OffsetToDayOfWeek(dwNum, calcDay); //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию if (calcDay < DateTimeOffset.UtcNow) - calcDay = GetNextDateWeekly(values, calcDay.AddDays(7));//а вот тут что-то новенькое return calcDay; } - private DateTimeOffset GetNextDateMonthly(List values, DateTimeOffset lastRun) + private DateTimeOffset GetNextDateMonthly(List values, DateTimeOffset referenceDate) { - int.TryParse(values[0].Value.Value, out int dayNum); + if (!int.TryParse(values[0].Value.Value, out int dayNum)) + { + logger.LogError("Неизвестное значение дня месяца: {Value}", values[0].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); + } - - var calcDay = new DateTimeOffset(lastRun.Year, lastRun.Month, dayNum, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); - if (lastRun < DateTimeOffset.UtcNow) - calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, dayNum, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); + var calcDay = new DateTimeOffset(referenceDate.Year, referenceDate.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + if (referenceDate < DateTimeOffset.UtcNow) + calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию if (calcDay < DateTimeOffset.UtcNow) - //todo: fixme - calcDay = GetNextDateMonthly(values, calcDay.AddMonths(1));//и вот тут что-то новенькое + calcDay = GetNextDateMonthly(values, calcDay.AddMonths(1)); return calcDay; } - private DateTimeOffset GetNextDateMonthly2(List values, DateTimeOffset lastRun) + private DateTimeOffset GetNextDateMonthly2(List values, DateTimeOffset referenceDate) { - var orderNum = orderDict[values[0].Value.Value]; - var dwNum = dwDict[values[1].Value.Value]; + if (!orderDict.TryGetValue(values[0].Value.Value, out int orderNum)) + { + logger.LogError("Неизвестное значение порядка дня: {Value}", values[0].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); + } - var calcDay = new DateTimeOffset(lastRun.Year, lastRun.Month, 1, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); - if (lastRun < DateTimeOffset.UtcNow) - calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, 1, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); + if (!dwDict.TryGetValue(values[1].Value.Value, out int dwNum)) + { + logger.LogError("Неизвестное значение дня недели: {Value}", values[1].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}"); + } + var startDay = referenceDate < DateTimeOffset.UtcNow + ? new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset) + : new DateTimeOffset(referenceDate.Year, referenceDate.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - List daysList = FindAllDaysInMonth(dwNum, ref calcDay); + var daysList = FindAllDaysInMonth(dwNum, startDay); - if (orderNum == 5)//5-последний + DateTimeOffset calcDay; + if (orderNum == 5) // Последний + { calcDay = daysList.Last(); + } else - calcDay = daysList[orderNum - 1]; + { + if (orderNum > daysList.Count) + { + logger.LogError("Запрошенный порядковый день недели ({OrderNum}) не существует в месяце {Month}/{Year}. Найдено только {Count} дней.", orderNum, startDay.Month, startDay.Year, daysList.Count); + throw new InvalidOperationException($"Запрошенный порядковый день недели ({orderNum}) не существует в месяце {startDay.Month}/{startDay.Year}. Найдено только {daysList.Count} дней."); + } + else + calcDay = daysList[orderNum - 1]; + } - //TODO: вот это повторяется от метода к методу - //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию - if (calcDay < DateTimeOffset.UtcNow) - calcDay = GetNextDateMonthly2(values, calcDay.AddMonths(1)); + // Если дата <= текущей, переходим на следующий месяц + if (calcDay <= DateTimeOffset.UtcNow) + return GetNextDateMonthly2(values, startDay.AddMonths(1)); return calcDay; } - - private DateTimeOffset GetNextDateAnnually(List values, DateTimeOffset lastRun) + + private DateTimeOffset GetNextDateAnnually(List values, DateTimeOffset referenceDate) { - var monthNum = monthDict[values[0].Value.Value]; - int.TryParse(values[1].Value.Value, out int dayNum); - var calcDay = new DateTimeOffset(lastRun.Year + 1, monthNum, dayNum, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); + if (!monthDict.TryGetValue(values[0].Value.Value, out int monthNum)) + { + logger.LogError("Неизвестное значение номера месяца: {Value}", values[0].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); + } + + if (!int.TryParse(values[1].Value.Value, out int dayNum)) + { + logger.LogError("Неизвестное значение дня месяца: {Value}", values[0].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); + } + + var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year; + + var calcDay = new DateTimeOffset( + year, monthNum, dayNum, + referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию @@ -330,7 +336,7 @@ namespace PARR.DAL.TransformServices } - private DateTimeOffset GetNextDateAnnually2(List values, DateTimeOffset lastRun) + private DateTimeOffset GetNextDateAnnually2(List values, DateTimeOffset referenceDate) { var monthADict = new Dictionary { @@ -348,33 +354,52 @@ namespace PARR.DAL.TransformServices { "Декабря", 12 } }; - var monthNum = monthADict[values[2].Value.Value]; - var orderNum = orderDict[values[0].Value.Value]; - var dwNum = dwDict[values[1].Value.Value]; + if (!monthADict.TryGetValue(values[2].Value.Value, out int monthNum)) + { + logger.LogError("Неизвестное значение номера месяца: {Value}", values[2].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[2].Value.Value}"); + } + if (!orderDict.TryGetValue(values[0].Value.Value, out int orderNum)) + { + logger.LogError("Неизвестное значение порядка дня: {Value}", values[0].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); + } + if (!dwDict.TryGetValue(values[1].Value.Value, out int dwNum)) + { + logger.LogError("Неизвестное значение дня недели: {Value}", values[1].Value.Value); + throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}"); + } - var calcDay = new DateTimeOffset(lastRun.Year + 1, monthNum, 1, lastRun.Hour, lastRun.Minute, lastRun.Second, lastRun.Offset); + var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year; - List daysList = FindAllDaysInMonth(dwNum, ref calcDay); + if (year == DateTimeOffset.UtcNow.Year && referenceDate.Month < DateTimeOffset.UtcNow.Month) + year += 1; + + var calcDay = new DateTimeOffset( + year, monthNum, 1, + referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + + List daysList = FindAllDaysInMonth(dwNum, calcDay); if (orderNum == 5)//5-последний calcDay = daysList.Last(); else - calcDay = daysList[orderNum]; + calcDay = daysList[orderNum - 1]; //TODO: вот это повторяется от метода к методу //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию if (calcDay < DateTimeOffset.UtcNow) - //calcDay = GetNextDateAnnually2(values, calcDay); calcDay = GetNextDateAnnually2(values, calcDay.AddYears(1)); return calcDay; } - private List FindAllDaysInMonth(int dwNum, ref DateTimeOffset calcDay) + private List FindAllDaysInMonth(int dwNum, DateTimeOffset startDay) { + var calcDay = startDay; if (((int)calcDay.DayOfWeek) != dwNum) - OffsetToDayOfWeek(dwNum, ref calcDay); + calcDay = OffsetToDayOfWeek(dwNum, calcDay); var daysList = new List(); var monthNum = calcDay.Month; @@ -388,14 +413,14 @@ namespace PARR.DAL.TransformServices } - private void OffsetToDayOfWeek(int dwNum, ref DateTimeOffset calcDay) + private DateTimeOffset OffsetToDayOfWeek(int dwNum, DateTimeOffset calcDay) { var daysOffset = dwNum - ((int)calcDay.DayOfWeek); if (daysOffset < 0) daysOffset = 7 + daysOffset; - calcDay = calcDay.AddDays(daysOffset); + return calcDay.AddDays(daysOffset); } @@ -406,15 +431,12 @@ namespace PARR.DAL.TransformServices switch (periodType) { case (DistributionPeriodTypeEnum.Day): - //return lastRun.AddDays(ParseInt(distributionPeriod)); nextRun = lastRun.AddDays(ParseInt(distributionPeriod)); break; case (DistributionPeriodTypeEnum.Month): - //return lastRun.AddMonths(ParseInt(distributionPeriod)); nextRun = lastRun.AddMonths(ParseInt(distributionPeriod)); break; case (DistributionPeriodTypeEnum.Year): - //return lastRun.AddYears(ParseInt(distributionPeriod)); nextRun = lastRun.AddYears(ParseInt(distributionPeriod)); break; }