Files

533 lines
24 KiB
C#

using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Strata.ContinuousImprovement.Biz.DbContexts;
using Strata.ContinuousImprovement.Biz.Generics;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities;
using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Enums;
using Strata.ContinuousImprovement.Biz.Utilities;
using Strata.Id.Client;
using Strata.SqlTools.Configuration.Common.AsyncFactory;
using System.Diagnostics.CodeAnalysis;
namespace Strata.ContinuousImprovement.Biz.Test.Integration.StrategicItems
{
[ExcludeFromCodeCoverage]
[TestFixture]
public class StrategicItemDimensionSyncTests : IntegrationTestBase
{
private StrategicItemDimensionSyncService _syncService;
private int _testStrataId;
private List<long> _testOpportunityIds;
private List<Guid> _testUserGuids;
[OneTimeSetUp]
public async Task TestFixtureSetup()
{
await SetDbContextAndServices();
// Get real StrataId from the test database
var idServiceClient = ServiceProvider.GetRequiredService<IIdServiceClient>();
_testStrataId = await idServiceClient.GetStrataId(GmAutomationDatabase, CancellationToken.None);
_testOpportunityIds = new List<long>();
_testUserGuids = new List<Guid>();
try
{
_testUserGuids = await CreateTestUserProfiles();
_syncService = new StrategicItemDimensionSyncService(
ServiceProvider.GetRequiredService<IAsyncDbContextFactory<JazzDbContext>>(),
(CentralDbContext)CentralDbContext,
SnowflakeDatabaseContext
);
}
catch
{
// If setup fails, clean up any partial data
await CleanupTestUserProfiles();
throw;
}
}
[TearDown]
public async Task TestCleanup()
{
// Clean up Snowflake dimension records
foreach (var opportunityId in _testOpportunityIds)
{
try
{
var deleteSql = "DELETE FROM cci.dimstrategicitem WHERE strategic_item_id = :StrategicItemId";
await SnowflakeDatabaseContext.ExecuteCommandAsync(deleteSql, new { StrategicItemId = opportunityId });
}
catch
{
// Ignore cleanup errors
}
}
// Clean up CentralDbContext records
var opportunities = await ((CentralDbContext)CentralDbContext).StrategicOpportunities
.Where(o => _testOpportunityIds.Contains(o.OpportunityId))
.ToListAsync();
((CentralDbContext)CentralDbContext).StrategicOpportunities.RemoveRange(opportunities);
var glOpportunities = await ((CentralDbContext)CentralDbContext).StrategicOpportunityGLs
.Where(o => _testOpportunityIds.Contains(o.OpportunityId))
.ToListAsync();
((CentralDbContext)CentralDbContext).StrategicOpportunityGLs.RemoveRange(glOpportunities);
// Clean up stakeholder links
var stakeholders = await ((CentralDbContext)CentralDbContext).StakeholdersLinks
.Where(s => _testOpportunityIds.Contains(s.StrategicOpportunityId))
.ToListAsync();
((CentralDbContext)CentralDbContext).StakeholdersLinks.RemoveRange(stakeholders);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
_testOpportunityIds.Clear();
}
[OneTimeTearDown]
public async Task TestFixtureTearDown()
{
await CleanupTestUserProfiles();
// Ensure base class cleanup runs to delete CentralDbContext database
await base.TearDown();
}
[Test]
public async Task UpsertStrategicItemDimension_NewOpportunity_ShouldInsertDimensionRecord()
{
// Arrange
var opportunity = CreateTestOpportunity(_testUserGuids);
((CentralDbContext)CentralDbContext).StrategicOpportunityChargeCodes.Add(opportunity);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
_testOpportunityIds.Add(opportunity.OpportunityId);
// Act
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
// Assert
var dimensionRecord = await GetDimensionRecord(opportunity.OpportunityId);
dimensionRecord.Should().NotBeNull();
dimensionRecord.Name.Should().Be(opportunity.Name);
dimensionRecord.StrategicItemType.Should().Be("Opportunity");
dimensionRecord.OpportunityType.Should().Be("Charge Code");
dimensionRecord.Status.Should().Be("New Opportunity");
dimensionRecord.BaselineType.Should().Be(opportunity.BaselineType.ToString().SplitCamelCase());
dimensionRecord.BaselineDistribution.Should().Be(opportunity.BaselineDistribution.ToString());
dimensionRecord.RollupOneId.Should().Be(opportunity.Rollup1Id);
dimensionRecord.RollupTwoId.Should().Be(opportunity.Rollup2Id);
dimensionRecord.RollupThreeId.Should().Be(opportunity.Rollup3Id);
dimensionRecord.BaselineStartDate.Should().Be(opportunity.BaselineStartDate);
dimensionRecord.BaselineEndDate.Should().Be(opportunity.BaselineEndDate);
dimensionRecord.EstimatedTrackingDuration.Should().Be(opportunity.EstimatedTrackingDuration);
dimensionRecord.TeamMembers.Should().Be(opportunity.TeamMembers);
dimensionRecord.Owner.Should().NotBeNullOrEmpty();
dimensionRecord.CostLeader.Should().NotBeNullOrEmpty();
dimensionRecord.ExecutiveSponsor.Should().NotBeNullOrEmpty();
}
[Test]
public async Task UpsertStrategicItemDimension_ExistingOpportunity_ShouldUpdateDimensionRecord()
{
// Arrange
var opportunity = CreateTestOpportunity(_testUserGuids);
((CentralDbContext)CentralDbContext).StrategicOpportunityChargeCodes.Add(opportunity);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
_testOpportunityIds.Add(opportunity.OpportunityId);
// Insert initial dimension record
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
// Modify opportunity
opportunity.Name = "Updated Test Opportunity";
opportunity.Status = (byte)StrategicOpportunityStatus.InValidation;
opportunity.EstimatedGoal = 50000;
opportunity.TotalImprovement = 25000;
opportunity.LastRunTrackingDate = DateTime.SpecifyKind(new DateTime(2024, 3, 15), DateTimeKind.Utc);
opportunity.LastRunTrackingStatus = RunTrackingStatus.Success;
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
// Act
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
// Assert
var dimensionRecord = await GetDimensionRecord(opportunity.OpportunityId);
dimensionRecord.Should().NotBeNull();
dimensionRecord.Name.Should().Be("Updated Test Opportunity");
dimensionRecord.Status.Should().Be("In Validation");
dimensionRecord.EstimatedGoal.Should().Be(50000);
dimensionRecord.TotalImprovement.Should().Be(25000);
dimensionRecord.LastRunTrackingDate.Should().Be(new DateOnly(2024, 3, 15));
dimensionRecord.LastRunTrackingStatus.Should().Be(RunTrackingStatus.Success.ToString());
}
[Test]
public async Task UpsertStrategicItemDimension_WithStakeholders_ShouldSyncStakeholderNames()
{
// Arrange
var opportunity = CreateTestOpportunity(_testUserGuids);
((CentralDbContext)CentralDbContext).StrategicOpportunityChargeCodes.Add(opportunity);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
_testOpportunityIds.Add(opportunity.OpportunityId);
// Add stakeholders
var stakeholder1 = new StrategicStakeholdersLink(opportunity.OpportunityId, _testUserGuids[3]);
var stakeholder2 = new StrategicStakeholdersLink(opportunity.OpportunityId, _testUserGuids[4]);
((CentralDbContext)CentralDbContext).StakeholdersLinks.AddRange(stakeholder1, stakeholder2);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
// Act
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
// Assert
var dimensionRecord = await GetDimensionRecord(opportunity.OpportunityId);
dimensionRecord.Should().NotBeNull();
dimensionRecord.Stakeholders.Should().NotBeNullOrEmpty();
dimensionRecord.Stakeholders.Should().Contain("Stakeholder1, Test");
dimensionRecord.Stakeholders.Should().Contain("Stakeholder2, Test");
}
[Test]
public async Task UpsertStrategicItemDimension_StatusChange_ShouldUpdateStrategicItemType()
{
// Arrange
var opportunity = CreateTestOpportunity(_testUserGuids);
opportunity.Status = (byte)StrategicOpportunityStatus.NewOpportunity;
((CentralDbContext)CentralDbContext).StrategicOpportunityChargeCodes.Add(opportunity);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
_testOpportunityIds.Add(opportunity.OpportunityId);
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
var initialRecord = await GetDimensionRecord(opportunity.OpportunityId);
initialRecord.StrategicItemType.Should().Be("Opportunity");
// Change to Initiative
opportunity.Status = (byte)StrategicOpportunityStatus.NewInitiative;
opportunity.TrackingStartDate = new DateOnly(2024, 1, 1);
opportunity.TrackingEndDate = new DateOnly(2024, 12, 31);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
// Act
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
// Assert
var updatedRecord = await GetDimensionRecord(opportunity.OpportunityId);
updatedRecord.StrategicItemType.Should().Be("Initiative");
updatedRecord.Status.Should().Be("New Initiative");
updatedRecord.TrackingStartDate.Should().Be(new DateOnly(2024, 1, 1));
updatedRecord.TrackingEndDate.Should().Be(new DateOnly(2024, 12, 31));
}
[Test]
public async Task UpsertStrategicItemDimension_GLOpportunity_ShouldSetOpportunityTypeToGeneralLedger()
{
// Arrange
var opportunity = CreateTestGLOpportunity(_testUserGuids);
((CentralDbContext)CentralDbContext).StrategicOpportunityGLs.Add(opportunity);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
_testOpportunityIds.Add(opportunity.OpportunityId);
// Act
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
// Assert
var dimensionRecord = await GetDimensionRecord(opportunity.OpportunityId);
dimensionRecord.Should().NotBeNull();
dimensionRecord.OpportunityType.Should().Be("General Ledger");
dimensionRecord.Name.Should().Be(opportunity.Name);
dimensionRecord.StrategicItemType.Should().Be("Opportunity");
dimensionRecord.Status.Should().Be("New Opportunity");
}
[Test]
public async Task UpsertStrategicItemDimension_GLOpportunityTrackingStatus_ShouldSyncLastRunTracking()
{
// Arrange
var opportunity = CreateTestGLOpportunity(_testUserGuids);
((CentralDbContext)CentralDbContext).StrategicOpportunityGLs.Add(opportunity);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
_testOpportunityIds.Add(opportunity.OpportunityId);
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
// Simulate tracking run completing
opportunity.LastRunTrackingDate = DateTime.SpecifyKind(new DateTime(2024, 5, 1), DateTimeKind.Utc);
opportunity.LastRunTrackingStatus = RunTrackingStatus.Success;
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
// Act
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
// Assert
var dimensionRecord = await GetDimensionRecord(opportunity.OpportunityId);
dimensionRecord.Should().NotBeNull();
dimensionRecord.LastRunTrackingDate.Should().Be(new DateOnly(2024, 5, 1));
dimensionRecord.LastRunTrackingStatus.Should().Be(RunTrackingStatus.Success.ToString());
}
[Test]
public async Task DeleteStrategicItemDimension_ShouldRemoveDimensionRecord()
{
// Arrange
var opportunity = CreateTestOpportunity(_testUserGuids);
((CentralDbContext)CentralDbContext).StrategicOpportunityChargeCodes.Add(opportunity);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
_testOpportunityIds.Add(opportunity.OpportunityId);
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
var recordBeforeDelete = await GetDimensionRecord(opportunity.OpportunityId);
recordBeforeDelete.Should().NotBeNull();
// Act
await _syncService.DeleteAsync(opportunity.OpportunityId);
// Assert
var recordAfterDelete = await GetDimensionRecord(opportunity.OpportunityId);
recordAfterDelete.Should().BeNull();
}
[Test]
public async Task UpsertStrategicItemDimension_WithNullDates_ShouldHandleNullsCorrectly()
{
// Arrange
var opportunity = CreateTestOpportunity(_testUserGuids);
opportunity.TrackingStartDate = default;
opportunity.TrackingEndDate = default;
opportunity.LastRunTrackingDate = null;
((CentralDbContext)CentralDbContext).StrategicOpportunityChargeCodes.Add(opportunity);
await ((CentralDbContext)CentralDbContext).SaveChangesAsync();
_testOpportunityIds.Add(opportunity.OpportunityId);
// Act
await _syncService.UpsertAsync(opportunity, CancellationToken.None);
// Assert
var dimensionRecord = await GetDimensionRecord(opportunity.OpportunityId);
dimensionRecord.Should().NotBeNull();
dimensionRecord.TrackingStartDate.Should().BeNull();
dimensionRecord.TrackingEndDate.Should().BeNull();
dimensionRecord.LastRunTrackingDate.Should().BeNull();
dimensionRecord.BaselineStartDate.Should().NotBeNull(); // Required field
}
#region Helper Methods
private async Task CleanupTestUserProfiles()
{
if (_testUserGuids == null || _testUserGuids.Count == 0) return;
var jazzContextFactory = ServiceProvider.GetRequiredService<IAsyncDbContextFactory<JazzDbContext>>();
await using var jazzContext = await jazzContextFactory.CreateDbContextAsync(CancellationToken.None);
var guidParams = _testUserGuids
.Select((g, i) => new Microsoft.Data.SqlClient.SqlParameter($"@g{i}", g))
.ToArray();
var paramNames = string.Join(", ", guidParams.Select(p => p.ParameterName));
await jazzContext.Database.ExecuteSqlRawAsync(
$"DELETE FROM dbo.UserProfile WHERE UserGuid IN ({paramNames})",
guidParams.Cast<object>().ToArray());
}
private async Task<List<Guid>> CreateTestUserProfiles()
{
var runId = Guid.NewGuid().ToString("N")[..8];
var userGuids = new List<Guid>
{
Guid.NewGuid(), // Owner
Guid.NewGuid(), // Cost Leader
Guid.NewGuid(), // Executive Sponsor
Guid.NewGuid(), // Stakeholder 1
Guid.NewGuid() // Stakeholder 2
};
var jazzContextFactory = ServiceProvider.GetRequiredService<IAsyncDbContextFactory<JazzDbContext>>();
await using var jazzContext = await jazzContextFactory.CreateDbContextAsync(CancellationToken.None);
var insertSql = @"
INSERT INTO dbo.UserProfile (UserGuid, UserName, NameFirst, NameLast)
VALUES (@UserGuid, @UserName, @NameFirst, @NameLast)";
var profiles = new[]
{
new { UserGuid = userGuids[0], UserName = $"owner_{runId}", NameFirst = "Test", NameLast = "Owner" },
new { UserGuid = userGuids[1], UserName = $"costleader_{runId}", NameFirst = "Test", NameLast = "CostLeader" },
new { UserGuid = userGuids[2], UserName = $"executivesponsor_{runId}", NameFirst = "Test", NameLast = "ExecutiveSponsor" },
new { UserGuid = userGuids[3], UserName = $"stakeholder1_{runId}", NameFirst = "Test", NameLast = "Stakeholder1" },
new { UserGuid = userGuids[4], UserName = $"stakeholder2_{runId}", NameFirst = "Test", NameLast = "Stakeholder2" }
};
foreach (var profile in profiles)
{
await jazzContext.Database.ExecuteSqlRawAsync(insertSql,
new Microsoft.Data.SqlClient.SqlParameter("@UserGuid", profile.UserGuid),
new Microsoft.Data.SqlClient.SqlParameter("@UserName", profile.UserName),
new Microsoft.Data.SqlClient.SqlParameter("@NameFirst", profile.NameFirst),
new Microsoft.Data.SqlClient.SqlParameter("@NameLast", profile.NameLast));
}
return userGuids;
}
private StrategicOpportunityChargeCode CreateTestOpportunity(List<Guid> userGuids)
{
return new StrategicOpportunityChargeCode
{
Name = $"Test Opportunity {Guid.NewGuid()}",
StrataId = _testStrataId,
OpportunityType = StrategicOpportunityTypes.ChargeCode,
Status = (byte)StrategicOpportunityStatus.NewOpportunity,
BaselineType = BaselineType.MonthRange,
BaselineDistribution = DistributionMethod.Monthly,
BaselineStartDate = new DateOnly(2024, 1, 1),
BaselineEndDate = new DateOnly(2024, 6, 30),
EstimatedTrackingDuration = 12,
EstimatedGoal = 100000,
TotalImprovement = 0,
Rollup1Id = 1,
Rollup2Id = 1,
Rollup3Id = 1,
OwnerGuid = userGuids[0],
CostLeaderGuid = userGuids[1],
ExecutiveSponsorGuid = userGuids[2],
TeamMembers = "Team Member 1, Team Member 2",
ChargeCodeHPath = "test/path",
PatientPopulationHPath = string.Empty,
OpportunityFiltersJSON = "[]",
LayoutJSON = "{}",
LastRunTrackingStatus = RunTrackingStatus.None
};
}
private StrategicOpportunityGL CreateTestGLOpportunity(List<Guid> userGuids)
{
return new StrategicOpportunityGL
{
Name = $"Test GL Opportunity {Guid.NewGuid()}",
StrataId = _testStrataId,
OpportunityType = StrategicOpportunityTypes.GeneralLedger,
Status = (byte)StrategicOpportunityStatus.NewOpportunity,
BaselineType = BaselineType.MonthRange,
BaselineDistribution = DistributionMethod.Monthly,
BaselineStartDate = new DateOnly(2024, 1, 1),
BaselineEndDate = new DateOnly(2024, 6, 30),
EstimatedTrackingDuration = 12,
EstimatedGoal = 100000,
TotalImprovement = 0,
Rollup1Id = 1,
Rollup2Id = 1,
Rollup3Id = 1,
OwnerGuid = userGuids[0],
CostLeaderGuid = userGuids[1],
ExecutiveSponsorGuid = userGuids[2],
TeamMembers = "Team Member 1, Team Member 2",
DepartmentHPath = string.Empty,
OpportunityFiltersJSON = "[]",
LayoutJSON = "{}",
LastRunTrackingStatus = RunTrackingStatus.None
};
}
private async Task<DimensionRecord> GetDimensionRecord(long opportunityId)
{
var query = @"
SELECT
strategic_item_id AS StrategicItemId,
name AS Name,
rollup_one_id AS RollupOneId,
rollup_two_id AS RollupTwoId,
rollup_three_id AS RollupThreeId,
baseline_type AS BaselineType,
strategic_item_type AS StrategicItemType,
opportunity_type AS OpportunityType,
status AS Status,
baseline_distribution AS BaselineDistribution,
baseline_start_date AS BaselineStartDate,
baseline_end_date AS BaselineEndDate,
estimated_tracking_duration AS EstimatedTrackingDuration,
tracking_start_date AS TrackingStartDate,
tracking_end_date AS TrackingEndDate,
estimated_goal AS EstimatedGoal,
total_improvement AS TotalImprovement,
cost_leader AS CostLeader,
executive_sponsor AS ExecutiveSponsor,
owner AS Owner,
stakeholders AS Stakeholders,
team_members AS TeamMembers,
last_run_tracking_date AS LastRunTrackingDate,
last_run_tracking_status AS LastRunTrackingStatus
FROM cci.dimstrategicitem
WHERE strategic_item_id = :StrategicItemId";
var parameters = new { StrategicItemId = opportunityId };
var results = await SnowflakeDatabaseContext.QueryAsync<DimensionRecord>(
query,
"GetDimensionRecord",
parameters
);
return results.FirstOrDefault();
}
#endregion
#region Test DTO
private class DimensionRecord
{
public long StrategicItemId { get; set; }
public string Name { get; set; }
public int RollupOneId { get; set; }
public int RollupTwoId { get; set; }
public int RollupThreeId { get; set; }
public string BaselineType { get; set; }
public string StrategicItemType { get; set; }
public string OpportunityType { get; set; }
public string Status { get; set; }
public string BaselineDistribution { get; set; }
public DateOnly? BaselineStartDate { get; set; }
public DateOnly? BaselineEndDate { get; set; }
public int EstimatedTrackingDuration { get; set; }
public DateOnly? TrackingStartDate { get; set; }
public DateOnly? TrackingEndDate { get; set; }
public decimal EstimatedGoal { get; set; }
public decimal TotalImprovement { get; set; }
public string CostLeader { get; set; }
public string ExecutiveSponsor { get; set; }
public string Owner { get; set; }
public string Stakeholders { get; set; }
public string TeamMembers { get; set; }
public DateOnly? LastRunTrackingDate { get; set; }
public string LastRunTrackingStatus { get; set; }
}
#endregion
}
}