diff --git a/PARR.API.sln b/PARR.API.sln
index e8856eae..c96467c0 100644
--- a/PARR.API.sln
+++ b/PARR.API.sln
@@ -82,14 +82,12 @@ 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}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.Core.Tests", "PARR.DAL.Tests\PARR.Core.Tests.csproj", "{D61A7AB6-D785-477A-95D1-FBED049D6CE2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateUpdaterWorker", "PARR.TemplateUpdaterWorker\PARR.TemplateUpdaterWorker.csproj", "{27263750-9C80-4F9D-A550-B9D9C473B386}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.TemplateUpdater", "PARR.TemplateUpdater\PARR.TemplateUpdater.csproj", "{10905C04-A6E5-42BC-9804-8671D22C5D6E}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.DAL.IntegrationTests", "PARR.DAL.IntegrationTests\PARR.DAL.IntegrationTests.csproj", "{D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.Core", "PARR.Core\PARR.Core.csproj", "{E93DFC30-9886-486A-9F67-F782ABE0D1E0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PARR.Domain", "PARR.Domain\PARR.Domain.csproj", "{3740461A-7235-4C26-81F3-3977529D646D}"
@@ -256,10 +254,6 @@ Global
{10905C04-A6E5-42BC-9804-8671D22C5D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10905C04-A6E5-42BC-9804-8671D22C5D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10905C04-A6E5-42BC-9804-8671D22C5D6E}.Release|Any CPU.Build.0 = Release|Any CPU
- {D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D1C65DB8-77BC-00FD-5BA1-6C7303FD093C}.Release|Any CPU.Build.0 = Release|Any CPU
{E93DFC30-9886-486A-9F67-F782ABE0D1E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E93DFC30-9886-486A-9F67-F782ABE0D1E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E93DFC30-9886-486A-9F67-F782ABE0D1E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/PARR.Core/PARR.Core.csproj b/PARR.Core/PARR.Core.csproj
index a0e51996..a6ab4a72 100644
--- a/PARR.Core/PARR.Core.csproj
+++ b/PARR.Core/PARR.Core.csproj
@@ -1,22 +1,28 @@
-
- net9.0
- enable
- enable
-
+
+ net9.0
+ enable
+ enable
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/PARR.Core/Properties/AssemblyInfo.cs b/PARR.Core/Properties/AssemblyInfo.cs
deleted file mode 100644
index ec359717..00000000
--- a/PARR.Core/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("PARR.DAL.Tests")]
-[assembly: InternalsVisibleTo("PARR.DAL.IntegrationTests")]
diff --git a/PARR.Core/Services/UnitFilterService/Models/UnitFilterServiceOptions.cs b/PARR.Core/Services/UnitFilterService/Models/UnitFilterServiceOptions.cs
index fe798dc8..217dba05 100644
--- a/PARR.Core/Services/UnitFilterService/Models/UnitFilterServiceOptions.cs
+++ b/PARR.Core/Services/UnitFilterService/Models/UnitFilterServiceOptions.cs
@@ -2,6 +2,6 @@
{
internal class UnitFilterServiceOptions
{
- public int LoadBatchSize { get; set; } = 100;
+ public int LoadBatchSize { get; set; } = 1000;
}
}
diff --git a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs
index ee02368a..04a4d0af 100644
--- a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs
+++ b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs
@@ -16,7 +16,7 @@ namespace PARR.Core.Services.UnitFilterService;
internal class UnitFilterService : IUnitFilterService
{
#if DEBUG
- private readonly Guid debugTargetUnitId = Guid.Parse("3494fbec-adb9-4167-8619-b2a8b6b6e36a");
+ private readonly Guid debugTargetUnitId = Guid.Parse("a3b5f3e2-928e-48b7-b481-df4556e1ed32");
#endif
private const int DebugMaxUnitsToLog = 10;
diff --git a/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs b/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs
deleted file mode 100644
index c2811efe..00000000
--- a/PARR.DAL.Tests/DomainServices/Implementations/ShortcodesServiceTests.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Microsoft.Extensions.Logging;
-using Moq;
-using PARR.Core.Repositories.Interfaces.Job;
-using PARR.Core.Repositories.Interfaces.Unit;
-using PARR.Core.Services.Shortcodes;
-using PARR.Core.Services.UnitFilterService;
-using PARR.Domain.Settings;
-
-namespace PARR.DAL.Tests.DomainServices.Implementations
-{
- public class ShortcodesServiceTests
- {
- private readonly Mock> loggerMock = new();
- private readonly Mock settingsMock = new();
- private readonly Mock jobServiceMock = new();
- private readonly Mock unitServiceMock = new();
- private readonly Mock unitInValueServiceMock = new();
- private readonly Mock unitFieldServiceMock = new();
- private readonly Mock unitFilterServiceMock = new();
-
- [Fact]
- public async Task ApplyShortcodesAsync_WithEc_ShouldReplaceEc()
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/PARR.DAL.Tests/DomainServices/Implementations/UnitFilterServiceTests.cs b/PARR.DAL.Tests/DomainServices/Implementations/UnitFilterServiceTests.cs
deleted file mode 100644
index 51e71025..00000000
--- a/PARR.DAL.Tests/DomainServices/Implementations/UnitFilterServiceTests.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-namespace PARR.DAL.Tests
-{
- public class UnitFilterServiceTests //: IDisposable
- {
- // private readonly DbContextOptions _options;
- // private readonly DataContext _context;
- // private readonly UnitFilterService _service;
-
- // public UnitFilterServiceTests()
- // {
- // _options = new DbContextOptionsBuilder()
- // .UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}")
- // .EnableSensitiveDataLogging()
- // .Options;
-
- // _context = new DataContext(_options);
- // _context.Database.EnsureCreated();
- // TestDataGenerator.Seed(_context);
-
- // // Логгеры
- // var logger = Mock.Of>();
- // var jobServiceLogger = Mock.Of>();
- // var unitServiceLogger = Mock.Of>();
- // var unitInUnitServiceLogger = Mock.Of>();
- // var unitInValueServiceLogger = Mock.Of>();
-
- // // Реальные сервисы с правильными конструкторами
- // var jobService = new JobService(_context, jobServiceLogger);
- // var unitService = new UnitService(_context, unitServiceLogger);
- // var unitInUnitService = new UnitInUnitService(_context, unitInUnitServiceLogger);
- // var unitInValueService = new UnitInValueService(unitInValueServiceLogger, _context); // ← logger first!
-
- // _service = new UnitFilterService(
- // logger,
- // jobService,
- // unitService,
- // unitInUnitService,
- // unitInValueService
- // );
- // }
-
- // public void Dispose() => _context.Dispose();
-
- // [Fact]
- // public async Task GetUnitsIdByJobFilterAsync_WithExistingJobId_ShouldReturnExpectedUnits()
- // {
- // // Arrange
- // var jobId = _context.Jobs.First(j => j.Name == "TestJob_1").Id;
- // var expectedUnitIds = new[] { TestDataGenerator.UnitAId, TestDataGenerator.UnitBId };
-
- // // Act
- // var result = await _service.GetUnitsIdByJobFilterAsync(jobId, takeCount: 10);
-
- // // Assert
- // result.Should().NotBeNull()
- // .And.HaveCount(2)
- // .And.Contain(expectedUnitIds);
- // }
-
- // [Fact]
- // public async Task GetRelatedUnitNamesAsync_WithChildRelationshipFilter_ShouldReturnExpectedNames()
- // {
- // // Arrange
- // var jobId = _context.Jobs.First(j => j.Name == "TestJob_2").Id;
- // var unitId = TestDataGenerator.UnitWId; // Родитель
-
- // // rf.IsParent = false
- // // Ищем юниты, у которых ParentUnitId = UnitWId (т.е. это дети UnitWId)
- // // и у этих юнитов (детей) есть FieldId1 = "коммутатор"
- // // Это UnitXId и UnitYId -> имена: "СХД-КМТ-CISCO-MDS9148-1-ДВС", "СХД-КМТ-CISCO-MDS9148-3-ДВС"
-
- // // Act
- // var result = await _service.GetRelatedUnitNamesAsync(jobId, unitId);
-
- // // Assert
- // result.Should().Contain(new[] { "СХД-КМТ-CISCO-MDS9148-1-ДВС", "СХД-КМТ-CISCO-MDS9148-3-ДВС" });
- // }
- }
-}
\ No newline at end of file
diff --git a/PARR.DAL.Tests/PARR.Core.Tests.csproj b/PARR.DAL.Tests/PARR.Core.Tests.csproj
new file mode 100644
index 00000000..d793c578
--- /dev/null
+++ b/PARR.DAL.Tests/PARR.Core.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs b/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs
new file mode 100644
index 00000000..3dbf3c27
--- /dev/null
+++ b/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs
@@ -0,0 +1,616 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using PARR.Core.Common.Interfaces;
+using PARR.Core.Repositories.Interfaces.Job;
+using PARR.Core.Repositories.Interfaces.Unit;
+using PARR.DAL.Context;
+using PARR.Domain.Cache.Models;
+using PARR.Domain.Entities.Job;
+using PARR.Domain.Entities.Unit;
+using PARR.Domain.Enums;
+
+namespace PARR.Core.Tests.Services.UnitFilterService;
+
+public class UnitFilterServiceTests
+{
+ #region Helpers
+
+ ///
+ /// Создаёт тестовую сущность Unit с заданным значением поля.
+ /// Используется для подготовки данных в тестах фильтрации.
+ ///
+ /// Уникальный идентификатор юнита
+ /// Имя юнита для отладки
+ /// Идентификатор поля, к которому привязывается значение
+ /// Значение поля
+ private static Unit BuildTestUnit(Guid id, string name, Guid fieldId, string fieldValue)
+ {
+ var fieldVal = new UnitFieldValue { Id = Guid.NewGuid(), Value = fieldValue };
+ return new Unit
+ {
+ Id = id,
+ Name = name,
+ UnitValues = new List
+ {
+ new UnitInValue
+ {
+ UnitId = id,
+ FieldId = fieldId,
+ ValueId = fieldVal.Id,
+ Value = fieldVal
+ }
+ }
+ };
+ }
+
+ ///
+ /// Создаёт тестовый объект JobGroupType с кодом Umbrella.
+ /// Используется для тестирования логики групповых работ типа "зонтик".
+ ///
+ private static JobGroupType BuildTestJobGroupType() => new()
+ {
+ Id = Guid.NewGuid(),
+ Code = JobGroupTypesEnum.Group,
+ Name = "Grouped Group",
+ Description = "Test"
+ };
+
+ ///
+ /// Создаёт тестовый объект JobGroup с заполненными обязательными полями.
+ /// Используется для подготовки навигационных свойств в тестах.
+ ///
+ private static JobGroup BuildTestJobGroup() => new()
+ {
+ Id = Guid.NewGuid(),
+ GroupName = "Test Group",
+ ShortDescription = "Short",
+ FullDescription = "Full",
+ Solution = "Sol",
+ TemplateDuration = "0:00:00",
+ ReferenceDate = DateTimeOffset.UtcNow,
+ GroupTypeId = Guid.NewGuid(),
+ ScheduleExcludeTypeId = Guid.NewGuid(),
+ GroupType = BuildTestJobGroupType()
+ };
+
+ ///
+ /// Создаёт тестовый объект JobAutoControl с отключённым автоконтролем.
+ /// Используется для заполнения обязательного свойства в сущности Job.
+ ///
+ private static JobAutoControl BuildTestAutoControl(Guid jobId) => new()
+ {
+ JobId = jobId,
+ IsEnable = false
+ };
+
+ ///
+ /// Контейнер для моков репозиториев.
+ /// Упрощает передачу зависимостей в метод создания сервиса.
+ ///
+ private class RepositoryMocks
+ {
+ public Mock Unit { get; set; }
+ public Mock UnitInUnit { get; set; }
+ public Mock UnitInValue { get; set; }
+ public Mock UnitField { get; set; }
+ public Mock Job { get; set; }
+ public Mock Cache { get; set; }
+ }
+
+ ///
+ /// Настраивает базовые моки репозиториев с использованием InMemory-контекста.
+ /// Возвращает объект с подготовленными моками для повторного использования в тестах.
+ ///
+ /// Экземпляр DataContext с тестовыми данными
+ private static RepositoryMocks ArrangeRepositoryMocks(DataContext context)
+ {
+ var unitRepoMock = new Mock();
+ unitRepoMock.Setup(r => r.Get()).Returns(context.Units.AsQueryable());
+ unitRepoMock.Setup(r => r.GetInitialUnitIds(It.IsAny()))
+ .Returns(context.Units.Select(u => u.Id).AsQueryable());
+
+ var unitInUnitRepoMock = new Mock();
+ unitInUnitRepoMock.Setup(r => r.Get()).Returns(context.UnitInUnits.AsQueryable());
+
+ var unitInValueRepoMock = new Mock();
+ // Единый подход: используем контекст для поддержки IAsyncQueryProvider
+ // Это гарантирует корректную работу .ToListAsync() внутри сервиса
+ unitInValueRepoMock.Setup(r => r.GetMatchingTargetIds(It.IsAny(), It.IsAny()))
+ .Returns((Guid fId, string mask) =>
+ context.UnitInValues
+ .AsNoTracking()
+ .Where(uv => uv.FieldId == fId && uv.Value != null)
+ .Select(uv => uv.UnitId)
+ .Distinct()
+ .AsQueryable());
+
+ var unitFieldRepoMock = new Mock();
+ unitFieldRepoMock.Setup(r => r.Get()).Returns(new List().AsQueryable());
+
+ var jobRepoMock = new Mock();
+ jobRepoMock.Setup(r => r.Get()).Returns(context.Jobs.AsQueryable());
+
+ var cacheMock = new Mock();
+ cacheMock.Setup(r => r.GetCachedDataAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync((UnitFilterIds)null!);
+
+ return new RepositoryMocks
+ {
+ Unit = unitRepoMock,
+ UnitInUnit = unitInUnitRepoMock,
+ UnitInValue = unitInValueRepoMock,
+ UnitField = unitFieldRepoMock,
+ Job = jobRepoMock,
+ Cache = cacheMock
+ };
+ }
+
+ ///
+ /// Создаёт экземпляр тестируемого сервиса (SUT — System Under Test).
+ /// Инкапсулирует логику конструктора для упрощения тестов.
+ ///
+ /// Подготовленные моки репозиториев
+ /// Экземпляр логгера для сервиса
+ private static PARR.Core.Services.UnitFilterService.UnitFilterService CreateSut(
+ RepositoryMocks mocks,
+ ILogger logger)
+ {
+ var serviceOptions = Options.Create(new PARR.Core.Services.UnitFilterService.Models.UnitFilterServiceOptions { LoadBatchSize = 100 });
+
+ return new PARR.Core.Services.UnitFilterService.UnitFilterService(
+ logger,
+ mocks.Job.Object,
+ mocks.Unit.Object,
+ mocks.UnitInUnit.Object,
+ mocks.UnitInValue.Object,
+ mocks.Cache.Object,
+ serviceOptions,
+ mocks.UnitField.Object
+ );
+ }
+
+ #endregion
+
+
+ ///
+ /// Проверяет, что юниты, не прошедшие фильтрацию по полю (FieldFilter)
+ /// или по связям (RelationshipFilter), корректно исключаются из результата.
+ ///
+ [Fact]
+ public async Task GetUnitsByJobFilterAsync_UnitsWithInvalidFieldOrRelationshipValue_AreExcluded()
+ {
+ // === Arrange ===
+ var dbOptions = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
+ .Options;
+ using var context = new DataContext(dbOptions);
+
+ var jobId = Guid.NewGuid();
+ var fieldId = Guid.NewGuid();
+ var validParentId = Guid.NewGuid();
+ var wrongParentId = Guid.NewGuid();
+
+ var unitPassesAll = Guid.NewGuid();
+ var unitFailsFieldFilter = Guid.NewGuid();
+ var unitFailsRelationshipFilter = Guid.NewGuid();
+
+ // Подготовка тестовых данных: юниты
+ context.Units.AddRange(
+ BuildTestUnit(unitPassesAll, "Unit_PassesAll", fieldId, "Accepted Value"),
+ BuildTestUnit(unitFailsFieldFilter, "Unit_FailsField", fieldId, "Rejected Value"),
+ BuildTestUnit(unitFailsRelationshipFilter, "Unit_FailsRel", fieldId, "Accepted Value"),
+ BuildTestUnit(validParentId, "Parent_Valid", fieldId, "Valid Parent"),
+ BuildTestUnit(wrongParentId, "Parent_Wrong", fieldId, "Wrong Parent")
+ );
+
+ // Подготовка тестовых данных: связи между юнитами
+ context.UnitInUnits.AddRange(
+ new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitPassesAll, DateCreated = DateTimeOffset.UtcNow },
+ new UnitInUnit { ParentUnitId = wrongParentId, ChildUnitId = unitFailsRelationshipFilter, DateCreated = DateTimeOffset.UtcNow }
+ );
+
+ await context.SaveChangesAsync();
+
+ // Формируем тестовый объект Job с фильтрами
+ var job = new Job
+ {
+ Id = jobId,
+ Name = "Test Job",
+ WorkName = "Test Work",
+ TemplateNameMask = "TestMask",
+ WorkGroupMask = "TestGroup",
+ ResponseAreaMask = "TestArea",
+ GroupId = Guid.NewGuid(),
+ TnkId = Guid.NewGuid(),
+ MinValueRelationships = 1,
+ MaxValueRelationships = 10,
+ IsParentRelationships = true,
+ AutoControl = BuildTestAutoControl(jobId),
+ Group = BuildTestJobGroup(),
+ UnitFilters = new List
+ {
+ new JobUnitFilter
+ {
+ Id = Guid.NewGuid(),
+ JobId = jobId,
+ UnitFilter = "%",
+ FieldFilters = new List
+ {
+ new JobFieldFilter { Id = Guid.NewGuid(), FieldId = fieldId, ValueMask = "Accepted%", IsInverse = false }
+ },
+ RelationshipFilters = new List
+ {
+ new JobRelationshipFilter { UnitFilterId = Guid.NewGuid(), FieldId = fieldId, ValueMask = "%", IsParent = true, IsInverse = false, IsFullMatch = false }
+ }
+ }
+ }
+ };
+
+ // Настройка моков репозиториев
+ var mocks = ArrangeRepositoryMocks(context);
+
+ // Переопределяем мок фильтрации по полю: в реальном коде используется EF.Functions.ILike,
+ // в тесте заменяем на прямое сравнение строк для предсказуемости
+ mocks.Unit.Setup(r => r.GetUnitByFieldAndValue(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns((IQueryable q, Guid fId, string mask, bool inv) =>
+ q.Where(u => u.UnitValues != null && u.UnitValues.Any(v => v.FieldId == fId && v.Value != null && v.Value.Value == "Accepted Value")));
+
+ // ИСПРАВЛЕНО: используем запрос к контексту вместо массива.
+ // Массивный AsQueryable() не поддерживает IAsyncQueryProvider, что вызывает крах при вызове .ToListAsync() внутри сервиса.
+ // Запрос к InMemory DbSet гарантирует корректную асинхронную материализацию.
+ mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(fieldId, It.IsAny()))
+ .Returns(context.Units.Where(u => u.Id == validParentId).Select(u => u.Id).AsQueryable());
+
+ var logger = NullLoggerFactory.Instance.CreateLogger();
+ var service = CreateSut(mocks, logger);
+
+ // === Act ===
+ var result = await service.GetUnitsByJobFilterAsync(job);
+
+ // === Assert ===
+ var resultIds = result.Select(u => u.Id).ToList();
+
+ Assert.Contains(unitPassesAll, resultIds);
+ Assert.DoesNotContain(unitFailsFieldFilter, resultIds);
+ Assert.DoesNotContain(unitFailsRelationshipFilter, resultIds);
+
+ Assert.Single(result);
+ var dto = result.First();
+ Assert.Equal("Accepted Value", dto.Values.FirstOrDefault()?.Value);
+ Assert.Single(dto.Parents);
+ Assert.Equal(validParentId, dto.Parents.First().UnitId);
+ }
+
+ ///
+ /// Проверяет, что юниты исключаются, если их единственный родительский юнит
+ /// содержит запрещённое значение тега (сценарий с IsInverse = true).
+ ///
+ [Fact]
+ public async Task GetUnitsByJobFilterAsync_ParentWithForbiddenTag_UnitIsExcluded()
+ {
+ // === Arrange ===
+ var dbOptions = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
+ .Options;
+ using var context = new DataContext(dbOptions);
+
+ var jobId = Guid.NewGuid();
+ var tagFieldId = Guid.NewGuid();
+ var validParentId = Guid.NewGuid();
+ var invalidParentId = Guid.NewGuid();
+ var unitWithValidParent = Guid.NewGuid();
+ var unitWithInvalidParent = Guid.NewGuid();
+
+ // Подготовка тестовых данных: юниты
+ context.Units.AddRange(
+ BuildTestUnit(validParentId, "Parent_Valid", tagFieldId, "ОТВ.ЭК"),
+ BuildTestUnit(invalidParentId, "Parent_Invalid", tagFieldId, "ПАРР-РРПТК-ОТВ.ЭК"),
+ BuildTestUnit(unitWithValidParent, "Unit_Valid", Guid.NewGuid(), "Val"),
+ BuildTestUnit(unitWithInvalidParent, "Unit_Invalid", Guid.NewGuid(), "Val")
+ );
+
+ // Подготовка тестовых данных: связи между юнитами
+ context.UnitInUnits.AddRange(
+ new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitWithValidParent, DateCreated = DateTimeOffset.UtcNow },
+ new UnitInUnit { ParentUnitId = invalidParentId, ChildUnitId = unitWithInvalidParent, DateCreated = DateTimeOffset.UtcNow }
+ );
+
+ await context.SaveChangesAsync();
+
+ // Формируем тестовый объект Job с настройкой IsInverse = true
+ // IsInverse = true означает: исключить родителей, которые СОВПАДАЮТ с маской
+ var job = new Job
+ {
+ Id = jobId,
+ Name = "Test Job Inverse",
+ WorkName = "Test Work",
+ TemplateNameMask = "TestMask",
+ WorkGroupMask = "TestGroup",
+ ResponseAreaMask = "TestArea",
+ GroupId = Guid.NewGuid(),
+ TnkId = Guid.NewGuid(),
+ MinValueRelationships = 1,
+ MaxValueRelationships = 10,
+ IsParentRelationships = true,
+ AutoControl = BuildTestAutoControl(jobId),
+ Group = BuildTestJobGroup(),
+ UnitFilters = new List
+ {
+ new JobUnitFilter
+ {
+ Id = Guid.NewGuid(),
+ JobId = jobId,
+ UnitFilter = "%",
+ FieldFilters = new List(),
+ RelationshipFilters = new List
+ {
+ new JobRelationshipFilter
+ {
+ UnitFilterId = Guid.NewGuid(),
+ FieldId = tagFieldId,
+ ValueMask = "%ПАРР-РРПТК-ОТВ.ЭК%",
+ IsParent = true,
+ IsInverse = true,
+ IsFullMatch = false
+ }
+ }
+ }
+ }
+ };
+
+ // Настройка моков репозиториев
+ var mocks = ArrangeRepositoryMocks(context);
+
+ // Переопределяем мок: возвращаем только родителей с запрещённым тегом.
+ // В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
+ // IsInverse = true в сервисе инвертирует результат, поэтому эти родители будут исключены.
+ mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny()))
+ .Returns((Guid fId, string mask) =>
+ context.UnitInValues
+ .AsNoTracking()
+ .Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("ПАРР-РРПТК-ОТВ.ЭК", StringComparison.OrdinalIgnoreCase))
+ .Select(uv => uv.UnitId)
+ .Distinct()
+ .AsQueryable());
+
+ var logger = NullLoggerFactory.Instance.CreateLogger();
+ var service = CreateSut(mocks, logger);
+
+ // === Act ===
+ var result = await service.GetUnitsByJobFilterAsync(job);
+
+ // === Assert ===
+ var resultIds = result.Select(u => u.Id).ToList();
+
+ Assert.Contains(unitWithValidParent, resultIds);
+ Assert.DoesNotContain(unitWithInvalidParent, resultIds);
+ }
+
+ ///
+ /// Проверяет логику IsFullMatch = true: юнит исключается,
+ /// если хотя бы один из его родительских юнитов не соответствует фильтру.
+ ///
+ [Fact]
+ public async Task GetUnitsByJobFilterAsync_IsFullMatch_UnitWithAnyNonMatchingParent_IsExcluded()
+ {
+ // === Arrange ===
+ var dbOptions = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
+ .Options;
+ using var context = new DataContext(dbOptions);
+
+ var jobId = Guid.NewGuid();
+ var checkFieldId = Guid.NewGuid();
+ var parentGoodId = Guid.NewGuid();
+ var parentBadId = Guid.NewGuid();
+ var unitMixedId = Guid.NewGuid();
+ var unitCleanId = Guid.NewGuid();
+
+ // Подготовка тестовых данных: юниты
+ context.Units.AddRange(
+ BuildTestUnit(parentGoodId, "Parent_Good", checkFieldId, "Good Parent"),
+ BuildTestUnit(parentBadId, "Parent_Bad", checkFieldId, "Bad Parent"),
+ BuildTestUnit(unitMixedId, "Unit_Mixed", Guid.NewGuid(), "Mixed"),
+ BuildTestUnit(unitCleanId, "Unit_Clean", Guid.NewGuid(), "Clean")
+ );
+
+ // Подготовка тестовых данных: связи между юнитами
+ context.UnitInUnits.AddRange(
+ new UnitInUnit { ParentUnitId = parentGoodId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
+ new UnitInUnit { ParentUnitId = parentBadId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
+ new UnitInUnit { ParentUnitId = parentGoodId, ChildUnitId = unitCleanId, DateCreated = DateTimeOffset.UtcNow }
+ );
+
+ await context.SaveChangesAsync();
+
+ // Формируем тестовый объект Job с настройкой IsFullMatch = true
+ // IsFullMatch = true означает: ВСЕ родители должны соответствовать фильтру
+ var job = new Job
+ {
+ Id = jobId,
+ Name = "Test Job FullMatch",
+ WorkName = "Test Work",
+ TemplateNameMask = "TestMask",
+ WorkGroupMask = "TestGroup",
+ ResponseAreaMask = "TestArea",
+ GroupId = Guid.NewGuid(),
+ TnkId = Guid.NewGuid(),
+ MinValueRelationships = 1,
+ MaxValueRelationships = 10,
+ IsParentRelationships = true,
+ AutoControl = BuildTestAutoControl(jobId),
+ Group = BuildTestJobGroup(),
+ UnitFilters = new List
+ {
+ new JobUnitFilter
+ {
+ Id = Guid.NewGuid(),
+ JobId = jobId,
+ UnitFilter = "%",
+ FieldFilters = new List(),
+ RelationshipFilters = new List
+ {
+ new JobRelationshipFilter
+ {
+ UnitFilterId = Guid.NewGuid(),
+ FieldId = checkFieldId,
+ ValueMask = "%Good%",
+ IsParent = true,
+ IsInverse = false,
+ IsFullMatch = true
+ }
+ }
+ }
+ }
+ };
+
+ // Настройка моков репозиториев
+ var mocks = ArrangeRepositoryMocks(context);
+
+ // Переопределяем мок: возвращаем только родителей, соответствующих маске "Good".
+ // В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
+ mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(checkFieldId, It.IsAny()))
+ .Returns((Guid fId, string mask) =>
+ context.UnitInValues
+ .AsNoTracking()
+ .Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("Good", StringComparison.OrdinalIgnoreCase))
+ .Select(uv => uv.UnitId)
+ .Distinct()
+ .AsQueryable());
+
+ var logger = NullLoggerFactory.Instance.CreateLogger();
+ var service = CreateSut(mocks, logger);
+
+ // === Act ===
+ var result = await service.GetUnitsByJobFilterAsync(job);
+
+ // === Assert ===
+ var resultIds = result.Select(u => u.Id).ToList();
+
+ Assert.Contains(unitCleanId, resultIds);
+ Assert.DoesNotContain(unitMixedId, resultIds);
+ }
+
+
+ ///
+ /// Проверяет комбинацию флагов: IsParent=true, IsInverse=true, IsFullMatch=true.
+ /// Логика: юнит проходит, только если НИ ОДИН из его родителей не содержит запрещённое значение.
+ ///
+ [Fact]
+ public async Task GetUnitsByJobFilterAsync_IsParentAndInverseAndFullMatch_UnitWithAnyForbiddenParent_IsExcluded()
+ {
+ // === Arrange ===
+ var dbOptions = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
+ .Options;
+ using var context = new DataContext(dbOptions);
+
+ var jobId = Guid.NewGuid();
+ var tagFieldId = Guid.NewGuid();
+ var parentForbiddenId = Guid.NewGuid(); // Родитель с запрещённым тегом
+ var parentCleanId = Guid.NewGuid(); // Родитель без запрещённого тега
+ var unitMixedId = Guid.NewGuid(); // Юнит с одним "плохим" и одним "хорошим" родителем
+ var unitAllCleanId = Guid.NewGuid(); // Юнит только с "хорошими" родителями
+
+ // Подготовка тестовых данных: юниты
+ context.Units.AddRange(
+ BuildTestUnit(parentForbiddenId, "Parent_Forbidden", tagFieldId, "FORBIDDEN-TAG"),
+ BuildTestUnit(parentCleanId, "Parent_Clean", tagFieldId, "CLEAN-TAG"),
+ BuildTestUnit(unitMixedId, "Unit_Mixed", Guid.NewGuid(), "Mixed"),
+ BuildTestUnit(unitAllCleanId, "Unit_AllClean", Guid.NewGuid(), "AllClean")
+ );
+
+ // Подготовка тестовых данных: связи
+ // unitMixed имеет обоих родителей — один с запрещённым тегом, один без
+ // unitAllClean имеет только "чистого" родителя
+ context.UnitInUnits.AddRange(
+ new UnitInUnit { ParentUnitId = parentForbiddenId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
+ new UnitInUnit { ParentUnitId = parentCleanId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow },
+ new UnitInUnit { ParentUnitId = parentCleanId, ChildUnitId = unitAllCleanId, DateCreated = DateTimeOffset.UtcNow }
+ );
+
+ await context.SaveChangesAsync();
+
+ // Формируем тестовый объект Job с комбинацией флагов:
+ // IsParent = true: проверяем родительские связи
+ // IsInverse = true: исключаем родителей, которые СОВПАДАЮТ с маской
+ // IsFullMatch = true: ВСЕ родители должны пройти фильтр (ни один не должен совпасть с маской)
+ var job = new Job
+ {
+ Id = jobId,
+ Name = "Test Job Combined Flags",
+ WorkName = "Test Work",
+ TemplateNameMask = "TestMask",
+ WorkGroupMask = "TestGroup",
+ ResponseAreaMask = "TestArea",
+ GroupId = Guid.NewGuid(),
+ TnkId = Guid.NewGuid(),
+ MinValueRelationships = 1,
+ MaxValueRelationships = 10,
+ IsParentRelationships = true,
+ AutoControl = BuildTestAutoControl(jobId),
+ Group = BuildTestJobGroup(),
+ UnitFilters = new List
+ {
+ new JobUnitFilter
+ {
+ Id = Guid.NewGuid(),
+ JobId = jobId,
+ UnitFilter = "%",
+ FieldFilters = new List(),
+ RelationshipFilters = new List
+ {
+ new JobRelationshipFilter
+ {
+ UnitFilterId = Guid.NewGuid(),
+ FieldId = tagFieldId,
+ ValueMask = "%FORBIDDEN%",
+ IsParent = true,
+ IsInverse = true,
+ IsFullMatch = true
+ }
+ }
+ }
+ }
+ };
+
+ // Настройка моков репозиториев
+ var mocks = ArrangeRepositoryMocks(context);
+
+ // Переопределяем мок: возвращаем только родителей, содержащих "FORBIDDEN".
+ // В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты.
+ // Из-за IsInverse=true эти родители будут исключены из "валидных".
+ // Из-за IsFullMatch=true юнит пройдёт, только если ВСЕ его родители валидны.
+ mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny()))
+ .Returns((Guid fId, string mask) =>
+ context.UnitInValues
+ .AsNoTracking()
+ .Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("FORBIDDEN", StringComparison.OrdinalIgnoreCase))
+ .Select(uv => uv.UnitId)
+ .Distinct()
+ .AsQueryable());
+
+ var logger = NullLoggerFactory.Instance.CreateLogger();
+ var service = CreateSut(mocks, logger);
+
+ // === Act ===
+ var result = await service.GetUnitsByJobFilterAsync(job);
+
+ // === Assert ===
+ var resultIds = result.Select(u => u.Id).ToList();
+
+ // unitAllCleanId должен остаться: у него один родитель, и он не содержит запрещённый тег
+ Assert.Contains(unitAllCleanId, resultIds);
+
+ // unitMixedId должен быть исключён: у него есть родитель с запрещённым тегом.
+ // Логика:
+ // 1. GetMatchingTargetIds возвращает {parentForbiddenId}
+ // 2. IsInverse=true → валидные родители = все.Кроме({parentForbiddenId}) = {parentCleanId}
+ // 3. IsFullMatch=true → проверяем: все родители {parentForbiddenId, parentCleanId} входят в {parentCleanId}? Нет.
+ // 4. Юнит исключается.
+ Assert.DoesNotContain(unitMixedId, resultIds);
+ }
+}
\ No newline at end of file
diff --git a/PARR.DAL.Tests/TestDataGenerator.cs b/PARR.DAL.Tests/TestDataGenerator.cs
deleted file mode 100644
index fa97f333..00000000
--- a/PARR.DAL.Tests/TestDataGenerator.cs
+++ /dev/null
@@ -1,253 +0,0 @@
-using PARR.DAL.Context;
-using PARR.Domain.Entities.Job;
-using PARR.Domain.Entities.Unit;
-using PARR.Domain.Enums;
-
-namespace PARR.DAL.Tests
-{
- public static class TestDataGenerator
- {
- // --- Unit IDs ---
- public static readonly Guid UnitAId = Guid.Parse("11111111-1111-1111-1111-111111111111");
- public static readonly Guid UnitBId = Guid.Parse("00000000-0000-1111-1111-111111111111");
- public static readonly Guid UnitCId = Guid.Parse("22222222-2222-2222-2222-222222222222");
- public static readonly Guid UnitDId = Guid.Parse("33333333-3333-3333-3333-333333333333");
- public static readonly Guid UnitWId = Guid.Parse("44444444-4444-4444-4444-444444444444");
- public static readonly Guid UnitXId = Guid.Parse("55555555-5555-5555-5555-555555555555");
- public static readonly Guid UnitYId = Guid.Parse("66666666-6666-6666-6666-666666666666");
- public static readonly Guid UnitZId = Guid.Parse("77777777-7777-7777-7777-777777777777");
-
- // --- Field IDs (для обхода составного ключа UnitFilterId+FieldId) ---
- public static readonly Guid FieldId1 = Guid.Parse("08b42860-09a7-4a1f-aa4f-0290e86df883");//ТИП_ЭК
- public static readonly Guid FieldId2 = Guid.Parse("5b2d3f5b-a9f3-4a6b-9142-70ef4357ac37"); //неуникальный
- public static readonly Guid FieldId3 = Guid.Parse("5f3c7b8b-583d-411b-b9bd-1e045ddc5a8e");//ПОДКАТЕГОРИЯ_ЭК
- public static readonly Guid FieldId4 = Guid.Parse("3e9ac5aa-531b-4ffc-8f93-694d00aa92a0");//активный
-
- // --- Field Value IDs ---
- public static readonly Guid FieldValueId1 = Guid.Parse("fe18edcd-ab6a-4f3e-b7bc-e54aad9ecc0d"); // система хранения данных (схд) field3
- public static readonly Guid FieldValueId2 = Guid.Parse("f385b22c-42c3-4db9-bd41-b829603cc2d9"); // 1 field2+field4
- public static readonly Guid FieldValueId3 = Guid.Parse("2badc139-69cb-4d52-830a-1b3c6a9faf9d"); // 0 field2+field4
- public static readonly Guid FieldValueId4 = Guid.Parse("fde68ef7-a07e-4c10-872c-cd98c3d1c30b"); // коммутатор
- public static readonly Guid FieldValueId5 = Guid.Parse("9f169862-3f78-49f2-b967-13b5b2f56231"); // серверное оборудование
- public static readonly Guid FieldValueId6 = Guid.Parse("075b6fad-d24f-40f4-aa0e-83a4cfc46bbf"); // стойка цвк
- public static readonly Guid FieldValueId7 = Guid.Parse("9679afd8-8004-4ce2-bd6f-9005b037ef96"); // схд
-
- internal static void Seed(DataContext context)
- {
- if (context.Units.Any()) return;
-
- var now = DateTimeOffset.UtcNow;
-
- // 1. Значения полей (остаются те же)
- var fieldValues = new[]
- {
- new UnitFieldValue { Id = FieldValueId1, Value = "система хранения данных (схд)", DateCreated = now },
- new UnitFieldValue { Id = FieldValueId2, Value = "1", DateCreated = now },
- new UnitFieldValue { Id = FieldValueId3, Value = "0", DateCreated = now },
- new UnitFieldValue { Id = FieldValueId4, Value = "коммутатор", DateCreated = now },
- };
- context.UnitFieldValues.AddRange(fieldValues);
-
- // 2. Unit'ы
- var units = new[]
- {
- new Unit { Id = UnitAId, Name = "СХД-КМТ-DVGD-MDS9148-1-ДВС", DateCreated = now },
- new Unit { Id = UnitBId, Name = "СХД-КМТ-DVGD-MDS9148-2-ДВС", DateCreated = now },
- new Unit { Id = UnitCId, Name = "СРВ-СТОЙКА-СТОЙКА-ХИВЦ-4.0-ДВС", DateCreated = now },
- new Unit { Id = UnitDId, Name = "СХД-VSP-E590-01-ДВС", DateCreated = now },
- new Unit { Id = UnitWId, Name = "СХД-КМТ-SAN1_FAB_1-ДВС", DateCreated = now },
- new Unit { Id = UnitXId, Name = "СХД-КМТ-CISCO-MDS9148-1-ДВС", DateCreated = now },
- new Unit { Id = UnitYId, Name = "СХД-КМТ-CISCO-MDS9148-3-ДВС", DateCreated = now },
- };
- context.Units.AddRange(units);
-
- // 3. UnitInUnit (связи)
- var unitInUnits = new[]
- {
- new UnitInUnit { ParentUnitId = UnitAId, ChildUnitId = UnitCId, DateCreated = now },
- new UnitInUnit { ParentUnitId = UnitBId, ChildUnitId = UnitCId, DateCreated = now },
- new UnitInUnit { ParentUnitId = UnitDId, ChildUnitId = UnitAId, DateCreated = now },
- new UnitInUnit { ParentUnitId = UnitDId, ChildUnitId = UnitBId, DateCreated = now },
- new UnitInUnit { ParentUnitId = UnitWId, ChildUnitId = UnitXId, DateCreated = now },
- new UnitInUnit { ParentUnitId = UnitWId, ChildUnitId = UnitYId, DateCreated = now }
- };
- context.UnitInUnits.AddRange(unitInUnits);
-
- // 4. UnitInValue
- var unitInValues = new List
- {
- // Основные (FieldId)
- new UnitInValue { UnitId = UnitAId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
- new UnitInValue { UnitId = UnitAId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
- new UnitInValue { UnitId = UnitAId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
- new UnitInValue { UnitId = UnitAId, FieldId = FieldId2, ValueId = FieldValueId3, DateCreated = now },
- new UnitInValue { UnitId = UnitBId, FieldId = FieldId2, ValueId = FieldValueId3, DateCreated = now },
- new UnitInValue { UnitId = UnitBId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
- new UnitInValue { UnitId = UnitBId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
- new UnitInValue { UnitId = UnitBId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
- new UnitInValue { UnitId = UnitCId, FieldId = FieldId3, ValueId = FieldValueId5, DateCreated = now },
- new UnitInValue { UnitId = UnitCId, FieldId = FieldId1, ValueId = FieldValueId6, DateCreated = now },
- new UnitInValue { UnitId = UnitCId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
- new UnitInValue { UnitId = UnitDId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
- new UnitInValue { UnitId = UnitDId, FieldId = FieldId1, ValueId = FieldValueId7, DateCreated = now },
- new UnitInValue { UnitId = UnitDId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
- new UnitInValue { UnitId = UnitWId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
- new UnitInValue { UnitId = UnitWId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
- new UnitInValue { UnitId = UnitWId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
- new UnitInValue { UnitId = UnitWId, FieldId = FieldId2, ValueId = FieldValueId2, DateCreated = now },
- new UnitInValue { UnitId = UnitXId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
- new UnitInValue { UnitId = UnitXId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
- new UnitInValue { UnitId = UnitXId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
- new UnitInValue { UnitId = UnitYId, FieldId = FieldId3, ValueId = FieldValueId1, DateCreated = now },
- new UnitInValue { UnitId = UnitYId, FieldId = FieldId1, ValueId = FieldValueId4, DateCreated = now },
- new UnitInValue { UnitId = UnitYId, FieldId = FieldId4, ValueId = FieldValueId2, DateCreated = now },
-
- };
- context.UnitInValues.AddRange(unitInValues);
-
- // 5. JobGroupType и Group
- var groupType = context.JobGroupTypes
- .FirstOrDefault(t => t.Code == JobGroupTypesEnum.Simple)
- ?? throw new InvalidOperationException("JobGroupType 'Simple' не найден в контексте.");
-
- var group = new JobGroup
- {
- Id = Guid.NewGuid(),
- GroupTypeId = groupType.Id,
- GroupType = groupType,
- GroupName = "Группа",
- ShortDescription = "%ТНК-КРАТКО% %ЭК%",
- FullDescription = "Подробно:\n%СВЯЗИ%",
- Solution = "Решение",
- TemplateDuration = "540 00:00:00",
- ReferenceDate = now,
- IsAutoDistributionEnabled = false,
- IsAgent = false,
- DateCreated = now
- };
- context.JobGroups.Add(group);
-
- // 6. Jobs
- var jobs = new List();
-
- // --- TestJob ---
- var job1 = new Job
- {
- Id = Guid.NewGuid(),
- Name = "TestJob_1",
- WorkName = "Работа",
- TemplateNameMask = "%ЭК%",
- WorkGroupMask = "%ГРУППА_РАБОТ%",
- GroupId = group.Id,
- TnkId = Guid.NewGuid(),
- Group = group,
- DateCreated = now,
- ResponseAreaMask = "%ЗО_РГ%"
- };
- var unitFilter1Id = Guid.NewGuid();
- job1.UnitFilters = new List
- {
- new JobUnitFilter
- {
- Id = unitFilter1Id,
- UnitFilter = "СХД-%",
- JobId = job1.Id,
- Job = job1,
- DateCreated = now,
- FieldFilters = new List
- {
- new JobFieldFilter
- {
- UnitFilterId = unitFilter1Id,
- FieldId = FieldId1,
- ValueMask="коммутатор"
- },
- new JobFieldFilter
- {
- UnitFilterId = unitFilter1Id,
- FieldId = FieldId4,
- ValueMask="1"
- },
- new JobFieldFilter
- {
- UnitFilterId = unitFilter1Id,
- FieldId = FieldId2,
- ValueMask="0"
- },
- },
- RelationshipFilters = new List
- {
- new JobRelationshipFilter
- {
- IsParent = true,
- IsInverse = true,
- IsFullMatch = true,
- FieldId = FieldId1,
- ValueMask = "коммутатор",
- UnitFilterId = unitFilter1Id
- }
- }
- }
- };
- jobs.Add(job1);
-
- // --- TestJob_2 (с IsParent = false для дочерних связей) ---
- var job2 = new Job
- {
- Id = Guid.NewGuid(),
- Name = "TestJob_2",
- WorkName = "Работа2",
- TemplateNameMask = "%ЭК%",
- WorkGroupMask = "%ГРУППА_РАБОТ%",
- GroupId = group.Id,
- TnkId = Guid.NewGuid(),
- Group = group,
- DateCreated = now,
- ResponseAreaMask = "%ЗО_РГ%"
- };
- var unitFilter2Id = Guid.NewGuid();
- job2.UnitFilters = new List
- {
- new JobUnitFilter
- {
- Id = unitFilter2Id,
- UnitFilter = "СХД-%",
- JobId = job2.Id,
- Job = job2,
- DateCreated = now,
- FieldFilters = new List
- {
- new JobFieldFilter
- {
- UnitFilterId = unitFilter2Id,
- FieldId = FieldId1,
- ValueMask="коммутатор"
- },
- new JobFieldFilter
- {
- UnitFilterId = unitFilter2Id,
- FieldId = FieldId4,
- ValueMask="1"
- },
- },
- RelationshipFilters = new List
- {
- new JobRelationshipFilter
- {
- IsParent = false, // <-- Новый фильтр: ищем дочерние юниты
- IsInverse = false, // <-- Простой фильтр
- IsFullMatch = false, // <-- Простой фильтр
- FieldId = FieldId1,
- ValueMask = "коммутатор",
- UnitFilterId = unitFilter2Id
- }
- }
- }
- };
- jobs.Add(job2);
-
- context.Jobs.AddRange(jobs);
- context.SaveChanges();
- }
- }
-}
\ No newline at end of file
diff --git a/PARR.DAL/PARR.DAL.csproj b/PARR.DAL/PARR.DAL.csproj
index c2efc3ca..83140405 100644
--- a/PARR.DAL/PARR.DAL.csproj
+++ b/PARR.DAL/PARR.DAL.csproj
@@ -22,4 +22,11 @@
+
+
+
+
+
+
+
diff --git a/PARR.DAL/Properties/AssemblyInfo.cs b/PARR.DAL/Properties/AssemblyInfo.cs
deleted file mode 100644
index 32a4a814..00000000
--- a/PARR.DAL/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("PARR.DAL.Tests")]
-[assembly: InternalsVisibleTo("PARR.DAL.IntegrationTests")]
-[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
\ No newline at end of file
diff --git a/PARR.TemplateMatcher/IMqTemplateTaskGenerator.cs b/PARR.TemplateMatcher/IMqTemplateMatcher.cs
similarity index 100%
rename from PARR.TemplateMatcher/IMqTemplateTaskGenerator.cs
rename to PARR.TemplateMatcher/IMqTemplateMatcher.cs
diff --git a/PARR.TemplateMatcher/Models/TemplateAllocationRequest.cs b/PARR.TemplateMatcher/Models/TemplateAllocationRequest.cs
new file mode 100644
index 00000000..5be8606f
--- /dev/null
+++ b/PARR.TemplateMatcher/Models/TemplateAllocationRequest.cs
@@ -0,0 +1,17 @@
+using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
+using PARR.Domain.Entities.Base.History;
+using PARR.Domain.Entities.Job;
+using PARR.Domain.Entities.Unit;
+
+namespace PARR.TemplateMatcher.Models
+{
+ public record TemplateAllocationRequest(
+ Job TargetJob,
+ Guid TargetUnitId,
+ Unit? TargetUnit,
+ int? Index,
+ List UnitsInTemplate,
+ bool IsActiveTemplate,
+ bool IsActiveSchedule,
+ HistoryInitiator Initiator);
+}
diff --git a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateProcessor.cs b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateProcessor.cs
index f7e83b64..b5bcb655 100644
--- a/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateProcessor.cs
+++ b/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateProcessor.cs
@@ -19,30 +19,26 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
private readonly ILogger logger;
private readonly ITemplateRepository templateRepository;
private readonly IUnitRepository unitRepository;
- private readonly ITemplateReuser templateReuser;
private readonly ITemplateNameNormalizer templateNameNormalizer;
- private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
- private readonly MqSettings mqSettings;
- private readonly IRabbitService mqService;
+ private readonly ITemplateAllocationService templateAllocationService;
+ private readonly ITemplateMqPublisher templateMqPublisher;
public GroupedTemplateProcessor(
ILogger logger,
ITemplateRepository templateRepository,
IUnitRepository unitRepository,
- ITemplateReuser templateReuser,
ITemplateNameNormalizer templateNameNormalizer,
- ITemplateUpdaterMqSender templateUpdaterMqSender,
+ ITemplateAllocationService templateAllocationService,
+ ITemplateMqPublisher templateMqPublisher,
MqSettings mqSettings,
IRabbitService mqService)
{
this.logger = logger;
this.templateRepository = templateRepository;
this.unitRepository = unitRepository;
- this.templateReuser = templateReuser;
this.templateNameNormalizer = templateNameNormalizer;
- this.templateUpdaterMqSender = templateUpdaterMqSender;
- this.mqSettings = mqSettings;
- this.mqService = mqService;
+ this.templateAllocationService = templateAllocationService;
+ this.templateMqPublisher = templateMqPublisher;
}
public async Task> ProcessAsync(
@@ -89,7 +85,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
}
else
{
- await HandleNewOrReusableTemplateAsync(potentialUnitId, unitsInTemplateSubGroup, targetJob, globalIndex, initiator);
+ await HandleNewOrReusableTemplateAsync(potentialUnitId, unitsInTemplateSubGroup, targetJob, globalIndex, initiator, ct);
}
}
}
@@ -159,7 +155,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
UnitFieldValueId = t.UnitFieldValueId
}).ToList()
};
- await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
+ await templateMqPublisher.PublishUpdateAsync(updateRequest);
}
}
else
@@ -174,50 +170,33 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplateSubGroup,
Job targetJob,
int globalIndex,
- HistoryInitiator initiator)
+ HistoryInitiator initiator,
+ CancellationToken ct)
{
- var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator);
+ // Вычисляем флаги активности
+ var isActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false;
+ var isActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false;
- if (reusableTemplate != null)
- {
- logger.LogInformation("Переиспользован шаблон {TemplateId}.", reusableTemplate.Id);
-
- var tempTemplateForName = new Template
+ // Маппим кортежи в сообщения
+ var unitsInTemplateMsg = unitsInTemplateSubGroup
+ .Select(e => new UnitInTemplateMessage
{
- Id = reusableTemplate.Id,
- Name = reusableTemplate.Name,
- JobId = targetJob.Id,
- UnitId = potentialUnitId,
- Index = globalIndex,
- Job = targetJob,
- Unit = reusableTemplate.Unit,
- UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitsInTemplate { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
- };
+ UnitId = e.UnitId,
+ UnitFieldValueId = e.UnitFieldValueId
+ })
+ .ToList();
- var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
+ var request = new TemplateAllocationRequest(
+ TargetJob: targetJob,
+ TargetUnitId: potentialUnitId,
+ TargetUnit: null,
+ Index: globalIndex,
+ UnitsInTemplate: unitsInTemplateMsg,
+ IsActiveTemplate: isActiveTemplate,
+ IsActiveSchedule: isActiveSchedule,
+ Initiator: initiator);
- var updateRequest = new TemplateUpdaterMessage
- {
- TemplateId = reusableTemplate.Id,
- JobId = targetJob.Id,
- UnitId = potentialUnitId,
- Name = expectedName,
- IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false,
- IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false,
- StatusTypeId = TemplateStatusTypeEnum.Used,
- Initiator = initiator,
- IsNew = true,
- Index = globalIndex,
- UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
- };
-
- await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
- }
- else
- {
- logger.LogDebug("Создание нового шаблона.");
- await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, globalIndex, initiator);
- }
+ await templateAllocationService.AllocateAsync(request, ct);
}
private async Task UpdateTemplateUnitsAsync(
@@ -270,32 +249,9 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
UnitsInTemplate = newUnitEntries.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
- await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
+ await templateMqPublisher.PublishUpdateAsync(updateRequest);
}
- private async Task CreateGroupedTemplateAsync(
- Guid jobId,
- Guid relationshipUnitId,
- List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplate,
- int index,
- HistoryInitiator initiator)
- {
- logger.LogInformation("Создание нового группового шаблона.");
-
- var mqRequest = new TemplateGeneratorMessage
- {
- JobId = jobId,
- UnitId = relationshipUnitId,
- UnitsInTemplate = unitsInTemplate.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList(),
- Index = index,
- HistoryInitiator = initiator
- };
-
- var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List