Skip to content

Commit

Permalink
Removing server-side approval "calculations" that are just filters
Browse files Browse the repository at this point in the history
With the new "all of history" concept, temporal filtering needs to be done at the point of use (report, web app, etc.)
  • Loading branch information
LarsKemmann committed Jan 23, 2024
1 parent eea8646 commit 4f18010
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 117 deletions.
123 changes: 11 additions & 112 deletions src/CareTogether.Core/Engines/PolicyEvaluation/ApprovalCalculations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,34 +223,28 @@ internal static
}

internal static
(RoleApprovalStatus? Status, DateTime? ExpiresAtUtc, ImmutableList<string> MissingRequirements, ImmutableList<string> AvailableApplications)
(DateOnlyTimeline<RoleApprovalStatus>? Status,
ImmutableList<(string ActionName, RequirementStage Stage, DateOnlyTimeline? MetOrExemptedWhen)> RequirementCompletions)
CalculateIndividualVolunteerRoleApprovalStatus(
ImmutableDictionary<string, ActionRequirement> actionDefinitions, VolunteerRolePolicyVersion policyVersion,
ImmutableList<CompletedRequirementInfo> completedRequirements, ImmutableList<ExemptedRequirementInfo> exemptedRequirements)
{
var supersededAtUtc = policyVersion.SupersededAtUtc;
var policyVersionSupersededAtUtc = policyVersion.SupersededAtUtc;

// For each requirement of the policy version for this role, find the dates for which it was met or exempted.
// If there are none, the resulting timeline will be 'null'.
var requirementCompletionStatus = policyVersion.Requirements.Select(requirement =>
{
var actionDefinition = actionDefinitions[requirement.ActionName];
return (requirement.ActionName, requirement.Stage, RequirementApprovals:
return (requirement.ActionName, requirement.Stage, MetOrExemptedWhen:
SharedCalculations.FindRequirementApprovals(requirement.ActionName, actionDefinition.Validity,
completedRequirements, exemptedRequirements));
policyVersionSupersededAtUtc, completedRequirements, exemptedRequirements));
}).ToImmutableList();

var simpleRequirementCompletionStatus = requirementCompletionStatus
.Select(status => (status.ActionName, status.Stage, RequirementMetOrExempted: status.RequirementApprovals != null))
.ToImmutableList();

var status = CalculateRoleApprovalStatusesFromRequirementCompletions(requirementCompletionStatus);
//TODO: This is simplistic. Do we want to instead calculate 'missing as of date'? If so, that feels like a separate calculation.
//TODO: Do we want to return 'expiredRequirements' as well?
var missingRequirements = CalculateMissingIndividualRequirementsFromRequirementCompletion(status.Status, simpleRequirementCompletionStatus);
var availableApplications = CalculateAvailableIndividualApplicationsFromRequirementCompletion(status.Status, simpleRequirementCompletionStatus);
// Calculate the combined approval status timeline for this role based on this policy version.
var roleApprovalStatus = CalculateRoleApprovalStatusesFromRequirementCompletions(requirementCompletionStatus);

return (status.Status, status.ExpiresAtUtc, MissingRequirements: missingRequirements, AvailableApplications: availableApplications);
return (roleApprovalStatus, requirementCompletionStatus);
}

internal static
Expand Down Expand Up @@ -303,16 +297,16 @@ internal static
}

internal static DateOnlyTimeline<RoleApprovalStatus> CalculateRoleApprovalStatusesFromRequirementCompletions(
ImmutableList<(string ActionName, RequirementStage Stage, DateOnlyTimeline? RequirementApprovals)> requirementCompletionStatus)
ImmutableList<(string ActionName, RequirementStage Stage, DateOnlyTimeline? MetOrExemptedWhen)> requirementCompletionStatus)
{
// Instead of a single status and an expiration, return a set of *each* RoleApprovalStatus and DateOnlyTimeline?
// so that the caller gets a full picture of the role's approval history.

static DateOnlyTimeline? FindRangesWhereAllAreSatisfied(
IEnumerable<(string ActionName, RequirementStage Stage, DateOnlyTimeline? RequirementApprovals)> values)
IEnumerable<(string ActionName, RequirementStage Stage, DateOnlyTimeline? MetOrExemptedWhen)> values)
{
return DateOnlyTimeline.IntersectionOf(
values.Select(value => value.RequirementApprovals).ToImmutableList());
values.Select(value => value.MetOrExemptedWhen).ToImmutableList());
}

var onboarded = FindRangesWhereAllAreSatisfied(requirementCompletionStatus);
Expand Down Expand Up @@ -352,101 +346,6 @@ internal static DateOnlyTimeline<RoleApprovalStatus> CalculateRoleApprovalStatus
return result;
}

internal static ImmutableList<string> CalculateAvailableIndividualApplicationsFromRequirementCompletion(RoleApprovalStatus? status,
ImmutableList<(string ActionName, RequirementStage Stage, bool RequirementMetOrExempted)> requirementCompletionStatus) =>
status switch
{
RoleApprovalStatus.Onboarded => ImmutableList<string>.Empty,
RoleApprovalStatus.Approved => ImmutableList<string>.Empty,
RoleApprovalStatus.Prospective => ImmutableList<string>.Empty,
null => requirementCompletionStatus
.Where(x => !x.RequirementMetOrExempted && x.Stage == RequirementStage.Application)
.Select(x => x.ActionName)
.ToImmutableList(),
_ => throw new NotImplementedException(
$"The volunteer role approval status '{status}' has not been implemented.")
};

internal static ImmutableList<string> CalculateMissingIndividualRequirementsFromRequirementCompletion(RoleApprovalStatus? status,
ImmutableList<(string ActionName, RequirementStage Stage, bool RequirementMetOrExempted)> requirementCompletionStatus) =>
status switch
{
RoleApprovalStatus.Onboarded => ImmutableList<string>.Empty,
RoleApprovalStatus.Approved => requirementCompletionStatus
.Where(x => !x.RequirementMetOrExempted && x.Stage == RequirementStage.Onboarding)
.Select(x => x.ActionName)
.ToImmutableList(),
RoleApprovalStatus.Prospective => requirementCompletionStatus
.Where(x => !x.RequirementMetOrExempted && x.Stage == RequirementStage.Approval)
.Select(x => x.ActionName)
.ToImmutableList(),
null => ImmutableList<string>.Empty,
_ => throw new NotImplementedException(
$"The volunteer role approval status '{status}' has not been implemented.")
};

internal static ImmutableList<string> CalculateMissingFamilyRequirementsFromRequirementCompletion(RoleApprovalStatus? status,
ImmutableList<(string ActionName, RequirementStage Stage, VolunteerFamilyRequirementScope Scope, bool RequirementMetOrExempted, List<Guid> RequirementMissingForIndividuals)> requirementsMet)
{
return status switch
{
RoleApprovalStatus.Onboarded => ImmutableList<string>.Empty,
RoleApprovalStatus.Approved => requirementsMet
.Where(x => !x.RequirementMetOrExempted && x.Stage == RequirementStage.Onboarding
&& x.Scope == VolunteerFamilyRequirementScope.OncePerFamily)
.Select(x => x.ActionName)
.ToImmutableList(),
RoleApprovalStatus.Prospective => requirementsMet
.Where(x => !x.RequirementMetOrExempted && x.Stage == RequirementStage.Approval
&& x.Scope == VolunteerFamilyRequirementScope.OncePerFamily)
.Select(x => x.ActionName)
.ToImmutableList(),
null => ImmutableList<string>.Empty,
_ => throw new NotImplementedException(
$"The volunteer role approval status '{status}' has not been implemented.")
};
}

internal static ImmutableList<string> CalculateAvailableFamilyApplicationsFromRequirementCompletion(RoleApprovalStatus? status,
ImmutableList<(string ActionName, RequirementStage Stage, VolunteerFamilyRequirementScope Scope, bool RequirementMetOrExempted, List<Guid> RequirementMissingForIndividuals)> requirementsMet)
{
return status switch
{
RoleApprovalStatus.Onboarded => ImmutableList<string>.Empty,
RoleApprovalStatus.Approved => ImmutableList<string>.Empty,
RoleApprovalStatus.Prospective => ImmutableList<string>.Empty,
null => requirementsMet
.Where(x => !x.RequirementMetOrExempted && x.Stage == RequirementStage.Application
&& x.Scope == VolunteerFamilyRequirementScope.OncePerFamily)
.Select(x => x.ActionName)
.ToImmutableList(),
_ => throw new NotImplementedException(
$"The volunteer role approval status '{status}' has not been implemented.")
};
}

internal static ImmutableDictionary<Guid, ImmutableList<string>> CalculateMissingFamilyIndividualRequirementsFromRequirementCompletion(RoleApprovalStatus? status,
ImmutableList<(string ActionName, RequirementStage Stage, VolunteerFamilyRequirementScope Scope, bool RequirementMetOrExempted, List<Guid> RequirementMissingForIndividuals)> requirementsMet)
{
return status switch
{
RoleApprovalStatus.Onboarded => ImmutableDictionary<Guid, ImmutableList<string>>.Empty,
RoleApprovalStatus.Approved => requirementsMet
.Where(x => x.Stage == RequirementStage.Onboarding)
.SelectMany(x => x.RequirementMissingForIndividuals.Select(y => (PersonId: y, x.ActionName)))
.GroupBy(x => x.PersonId)
.ToImmutableDictionary(x => x.Key, x => x.Select(y => y.ActionName).ToImmutableList()),
RoleApprovalStatus.Prospective => requirementsMet
.Where(x => x.Stage == RequirementStage.Approval)
.SelectMany(x => x.RequirementMissingForIndividuals.Select(y => (PersonId: y, x.ActionName)))
.GroupBy(x => x.PersonId)
.ToImmutableDictionary(x => x.Key, x => x.Select(y => y.ActionName).ToImmutableList()),
null => ImmutableDictionary<Guid, ImmutableList<string>>.Empty,
_ => throw new NotImplementedException(
$"The volunteer role approval status '{status}' has not been implemented.")
};
}

internal static SharedCalculations.RequirementCheckResult FamilyRequirementMetOrExempted(string roleName,
string requirementActionName, VolunteerFamilyRequirementScope requirementScope,
DateTime? supersededAtUtc,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,18 @@ internal static RequirementCheckResult RequirementMetOrExempted(string requireme
// A return value of 'null' indicates no approval.
// Further note: action validity was previously not being handled but now is.
internal static DateOnlyTimeline? FindRequirementApprovals(
string requirementName, TimeSpan? actionValidity,
string requirementName, TimeSpan? actionValidity, DateTime? policyVersionSupersededAtUtc,
ImmutableList<CompletedRequirementInfo> completedRequirementsInScope,
ImmutableList<ExemptedRequirementInfo> exemptedRequirementsInScope)
{
//NOTE: This does *not* account for policy supersedence, as that is a higher-level concept
// that applies to *sets* of requirements.
// Policy supersedence means that, as of the 'SupersededAtUtc' date, the policy version is no longer in effect.
// As a result, while approvals granted under that policy version continue to be valid, any requirements that
// were completed or exempted *on or after* that date cannot be taken into account for the purposes of determining
// role approval status under this policy version.

var matchingCompletions = completedRequirementsInScope
.Where(completed => completed.RequirementName == requirementName)
.Where(completed => completed.RequirementName == requirementName &&
(policyVersionSupersededAtUtc == null || completed.CompletedAtUtc < policyVersionSupersededAtUtc))
.Select(completed => new DateRange(
DateOnly.FromDateTime(completed.CompletedAtUtc),
actionValidity == null
Expand All @@ -65,6 +68,9 @@ internal static RequirementCheckResult RequirementMetOrExempted(string requireme

var matchingExemptions = exemptedRequirementsInScope
.Where(exempted => exempted.RequirementName == requirementName)
//TODO: Exemptions currently cannot be backdated, which may need to change in order to
// fully support handling policy exemptions correctly within the supersedence constraint.
// && (policyVersionSupersededAtUtc == null || exempted.TimestampUtc < policyVersionSupersededAtUtc))
.Select(exempted => new DateRange(
//NOTE: This limits exemptions to being valid as of the time they were created.
// If we want to allow backdating or postdating exemptions, we'll need to change this.
Expand All @@ -74,7 +80,6 @@ internal static RequirementCheckResult RequirementMetOrExempted(string requireme
: DateOnly.FromDateTime(exempted.ExemptionExpiresAtUtc.Value)))
.ToImmutableList();

//TODO: Upgrade to .NET 8 and start using frozen collections?
return DateOnlyTimeline.UnionOf(matchingCompletions.Concat(matchingExemptions).ToImmutableList());
}
}
Expand Down

0 comments on commit 4f18010

Please sign in to comment.