Skip to content

Commit 0697b9e

Browse files
CohortStatus dataclass, built final result
1 parent 0b86fb2 commit 0697b9e

2 files changed

Lines changed: 67 additions & 56 deletions

File tree

src/eligibility_signposting_api/model/eligibility.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from functools import total_ordering
77
from typing import NewType, Self
88

9+
from eligibility_signposting_api.model import rules
10+
911
NHSNumber = NewType("NHSNumber", str)
1012
DateOfBirth = NewType("DateOfBirth", date)
1113
Postcode = NewType("Postcode", str)
@@ -70,6 +72,13 @@ class Condition:
7072
reasons: list[Reason]
7173

7274

75+
@dataclass
76+
class CohortStatus:
77+
cohort: rules.IterationCohort
78+
status: Status
79+
reasons: list[Reason]
80+
81+
7382
@dataclass
7483
class EligibilityStatus:
7584
"""Represents a person's eligibility for vaccination."""

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 58 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
from __future__ import annotations
22

3-
from _operator import attrgetter
3+
from _operator import add, attrgetter
44
from collections import defaultdict
55
from collections.abc import Collection, Iterator, Mapping
66
from dataclasses import dataclass, field
7-
from functools import cached_property
7+
from functools import cached_property, reduce
88
from itertools import groupby
99
from typing import Any
1010

11-
from localstack.services.stepfunctions.asl.component.state.state_execution.state_map import iteration
1211
from wireup import service
1312

1413
from eligibility_signposting_api.model import eligibility, rules
15-
from eligibility_signposting_api.model.eligibility import Status
14+
from eligibility_signposting_api.model.eligibility import CohortStatus, Condition, ConditionName, Status
1615
from eligibility_signposting_api.services.calculators.rule_calculator import RuleCalculator
1716

1817
Row = Collection[Mapping[str, Any]]
@@ -73,78 +72,82 @@ def evaluate_eligibility_back_up(self) -> eligibility.EligibilityStatus:
7372
from collections import defaultdict
7473

7574
# Assuming cohort_results contains tuples of (IterationCohort, Status, list[Reason])
76-
def get_best_cohort(self,
77-
cohort_results: dict[
78-
str, tuple[rules.IterationCohort, eligibility.Status, list[eligibility.Reason]]]) -> tuple[
79-
rules.IterationCohort, eligibility.Status,
80-
list[
81-
eligibility.Reason]] | None:
82-
83-
84-
if not cohort_results:
85-
return None
86-
75+
def get_best_cohort(self, cohort_results: dict[str, CohortStatus]) -> tuple[Status, list[CohortStatus]]:
8776
# Find the best status across cohorts
88-
best_status = eligibility.Status.best(*[result[1] for result in cohort_results.values()])
77+
best_status = eligibility.Status.best(*[result.status for result in cohort_results.values()])
8978

9079
# Filter cohorts that match the best status
91-
best_cohorts = [result for result in cohort_results.values() if result[1] == best_status]
92-
93-
# Pick the cohort with the highest priority
94-
#TODO return the appropriate result
95-
# Actionable,Actionable, not-actionable, not-actionable, not eligible,not eligible
96-
return max(best_cohorts, key=lambda cohort: cohort[0].priority) if best_cohorts else None
80+
best_cohorts = [result for result in cohort_results.values() if result.status == best_status]
81+
return best_status, best_cohorts
9782

98-
def evaluate_eligibility(self):
83+
def evaluate_eligibility(self) -> eligibility.EligibilityStatus:
9984
"""Iterates over campaign groups, evaluates eligibility, and returns a consolidated status."""
10085
priority_getter = attrgetter("priority")
101-
results: dict[
102-
str, tuple[rules.IterationCohort, eligibility.Status, list[eligibility.Reason]]] = defaultdict()
86+
results: dict[ConditionName, tuple[Status, list[CohortStatus]]] = defaultdict()
10387
for condition_name, campaign_group in self.campaigns_grouped_by_condition_name:
104-
iteration_results: dict[
105-
str, tuple[rules.IterationCohort, eligibility.Status, list[eligibility.Reason]]] = defaultdict()
88+
iteration_results: dict[str, tuple[Status, list[CohortStatus]]] = defaultdict()
10689
for active_iteration in [cc.current_iteration for cc in campaign_group]:
107-
cohort_results: dict[
108-
str, tuple[rules.IterationCohort, eligibility.Status, list[eligibility.Reason]]] = defaultdict()
109-
rules_by_type = {rule_type: tuple(
110-
rule for rule in active_iteration.iteration_rules if attrgetter("type")(rule) == rule_type)
111-
for rule_type in {"F", "R", "S"}}
90+
cohort_results: dict[str, CohortStatus] = defaultdict()
91+
92+
# Get the rules for this iteration
93+
rules_filter, rules_suppression, rules_redirect = {
94+
rule_type: tuple(
95+
rule for rule in active_iteration.iteration_rules if attrgetter("type")(rule) == rule_type
96+
)
97+
for rule_type in (rules.RuleType.filter, rules.RuleType.suppression, rules.RuleType.redirect)
98+
}.values()
11299

113100
for cohort in sorted(active_iteration.iteration_cohorts, key=priority_getter):
114101
# Check base Eligibility
115102
if cohort.cohort_label in self.person_cohorts:
116103
# Base eligible
117104
# Check Eligibility - F - Rules
118105
eli_flag: bool = True
119-
for _, rule_group in groupby(sorted(rules_by_type["F"], key=priority_getter),
120-
key=priority_getter):
106+
for _, rule_group in groupby(sorted(rules_filter, key=priority_getter), key=priority_getter):
121107
# iter F rules by priority and grouping
122108
# find first exclusion - throws
123109
status, group_actionable, group_exclusions = self.evaluate_rules_priority_group(rule_group)
124110
if status.is_exclusion:
125-
cohort_results[cohort.cohort_label] = (cohort, status, group_exclusions)
111+
cohort_results[cohort.cohort_label] = CohortStatus(cohort, status, group_exclusions)
126112
eli_flag = False
127113
break
128-
if not eli_flag: continue
129114

130-
for _, rule_group in groupby(sorted(rules_by_type["S"], key=priority_getter),
131-
key=priority_getter):
132-
status, group_actionable, group_exclusions = self.evaluate_rules_priority_group(
133-
rule_group)
134-
if status.is_exclusion:
135-
cohort_results[cohort.cohort_label] = (cohort, status, group_exclusions)
136-
break
115+
if eli_flag:
116+
for _, rule_group in groupby(
117+
sorted(rules_suppression, key=priority_getter), key=priority_getter
118+
):
119+
status, group_actionable, group_exclusions = self.evaluate_rules_priority_group(
120+
rule_group
121+
)
122+
if status.is_exclusion:
123+
cohort_results[cohort.cohort_label] = CohortStatus(cohort, status, group_exclusions)
124+
break
125+
# No exclusions - actionable
126+
cohort_results[cohort.cohort_label] = CohortStatus(cohort, Status.actionable, [])
137127
else:
138128
# Not base eligibility
139-
cohort_results[cohort.cohort_label] = (cohort, eligibility.Status.not_eligible, [])
140-
141-
# Determine Result between cohorts
142-
iteration_results[active_iteration.name] = self.get_best_cohort(cohort_results)
143-
# Determine results between iterations
144-
results[condition_name] = self.get_best_cohort(iteration_results)
129+
cohort_results[cohort.cohort_label] = CohortStatus(cohort, eligibility.Status.not_eligible, [])
130+
131+
# Determine Result between cohorts - get the best
132+
iteration_results[active_iteration.name] = self.get_best_cohort(cohort_results) # multiple
133+
# Determine results between iterations - get the best
134+
best_so_far: Status = eligibility.Status.not_eligible
135+
for iteration_result in iteration_results.values():
136+
best_so_far = eligibility.Status.best(best_so_far, iteration_result[0])
137+
for iteration_result in iteration_results.values():
138+
if iteration_result[0] is best_so_far:
139+
results[condition_name] = iteration_result
140+
break # if the status is the same, we can break out of the loop - we are picking the first one
145141
# Consolidate all the results and return
146-
147-
return eligibility.EligibilityStatus(conditions=list(self.results))
142+
final_result = [
143+
Condition(
144+
condition_name=condition_name,
145+
status=status,
146+
reasons=reduce(add, [cohort.reasons for cohort in cohort_status_list], []),
147+
)
148+
for condition_name, (status, cohort_status_list) in results.items()
149+
]
150+
return eligibility.EligibilityStatus(conditions=final_result)
148151

149152
def get_the_base_eligible_campaigns(self, campaign_group: list[rules.CampaignConfig]) -> list[rules.CampaignConfig]:
150153
"""Return campaigns for which the person is base eligible via cohorts."""
@@ -213,7 +216,7 @@ def evaluate_priority_group(
213216
ir
214217
for ir in iteration_rule_group
215218
if ir.type in (rules.RuleType.filter, rules.RuleType.suppression)
216-
and (ir.cohort_label is None or (ir.cohort_label in self.person_cohorts))
219+
and (ir.cohort_label is None or (ir.cohort_label in self.person_cohorts))
217220
]
218221

219222
best_status = eligibility.Status.not_eligible if exclude_capable_rules else eligibility.Status.actionable
@@ -233,17 +236,16 @@ def evaluate_priority_group(
233236
is_rule_stop = any(rule.rule_stop for rule in exclude_capable_rules)
234237
return worst_group_status, actionable_reasons, exclusion_reasons, is_rule_stop
235238

236-
def evaluate_rules_priority_group( # TODO refractor
237-
self,
238-
iteration_rule_group: Iterator[rules.IterationRule]
239+
def evaluate_rules_priority_group(
240+
self, iteration_rule_group: Iterator[rules.IterationRule]
239241
) -> tuple[eligibility.Status, list[eligibility.Reason], list[eligibility.Reason]]:
240242
status = Status.not_eligible
241243
exclusion_reasons, actionable_reasons = [], []
242244
exclude_capable_rules = [
243245
ir
244246
for ir in iteration_rule_group
245247
if ir.type in (rules.RuleType.filter, rules.RuleType.suppression)
246-
and (ir.cohort_label is None or (ir.cohort_label in self.person_cohorts))
248+
and (ir.cohort_label is None or (ir.cohort_label in self.person_cohorts))
247249
]
248250

249251
for rule in exclude_capable_rules:

0 commit comments

Comments
 (0)