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 _testOpportunityIds; private List _testUserGuids; [OneTimeSetUp] public async Task TestFixtureSetup() { await SetDbContextAndServices(); // Get real StrataId from the test database var idServiceClient = ServiceProvider.GetRequiredService(); _testStrataId = await idServiceClient.GetStrataId(GmAutomationDatabase, CancellationToken.None); _testOpportunityIds = new List(); _testUserGuids = new List(); try { _testUserGuids = await CreateTestUserProfiles(); _syncService = new StrategicItemDimensionSyncService( ServiceProvider.GetRequiredService>(), (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>(); 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().ToArray()); } private async Task> CreateTestUserProfiles() { var runId = Guid.NewGuid().ToString("N")[..8]; var userGuids = new List { Guid.NewGuid(), // Owner Guid.NewGuid(), // Cost Leader Guid.NewGuid(), // Executive Sponsor Guid.NewGuid(), // Stakeholder 1 Guid.NewGuid() // Stakeholder 2 }; var jazzContextFactory = ServiceProvider.GetRequiredService>(); 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 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 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 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( 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 } }