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;
}