Skip to content

Commit

Permalink
Refactoring + updated TBNPositionsStartingWithinThreeMonths calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonathanio123 committed Dec 12, 2024
1 parent b6fdea2 commit 4ebbe0c
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TBNPosition> GetTBNPositionsStartingWithinThreeMonths(IEnumerable<ApiPositionV2> allProjectPositions,
ICollection<IResourcesApiClient.ResourceAllocationRequest> requests)
public static List<TBNPosition> GetTBNPositionsStartingWithinThreeMonths(IEnumerable<ApiPositionV2> allProjectPositions)
{
var nowDate = NowDate;
var expiringDate = nowDate.AddMonths(3);
Expand All @@ -29,16 +28,12 @@ public static List<TBNPosition> 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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest()
{
#region Arrange

var testData = new ReportTestDataContainer();


var personA = new ApiPersonV2()
{
AzureUniqueId = Guid.NewGuid(),
Expand All @@ -56,75 +59,71 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest()
Name = "Test NameB"
};

var shouldBeIncludedInReport = new List<string>();
var positionsToTest = new List<ApiPositionV2>();
var instanceToBeIncluded = new Dictionary<ApiPositionV2, ApiPositionInstanceV2>();

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
Expand All @@ -133,15 +132,15 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest()
.AddNextInstance(now.AddDays(30 * 2), now.AddDays(30 * 4), person: personA)
.Build();

AddPosition(activePositionWithFutureInstanceWithSmallGap);
testData.AddPosition(activePositionWithFutureInstanceWithSmallGap);


var activePositionWithFutureInstanceWithLargerGap = new PositionBuilder()
.WithInstance(Past, now.AddDays(30), person: personA, extId: "1")
.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()
Expand All @@ -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)}");
Expand All @@ -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<ApiPositionInstanceV2, bool>? 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<ApiPositionV2>
// 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<string> ShouldBeIncludedInReport { get; } = new();
public List<ApiPositionV2> PositionsToTest { get; } = new();
public Dictionary<ApiPositionV2, ApiPositionInstanceV2> InstanceToBeIncluded { get; } = new();

public void AddPosition(ApiPositionV2 position, bool shouldBeIncludedInReportList = false, Func<ApiPositionInstanceV2, bool>? 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());
}
}
}


Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit 4ebbe0c

Please sign in to comment.