using FluentAssertions; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Moq; using Strata.ContinuousImprovement.Biz.DbContexts; using Strata.ContinuousImprovement.Biz.Exploration; using Strata.ContinuousImprovement.Biz.Exploration.Filters; using Strata.ContinuousImprovement.Biz.Notification; using Strata.ContinuousImprovement.Biz.StrategicOpportunities; using Strata.ContinuousImprovement.Biz.StrategicOpportunities.DistributionProcess; using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Enums; using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Queries; using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities; using Strata.DataSchema.Client; using Strata.DataSchema.Models.Query; using Strata.DataSchema.Models.Schema; using Strata.Schema.Client; using Strata.Schema.Client.Dtos; using System.Diagnostics.CodeAnalysis; using static Strata.ContinuousImprovement.Biz.Exploration.ExplorationService; namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests { [ExcludeFromCodeCoverage] [TestFixture, Category("Integration"), Category("CCI")] public class StrategicChargeCodeOpportunityDetailServiceTests : IntegrationTestBase { private IStrategicChargeCodeOpportunityDetailService _strategicOpportunityDetailService; private Mock _mockDataSchemaService = new Mock(); private Mock _mockSchemaServiceClient = new Mock(); private Mock _mockExplorationService = new Mock(); private Mock _mockStrategicItemDimensionSyncService = new Mock(); private IStrategicOpportunityDetailQueryBuilder _strategicOpportunityDetailQueryBuilder; private const long TestOpportunityId = 999_000_001; private const long TestMeasureId = TestOpportunityId * 10 + 2; private const string MeasureFiltersJSON = "[17993, 17995, 18001]"; private static readonly Guid ChargeCodeDimensionGuid = new("aaaaaaaa-0000-0000-0000-000000000001"); private static readonly Guid ClinicalIndicatorDimensionGuid = new("bbbbbbbb-0000-0000-0000-000000000002"); private const long TestExplorationPopulationId = 999_000_002; private const long TestOpportunityId_Exploration = 999_000_002; private const long TestMeasureId_Exploration = TestOpportunityId_Exploration * 10 + 2; [OneTimeSetUp] public void Setup() { var notificationHubClient = new Mock>(); var mockNotificationClients = new Mock>(); var mockNotificationGroup = new Mock(); var distributionProcessFactory = new Mock(); notificationHubClient.Setup(h => h.Clients).Returns(mockNotificationClients.Object); mockNotificationClients.Setup(c => c.Group(It.IsAny())).Returns(mockNotificationGroup.Object); mockNotificationGroup .Setup(g => g.SendStrategicInitiativeTrackerSuccess(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); mockNotificationGroup .Setup(g => g.SendStrategicInitiativeTrackerError(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); _mockDataSchemaService.Setup(x => x.GetDataTableBySqlFullNameAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((string tableName, CancellationToken _) => new DataTable { SqlAlias = tableName.Split('.').Last() }); _mockDataSchemaService.Setup(x => x.GetDataColumnsBySqlColumnNameAsync(It.IsAny(), It.IsAny())) .ReturnsAsync([]); _mockSchemaServiceClient .Setup(x => x.GetDimensionByGlobalIdAsync("Charge Code", It.IsAny())) .ReturnsAsync(new Schema.Client.Dtos.Info.ScoreDimensionInfoDto { DimensionGuid = ChargeCodeDimensionGuid }); _mockSchemaServiceClient .Setup(x => x.GetDimensionByGlobalIdAsync(It.Is(s => s != "Charge Code"), It.IsAny())) .ReturnsAsync(new Schema.Client.Dtos.Info.ScoreDimensionInfoDto { DimensionGuid = ClinicalIndicatorDimensionGuid }); _mockSchemaServiceClient .Setup(x => x.GetDimensionMembersFromHierarchyPathsAsync( ChargeCodeDimensionGuid, It.IsAny>(), It.IsAny())) .ReturnsAsync([ new MemberDto { Id = "17993", Name = "39017762 - LAB CAB FEE(FROM BBACCA)*ARC" }, new MemberDto { Id = "17995", Name = "39022140 - LAB FROZEN POOLED CRYO PRODUCT(5)*ARC" }, new MemberDto { Id = "18001", Name = "39022185 - LAB LEUKOFILTERED CELLS 1 UNIT *ARC" }, ]); _mockSchemaServiceClient .Setup(x => x.GetDimensionMembersFromHierarchyPathsAsync( ClinicalIndicatorDimensionGuid, It.IsAny>(), It.IsAny())) .ReturnsAsync([new MemberDto { Id = "188", Name = "All Readmissions" }]); _mockSchemaServiceClient.Setup(x => x.GetHierarchyNodesFromHierarchyPathsAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync([]); _strategicOpportunityDetailQueryBuilder = new StrategicOpportunityDetailQueryBuilder(); _strategicOpportunityDetailService = new StrategicChargeCodeOpportunityDetailService( JazzConnBuilderFactory, (CentralDbContext)CentralDbContext, TestUtilities.GetClaimsPrincipalAccessor(), _mockDataSchemaService.Object, _mockSchemaServiceClient.Object, SnowflakeDatabaseContext, _mockExplorationService.Object, distributionProcessFactory.Object, _strategicOpportunityDetailQueryBuilder, notificationHubClient.Object, _mockStrategicItemDimensionSyncService.Object, (Microsoft.Extensions.Logging.ILogger)logger); } [TearDown] public async Task CleanupData() { await SnowflakeDatabaseContext.ExecuteCommandAsync( $"DELETE FROM cci.FactStrategicChargeCodeDetail WHERE OpportunityId IN ({TestOpportunityId}, {TestOpportunityId_Exploration})"); await SnowflakeDatabaseContext.ExecuteCommandAsync( $"DELETE FROM cci.StrategicOpportunityMeasureDetail WHERE MeasureId IN " + $"(SELECT MeasureId FROM cci.StrategicOpportunityMeasureDetail WHERE MeasureId >= {TestOpportunityId * 10})"); var centralDb = (CentralDbContext)CentralDbContext; var measures = centralDb.StrategicOpportunityMeasures .Where(m => m.OpportunityId == TestOpportunityId || m.OpportunityId == TestOpportunityId_Exploration); centralDb.StrategicOpportunityMeasures.RemoveRange(measures); var opportunity = await centralDb.StrategicOpportunityChargeCodes .FirstOrDefaultAsync(o => o.OpportunityId == TestOpportunityId || o.OpportunityId == TestOpportunityId_Exploration); if (opportunity != null) centralDb.StrategicOpportunityChargeCodes.Remove(opportunity); var explorationPopulation = await centralDb.ExplorationPopulations .FirstOrDefaultAsync(p => p.ExplorationPopulationId == TestExplorationPopulationId); if (explorationPopulation != null) centralDb.ExplorationPopulations.Remove(explorationPopulation); await centralDb.SaveChangesAsync(); } // Tests for standard Tracking opportunity (patient population) [Test, Ignore("Test database needs DS Encounter Cost datasource")] public async Task PopulateTrackingDetails_WithMeasures_PopulatesMeasureDetailRows() { // Arrange var opportunity = CreateTrackingOpportunity(patientPopulationHPath: "CLINICALINDICATOR|CLINICALINDI|188"); opportunity.Measures = [ new StrategicOpportunityMeasure { MeasureId = TestMeasureId, OpportunityId = TestOpportunityId, MeasureType = MeasureType.DirectVariableCost, MeasureGroup = MeasureGroup.Primary, FiltersJSON = MeasureFiltersJSON, StrataId = 1 } ]; await SeedOpportunityInCentralDb(opportunity); SetupBuildSqlQueryForTracking(); // Act await _strategicOpportunityDetailService.PopulateTrackingDetails(opportunity, CancellationToken.None); // Assert - measure detail rows written var measureRows = await SnowflakeDatabaseContext.QueryAsync( $"SELECT * FROM cci.StrategicOpportunityMeasureDetail WHERE MeasureId = {opportunity.Measures.First().MeasureId}", nameof(PopulateTrackingDetails_WithMeasures_PopulatesMeasureDetailRows)); // Assert - verify rows were written to Snowflake var rows = await SnowflakeDatabaseContext.QueryAsync( $"SELECT * FROM cci.FactStrategicChargeCodeDetail WHERE OpportunityId = {TestOpportunityId} AND IsBaseline = FALSE", nameof(PopulateTrackingDetails_WithMeasures_PopulatesMeasureDetailRows)); measureRows.Should().NotBeNullOrEmpty(); rows.Should().NotBeNullOrEmpty(); // Assert - AverageValue is written back to CentralDb var savedMeasure = await ((CentralDbContext)CentralDbContext) .StrategicOpportunityMeasures .FirstOrDefaultAsync(m => m.MeasureId == opportunity.Measures.First().MeasureId); savedMeasure.Should().NotBeNull(); } [Test, Ignore("Test database needs DS Encounter Cost datasource")] public async Task PopulateTrackingDetails_WhenStatusIsNotTracking_SkipsDetailPopulationAndSucceeds() { // Arrange var opportunity = CreateTrackingOpportunity(patientPopulationHPath: "CLINICALINDICATOR|CLINICALINDI|188"); opportunity.Status = (byte)StrategicOpportunityStatus.CompleteMonitoring; opportunity.Measures = [ new StrategicOpportunityMeasure { MeasureId = TestMeasureId, OpportunityId = TestOpportunityId, MeasureType = MeasureType.DirectVariableCost, MeasureGroup = MeasureGroup.Primary, FiltersJSON = MeasureFiltersJSON, StrataId = 1 } ]; await SeedOpportunityInCentralDb(opportunity); SetupBuildSqlQueryForMonitoring(); // Act await _strategicOpportunityDetailService.PopulateTrackingDetails(opportunity, CancellationToken.None); // Assert - no detail rows written for non-Tracking status var rows = await SnowflakeDatabaseContext.QueryAsync( $"SELECT * FROM cci.FactStrategicChargeCodeDetail WHERE OpportunityId = {TestOpportunityId} AND IsBaseline = FALSE", nameof(PopulateTrackingDetails_WhenStatusIsNotTracking_SkipsDetailPopulationAndSucceeds)); rows.Should().BeEmpty("detail rows should not be populated when status is not Tracking"); // Assert - tracking is still updated var savedOpportunity = await ((CentralDbContext)CentralDbContext) .StrategicOpportunityChargeCodes .FirstOrDefaultAsync(o => o.OpportunityId == TestOpportunityId); savedOpportunity.LastRunTrackingStatus.Should().Be(RunTrackingStatus.Success); } [Test, Ignore("Test database needs DS Encounter Cost datasource")] public async Task PopulateTrackingDetails_WithMeasures_PopulatesMeasureDetailRows_Exploration() { // Arrange await SeedExplorationPopulationInCentralDb(); var opportunity = CreateExplorationOpportunity(); opportunity.Measures = [ new StrategicOpportunityMeasure { MeasureId = TestMeasureId_Exploration, OpportunityId = opportunity.OpportunityId, MeasureType = MeasureType.DirectVariableCost, MeasureGroup = MeasureGroup.Primary, FiltersJSON = MeasureFiltersJSON, StrataId = 1 } ]; await SeedOpportunityInCentralDb(opportunity); _mockExplorationService .Setup(x => x.GetFilterStringsAsync( It.IsAny>(), It.IsAny(), It.IsAny())) .Callback, IQueryParamBase, CancellationToken>( (_, queryParams, _) => { queryParams.DischargeDateStart = "2024-01-01"; queryParams.DischargeDateEnd = "2024-12-31"; }) .ReturnsAsync(( string.Empty, string.Empty)); SetupBuildSqlQueryForTrackingExploration(); // Act await _strategicOpportunityDetailService.PopulateTrackingDetails(opportunity, CancellationToken.None); // Assert - measure detail rows written var measureRows = await SnowflakeDatabaseContext.QueryAsync( $"SELECT * FROM cci.StrategicOpportunityMeasureDetail WHERE MeasureId = {opportunity.Measures.First().MeasureId}", nameof(PopulateTrackingDetails_WithMeasures_PopulatesMeasureDetailRows)); // Assert - verify rows were written to Snowflake var rows = await SnowflakeDatabaseContext.QueryAsync( $"SELECT * FROM cci.FactStrategicChargeCodeDetail WHERE OpportunityId = {opportunity.OpportunityId} AND IsBaseline = FALSE", nameof(PopulateTrackingDetails_WithMeasures_PopulatesMeasureDetailRows)); measureRows.Should().NotBeNullOrEmpty(); rows.Should().NotBeNullOrEmpty(); // Assert - AverageValue is written back to CentralDb var savedMeasure = await ((CentralDbContext)CentralDbContext) .StrategicOpportunityMeasures .FirstOrDefaultAsync(m => m.MeasureId == opportunity.Measures.First().MeasureId); savedMeasure.Should().NotBeNull(); } private StrategicOpportunityChargeCode CreateTrackingOpportunity( string patientPopulationHPath, DateOnly? trackingStart = null, DateOnly? trackingEnd = null) => new() { OpportunityId = TestOpportunityId, PatientPopulationHPath = patientPopulationHPath, ExplorationPopulationId = 0, ChargeCodeHPath = "CC|CCRU|Blood", OpportunityFiltersJSON = "[]", Status = (byte)StrategicOpportunityStatus.Tracking, TrackingStartDate = trackingStart ?? new DateOnly(2024, 1, 1), TrackingEndDate = trackingEnd ?? new DateOnly(2024, 12, 31), BaselineStartDate = new DateOnly(2023, 1, 1), BaselineEndDate = new DateOnly(2023, 12, 31), LastRunTrackingDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), EstimatedTrackingDuration = 12, StrataId = 1, Measures = [] }; private StrategicOpportunityChargeCode CreateExplorationOpportunity( DateOnly? trackingStart = null, DateOnly? trackingEnd = null) => new() { OpportunityId = TestOpportunityId_Exploration, PatientPopulationHPath = null, ExplorationPopulationId = TestExplorationPopulationId, ChargeCodeHPath = "CC|CCRU|Blood", OpportunityFiltersJSON = "[]", Status = (byte)StrategicOpportunityStatus.Tracking, TrackingStartDate = trackingStart ?? new DateOnly(2024, 1, 1), TrackingEndDate = trackingEnd ?? new DateOnly(2024, 12, 31), BaselineStartDate = new DateOnly(2023, 1, 1), BaselineEndDate = new DateOnly(2023, 12, 31), LastRunTrackingDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), EstimatedTrackingDuration = 12, StrataId = 1, Measures = [] }; private async Task SeedOpportunityInCentralDb(StrategicOpportunityChargeCode opportunity) { var centralDb = (CentralDbContext)CentralDbContext; // Detach any already-tracked instance with the same key to avoid tracking conflicts var tracked = centralDb.ChangeTracker.Entries() .FirstOrDefault(e => e.Entity.OpportunityId == opportunity.OpportunityId); if (tracked != null) tracked.State = Microsoft.EntityFrameworkCore.EntityState.Detached; var existing = await centralDb.StrategicOpportunityChargeCodes .AsNoTracking() .FirstOrDefaultAsync(o => o.OpportunityId == opportunity.OpportunityId); if (existing != null) { centralDb.StrategicOpportunityChargeCodes.Remove(existing); await centralDb.SaveChangesAsync(); } centralDb.StrategicOpportunityChargeCodes.Add(opportunity); await centralDb.SaveChangesAsync(); } private async Task SeedExplorationPopulationInCentralDb() { var centralDb = (CentralDbContext)CentralDbContext; var existing = await centralDb.ExplorationPopulations .AsNoTracking() .FirstOrDefaultAsync(p => p.ExplorationPopulationId == TestExplorationPopulationId); if (existing != null) return; centralDb.ExplorationPopulations.Add(new Population.ExplorationPopulation { ExplorationPopulationId = TestExplorationPopulationId, Name = "Test Exploration Population", CaseTypeFamilyId = 8016, FiltersJSON = $@"[{{ ""key"": 25, ""chipType"": {(byte)ChipType.DateRange}, ""operator"": 0, ""dateRange"": [""{new DateTime(2024, 1, 1):yyyy-MM-dd}"", ""{new DateTime(2024, 12, 31):yyyy-MM-dd}""], ""filters"": [], ""isSelected"": null }}]", StrataId = 1 }); await centralDb.SaveChangesAsync(); } private void SetupBuildSqlQueryForTracking() { _mockDataSchemaService.SetupSequence(x => x.BuildSqlQuery(It.IsAny(), It.IsAny())) .ReturnsAsync(new SqlResponse { SqlQuery = StrategicOpportunityQueryConstants.TrackingSelectSql, Parameters = [ new(":dischargedatetime_Start_0", new DateTime(2024, 1, 1)), new(":dischargedatetime_End_1", new DateTime(2024, 12, 31)), new(":clinicalindicatorid_2", 188), new(":dischargedatetime_Start_3", new DateTime(2024, 1, 1)), new(":dischargedatetime_End_4", new DateTime(2024, 12, 31)), new(":clinicalindicatorid_5", 188), ] }) .ReturnsAsync(new SqlResponse { SqlQuery = StrategicOpportunityQueryConstants.MeasureSelectSql, Parameters = [ new(":dischargedatetime_Start_0", new DateTime(2024, 1, 1)), new(":dischargedatetime_End_1", new DateTime(2024, 12, 31)), ] }); } private void SetupBuildSqlQueryForTrackingExploration() { _mockDataSchemaService.SetupSequence(x => x.BuildSqlQuery(It.IsAny(), It.IsAny())) .ReturnsAsync(new SqlResponse { SqlQuery = StrategicOpportunityQueryConstants.ExplorationTrackingSelectSql, Parameters = [ new(":dischargedatetime_Start_0", new DateTime(2024, 1, 1)), new(":dischargedatetime_End_1", new DateTime(2024, 12, 31)), new(":explorationpopulationid_2", TestExplorationPopulationId), new(":dischargedatetime_Start_3", new DateTime(2024, 1, 1)), new(":dischargedatetime_End_4", new DateTime(2024, 12, 31)), new(":explorationpopulationid_5", TestExplorationPopulationId), ] }) .ReturnsAsync(new SqlResponse { SqlQuery = StrategicOpportunityQueryConstants.ExplorationMeasureSelectSql, Parameters = [ new(":dischargedatetime_Start_0", new DateTime(2024, 1, 1)), new(":dischargedatetime_End_1", new DateTime(2024, 12, 31)), new(":explorationpopulationid_2", TestExplorationPopulationId), ] }); } private void SetupBuildSqlQueryForMonitoring() { _mockDataSchemaService.Setup(x => x.BuildSqlQuery(It.IsAny(), It.IsAny())) .ReturnsAsync(new SqlResponse { SqlQuery = StrategicOpportunityQueryConstants.MeasureSelectSql, Parameters = [ new(":dischargedatetime_Start_0", new DateTime(2024, 1, 1)), new(":dischargedatetime_End_1", new DateTime(2024, 12, 31)), ] }); } } }