From c6e471725747b867c6a797f4cdbd3348d733545b Mon Sep 17 00:00:00 2001 From: Shweta <216860557+shweta-nhs@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:59:27 +0100 Subject: [PATCH 1/5] Adds campaign_evaluator and tests --- .../services/campaign_evaluator.py | 39 ++++++ .../unit/services/test_campaign_evaluator.py | 118 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/eligibility_signposting_api/services/campaign_evaluator.py create mode 100644 tests/unit/services/test_campaign_evaluator.py diff --git a/src/eligibility_signposting_api/services/campaign_evaluator.py b/src/eligibility_signposting_api/services/campaign_evaluator.py new file mode 100644 index 000000000..45be94f57 --- /dev/null +++ b/src/eligibility_signposting_api/services/campaign_evaluator.py @@ -0,0 +1,39 @@ +from itertools import groupby +from operator import attrgetter +from typing import Collection, Iterator + +from wireup import service + +from eligibility_signposting_api.model import rules, eligibility_status + + +@service +class CampaignEvaluator: + """Filters and groups campaign configurations.""" + + def get_active_campaigns(self, campaign_configs: Collection[rules.CampaignConfig]) -> list[rules.CampaignConfig]: + return [cc for cc in campaign_configs if cc.campaign_live] + + def campaigns_grouped_by_condition_name( + self, campaign_configs: Collection[rules.CampaignConfig], conditions: list[str], category: str + ) -> Iterator[tuple[eligibility_status.ConditionName, list[rules.CampaignConfig]]]: + mapping = { + "ALL": {"V", "S"}, + "VACCINATIONS": {"V"}, + "SCREENING": {"S"}, + } + + allowed_types = mapping.get(category, set()) + + filter_all_conditions = "ALL" in conditions + + active_campaigns = self.get_active_campaigns(campaign_configs) + + for condition_name, campaign_group in groupby( + sorted(active_campaigns, key=attrgetter("target")), + key=attrgetter("target"), + ): + campaigns = list(campaign_group) + if campaigns and campaigns[0].type in allowed_types and ( + filter_all_conditions or str(condition_name) in conditions): + yield condition_name, campaigns diff --git a/tests/unit/services/test_campaign_evaluator.py b/tests/unit/services/test_campaign_evaluator.py new file mode 100644 index 000000000..9ad800e68 --- /dev/null +++ b/tests/unit/services/test_campaign_evaluator.py @@ -0,0 +1,118 @@ +import datetime + +import pytest +from hamcrest import assert_that, is_ + +from eligibility_signposting_api.model.rules import CampaignID +from eligibility_signposting_api.services.campaign_evaluator import CampaignEvaluator +from tests.fixtures.builders.model import rule + + +@pytest.fixture +def campaign_evaluator(): + return CampaignEvaluator() + + +@pytest.mark.parametrize( + ("campaign_target", "campaign_type", "conditions_filter", "category_filter", "expected_result"), + [ + ("RSV", "V", ["RSV"], "VACCINATIONS", [("RSV", "V")]), + ("RSV", "V", ["COVID"], "VACCINATIONS", []), + ("RSV", "S", ["RSV"], "ALL", [("RSV", "S")]), + ("RSV", "S", ["ALL"], "ALL", [("RSV", "S")]), + ("RSV", "S", ["RSV"], "VACCINATIONS", []), + ("RSV", "V", ["RSV"], "ALL", [("RSV", "V")]), + ("FLU", "V", ["COVID", "RSV"], "ALL", []), + ("FLU", "S", ["ALL"], "ALL", [("FLU", "S")]), + ("COVID", "V", ["UNKNOWN"], "VACCINATIONS", []), + ("FLU", "V", ["COVID", "FLU"], "VACCINATIONS", [("FLU", "V")]), + ], +) +def test_campaigns_grouped_by_condition_name_filters_correctly( + campaign_evaluator, campaign_target, campaign_type, conditions_filter, category_filter, expected_result +): + campaign = rule.CampaignConfigFactory.build(target=campaign_target, type=campaign_type) + + result = campaign_evaluator.campaigns_grouped_by_condition_name([campaign], conditions_filter, category_filter) + assert_that([(str(name), group[0].type) for name, group in result], is_(expected_result)) + + +def test_campaigns_grouped_by_condition_name_with_no_campaigns(campaign_evaluator): + + result = campaign_evaluator.campaigns_grouped_by_condition_name([], ["RSV"], "VACCINATIONS") + assert_that(list(result), is_([])) + + +def test_campaigns_grouped_by_condition_name_with_no_active_campaigns(campaign_evaluator): + campaign = rule.CampaignConfigFactory.build(target="RSV", type="V", + start_date=datetime.date(2025, 4, 20), + end_date=datetime.date(2025, 4, 21)) + + result = campaign_evaluator.campaigns_grouped_by_condition_name([campaign], ["RSV"], "VACCINATIONS") + assert_that(list(result), is_([])) + + +@pytest.mark.parametrize( + ("category_filter", "campaign_type", "expected_count"), + [ + ("SCREENING", "S", 1), + ("SCREENING", "V", 0), + ("INVALID_CATEGORY", "S", 0), + ], +) +def test_campaigns_grouped_by_condition_name_with_various_categories( + campaign_evaluator, category_filter, campaign_type, expected_count +): + campaign = rule.CampaignConfigFactory.build(target="COVID", type=campaign_type) + result = list(campaign_evaluator.campaigns_grouped_by_condition_name([campaign], ["COVID"], category_filter)) + assert_that(len(result), is_(expected_count)) + if expected_count > 0: + assert_that(str(result[0][0]), is_("COVID")) + + +def test_campaigns_grouped_by_condition_name_with_empty_conditions_filter(campaign_evaluator): + campaign = rule.CampaignConfigFactory.build(target="RSV", type="V") + result = campaign_evaluator.campaigns_grouped_by_condition_name([campaign], [], "VACCINATIONS") + assert_that(list(result), is_([])) + + +def test_campaigns_grouped_by_condition_name_groups_multiple_campaigns_for_same_target(campaign_evaluator): + campaign1 = rule.CampaignConfigFactory.build(target="COVID", type="V", id="C1") + campaign2 = rule.CampaignConfigFactory.build(target="COVID", type="V", id="C2") + campaign3 = rule.CampaignConfigFactory.build(target="FLU", type="V", id="F1") + inactive_campaign = rule.CampaignConfigFactory.build(target="COVID", type="V", id="C3", + start_date=datetime.date(2025, 4, 20), + end_date=datetime.date(2025, 4, 21)) + + all_campaigns = [campaign1, campaign2, campaign3, inactive_campaign] + result = list(campaign_evaluator.campaigns_grouped_by_condition_name(all_campaigns, ["COVID", "FLU"], "VACCINATIONS")) + + assert_that(len(result), is_(2)) + + result_dict = {str(name): campaigns for name, campaigns in result} + assert_that("COVID" in result_dict) + assert_that("FLU" in result_dict) + + assert_that(len(result_dict["COVID"]), is_(2)) + assert_that({c.id for c in result_dict["COVID"]}, is_({CampaignID("C1"), CampaignID("C2")})) + + assert_that(len(result_dict["FLU"]), is_(1)) + assert_that(result_dict["FLU"][0].id, is_(CampaignID("F1"))) + + +def test_campaign_grouping_is_affected_by_order_for_mixed_types(campaign_evaluator): + campaign_v = rule.CampaignConfigFactory.build(target="RSV", type="V") + campaign_s = rule.CampaignConfigFactory.build(target="RSV", type="S") + + + + + evaluator_s_first = campaign_evaluator + result_s_first = list(evaluator_s_first.campaigns_grouped_by_condition_name([campaign_s, campaign_v], ["RSV"], "VACCINATIONS")) + assert_that(result_s_first, is_([])) + + + evaluator_v_first = campaign_evaluator + result_v_first = list(evaluator_v_first.campaigns_grouped_by_condition_name([campaign_v, campaign_s], ["RSV"], "VACCINATIONS")) + assert_that(len(result_v_first), is_(1)) + assert_that(len(result_v_first[0][1]), is_(2)) # Both V and S campaigns are in the group From 0405260843977034f79aaca15fe10fbfd5dc7219 Mon Sep 17 00:00:00 2001 From: Shweta <216860557+shweta-nhs@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:10:36 +0100 Subject: [PATCH 2/5] Adds person_data_reader and tests --- .../services/person_data_reader.py | 21 +++++ .../unit/services/test_person_data_reader.py | 85 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/eligibility_signposting_api/services/person_data_reader.py create mode 100644 tests/unit/services/test_person_data_reader.py diff --git a/src/eligibility_signposting_api/services/person_data_reader.py b/src/eligibility_signposting_api/services/person_data_reader.py new file mode 100644 index 000000000..e3ee38f63 --- /dev/null +++ b/src/eligibility_signposting_api/services/person_data_reader.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from collections.abc import Collection, Mapping +from typing import Any + +from wireup import service + +Row = Collection[Mapping[str, Any]] + + +@service +class PersonDataReader: + """Handles extracting and interpreting person data.""" + + def get_person_cohorts(self, person_data: Row) -> set[str]: + cohorts_row: Mapping[str, dict[str, dict[str, dict[str, Any]]]] = next( + (row for row in person_data if row.get("ATTRIBUTE_TYPE") == "COHORTS"), + {}, + ) + return set(cohorts_row.get("COHORT_MAP", {}).get("cohorts", {}).get("M", {}).keys()) + diff --git a/tests/unit/services/test_person_data_reader.py b/tests/unit/services/test_person_data_reader.py new file mode 100644 index 000000000..9abaaaadf --- /dev/null +++ b/tests/unit/services/test_person_data_reader.py @@ -0,0 +1,85 @@ +import pytest +from hamcrest import assert_that, is_ + +from eligibility_signposting_api.services.person_data_reader import PersonDataReader + + +@pytest.fixture +def person_data_reader(): + return PersonDataReader() + + +def test_get_person_cohorts_empty_data(person_data_reader): + result = person_data_reader.get_person_cohorts([]) + assert_that(result, is_(set())) + + +def test_get_person_cohorts_no_cohorts_attribute_type(person_data_reader): + no_cohorts_type = [ + {"ATTRIBUTE_TYPE": "NAME", "VALUE": "John Doe"}, + {"ATTRIBUTE_TYPE": "AGE", "VALUE": 30}, + ] + result = person_data_reader.get_person_cohorts(no_cohorts_type) + assert_that(result, is_(set())) + + +def test_get_person_cohorts_no_cohort_map_key(person_data_reader): + no_cohorts_map = [ + {"ATTRIBUTE_TYPE": "COHORTS", "OTHER_FIELD": "value"}, + ] + result = person_data_reader.get_person_cohorts(no_cohorts_map) + assert_that(result, is_(set())) + + +def test_get_person_cohorts_no_cohorts_list_key(person_data_reader): + no_cohorts_list = [ + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"another_key": {}}}, + ] + result = person_data_reader.get_person_cohorts(no_cohorts_list) + assert_that(result, is_(set())) + + +def test_get_person_cohorts_no_m_key(person_data_reader): + no_m_key = [ + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"X": {}}}}, + ] + result = person_data_reader.get_person_cohorts(no_m_key) + assert_that(result, is_(set())) + + +def test_get_person_cohorts_single_cohort(person_data_reader): + single_cohorts = [ + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"M": {"COHORT_A": {}}}}}, + {"ATTRIBUTE_TYPE": "NAME", "VALUE": "Jane Smith"}, + ] + result = person_data_reader.get_person_cohorts(single_cohorts) + assert_that(result, is_({"COHORT_A"})) + + +def test_get_person_cohorts_multiple_cohorts(person_data_reader): + multiple_cohorts = [ + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"M": {"COHORT_B": {}, "COHORT_C": {}}}}}, + {"ATTRIBUTE_TYPE": "AGE", "VALUE": 45}, + ] + result = person_data_reader.get_person_cohorts(multiple_cohorts) + assert_that(result, is_({"COHORT_B", "COHORT_C"})) + + +def test_get_person_cohorts_mixed_data(person_data_reader): + mixed_data = [ + {"ATTRIBUTE_TYPE": "NAME", "VALUE": "Alice"}, + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"M": {"COHORT_D": {}, "COHORT_E": {}}}}}, + {"ATTRIBUTE_TYPE": "ADDRESS", "VALUE": "123 Main St"}, + ] + result = person_data_reader.get_person_cohorts(mixed_data) + assert_that(result, is_({"COHORT_D", "COHORT_E"})) + + +def test_get_person_cohorts_with_other_attribute_types_present(person_data_reader): + data = [ + {"ATTRIBUTE_TYPE": "NAME", "VALUE": "Charlie"}, + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"M": {"COHORT_F": {}}}}}, + {"ATTRIBUTE_TYPE": "AGE", "VALUE": 25}, + ] + result = person_data_reader.get_person_cohorts(data) + assert_that(result, is_({"COHORT_F"})) From 113b9494beefdc9285026ba13fea701c25286325 Mon Sep 17 00:00:00 2001 From: Shweta <216860557+shweta-nhs@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:11:05 +0100 Subject: [PATCH 3/5] Injects person data reader and campaign processor into eligibility calculator --- .../calculators/eligibility_calculator.py | 46 +++++-------------- .../services/calculators/rule_calculator.py | 2 +- .../services/{rules => operators}/__init__.py | 0 .../{rules => operators}/operators.py | 0 .../services/processors/__init__.py | 0 .../{ => processors}/campaign_evaluator.py | 2 +- .../{ => processors}/person_data_reader.py | 0 tests/fixtures/builders/model/rule.py | 4 +- .../test_eligibility_calculator.py | 32 ------------- .../unit/services/operators/test_operators.py | 2 +- tests/unit/services/processors/__init__.py | 0 .../test_campaign_evaluator.py | 28 ++++++----- .../test_person_data_reader.py | 2 +- 13 files changed, 31 insertions(+), 87 deletions(-) rename src/eligibility_signposting_api/services/{rules => operators}/__init__.py (100%) rename src/eligibility_signposting_api/services/{rules => operators}/operators.py (100%) create mode 100644 src/eligibility_signposting_api/services/processors/__init__.py rename src/eligibility_signposting_api/services/{ => processors}/campaign_evaluator.py (96%) rename src/eligibility_signposting_api/services/{ => processors}/person_data_reader.py (100%) create mode 100644 tests/unit/services/processors/__init__.py rename tests/unit/services/{ => processors}/test_campaign_evaluator.py (79%) rename tests/unit/services/{ => processors}/test_person_data_reader.py (96%) diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index a2e4f6bd8..dce05878e 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -8,6 +8,8 @@ from typing import TYPE_CHECKING, Any from eligibility_signposting_api.audit.audit_context import AuditContext +from eligibility_signposting_api.services.processors.campaign_evaluator import CampaignEvaluator +from eligibility_signposting_api.services.processors.person_data_reader import PersonDataReader if TYPE_CHECKING: from eligibility_signposting_api.model.rules import ( @@ -58,42 +60,12 @@ class EligibilityCalculator: person_data: Row campaign_configs: Collection[rules.CampaignConfig] - results: list[eligibility_status.Condition] = field(default_factory=list) - - @property - def active_campaigns(self) -> list[rules.CampaignConfig]: - return [cc for cc in self.campaign_configs if cc.campaign_live] - - def campaigns_grouped_by_condition_name( - self, conditions: list[str], category: str - ) -> Iterator[tuple[eligibility_status.ConditionName, list[rules.CampaignConfig]]]: - """Generator that yields campaign groups filtered by condition names and campaign category.""" - - mapping = { - "ALL": {"V", "S"}, - "VACCINATIONS": {"V"}, - "SCREENING": {"S"}, - } - - allowed_types = mapping.get(category, set()) + campaign_evaluator: CampaignEvaluator = field(default_factory=CampaignEvaluator) + person_data_reader: PersonDataReader = field(default_factory=PersonDataReader) - filter_all_conditions = "ALL" in conditions + results: list[eligibility_status.Condition] = field(default_factory=list) - for condition_name, campaign_group in groupby( - sorted(self.active_campaigns, key=attrgetter("target")), - key=attrgetter("target"), - ): - campaigns = list(campaign_group) - if campaigns[0].type in allowed_types and (filter_all_conditions or str(condition_name) in conditions): - yield condition_name, campaigns - @property - def person_cohorts(self) -> set[str]: - cohorts_row: Mapping[str, dict[str, dict[str, dict[str, Any]]]] = next( - (row for row in self.person_data if row.get("ATTRIBUTE_TYPE") == "COHORTS"), - {}, - ) - return set(cohorts_row.get("COHORT_MAP", {}).get("cohorts", {}).get("M", {}).keys()) @staticmethod def get_the_best_cohort_memberships( @@ -164,7 +136,10 @@ def evaluate_eligibility( actions: list[SuggestedAction] | None = [] action_rule_priority, action_rule_name = None, None - for condition_name, campaign_group in self.campaigns_grouped_by_condition_name(conditions, category): + requested_grouped_campaigns = self.campaign_evaluator.get_requested_grouped_campaigns(self.campaign_configs, + conditions, + category) + for condition_name, campaign_group in requested_grouped_campaigns: best_active_iteration: Iteration | None best_candidate: IterationResult best_campaign_id: CampaignID | None @@ -289,7 +264,8 @@ def get_cohort_results(self, active_iteration: rules.Iteration) -> dict[str, Coh filter_rules, suppression_rules = self.get_rules_by_type(active_iteration) for cohort in sorted(active_iteration.iteration_cohorts, key=attrgetter("priority")): # Base Eligibility - check - if cohort.cohort_label in self.person_cohorts or cohort.is_magic_cohort: + person_cohorts = self.person_data_reader.get_person_cohorts(self.person_data) + if cohort.cohort_label in person_cohorts or cohort.is_magic_cohort: # Eligibility - check if self.is_eligible_by_filter_rules(cohort, cohort_results, filter_rules): # Actionability - evaluation diff --git a/src/eligibility_signposting_api/services/calculators/rule_calculator.py b/src/eligibility_signposting_api/services/calculators/rule_calculator.py index 96b0dfd87..f2fab06db 100644 --- a/src/eligibility_signposting_api/services/calculators/rule_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/rule_calculator.py @@ -7,7 +7,7 @@ from hamcrest.core.string_description import StringDescription from eligibility_signposting_api.model import eligibility_status, rules -from eligibility_signposting_api.services.rules.operators import OperatorRegistry +from eligibility_signposting_api.services.operators.operators import OperatorRegistry Row = Collection[Mapping[str, Any]] diff --git a/src/eligibility_signposting_api/services/rules/__init__.py b/src/eligibility_signposting_api/services/operators/__init__.py similarity index 100% rename from src/eligibility_signposting_api/services/rules/__init__.py rename to src/eligibility_signposting_api/services/operators/__init__.py diff --git a/src/eligibility_signposting_api/services/rules/operators.py b/src/eligibility_signposting_api/services/operators/operators.py similarity index 100% rename from src/eligibility_signposting_api/services/rules/operators.py rename to src/eligibility_signposting_api/services/operators/operators.py diff --git a/src/eligibility_signposting_api/services/processors/__init__.py b/src/eligibility_signposting_api/services/processors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/eligibility_signposting_api/services/campaign_evaluator.py b/src/eligibility_signposting_api/services/processors/campaign_evaluator.py similarity index 96% rename from src/eligibility_signposting_api/services/campaign_evaluator.py rename to src/eligibility_signposting_api/services/processors/campaign_evaluator.py index 45be94f57..721a04447 100644 --- a/src/eligibility_signposting_api/services/campaign_evaluator.py +++ b/src/eligibility_signposting_api/services/processors/campaign_evaluator.py @@ -14,7 +14,7 @@ class CampaignEvaluator: def get_active_campaigns(self, campaign_configs: Collection[rules.CampaignConfig]) -> list[rules.CampaignConfig]: return [cc for cc in campaign_configs if cc.campaign_live] - def campaigns_grouped_by_condition_name( + def get_requested_grouped_campaigns( self, campaign_configs: Collection[rules.CampaignConfig], conditions: list[str], category: str ) -> Iterator[tuple[eligibility_status.ConditionName, list[rules.CampaignConfig]]]: mapping = { diff --git a/src/eligibility_signposting_api/services/person_data_reader.py b/src/eligibility_signposting_api/services/processors/person_data_reader.py similarity index 100% rename from src/eligibility_signposting_api/services/person_data_reader.py rename to src/eligibility_signposting_api/services/processors/person_data_reader.py diff --git a/tests/fixtures/builders/model/rule.py b/tests/fixtures/builders/model/rule.py index bbc9c340d..ac5e9ab31 100644 --- a/tests/fixtures/builders/model/rule.py +++ b/tests/fixtures/builders/model/rule.py @@ -5,7 +5,9 @@ from polyfactory import Use from polyfactory.factories.pydantic_factory import ModelFactory -from eligibility_signposting_api.model import rules +from eligibility_signposting_api.model import rules, eligibility_status +from eligibility_signposting_api.model.eligibility_status import Reason, RuleName, RuleType, RulePriority, \ + RuleDescription def past_date(days_behind: int = 365) -> date: diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 7b9c39bf5..d127444d6 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2383,38 +2383,6 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat ) -@pytest.mark.parametrize( - ("campaign_target", "campaign_type", "conditions_filter", "category_filter", "expected_result"), - [ - # Multiple matching campaigns under the same condition - ("RSV", "V", ["RSV"], "VACCINATIONS", [("RSV", "V")]), - ("RSV", "V", ["COVID"], "VACCINATIONS", []), - ("RSV", "S", ["RSV"], "ALL", [("RSV", "S")]), - ("RSV", "S", ["ALL"], "ALL", [("RSV", "S")]), - ("RSV", "S", ["RSV"], "VACCINATIONS", []), - # Multiple campaigns with different types under the same condition name - ("RSV", "V", ["RSV"], "ALL", [("RSV", "V")]), - # Campaign is live but condition not in filter (no yield) - ("FLU", "V", ["COVID", "RSV"], "ALL", []), - # Category is ALL and condition filter includes ALL (everything matches) - ("FLU", "S", ["ALL"], "ALL", [("FLU", "S")]), - # Condition filter is unknown (should not match anything) - ("COVID", "V", ["UNKNOWN"], "VACCINATIONS", []), - # Campaign with the target matching one of several condition filters - ("FLU", "V", ["COVID", "FLU"], "VACCINATIONS", [("FLU", "V")]), - ], -) -def test_campaigns_grouped_by_condition_name_filters_correctly( - campaign_target, campaign_type, conditions_filter, category_filter, expected_result -): - campaign = rule_builder.CampaignConfigFactory.build(target=campaign_target, type=campaign_type, campaign_live=True) - - calculator = EligibilityCalculator(person_data=[], campaign_configs=[campaign]) - result = list(calculator.campaigns_grouped_by_condition_name(conditions_filter, category_filter)) - - assert_that([(str(name), group[0].type) for name, group in result], is_(expected_result)) - - @pytest.mark.parametrize( ( "test_comment", diff --git a/tests/unit/services/operators/test_operators.py b/tests/unit/services/operators/test_operators.py index 1c2b2ba70..ffb5777c5 100644 --- a/tests/unit/services/operators/test_operators.py +++ b/tests/unit/services/operators/test_operators.py @@ -3,7 +3,7 @@ from hamcrest import assert_that, equal_to from eligibility_signposting_api.model.rules import RuleOperator -from eligibility_signposting_api.services.rules.operators import Operator, OperatorRegistry +from eligibility_signposting_api.services.operators.operators import Operator, OperatorRegistry # Test cases: person_data, rule_operator, rule_value, expected, test_comment cases: list[tuple[str | None, RuleOperator, str | None, bool, str]] = [] diff --git a/tests/unit/services/processors/__init__.py b/tests/unit/services/processors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/services/test_campaign_evaluator.py b/tests/unit/services/processors/test_campaign_evaluator.py similarity index 79% rename from tests/unit/services/test_campaign_evaluator.py rename to tests/unit/services/processors/test_campaign_evaluator.py index 9ad800e68..ec1b069bb 100644 --- a/tests/unit/services/test_campaign_evaluator.py +++ b/tests/unit/services/processors/test_campaign_evaluator.py @@ -4,7 +4,7 @@ from hamcrest import assert_that, is_ from eligibility_signposting_api.model.rules import CampaignID -from eligibility_signposting_api.services.campaign_evaluator import CampaignEvaluator +from eligibility_signposting_api.services.processors.campaign_evaluator import CampaignEvaluator from tests.fixtures.builders.model import rule @@ -33,13 +33,12 @@ def test_campaigns_grouped_by_condition_name_filters_correctly( ): campaign = rule.CampaignConfigFactory.build(target=campaign_target, type=campaign_type) - result = campaign_evaluator.campaigns_grouped_by_condition_name([campaign], conditions_filter, category_filter) + result = campaign_evaluator.get_requested_grouped_campaigns([campaign], conditions_filter, category_filter) assert_that([(str(name), group[0].type) for name, group in result], is_(expected_result)) def test_campaigns_grouped_by_condition_name_with_no_campaigns(campaign_evaluator): - - result = campaign_evaluator.campaigns_grouped_by_condition_name([], ["RSV"], "VACCINATIONS") + result = campaign_evaluator.get_requested_grouped_campaigns([], ["RSV"], "VACCINATIONS") assert_that(list(result), is_([])) @@ -48,7 +47,7 @@ def test_campaigns_grouped_by_condition_name_with_no_active_campaigns(campaign_e start_date=datetime.date(2025, 4, 20), end_date=datetime.date(2025, 4, 21)) - result = campaign_evaluator.campaigns_grouped_by_condition_name([campaign], ["RSV"], "VACCINATIONS") + result = campaign_evaluator.get_requested_grouped_campaigns([campaign], ["RSV"], "VACCINATIONS") assert_that(list(result), is_([])) @@ -64,7 +63,7 @@ def test_campaigns_grouped_by_condition_name_with_various_categories( campaign_evaluator, category_filter, campaign_type, expected_count ): campaign = rule.CampaignConfigFactory.build(target="COVID", type=campaign_type) - result = list(campaign_evaluator.campaigns_grouped_by_condition_name([campaign], ["COVID"], category_filter)) + result = list(campaign_evaluator.get_requested_grouped_campaigns([campaign], ["COVID"], category_filter)) assert_that(len(result), is_(expected_count)) if expected_count > 0: assert_that(str(result[0][0]), is_("COVID")) @@ -72,7 +71,7 @@ def test_campaigns_grouped_by_condition_name_with_various_categories( def test_campaigns_grouped_by_condition_name_with_empty_conditions_filter(campaign_evaluator): campaign = rule.CampaignConfigFactory.build(target="RSV", type="V") - result = campaign_evaluator.campaigns_grouped_by_condition_name([campaign], [], "VACCINATIONS") + result = campaign_evaluator.get_requested_grouped_campaigns([campaign], [], "VACCINATIONS") assert_that(list(result), is_([])) @@ -85,7 +84,8 @@ def test_campaigns_grouped_by_condition_name_groups_multiple_campaigns_for_same_ end_date=datetime.date(2025, 4, 21)) all_campaigns = [campaign1, campaign2, campaign3, inactive_campaign] - result = list(campaign_evaluator.campaigns_grouped_by_condition_name(all_campaigns, ["COVID", "FLU"], "VACCINATIONS")) + result = list( + campaign_evaluator.get_requested_grouped_campaigns(all_campaigns, ["COVID", "FLU"], "VACCINATIONS")) assert_that(len(result), is_(2)) @@ -104,15 +104,13 @@ def test_campaign_grouping_is_affected_by_order_for_mixed_types(campaign_evaluat campaign_v = rule.CampaignConfigFactory.build(target="RSV", type="V") campaign_s = rule.CampaignConfigFactory.build(target="RSV", type="S") - - - evaluator_s_first = campaign_evaluator - result_s_first = list(evaluator_s_first.campaigns_grouped_by_condition_name([campaign_s, campaign_v], ["RSV"], "VACCINATIONS")) + result_s_first = list( + evaluator_s_first.get_requested_grouped_campaigns([campaign_s, campaign_v], ["RSV"], "VACCINATIONS")) assert_that(result_s_first, is_([])) - evaluator_v_first = campaign_evaluator - result_v_first = list(evaluator_v_first.campaigns_grouped_by_condition_name([campaign_v, campaign_s], ["RSV"], "VACCINATIONS")) + result_v_first = list( + evaluator_v_first.get_requested_grouped_campaigns([campaign_v, campaign_s], ["RSV"], "VACCINATIONS")) assert_that(len(result_v_first), is_(1)) - assert_that(len(result_v_first[0][1]), is_(2)) # Both V and S campaigns are in the group + assert_that(len(result_v_first[0][1]), is_(2)) diff --git a/tests/unit/services/test_person_data_reader.py b/tests/unit/services/processors/test_person_data_reader.py similarity index 96% rename from tests/unit/services/test_person_data_reader.py rename to tests/unit/services/processors/test_person_data_reader.py index 9abaaaadf..d4866684b 100644 --- a/tests/unit/services/test_person_data_reader.py +++ b/tests/unit/services/processors/test_person_data_reader.py @@ -1,7 +1,7 @@ import pytest from hamcrest import assert_that, is_ -from eligibility_signposting_api.services.person_data_reader import PersonDataReader +from eligibility_signposting_api.services.processors.person_data_reader import PersonDataReader @pytest.fixture From 6845a0fafec49ce6e62f20aa9d342f1984d97dc8 Mon Sep 17 00:00:00 2001 From: Shweta <216860557+shweta-nhs@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:25:27 +0100 Subject: [PATCH 4/5] ELI-342 Dynamo Cohort Schema Mismatch --- .../services/processors/person_data_reader.py | 8 +++- .../processors/test_person_data_reader.py | 43 ++++++++----------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/eligibility_signposting_api/services/processors/person_data_reader.py b/src/eligibility_signposting_api/services/processors/person_data_reader.py index e3ee38f63..b903e09d0 100644 --- a/src/eligibility_signposting_api/services/processors/person_data_reader.py +++ b/src/eligibility_signposting_api/services/processors/person_data_reader.py @@ -17,5 +17,11 @@ def get_person_cohorts(self, person_data: Row) -> set[str]: (row for row in person_data if row.get("ATTRIBUTE_TYPE") == "COHORTS"), {}, ) - return set(cohorts_row.get("COHORT_MAP", {}).get("cohorts", {}).get("M", {}).keys()) + person_cohorts = set() + + for membership in cohorts_row.get("COHORT_MEMBERSHIPS", []): + if membership.get("COHORT_LABEL"): + person_cohorts.add(membership.get("COHORT_LABEL")) + + return person_cohorts diff --git a/tests/unit/services/processors/test_person_data_reader.py b/tests/unit/services/processors/test_person_data_reader.py index d4866684b..88702faf6 100644 --- a/tests/unit/services/processors/test_person_data_reader.py +++ b/tests/unit/services/processors/test_person_data_reader.py @@ -31,35 +31,23 @@ def test_get_person_cohorts_no_cohort_map_key(person_data_reader): assert_that(result, is_(set())) -def test_get_person_cohorts_no_cohorts_list_key(person_data_reader): - no_cohorts_list = [ - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"another_key": {}}}, - ] - result = person_data_reader.get_person_cohorts(no_cohorts_list) - assert_that(result, is_(set())) - - -def test_get_person_cohorts_no_m_key(person_data_reader): - no_m_key = [ - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"X": {}}}}, - ] - result = person_data_reader.get_person_cohorts(no_m_key) - assert_that(result, is_(set())) - - def test_get_person_cohorts_single_cohort(person_data_reader): single_cohorts = [ - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"M": {"COHORT_A": {}}}}}, + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": [ + {"COHORT_LABEL": "flu_65+_autumnwinter2023", "DATE_JOINED": "20231020"}]}, {"ATTRIBUTE_TYPE": "NAME", "VALUE": "Jane Smith"}, ] result = person_data_reader.get_person_cohorts(single_cohorts) - assert_that(result, is_({"COHORT_A"})) + assert_that(result, is_({"flu_65+_autumnwinter2023"})) def test_get_person_cohorts_multiple_cohorts(person_data_reader): multiple_cohorts = [ - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"M": {"COHORT_B": {}, "COHORT_C": {}}}}}, - {"ATTRIBUTE_TYPE": "AGE", "VALUE": 45}, + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": [ + {"COHORT_LABEL": "COHORT_B", "DATE_JOINED": "20231020"}, + {"COHORT_LABEL": "COHORT_C", "DATE_JOINED": "20241020"} + ]}, + {"ATTRIBUTE_TYPE": "AGE", "VALUE": 45} ] result = person_data_reader.get_person_cohorts(multiple_cohorts) assert_that(result, is_({"COHORT_B", "COHORT_C"})) @@ -67,19 +55,26 @@ def test_get_person_cohorts_multiple_cohorts(person_data_reader): def test_get_person_cohorts_mixed_data(person_data_reader): mixed_data = [ + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": [ + {"COHORT_LABEL": "COHORT_D", "DATE_JOINED": "20231020"}, + {"COHORT_LABEL": "COHORT_E", "DATE_JOINED": "20241020"} + ]}, {"ATTRIBUTE_TYPE": "NAME", "VALUE": "Alice"}, - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"M": {"COHORT_D": {}, "COHORT_E": {}}}}}, - {"ATTRIBUTE_TYPE": "ADDRESS", "VALUE": "123 Main St"}, + {"ATTRIBUTE_TYPE": "ADDRESS", "VALUE": "123 Main St"} ] + result = person_data_reader.get_person_cohorts(mixed_data) assert_that(result, is_({"COHORT_D", "COHORT_E"})) def test_get_person_cohorts_with_other_attribute_types_present(person_data_reader): data = [ + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": [ + {"COHORT_LABEL": "COHORT_F", "DATE_JOINED": "20231020"} + ]}, {"ATTRIBUTE_TYPE": "NAME", "VALUE": "Charlie"}, - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MAP": {"cohorts": {"M": {"COHORT_F": {}}}}}, - {"ATTRIBUTE_TYPE": "AGE", "VALUE": 25}, + {"ATTRIBUTE_TYPE": "AGE", "VALUE": 25} ] + result = person_data_reader.get_person_cohorts(data) assert_that(result, is_({"COHORT_F"})) From af7f9b08b3317cea326fbf09e40a43ef2311366f Mon Sep 17 00:00:00 2001 From: Shweta <216860557+shweta-nhs@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:14:45 +0100 Subject: [PATCH 5/5] ELI-342: Fixes usage of person cohorts method and tests --- .../calculators/eligibility_calculator.py | 8 ++-- .../services/calculators/rule_calculator.py | 20 +++------- .../services/processors/campaign_evaluator.py | 11 ++++-- .../services/processors/person_data_reader.py | 3 +- tests/fixtures/builders/model/rule.py | 4 +- tests/fixtures/builders/repos/person.py | 10 ++--- .../test_eligibility_calculator.py | 15 +++----- .../processors/test_campaign_evaluator.py | 23 +++++------ .../processors/test_person_data_reader.py | 38 +++++++++++-------- 9 files changed, 60 insertions(+), 72 deletions(-) diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index dce05878e..e73bd39dc 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -65,8 +65,6 @@ class EligibilityCalculator: results: list[eligibility_status.Condition] = field(default_factory=list) - - @staticmethod def get_the_best_cohort_memberships( cohort_results: dict[str, CohortGroupResult], @@ -136,9 +134,9 @@ def evaluate_eligibility( actions: list[SuggestedAction] | None = [] action_rule_priority, action_rule_name = None, None - requested_grouped_campaigns = self.campaign_evaluator.get_requested_grouped_campaigns(self.campaign_configs, - conditions, - category) + requested_grouped_campaigns = self.campaign_evaluator.get_requested_grouped_campaigns( + self.campaign_configs, conditions, category + ) for condition_name, campaign_group in requested_grouped_campaigns: best_active_iteration: Iteration | None best_candidate: IterationResult diff --git a/src/eligibility_signposting_api/services/calculators/rule_calculator.py b/src/eligibility_signposting_api/services/calculators/rule_calculator.py index f2fab06db..4791aead6 100644 --- a/src/eligibility_signposting_api/services/calculators/rule_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/rule_calculator.py @@ -1,13 +1,14 @@ from __future__ import annotations from collections.abc import Collection, Mapping -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any from hamcrest.core.string_description import StringDescription from eligibility_signposting_api.model import eligibility_status, rules from eligibility_signposting_api.services.operators.operators import OperatorRegistry +from eligibility_signposting_api.services.processors.person_data_reader import PersonDataReader Row = Collection[Mapping[str, Any]] @@ -17,6 +18,8 @@ class RuleCalculator: person_data: Row rule: rules.IterationRule + person_data_reader: PersonDataReader = field(default_factory=PersonDataReader) + def evaluate_exclusion(self) -> tuple[eligibility_status.Status, eligibility_status.Reason]: """Evaluate if a particular rule excludes this person. Return the result, and the reason for the result.""" attribute_value = self.get_attribute_value() @@ -43,15 +46,7 @@ def get_attribute_value(self) -> str | None: (r for r in self.person_data if r.get("ATTRIBUTE_TYPE", "") == "COHORTS"), None ) if cohorts: - attr_name = ( - "COHORT_MAP" - if not self.rule.attribute_name or self.rule.attribute_name == "COHORT_LABEL" - else self.rule.attribute_name - ) - cohort_map = self.get_value(cohorts, attr_name) - cohorts_dict = self.get_value(cohort_map, "cohorts") - m_dict = self.get_value(cohorts_dict, "M") - person_cohorts: set[str] = set(m_dict.keys()) + person_cohorts = self.person_data_reader.get_person_cohorts(self.person_data) attribute_value = ",".join(person_cohorts) else: attribute_value = None @@ -66,11 +61,6 @@ def get_attribute_value(self) -> str | None: raise NotImplementedError(msg) return attribute_value - @staticmethod - def get_value(dictionary: Mapping[str, Any] | None, key: str) -> dict: - v = dictionary.get(key, {}) if isinstance(dictionary, dict) else {} - return v if isinstance(v, dict) else {} - def evaluate_rule(self, attribute_value: str | None) -> tuple[eligibility_status.Status, str, bool]: """Evaluate a rule against a person data attribute. Return the result, and the reason for the result.""" matcher_class = OperatorRegistry.get(self.rule.operator) diff --git a/src/eligibility_signposting_api/services/processors/campaign_evaluator.py b/src/eligibility_signposting_api/services/processors/campaign_evaluator.py index 721a04447..286e43e7c 100644 --- a/src/eligibility_signposting_api/services/processors/campaign_evaluator.py +++ b/src/eligibility_signposting_api/services/processors/campaign_evaluator.py @@ -1,10 +1,10 @@ +from collections.abc import Collection, Iterator from itertools import groupby from operator import attrgetter -from typing import Collection, Iterator from wireup import service -from eligibility_signposting_api.model import rules, eligibility_status +from eligibility_signposting_api.model import eligibility_status, rules @service @@ -34,6 +34,9 @@ def get_requested_grouped_campaigns( key=attrgetter("target"), ): campaigns = list(campaign_group) - if campaigns and campaigns[0].type in allowed_types and ( - filter_all_conditions or str(condition_name) in conditions): + if ( + campaigns + and campaigns[0].type in allowed_types + and (filter_all_conditions or str(condition_name) in conditions) + ): yield condition_name, campaigns diff --git a/src/eligibility_signposting_api/services/processors/person_data_reader.py b/src/eligibility_signposting_api/services/processors/person_data_reader.py index b903e09d0..20a8f6dca 100644 --- a/src/eligibility_signposting_api/services/processors/person_data_reader.py +++ b/src/eligibility_signposting_api/services/processors/person_data_reader.py @@ -13,7 +13,7 @@ class PersonDataReader: """Handles extracting and interpreting person data.""" def get_person_cohorts(self, person_data: Row) -> set[str]: - cohorts_row: Mapping[str, dict[str, dict[str, dict[str, Any]]]] = next( + cohorts_row: Mapping[str, list[dict[str, str]]] = next( (row for row in person_data if row.get("ATTRIBUTE_TYPE") == "COHORTS"), {}, ) @@ -24,4 +24,3 @@ def get_person_cohorts(self, person_data: Row) -> set[str]: person_cohorts.add(membership.get("COHORT_LABEL")) return person_cohorts - diff --git a/tests/fixtures/builders/model/rule.py b/tests/fixtures/builders/model/rule.py index ac5e9ab31..bbc9c340d 100644 --- a/tests/fixtures/builders/model/rule.py +++ b/tests/fixtures/builders/model/rule.py @@ -5,9 +5,7 @@ from polyfactory import Use from polyfactory.factories.pydantic_factory import ModelFactory -from eligibility_signposting_api.model import rules, eligibility_status -from eligibility_signposting_api.model.eligibility_status import Reason, RuleName, RuleType, RulePriority, \ - RuleDescription +from eligibility_signposting_api.model import rules def past_date(days_behind: int = 365) -> date: diff --git a/tests/fixtures/builders/repos/person.py b/tests/fixtures/builders/repos/person.py index 6cc418e19..6b9772acb 100644 --- a/tests/fixtures/builders/repos/person.py +++ b/tests/fixtures/builders/repos/person.py @@ -66,13 +66,9 @@ def person_rows_builder( # noqa:PLR0913 { "NHS_NUMBER": key, "ATTRIBUTE_TYPE": "COHORTS", - "COHORT_MAP": { - "cohorts": { - "M": { - cohort: {"M": {"dateJoined": {"S": faker.past_date().strftime("%Y%m%d")}}} for cohort in cohorts - } - } - }, + "COHORT_MEMBERSHIPS": [ + {"COHORT_LABEL": cohort, "DATE_JOINED": faker.past_date().strftime("%Y%m%d")} for cohort in cohorts + ], }, ] rows.extend( diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index d127444d6..819feb0c5 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -5,7 +5,7 @@ from faker import Faker from flask import Flask, g from freezegun import freeze_time -from hamcrest import assert_that, contains_exactly, contains_inanyorder, equal_to, has_item, has_items, is_, is_in +from hamcrest import assert_that, contains_exactly, contains_inanyorder, equal_to, has_item, has_items, is_in from pydantic import HttpUrl, ValidationError from eligibility_signposting_api.audit.audit_models import AuditAction, AuditEvent @@ -95,7 +95,8 @@ def test_not_base_eligible(faker: Faker): target="RSV", iterations=[ rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort2")] + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort2")], + iteration_rules=[], ) ], ) @@ -175,6 +176,7 @@ def test_only_live_campaigns_considered(faker: Faker): iterations=[ rule_builder.IterationFactory.build( iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort2")], + iteration_rules=[], ) ], start_date=datetime.date(2025, 4, 20), @@ -218,7 +220,7 @@ def test_campaigns_with_applicable_iteration_types_in_campaign_level_considered( # Given nhs_number = NHSNumber(faker.nhs_number()) - person_rows = person_rows_builder(nhs_number) + person_rows = person_rows_builder(nhs_number, cohorts=[]) campaign_configs = [rule_builder.CampaignConfigFactory.build(target="RSV", iteration_type=iteration_type)] calculator = EligibilityCalculator(person_rows, campaign_configs) @@ -247,7 +249,7 @@ def test_campaigns_with_applicable_iteration_types_in_iteration_level_considered # Given nhs_number = NHSNumber(faker.nhs_number()) - person_rows = person_rows_builder(nhs_number) + person_rows = person_rows_builder(nhs_number, cohorts=[]) campaign_configs = [ rule_builder.CampaignConfigFactory.build( target="RSV", iterations=[rule_builder.IterationFactory.build(type=iteration_type)] @@ -935,11 +937,6 @@ def test_status_on_target_based_on_last_successful_date( Status.not_eligible, "cohort label is the default attribute name for the cohort attribute level", ), - ( - rules.RuleAttributeName("LOCATION"), - Status.actionable, - "attribute name that is not cohort label", - ), ], ) def test_status_on_cohort_attribute_level( diff --git a/tests/unit/services/processors/test_campaign_evaluator.py b/tests/unit/services/processors/test_campaign_evaluator.py index ec1b069bb..e968e3e9e 100644 --- a/tests/unit/services/processors/test_campaign_evaluator.py +++ b/tests/unit/services/processors/test_campaign_evaluator.py @@ -28,7 +28,7 @@ def campaign_evaluator(): ("FLU", "V", ["COVID", "FLU"], "VACCINATIONS", [("FLU", "V")]), ], ) -def test_campaigns_grouped_by_condition_name_filters_correctly( +def test_campaigns_grouped_by_condition_name_filters_correctly( # noqa: PLR0913 campaign_evaluator, campaign_target, campaign_type, conditions_filter, category_filter, expected_result ): campaign = rule.CampaignConfigFactory.build(target=campaign_target, type=campaign_type) @@ -43,9 +43,9 @@ def test_campaigns_grouped_by_condition_name_with_no_campaigns(campaign_evaluato def test_campaigns_grouped_by_condition_name_with_no_active_campaigns(campaign_evaluator): - campaign = rule.CampaignConfigFactory.build(target="RSV", type="V", - start_date=datetime.date(2025, 4, 20), - end_date=datetime.date(2025, 4, 21)) + campaign = rule.CampaignConfigFactory.build( + target="RSV", type="V", start_date=datetime.date(2025, 4, 20), end_date=datetime.date(2025, 4, 21) + ) result = campaign_evaluator.get_requested_grouped_campaigns([campaign], ["RSV"], "VACCINATIONS") assert_that(list(result), is_([])) @@ -79,13 +79,12 @@ def test_campaigns_grouped_by_condition_name_groups_multiple_campaigns_for_same_ campaign1 = rule.CampaignConfigFactory.build(target="COVID", type="V", id="C1") campaign2 = rule.CampaignConfigFactory.build(target="COVID", type="V", id="C2") campaign3 = rule.CampaignConfigFactory.build(target="FLU", type="V", id="F1") - inactive_campaign = rule.CampaignConfigFactory.build(target="COVID", type="V", id="C3", - start_date=datetime.date(2025, 4, 20), - end_date=datetime.date(2025, 4, 21)) + inactive_campaign = rule.CampaignConfigFactory.build( + target="COVID", type="V", id="C3", start_date=datetime.date(2025, 4, 20), end_date=datetime.date(2025, 4, 21) + ) all_campaigns = [campaign1, campaign2, campaign3, inactive_campaign] - result = list( - campaign_evaluator.get_requested_grouped_campaigns(all_campaigns, ["COVID", "FLU"], "VACCINATIONS")) + result = list(campaign_evaluator.get_requested_grouped_campaigns(all_campaigns, ["COVID", "FLU"], "VACCINATIONS")) assert_that(len(result), is_(2)) @@ -106,11 +105,13 @@ def test_campaign_grouping_is_affected_by_order_for_mixed_types(campaign_evaluat evaluator_s_first = campaign_evaluator result_s_first = list( - evaluator_s_first.get_requested_grouped_campaigns([campaign_s, campaign_v], ["RSV"], "VACCINATIONS")) + evaluator_s_first.get_requested_grouped_campaigns([campaign_s, campaign_v], ["RSV"], "VACCINATIONS") + ) assert_that(result_s_first, is_([])) evaluator_v_first = campaign_evaluator result_v_first = list( - evaluator_v_first.get_requested_grouped_campaigns([campaign_v, campaign_s], ["RSV"], "VACCINATIONS")) + evaluator_v_first.get_requested_grouped_campaigns([campaign_v, campaign_s], ["RSV"], "VACCINATIONS") + ) assert_that(len(result_v_first), is_(1)) assert_that(len(result_v_first[0][1]), is_(2)) diff --git a/tests/unit/services/processors/test_person_data_reader.py b/tests/unit/services/processors/test_person_data_reader.py index 88702faf6..2191c44b3 100644 --- a/tests/unit/services/processors/test_person_data_reader.py +++ b/tests/unit/services/processors/test_person_data_reader.py @@ -33,8 +33,10 @@ def test_get_person_cohorts_no_cohort_map_key(person_data_reader): def test_get_person_cohorts_single_cohort(person_data_reader): single_cohorts = [ - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": [ - {"COHORT_LABEL": "flu_65+_autumnwinter2023", "DATE_JOINED": "20231020"}]}, + { + "ATTRIBUTE_TYPE": "COHORTS", + "COHORT_MEMBERSHIPS": [{"COHORT_LABEL": "flu_65+_autumnwinter2023", "DATE_JOINED": "20231020"}], + }, {"ATTRIBUTE_TYPE": "NAME", "VALUE": "Jane Smith"}, ] result = person_data_reader.get_person_cohorts(single_cohorts) @@ -43,11 +45,14 @@ def test_get_person_cohorts_single_cohort(person_data_reader): def test_get_person_cohorts_multiple_cohorts(person_data_reader): multiple_cohorts = [ - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": [ - {"COHORT_LABEL": "COHORT_B", "DATE_JOINED": "20231020"}, - {"COHORT_LABEL": "COHORT_C", "DATE_JOINED": "20241020"} - ]}, - {"ATTRIBUTE_TYPE": "AGE", "VALUE": 45} + { + "ATTRIBUTE_TYPE": "COHORTS", + "COHORT_MEMBERSHIPS": [ + {"COHORT_LABEL": "COHORT_B", "DATE_JOINED": "20231020"}, + {"COHORT_LABEL": "COHORT_C", "DATE_JOINED": "20241020"}, + ], + }, + {"ATTRIBUTE_TYPE": "AGE", "VALUE": 45}, ] result = person_data_reader.get_person_cohorts(multiple_cohorts) assert_that(result, is_({"COHORT_B", "COHORT_C"})) @@ -55,12 +60,15 @@ def test_get_person_cohorts_multiple_cohorts(person_data_reader): def test_get_person_cohorts_mixed_data(person_data_reader): mixed_data = [ - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": [ - {"COHORT_LABEL": "COHORT_D", "DATE_JOINED": "20231020"}, - {"COHORT_LABEL": "COHORT_E", "DATE_JOINED": "20241020"} - ]}, + { + "ATTRIBUTE_TYPE": "COHORTS", + "COHORT_MEMBERSHIPS": [ + {"COHORT_LABEL": "COHORT_D", "DATE_JOINED": "20231020"}, + {"COHORT_LABEL": "COHORT_E", "DATE_JOINED": "20241020"}, + ], + }, {"ATTRIBUTE_TYPE": "NAME", "VALUE": "Alice"}, - {"ATTRIBUTE_TYPE": "ADDRESS", "VALUE": "123 Main St"} + {"ATTRIBUTE_TYPE": "ADDRESS", "VALUE": "123 Main St"}, ] result = person_data_reader.get_person_cohorts(mixed_data) @@ -69,11 +77,9 @@ def test_get_person_cohorts_mixed_data(person_data_reader): def test_get_person_cohorts_with_other_attribute_types_present(person_data_reader): data = [ - {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": [ - {"COHORT_LABEL": "COHORT_F", "DATE_JOINED": "20231020"} - ]}, + {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": [{"COHORT_LABEL": "COHORT_F", "DATE_JOINED": "20231020"}]}, {"ATTRIBUTE_TYPE": "NAME", "VALUE": "Charlie"}, - {"ATTRIBUTE_TYPE": "AGE", "VALUE": 25} + {"ATTRIBUTE_TYPE": "AGE", "VALUE": 25}, ] result = person_data_reader.get_person_cohorts(data)