diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index f9a47f595..d7dde9127 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, 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..85e833908 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 = Field(default=None, exclude_if=lambda value: value is None) 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/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index 9f83bd916..f860184aa 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -102,6 +102,9 @@ def get_eligibility_status( include_actions_flag=include_actions_flag, ) + 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/tests/fixtures/builders/model/rule.py b/tests/fixtures/builders/model/rule.py index a7bdf2cbc..36e17dfff 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 PostcodeNonActionableRuleFactory(IterationRuleFactory): + type = RuleType.not_actionable_actions + name = RuleName("Excluded postcode In SW19") + code = None + description = RuleDescription("In SW19") + priority = RulePriority(20) + operator = RuleOperator.starts_with + attribute_level = RuleAttributeLevel.PERSON + attribute_name = RuleAttributeName("POSTCODE") + comparator = RuleComparator("SW19") + comms_routing = CommsRouting("ActionCode1") diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 2c9938557..b5d04ec1a 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,344 @@ 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)) + + +@pytest.mark.parametrize( + ("status", "status_text", "scenario_filter", "scenario_suppression", "action_r_rule","action_x_rule","action_y_rule", "action_status_text","iteration_status_text", "test_comment"), + [ + (Status.actionable, + "You should have the RSV vaccine", + 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("-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("-90"), # Actionable + ), + rule_builder.ICBRedirectRuleFactory.build(comms_routing=CommsRouting(None)), + rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + AvailableAction(ExternalRoutingCode="", ActionType="", ActionDescription=None, + ), + campaign_config.StatusText(NotEligible=None, + NotActionable=None, + Actionable=None, + ), + "actionable hardcoded"), + (Status.not_actionable, + "Original you are not actionable status text", + 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("-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 + ), + rule_builder.ICBRedirectRuleFactory.build(comms_routing=CommsRouting(None)), + rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=CommsRouting(None)), + rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=CommsRouting(None)), + AvailableAction(ExternalRoutingCode="StatusText", ActionType="StatusText", ActionDescription="Original you are not eligible status text",), + campaign_config.StatusText(NotEligible="Original you are not eligible status text", + NotActionable="Original you are not actionable status text", + Actionable="Original you are actionable status text", + ), + "not actionable original"), + (Status.actionable, + "Status Text Override Actionable", + 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("-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("-90"), # Actionable + ), + rule_builder.ICBRedirectRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + AvailableAction(ExternalRoutingCode="StatusTextOverride", ActionType="norender_StatusTextOverride", ActionDescription="Status Text Override Actionable", + ), + campaign_config.StatusText(NotEligible="Original you are not eligible status text", + NotActionable="Original you are not actionable status text", + Actionable="Original you are actionable status text", + ), + "actionable override"), + (Status.not_actionable, + "Original you are not actionable status text", + 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("-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 + ), + rule_builder.ICBRedirectRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=CommsRouting(None)), + rule_builder.PostcodeNonActionableRuleFactory.build(comms_routing=CommsRouting(None)), + AvailableAction(ExternalRoutingCode="StatusTextOverride", ActionType="norender_StatusTextOverride", ActionDescription="Status Text Override Actionable", + ), + campaign_config.StatusText(NotEligible="Original you are not eligible status text", + NotActionable="Original you are not actionable status text", + Actionable="Original you are actionable status text", + ), + "not actionable original"), + (Status.not_actionable, + "Status Text Override Not Actionable", + 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("-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 + ), + rule_builder.ICBRedirectRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + AvailableAction(ExternalRoutingCode="StatusTextOverride", ActionType="norender_StatusTextOverride", ActionDescription="Status Text Override Not Actionable",), + campaign_config.StatusText(NotEligible="Original you are not eligible status text", + NotActionable="Original you are not actionable status text", + Actionable="Original you are actionable status text", + ), + "not actionable override"), + (Status.not_eligible, + "Status Text Override Not Eligible", + 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"), # 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 + ), + rule_builder.ICBRedirectRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + AvailableAction(ExternalRoutingCode="StatusTextOverride", ActionType="norender_StatusTextOverride", ActionDescription="Status Text Override Not Eligible",), + campaign_config.StatusText(NotEligible="Original you are not eligible status text", + NotActionable="Original you are not actionable status text", + Actionable="Original you are actionable status text", + ), + "not eligible override"), + (Status.actionable, + "Status Text Override Actionable", + 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("-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("-90"), # Actionable + ), + rule_builder.ICBRedirectRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=CommsRouting(None)), + rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=CommsRouting(None)), + AvailableAction(ExternalRoutingCode="StatusTextOverride", ActionType="norender_StatusTextOverride", ActionDescription="Status Text Override Actionable",), + campaign_config.StatusText(NotEligible=None, + NotActionable=None, + Actionable="Original you are actionable status text", + ), + "eligible override"), + (Status.not_actionable, + "You should have the RSV vaccine", + 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("-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"), # Actionable + ), + rule_builder.ICBRedirectRuleFactory.build(comms_routing=CommsRouting("STATUS_TEXT_OVERRIDE")), + rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=CommsRouting(None)), + rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=CommsRouting(None)), + AvailableAction(ExternalRoutingCode="StatusTextOverride", ActionType="norender_StatusTextOverride", ActionDescription="Status Text Override Actionable",), + campaign_config.StatusText(NotEligible=None, + NotActionable=None, + Actionable="Original you are actionable status text", + ), + "not actionable hardcoded"), + + ], +) + +def test_configureable_status_text_actionable( + faker: Faker, + status: Status, + status_text: str, + scenario_filter: object, + scenario_suppression: object, + action_r_rule: object, + action_x_rule: object, + action_y_rule: object, + action_status_text: object, + iteration_status_text: object, + test_comment: str, +): + # 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", + postcode="SW19" + ) + + + action_rule = { + Status.actionable: action_r_rule, + Status.not_eligible: action_x_rule, + Status.not_actionable: action_y_rule, + }[status] + + scenario_rules = [ + scenario_filter, + scenario_suppression, + action_rule, + ] + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + status_text=iteration_status_text, + + iteration_cohorts=[ + rule_builder.IterationCohortFactory.build( + cohort_label="rsv_cohort_1", cohort_group="rsv_cohort_group", priority=0 + ), + ], + iteration_rules=scenario_rules, + + actions_mapper=rule_builder.ActionsMapperFactory.build( + root={"STATUS_TEXT_OVERRIDE": action_status_text}), + ) + ], + ) + ] + + 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) + #.and_status_text(StatusText("You can take RSV vaccine.")) + + # Actionable and Status Text Override + .and_status_text(StatusText(status_text)) + + # Actionable and NO Override (remove comms routing) + #.and_status_text(StatusText("Status Text Override Actionable")) + ) + ), + ) +