Skip to content

Commit ac6550c

Browse files
more tests
1 parent b5374f9 commit ac6550c

3 files changed

Lines changed: 253 additions & 9 deletions

File tree

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from _operator import attrgetter
4+
from collections import defaultdict
45
from collections.abc import Collection, Iterable, Iterator, Mapping
56
from dataclasses import dataclass, field
67
from itertools import groupby
@@ -130,16 +131,35 @@ def evaluate_eligibility(self) -> eligibility.EligibilityStatus:
130131
condition_results[condition_name] = best_candidate
131132

132133
# Consolidate all the results and return
133-
final_result = [
134-
Condition(
135-
condition_name=condition_name,
136-
status=active_iteration_result.status,
137-
cohort_results=active_iteration_result.cohort_results,
138-
)
139-
for condition_name, active_iteration_result in condition_results.items()
140-
]
134+
final_result = self.build_condition_results(condition_results)
141135
return eligibility.EligibilityStatus(conditions=final_result)
142136

137+
@staticmethod
138+
def build_condition_results(condition_results: dict[ConditionName, IterationResult]) -> list[Condition]:
139+
conditions: list[Condition] = []
140+
# iterate over conditions
141+
for condition_name, active_iteration_result in condition_results.items():
142+
grouped_cohort_results = defaultdict(list)
143+
# iterate over cohorts and group them by status and cohort_group
144+
for cohort_result in active_iteration_result.cohort_results:
145+
if active_iteration_result.status == cohort_result.status:
146+
grouped_cohort_results[cohort_result.cohort_code].append(cohort_result)
147+
148+
# deduplicate grouped cohort results by cohort_code
149+
deduplicated_cohort_results = {
150+
cohort_code: results[0] for cohort_code, results in grouped_cohort_results.items() if results
151+
}
152+
153+
# return condition with cohort results
154+
conditions.append(
155+
Condition(
156+
condition_name=condition_name,
157+
status=active_iteration_result.status,
158+
cohort_results=list(deduplicated_cohort_results.values()),
159+
)
160+
)
161+
return conditions
162+
143163
def is_eligible_by_filter_rules(
144164
self,
145165
cohort: IterationCohort,

tests/fixtures/builders/model/rule.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,29 @@ def fix_iteration_date_invariants(iterations: list[rules.Iteration], start_date:
7171
iteration.iteration_date = current
7272
previous = current
7373

74-
74+
# Iteration cohort factories
75+
class Rsv75RollingCohortFactory(IterationCohortFactory):
76+
cohort_label = rules.CohortLabel("rsv_75_rolling")
77+
cohort_group = rules.CohortGroup("rsv_age_range")
78+
positive_description = rules.Description("rsv_age_range positive description")
79+
negative_description = rules.Description("rsv_age_range negative description")
80+
priority = 1
81+
82+
class Rsv75to79CohortFactory(IterationCohortFactory):
83+
cohort_label = rules.CohortLabel("rsv_75to79_2024")
84+
cohort_group = rules.CohortGroup("rsv_age_range")
85+
positive_description = rules.Description("rsv_age_range positive description")
86+
negative_description = rules.Description("rsv_age_range negative description")
87+
priority = 1
88+
89+
class RsvPretendClinicalCohortFactory(IterationCohortFactory):
90+
cohort_label = rules.CohortLabel("rsv_pretend_clinical_cohort")
91+
cohort_group = rules.CohortGroup("rsv_clinical_cohort")
92+
positive_description = rules.Description("rsv_clinical_cohort positive description")
93+
negative_description = rules.Description("rsv_clinical_cohort negative description")
94+
priority = 1
95+
96+
# Iteration rule factories
7597
class PersonAgeSuppressionRuleFactory(IterationRuleFactory):
7698
type = rules.RuleType.suppression
7799
name = rules.RuleName("Exclude too young less than 75")
@@ -103,3 +125,15 @@ class ICBSuppressionRuleFactory(IterationRuleFactory):
103125
attribute_level = rules.RuleAttributeLevel.PERSON
104126
attribute_name = rules.RuleAttributeName("ICB")
105127
comparator = rules.RuleComparator("QE1")
128+
129+
130+
class ICBSuppressionRuleFactory(IterationRuleFactory):
131+
type = rules.RuleType.filter
132+
name = rules.RuleName("Not in QE1")
133+
description = rules.RuleDescription("Not in QE1")
134+
priority = rules.RulePriority(10)
135+
operator = rules.RuleOperator.ne
136+
attribute_level = rules.RuleAttributeLevel.PERSON
137+
attribute_name = rules.RuleAttributeName("ICB")
138+
comparator = rules.RuleComparator("QE1")
139+

tests/unit/services/calculators/test_eligibility_calculator.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
)
1919
from eligibility_signposting_api.services.calculators.eligibility_calculator import EligibilityCalculator
2020
from tests.fixtures.builders.model import rule as rule_builder
21+
from tests.fixtures.builders.model.rule import Rsv75RollingCohortFactory, Rsv75to79CohortFactory, \
22+
RsvPretendClinicalCohortFactory
2123
from tests.fixtures.builders.repos.person import person_rows_builder
2224
from tests.fixtures.matchers.eligibility import (
2325
is_cohort_result,
@@ -1020,3 +1022,191 @@ def test_eligibility_results_when_multiple_cohorts(
10201022
)
10211023
),
10221024
)
1025+
1026+
1027+
@pytest.mark.parametrize(
1028+
("person_cohorts", "expected_cohort_results", "test_comment"),
1029+
[
1030+
(
1031+
["rsv_75_rolling"],
1032+
["rsv_age_range"],
1033+
"rsv_75_rolling is actionable, others are not-eligible",
1034+
),
1035+
(
1036+
["rsv_75_rolling", "rsv_75to79_2024"],
1037+
["rsv_age_range"],
1038+
"rsv_75_rolling, rsv_75to79_2024 is actionable, rsv_pretend_clinical_cohort are not-eligible",
1039+
),
1040+
(
1041+
["rsv_75_rolling", "rsv_75to79_2024", "rsv_pretend_clinical_cohort"],
1042+
["rsv_age_range", "rsv_clinical_cohort"],
1043+
"all are actionable",
1044+
),
1045+
],
1046+
)
1047+
def test_grouped_description_for_multiple_clinical_risk_cohort_if_best_status_is_actionable(
1048+
person_cohorts: list[str],
1049+
expected_cohort_results: list[str],
1050+
test_comment: str,
1051+
faker: Faker,
1052+
):
1053+
# Given
1054+
nhs_number = NHSNumber(faker.nhs_number())
1055+
date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=66, maximum_age=74))
1056+
1057+
person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=person_cohorts, postcode="hp")
1058+
campaign_configs = [
1059+
rule_builder.CampaignConfigFactory.build(
1060+
target="RSV",
1061+
iterations=[
1062+
rule_builder.IterationFactory.build(
1063+
iteration_cohorts=[
1064+
rule_builder.Rsv75RollingCohortFactory.build(),
1065+
rule_builder.Rsv75to79CohortFactory.build(),
1066+
rule_builder.RsvPretendClinicalCohortFactory.build()
1067+
],
1068+
iteration_rules=[rule_builder.PostcodeSuppressionRuleFactory.build()],
1069+
)
1070+
],
1071+
)
1072+
]
1073+
1074+
calculator = EligibilityCalculator(person_rows, campaign_configs)
1075+
1076+
# When
1077+
actual = calculator.evaluate_eligibility()
1078+
1079+
# Then
1080+
assert_that(
1081+
actual,
1082+
is_eligibility_status().with_conditions(
1083+
has_items(
1084+
is_condition()
1085+
.with_condition_name(ConditionName("RSV"))
1086+
.and_status(Status.actionable)
1087+
.and_cohort_results(
1088+
contains_exactly(*[is_cohort_result().with_cohort_code(code) for code in expected_cohort_results])
1089+
)
1090+
)
1091+
),
1092+
test_comment,
1093+
)
1094+
1095+
1096+
@pytest.mark.parametrize(
1097+
("person_cohorts", "expected_cohort_results", "test_comment"),
1098+
[
1099+
(
1100+
["rsv_75_rolling"],
1101+
["rsv_age_range"],
1102+
"rsv_75_rolling is not-actionable, others are not-eligible",
1103+
),
1104+
(
1105+
["rsv_75_rolling", "rsv_75to79_2024"],
1106+
["rsv_age_range"],
1107+
"rsv_75_rolling, rsv_75to79_2024 is not-actionable, rsv_pretend_clinical_cohort are not-eligible",
1108+
),
1109+
(
1110+
["rsv_75_rolling", "rsv_75to79_2024", "rsv_pretend_clinical_cohort"],
1111+
["rsv_age_range", "rsv_clinical_cohort"],
1112+
"all are not-actionable",
1113+
),
1114+
],
1115+
)
1116+
def test_grouped_description_for_multiple_clinical_risk_cohort_if_best_status_is_not_actionable(
1117+
person_cohorts: list[str],
1118+
expected_cohort_results: list[str],
1119+
test_comment: str,
1120+
faker: Faker,
1121+
):
1122+
# Given
1123+
nhs_number = NHSNumber(faker.nhs_number())
1124+
date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=66, maximum_age=74))
1125+
1126+
person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=person_cohorts, postcode="SW19")
1127+
campaign_configs = [
1128+
rule_builder.CampaignConfigFactory.build(
1129+
target="RSV",
1130+
iterations=[
1131+
rule_builder.IterationFactory.build(
1132+
iteration_cohorts=[
1133+
rule_builder.Rsv75RollingCohortFactory.build(),
1134+
rule_builder.Rsv75to79CohortFactory.build(),
1135+
rule_builder.RsvPretendClinicalCohortFactory.build()
1136+
],
1137+
iteration_rules=[rule_builder.PostcodeSuppressionRuleFactory.build()],
1138+
)
1139+
],
1140+
)
1141+
]
1142+
1143+
calculator = EligibilityCalculator(person_rows, campaign_configs)
1144+
1145+
# When
1146+
actual = calculator.evaluate_eligibility()
1147+
1148+
# Then
1149+
assert_that(
1150+
actual,
1151+
is_eligibility_status().with_conditions(
1152+
has_items(
1153+
is_condition()
1154+
.with_condition_name(ConditionName("RSV"))
1155+
.and_status(Status.not_actionable)
1156+
.and_cohort_results(
1157+
contains_exactly(*[is_cohort_result().with_cohort_code(code) for code in expected_cohort_results])
1158+
)
1159+
)
1160+
),
1161+
test_comment,
1162+
)
1163+
1164+
1165+
def test_grouped_description_for_multiple_clinical_risk_cohort_if_best_status_is_not_eligible(
1166+
faker: Faker,
1167+
):
1168+
# Given
1169+
nhs_number = NHSNumber(faker.nhs_number())
1170+
date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=66, maximum_age=74))
1171+
1172+
person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=[])
1173+
campaign_configs = [
1174+
rule_builder.CampaignConfigFactory.build(
1175+
target="RSV",
1176+
iterations=[
1177+
rule_builder.IterationFactory.build(
1178+
iteration_cohorts=[
1179+
rule_builder.Rsv75RollingCohortFactory.build(),
1180+
rule_builder.Rsv75to79CohortFactory.build(),
1181+
rule_builder.RsvPretendClinicalCohortFactory.build()
1182+
],
1183+
iteration_rules=[rule_builder.PostcodeSuppressionRuleFactory.build()],
1184+
)
1185+
],
1186+
)
1187+
]
1188+
1189+
calculator = EligibilityCalculator(person_rows, campaign_configs)
1190+
1191+
# When
1192+
actual = calculator.evaluate_eligibility()
1193+
1194+
# Then
1195+
assert_that(
1196+
actual,
1197+
is_eligibility_status().with_conditions(
1198+
has_items(
1199+
is_condition()
1200+
.with_condition_name(ConditionName("RSV"))
1201+
.and_status(Status.not_eligible)
1202+
.and_cohort_results(
1203+
contains_exactly(
1204+
*[
1205+
is_cohort_result().with_cohort_code(code)
1206+
for code in ["rsv_age_range", "rsv_clinical_cohort"]
1207+
]
1208+
)
1209+
)
1210+
)
1211+
),
1212+
)

0 commit comments

Comments
 (0)