Files
continuousimprovement/tests/Strata.ContinuousImprovement.Biz.Test.Integration/SnowflakeIntegrationTests/StrategicChargeCodeOpportunityDetailServiceTests.cs
T

458 lines
23 KiB
C#

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<IDataSchemaService> _mockDataSchemaService = new Mock<IDataSchemaService>();
private Mock<ISchemaServiceClient> _mockSchemaServiceClient = new Mock<ISchemaServiceClient>();
private Mock<IExplorationFilterService> _mockExplorationService = new Mock<IExplorationFilterService>();
private Mock<IStrategicItemDimensionSyncService> _mockStrategicItemDimensionSyncService = new Mock<IStrategicItemDimensionSyncService>();
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<IHubContext<NotificationHub, INotificationHub>>();
var mockNotificationClients = new Mock<IHubClients<INotificationHub>>();
var mockNotificationGroup = new Mock<INotificationHub>();
var distributionProcessFactory = new Mock<IDistributionProcessFactory>();
notificationHubClient.Setup(h => h.Clients).Returns(mockNotificationClients.Object);
mockNotificationClients.Setup(c => c.Group(It.IsAny<string>())).Returns(mockNotificationGroup.Object);
mockNotificationGroup
.Setup(g => g.SendStrategicInitiativeTrackerSuccess(It.IsAny<string>(), It.IsAny<byte>(), It.IsAny<DateTime?>()))
.Returns(Task.CompletedTask);
mockNotificationGroup
.Setup(g => g.SendStrategicInitiativeTrackerError(It.IsAny<string>(), It.IsAny<byte>(), It.IsAny<DateTime?>()))
.Returns(Task.CompletedTask);
_mockDataSchemaService.Setup(x =>
x.GetDataTableBySqlFullNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((string tableName, CancellationToken _) => new DataTable
{
SqlAlias = tableName.Split('.').Last()
});
_mockDataSchemaService.Setup(x =>
x.GetDataColumnsBySqlColumnNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
_mockSchemaServiceClient
.Setup(x => x.GetDimensionByGlobalIdAsync("Charge Code", It.IsAny<CancellationToken>()))
.ReturnsAsync(new Schema.Client.Dtos.Info.ScoreDimensionInfoDto { DimensionGuid = ChargeCodeDimensionGuid });
_mockSchemaServiceClient
.Setup(x => x.GetDimensionByGlobalIdAsync(It.Is<string>(s => s != "Charge Code"), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Schema.Client.Dtos.Info.ScoreDimensionInfoDto { DimensionGuid = ClinicalIndicatorDimensionGuid });
_mockSchemaServiceClient
.Setup(x => x.GetDimensionMembersFromHierarchyPathsAsync(
ChargeCodeDimensionGuid,
It.IsAny<IEnumerable<string>>(),
It.IsAny<CancellationToken>()))
.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<IEnumerable<string>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync([new MemberDto { Id = "188", Name = "All Readmissions" }]);
_mockSchemaServiceClient.Setup(x =>
x.GetHierarchyNodesFromHierarchyPathsAsync(It.IsAny<Guid>(), It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
.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<StrategicChargeCodeOpportunityDetailService>)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<StrategicOpportunityMeasureDetail>(
$"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<StrategicOpportunityDetail>(
$"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<StrategicOpportunityDetail>(
$"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<IEnumerable<FilterChipItem>>(),
It.IsAny<IQueryParamBase>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<FilterChipItem>, 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<StrategicOpportunityMeasureDetail>(
$"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<StrategicOpportunityDetail>(
$"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<StrategicOpportunityChargeCode>()
.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<QueryConfig>(), It.IsAny<CancellationToken>()))
.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<QueryConfig>(), It.IsAny<CancellationToken>()))
.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<QueryConfig>(), It.IsAny<CancellationToken>()))
.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)),
]
});
}
}
}