Skip to content

Commit 94105b9

Browse files
committed
Merge branch 'main' into feature/terb-ELI-275-manual-uploads
2 parents 85ec07b + bec6439 commit 94105b9

4 files changed

Lines changed: 85 additions & 72 deletions

File tree

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 54 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ def get_best_cohort(cohort_results: dict[str, CohortResult]) -> tuple[Status, li
7070

7171
@staticmethod
7272
def get_exclusion_rules(
73-
cohort: IterationCohort, rules_filter: Iterable[rules.IterationRule]
73+
cohort: IterationCohort, filter_rules: Iterable[rules.IterationRule]
7474
) -> Iterator[rules.IterationRule]:
7575
return (
7676
ir
77-
for ir in rules_filter
77+
for ir in filter_rules
7878
if ir.cohort_label is None
7979
or cohort.cohort_label == ir.cohort_label
8080
or (isinstance(ir.cohort_label, (list, set, tuple)) and cohort.cohort_label in ir.cohort_label)
@@ -84,59 +84,30 @@ def get_exclusion_rules(
8484
def get_rules_by_type(
8585
active_iteration: Iteration,
8686
) -> tuple[tuple[rules.IterationRule, ...], tuple[rules.IterationRule, ...]]:
87-
rules_by_type = {
88-
rule_type: tuple(rule for rule in active_iteration.iteration_rules if attrgetter("type")(rule) == rule_type)
89-
for rule_type in (rules.RuleType.filter, rules.RuleType.suppression, rules.RuleType.redirect)
90-
}
91-
rules_filter = rules_by_type[rules.RuleType.filter]
92-
rules_suppression = rules_by_type[rules.RuleType.suppression]
93-
return rules_filter, rules_suppression
87+
filter_rules, suppression_rules = (
88+
tuple(rule for rule in active_iteration.iteration_rules if attrgetter("type")(rule) == rule_type)
89+
for rule_type in (rules.RuleType.filter, rules.RuleType.suppression)
90+
)
91+
return filter_rules, suppression_rules
9492

9593
def evaluate_eligibility(self) -> eligibility.EligibilityStatus:
9694
"""Iterates over campaign groups, evaluates eligibility, and returns a consolidated status."""
97-
results: dict[ConditionName, IterationResult] = {}
95+
condition_results: dict[ConditionName, IterationResult] = {}
9896

9997
for condition_name, campaign_group in self.campaigns_grouped_by_condition_name:
10098
iteration_results: dict[str, IterationResult] = {}
10199

102100
for active_iteration in [cc.current_iteration for cc in campaign_group]:
103101
cohort_results: dict[str, CohortResult] = {}
104102

105-
rules_filter, rules_suppression = self.get_rules_by_type(active_iteration)
103+
filter_rules, suppression_rules = self.get_rules_by_type(active_iteration)
106104
for cohort in sorted(active_iteration.iteration_cohorts, key=attrgetter("priority")):
107-
# Check Base Eligibility
105+
# Base Eligibility - check
108106
if cohort.cohort_label in self.person_cohorts or cohort.cohort_label == magic_cohort:
109-
is_eligible: bool = True
110-
is_eligible = self.evaluate_filter_rules(
111-
cohort,
112-
cohort_results,
113-
rules_filter,
114-
is_eligible=is_eligible,
115-
)
116-
117-
if is_eligible:
118-
is_actionable: bool = True
119-
suppression_reasons, is_actionable = self.evaluate_suppression_rules(
120-
cohort,
121-
rules_suppression,
122-
is_actionable=is_actionable,
123-
)
124-
if cohort.cohort_label is not None:
125-
key = cohort.cohort_label
126-
if is_actionable:
127-
cohort_results[key] = CohortResult(
128-
cohort.cohort_group if cohort.cohort_group else key,
129-
Status.actionable,
130-
[],
131-
str(cohort.positive_description),
132-
)
133-
else:
134-
cohort_results[key] = CohortResult(
135-
cohort.cohort_group if cohort.cohort_group else key,
136-
Status.not_actionable,
137-
suppression_reasons,
138-
str(cohort.positive_description),
139-
)
107+
# Eligibility - check
108+
if self.is_eligible_by_filter_rules(cohort, cohort_results, filter_rules):
109+
# Actionability - evaluation
110+
self.evaluate_suppression_rules(cohort, cohort_results, suppression_rules)
140111

141112
# Not base eligible
142113
elif cohort.cohort_label is not None:
@@ -156,7 +127,7 @@ def evaluate_eligibility(self) -> eligibility.EligibilityStatus:
156127
best_candidate = max(iteration_results.values(), key=lambda r: r.status.value)
157128
else:
158129
best_candidate = IterationResult(eligibility.Status.not_eligible, [])
159-
results[condition_name] = best_candidate
130+
condition_results[condition_name] = best_candidate
160131

161132
# Consolidate all the results and return
162133
final_result = [
@@ -165,23 +136,24 @@ def evaluate_eligibility(self) -> eligibility.EligibilityStatus:
165136
status=active_iteration_result.status,
166137
cohort_results=active_iteration_result.cohort_results,
167138
)
168-
for condition_name, active_iteration_result in results.items()
139+
for condition_name, active_iteration_result in condition_results.items()
169140
]
170141
return eligibility.EligibilityStatus(conditions=final_result)
171142

172-
def evaluate_filter_rules(
143+
def is_eligible_by_filter_rules(
173144
self,
174145
cohort: IterationCohort,
175146
cohort_results: dict[str, CohortResult],
176-
rules_filter: Iterable[rules.IterationRule],
177-
*,
178-
is_eligible: bool,
147+
filter_rules: Iterable[rules.IterationRule],
179148
) -> bool:
149+
is_eligible = True
180150
priority_getter = attrgetter("priority")
181-
sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, rules_filter), key=priority_getter)
151+
sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, filter_rules), key=priority_getter)
182152

183153
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
184-
status, group_actionable, group_exclusions, rule_stop = self.evaluate_rules_priority_group(rule_group)
154+
status, group_inclusion_reasons, group_exclusion_reasons, rule_stop = self.evaluate_rules_priority_group(
155+
rule_group
156+
)
185157
if status.is_exclusion:
186158
if cohort.cohort_label is not None:
187159
cohort_results[str(cohort.cohort_label)] = CohortResult(
@@ -197,27 +169,47 @@ def evaluate_filter_rules(
197169
def evaluate_suppression_rules(
198170
self,
199171
cohort: IterationCohort,
200-
rules_suppression: Iterable[rules.IterationRule],
201-
*,
202-
is_actionable: bool,
203-
) -> tuple[list, bool]:
172+
cohort_results: dict[str, CohortResult],
173+
suppression_rules: Iterable[rules.IterationRule],
174+
) -> None:
175+
is_actionable: bool = True
204176
priority_getter = attrgetter("priority")
205177
suppression_reasons = []
206-
sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, rules_suppression), key=priority_getter)
178+
179+
sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, suppression_rules), key=priority_getter)
180+
207181
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
208-
status, group_actionable, group_exclusions, rule_stop = self.evaluate_rules_priority_group(rule_group)
182+
status, group_inclusion_reasons, group_exclusion_reasons, rule_stop = self.evaluate_rules_priority_group(
183+
rule_group
184+
)
209185
if status.is_exclusion:
210186
is_actionable = False
211-
suppression_reasons.extend(group_exclusions)
187+
suppression_reasons.extend(group_exclusion_reasons)
212188
if rule_stop:
213189
break
214-
return suppression_reasons, is_actionable
190+
191+
if cohort.cohort_label is not None:
192+
key = cohort.cohort_label
193+
if is_actionable:
194+
cohort_results[key] = CohortResult(
195+
cohort.cohort_group if cohort.cohort_group else key,
196+
Status.actionable,
197+
[],
198+
str(cohort.positive_description),
199+
)
200+
else:
201+
cohort_results[key] = CohortResult(
202+
cohort.cohort_group if cohort.cohort_group else key,
203+
Status.not_actionable,
204+
suppression_reasons,
205+
str(cohort.positive_description),
206+
)
215207

216208
def evaluate_rules_priority_group(
217209
self, rules_group: Iterator[rules.IterationRule]
218210
) -> tuple[eligibility.Status, list[eligibility.Reason], list[eligibility.Reason], bool]:
219211
is_rule_stop = False
220-
actionable_reasons, exclusion_reasons = [], []
212+
inclusion_reasons, exclusion_reasons = [], []
221213
best_status = eligibility.Status.not_eligible
222214

223215
for rule in rules_group:
@@ -229,6 +221,6 @@ def evaluate_rules_priority_group(
229221
exclusion_reasons.append(reason)
230222
else:
231223
best_status = eligibility.Status.actionable
232-
actionable_reasons.append(reason)
224+
inclusion_reasons.append(reason)
233225

234-
return best_status, actionable_reasons, exclusion_reasons, is_rule_stop
226+
return best_status, inclusion_reasons, exclusion_reasons, is_rule_stop

tests/integration/conftest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,9 @@ def persisted_person(person_table: Any, faker: Faker) -> Generator[eligibility.N
218218
nhs_number = eligibility.NHSNumber(faker.nhs_number())
219219
date_of_birth = eligibility.DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=65))
220220

221-
for row in (rows := person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"])):
221+
for row in (
222+
rows := person_rows_builder(nhs_number, date_of_birth=date_of_birth, postcode="hp1", cohorts=["cohort1"])
223+
):
222224
person_table.put_item(Item=row)
223225

224226
yield nhs_number
@@ -232,7 +234,11 @@ def persisted_77yo_person(person_table: Any, faker: Faker) -> Generator[eligibil
232234
nhs_number = eligibility.NHSNumber(faker.nhs_number())
233235
date_of_birth = eligibility.DateOfBirth(faker.date_of_birth(minimum_age=77, maximum_age=77))
234236

235-
for row in (rows := person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1", "cohort2"])):
237+
for row in (
238+
rows := person_rows_builder(
239+
nhs_number, date_of_birth=date_of_birth, postcode="hp1", cohorts=["cohort1", "cohort2"]
240+
)
241+
):
236242
person_table.put_item(Item=row)
237243

238244
yield nhs_number

tests/unit/services/calculators/test_eligibility_calculator.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -811,15 +811,15 @@ def test_status_on_cohort_attribute_level(
811811

812812

813813
@pytest.mark.parametrize(
814-
("person_cohorts", "cohort_label", "expected_status", "test_comment"),
814+
("person_cohorts", "expected_status", "test_comment"),
815815
[
816-
(["cohort1", "cohort2"], "cohort1", Status.actionable, "cohort1 is not actionable, cohort 2 is actionable"),
817-
(["cohort2", "cohort3"], "cohort1", Status.actionable, "doesn't match the cohort label"),
818-
(["cohort1"], "cohort1", Status.not_actionable, "cohort1 is not actionable"),
816+
(["cohort1", "cohort2"], Status.actionable, "cohort1 is not actionable, cohort 2 is actionable"),
817+
(["cohort3", "cohort2"], Status.actionable, "cohort3 is not eligible, cohort 2 is actionable"),
818+
(["cohort1"], Status.not_actionable, "cohort1 is not actionable"),
819819
],
820820
)
821821
def test_status_if_iteration_rules_contains_cohort_label_field(
822-
person_cohorts, cohort_label: str, expected_status: Status, test_comment: str, faker: Faker
822+
person_cohorts, expected_status: Status, test_comment: str, faker: Faker
823823
):
824824
# Given
825825
nhs_number = NHSNumber(faker.nhs_number())
@@ -835,7 +835,7 @@ def test_status_if_iteration_rules_contains_cohort_label_field(
835835
rule_builder.IterationCohortFactory.build(cohort_label="cohort1"),
836836
rule_builder.IterationCohortFactory.build(cohort_label="cohort2"),
837837
],
838-
iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(cohort_label=cohort_label)],
838+
iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(cohort_label="cohort1")],
839839
)
840840
],
841841
)
@@ -883,8 +883,8 @@ def test_rules_stop_behavior(
883883
) -> None:
884884
# Given
885885
nhs_number = NHSNumber(faker.nhs_number())
886-
date_obj = datetime.datetime.strptime("19980309", "%Y%m%d").replace(tzinfo=datetime.UTC).date()
887-
person_rows = person_rows_builder(nhs_number, date_of_birth=(DateOfBirth(date_obj)), cohorts=["cohort1"])
886+
date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=66, maximum_age=74))
887+
person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"])
888888

889889
# Build campaign configuration
890890
campaign_config = rule_builder.CampaignConfigFactory.build(

tests/unit/views/test_eligibility.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ def test_build_eligibility_cohorts_results_consider_only_cohorts_with_best_statu
115115
condition: Condition = ConditionFactory.build(
116116
status=Status.not_actionable,
117117
cohort_results=[
118+
CohortResultFactory.build(
119+
cohort_code="cohort_group1",
120+
status=Status.not_actionable,
121+
),
118122
CohortResultFactory.build(
119123
cohort_code="cohort_group1",
120124
status=Status.not_actionable,
@@ -165,6 +169,17 @@ def test_build_suitability_results_with_deduplication():
165169
)
166170
],
167171
),
172+
CohortResultFactory.build(
173+
cohort_code="cohort_group3",
174+
status=Status.not_eligible,
175+
reasons=[
176+
Reason(
177+
rule_type=RuleType.filter,
178+
rule_name=RuleName("Exclude is present in sw1"),
179+
rule_result=RuleResult("memberof sw1"),
180+
)
181+
],
182+
),
168183
],
169184
)
170185

0 commit comments

Comments
 (0)