From dc83df32c87970495a43cc0bd18ab8dc72ad578f Mon Sep 17 00:00:00 2001 From: Shweta <216860557+shweta-nhs@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:18:58 +0100 Subject: [PATCH 1/4] ELI-294: Personalised/customised status text --- .../audit/audit_context.py | 2 +- .../model/eligibility.py | 11 +++++++++ .../calculators/eligibility_calculator.py | 1 + .../views/eligibility.py | 2 +- .../in_process/test_eligibility_endpoint.py | 24 +++++++++---------- .../lambda/test_app_running_as_lambda.py | 8 +++---- tests/unit/audit/test_audit_context.py | 2 +- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index b52edbe9b..a2ccc2a62 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -102,7 +102,7 @@ def append_audit_condition( iteration_version=best_active_iteration.version if best_active_iteration else None, condition_name=condition_name, status=best_candidate.status.name if best_candidate and best_candidate.status else None, - status_text=best_candidate.status.name if best_candidate and best_candidate.status else None, + status_text=best_candidate.status.get_status_text(condition_name) if best_candidate else None, eligibility_cohorts=audit_eligibility_cohorts, eligibility_cohort_groups=audit_eligibility_cohort_groups, filter_rules=audit_filter_rule, diff --git a/src/eligibility_signposting_api/model/eligibility.py b/src/eligibility_signposting_api/model/eligibility.py index bad948361..15934880b 100644 --- a/src/eligibility_signposting_api/model/eligibility.py +++ b/src/eligibility_signposting_api/model/eligibility.py @@ -24,6 +24,8 @@ UrlLink = NewType("UrlLink", HttpUrl) UrlLabel = NewType("UrlLabel", str) +StatusText = NewType("StatusText", str) + class RuleType(StrEnum): filter = "F" @@ -65,6 +67,14 @@ def best(*statuses: Status) -> Status: """ return max(statuses) + def get_status_text(self, condition_name: ConditionName) -> StatusText: + status_to_text_mapping = { + self.not_eligible: lambda: StatusText("We do not believe you can have it"), + self.not_actionable: lambda: StatusText(f"You should have the {condition_name} vaccine"), + self.actionable: lambda: StatusText(f"You should have the {condition_name} vaccine"), + } + return status_to_text_mapping.get(self, lambda: StatusText("Unknown status provided"))() + @dataclass class Reason: @@ -90,6 +100,7 @@ class Condition: condition_name: ConditionName status: Status cohort_results: list[CohortGroupResult] + status_text: StatusText actions: list[SuggestedAction] | 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 a64a8b878..4764c745b 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -311,6 +311,7 @@ def build_condition_results(condition_results: dict[ConditionName, IterationResu status=active_iteration_result.status, cohort_results=list(deduplicated_cohort_results), actions=condition_results[condition_name].actions, + status_text=active_iteration_result.status.get_status_text(condition_name), ) ) return conditions diff --git a/src/eligibility_signposting_api/views/eligibility.py b/src/eligibility_signposting_api/views/eligibility.py index 0f507f65c..bc8f95a84 100644 --- a/src/eligibility_signposting_api/views/eligibility.py +++ b/src/eligibility_signposting_api/views/eligibility.py @@ -113,7 +113,7 @@ def build_eligibility_response(eligibility_status: EligibilityStatus) -> eligibi suggestions = ProcessedSuggestion( # pyright: ignore[reportCallIssue] condition=eligibility.ConditionName(condition.condition_name), # pyright: ignore[reportCallIssue] status=STATUS_MAPPING[condition.status], - statusText=eligibility.StatusText(f"{condition.status}"), # pyright: ignore[reportCallIssue] + statusText=eligibility.StatusText(condition.status_text), # pyright: ignore[reportCallIssue] eligibilityCohorts=build_eligibility_cohorts(condition), # pyright: ignore[reportCallIssue] suitabilityRules=build_suitability_results(condition), # pyright: ignore[reportCallIssue] actions=build_actions(condition), diff --git a/tests/integration/in_process/test_eligibility_endpoint.py b/tests/integration/in_process/test_eligibility_endpoint.py index 5356eed59..229d8652c 100644 --- a/tests/integration/in_process/test_eligibility_endpoint.py +++ b/tests/integration/in_process/test_eligibility_endpoint.py @@ -85,7 +85,7 @@ def test_not_base_eligible( ], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -128,7 +128,7 @@ def test_not_eligible_by_rule( ], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -177,7 +177,7 @@ def test_not_actionable( "ruleType": "S", } ], - "statusText": "Status.not_actionable", + "statusText": "You should have the RSV vaccine", } ] ), @@ -228,7 +228,7 @@ def test_actionable( } ], "suitabilityRules": [], - "statusText": "Status.actionable", + "statusText": "You should have the RSV vaccine", } ] ), @@ -273,7 +273,7 @@ def test_not_eligible_by_rule_when_only_magic_cohort_is_present( ], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -322,7 +322,7 @@ def test_not_actionable_when_only_magic_cohort_is_present( "ruleType": "S", } ], - "statusText": "Status.not_actionable", + "statusText": "You should have the COVID vaccine", } ] ), @@ -373,7 +373,7 @@ def test_actionable_when_only_magic_cohort_is_present( } ], "suitabilityRules": [], - "statusText": "Status.actionable", + "statusText": "You should have the COVID vaccine", } ] ), @@ -412,7 +412,7 @@ def test_not_base_eligible( "eligibilityCohorts": [], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -449,7 +449,7 @@ def test_not_eligible_by_rule( "eligibilityCohorts": [], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -492,7 +492,7 @@ def test_not_actionable( "ruleType": "S", } ], - "statusText": "Status.not_actionable", + "statusText": "You should have the FLU vaccine", } ] ), @@ -537,7 +537,7 @@ def test_actionable( } ], "suitabilityRules": [], - "statusText": "Status.actionable", + "statusText": "You should have the FLU vaccine", } ] ), @@ -573,7 +573,7 @@ def test_actionable_no_actions( "status": "Actionable", "eligibilityCohorts": [], "suitabilityRules": [], - "statusText": "Status.actionable", + "statusText": "You should have the FLU vaccine", } ] ), diff --git a/tests/integration/lambda/test_app_running_as_lambda.py b/tests/integration/lambda/test_app_running_as_lambda.py index 54e0370f8..4646aab99 100644 --- a/tests/integration/lambda/test_app_running_as_lambda.py +++ b/tests/integration/lambda/test_app_running_as_lambda.py @@ -232,7 +232,7 @@ def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_check_i "iterationVersion": campaign_config.iterations[0].version, "conditionName": campaign_config.target, "status": "not_actionable", - "statusText": "not_actionable", + "statusText": f"You should have the {campaign_config.target} vaccine", "eligibilityCohorts": [{"cohortCode": "cohort1", "cohortStatus": "not_actionable"}], "eligibilityCohortGroups": [ { @@ -456,7 +456,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationVersion": rsv_campaign.iterations[0].version, "conditionName": rsv_campaign.target, "status": "not_eligible", - "statusText": "not_eligible", + "statusText": "We do not believe you can have it", "eligibilityCohorts": [{"cohortCode": "cohort_label1", "cohortStatus": "not_eligible"}], "eligibilityCohortGroups": [ { @@ -477,7 +477,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationVersion": covid_campaign.iterations[0].version, "conditionName": covid_campaign.target, "status": "not_actionable", - "statusText": "not_actionable", + "statusText": f"You should have the {covid_campaign.target} vaccine", "eligibilityCohorts": [{"cohortCode": "cohort_label2", "cohortStatus": "not_actionable"}], "eligibilityCohortGroups": [ { @@ -502,7 +502,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationVersion": flu_campaign.iterations[0].version, "conditionName": flu_campaign.target, "status": "actionable", - "statusText": "actionable", + "statusText": f"You should have the {flu_campaign.target} vaccine", "eligibilityCohorts": [{"cohortCode": "cohort_label3", "cohortStatus": "actionable"}], "eligibilityCohortGroups": [ { diff --git a/tests/unit/audit/test_audit_context.py b/tests/unit/audit/test_audit_context.py index 2ee9aafdc..25897bd9a 100644 --- a/tests/unit/audit/test_audit_context.py +++ b/tests/unit/audit/test_audit_context.py @@ -149,7 +149,7 @@ def test_append_audit_condition_adds_condition_to_audit_log_on_g(app): assert cond.iteration_id == iteration.id assert cond.iteration_version == iteration.version assert cond.status == best_results[1].status.name - assert cond.status_text == best_results[1].status.name + assert cond.status_text == "You should have the Condition1 vaccine" assert cond.actions == expected_audit_action assert cond.action_rule.rule_priority == "1" assert cond.action_rule.rule_name == "RedirectRuleName1" From 3b7078096364f7dabcff1c3fb05210a06ef4e22b Mon Sep 17 00:00:00 2001 From: Shweta <216860557+shweta-nhs@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:18:58 +0100 Subject: [PATCH 2/4] ELI-294: Personalised/customised status text --- .../audit/audit_context.py | 2 +- .../model/eligibility.py | 11 +++++++++ .../calculators/eligibility_calculator.py | 1 + .../views/eligibility.py | 2 +- .../in_process/test_eligibility_endpoint.py | 24 +++++++++---------- .../lambda/test_app_running_as_lambda.py | 8 +++---- tests/unit/audit/test_audit_context.py | 2 +- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index b52edbe9b..a2ccc2a62 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -102,7 +102,7 @@ def append_audit_condition( iteration_version=best_active_iteration.version if best_active_iteration else None, condition_name=condition_name, status=best_candidate.status.name if best_candidate and best_candidate.status else None, - status_text=best_candidate.status.name if best_candidate and best_candidate.status else None, + status_text=best_candidate.status.get_status_text(condition_name) if best_candidate else None, eligibility_cohorts=audit_eligibility_cohorts, eligibility_cohort_groups=audit_eligibility_cohort_groups, filter_rules=audit_filter_rule, diff --git a/src/eligibility_signposting_api/model/eligibility.py b/src/eligibility_signposting_api/model/eligibility.py index bad948361..15934880b 100644 --- a/src/eligibility_signposting_api/model/eligibility.py +++ b/src/eligibility_signposting_api/model/eligibility.py @@ -24,6 +24,8 @@ UrlLink = NewType("UrlLink", HttpUrl) UrlLabel = NewType("UrlLabel", str) +StatusText = NewType("StatusText", str) + class RuleType(StrEnum): filter = "F" @@ -65,6 +67,14 @@ def best(*statuses: Status) -> Status: """ return max(statuses) + def get_status_text(self, condition_name: ConditionName) -> StatusText: + status_to_text_mapping = { + self.not_eligible: lambda: StatusText("We do not believe you can have it"), + self.not_actionable: lambda: StatusText(f"You should have the {condition_name} vaccine"), + self.actionable: lambda: StatusText(f"You should have the {condition_name} vaccine"), + } + return status_to_text_mapping.get(self, lambda: StatusText("Unknown status provided"))() + @dataclass class Reason: @@ -90,6 +100,7 @@ class Condition: condition_name: ConditionName status: Status cohort_results: list[CohortGroupResult] + status_text: StatusText actions: list[SuggestedAction] | 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 a64a8b878..4764c745b 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -311,6 +311,7 @@ def build_condition_results(condition_results: dict[ConditionName, IterationResu status=active_iteration_result.status, cohort_results=list(deduplicated_cohort_results), actions=condition_results[condition_name].actions, + status_text=active_iteration_result.status.get_status_text(condition_name), ) ) return conditions diff --git a/src/eligibility_signposting_api/views/eligibility.py b/src/eligibility_signposting_api/views/eligibility.py index 8875ae5e9..1ce27ca39 100644 --- a/src/eligibility_signposting_api/views/eligibility.py +++ b/src/eligibility_signposting_api/views/eligibility.py @@ -113,7 +113,7 @@ def build_eligibility_response(eligibility_status: EligibilityStatus) -> eligibi suggestions = ProcessedSuggestion( # pyright: ignore[reportCallIssue] condition=eligibility.ConditionName(condition.condition_name), # pyright: ignore[reportCallIssue] status=STATUS_MAPPING[condition.status], - statusText=eligibility.StatusText(f"{condition.status}"), # pyright: ignore[reportCallIssue] + statusText=eligibility.StatusText(condition.status_text), # pyright: ignore[reportCallIssue] eligibilityCohorts=build_eligibility_cohorts(condition), # pyright: ignore[reportCallIssue] suitabilityRules=build_suitability_results(condition), # pyright: ignore[reportCallIssue] actions=build_actions(condition), diff --git a/tests/integration/in_process/test_eligibility_endpoint.py b/tests/integration/in_process/test_eligibility_endpoint.py index 5356eed59..229d8652c 100644 --- a/tests/integration/in_process/test_eligibility_endpoint.py +++ b/tests/integration/in_process/test_eligibility_endpoint.py @@ -85,7 +85,7 @@ def test_not_base_eligible( ], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -128,7 +128,7 @@ def test_not_eligible_by_rule( ], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -177,7 +177,7 @@ def test_not_actionable( "ruleType": "S", } ], - "statusText": "Status.not_actionable", + "statusText": "You should have the RSV vaccine", } ] ), @@ -228,7 +228,7 @@ def test_actionable( } ], "suitabilityRules": [], - "statusText": "Status.actionable", + "statusText": "You should have the RSV vaccine", } ] ), @@ -273,7 +273,7 @@ def test_not_eligible_by_rule_when_only_magic_cohort_is_present( ], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -322,7 +322,7 @@ def test_not_actionable_when_only_magic_cohort_is_present( "ruleType": "S", } ], - "statusText": "Status.not_actionable", + "statusText": "You should have the COVID vaccine", } ] ), @@ -373,7 +373,7 @@ def test_actionable_when_only_magic_cohort_is_present( } ], "suitabilityRules": [], - "statusText": "Status.actionable", + "statusText": "You should have the COVID vaccine", } ] ), @@ -412,7 +412,7 @@ def test_not_base_eligible( "eligibilityCohorts": [], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -449,7 +449,7 @@ def test_not_eligible_by_rule( "eligibilityCohorts": [], "actions": [], "suitabilityRules": [], - "statusText": "Status.not_eligible", + "statusText": "We do not believe you can have it", } ] ), @@ -492,7 +492,7 @@ def test_not_actionable( "ruleType": "S", } ], - "statusText": "Status.not_actionable", + "statusText": "You should have the FLU vaccine", } ] ), @@ -537,7 +537,7 @@ def test_actionable( } ], "suitabilityRules": [], - "statusText": "Status.actionable", + "statusText": "You should have the FLU vaccine", } ] ), @@ -573,7 +573,7 @@ def test_actionable_no_actions( "status": "Actionable", "eligibilityCohorts": [], "suitabilityRules": [], - "statusText": "Status.actionable", + "statusText": "You should have the FLU vaccine", } ] ), diff --git a/tests/integration/lambda/test_app_running_as_lambda.py b/tests/integration/lambda/test_app_running_as_lambda.py index 99c936e08..0e9308796 100644 --- a/tests/integration/lambda/test_app_running_as_lambda.py +++ b/tests/integration/lambda/test_app_running_as_lambda.py @@ -235,7 +235,7 @@ def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_check_i "iterationVersion": campaign_config.iterations[0].version, "conditionName": campaign_config.target, "status": "not_actionable", - "statusText": "not_actionable", + "statusText": f"You should have the {campaign_config.target} vaccine", "eligibilityCohorts": [{"cohortCode": "cohort1", "cohortStatus": "not_actionable"}], "eligibilityCohortGroups": [ { @@ -461,7 +461,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationVersion": rsv_campaign.iterations[0].version, "conditionName": rsv_campaign.target, "status": "not_eligible", - "statusText": "not_eligible", + "statusText": "We do not believe you can have it", "eligibilityCohorts": [{"cohortCode": "cohort_label1", "cohortStatus": "not_eligible"}], "eligibilityCohortGroups": [ { @@ -482,7 +482,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationVersion": covid_campaign.iterations[0].version, "conditionName": covid_campaign.target, "status": "not_actionable", - "statusText": "not_actionable", + "statusText": f"You should have the {covid_campaign.target} vaccine", "eligibilityCohorts": [{"cohortCode": "cohort_label2", "cohortStatus": "not_actionable"}], "eligibilityCohortGroups": [ { @@ -507,7 +507,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationVersion": flu_campaign.iterations[0].version, "conditionName": flu_campaign.target, "status": "actionable", - "statusText": "actionable", + "statusText": f"You should have the {flu_campaign.target} vaccine", "eligibilityCohorts": [{"cohortCode": "cohort_label3", "cohortStatus": "actionable"}], "eligibilityCohortGroups": [ { diff --git a/tests/unit/audit/test_audit_context.py b/tests/unit/audit/test_audit_context.py index 2ee9aafdc..25897bd9a 100644 --- a/tests/unit/audit/test_audit_context.py +++ b/tests/unit/audit/test_audit_context.py @@ -149,7 +149,7 @@ def test_append_audit_condition_adds_condition_to_audit_log_on_g(app): assert cond.iteration_id == iteration.id assert cond.iteration_version == iteration.version assert cond.status == best_results[1].status.name - assert cond.status_text == best_results[1].status.name + assert cond.status_text == "You should have the Condition1 vaccine" assert cond.actions == expected_audit_action assert cond.action_rule.rule_priority == "1" assert cond.action_rule.rule_name == "RedirectRuleName1" From a3571c0cec30f464111297bd6db6ceefef7c8b17 Mon Sep 17 00:00:00 2001 From: Shweta <216860557+shweta-nhs@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:54:13 +0100 Subject: [PATCH 3/4] ELI-294: Adds unit tests for Status Enum --- tests/unit/model/test_status.py | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/unit/model/test_status.py diff --git a/tests/unit/model/test_status.py b/tests/unit/model/test_status.py new file mode 100644 index 000000000..72ba73b62 --- /dev/null +++ b/tests/unit/model/test_status.py @@ -0,0 +1,40 @@ +from eligibility_signposting_api.model.eligibility import ConditionName, Status, StatusText + + +class TestStatus: + def test_ordering(self): + assert Status.not_eligible < Status.not_actionable + assert Status.not_actionable < Status.actionable + assert Status.actionable > Status.not_actionable + assert Status.not_actionable > Status.not_eligible + assert Status.not_eligible == Status.not_eligible + + def test_is_exclusion(self): + assert Status.not_eligible.is_exclusion + assert Status.not_actionable.is_exclusion + assert not Status.actionable.is_exclusion + + def test_worst_status(self): + assert Status.worst(Status.not_eligible, Status.actionable) == Status.not_eligible + assert Status.worst(Status.actionable, Status.not_actionable) == Status.not_actionable + assert Status.worst(Status.not_eligible, Status.not_actionable, Status.actionable) == Status.not_eligible + assert Status.worst(Status.actionable) == Status.actionable + + def test_best_status(self): + assert Status.best(Status.not_eligible, Status.actionable) == Status.actionable + assert Status.best(Status.actionable, Status.not_actionable) == Status.actionable + assert Status.best(Status.not_eligible, Status.not_actionable, Status.actionable) == Status.actionable + assert Status.best(Status.not_eligible) == Status.not_eligible + + def test_get_status_text(self): + assert Status.not_eligible.get_status_text(ConditionName("COVID")) == StatusText( + "We do not believe you can have it" + ) + + assert Status.not_actionable.get_status_text(ConditionName("FLU")) == StatusText( + "You should have the FLU vaccine" + ) + + assert Status.actionable.get_status_text(ConditionName("COVID")) == StatusText( + "You should have the COVID vaccine" + ) From 88500c4077813aaa39d12cec2b83449835346121 Mon Sep 17 00:00:00 2001 From: Shweta <216860557+shweta-nhs@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:20:01 +0100 Subject: [PATCH 4/4] ELI-294: Fix sonar issues --- .../audit/audit_context.py | 18 +++-- .../calculators/eligibility_calculator.py | 66 ++++++++++--------- 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index a2ccc2a62..18afc6ecc 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -66,7 +66,7 @@ def append_audit_condition( redirect_rule_details: tuple[RulePriority | None, RuleName | None], ) -> None: audit_eligibility_cohorts, audit_eligibility_cohort_groups = [], [] - audit_filter_rule, audit_suitability_rule, audit_redirect_rule = None, None, None + audit_filter_rule, audit_suitability_rule = None, None best_active_iteration = best_results[0] best_candidate = best_results[1] best_cohort_results = best_results[2] @@ -88,10 +88,7 @@ def append_audit_condition( audit_filter_rule = AuditContext.create_audit_filter_rule(best_candidate, result) audit_suitability_rule = AuditContext.create_audit_suitability_rule(best_candidate, result) - if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: - audit_redirect_rule = AuditRedirectRule( - rule_priority=str(redirect_rule_details[0]), rule_name=redirect_rule_details[1] - ) + audit_redirect_rule = AuditContext.get_audit_redirect_rule(best_candidate, redirect_rule_details) audit_actions = AuditContext.create_audit_actions(suggested_actions) @@ -113,6 +110,17 @@ def append_audit_condition( g.audit_log.response.condition.append(audit_condition) + @staticmethod + def get_audit_redirect_rule( + best_candidate: IterationResult | None, redirect_rule_details: tuple[RulePriority | None, RuleName | None] + ) -> AuditRedirectRule | None: + audit_redirect_rule = None + if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: + audit_redirect_rule = AuditRedirectRule( + rule_priority=str(redirect_rule_details[0]), rule_name=redirect_rule_details[1] + ) + return audit_redirect_rule + @staticmethod def add_response_details(response_id: UUID, last_updated: datetime) -> None: g.audit_log.response.response_id = response_id diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index 4764c745b..43c832719 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from eligibility_signposting_api.model.rules import ( ActionsMapper, + CampaignConfig, CampaignID, CampaignVersion, Iteration, @@ -67,9 +68,14 @@ def campaigns_grouped_by_condition_name( ) -> Iterator[tuple[eligibility.ConditionName, list[rules.CampaignConfig]]]: """Generator that yields campaign groups filtered by condition names and campaign category.""" - allowed_types = ( - {"V", "S"} if category == "ALL" else {category[0]} if category in {"VACCINATIONS", "SCREENING"} else set() - ) + mapping = { + "ALL": {"V", "S"}, + "VACCINATIONS": {"V"}, + "SCREENING": {"S"}, + } + + allowed_types = mapping.get(category, set()) + filter_all_conditions = "ALL" in conditions for condition_name, campaign_group in groupby( @@ -159,23 +165,7 @@ def evaluate_eligibility( best_campaign_version: CampaignVersion | None best_cohort_results: dict[str, CohortGroupResult] | None - iteration_results: dict[ - str, tuple[Iteration, IterationResult, CampaignID, CampaignVersion, dict[str, CohortGroupResult]] - ] = {} - - for cc in campaign_group: - active_iteration = cc.current_iteration - cohort_results: dict[str, CohortGroupResult] = self.get_cohort_results(active_iteration) - - # Determine Result between cohorts - get the best - status, best_cohorts = self.get_the_best_cohort_memberships(cohort_results) - iteration_results[active_iteration.name] = ( - active_iteration, - IterationResult(status, best_cohorts, actions), - cc.id, - cc.version, - cohort_results, - ) + iteration_results = self.get_iteration_results(actions, campaign_group) # Determine results between iterations - get the best if iteration_results: @@ -229,6 +219,27 @@ def evaluate_eligibility( final_result = self.build_condition_results(condition_results) return eligibility.EligibilityStatus(conditions=final_result) + def get_iteration_results( + self, actions: list[SuggestedAction] | None, campaign_group: list[CampaignConfig] + ) -> dict[str, tuple[Iteration, IterationResult, CampaignID, CampaignVersion, dict[str, CohortGroupResult]]]: + iteration_results: dict[ + str, tuple[Iteration, IterationResult, CampaignID, CampaignVersion, dict[str, CohortGroupResult]] + ] = {} + for cc in campaign_group: + active_iteration = cc.current_iteration + cohort_results: dict[str, CohortGroupResult] = self.get_cohort_results(active_iteration) + + # Determine Result between cohorts - get the best + status, best_cohorts = self.get_the_best_cohort_memberships(cohort_results) + iteration_results[active_iteration.name] = ( + active_iteration, + IterationResult(status, best_cohorts, actions), + cc.id, + cc.version, + cohort_results, + ) + return iteration_results + def handle_redirect_rules( self, best_active_iteration: Iteration ) -> tuple[list[SuggestedAction] | None, RulePriority | None, RuleName | None]: @@ -327,9 +338,7 @@ def is_eligible_by_filter_rules( sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, filter_rules), key=priority_getter) for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter): - status, group_inclusion_reasons, group_exclusion_reasons, rule_stop = self.evaluate_rules_priority_group( - rule_group - ) + status, group_exclusion_reasons, _ = self.evaluate_rules_priority_group(rule_group) if status.is_exclusion: if cohort.cohort_label is not None: cohort_results[cohort.cohort_label] = CohortGroupResult( @@ -356,9 +365,7 @@ def evaluate_suppression_rules( sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, suppression_rules), key=priority_getter) for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter): - status, group_inclusion_reasons, group_exclusion_reasons, rule_stop = self.evaluate_rules_priority_group( - rule_group - ) + status, group_exclusion_reasons, rule_stop = self.evaluate_rules_priority_group(rule_group) if status.is_exclusion: is_actionable = False suppression_reasons.extend(group_exclusion_reasons) @@ -382,9 +389,9 @@ def evaluate_suppression_rules( def evaluate_rules_priority_group( self, rules_group: Iterator[rules.IterationRule] - ) -> tuple[eligibility.Status, list[eligibility.Reason], list[eligibility.Reason], bool]: + ) -> tuple[eligibility.Status, list[eligibility.Reason], bool]: is_rule_stop = False - inclusion_reasons, exclusion_reasons = [], [] + exclusion_reasons = [] best_status = eligibility.Status.not_eligible for rule in rules_group: @@ -396,9 +403,8 @@ def evaluate_rules_priority_group( exclusion_reasons.append(reason) else: best_status = eligibility.Status.actionable - inclusion_reasons.append(reason) - return best_status, inclusion_reasons, exclusion_reasons, is_rule_stop + return best_status, exclusion_reasons, is_rule_stop @staticmethod def get_actions_from_comms(action_mapper: ActionsMapper, comms: str) -> list[SuggestedAction] | None: