From 4f180103759a8f1a43ee4dbe8d23da5410d30784 Mon Sep 17 00:00:00 2001 From: Lars Kemmann Date: Mon, 22 Jan 2024 23:02:14 -0500 Subject: [PATCH] Removing server-side approval "calculations" that are just filters With the new "all of history" concept, temporal filtering needs to be done at the point of use (report, web app, etc.) --- .../PolicyEvaluation/ApprovalCalculations.cs | 123 ++---------------- .../PolicyEvaluation/SharedCalculations.cs | 15 ++- 2 files changed, 21 insertions(+), 117 deletions(-) diff --git a/src/CareTogether.Core/Engines/PolicyEvaluation/ApprovalCalculations.cs b/src/CareTogether.Core/Engines/PolicyEvaluation/ApprovalCalculations.cs index a9a9efa0..9df6fb3e 100644 --- a/src/CareTogether.Core/Engines/PolicyEvaluation/ApprovalCalculations.cs +++ b/src/CareTogether.Core/Engines/PolicyEvaluation/ApprovalCalculations.cs @@ -223,34 +223,28 @@ internal static } internal static - (RoleApprovalStatus? Status, DateTime? ExpiresAtUtc, ImmutableList MissingRequirements, ImmutableList AvailableApplications) + (DateOnlyTimeline? Status, + ImmutableList<(string ActionName, RequirementStage Stage, DateOnlyTimeline? MetOrExemptedWhen)> RequirementCompletions) CalculateIndividualVolunteerRoleApprovalStatus( ImmutableDictionary actionDefinitions, VolunteerRolePolicyVersion policyVersion, ImmutableList completedRequirements, ImmutableList 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 @@ -303,16 +297,16 @@ internal static } internal static DateOnlyTimeline 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); @@ -352,101 +346,6 @@ internal static DateOnlyTimeline CalculateRoleApprovalStatus return result; } - internal static ImmutableList CalculateAvailableIndividualApplicationsFromRequirementCompletion(RoleApprovalStatus? status, - ImmutableList<(string ActionName, RequirementStage Stage, bool RequirementMetOrExempted)> requirementCompletionStatus) => - status switch - { - RoleApprovalStatus.Onboarded => ImmutableList.Empty, - RoleApprovalStatus.Approved => ImmutableList.Empty, - RoleApprovalStatus.Prospective => ImmutableList.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 CalculateMissingIndividualRequirementsFromRequirementCompletion(RoleApprovalStatus? status, - ImmutableList<(string ActionName, RequirementStage Stage, bool RequirementMetOrExempted)> requirementCompletionStatus) => - status switch - { - RoleApprovalStatus.Onboarded => ImmutableList.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.Empty, - _ => throw new NotImplementedException( - $"The volunteer role approval status '{status}' has not been implemented.") - }; - - internal static ImmutableList CalculateMissingFamilyRequirementsFromRequirementCompletion(RoleApprovalStatus? status, - ImmutableList<(string ActionName, RequirementStage Stage, VolunteerFamilyRequirementScope Scope, bool RequirementMetOrExempted, List RequirementMissingForIndividuals)> requirementsMet) - { - return status switch - { - RoleApprovalStatus.Onboarded => ImmutableList.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.Empty, - _ => throw new NotImplementedException( - $"The volunteer role approval status '{status}' has not been implemented.") - }; - } - - internal static ImmutableList CalculateAvailableFamilyApplicationsFromRequirementCompletion(RoleApprovalStatus? status, - ImmutableList<(string ActionName, RequirementStage Stage, VolunteerFamilyRequirementScope Scope, bool RequirementMetOrExempted, List RequirementMissingForIndividuals)> requirementsMet) - { - return status switch - { - RoleApprovalStatus.Onboarded => ImmutableList.Empty, - RoleApprovalStatus.Approved => ImmutableList.Empty, - RoleApprovalStatus.Prospective => ImmutableList.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> CalculateMissingFamilyIndividualRequirementsFromRequirementCompletion(RoleApprovalStatus? status, - ImmutableList<(string ActionName, RequirementStage Stage, VolunteerFamilyRequirementScope Scope, bool RequirementMetOrExempted, List RequirementMissingForIndividuals)> requirementsMet) - { - return status switch - { - RoleApprovalStatus.Onboarded => ImmutableDictionary>.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>.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, diff --git a/src/CareTogether.Core/Engines/PolicyEvaluation/SharedCalculations.cs b/src/CareTogether.Core/Engines/PolicyEvaluation/SharedCalculations.cs index 2094d2e6..4dae1e9a 100644 --- a/src/CareTogether.Core/Engines/PolicyEvaluation/SharedCalculations.cs +++ b/src/CareTogether.Core/Engines/PolicyEvaluation/SharedCalculations.cs @@ -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 completedRequirementsInScope, ImmutableList 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 @@ -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. @@ -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()); } }