From 38bb41f7789d14f0f0f64a206cd3ef9cc7037531 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:52:28 +0100 Subject: [PATCH 1/2] Testing --- .../audit/audit_context.py | 1 + .../audit/audit_models.py | 1 + .../config/constants.py | 2 + .../model/eligibility_status.py | 1 + .../calculators/eligibility_calculator.py | 4 + .../processors/action_rule_handler.py | 29 +++++- tests/fixtures/builders/model/rule.py | 12 +++ .../test_eligibility_calculator.py | 95 ++++++++++++++++++- 8 files changed, 142 insertions(+), 3 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index f9a47f595..60ddcccb8 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -101,6 +101,7 @@ def append_audit_condition( condition_name=condition_name, status=best_candidate.status.name if best_candidate and best_candidate.status else None, status_text=best_candidate.status_text if best_candidate else None, + status_text_override=action_detail.status_text_override, # NEW eligibility_cohorts=audit_eligibility_cohorts, eligibility_cohort_groups=audit_eligibility_cohort_groups, filter_rules=audit_filter_rule, diff --git a/src/eligibility_signposting_api/audit/audit_models.py b/src/eligibility_signposting_api/audit/audit_models.py index a9d3f8896..6b89f14b9 100644 --- a/src/eligibility_signposting_api/audit/audit_models.py +++ b/src/eligibility_signposting_api/audit/audit_models.py @@ -77,6 +77,7 @@ class AuditCondition(CamelCaseBaseModel): condition_name: str | None = None status: str | None = None status_text: str | None = None + status_text_override: str | None = None # NEW — records override when applied eligibility_cohorts: list[AuditEligibilityCohorts] | None = None eligibility_cohort_groups: list[AuditEligibilityCohortGroups] | None = None filter_rules: list[AuditFilterRule] | None = None diff --git a/src/eligibility_signposting_api/config/constants.py b/src/eligibility_signposting_api/config/constants.py index 813a12b19..2b09a1e5a 100644 --- a/src/eligibility_signposting_api/config/constants.py +++ b/src/eligibility_signposting_api/config/constants.py @@ -9,3 +9,5 @@ CONSUMER_MAPPING_FILE_NAME = "consumer_mapping_config.json" CACHE_TTL_SECONDS = int(os.getenv("CONFIG_CACHE_TTL_SECONDS", "1800")) + +STATUS_TEXT_OVERRIDE_ACTION_TYPE = "norender_StatusTextOverride" diff --git a/src/eligibility_signposting_api/model/eligibility_status.py b/src/eligibility_signposting_api/model/eligibility_status.py index 89b73828b..f7096a08d 100644 --- a/src/eligibility_signposting_api/model/eligibility_status.py +++ b/src/eligibility_signposting_api/model/eligibility_status.py @@ -151,6 +151,7 @@ class MatchedActionDetail: rule_name: campaign_config.RuleName | None = None rule_priority: campaign_config.RulePriority | None = None actions: list[SuggestedAction] | None = None + status_text_override: StatusText | None = None @dataclass diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index 9f83bd916..d6b098075 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -102,6 +102,10 @@ def get_eligibility_status( include_actions_flag=include_actions_flag, ) + # Apply status text override if the matched actions contained one + if matched_action_detail.status_text_override: + iteration_result_summary.iteration_result.status_text = matched_action_detail.status_text_override + iteration_result_summary = TokenProcessor.find_and_replace_tokens(self.person, iteration_result_summary) matched_action_detail = TokenProcessor.find_and_replace_tokens(self.person, matched_action_detail) diff --git a/src/eligibility_signposting_api/services/processors/action_rule_handler.py b/src/eligibility_signposting_api/services/processors/action_rule_handler.py index 44fd4f9ae..afc5858ae 100644 --- a/src/eligibility_signposting_api/services/processors/action_rule_handler.py +++ b/src/eligibility_signposting_api/services/processors/action_rule_handler.py @@ -1,6 +1,7 @@ from itertools import groupby from operator import attrgetter +from eligibility_signposting_api.config.constants import STATUS_TEXT_OVERRIDE_ACTION_TYPE from eligibility_signposting_api.model.campaign_config import ( ActionsMapper, Iteration, @@ -16,7 +17,7 @@ RuleType, SuggestedAction, UrlLabel, - UrlLink, + UrlLink, StatusText, ) from eligibility_signposting_api.model.person import Person from eligibility_signposting_api.services.calculators.rule_calculator import RuleCalculator @@ -65,7 +66,31 @@ def _handle(self, person: Person, best_active_iteration: Iteration, rule_type: R matched_action_rule_name = rule_group_list[0].name break - return MatchedActionDetail(matched_action_rule_name, matched_action_rule_priority, actions) + #return MatchedActionDetail(matched_action_rule_name, matched_action_rule_priority, actions) + actions, status_text_override = self._extract_status_text_override(actions) + return MatchedActionDetail(matched_action_rule_name, matched_action_rule_priority, actions, status_text_override) + + @staticmethod + def _extract_status_text_override( + actions: list[SuggestedAction] | None, + ) -> tuple[list[SuggestedAction] | None, StatusText | None]: + """Extract and remove any status text override action from the actions list. + + Returns the filtered actions list and the override text (if found). + """ + if not actions: + return actions, None + + override_text: StatusText | None = None + filtered: list[SuggestedAction] = [] + + for action in actions: + if action.action_type == STATUS_TEXT_OVERRIDE_ACTION_TYPE: + override_text = StatusText(action.action_description) if action.action_description else None + else: + filtered.append(action) + + return filtered or None, override_text @staticmethod def _get_action_rules_components( diff --git a/tests/fixtures/builders/model/rule.py b/tests/fixtures/builders/model/rule.py index a7bdf2cbc..71f83304a 100644 --- a/tests/fixtures/builders/model/rule.py +++ b/tests/fixtures/builders/model/rule.py @@ -255,3 +255,15 @@ class ICBNonActionableActionRuleFactory(IterationRuleFactory): attribute_name = RuleAttributeName("ICB") comparator = RuleComparator("QE1") comms_routing = CommsRouting("ActionCode1") + +class ClinicalRiskRedirectRuleFactory(IterationRuleFactory): + type = RuleType.redirect + name = RuleName("Health reason redirect with override") + code = None + description = RuleDescription("Redirect for clinical risk individuals") + priority = RulePriority(10) + operator = RuleOperator.is_not_null + attribute_level = RuleAttributeLevel.PERSON + attribute_name = RuleAttributeName("CLINICAL_RISK_GROUP") + comparator = RuleComparator("") + comms_routing = CommsRouting("STATUS_TEXT_OVERRIDE") diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 2c9938557..95c57baa8 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -21,7 +21,7 @@ RuleComparator, RuleName, RuleOperator, - RuleType, + RuleType, CommsRouting, ) from eligibility_signposting_api.model.eligibility_status import ( ActionCode, @@ -2170,3 +2170,96 @@ def test_build_condition_results_single_cohort(self, reason_2, expected_reasons) assert_that(len(result.cohort_results), is_(1)) assert_that(result.cohort_results[0].reasons, contains_inanyorder(*expected_reasons)) + + +def test_configureable_status_text(faker: Faker): + # Given + nhs_number = NHSNumber(faker.nhs_number()) + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=85, maximum_age=85)) + + person_rows = person_rows_builder( + nhs_number, + date_of_birth=date_of_birth, + cohorts=["rsv_cohort_1"], + icb="QE1", + ) + + available_action = AvailableAction( + ExternalRoutingCode="StatusTextOverride", + ActionType="norender_StatusTextOverride", + ActionDescription="You maybe eligible for an RSV vaccine.", + ) + + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + #default_comms_routing="TOKEN_TEST", + #default_not_actionable_routing="TOKEN_TEST", + #default_not_eligible_routing="TOKEN_TEST", + iteration_cohorts=[ + rule_builder.IterationCohortFactory.build( + cohort_label="rsv_cohort_1", cohort_group="rsv_cohort_group", priority=0 + ), + ], + iteration_rules=[ + rule_builder.PersonAgeSuppressionRuleFactory.build( + type=RuleType.filter, + name=RuleName("NotEligible Reason 1"), + description=RuleText("NotEligible Description 1"), + priority=RulePriority("100"), + operator=RuleOperator.year_lte, + attribute_level=RuleAttributeLevel.PERSON, + attribute_name=RuleAttributeName("DATE_OF_BIRTH"), + comparator=RuleComparator("-90"), + ), + + # rule_builder.ClinicalRiskRedirectRuleFactory.build( + # comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE") + # ), + + rule_builder.ICBRedirectRuleFactory.build( + comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE") + ), + + + rule_builder.PersonAgeSuppressionRuleFactory.build( + type=RuleType.suppression, + name=RuleName("NotActionable Reason 1"), + description=RuleText("NotActionable Description 1"), + priority=RulePriority("110"), + operator=RuleOperator.year_lte, + attribute_level=RuleAttributeLevel.PERSON, + attribute_name=RuleAttributeName("DATE_OF_BIRTH"), + comparator=RuleComparator("-90"), + ), + ], + status_text=campaign_config.StatusText( + NotEligible="You are not eligible to take RSV vaccines.", + NotActionable="You have taken RSV vaccine in the last 90 days", + Actionable="You can take RSV vaccine.", + ), + actions_mapper=rule_builder.ActionsMapperFactory.build(root={"STATUS_TEXT_OVERRIDE": available_action}), + ) + ], + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.get_eligibility_status("Y", ["ALL"], "ALL") + + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(Status.actionable) + #.and_status_text(StatusText("You can take RSV vaccine.")) + .and_status_text(StatusText("You maybe eligible for an RSV vaccine.")) + ) + ), + ) From 86e14c6ef48d601e60eca03e21b53a1632dc5b72 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:17:13 +0100 Subject: [PATCH 2/2] Added few notes on tests. --- .../test_eligibility_calculator.py | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 95c57baa8..6bd74b5aa 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2184,10 +2184,10 @@ def test_configureable_status_text(faker: Faker): icb="QE1", ) - available_action = AvailableAction( + action_status_text_override_actionable = AvailableAction( ExternalRoutingCode="StatusTextOverride", ActionType="norender_StatusTextOverride", - ActionDescription="You maybe eligible for an RSV vaccine.", + ActionDescription="Status Text Override Actionable", ) campaign_configs = [ @@ -2198,6 +2198,13 @@ def test_configureable_status_text(faker: Faker): #default_comms_routing="TOKEN_TEST", #default_not_actionable_routing="TOKEN_TEST", #default_not_eligible_routing="TOKEN_TEST", + + status_text=campaign_config.StatusText( + NotEligible="Orignal you are not eligible status text", + NotActionable="Orignal you are not actionable status text", + Actionable="Orignal you are actionable status text", + ), + iteration_cohorts=[ rule_builder.IterationCohortFactory.build( cohort_label="rsv_cohort_1", cohort_group="rsv_cohort_group", priority=0 @@ -2205,42 +2212,41 @@ def test_configureable_status_text(faker: Faker): ], iteration_rules=[ rule_builder.PersonAgeSuppressionRuleFactory.build( - type=RuleType.filter, + type=RuleType.filter, # FILTER RULE !! name=RuleName("NotEligible Reason 1"), description=RuleText("NotEligible Description 1"), priority=RulePriority("100"), operator=RuleOperator.year_lte, attribute_level=RuleAttributeLevel.PERSON, attribute_name=RuleAttributeName("DATE_OF_BIRTH"), - comparator=RuleComparator("-90"), - ), - - # rule_builder.ClinicalRiskRedirectRuleFactory.build( - # comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE") - # ), - - rule_builder.ICBRedirectRuleFactory.build( - comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE") + #comparator=RuleComparator("-80"), # Not Base Eligible - Not Eligible + comparator=RuleComparator("-90"), # Base Eligible - ? ), - rule_builder.PersonAgeSuppressionRuleFactory.build( - type=RuleType.suppression, + type=RuleType.suppression, # SUPPRESSION RULE !! name=RuleName("NotActionable Reason 1"), description=RuleText("NotActionable Description 1"), priority=RulePriority("110"), operator=RuleOperator.year_lte, attribute_level=RuleAttributeLevel.PERSON, attribute_name=RuleAttributeName("DATE_OF_BIRTH"), - comparator=RuleComparator("-90"), + #comparator=RuleComparator("-80"), # Not Actionable + comparator=RuleComparator("-90"), # Actionable ), + + # rule_builder.ClinicalRiskRedirectRuleFactory.build( + # comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE") + # ), + + rule_builder.ICBRedirectRuleFactory.build( + comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE_ACTIONABLE") + ), + ], - status_text=campaign_config.StatusText( - NotEligible="You are not eligible to take RSV vaccines.", - NotActionable="You have taken RSV vaccine in the last 90 days", - Actionable="You can take RSV vaccine.", - ), - actions_mapper=rule_builder.ActionsMapperFactory.build(root={"STATUS_TEXT_OVERRIDE": available_action}), + + actions_mapper=rule_builder.ActionsMapperFactory.build( + root={"STATUS_TEXT_OVERRIDE_ACTIONABLE": action_status_text_override_actionable}), ) ], ) @@ -2259,7 +2265,12 @@ def test_configureable_status_text(faker: Faker): .with_condition_name(ConditionName("RSV")) .and_status(Status.actionable) #.and_status_text(StatusText("You can take RSV vaccine.")) - .and_status_text(StatusText("You maybe eligible for an RSV vaccine.")) + + # Actionable and Status Text Override + .and_status_text(StatusText("Status Text Override Actionable")) + + # Actionable and NO Override (remove comms routing) + #.and_status_text(StatusText("Status Text Override Actionable")) ) ), )