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..6bd74b5aa 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,107 @@ 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", + ) + + action_status_text_override_actionable = AvailableAction( + ExternalRoutingCode="StatusTextOverride", + ActionType="norender_StatusTextOverride", + ActionDescription="Status Text Override Actionable", + ) + + 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", + + 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 + ), + ], + iteration_rules=[ + rule_builder.PersonAgeSuppressionRuleFactory.build( + 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("-80"), # Not Base Eligible - Not Eligible + comparator=RuleComparator("-90"), # Base Eligible - ? + ), + + rule_builder.PersonAgeSuppressionRuleFactory.build( + 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("-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") + ), + + ], + + actions_mapper=rule_builder.ActionsMapperFactory.build( + root={"STATUS_TEXT_OVERRIDE_ACTIONABLE": action_status_text_override_actionable}), + ) + ], + ) + ] + + 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.")) + + # 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")) + ) + ), + )