diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs index 9186367ca..fd30c98c5 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs @@ -69,7 +69,7 @@ private async Task CreateAndStoreReportAsync(WeeklyTaskOwnerReportMessage messag var expiringAdmins = WeeklyTaskOwnerReportDataCreator.GetExpiringAdmins(admins); var actionsAwaitingTaskOwner = WeeklyTaskOwnerReportDataCreator.GetActionsAwaitingTaskOwnerAsync(activeRequestsForProject); var expiringPositionAllocations = WeeklyTaskOwnerReportDataCreator.GetPositionAllocationsEndingNextThreeMonths(allProjectPositions); - var tbnPositions = WeeklyTaskOwnerReportDataCreator.GetTBNPositionsStartingWithinThreeMonths(allProjectPositions, activeRequestsForProject); + var tbnPositions = WeeklyTaskOwnerReportDataCreator.GetTBNPositionsStartingWithinThreeMonths(allProjectPositions); var lastMonday = now.GetPreviousWeeksMondayDate(); var report = new ApiWeeklyTaskOwnerReport() diff --git a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs index e03783926..c913614df 100644 --- a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs +++ b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs @@ -13,8 +13,7 @@ public abstract class WeeklyTaskOwnerReportDataCreator // Logic taken/inspired from the frontend // https://github.com/equinor/fusion-resource-allocation-apps/blob/a9330b2aa8d104e51536692a72334252d5e474e1/apps/org-admin/src/pages/ProjectPage/components/ChartComponent/components/utils.ts#L28 // https://github.com/equinor/fusion-resource-allocation-apps/blob/0c8477f48021c594af20c0b1ba7b549b187e2e71/apps/org-admin/src/pages/ProjectPage/pages/EditPositionsPage/pages/TimelineViewPage/components/TimelineFilter/selectors/positionSelector.ts#L14 - public static List GetTBNPositionsStartingWithinThreeMonths(IEnumerable allProjectPositions, - ICollection requests) + public static List GetTBNPositionsStartingWithinThreeMonths(IEnumerable allProjectPositions) { var nowDate = NowDate; var expiringDate = nowDate.AddMonths(3); @@ -29,16 +28,12 @@ public static List GetTBNPositionsStartingWithinThreeMonths(IEnumer var expiringInstance = position.Instances .OrderBy(i => i.AppliesFrom) .Where(i => i.AppliesFrom < expiringDate) // hasDueWithinThreeMonths - .FirstOrDefault(i => i.AppliesTo >= nowDate); // !isInstancePast + .Where(i => i.AssignedPerson is null) // TBN instance + .FirstOrDefault(i => nowDate <= i.AppliesTo); if (expiringInstance is null) continue; - var hasPersonAssigned = expiringInstance.AssignedPerson is not null; - - if (hasPersonAssigned) - continue; - tbnPositions.Add(new TBNPosition(position, expiringInstance.AppliesFrom)); } diff --git a/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs b/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs index 4a26b36db..f1574a6a7 100644 --- a/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs +++ b/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs @@ -45,6 +45,9 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() { #region Arrange + var testData = new ReportTestDataContainer(); + + var personA = new ApiPersonV2() { AzureUniqueId = Guid.NewGuid(), @@ -56,75 +59,71 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() Name = "Test NameB" }; - var shouldBeIncludedInReport = new List(); - var positionsToTest = new List(); - var instanceToBeIncluded = new Dictionary(); - var activeWithFutureInstance = new PositionBuilder() .WithInstance(Past, now.AddDays(30 * 1.5), person: personA) .AddNextInstance(TimeSpan.FromDays(30 * 4), person: personA) .Build(); - AddPosition(activeWithFutureInstance); + testData.AddPosition(activeWithFutureInstance); var activeWithoutFutureInstance = new PositionBuilder() .WithInstance(Past, now.AddDays(30), person: personA) .AddNextInstance(TimeSpan.FromDays(30), person: personA, extId: "1") .Build(); - AddPosition(activeWithoutFutureInstance, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); + testData.AddPosition(activeWithoutFutureInstance, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); var activeWithFutureInstanceDifferentPerson = new PositionBuilder() .WithInstance(Past, now.AddDays(30 * 1.5), person: personA) .AddNextInstance(TimeSpan.FromDays(30 * 4), person: personB) .Build(); - AddPosition(activeWithFutureInstanceDifferentPerson); + testData.AddPosition(activeWithFutureInstanceDifferentPerson); var singleActiveWithoutFutureInstance = new PositionBuilder() .WithInstance(Past, now.Add(TimeSpan.FromDays(30 * 1.5)), person: personA) .Build(); - AddPosition(singleActiveWithoutFutureInstance, shouldBeIncludedInReportList: true); + testData.AddPosition(singleActiveWithoutFutureInstance, shouldBeIncludedInReportList: true); var activeWithFutureInstanceUnassignedPerson = new PositionBuilder() .WithInstance(Past, now.AddDays(30 * 2), person: personA) .AddNextInstance(TimeSpan.FromDays(30 * 2), person: null) .Build(); - AddPosition(activeWithFutureInstanceUnassignedPerson, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); + testData.AddPosition(activeWithFutureInstanceUnassignedPerson, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); var futureInstanceThatIsAlsoExpiring = new PositionBuilder() .WithInstance(now.AddDays(30), now.AddDays(30 * 2), person: personA) .Build(); - AddPosition(futureInstanceThatIsAlsoExpiring, shouldBeIncludedInReportList: true); + testData.AddPosition(futureInstanceThatIsAlsoExpiring, shouldBeIncludedInReportList: true); var futureInstancesThatIsAlsoExpiring = new PositionBuilder() .WithInstance(now.AddDays(30), now.AddDays(30 * 2), person: personA) .AddNextInstance(TimeSpan.FromDays(1), person: personA, extId: "1") .Build(); - AddPosition(futureInstancesThatIsAlsoExpiring, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); + testData.AddPosition(futureInstancesThatIsAlsoExpiring, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); var futureInstanceThatIsMissingAllocation = new PositionBuilder() .WithInstance(now.AddDays(30), now.AddDays(30 * 2), person: personA) .AddNextInstance(TimeSpan.FromDays(1), person: null) .Build(); - AddPosition(futureInstanceThatIsMissingAllocation, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); + testData.AddPosition(futureInstanceThatIsMissingAllocation, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); var futureInstancesWhereOneIsTBN = new PositionBuilder() .WithInstance(now.AddDays(10), now.AddDays(30), person: personA) .AddNextInstance(TimeSpan.FromDays(2), person: null) .AddNextInstance(TimeSpan.FromDays(6), person: personA) .Build(); - AddPosition(futureInstancesWhereOneIsTBN, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); + testData.AddPosition(futureInstancesWhereOneIsTBN, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); var futureInstanceThatIsNotExpiring = new PositionBuilder() .WithInstance(now.AddDays(30), now.AddDays(60), person: personA) .AddNextInstance(TimeSpan.FromDays(100), person: personA) .Build(); - AddPosition(futureInstanceThatIsNotExpiring); + testData.AddPosition(futureInstanceThatIsNotExpiring); // Entire gap/time-period is within the 3-month window @@ -133,7 +132,7 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() .AddNextInstance(now.AddDays(30 * 2), now.AddDays(30 * 4), person: personA) .Build(); - AddPosition(activePositionWithFutureInstanceWithSmallGap); + testData.AddPosition(activePositionWithFutureInstanceWithSmallGap); var activePositionWithFutureInstanceWithLargerGap = new PositionBuilder() @@ -141,7 +140,7 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() .AddNextInstance(now.AddDays(30 * 5), now.AddDays(30 * 7), person: personA) .AddNextInstance(TimeSpan.FromDays(10), person: personA) .Build(); - AddPosition(activePositionWithFutureInstanceWithLargerGap, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); + testData.AddPosition(activePositionWithFutureInstanceWithLargerGap, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); var manySmallInstancesWithoutFutureInstance = new PositionBuilder() @@ -150,21 +149,25 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() .AddNextInstance(TimeSpan.FromDays(10), person: personA) .AddNextInstance(TimeSpan.FromDays(10), person: personB, extId: "1") .Build(); - AddPosition(manySmallInstancesWithoutFutureInstance, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); + testData.AddPosition(manySmallInstancesWithoutFutureInstance, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); var nonEndingPosition = new PositionBuilder() .WithInstance(Past, now.AddMonths(2), person: personA) .AddNextInstance(TimeSpan.FromDays(31), person: personB) .Build(); - AddPosition(nonEndingPosition); + testData.AddPosition(nonEndingPosition); var nonActivePositionWithPastAllocationAndFutureTBN = new PositionBuilder() .WithInstance(now.AddDays(-3), now.AddDays(-2), person: personA) .AddNextInstance(now.AddDays(80), now.AddDays(120)) .Build(); - AddPosition(nonActivePositionWithPastAllocationAndFutureTBN); + testData.AddPosition(nonActivePositionWithPastAllocationAndFutureTBN); + + var shouldBeIncludedInReport = testData.ShouldBeIncludedInReport; + var positionsToTest = testData.PositionsToTest; + var instanceToBeIncluded = testData.InstanceToBeIncluded; if (shouldBeIncludedInReport.Distinct().Count() != shouldBeIncludedInReport.Count) throw new InvalidOperationException($"Test setup error: Duplicate position names in {nameof(shouldBeIncludedInReport)}"); @@ -189,99 +192,125 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() } // Check that there are no extra positions that should not be included - data.Should().HaveSameCount(shouldBeIncludedInReport, "All positions that should be included in the report should be included"); - return; - - // Helper method - void AddPosition(ApiPositionV2 position, bool shouldBeIncludedInReportList = false, Func? instanceSelector = null, [CallerArgumentExpression("position")] string positionName = null!) - { - ArgumentNullException.ThrowIfNull(position); - - if (shouldBeIncludedInReportList) - shouldBeIncludedInReport.Add(positionName); - - positionsToTest.Add(position); - position.Name = positionName; - - if (shouldBeIncludedInReportList && instanceSelector is not null) - { - var instances = position.Instances.Where(instanceSelector).ToArray(); - - if (instances.Length == 0) - throw new InvalidOperationException($"Test setup error: No instance found for position {positionName} that matches the selector"); - - if (instances.Length > 1) - throw new InvalidOperationException($"Test setup error: Multiple instances found for position {positionName} that matches the selector"); - - instanceToBeIncluded.Add(position, instances.First()); - } - } + data.Should().HaveSameCount(shouldBeIncludedInReport, $"Exactly these positions should be included in the report, {string.Join(", ", shouldBeIncludedInReport)}, these should not be included {string.Join(", ", positionsToTest.Select(p => p.Name).Except(shouldBeIncludedInReport))}"); } [Fact] public void GetTBNPositionsStartingWithinThreeMonthsTests() { + var testData = new ReportTestDataContainer(); + var person = new ApiPersonV2() { AzureUniqueId = Guid.NewGuid(), Name = "Test Name" }; - var activePositions = + var activePositionWithPerson = new PositionBuilder() - .WithInstance(now.Subtract(TimeSpan.FromDays(1)), now.AddMonths(2)) - .AddNextInstance(TimeSpan.FromDays(26)) + .WithInstance(Past, now.AddMonths(2), person: person) + .AddNextInstance(TimeSpan.FromDays(40), person: person) .Build(); - activePositions.Name = nameof(activePositions); + testData.AddPosition(activePositionWithPerson); var nonActiveWithinThreeMonthsWithPerson = new PositionBuilder() .WithInstance(now.AddMonths(2), now.AddMonths(3), person) .AddNextInstance(TimeSpan.FromDays(26)) + .AddNextInstance(TimeSpan.FromDays(26)) .Build(); - nonActiveWithinThreeMonthsWithPerson.Name = nameof(nonActiveWithinThreeMonthsWithPerson); + testData.AddPosition(nonActiveWithinThreeMonthsWithPerson); + + var activePositionWithPersonButFutureWithoutPerson = + new PositionBuilder() + .WithInstance(Past, now.AddMonths(2), person: person) + .AddNextInstance(TimeSpan.FromDays(40), extId: "1") + .AddNextInstance(TimeSpan.FromDays(40)) + .Build(); + testData.AddPosition(activePositionWithPersonButFutureWithoutPerson, shouldBeIncludedInReportList: true, i => i.ExternalId == "1"); - var nonActiveWithinThreeMonthsNoPersonButHasRequest = + var nonActiveWithinThreeMonths = new PositionBuilder() .WithInstance(now.AddMonths(2), now.AddMonths(3)) + .AddNextInstance(TimeSpan.FromDays(26), person) .Build(); - nonActiveWithinThreeMonthsNoPersonButHasRequest.Name = nameof(nonActiveWithinThreeMonthsNoPersonButHasRequest); + testData.AddPosition(nonActiveWithinThreeMonths, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); - var request = new IResourcesApiClient.ResourceAllocationRequest() - { - Id = Guid.NewGuid(), - OrgPosition = new() - { - Id = nonActiveWithinThreeMonthsNoPersonButHasRequest.Id - } - }; - var nonActiveOutsideThreeMonths = new PositionBuilder() .WithInstance(now.AddMonths(4), now.AddMonths(5)) .Build(); - nonActiveOutsideThreeMonths.Name = nameof(nonActiveOutsideThreeMonths); + testData.AddPosition(nonActiveOutsideThreeMonths); var nonActiveWithinThreeMonthsNoPerson = new PositionBuilder() - .WithInstance(now.AddMonths(2), now.AddMonths(3)) + .WithInstance(now.AddMonths(-3), now.AddMonths(-2)) + .AddNextInstance(now.AddMonths(2), now.AddMonths(3), extId: "1") .Build(); - nonActiveWithinThreeMonthsNoPerson.Name = nameof(nonActiveWithinThreeMonthsNoPerson); + testData.AddPosition(nonActiveWithinThreeMonthsNoPerson, shouldBeIncludedInReportList: true, i => i.ExternalId == "1"); + var pastPositionWithPerson = + new PositionBuilder() + .WithInstance(now.AddMonths(-3), now.AddMonths(-2), person) + .AddNextInstance(now.AddMonths(-2), now.AddMonths(-1)) + .Build(); + testData.AddPosition(pastPositionWithPerson); + + + var data = WeeklyTaskOwnerReportDataCreator.GetTBNPositionsStartingWithinThreeMonths(testData.PositionsToTest); + + data.Should().OnlyHaveUniqueItems(); + foreach (var positionName in testData.ShouldBeIncludedInReport) + { + data.Should().ContainSingle(p => p.Position.Name == positionName, $"Position {positionName} should be included in the report"); + } - var data = WeeklyTaskOwnerReportDataCreator.GetTBNPositionsStartingWithinThreeMonths(new List + // Ensure that the starts at date is set correctly + foreach (var (position, apiPositionInstanceV2) in testData.InstanceToBeIncluded) { - activePositions, - nonActiveWithinThreeMonthsWithPerson, - nonActiveWithinThreeMonthsNoPerson, - nonActiveOutsideThreeMonths - }, [request]); + data.Should().ContainSingle(p => p.StartsAt == apiPositionInstanceV2.AppliesFrom && p.Position.Id == position.Id, $"Position {position.Name} should have an instance that starts at {apiPositionInstanceV2.AppliesFrom}"); + } + + // Check that there are no extra positions that should not be included + data.Should().HaveSameCount(testData.ShouldBeIncludedInReport, + $"Exactly these positions should be included in the report, {string.Join(", ", testData.ShouldBeIncludedInReport)}," + + $" these should not be included {string.Join(", ", testData.PositionsToTest.Select(p => p.Name).Except(testData.ShouldBeIncludedInReport))}"); + } + - data.Should().ContainSingle(p => p.Position.Id == nonActiveWithinThreeMonthsNoPerson.Id); + private class ReportTestDataContainer + { + public List ShouldBeIncludedInReport { get; } = new(); + public List PositionsToTest { get; } = new(); + public Dictionary InstanceToBeIncluded { get; } = new(); + + public void AddPosition(ApiPositionV2 position, bool shouldBeIncludedInReportList = false, Func? instanceSelector = null, [CallerArgumentExpression("position")] string positionName = null!) + { + ArgumentNullException.ThrowIfNull(position); + + if (shouldBeIncludedInReportList) + ShouldBeIncludedInReport.Add(positionName); + + PositionsToTest.Add(position); + position.Name = positionName; + + if (shouldBeIncludedInReportList && instanceSelector is not null) + { + var instances = position.Instances.Where(instanceSelector).ToArray(); + + if (instances.Length == 0) + throw new InvalidOperationException($"Test setup error: No instance found for position {positionName} that matches the selector"); + + if (instances.Length > 1) + throw new InvalidOperationException($"Test setup error: Multiple instances found for position {positionName} that matches the selector"); + + InstanceToBeIncluded.Add(position, instances.First()); + } + } } @@ -360,7 +389,8 @@ public InstanceChainBuilder AddNextInstance(DateTime appliesFrom, DateTime appli AssignedPerson = person, Type = type, AppliesFrom = appliesFrom, - AppliesTo = appliesTo + AppliesTo = appliesTo, + ExternalId = extId }); return this; }