Skip to content

Commit ae2d338

Browse files
Eli 268 impl cohort attribute level (#102)
* Impl cohort rule in eligibility check | is_in and not_in operators modified * Deal with COHORT_LABEL in campaign config * Deal with COHORT_LABEL in campaign config * code clean up
1 parent 4694022 commit ae2d338

File tree

4 files changed

+80
-5
lines changed

4 files changed

+80
-5
lines changed

src/eligibility_signposting_api/services/calculators/rule_calculator.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ def get_attribute_value(self) -> str | None:
4040
(r for r in self.person_data if r.get("ATTRIBUTE_TYPE", "") == "PERSON"), None
4141
)
4242
attribute_value = person.get(self.rule.attribute_name) if person else None
43+
case rules.RuleAttributeLevel.COHORT:
44+
cohorts: Mapping[str, str | None] | None = next(
45+
(r for r in self.person_data if r.get("ATTRIBUTE_TYPE", "") == "COHORTS"), None
46+
)
47+
if self.rule.attribute_name == "COHORT_LABEL":
48+
cohort_map = self.get_value(cohorts, "COHORT_MAP")
49+
cohorts_dict = self.get_value(cohort_map, "cohorts")
50+
m_dict = self.get_value(cohorts_dict, "M")
51+
person_cohorts: set[str] = set(m_dict.keys())
52+
attribute_value = ",".join(person_cohorts)
53+
else:
54+
attribute_value = cohorts.get(self.rule.attribute_name) if cohorts else None
4355
case rules.RuleAttributeLevel.TARGET:
4456
target: Mapping[str, str | None] | None = next(
4557
(r for r in self.person_data if r.get("ATTRIBUTE_TYPE", "") == self.rule.attribute_target), None
@@ -50,6 +62,11 @@ def get_attribute_value(self) -> str | None:
5062
raise NotImplementedError(msg)
5163
return attribute_value
5264

65+
@staticmethod
66+
def get_value(dictionary: Mapping[str, Any] | None, key: str) -> dict:
67+
v = dictionary.get(key, {}) if isinstance(dictionary, dict) else {}
68+
return v if isinstance(v, dict) else {}
69+
5370
def evaluate_rule(self, attribute_value: str | None) -> tuple[eligibility.Status, str]:
5471
"""Evaluate a rule against a person data attribute. Return the result, and the reason for the result."""
5572
matcher_class = OperatorRegistry.get(self.rule.operator)

src/eligibility_signposting_api/services/rules/operators.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,17 +165,19 @@ def _matches(self, item: str | None) -> bool:
165165
class IsIn(Operator):
166166
def _matches(self, item: str | None) -> bool:
167167
item = item if item is not None else self.item_default
168-
comparators = str(self.rule_value).split(",")
169-
return str(item) in comparators
168+
comparators = set(str(self.rule_value).split(","))
169+
items = set(str(item).split(","))
170+
return bool(items & comparators)
170171

171172

172173
@OperatorRegistry.register(RuleOperator.not_in)
173174
@OperatorRegistry.register(RuleOperator.not_member_of)
174175
class NotIn(Operator):
175176
def _matches(self, item: str | None) -> bool:
176177
item = item if item is not None else self.item_default
177-
comparators = str(self.rule_value).split(",")
178-
return str(item) not in comparators
178+
comparators = set(str(self.rule_value).split(","))
179+
items = set(str(item).split(","))
180+
return not bool(items & comparators)
179181

180182

181183
@OperatorRegistry.register(RuleOperator.is_null)

tests/unit/services/calculators/test_eligibility_calculator.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ def test_base_eligible_and_icb_example(
649649
],
650650
)
651651
@freeze_time("2025-01-01")
652-
def test_not_actionable_status_on_target_when_last_successful_date_lte_today(
652+
def test_status_on_target_based_on_last_successful_date(
653653
vaccine: str, last_successful_date: str, expected_status: Status, test_comment: str, faker: Faker
654654
):
655655
# Given
@@ -718,3 +718,48 @@ def test_not_actionable_status_on_target_when_last_successful_date_lte_today(
718718
),
719719
test_comment,
720720
)
721+
722+
723+
def test_status_on_cohort_attribute_level(faker: Faker):
724+
# Given
725+
nhs_number = NHSNumber(faker.nhs_number())
726+
727+
person_row = person_rows_builder(nhs_number, cohorts=["cohort1", "covid_eligibility_complaint_list"])
728+
729+
campaign_configs = [
730+
rule_builder.CampaignConfigFactory.build(
731+
target="RSV",
732+
iterations=[
733+
rule_builder.IterationFactory.build(
734+
iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")],
735+
iteration_rules=[
736+
rule_builder.IterationRuleFactory.build(
737+
type=rules.RuleType.filter,
738+
name=rules.RuleName("Exclude those in a complaint cohort"),
739+
description=rules.RuleDescription(
740+
"Ensure anyone who has registered a complaint is not shown as eligible"
741+
),
742+
priority=15,
743+
operator=rules.RuleOperator.member_of,
744+
attribute_level=rules.RuleAttributeLevel.COHORT,
745+
attribute_name=rules.RuleAttributeName("COHORT_LABEL"),
746+
comparator=rules.RuleComparator("covid_eligibility_complaint_list"),
747+
)
748+
],
749+
)
750+
],
751+
)
752+
]
753+
754+
calculator = EligibilityCalculator(person_row, campaign_configs)
755+
756+
# When
757+
actual = calculator.evaluate_eligibility()
758+
759+
# Then
760+
assert_that(
761+
actual,
762+
is_eligibility_status().with_conditions(
763+
has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_eligible))
764+
),
765+
)

tests/unit/services/operators/test_operators.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,11 @@
374374
("PP77", RuleOperator.is_in, "QH8,QJG[[NVL:QH8]]", False, "Default value specified, but unused"),
375375
(None, RuleOperator.is_in, "QH8,QJG[[NVL:QH8]]", True, "Default value used"),
376376
(None, RuleOperator.is_in, "QH8,QJG[[NVL:PP77]]", False, "Default value used"),
377+
("QH8", RuleOperator.is_in, "QH8", True, ""),
378+
("QH8,QJG", RuleOperator.is_in, "QH8", True, ""),
379+
("QH8,QJG,QGX", RuleOperator.is_in, "QH8,QJG", True, ""),
380+
("QH8,QGX", RuleOperator.is_in, "QH8,QJG", True, ""),
381+
("QH8,QJG", RuleOperator.is_in, "QH8,QJG,QGX", True, ""),
377382
]
378383

379384
# is not_in
@@ -386,6 +391,12 @@
386391
("PP77", RuleOperator.not_in, "QH8,QJG[[NVL:QH8]]", True, "Default value specified, but unused"),
387392
(None, RuleOperator.not_in, "QH8,QJG[[NVL:QH8]]", False, "Default value used"),
388393
(None, RuleOperator.not_in, "QH8,QJG[[NVL:PP77]]", True, "Default value used"),
394+
("QH8", RuleOperator.not_in, "QH8", False, ""),
395+
("QH8,QJG", RuleOperator.not_in, "QH8", False, ""),
396+
("QH8,QJG,QGX", RuleOperator.not_in, "QH8,QJG", False, ""),
397+
("QH8,QGX", RuleOperator.not_in, "QH8,QJG", False, ""),
398+
("QH8,QJG", RuleOperator.not_in, "QH8,QJG,QGX", False, ""),
399+
("QH8,QJG", RuleOperator.not_in, "QHX", True, ""),
389400
]
390401

391402
# is member_of

0 commit comments

Comments
 (0)