Template
chore: deploy initial code base
This commit is contained in:
+410
@@ -0,0 +1,410 @@
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Strata.ContinuousImprovement.Biz.Shared;
|
||||
using Strata.ContinuousImprovement.Biz.StrategicOpportunities;
|
||||
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.DistributionProcess;
|
||||
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.DistributionProcess.ChargeCode;
|
||||
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Enums;
|
||||
using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests.DistributionProcess
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
[TestFixture, Category("Integration"), Category("CCI")]
|
||||
public class DistributionProcessChargeCodeTests : IntegrationTestBase
|
||||
{
|
||||
private const string SourceFilePath = @"SnowflakeIntegrationTests/DistributionProcess/TestData/ChargeCode";
|
||||
|
||||
private static readonly ImmutableDictionary<string, (Period<DateOnly> Source, Period<DateOnly> Target)> _dataPeriod = new Dictionary<string, (Period<DateOnly> Source, Period<DateOnly> Target)>()
|
||||
{
|
||||
{ "AllMonthsAreCross", (new Period<DateOnly>(new DateOnly(2022, 12, 1), new DateOnly(2023, 12, 31)), new Period<DateOnly>(new DateOnly(2024, 1, 1), new DateOnly(2024, 12, 31))) },
|
||||
{ "AllMonthsAreNotCross", (new Period<DateOnly>(new DateOnly(2023, 1, 1), new DateOnly(2023, 5, 30)), new Period<DateOnly>(new DateOnly(2023, 6, 1), new DateOnly(2023, 12, 31))) },
|
||||
{ "MixedMonths", (new Period<DateOnly>(new DateOnly(2022, 12, 1), new DateOnly(2023, 8, 31)), new Period<DateOnly>(new DateOnly(2023, 5, 1), new DateOnly(2023, 12, 31))) }
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
private IDistributionProcessFactory _distributionProcessFactory;
|
||||
private const long TestOpportunityId = 1;
|
||||
private Period<DateOnly> _baselinePeriod;
|
||||
private Period<DateOnly> _goalPeriod;
|
||||
private Period<DateOnly> _trackingPeriod;
|
||||
private double[] _rampUps;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
await CleanData();
|
||||
await SeedDataFromCsv("BaselineData.csv");
|
||||
_distributionProcessFactory = ServiceProvider.GetRequiredService<IDistributionProcessFactory>();
|
||||
_baselinePeriod = new Period<DateOnly>(new DateOnly(2023, 1, 1), new DateOnly(2023, 12, 31));
|
||||
_goalPeriod = new Period<DateOnly>(new DateOnly(2024, 1, 1), new DateOnly(2024, 12, 31));
|
||||
_trackingPeriod = new Period<DateOnly>(new DateOnly(2024, 1, 1), new DateOnly(2024, 12, 31));
|
||||
_rampUps = new double[] { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0 };
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public async Task Cleanup()
|
||||
{
|
||||
await CleanData();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public async Task CleanupTestData()
|
||||
{
|
||||
await CleanData(false);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(DistributeAsync_FromSourceToOpportunity_TestCases))]
|
||||
public async Task DistributeAsync_FromSourceToOpportunity
|
||||
(
|
||||
DistributionMethod distributionMethod,
|
||||
Period<DateOnly> sourcePeriod,
|
||||
Period<DateOnly> targetPeriod,
|
||||
double[] rampUps
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode(
|
||||
TestOpportunityId,
|
||||
sourcePeriod ?? _baselinePeriod,
|
||||
targetPeriod ?? _goalPeriod,
|
||||
_trackingPeriod,
|
||||
[]);
|
||||
|
||||
// Act
|
||||
var summary = await distributionProcess.DistributeAsync(distributionMethod, DistributionStage.FromSourceToOpportunity);
|
||||
|
||||
// Assert
|
||||
var sbResult = new StringBuilder();
|
||||
var details = await GetDetailRecords(isBaseline: false);
|
||||
sbResult.AppendLine(ConvertListDetailToCsvString(details));
|
||||
sbResult.AppendLine(ConvertSummaryToCsvString(summary));
|
||||
await Verifier.Verify(sbResult.ToString(), verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(DistributeAsync_FromOpportunityToInitiative_TestCases))]
|
||||
public async Task DistributeAsync_FromOpportunityToInitiative
|
||||
(
|
||||
DistributionMethod distributionMethod,
|
||||
Period<DateOnly> sourcePeriod,
|
||||
Period<DateOnly> targetPeriod,
|
||||
double[] rampUps
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
await SeedDataFromCsv("GoalData.csv");
|
||||
var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode(
|
||||
TestOpportunityId,
|
||||
_baselinePeriod,
|
||||
sourcePeriod ?? _goalPeriod,
|
||||
targetPeriod ?? _trackingPeriod,
|
||||
rampUps ?? _rampUps);
|
||||
|
||||
// Act
|
||||
var summary = await distributionProcess.DistributeAsync(distributionMethod, DistributionStage.FromOpportunityToInitiative);
|
||||
|
||||
// Assert
|
||||
var sbResult = new StringBuilder();
|
||||
var details = await GetDetailRecords(isBaseline: false);
|
||||
sbResult.AppendLine(ConvertListDetailToCsvString(details));
|
||||
sbResult.AppendLine(ConvertSummaryToCsvString(summary));
|
||||
await Verifier.Verify(sbResult.ToString(), verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(DistributeAsync_FromInitiativeToOpportunity_TestCases))]
|
||||
public async Task DistributeAsync_FromInitiativeToOpportunity
|
||||
(
|
||||
DistributionMethod distributionMethod,
|
||||
Period<DateOnly> sourcePeriod,
|
||||
Period<DateOnly> targetPeriod,
|
||||
double[] rampUps
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
await SeedDataFromCsv("GoalData.csv");
|
||||
var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode(
|
||||
TestOpportunityId,
|
||||
_baselinePeriod,
|
||||
sourcePeriod ?? _goalPeriod,
|
||||
targetPeriod ?? _trackingPeriod,
|
||||
rampUps ?? _rampUps);
|
||||
|
||||
// Act
|
||||
var summary = await distributionProcess.DistributeAsync(distributionMethod, DistributionStage.FromInitiativeToOpportunity);
|
||||
|
||||
// Assert
|
||||
var sbResult = new StringBuilder();
|
||||
var details = await GetDetailRecords(isBaseline: false);
|
||||
sbResult.AppendLine(ConvertListDetailToCsvString(details));
|
||||
sbResult.AppendLine(ConvertSummaryToCsvString(summary));
|
||||
await Verifier.Verify(sbResult.ToString(), verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(UpdateDetailCommittedByRampUp_TestCases))]
|
||||
public async Task UpdateDetailCommittedByRampUp
|
||||
(
|
||||
double[] oldRampUps,
|
||||
double[] newRampUps
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
await SeedDataFromCsv("GoalData.csv");
|
||||
var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode(
|
||||
TestOpportunityId,
|
||||
_baselinePeriod,
|
||||
_goalPeriod,
|
||||
_trackingPeriod,
|
||||
newRampUps ?? _rampUps);
|
||||
|
||||
// Act
|
||||
var summary = await distributionProcess.UpdateDetailCommittedByRampUp(oldRampUps);
|
||||
|
||||
// Assert
|
||||
var sbResult = new StringBuilder();
|
||||
var details = await GetDetailRecords(isBaseline: false);
|
||||
sbResult.AppendLine(ConvertListDetailToCsvString(details));
|
||||
sbResult.AppendLine(ConvertSummaryToCsvString(summary));
|
||||
await Verifier.Verify(sbResult.ToString(), verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetDistributionSummary_TestCases))]
|
||||
public async Task GetDistributionSummary
|
||||
(
|
||||
bool isInitiative,
|
||||
double[] rampUps
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
await SeedDataFromCsv("GoalData.csv");
|
||||
var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode(
|
||||
TestOpportunityId,
|
||||
_baselinePeriod,
|
||||
_goalPeriod,
|
||||
_trackingPeriod,
|
||||
rampUps ?? _rampUps);
|
||||
|
||||
// Act
|
||||
var summary = await distributionProcess.GetDistributionSummaryAsync(isInitiative);
|
||||
|
||||
// Assert
|
||||
var sbResult = new StringBuilder();
|
||||
sbResult.AppendLine(ConvertSummaryToCsvString(summary));
|
||||
await Verifier.Verify(sbResult.ToString(), verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
#region Test Case Sources
|
||||
|
||||
private static IEnumerable<TestCaseData> DistributeAsync_FromSourceToOpportunity_TestCases()
|
||||
{
|
||||
var distributionStageName = DistributionStage.FromSourceToOpportunity.ToString();
|
||||
return DistributeAsync_TestCases(distributionStageName);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> DistributeAsync_FromOpportunityToInitiative_TestCases()
|
||||
{
|
||||
var distributionStageName = DistributionStage.FromOpportunityToInitiative.ToString();
|
||||
foreach (var x in DistributeAsync_TestCases(distributionStageName))
|
||||
yield return x;
|
||||
|
||||
if (_dataPeriod.TryGetValue("AllMonthsAreCross", out var periods))
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
DistributionMethod.Average,
|
||||
periods.Source,
|
||||
periods.Target,
|
||||
Array.Empty<double>()
|
||||
).SetName($"DistributeAsync_{distributionStageName}_Average_AllMonthsAreCross_WithoutRampups");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> DistributeAsync_FromInitiativeToOpportunity_TestCases()
|
||||
{
|
||||
var distributionStageName = DistributionStage.FromInitiativeToOpportunity.ToString();
|
||||
foreach (var x in DistributeAsync_TestCases(distributionStageName))
|
||||
yield return x;
|
||||
|
||||
if (_dataPeriod.TryGetValue("AllMonthsAreCross", out var periods))
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
DistributionMethod.Average,
|
||||
periods.Source,
|
||||
periods.Target,
|
||||
Array.Empty<double>()
|
||||
).SetName($"DistributeAsync_{distributionStageName}_Average_AllMonthsAreCross_WithoutRampups");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> DistributeAsync_TestCases(string distributionStageName)
|
||||
{
|
||||
|
||||
if (_dataPeriod.TryGetValue("AllMonthsAreCross", out var periods))
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
DistributionMethod.Average,
|
||||
periods.Source,
|
||||
periods.Target,
|
||||
null
|
||||
).SetName($"DistributeAsync_{distributionStageName}_Average_AllMonthsAreCross");
|
||||
}
|
||||
|
||||
foreach (var x in _dataPeriod)
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
DistributionMethod.Monthly,
|
||||
x.Value.Source,
|
||||
x.Value.Target,
|
||||
null
|
||||
).SetName($"DistributeAsync_{distributionStageName}_Monthly_{x.Key}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> UpdateDetailCommittedByRampUp_TestCases()
|
||||
{
|
||||
var rampUp = new double[] { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0 };
|
||||
yield return new TestCaseData(
|
||||
rampUp,
|
||||
rampUp.Reverse().ToArray()
|
||||
).SetName($"UpdateDetailCommittedByRampUp_RampUpToRampUp");
|
||||
yield return new TestCaseData(
|
||||
rampUp,
|
||||
Array.Empty<double>()
|
||||
).SetName($"UpdateDetailCommittedByRampUp_RampUpToNoRampUp");
|
||||
yield return new TestCaseData(
|
||||
Array.Empty<double>(),
|
||||
rampUp
|
||||
).SetName($"UpdateDetailCommittedByRampUp_NoRampUpToRampUp");
|
||||
yield return new TestCaseData(
|
||||
Array.Empty<double>(),
|
||||
Array.Empty<double>()
|
||||
).SetName($"UpdateDetailCommittedByRampUp_NoRampUpToNoRampUp");
|
||||
yield return new TestCaseData(
|
||||
rampUp,
|
||||
rampUp
|
||||
).SetName($"UpdateDetailCommittedByRampUp_NoChangeRampUp");
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetDistributionSummary_TestCases()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
true,
|
||||
null
|
||||
).SetName($"GetDistributionSummary_Initiative");
|
||||
yield return new TestCaseData(
|
||||
true,
|
||||
Array.Empty<double>()
|
||||
).SetName($"GetDistributionSummary_Initiative_NoRampUp");
|
||||
yield return new TestCaseData(
|
||||
false,
|
||||
null
|
||||
).SetName($"GetDistributionSummary_Opportunity");
|
||||
yield return new TestCaseData(
|
||||
false,
|
||||
Array.Empty<double>()
|
||||
).SetName($"GetDistributionSummary_Opportunity_NoRampUp");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private async Task SeedDataFromCsv(string fileName)
|
||||
{
|
||||
var filePath = Path.IsPathRooted(fileName)
|
||||
? fileName
|
||||
: Path.Combine(AppContext.BaseDirectory, SourceFilePath, fileName);
|
||||
|
||||
var insertSql = await GetSqlInsertFromCsv(filePath, StrategicOpportunityService.StrategicOpportunityDetailTableName);
|
||||
|
||||
await SnowflakeDatabaseContext.ExecuteCommandAsync(insertSql);
|
||||
}
|
||||
|
||||
private async Task<string> GetSqlInsertFromCsv(string filePath, string tableName)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Test data file not found: {filePath}");
|
||||
}
|
||||
|
||||
var lines = await File.ReadAllLinesAsync(filePath);
|
||||
if (lines.Length < 2)
|
||||
{
|
||||
throw new InvalidOperationException($"CSV file {filePath} must contain a header and at least one data row");
|
||||
}
|
||||
|
||||
var values = lines.Skip(1).Select(x => $"({x})").ToList();
|
||||
|
||||
var insertSql = $@"
|
||||
INSERT INTO {tableName} ({lines[0]})
|
||||
VALUES {string.Join(",\n", values)}
|
||||
";
|
||||
|
||||
return insertSql;
|
||||
}
|
||||
|
||||
private string ConvertListDetailToCsvString(IEnumerable<StrategicOpportunityDetail> records)
|
||||
{
|
||||
using (var writer = new StringWriter())
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.Context.RegisterClassMap<StrategicOpportunityDetailCsvMap>();
|
||||
csv.WriteRecords(records);
|
||||
return writer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private string ConvertSummaryToCsvString(DistributionChargeCodeSummary summary)
|
||||
{
|
||||
using (var writer = new StringWriter())
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.WriteRecords([summary]);
|
||||
return writer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<StrategicOpportunityDetail>> GetDetailRecords(bool? isBaseline = null)
|
||||
{
|
||||
var sql = $@"
|
||||
SELECT * FROM {StrategicOpportunityService.StrategicOpportunityDetailTableName}
|
||||
WHERE OpportunityId = {TestOpportunityId} {(isBaseline.HasValue ? $"AND IsBaseline = {isBaseline}" : "")}
|
||||
ORDER BY DischargeFiscalYearId, DischargeFiscalMonthId, EntityId
|
||||
";
|
||||
var records = await SnowflakeDatabaseContext.QueryAsync<StrategicOpportunityDetail>(sql, nameof(GetDetailRecords));
|
||||
return records;
|
||||
}
|
||||
|
||||
private async Task CleanData(bool? isBaseline = null)
|
||||
{
|
||||
await SnowflakeDatabaseContext.ExecuteCommandAsync($@"
|
||||
DELETE FROM {StrategicOpportunityService.StrategicOpportunityDetailTableName}
|
||||
WHERE OpportunityId = {TestOpportunityId} {(isBaseline.HasValue ? $"AND IsBaseline = {isBaseline}" : "")}
|
||||
");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class StrategicOpportunityDetailCsvMap : ClassMap<StrategicOpportunityDetail>
|
||||
{
|
||||
public StrategicOpportunityDetailCsvMap()
|
||||
{
|
||||
AutoMap(CultureInfo.InvariantCulture);
|
||||
Map(m => m.RowId).Ignore();
|
||||
}
|
||||
}
|
||||
}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Strata.ContinuousImprovement.Biz.DbContexts;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration.Filters;
|
||||
using Strata.ContinuousImprovement.Biz.FiscalMonths;
|
||||
using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities;
|
||||
using Strata.ContinuousImprovement.Biz.Utilities;
|
||||
using Strata.Schema.Client;
|
||||
using Strata.Schema.Client.Dtos;
|
||||
using Strata.Schema.Client.Dtos.Info;
|
||||
using Strata.SqlTools.Configuration.Common.AsyncFactory;
|
||||
using Strata.StrataSphereCompare.Client;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using VerifyNUnit;
|
||||
|
||||
namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Baseline regression tests for the exploration query refactor.
|
||||
///
|
||||
/// WORKFLOW:
|
||||
/// 1. On main (or a branch from main): run these tests to generate golden snapshots.
|
||||
/// Commit the resulting *.verified.txt files.
|
||||
/// 2. On the refactor branch: run the same tests.
|
||||
/// Verify diffs output against the golden snapshots — any divergence fails.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
[TestFixture, Category("Integration"), Category("CCI")]
|
||||
public class FlexibleExplorationBaselineTest : IntegrationTestBase
|
||||
{
|
||||
private static readonly Guid _configurationGuid = Guid.Parse("aa4cba4f-2506-462c-8e34-c45b21653988");
|
||||
|
||||
private ExplorationDetailInfo _detailInfo;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public async Task SetupDetailInfo()
|
||||
{
|
||||
var summaryInfo = CreateSummaryInfo();
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(summaryInfo, CancellationToken.None);
|
||||
|
||||
if (!summaries.Any())
|
||||
throw new InvalidOperationException("No Case Type Family summaries returned — cannot run baseline tests.");
|
||||
|
||||
// Configure the schema service mock so the Gender ScoreMember filter resolves to
|
||||
// a real SQL fragment (fpes.GenderID IN (1)) rather than being silently skipped.
|
||||
// GenderID 1 = Male in the GM Automation test database.
|
||||
var genderDimensionGuid = Guid.NewGuid();
|
||||
Mock.Get(SchemaServiceClient)
|
||||
.Setup(s => s.GetDimensionByGlobalIdAsync("Gender", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ScoreDimensionInfoDto { DimensionGuid = genderDimensionGuid });
|
||||
Mock.Get(SchemaServiceClient)
|
||||
.Setup(s => s.GetDimensionMembersFromHierarchyPathsAsync(genderDimensionGuid, It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new[] { new MemberDto { Id = "1" } });
|
||||
|
||||
// Rebuild ExplorationFilterService and ExplorationService with the configured mock
|
||||
// so the gender filter is active for all tests in this fixture.
|
||||
var filterService = new ExplorationFilterService(
|
||||
ServiceProvider.GetRequiredService<IAsyncDbContextFactory<JazzDbContext>>(),
|
||||
SchemaServiceClient,
|
||||
Mock.Of<ILogger<ExplorationFilterService>>());
|
||||
var featureFlagWrapper = Mock.Of<IFeatureFlagWrapper>();
|
||||
Mock.Get(featureFlagWrapper)
|
||||
.Setup(f => f.IsCiBenchmarkingEnabled(It.IsAny<bool>()))
|
||||
.ReturnsAsync(true);
|
||||
ExplorationService = new ExplorationService(
|
||||
JazzConnBuilderFactory,
|
||||
SnowflakeDatabaseContext,
|
||||
Mock.Of<IFiscalMonthResolver>(),
|
||||
featureFlagWrapper,
|
||||
Mock.Of<IStrataSphereCompareService>(),
|
||||
filterService,
|
||||
Mock.Of<ILogger<ExplorationService>>());
|
||||
|
||||
// Always pick the same CTF (highest encounter count) so snapshots are deterministic.
|
||||
var busiest = summaries.OrderByDescending(s => s.Data.TotalEncounters).First();
|
||||
|
||||
// Derive a real cost driver from the CTF's detail breakdown so the
|
||||
// encounter query (which filters on cc.costdriver = @CostDriver) returns data.
|
||||
var costDriverInfo = CreateSummaryInfo();
|
||||
costDriverInfo.CaseTypeFamilyId = busiest.Data.Id;
|
||||
var costDriverDetails = await ExplorationService.GetCaseTypeFamilySummaryDetailsAsync(costDriverInfo, CancellationToken.None);
|
||||
var costDriver = costDriverDetails
|
||||
.SelectMany(d => d.Children ?? [])
|
||||
.OrderByDescending(c => c.Data.TotalEncounters)
|
||||
.FirstOrDefault()?.Name
|
||||
?? throw new InvalidOperationException("No cost driver children found for the busiest CTF.");
|
||||
|
||||
_detailInfo = new ExplorationDetailInfo
|
||||
{
|
||||
ConfigurationGuid = _configurationGuid,
|
||||
CaseTypeFamilyId = busiest.Data.Id,
|
||||
CostDriver = costDriver,
|
||||
ExclusionCriteria = new ExclusionCriteria(),
|
||||
ExcludedEncounters = new List<long>(),
|
||||
IncludedEncounters = new List<long>(),
|
||||
Filters = new Biz.Exploration.Filters.Filter
|
||||
{
|
||||
DischargeDateStart = new DateTime(2024, 1, 1),
|
||||
DischargeDateEnd = new DateTime(2025, 1, 1),
|
||||
FilterChipItems = new List<FilterChipItem>
|
||||
{
|
||||
new FilterChipItem
|
||||
{
|
||||
ChipType = ChipType.DateRange,
|
||||
Key = ChipKey.DischargeDateID,
|
||||
Filters = [],
|
||||
DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── No filters ──────────────────────────────────────────────────────────
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilyDetailsAsync_MatchesBaseline()
|
||||
{
|
||||
var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(_detailInfo, CancellationToken.None);
|
||||
|
||||
await Verifier.Verify(result, verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetEncountersAsync_MatchesBaseline()
|
||||
{
|
||||
var result = await ExplorationService.GetEncountersAsync(_detailInfo, _configurationGuid, CancellationToken.None);
|
||||
|
||||
await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
// ── Boolean filter (charges > 0) ─────────────────────────────────────
|
||||
// IsGreaterThanZeroCharges hits the queryFilter path and reliably
|
||||
// produces a meaningful subset without risking empty results.
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilyDetailsAsync_WithChargesFilter_MatchesBaseline()
|
||||
{
|
||||
var info = CreateDetailInfo(extraFilter: CreateChargesFilter());
|
||||
|
||||
var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(info, CancellationToken.None);
|
||||
|
||||
await Verifier.Verify(result, verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetEncountersAsync_WithChargesFilter_MatchesBaseline()
|
||||
{
|
||||
var info = CreateDetailInfo(extraFilter: CreateChargesFilter());
|
||||
|
||||
var result = await ExplorationService.GetEncountersAsync(info, _configurationGuid, CancellationToken.None);
|
||||
|
||||
await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
// ── FPES ScoreMember filter (gender) ────────────────────────────────
|
||||
// Gender is an FPES-level filter — it resolves to fpes.GenderID IN (...)
|
||||
// via the schema service and lands in the encounterSummaryFilter placeholder.
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilyDetailsAsync_WithGenderFilter_MatchesBaseline()
|
||||
{
|
||||
var info = CreateDetailInfo(extraFilter: CreateGenderFilter());
|
||||
|
||||
var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(info, CancellationToken.None);
|
||||
|
||||
await Verifier.Verify(result, verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetEncountersAsync_WithGenderFilter_MatchesBaseline()
|
||||
{
|
||||
var info = CreateDetailInfo(extraFilter: CreateGenderFilter());
|
||||
|
||||
var result = await ExplorationService.GetEncountersAsync(info, _configurationGuid, CancellationToken.None);
|
||||
|
||||
await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
// ── IQR exclusions ───────────────────────────────────────────────────
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilyDetailsAsync_WithIqrExclusions_MatchesBaseline()
|
||||
{
|
||||
var info = CreateDetailInfo(exclusion: new ExclusionCriteria
|
||||
{
|
||||
CostExclusionType = ExclusionCriteriaType.Iqr,
|
||||
CostExclusionValue = 1.5,
|
||||
LosExclusionType = ExclusionCriteriaType.Iqr,
|
||||
LosExclusionValue = 1.5
|
||||
});
|
||||
|
||||
var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(info, CancellationToken.None);
|
||||
|
||||
await Verifier.Verify(result, verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetEncountersAsync_WithIqrExclusions_MatchesBaseline()
|
||||
{
|
||||
var info = CreateDetailInfo(exclusion: new ExclusionCriteria
|
||||
{
|
||||
CostExclusionType = ExclusionCriteriaType.Iqr,
|
||||
CostExclusionValue = 1.5,
|
||||
LosExclusionType = ExclusionCriteriaType.Iqr,
|
||||
LosExclusionValue = 1.5
|
||||
});
|
||||
|
||||
var result = await ExplorationService.GetEncountersAsync(info, _configurationGuid, CancellationToken.None);
|
||||
|
||||
await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings)
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
private ExplorationDetailInfo CreateDetailInfo(
|
||||
FilterChipItem extraFilter = null,
|
||||
ExclusionCriteria exclusion = null)
|
||||
{
|
||||
var dateFilter = new FilterChipItem
|
||||
{
|
||||
ChipType = ChipType.DateRange,
|
||||
Key = ChipKey.DischargeDateID,
|
||||
Filters = [],
|
||||
DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)]
|
||||
};
|
||||
|
||||
var chips = extraFilter != null
|
||||
? new List<FilterChipItem> { dateFilter, extraFilter }
|
||||
: new List<FilterChipItem> { dateFilter };
|
||||
|
||||
return new ExplorationDetailInfo
|
||||
{
|
||||
ConfigurationGuid = _detailInfo.ConfigurationGuid,
|
||||
CaseTypeFamilyId = _detailInfo.CaseTypeFamilyId,
|
||||
CostDriver = _detailInfo.CostDriver,
|
||||
ExclusionCriteria = exclusion ?? new ExclusionCriteria(),
|
||||
ExcludedEncounters = new List<long>(),
|
||||
IncludedEncounters = new List<long>(),
|
||||
Filters = new Biz.Exploration.Filters.Filter
|
||||
{
|
||||
DischargeDateStart = new DateTime(2024, 1, 1),
|
||||
DischargeDateEnd = new DateTime(2025, 1, 1),
|
||||
FilterChipItems = chips
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Gender filter — FPES-level (encounterSummaryFilter placeholder).
|
||||
// GlobalId "Gender" maps to the Gender dimension in the schema service.
|
||||
// The Filters list contains the hierarchy path ID for "Male" as resolved
|
||||
// by the schema service; adjust the Id if needed for your test database.
|
||||
private static FilterChipItem CreateGenderFilter() => new()
|
||||
{
|
||||
ChipType = ChipType.ScoreMember,
|
||||
GlobalId = "Gender",
|
||||
Key = ChipKey.GenderID,
|
||||
Operator = OperatorType.In,
|
||||
Filters = new List<TreeNode> { new() { Id = "Male", DisplayText = "Male" } }
|
||||
};
|
||||
|
||||
private static FilterChipItem CreateChargesFilter() => new()
|
||||
{
|
||||
ChipType = ChipType.Boolean,
|
||||
Key = ChipKey.IsGreaterThanZeroCharges,
|
||||
IsSelected = true,
|
||||
Operator = OperatorType.In,
|
||||
Filters = new List<TreeNode>()
|
||||
};
|
||||
|
||||
private ExplorationCostDriverInfo CreateSummaryInfo() => new()
|
||||
{
|
||||
ConfigurationGuid = _configurationGuid,
|
||||
ExclusionCriteria = new ExclusionCriteria(),
|
||||
Filters = new Biz.Exploration.Filters.Filter
|
||||
{
|
||||
DischargeDateStart = new DateTime(2024, 1, 1),
|
||||
DischargeDateEnd = new DateTime(2025, 1, 1),
|
||||
FilterChipItems = new List<FilterChipItem>
|
||||
{
|
||||
new FilterChipItem
|
||||
{
|
||||
ChipType = ChipType.DateRange,
|
||||
Key = ChipKey.DischargeDateID,
|
||||
Filters = [],
|
||||
DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
using FluentAssertions;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration.CaseTypeFamilies;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration.Filters;
|
||||
using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
[TestFixture, Category("Integration"), Category("CCI")]
|
||||
public class FlexibleExplorationSummaryDetailTest : IntegrationTestBase
|
||||
{
|
||||
private static readonly Guid _configurationGuid = Guid.Parse("aa4cba4f-2506-462c-8e34-c45b21653988");
|
||||
|
||||
[OneTimeSetUp]
|
||||
public async Task SetupExploration()
|
||||
{
|
||||
var explorationInfo = CreateExplorationInfo();
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
|
||||
if (!summaries.Any())
|
||||
throw new InvalidOperationException("No Case Type Family summaries returned for setup.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilySummaryDetails_WhenNoFilters_IsEquivalent()
|
||||
{
|
||||
// arrange
|
||||
var explorationInfo = CreateExplorationInfo();
|
||||
|
||||
// act
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
var testSummary = summaries.OrderByDescending(x => x.Data.TotalEncounters).First();
|
||||
explorationInfo.CaseTypeFamilyId = testSummary.Data.Id;
|
||||
|
||||
var testDetails = await ExplorationService.GetCaseTypeFamilySummaryDetailsAsync(explorationInfo, CancellationToken.None);
|
||||
var testDetail = testDetails.First();
|
||||
// assert
|
||||
testDetail.Data.Should().BeEquivalentTo(testSummary.Data,
|
||||
options => options
|
||||
.Using<double>(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.001))
|
||||
.WhenTypeIs<double>());
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(ExclusionCriteriaTestCases))]
|
||||
public async Task GetCaseTypeFamilySummaries_WithExclusions(ExclusionCriteria exclusions)
|
||||
{
|
||||
await RunExclusionSummaryTests(exclusions);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilySummaryDetails_WithFilters_IsEquivalent()
|
||||
{
|
||||
// arrange - filtering on mortality (IsSelected = true => MortalityRate = 1)
|
||||
var booleanFilter = CreateBooleanFilter(ChipKey.Mortality);
|
||||
var explorationInfo = CreateExplorationInfo(booleanFilter);
|
||||
|
||||
// act
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
var testSummary = summaries.OrderByDescending(x => x.Data.TotalEncounters).First();
|
||||
explorationInfo.CaseTypeFamilyId = testSummary.Data.Id;
|
||||
|
||||
var testDetails = await ExplorationService.GetCaseTypeFamilySummaryDetailsAsync(explorationInfo, CancellationToken.None);
|
||||
var testDetail = testDetails.First();
|
||||
// assert
|
||||
testDetail.Data.Should().BeEquivalentTo(testSummary.Data,
|
||||
options => options
|
||||
.Using<double>(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.001))
|
||||
.WhenTypeIs<double>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilySummaryDetails_WithFiltersAndExclusions_IsEquivalent()
|
||||
{
|
||||
// arrange - iqr + mad exclusion combo with a boolean filter
|
||||
var booleanFilter = CreateBooleanFilter(ChipKey.Mortality);
|
||||
var exclusions = new ExclusionCriteria
|
||||
{
|
||||
CostExclusionType = ExclusionCriteriaType.Iqr,
|
||||
CostExclusionValue = 1.5,
|
||||
LosExclusionType = ExclusionCriteriaType.Mad,
|
||||
LosExclusionValue = 2
|
||||
};
|
||||
var explorationInfo = CreateExplorationInfo(booleanFilter, exclusions);
|
||||
|
||||
// act
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
var testSummary = summaries.OrderByDescending(x => x.Data.TotalEncounters).First();
|
||||
explorationInfo.CaseTypeFamilyId = testSummary.Data.Id;
|
||||
|
||||
var testDetails = await ExplorationService.GetCaseTypeFamilySummaryDetailsAsync(explorationInfo, CancellationToken.None);
|
||||
var testDetail = testDetails.First();
|
||||
// assert
|
||||
testDetail.Data.Should().BeEquivalentTo(testSummary.Data,
|
||||
options => options
|
||||
.Using<double>(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.001))
|
||||
.WhenTypeIs<double>());
|
||||
}
|
||||
|
||||
private FilterChipItem CreateDischargeDateFilter() => new()
|
||||
{
|
||||
ChipType = ChipType.DateRange,
|
||||
Filters = [],
|
||||
DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)]
|
||||
};
|
||||
|
||||
private FilterChipItem CreateBooleanFilter(ChipKey key)
|
||||
{
|
||||
if (key is ChipKey.Mortality)
|
||||
{
|
||||
return new FilterChipItem
|
||||
{
|
||||
ChipType = ChipType.Boolean,
|
||||
GlobalId = "Mortality",
|
||||
IsSelected = true, // determines true/false of filter
|
||||
Key = ChipKey.Mortality,
|
||||
Operator = OperatorType.In,
|
||||
Filters = new List<TreeNode>()
|
||||
{
|
||||
new TreeNode
|
||||
{
|
||||
DisplayText = "Yes"
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unsupported boolean filter: {key}", nameof(key));
|
||||
}
|
||||
|
||||
private ExplorationCostDriverInfo CreateExplorationInfo(FilterChipItem? additionalFilter = null, ExclusionCriteria? exclusion = null)
|
||||
{
|
||||
var dischargeDateFilter = CreateDischargeDateFilter();
|
||||
var filterChipItems = additionalFilter != null ?
|
||||
new List<FilterChipItem> { dischargeDateFilter, additionalFilter }
|
||||
: new List<FilterChipItem> { dischargeDateFilter };
|
||||
|
||||
return new ExplorationCostDriverInfo
|
||||
{
|
||||
ConfigurationGuid = _configurationGuid,
|
||||
ExclusionCriteria = exclusion ?? new ExclusionCriteria(),
|
||||
Filters = new Biz.Exploration.Filters.Filter
|
||||
{
|
||||
DischargeDateStart = dischargeDateFilter.DateRange.First(),
|
||||
DischargeDateEnd = dischargeDateFilter.DateRange.Last(),
|
||||
FilterChipItems = filterChipItems
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task RunExclusionSummaryTests(ExclusionCriteria exclusions)
|
||||
{
|
||||
// arrange
|
||||
var explorationInfo = CreateExplorationInfo(null, exclusions);
|
||||
|
||||
// act
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
var testSummary = summaries.OrderByDescending(x => x.Data.TotalEncounters).First();
|
||||
explorationInfo.CaseTypeFamilyId = testSummary.Data.Id;
|
||||
|
||||
var testDetails = await ExplorationService.GetCaseTypeFamilySummaryDetailsAsync(explorationInfo, CancellationToken.None);
|
||||
var testDetail = testDetails.First();
|
||||
// assert
|
||||
testDetail.Data.Should().BeEquivalentTo(testSummary.Data,
|
||||
options => options
|
||||
.Using<double>(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.001))
|
||||
.WhenTypeIs<double>());
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> ExclusionCriteriaTestCases()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new ExclusionCriteria
|
||||
{
|
||||
CostExclusionType = ExclusionCriteriaType.Mad,
|
||||
CostExclusionValue = 2,
|
||||
LosExclusionType = ExclusionCriteriaType.Mad,
|
||||
LosExclusionValue = 2
|
||||
}
|
||||
).SetName("GetCaseTypeFamilySummaryDetails_WithMADExclusions_IsEquivalent");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new ExclusionCriteria
|
||||
{
|
||||
CostExclusionType = ExclusionCriteriaType.Iqr,
|
||||
CostExclusionValue = 1.5,
|
||||
LosExclusionType = ExclusionCriteriaType.Iqr,
|
||||
LosExclusionValue = 1.5
|
||||
}
|
||||
).SetName("GetCaseTypeFamilySummaryDetails_WithIQRExclusions_IsEquivalent");
|
||||
}
|
||||
}
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
using FluentAssertions;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration.CaseTypeFamilies;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration.Filters;
|
||||
using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
[TestFixture, Category("Integration"), Category("CCI")]
|
||||
public class FlexibleExplorationSummaryTest : IntegrationTestBase
|
||||
{
|
||||
private static readonly Guid _configurationGuid = Guid.Parse("aa4cba4f-2506-462c-8e34-c45b21653988");
|
||||
private static int _totalCaseTypeFamilyCount;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public async Task SetupExploration()
|
||||
{
|
||||
var explorationInfo = CreateExplorationInfo();
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
|
||||
if (!summaries.Any())
|
||||
throw new InvalidOperationException("No Case Type Family summaries returned for setup.");
|
||||
|
||||
_totalCaseTypeFamilyCount = summaries.Count();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilySummaries_WhenNoFilters_ReturnsAll()
|
||||
{
|
||||
// arrange
|
||||
var explorationInfo = CreateExplorationInfo();
|
||||
|
||||
// act
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
|
||||
// assert
|
||||
summaries.Should().HaveCount(_totalCaseTypeFamilyCount);
|
||||
await VerifySummaries(summaries);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(ExclusionCriteriaTestCases))]
|
||||
public async Task GetCaseTypeFamilySummaries_WithExclusions(ExclusionCriteria exclusions)
|
||||
{
|
||||
await RunExclusionSummaryTests(exclusions);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilySummaries_WithFilters_ReturnsFiltered()
|
||||
{
|
||||
// arrange - filtering on mortality (IsSelected = true => MortalityRate = 1)
|
||||
var booleanFilter = CreateBooleanFilter(ChipKey.Mortality);
|
||||
var explorationInfo = CreateExplorationInfo(booleanFilter);
|
||||
|
||||
// act
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
|
||||
// assert
|
||||
summaries.Should().HaveCountGreaterThan(0);
|
||||
summaries.Should().HaveCountLessThan(_totalCaseTypeFamilyCount, "Filters should reduce data returned");
|
||||
// mortality rate only applies to included encounters
|
||||
summaries.Where(x => x.Data.IncludedEncounterCount > 0).Should().OnlyContain(s => s.Data.MortalityRate == 1);
|
||||
await VerifySummaries(summaries);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCaseTypeFamilySummaries_WithFiltersAndExclusions_ReturnsFilteredAndExcluded()
|
||||
{
|
||||
// arrange - iqr + mad exclusion combo with a boolean filter
|
||||
var booleanFilter = CreateBooleanFilter(ChipKey.Mortality);
|
||||
var exclusions = new ExclusionCriteria
|
||||
{
|
||||
CostExclusionType = ExclusionCriteriaType.Iqr,
|
||||
CostExclusionValue = 1.5,
|
||||
LosExclusionType = ExclusionCriteriaType.Mad,
|
||||
LosExclusionValue = 2
|
||||
};
|
||||
var explorationInfo = CreateExplorationInfo(booleanFilter, exclusions);
|
||||
|
||||
// act
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
|
||||
// assert
|
||||
summaries.Should().HaveCountGreaterThan(0);
|
||||
summaries.Should().HaveCountLessThan(_totalCaseTypeFamilyCount);
|
||||
// mortality rate only applies to included encounters
|
||||
summaries.Where(x => x.Data.IncludedEncounterCount > 0).Should().OnlyContain(s => s.Data.MortalityRate == 1);
|
||||
summaries.Should().Contain(s => s.Data.ExcludedEncountersAboveMedian > 0);
|
||||
await VerifySummaries(summaries);
|
||||
}
|
||||
|
||||
private FilterChipItem CreateDischargeDateFilter() => new()
|
||||
{
|
||||
ChipType = ChipType.DateRange,
|
||||
Filters = [],
|
||||
DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)]
|
||||
};
|
||||
|
||||
private FilterChipItem CreateBooleanFilter(ChipKey key)
|
||||
{
|
||||
if (key is ChipKey.Mortality)
|
||||
{
|
||||
return new FilterChipItem
|
||||
{
|
||||
ChipType = ChipType.Boolean,
|
||||
GlobalId = "Mortality",
|
||||
IsSelected = true, // determines true/false of filter
|
||||
Key = ChipKey.Mortality,
|
||||
Operator = OperatorType.In,
|
||||
Filters = new List<TreeNode>()
|
||||
{
|
||||
new TreeNode
|
||||
{
|
||||
DisplayText = "Yes"
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unsupported boolean filter: {key}", nameof(key));
|
||||
}
|
||||
|
||||
private ExplorationInfo CreateExplorationInfo(FilterChipItem? additionalFilter = null, ExclusionCriteria? exclusion = null)
|
||||
{
|
||||
var dischargeDateFilter = CreateDischargeDateFilter();
|
||||
var filterChipItems = additionalFilter != null ?
|
||||
new List<FilterChipItem> { dischargeDateFilter, additionalFilter }
|
||||
: new List<FilterChipItem> { dischargeDateFilter };
|
||||
|
||||
return new ExplorationInfo
|
||||
{
|
||||
ConfigurationGuid = _configurationGuid,
|
||||
ExclusionCriteria = exclusion ?? new ExclusionCriteria(),
|
||||
Filters = new Biz.Exploration.Filters.Filter
|
||||
{
|
||||
DischargeDateStart = dischargeDateFilter.DateRange.First(),
|
||||
DischargeDateEnd = dischargeDateFilter.DateRange.Last(),
|
||||
FilterChipItems = filterChipItems
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task AssertExclusionsWereApplied(IEnumerable<CaseTypeFamilySummaryData> summaries)
|
||||
{
|
||||
summaries.Should().HaveCount(_totalCaseTypeFamilyCount);
|
||||
summaries.Should().Contain(s => s.Data.ExcludedEncountersAboveMedian > 0);
|
||||
}
|
||||
|
||||
private Task VerifySummaries(IEnumerable<CaseTypeFamilySummaryData> summaries)
|
||||
{
|
||||
summaries.Should().NotBeNull().And.NotBeEmpty();
|
||||
summaries.Should().OnlyContain(s => !string.IsNullOrWhiteSpace(s.Key));
|
||||
summaries.Should().OnlyContain(s => !string.IsNullOrWhiteSpace(s.Name));
|
||||
summaries.Should().AllSatisfy(s => s.Key.Should().MatchRegex(@"^\d+-\d+$", "Key should follow format 'level-id'"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.Should().NotBeNull("Each summary should have data"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.Key.Should().MatchRegex(@"^\d+-\d+$", "Key should follow format 'level-id'"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.Id.Should().BeGreaterThan(0, "Each summary data should have a valid ID"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.TotalEncounters.Should().BeGreaterThanOrEqualTo(0, "Each summary data should have a valid TotalEncounters"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.TotalVariationEncounters.Should().BeGreaterThanOrEqualTo(0, "Each summary data should have a valid TotalVariationEncounters"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.IncludedEncounterCount.Should().BeGreaterThanOrEqualTo(0, "Each summary data should have a valid IncludedEncounterCount"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.IdentifiedVariation.Should().BeGreaterThanOrEqualTo(0, "Each summary data should have a valid IdentifiedVariation"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.MortalityRate.Should().BeInRange(0, 1, "MortalityRate should be between 0 and 1"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.MedianLos.Should().BeGreaterOrEqualTo(0, "MedianLos should be non-negative"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.MedianCost.Should().BeGreaterOrEqualTo(0, "MedianCost should be non-negative"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.IncludedEncountersBelowMedian.Should().BeGreaterOrEqualTo(0, "IncludedEncountersBelowMedian should be non-negative"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.IncludedEncountersAboveMedian.Should().BeGreaterOrEqualTo(0, "IncludedEncountersAboveMedian should be non-negative"));
|
||||
summaries.Should().AllSatisfy(s => s.Data.IncludedVariation.Should().BeGreaterOrEqualTo(0, "IncludedVariation should be non-negative"));
|
||||
return Verify(summaries, verifySettings)
|
||||
.UseDirectory("Exploration")
|
||||
.UseFileName(TestContext.CurrentContext.TestName());
|
||||
}
|
||||
|
||||
private async Task RunExclusionSummaryTests(ExclusionCriteria exclusions)
|
||||
{
|
||||
// act
|
||||
var explorationInfo = CreateExplorationInfo(null, exclusions);
|
||||
var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None);
|
||||
|
||||
// assert
|
||||
await AssertExclusionsWereApplied(summaries);
|
||||
await VerifySummaries(summaries);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> ExclusionCriteriaTestCases()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new ExclusionCriteria
|
||||
{
|
||||
CostExclusionType = ExclusionCriteriaType.Mad,
|
||||
CostExclusionValue = 2,
|
||||
LosExclusionType = ExclusionCriteriaType.Mad,
|
||||
LosExclusionValue = 2
|
||||
}
|
||||
).SetName("GetCaseTypeFamilySummaries_WithMADExclusions_ReturnsWithExclusions");
|
||||
|
||||
yield return new TestCaseData(
|
||||
new ExclusionCriteria
|
||||
{
|
||||
CostExclusionType = ExclusionCriteriaType.Iqr,
|
||||
CostExclusionValue = 1.5,
|
||||
LosExclusionType = ExclusionCriteriaType.Iqr,
|
||||
LosExclusionValue = 1.5
|
||||
}
|
||||
).SetName("GetCaseTypeFamilySummaries_WithIQRExclusions_ReturnsWithExclusions");
|
||||
}
|
||||
}
|
||||
}
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
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)),
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Queries;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates that the SQL generated by StrategicOpportunityGLDetailQueryBuilder
|
||||
/// is syntactically correct and references valid objects in Snowflake.
|
||||
/// Uses EXPLAIN to parse and plan each statement without executing DML.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
[TestFixture, Category("Integration"), Category("CCI")]
|
||||
public class StrategicOpportunityGLQueryBuilderIntegrationTests : IntegrationTestBase
|
||||
{
|
||||
private StrategicOpportunityGLDetailQueryBuilder _queryBuilder;
|
||||
|
||||
private static readonly List<int> DepartmentIds = [1];
|
||||
private static readonly List<int> RevenueAccountIds = [1];
|
||||
private static readonly List<int> ExpenseAccountIds = [2];
|
||||
private static readonly List<int> StatisticAccountIds = [3];
|
||||
private static readonly DateOnly BaselineStart = new(2024, 1, 1);
|
||||
private static readonly DateOnly BaselineEnd = new(2024, 12, 31);
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_queryBuilder = new StrategicOpportunityGLDetailQueryBuilder();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BuildInsertQuery_FwTable_IsValidSnowflakeSql()
|
||||
{
|
||||
var query = _queryBuilder.BuildInsertQuery(
|
||||
opportunityId: 0,
|
||||
sourceTable: "fw.factglsampled",
|
||||
departmentIds: DepartmentIds,
|
||||
revenueAccountIds: RevenueAccountIds,
|
||||
expenseAccountIds: ExpenseAccountIds,
|
||||
statisticAccountIds: StatisticAccountIds,
|
||||
baselineStart: BaselineStart,
|
||||
baselineEnd: BaselineEnd);
|
||||
|
||||
var act = async () => await SnowflakeDatabaseContext.QueryAsync<dynamic>($"EXPLAIN {query}", nameof(BuildInsertQuery_FwTable_IsValidSnowflakeSql));
|
||||
await act.Should().NotThrowAsync("the generated INSERT SQL should be valid Snowflake syntax");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BuildInsertQuery_IntTable_IsValidSnowflakeSql()
|
||||
{
|
||||
var query = _queryBuilder.BuildInsertQuery(
|
||||
opportunityId: 0,
|
||||
sourceTable: "int.factgl",
|
||||
departmentIds: DepartmentIds,
|
||||
revenueAccountIds: RevenueAccountIds,
|
||||
expenseAccountIds: ExpenseAccountIds,
|
||||
statisticAccountIds: StatisticAccountIds,
|
||||
baselineStart: BaselineStart,
|
||||
baselineEnd: BaselineEnd);
|
||||
|
||||
var act = async () => await SnowflakeDatabaseContext.QueryAsync<dynamic>($"EXPLAIN {query}", nameof(BuildInsertQuery_IntTable_IsValidSnowflakeSql));
|
||||
await act.Should().NotThrowAsync("the generated INSERT SQL should be valid Snowflake syntax");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BuildMergeQuery_FwTable_IsValidSnowflakeSql()
|
||||
{
|
||||
var query = _queryBuilder.BuildMergeQuery(
|
||||
opportunityId: 0,
|
||||
sourceTable: "fw.factglsampled",
|
||||
departmentIds: DepartmentIds,
|
||||
revenueAccountIds: RevenueAccountIds,
|
||||
expenseAccountIds: ExpenseAccountIds,
|
||||
statisticAccountIds: StatisticAccountIds,
|
||||
trackingStart: new DateOnly(2025, 1, 1),
|
||||
trackingEnd: new DateOnly(2025, 12, 31));
|
||||
|
||||
var act = async () => await SnowflakeDatabaseContext.QueryAsync<dynamic>($"EXPLAIN {query}", nameof(BuildMergeQuery_FwTable_IsValidSnowflakeSql));
|
||||
await act.Should().NotThrowAsync("the generated MERGE SQL should be valid Snowflake syntax");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BuildDeleteQuery_WithOpportunityId_IsValidSnowflakeSql()
|
||||
{
|
||||
var query = _queryBuilder.BuildDeleteQuery(opportunityId: 0);
|
||||
|
||||
var act = async () => await SnowflakeDatabaseContext.QueryAsync<dynamic>($"EXPLAIN {query}", nameof(BuildDeleteQuery_WithOpportunityId_IsValidSnowflakeSql));
|
||||
await act.Should().NotThrowAsync("the generated DELETE SQL should be valid Snowflake syntax");
|
||||
}
|
||||
}
|
||||
}
|
||||
+411
@@ -0,0 +1,411 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests
|
||||
{
|
||||
public static class StrategicOpportunityQueryConstants
|
||||
{
|
||||
public static readonly string TrackingSelectSql = @"SELECT
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup"",
|
||||
ZEROIFNULL(""Units"") AS ""Units"",
|
||||
ZEROIFNULL(""Cost"") AS ""Cost""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup"",
|
||||
SUM(""Units"") AS ""Units"",
|
||||
SUM(""Cost"") AS ""Cost""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
DSSPES.encounterid AS ""dsspesEncounterId"",
|
||||
DSSPES.dischargedatetime AS ""dsspesDischargeDateTime"",
|
||||
DSSPES.entityid AS ""dsspesEntityID"",
|
||||
DSSPES.primaryphysicianid AS ""dsspesPhysicianId"",
|
||||
DSSPES.servicelineid AS ""dsspesPrimaryServiceLineId"",
|
||||
DSSPES.cptcodecasetypefamilyid AS ""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
DSSPES.msdrgcasetypefamilyid AS ""dsspesMSDRGCaseTypeFamilyId"",
|
||||
DSSPES.aprdrgcasetypefamilyid AS ""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
PBLID.departmentid AS ""pblidDepartmentId"",
|
||||
PHYSPRIMARY.physicianspecialtyid AS ""physPrimaryPhysicianSpecialtyId"",
|
||||
CC.chargecodeid AS ""ccChargeCodeID"",
|
||||
IFNULL(CC.costdriver::TEXT, '') AS ""ccCostDriver"",
|
||||
IFNULL(ENCOUNTERPATIENTTYPEROLLUP.patienttyperollup::TEXT, '') AS ""encounterPatientTypeRollupPatientTypeRollup"",
|
||||
NULL AS ""Cost"",
|
||||
SUM(PBLID.unitsofservice) AS ""Units""
|
||||
|
||||
FROM dss.FactPatientBillingLineItemDetail AS PBLID
|
||||
RIGHT JOIN clientdss.FactPatientEncounterSummary AS DSSPES ON DSSPES.encounterid = PBLID.encounterid
|
||||
LEFT JOIN dss.FactPatientEncounterClinicalIndicator AS DSSPECI ON DSSPECI.encounterid = PBLID.encounterid
|
||||
LEFT JOIN CCI.VIEWENCOUNTERPATIENTTYPEROLLUP AS ENCOUNTERPATIENTTYPEROLLUP ON ENCOUNTERPATIENTTYPEROLLUP.encounterid = PBLID.encounterid
|
||||
INNER JOIN FW.DIMCHARGECODE AS CC ON CC.chargecodeid = PBLID.chargecodeid
|
||||
INNER JOIN DSS.DIMPHYSICIAN AS PHYSPRIMARY ON PHYSPRIMARY.physicianid = DSSPES.primaryphysicianid
|
||||
|
||||
WHERE
|
||||
((DSSPES.dischargedatetime >= :dischargedatetime_Start_0 AND DSSPES.dischargedatetime <= :dischargedatetime_End_1) AND DSSPECI.clinicalindicatorid = :clinicalindicatorid_2)
|
||||
|
||||
GROUP BY
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup""
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
DSSPES.encounterid AS ""dsspesEncounterId"",
|
||||
DSSPES.dischargedatetime AS ""dsspesDischargeDateTime"",
|
||||
DSSPES.entityid AS ""dsspesEntityID"",
|
||||
DSSPES.primaryphysicianid AS ""dsspesPhysicianId"",
|
||||
DSSPES.servicelineid AS ""dsspesPrimaryServiceLineId"",
|
||||
DSSPES.cptcodecasetypefamilyid AS ""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
DSSPES.msdrgcasetypefamilyid AS ""dsspesMSDRGCaseTypeFamilyId"",
|
||||
DSSPES.aprdrgcasetypefamilyid AS ""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
PBLID.departmentid AS ""pblidDepartmentId"",
|
||||
PHYSPRIMARY.physicianspecialtyid AS ""physPrimaryPhysicianSpecialtyId"",
|
||||
CC.chargecodeid AS ""ccChargeCodeID"",
|
||||
IFNULL(CC.costdriver::TEXT, '') AS ""ccCostDriver"",
|
||||
IFNULL(ENCOUNTERPATIENTTYPEROLLUP.patienttyperollup::TEXT, '') AS ""encounterPatientTypeRollupPatientTypeRollup"",
|
||||
SUM(COSTDETAIL.variabledirectcost) AS ""Cost"",
|
||||
NULL AS ""Units""
|
||||
|
||||
FROM dss.FactPatientCostDetail AS COSTDETAIL
|
||||
RIGHT JOIN dss.FactPatientBillingLineItemDetail AS PBLID ON PBLID.rowid = COSTDETAIL.pblidrowid
|
||||
RIGHT JOIN clientdss.FactPatientEncounterSummary AS DSSPES ON DSSPES.encounterid = PBLID.encounterid
|
||||
LEFT JOIN dss.FactPatientEncounterClinicalIndicator AS DSSPECI ON DSSPECI.encounterid = PBLID.encounterid
|
||||
LEFT JOIN CCI.VIEWENCOUNTERPATIENTTYPEROLLUP AS ENCOUNTERPATIENTTYPEROLLUP ON ENCOUNTERPATIENTTYPEROLLUP.encounterid = PBLID.encounterid
|
||||
INNER JOIN FW.DIMCHARGECODE AS CC ON CC.chargecodeid = PBLID.chargecodeid
|
||||
INNER JOIN DSS.DIMPHYSICIAN AS PHYSPRIMARY ON PHYSPRIMARY.physicianid = DSSPES.primaryphysicianid
|
||||
|
||||
WHERE
|
||||
((DSSPES.dischargedatetime >= :dischargedatetime_Start_3 AND DSSPES.dischargedatetime <= :dischargedatetime_End_4) AND DSSPECI.clinicalindicatorid = :clinicalindicatorid_5)
|
||||
|
||||
GROUP BY
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup""
|
||||
|
||||
|
||||
)
|
||||
|
||||
GROUP BY
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup""
|
||||
|
||||
|
||||
)
|
||||
|
||||
ORDER BY
|
||||
""dsspesEncounterId"" ASC NULLS FIRST,
|
||||
""dsspesDischargeDateTime"" ASC NULLS FIRST,
|
||||
""dsspesEntityID"" ASC NULLS FIRST,
|
||||
""dsspesPhysicianId"" ASC NULLS FIRST,
|
||||
""dsspesPrimaryServiceLineId"" ASC NULLS FIRST,
|
||||
""dsspesCPTCodeCaseTypeFamilyId"" ASC NULLS FIRST,
|
||||
""dsspesMSDRGCaseTypeFamilyId"" ASC NULLS FIRST,
|
||||
""dsspesAPRDRGCaseTypeFamilyId"" ASC NULLS FIRST,
|
||||
""pblidDepartmentId"" ASC NULLS FIRST,
|
||||
""physPrimaryPhysicianSpecialtyId"" ASC NULLS FIRST,
|
||||
""ccChargeCodeID"" ASC NULLS FIRST,
|
||||
""ccCostDriver"" ASC NULLS FIRST,
|
||||
""encounterPatientTypeRollupPatientTypeRollup"" ASC NULLS FIRST
|
||||
";
|
||||
|
||||
// SQL returned by BuildSqlQuery for the measure detail query (PopulateMeasureDetails, line 215).
|
||||
// Uses real column names from clientdss.FactPatientEncounterSummary and dss.FactPatientBillingLineItemDetail
|
||||
// that exist in the test client's Snowflake database.
|
||||
public static readonly string MeasureSelectSql = @"SELECT
|
||||
""DischargeDateTime"",
|
||||
ZEROIFNULL(""Value"") AS ""Value""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
""DischargeDateTime"",
|
||||
SUM(""Value"") AS ""Value""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
DSSPES.dischargedatetime AS ""DischargeDateTime"",
|
||||
SUM(PBLID.unitsofservice) AS ""Value""
|
||||
|
||||
FROM dss.FactPatientBillingLineItemDetail AS PBLID
|
||||
RIGHT JOIN clientdss.FactPatientEncounterSummary AS DSSPES ON DSSPES.encounterid = PBLID.encounterid
|
||||
|
||||
WHERE
|
||||
(DSSPES.dischargedatetime >= :dischargedatetime_Start_0 AND DSSPES.dischargedatetime <= :dischargedatetime_End_1)
|
||||
|
||||
GROUP BY
|
||||
""DischargeDateTime""
|
||||
|
||||
|
||||
)
|
||||
|
||||
GROUP BY
|
||||
""DischargeDateTime""
|
||||
|
||||
|
||||
)
|
||||
|
||||
ORDER BY
|
||||
""DischargeDateTime"" ASC NULLS FIRST
|
||||
";
|
||||
|
||||
// SQL for the tracking merge when opportunity uses an ExplorationPopulation instead of PatientPopulationHPath.
|
||||
// Contains LEFT JOIN CCI.FACTEXPLORATIONPOPULATIONENCOUNTER which ApplyExplorationFilters replaces
|
||||
// with an INNER JOIN subquery at runtime.
|
||||
public static readonly string ExplorationTrackingSelectSql = @"SELECT
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup"",
|
||||
ZEROIFNULL(""Units"") AS ""Units"",
|
||||
ZEROIFNULL(""Cost"") AS ""Cost""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup"",
|
||||
SUM(""Units"") AS ""Units"",
|
||||
SUM(""Cost"") AS ""Cost""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
DSSPES.encounterid AS ""dsspesEncounterId"",
|
||||
DSSPES.dischargedatetime AS ""dsspesDischargeDateTime"",
|
||||
DSSPES.entityid AS ""dsspesEntityID"",
|
||||
DSSPES.primaryphysicianid AS ""dsspesPhysicianId"",
|
||||
DSSPES.servicelineid AS ""dsspesPrimaryServiceLineId"",
|
||||
DSSPES.cptcodecasetypefamilyid AS ""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
DSSPES.msdrgcasetypefamilyid AS ""dsspesMSDRGCaseTypeFamilyId"",
|
||||
DSSPES.aprdrgcasetypefamilyid AS ""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
PBLID.departmentid AS ""pblidDepartmentId"",
|
||||
PHYSPRIMARY.physicianspecialtyid AS ""physPrimaryPhysicianSpecialtyId"",
|
||||
CC.chargecodeid AS ""ccChargeCodeID"",
|
||||
IFNULL(CC.costdriver::TEXT, '') AS ""ccCostDriver"",
|
||||
IFNULL(ENCOUNTERPATIENTTYPEROLLUP.patienttyperollup::TEXT, '') AS ""encounterPatientTypeRollupPatientTypeRollup"",
|
||||
NULL AS ""Cost"",
|
||||
SUM(PBLID.unitsofservice) AS ""Units""
|
||||
|
||||
FROM dss.FactPatientBillingLineItemDetail AS PBLID
|
||||
RIGHT JOIN clientdss.FactPatientEncounterSummary AS DSSPES ON DSSPES.encounterid = PBLID.encounterid
|
||||
LEFT JOIN CCI.FACTEXPLORATIONPOPULATIONENCOUNTER AS EXPLORATIONPOPULATIONENCOUNTER ON EXPLORATIONPOPULATIONENCOUNTER.encounterid = PBLID.encounterid
|
||||
LEFT JOIN CCI.VIEWENCOUNTERPATIENTTYPEROLLUP AS ENCOUNTERPATIENTTYPEROLLUP ON ENCOUNTERPATIENTTYPEROLLUP.encounterid = PBLID.encounterid
|
||||
INNER JOIN FW.DIMCHARGECODE AS CC ON CC.chargecodeid = PBLID.chargecodeid
|
||||
INNER JOIN DSS.DIMPHYSICIAN AS PHYSPRIMARY ON PHYSPRIMARY.physicianid = DSSPES.primaryphysicianid
|
||||
|
||||
WHERE
|
||||
((DSSPES.dischargedatetime >= :dischargedatetime_Start_0 AND DSSPES.dischargedatetime <= :dischargedatetime_End_1) AND EXPLORATIONPOPULATIONENCOUNTER.explorationpopulationid = :explorationpopulationid_2)
|
||||
|
||||
GROUP BY
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup""
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
DSSPES.encounterid AS ""dsspesEncounterId"",
|
||||
DSSPES.dischargedatetime AS ""dsspesDischargeDateTime"",
|
||||
DSSPES.entityid AS ""dsspesEntityID"",
|
||||
DSSPES.primaryphysicianid AS ""dsspesPhysicianId"",
|
||||
DSSPES.servicelineid AS ""dsspesPrimaryServiceLineId"",
|
||||
DSSPES.cptcodecasetypefamilyid AS ""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
DSSPES.msdrgcasetypefamilyid AS ""dsspesMSDRGCaseTypeFamilyId"",
|
||||
DSSPES.aprdrgcasetypefamilyid AS ""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
PBLID.departmentid AS ""pblidDepartmentId"",
|
||||
PHYSPRIMARY.physicianspecialtyid AS ""physPrimaryPhysicianSpecialtyId"",
|
||||
CC.chargecodeid AS ""ccChargeCodeID"",
|
||||
IFNULL(CC.costdriver::TEXT, '') AS ""ccCostDriver"",
|
||||
IFNULL(ENCOUNTERPATIENTTYPEROLLUP.patienttyperollup::TEXT, '') AS ""encounterPatientTypeRollupPatientTypeRollup"",
|
||||
SUM(COSTDETAIL.variabledirectcost) AS ""Cost"",
|
||||
NULL AS ""Units""
|
||||
|
||||
FROM dss.FactPatientCostDetail AS COSTDETAIL
|
||||
RIGHT JOIN dss.FactPatientBillingLineItemDetail AS PBLID ON PBLID.rowid = COSTDETAIL.pblidrowid
|
||||
RIGHT JOIN clientdss.FactPatientEncounterSummary AS DSSPES ON DSSPES.encounterid = PBLID.encounterid
|
||||
LEFT JOIN CCI.FACTEXPLORATIONPOPULATIONENCOUNTER AS EXPLORATIONPOPULATIONENCOUNTER ON EXPLORATIONPOPULATIONENCOUNTER.encounterid = PBLID.encounterid
|
||||
LEFT JOIN CCI.VIEWENCOUNTERPATIENTTYPEROLLUP AS ENCOUNTERPATIENTTYPEROLLUP ON ENCOUNTERPATIENTTYPEROLLUP.encounterid = PBLID.encounterid
|
||||
INNER JOIN FW.DIMCHARGECODE AS CC ON CC.chargecodeid = PBLID.chargecodeid
|
||||
INNER JOIN DSS.DIMPHYSICIAN AS PHYSPRIMARY ON PHYSPRIMARY.physicianid = DSSPES.primaryphysicianid
|
||||
|
||||
WHERE
|
||||
((DSSPES.dischargedatetime >= :dischargedatetime_Start_3 AND DSSPES.dischargedatetime <= :dischargedatetime_End_4) AND EXPLORATIONPOPULATIONENCOUNTER.explorationpopulationid = :explorationpopulationid_5)
|
||||
|
||||
GROUP BY
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup""
|
||||
|
||||
|
||||
)
|
||||
|
||||
GROUP BY
|
||||
""dsspesEncounterId"",
|
||||
""dsspesDischargeDateTime"",
|
||||
""dsspesEntityID"",
|
||||
""dsspesPhysicianId"",
|
||||
""dsspesPrimaryServiceLineId"",
|
||||
""dsspesCPTCodeCaseTypeFamilyId"",
|
||||
""dsspesMSDRGCaseTypeFamilyId"",
|
||||
""dsspesAPRDRGCaseTypeFamilyId"",
|
||||
""pblidDepartmentId"",
|
||||
""physPrimaryPhysicianSpecialtyId"",
|
||||
""ccChargeCodeID"",
|
||||
""ccCostDriver"",
|
||||
""encounterPatientTypeRollupPatientTypeRollup""
|
||||
|
||||
|
||||
)
|
||||
|
||||
ORDER BY
|
||||
""dsspesEncounterId"" ASC NULLS FIRST,
|
||||
""dsspesDischargeDateTime"" ASC NULLS FIRST,
|
||||
""dsspesEntityID"" ASC NULLS FIRST,
|
||||
""dsspesPhysicianId"" ASC NULLS FIRST,
|
||||
""dsspesPrimaryServiceLineId"" ASC NULLS FIRST,
|
||||
""dsspesCPTCodeCaseTypeFamilyId"" ASC NULLS FIRST,
|
||||
""dsspesMSDRGCaseTypeFamilyId"" ASC NULLS FIRST,
|
||||
""dsspesAPRDRGCaseTypeFamilyId"" ASC NULLS FIRST,
|
||||
""pblidDepartmentId"" ASC NULLS FIRST,
|
||||
""physPrimaryPhysicianSpecialtyId"" ASC NULLS FIRST,
|
||||
""ccChargeCodeID"" ASC NULLS FIRST,
|
||||
""ccCostDriver"" ASC NULLS FIRST,
|
||||
""encounterPatientTypeRollupPatientTypeRollup"" ASC NULLS FIRST
|
||||
";
|
||||
|
||||
// SQL for exploration-path tests (instead of clinical indicator)
|
||||
public static readonly string ExplorationMeasureSelectSql = @"SELECT
|
||||
""DischargeDateTime"",
|
||||
ZEROIFNULL(""Value"") AS ""Value""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
""DischargeDateTime"",
|
||||
SUM(""Value"") AS ""Value""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
DSSPES.dischargedatetime AS ""DischargeDateTime"",
|
||||
SUM(PBLID.unitsofservice) AS ""Value""
|
||||
|
||||
FROM dss.FactPatientBillingLineItemDetail AS PBLID
|
||||
RIGHT JOIN clientdss.FactPatientEncounterSummary AS DSSPES ON DSSPES.encounterid = PBLID.encounterid
|
||||
LEFT JOIN CCI.FACTEXPLORATIONPOPULATIONENCOUNTER AS EXPLORATIONPOPULATIONENCOUNTER ON EXPLORATIONPOPULATIONENCOUNTER.encounterid = PBLID.encounterid
|
||||
|
||||
WHERE
|
||||
((DSSPES.dischargedatetime >= :dischargedatetime_Start_0 AND DSSPES.dischargedatetime <= :dischargedatetime_End_1) AND EXPLORATIONPOPULATIONENCOUNTER.explorationpopulationid = :explorationpopulationid_2)
|
||||
|
||||
GROUP BY
|
||||
""DischargeDateTime""
|
||||
|
||||
|
||||
)
|
||||
|
||||
GROUP BY
|
||||
""DischargeDateTime""
|
||||
|
||||
|
||||
)
|
||||
|
||||
ORDER BY
|
||||
""DischargeDateTime"" ASC NULLS FIRST
|
||||
";
|
||||
}
|
||||
}
|
||||
+653
@@ -0,0 +1,653 @@
|
||||
using FluentAssertions;
|
||||
using Hangfire;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using Strata.ContinuousImprovement.Biz.Generics;
|
||||
using Strata.ContinuousImprovement.Biz.InitiativeRollupColumns;
|
||||
using Strata.ContinuousImprovement.Biz.OpportunityRoleAssignment;
|
||||
using Strata.ContinuousImprovement.Biz.OpportunityWorkbooks;
|
||||
using Strata.ContinuousImprovement.Biz.Security;
|
||||
using Strata.ContinuousImprovement.Biz.StrategicOpportunities;
|
||||
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.DistributionProcess;
|
||||
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Dtos;
|
||||
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Enums;
|
||||
using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities;
|
||||
using Strata.CoreLib.Claims.Extensions;
|
||||
using Strata.DataSchema.Client;
|
||||
using Strata.DataSchema.Models.Query;
|
||||
using Strata.DataSchema.Models.Schema;
|
||||
using Strata.Id.Client;
|
||||
using Strata.Schema.Client;
|
||||
using Strata.Schema.Client.Dtos;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
[Ignore("temporarily ignoring because schema is out of sync")]
|
||||
[TestFixture, Category("Integration"), Category("CCI")]
|
||||
public class StrategicOpportunityServiceTests : IntegrationTestBase
|
||||
{
|
||||
private const string POPULATION_NAME = "Mocked Population Name";
|
||||
private IStrategicOpportunityService _strategicOpportunityService;
|
||||
private IOpportunityRoleAssignmentService _opportunityRoleAssignmentService;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void Setup()
|
||||
{
|
||||
// Arrange mocks
|
||||
var mockSchemaServiceClient = new Mock<ISchemaServiceClient>();
|
||||
var mockDataSchemaService = new Mock<IDataSchemaService>();
|
||||
var mockIdServiceClient = new Mock<IIdServiceClient>();
|
||||
var mockIdRollupColumn = new Mock<IInitiativeRollupColumnService>();
|
||||
var mockOpportunityWorkbookService = new Mock<IOpportunityWorkbookService>();
|
||||
var mockStrategicOpportunityDetailService = new Mock<IStrategicChargeCodeOpportunityDetailService>();
|
||||
var mockStrategicGLOpportunityDetailService = new Mock<IStrategicGLOpportunityDetailService>();
|
||||
var mockSimpleSecurityServiceClient = new Mock<ISimpleSecurityService>();
|
||||
var mockOpportunityRoleAssignmentServiceClient = new Mock<IOpportunityRoleAssignmentService>();
|
||||
var mockBackgroundJobClient = new Mock<IBackgroundJobClient>();
|
||||
var mockDistributionProcessFactory = new Mock<IDistributionProcessFactory>();
|
||||
|
||||
// Add mock responses
|
||||
mockDataSchemaService.Setup(x =>
|
||||
x.GetDataTableBySqlFullNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new DataTable());
|
||||
|
||||
mockDataSchemaService.Setup(x =>
|
||||
x.GetDataColumnsBySqlColumnNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([]);
|
||||
|
||||
mockDataSchemaService.Setup(x =>
|
||||
x.BuildSqlQuery(It.IsAny<QueryConfig>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new SqlResponse
|
||||
{
|
||||
SqlQuery = DataSchemaServiceSelectSql,
|
||||
Parameters = new List<KeyValuePair<string, object>>
|
||||
{
|
||||
new(":dischargedatetime_Start_0", new DateTime(2024, 1, 1)),
|
||||
new(":dischargedatetime_End_1", new DateTime(2024, 12, 31)),
|
||||
new(":chargecodeid_2", 3541)
|
||||
}
|
||||
});
|
||||
|
||||
mockSchemaServiceClient.Setup(x =>
|
||||
x.GetDimensionByGlobalIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new Schema.Client.Dtos.Info.ScoreDimensionInfoDto());
|
||||
|
||||
var list = new List<HierarchyNodeDto>();
|
||||
list.Add(new HierarchyNodeDto
|
||||
{
|
||||
Path = "CLINICALINDICATOR|CLINICALINDI|188",
|
||||
Name = POPULATION_NAME
|
||||
});
|
||||
|
||||
mockSchemaServiceClient.Setup(x =>
|
||||
x.GetHierarchyNodesFromHierarchyPathsAsync(It.IsAny<Guid>(), It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(list);
|
||||
|
||||
mockSchemaServiceClient.Setup(x =>
|
||||
x.GetDimensionMembersFromHierarchyPathsAsync(It.IsAny<Guid>(), It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<MemberDto> {
|
||||
new MemberDto
|
||||
{
|
||||
Id = "188",
|
||||
Name = "All Readmissions"
|
||||
}});
|
||||
|
||||
var claimsPrincipalAccessor = TestUtilities.GetClaimsPrincipalAccessor();
|
||||
_opportunityRoleAssignmentService = new OpportunityRoleAssignmentService(
|
||||
(DbContexts.CentralDbContext)CentralDbContext,
|
||||
mockIdServiceClient.Object,
|
||||
mockSimpleSecurityServiceClient.Object,
|
||||
claimsPrincipalAccessor
|
||||
);
|
||||
|
||||
_strategicOpportunityService = new StrategicOpportunityService(
|
||||
JazzConnBuilderFactory,
|
||||
(DbContexts.CentralDbContext)CentralDbContext,
|
||||
claimsPrincipalAccessor,
|
||||
mockSchemaServiceClient.Object,
|
||||
SnowflakeDatabaseContext,
|
||||
mockIdServiceClient.Object,
|
||||
_opportunityRoleAssignmentService,
|
||||
mockIdRollupColumn.Object,
|
||||
mockOpportunityWorkbookService.Object,
|
||||
mockBackgroundJobClient.Object,
|
||||
mockStrategicOpportunityDetailService.Object,
|
||||
new StrategicItemDimensionSyncService(JazzConnBuilderFactory, (DbContexts.CentralDbContext)CentralDbContext, SnowflakeDatabaseContext),
|
||||
mockStrategicGLOpportunityDetailService.Object,
|
||||
(Microsoft.Extensions.Logging.ILogger<StrategicOpportunityService>)logger);
|
||||
}
|
||||
|
||||
[Test, Ignore("temporarily ignoring because schema is out of sync")]
|
||||
public async Task CreatingOpportunityShouldAlsoCreateDetails()
|
||||
{
|
||||
//arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
|
||||
//act
|
||||
var result = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
//assert
|
||||
result.Should().Be(1, "On success should return opportunity id.");
|
||||
}
|
||||
|
||||
private static StrategicOpportunityChargeCodeDto CreateOpportunityDto()
|
||||
{
|
||||
var opportunity = new StrategicOpportunityChargeCodeDto()
|
||||
{
|
||||
Name = "Integration-StrategicOpportunityChargeCode",
|
||||
OpportunityType = StrategicOpportunityTypes.ChargeCode,
|
||||
OpportunityFiltersJSON = "[]",
|
||||
Rollup1Id = 0,
|
||||
Rollup2Id = 0,
|
||||
Rollup3Id = 0,
|
||||
BaselineType = BaselineType.MonthRange,
|
||||
BaselineDistribution = DistributionMethod.Average,
|
||||
BaselineStartDate = new DateOnly(2024, 1, 1),
|
||||
BaselineEndDate = new DateOnly(2024, 12, 31),
|
||||
EstimatedTrackingDuration = 12,
|
||||
|
||||
// Assume empty or test collections for these
|
||||
Measures = Enumerable.Empty<StrategicOpportunityMeasureDto>(),
|
||||
WorkbookIds = Enumerable.Empty<int>(),
|
||||
|
||||
// ChargeCode-specific properties
|
||||
PatientPopulationHPath = "CLINICALINDICATOR|CLINICALINDI|188",
|
||||
ExplorationPopulationId = 0,
|
||||
ChargeCodeHPath = "CC|CCRU|Blood"
|
||||
};
|
||||
return opportunity;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateStrategicOpportunityRampUpsAsync_ShouldUpdateRampUps()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
var rampUps = new List<StrategicOpportunityRampUpDto>
|
||||
{
|
||||
new() { OpportunityId = opportunityId, MonthNumber = 1, Percentage = 0.1 },
|
||||
new() { OpportunityId = opportunityId, MonthNumber = 2, Percentage = 0.2 }
|
||||
};
|
||||
|
||||
// Act
|
||||
var updateResult = await _strategicOpportunityService.UpdateStrategicOpportunityRampUpsAsync(opportunityId, rampUps, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
updateResult.Should().BeTrue();
|
||||
|
||||
// Verify persisted values
|
||||
var retrieved = await _strategicOpportunityService.GetStrategicOpportunityRampUpsAsync(opportunityId, CancellationToken.None);
|
||||
retrieved.Should().HaveCount(rampUps.Count);
|
||||
retrieved[0].MonthNumber.Should().Be(rampUps[0].MonthNumber);
|
||||
retrieved[0].Percentage.Should().Be(rampUps[0].Percentage);
|
||||
retrieved[1].MonthNumber.Should().Be(rampUps[1].MonthNumber);
|
||||
retrieved[1].Percentage.Should().Be(rampUps[1].Percentage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResetStrategicOpportunityRampUps_ShouldResetRampUps()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
var rampUps = new List<StrategicOpportunityRampUpDto>
|
||||
{
|
||||
new() { OpportunityId = opportunityId, MonthNumber = 1, Percentage = 0.1 },
|
||||
new() { OpportunityId = opportunityId, MonthNumber = 2, Percentage = 0.2 }
|
||||
};
|
||||
|
||||
await _strategicOpportunityService.UpdateStrategicOpportunityRampUpsAsync(opportunityId, rampUps, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var resetResult = await _strategicOpportunityService.ResetStrategicOpportunityRampUps(opportunityId, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resetResult.Should().BeTrue();
|
||||
var retrieved = await _strategicOpportunityService.GetStrategicOpportunityRampUpsAsync(opportunityId, CancellationToken.None);
|
||||
|
||||
// Verify persisted values
|
||||
retrieved.Should().HaveCount(rampUps.Count);
|
||||
retrieved[0].Percentage.Should().Be(1);
|
||||
retrieved[1].Percentage.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResetStrategicOpportunityRampUps_ShouldNotResetRampUps()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var resetResult = await _strategicOpportunityService.ResetStrategicOpportunityRampUps(opportunityId, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resetResult.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateStrategicOpportunityRoleAssignment_ShouldUpdateRoleAssignments()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
var roleAssignments = new List<OpportunityRoleAssignmentView>()
|
||||
{
|
||||
new OpportunityRoleAssignmentView() { LootId = opportunityId, IdentityGuid = Guid.NewGuid(), IdentityName = "Test", CanRead = true, CanWrite = true, CanSecure = false }
|
||||
};
|
||||
|
||||
// Act
|
||||
var saveResult = await _opportunityRoleAssignmentService.SaveRoleAssignmentsAsync(opportunityId, roleAssignments, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
saveResult.Should().BeTrue();
|
||||
|
||||
// Verify persisted values
|
||||
var retrieved = await _opportunityRoleAssignmentService.GetRoleAssignmentsAsync(opportunityId, CancellationToken.None);
|
||||
var userPermission = await _opportunityRoleAssignmentService.GetUserRoleAssignmentsAsync(opportunityId, CancellationToken.None);
|
||||
|
||||
retrieved.Should().HaveCount(roleAssignments.Count);
|
||||
var item = retrieved.FirstOrDefault();
|
||||
item.LootId.Should().Be(opportunityId);
|
||||
item.IdentityGuid.Should().Be(roleAssignments[0].IdentityGuid);
|
||||
item.IdentityName.Should().Be(roleAssignments[0].IdentityName);
|
||||
item.CanRead.Should().Be(roleAssignments[0].CanRead);
|
||||
item.CanWrite.Should().Be(roleAssignments[0].CanWrite);
|
||||
item.CanSecure.Should().Be(roleAssignments[0].CanSecure);
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetStrategicOpportunityRampUpsAsync_ShouldReturnEmptyListIfNoneExist()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentOpportunityId = long.MaxValue;
|
||||
|
||||
// Act
|
||||
var rampUps = await _strategicOpportunityService.GetStrategicOpportunityRampUpsAsync(nonExistentOpportunityId, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
rampUps.Should().NotBeNull();
|
||||
rampUps.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetStrategicOpportunitiesAsync_ShouldReturnOpportunitiesWithRollupsAndPopulation()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var results = await _strategicOpportunityService.GetStrategicOpportunitiesAsync(CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
results.Should().NotBeEmpty("At least one opportunity should be returned");
|
||||
var createdOpportunity = results.FirstOrDefault(o => o.OpportunityId == opportunityId);
|
||||
createdOpportunity.Should().NotBeNull("Created opportunity should be found in results");
|
||||
|
||||
createdOpportunity.OpportunityId.Should().Be(opportunityId);
|
||||
createdOpportunity.Name.Should().Be(opportunity.Name);
|
||||
createdOpportunity.OpportunityTypeName.Should().Be("Charge Code");
|
||||
createdOpportunity.EncounterGroupName.Should().Be("Patient Population", "When PatientPopulationHPath is set, it should be Patient Population");
|
||||
createdOpportunity.AdditionalFilters.Should().Be("No");
|
||||
createdOpportunity.BaselinePeriod.Should()
|
||||
.Be($"{opportunity.BaselineStartDate:MMM d, yyyy} - {opportunity.BaselineEndDate:MMM d, yyyy}");
|
||||
createdOpportunity.EstimatedTrackDurationMonths.Should().Be(opportunity.EstimatedTrackingDuration);
|
||||
|
||||
createdOpportunity.Rollup1.Should().BeEmpty("When rollup1Id is 0, rollup should be empty");
|
||||
createdOpportunity.Rollup2.Should().BeEmpty("When rollup2Id is 0, rollup should be empty");
|
||||
createdOpportunity.Rollup3.Should().BeEmpty("When rollup3Id is 0, rollup should be empty");
|
||||
|
||||
createdOpportunity.PatientPopulationHPath.Should().Be(opportunity.PatientPopulationHPath);
|
||||
createdOpportunity.ExplorationPopulationId.Should().Be(opportunity.ExplorationPopulationId);
|
||||
createdOpportunity.Population.Should().Be(POPULATION_NAME);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateStrategicOpportunityStatus_WhenNewInitiative_ShouldUpdateStatusAndDates()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
var updateStatusDto = new StrategicOpportunityUpdateStatusDto
|
||||
{
|
||||
Status = (byte)StrategicOpportunityStatus.NewInitiative,
|
||||
TrackingStartDate = new DateOnly(2024, 1, 1),
|
||||
TrackingEndDate = new DateOnly(2024, 12, 31)
|
||||
};
|
||||
|
||||
// Act
|
||||
var updateResult = await _strategicOpportunityService.UpdateStrategicOpportunityStatus(opportunityId, updateStatusDto, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
updateResult.Should().BeTrue("Update should be successful");
|
||||
|
||||
var strataId = TestUtilities.GetClaimsPrincipalAccessor()?.GetCurrentClaimsPrincipal().GetStrataId();
|
||||
var opportunityAfterUpdate = await CentralDbContext.StrategicOpportunities
|
||||
.FirstOrDefaultAsync(x => x.OpportunityId == opportunityId && x.StrataId == strataId, CancellationToken.None);
|
||||
|
||||
opportunityAfterUpdate.Should().NotBeNull("Updated opportunity should exist");
|
||||
opportunityAfterUpdate.Status.Should().Be(updateStatusDto.Status, "Status should be updated to NewInitiative");
|
||||
opportunityAfterUpdate.TrackingStartDate.Should().Be(updateStatusDto.TrackingStartDate, "TrackingStartDate should be updated");
|
||||
opportunityAfterUpdate.TrackingEndDate.Should().Be(updateStatusDto.TrackingEndDate, "TrackingEndDate should be updated");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateStrategicOpportunityStatus_WhenNewOpportunity_ShouldResetTrackingData()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
var toInitiativeDto = new StrategicOpportunityUpdateStatusDto
|
||||
{
|
||||
Status = (byte)StrategicOpportunityStatus.NewInitiative,
|
||||
TrackingStartDate = new DateOnly(2024, 1, 1),
|
||||
TrackingEndDate = new DateOnly(2024, 12, 31)
|
||||
};
|
||||
await _strategicOpportunityService.UpdateStrategicOpportunityStatus(opportunityId, toInitiativeDto, CancellationToken.None);
|
||||
|
||||
var toOpportunityDto = new StrategicOpportunityUpdateStatusDto
|
||||
{
|
||||
Status = (byte)StrategicOpportunityStatus.NewOpportunity
|
||||
};
|
||||
|
||||
// Act
|
||||
var updateResult = await _strategicOpportunityService.UpdateStrategicOpportunityStatus(opportunityId, toOpportunityDto, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
updateResult.Should().BeTrue("Update should be successful");
|
||||
|
||||
var strataId = TestUtilities.GetClaimsPrincipalAccessor()?.GetCurrentClaimsPrincipal().GetStrataId();
|
||||
var opportunityAfterUpdate = await CentralDbContext.StrategicOpportunityChargeCodes
|
||||
.FirstOrDefaultAsync(x => x.OpportunityId == opportunityId && x.StrataId == strataId, CancellationToken.None);
|
||||
|
||||
opportunityAfterUpdate.Should().NotBeNull("Updated opportunity should exist");
|
||||
opportunityAfterUpdate.Status.Should().Be((byte)StrategicOpportunityStatus.NewOpportunity, "Status should be reset to NewOpportunity");
|
||||
opportunityAfterUpdate.TrackingStartDate.Should().Be(default(DateOnly), "TrackingStartDate should be reset to default");
|
||||
opportunityAfterUpdate.TrackingEndDate.Should().Be(default(DateOnly), "TrackingEndDate should be reset to default");
|
||||
opportunityAfterUpdate.LastRunTrackingDate.Should().BeNull("LastRunTrackingDate should be cleared");
|
||||
opportunityAfterUpdate.LastRunTrackingStatus.Should().Be(RunTrackingStatus.None, "LastRunTrackingStatus should be reset to None");
|
||||
opportunityAfterUpdate.ProgressToDateActual.Should().Be(0, "ProgressToDateActual should be reset to 0");
|
||||
opportunityAfterUpdate.ProgressToDateCommitted.Should().Be(0, "ProgressToDateCommitted should be reset to 0");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateStrategicOpportunityStatus_WhenOtherStatus_ShouldOnlyUpdateStatus()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
var toInitiativeDto = new StrategicOpportunityUpdateStatusDto
|
||||
{
|
||||
Status = (byte)StrategicOpportunityStatus.NewInitiative,
|
||||
TrackingStartDate = new DateOnly(2024, 1, 1),
|
||||
TrackingEndDate = new DateOnly(2024, 12, 31)
|
||||
};
|
||||
await _strategicOpportunityService.UpdateStrategicOpportunityStatus(opportunityId, toInitiativeDto, CancellationToken.None);
|
||||
|
||||
var toInValidationDto = new StrategicOpportunityUpdateStatusDto
|
||||
{
|
||||
Status = (byte)StrategicOpportunityStatus.InValidation
|
||||
};
|
||||
|
||||
// Act
|
||||
var updateResult = await _strategicOpportunityService.UpdateStrategicOpportunityStatus(opportunityId, toInValidationDto, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
updateResult.Should().BeTrue("Update should be successful");
|
||||
|
||||
var strataId = TestUtilities.GetClaimsPrincipalAccessor()?.GetCurrentClaimsPrincipal().GetStrataId();
|
||||
var opportunityAfterUpdate = await CentralDbContext.StrategicOpportunityChargeCodes
|
||||
.FirstOrDefaultAsync(x => x.OpportunityId == opportunityId && x.StrataId == strataId, CancellationToken.None);
|
||||
|
||||
opportunityAfterUpdate.Should().NotBeNull("Updated opportunity should exist");
|
||||
opportunityAfterUpdate.Status.Should().Be((byte)StrategicOpportunityStatus.InValidation, "Status should be updated to InValidation");
|
||||
opportunityAfterUpdate.TrackingStartDate.Should().Be(toInitiativeDto.TrackingStartDate, "TrackingStartDate should be unchanged");
|
||||
opportunityAfterUpdate.TrackingEndDate.Should().Be(toInitiativeDto.TrackingEndDate, "TrackingEndDate should be unchanged");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateStrategicOpportunityStatus_WhenOpportunityNotFound_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentOpportunityId = long.MaxValue;
|
||||
var updateStatusDto = new StrategicOpportunityUpdateStatusDto
|
||||
{
|
||||
Status = (byte)StrategicOpportunityStatus.NewOpportunity
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _strategicOpportunityService.UpdateStrategicOpportunityStatus(nonExistentOpportunityId, updateStatusDto, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse("Update should return false when the opportunity does not exist");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CopyStrategicOpportunityRoleAssignments_ShouldReturnEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var newOpportunityId = opportunityId + 1;
|
||||
await _opportunityRoleAssignmentService.CopyTo(opportunityId, newOpportunityId, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var newRoles = await _opportunityRoleAssignmentService.GetRoleAssignmentsAsync(newOpportunityId, CancellationToken.None);
|
||||
|
||||
newRoles.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CopyStrategicOpportunityRoleAssignments_ShouldReturnRoleAssignments()
|
||||
{
|
||||
// Arrange
|
||||
var opportunity = CreateOpportunityDto();
|
||||
var opportunityId = await _strategicOpportunityService.CreateOpportunityChargeCodeAsync(opportunity, CancellationToken.None);
|
||||
|
||||
var roleAssignments = new List<OpportunityRoleAssignmentView>()
|
||||
{
|
||||
new() { LootId = opportunityId, IdentityGuid = Guid.NewGuid(), IdentityName = "Test1", CanRead = true, CanWrite = false, CanSecure = true },
|
||||
new() { LootId = opportunityId, IdentityGuid = Guid.NewGuid(), IdentityName = "Test2", CanRead = false, CanWrite = true, CanSecure = false },
|
||||
};
|
||||
|
||||
await _opportunityRoleAssignmentService.SaveRoleAssignmentsAsync(opportunityId, roleAssignments, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var newOpportunityId = opportunityId + 1;
|
||||
await _opportunityRoleAssignmentService.CopyTo(opportunityId, newOpportunityId, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var newRoles = await _opportunityRoleAssignmentService.GetRoleAssignmentsAsync(newOpportunityId, CancellationToken.None);
|
||||
|
||||
// Verify persisted values
|
||||
var retrieved = newRoles.ToList();
|
||||
|
||||
retrieved.Should().HaveCount(roleAssignments.Count);
|
||||
CompareRoleAssignments(retrieved[0], roleAssignments[0]);
|
||||
CompareRoleAssignments(retrieved[1], roleAssignments[1]);
|
||||
}
|
||||
|
||||
private static void CompareRoleAssignments(OpportunityRoleAssignmentView item1, OpportunityRoleAssignmentView item2)
|
||||
{
|
||||
item1.IdentityGuid.Should().Be(item2.IdentityGuid);
|
||||
item1.IdentityName.Should().Be(item2.IdentityName);
|
||||
item1.CanRead.Should().Be(item2.CanRead);
|
||||
item1.CanWrite.Should().Be(item2.CanWrite);
|
||||
item1.CanSecure.Should().Be(item2.CanSecure);
|
||||
}
|
||||
|
||||
public string DataSchemaServiceSelectSql = @"SELECT
|
||||
""EncounterId"",
|
||||
""DischargeDateTime"",
|
||||
""EntityID"",
|
||||
""PhysicianId"",
|
||||
""PrimaryServiceLineId"",
|
||||
""CPTCodeCaseTypeFamilyId"",
|
||||
""MSDRGCaseTypeFamilyId"",
|
||||
""APRDRGCaseTypeFamilyId"",
|
||||
""DepartmentId"",
|
||||
""PhysicianSpecialtyId"",
|
||||
""ChargeCodeID"",
|
||||
""CostDriver"",
|
||||
""PatientTypeRollup"",
|
||||
ZEROIFNULL(""Units"") AS ""Units"",
|
||||
ZEROIFNULL(""Cost"") AS ""Cost""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
""EncounterId"",
|
||||
""DischargeDateTime"",
|
||||
""EntityID"",
|
||||
""PhysicianId"",
|
||||
""PrimaryServiceLineId"",
|
||||
""CPTCodeCaseTypeFamilyId"",
|
||||
""MSDRGCaseTypeFamilyId"",
|
||||
""APRDRGCaseTypeFamilyId"",
|
||||
""DepartmentId"",
|
||||
""PhysicianSpecialtyId"",
|
||||
""ChargeCodeID"",
|
||||
""CostDriver"",
|
||||
""PatientTypeRollup"",
|
||||
SUM(""Units"") AS ""Units"",
|
||||
SUM(""Cost"") AS ""Cost""
|
||||
|
||||
FROM (
|
||||
SELECT
|
||||
DSSPES.encounterid AS ""EncounterId"",
|
||||
DSSPES.dischargedatetime AS ""DischargeDateTime"",
|
||||
DSSPES.entityid AS ""EntityID"",
|
||||
DSSPES.primaryphysicianid AS ""PhysicianId"",
|
||||
DSSPES.servicelineid AS ""PrimaryServiceLineId"",
|
||||
DSSPES.cptcodecasetypefamilyid AS ""CPTCodeCaseTypeFamilyId"",
|
||||
DSSPES.msdrgcasetypefamilyid AS ""MSDRGCaseTypeFamilyId"",
|
||||
DSSPES.aprdrgcasetypefamilyid AS ""APRDRGCaseTypeFamilyId"",
|
||||
PBLID.departmentid AS ""DepartmentId"",
|
||||
PHYSPRIMARY.physicianspecialtyid AS ""PhysicianSpecialtyId"",
|
||||
CC.chargecodeid AS ""ChargeCodeID"",
|
||||
IFNULL(CC.costdriver::TEXT, '') AS ""CostDriver"",
|
||||
IFNULL(EncounterPatientTypeRollup.patienttyperollup::TEXT, '') AS ""PatientTypeRollup"",
|
||||
NULL AS ""Cost"",
|
||||
SUM(PBLID.unitsofservice) AS ""Units""
|
||||
|
||||
FROM dss.FactPatientBillingLineItemDetail AS PBLID
|
||||
INNER JOIN clientdss.FactPatientEncounterSummary AS DSSPES ON DSSPES.encounterrecordnumber = PBLID.encounterrecordnumber
|
||||
LEFT JOIN dss.FactPatientEncounterClinicalIndicator AS DSSPECI ON DSSPECI.encounterid = PBLID.encounterid
|
||||
LEFT JOIN CCI.VIEWENCOUNTERPATIENTTYPEROLLUP AS EncounterPatientTypeRollup ON EncounterPatientTypeRollup.encounterid = PBLID.encounterid
|
||||
INNER JOIN FW.DIMCHARGECODE AS CC ON CC.chargecodeid = PBLID.chargecodeid
|
||||
INNER JOIN DSS.DIMPHYSICIAN AS PHYSPRIMARY ON PHYSPRIMARY.physicianid = DSSPES.primaryphysicianid
|
||||
|
||||
WHERE
|
||||
((DSSPES.dischargedatetime >= :dischargedatetime_Start_0 AND DSSPES.dischargedatetime <= :dischargedatetime_End_1) AND CC.chargecodeid IN (:chargecodeid_2))
|
||||
|
||||
GROUP BY
|
||||
""EncounterId"",
|
||||
""DischargeDateTime"",
|
||||
""EntityID"",
|
||||
""PhysicianId"",
|
||||
""PrimaryServiceLineId"",
|
||||
""CPTCodeCaseTypeFamilyId"",
|
||||
""MSDRGCaseTypeFamilyId"",
|
||||
""APRDRGCaseTypeFamilyId"",
|
||||
""DepartmentId"",
|
||||
""PhysicianSpecialtyId"",
|
||||
""ChargeCodeID"",
|
||||
""CostDriver"",
|
||||
""PatientTypeRollup""
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
DSSPES.encounterid AS ""EncounterId"",
|
||||
DSSPES.dischargedatetime AS ""DischargeDateTime"",
|
||||
DSSPES.entityid AS ""EntityID"",
|
||||
DSSPES.primaryphysicianid AS ""PhysicianId"",
|
||||
DSSPES.servicelineid AS ""PrimaryServiceLineId"",
|
||||
DSSPES.cptcodecasetypefamilyid AS ""CPTCodeCaseTypeFamilyId"",
|
||||
DSSPES.msdrgcasetypefamilyid AS ""MSDRGCaseTypeFamilyId"",
|
||||
DSSPES.aprdrgcasetypefamilyid AS ""APRDRGCaseTypeFamilyId"",
|
||||
PBLID.departmentid AS ""DepartmentId"",
|
||||
PHYSPRIMARY.physicianspecialtyid AS ""PhysicianSpecialtyId"",
|
||||
CC.chargecodeid AS ""ChargeCodeID"",
|
||||
IFNULL(CC.costdriver::TEXT, '') AS ""CostDriver"",
|
||||
IFNULL(EncounterPatientTypeRollup.patienttyperollup::TEXT, '') AS ""PatientTypeRollup"",
|
||||
SUM(COSTDETAIL.variabledirectcost) AS ""Cost"",
|
||||
NULL AS ""Units""
|
||||
|
||||
FROM dss.FactPatientCostDetail AS COSTDETAIL
|
||||
RIGHT JOIN dss.FactPatientBillingLineItemDetail AS PBLID ON PBLID.rowid = COSTDETAIL.pblidrowid
|
||||
INNER JOIN clientdss.FactPatientEncounterSummary AS DSSPES ON DSSPES.encounterrecordnumber = PBLID.encounterrecordnumber
|
||||
LEFT JOIN dss.FactPatientEncounterClinicalIndicator AS DSSPECI ON DSSPECI.encounterid = PBLID.encounterid
|
||||
LEFT JOIN CCI.VIEWENCOUNTERPATIENTTYPEROLLUP AS EncounterPatientTypeRollup ON EncounterPatientTypeRollup.encounterid = PBLID.encounterid
|
||||
INNER JOIN FW.DIMCHARGECODE AS CC ON CC.chargecodeid = PBLID.chargecodeid
|
||||
INNER JOIN DSS.DIMPHYSICIAN AS PHYSPRIMARY ON PHYSPRIMARY.physicianid = DSSPES.primaryphysicianid
|
||||
|
||||
WHERE
|
||||
((DSSPES.dischargedatetime >= :dischargedatetime_Start_0 AND DSSPES.dischargedatetime <= :dischargedatetime_End_1) AND CC.chargecodeid IN (:chargecodeid_2))
|
||||
|
||||
GROUP BY
|
||||
""EncounterId"",
|
||||
""DischargeDateTime"",
|
||||
""EntityID"",
|
||||
""PhysicianId"",
|
||||
""PrimaryServiceLineId"",
|
||||
""CPTCodeCaseTypeFamilyId"",
|
||||
""MSDRGCaseTypeFamilyId"",
|
||||
""APRDRGCaseTypeFamilyId"",
|
||||
""DepartmentId"",
|
||||
""PhysicianSpecialtyId"",
|
||||
""ChargeCodeID"",
|
||||
""CostDriver"",
|
||||
""PatientTypeRollup""
|
||||
|
||||
|
||||
)
|
||||
|
||||
GROUP BY
|
||||
""EncounterId"",
|
||||
""DischargeDateTime"",
|
||||
""EntityID"",
|
||||
""PhysicianId"",
|
||||
""PrimaryServiceLineId"",
|
||||
""CPTCodeCaseTypeFamilyId"",
|
||||
""MSDRGCaseTypeFamilyId"",
|
||||
""APRDRGCaseTypeFamilyId"",
|
||||
""DepartmentId"",
|
||||
""PhysicianSpecialtyId"",
|
||||
""ChargeCodeID"",
|
||||
""CostDriver"",
|
||||
""PatientTypeRollup""
|
||||
|
||||
|
||||
)
|
||||
|
||||
ORDER BY
|
||||
""EncounterId"" ASC NULLS FIRST,
|
||||
""DischargeDateTime"" ASC NULLS FIRST,
|
||||
""EntityID"" ASC NULLS FIRST,
|
||||
""PhysicianId"" ASC NULLS FIRST,
|
||||
""PrimaryServiceLineId"" ASC NULLS FIRST,
|
||||
""CPTCodeCaseTypeFamilyId"" ASC NULLS FIRST,
|
||||
""MSDRGCaseTypeFamilyId"" ASC NULLS FIRST,
|
||||
""APRDRGCaseTypeFamilyId"" ASC NULLS FIRST,
|
||||
""DepartmentId"" ASC NULLS FIRST,
|
||||
""PhysicianSpecialtyId"" ASC NULLS FIRST,
|
||||
""ChargeCodeID"" ASC NULLS FIRST,
|
||||
""CostDriver"" ASC NULLS FIRST,
|
||||
""PatientTypeRollup"" ASC NULLS FIRST
|
||||
|
||||
";
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
using Strata.ContinuousImprovement.Biz.Exploration;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration.CaseTypeFamilies;
|
||||
using Strata.ContinuousImprovement.Biz.Exploration.Filters;
|
||||
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 TestSnowflakeDatabaseContext : IntegrationTestBase
|
||||
{
|
||||
[Test, Ignore("Test database needs to be rolled over to the new year")]
|
||||
public async Task BuildCaseTypeFamilySummaryQueryTest()
|
||||
{
|
||||
var queryParams = new QueryParams
|
||||
{
|
||||
CodeType = UtilizationVariation.VariationCaseTypes.CaseTypeCategories.MSDRG
|
||||
};
|
||||
|
||||
int year = DateTime.Now.Year;
|
||||
var dischargeDate = new FilterChipItem
|
||||
{
|
||||
ChipType = ChipType.DateRange,
|
||||
Filters = [],
|
||||
DateRange = [new DateTime(year, 1, 1), new DateTime(year + 1, 1, 1).AddTicks(-1)]
|
||||
};
|
||||
|
||||
var filters = new Exploration.Filters.Filter
|
||||
{
|
||||
DischargeDateStart = dischargeDate.DateRange.First(),
|
||||
DischargeDateEnd = dischargeDate.DateRange.Last(),
|
||||
FilterChipItems = [dischargeDate]
|
||||
};
|
||||
|
||||
var filterStrings = await ExplorationFilterService.GetFilterStringsAsync(filters.FilterChipItems, queryParams, CancellationToken.None);
|
||||
var ctfSummaryQuery = ExplorationServiceQuery.BuildCaseTypeFamilySummaryQuery(filterStrings.QueryFilters, filterStrings.EncounterSummaryFilters);
|
||||
|
||||
var ctfCostDriverInfos = await SnowflakeDatabaseContext
|
||||
.QueryAsync<CaseTypeFamilyEncounterInfo>(ctfSummaryQuery, "CaseTypeFamily CostDriver Summary", queryParams);
|
||||
Assert.That(ctfCostDriverInfos, Is.Not.Null.And.Not.Empty
|
||||
.And.All.InstanceOf<CaseTypeFamilyEncounterInfo>());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user