From 0007447c1c20842b807b99581fc107e69cf0bb09 Mon Sep 17 00:00:00 2001 From: Oneeb <258801025+oneeb-nhs@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:56:38 +0100 Subject: [PATCH 1/3] updated action rule handler and implemented tests --- .../processors/action_rule_handler.py | 23 ++++- tests/unit/model/test_status.py | 2 +- .../processors/test_action_rule_handler.py | 97 ++++++++++++++++++- 3 files changed, 119 insertions(+), 3 deletions(-) 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..e76f1793c 100644 --- a/src/eligibility_signposting_api/services/processors/action_rule_handler.py +++ b/src/eligibility_signposting_api/services/processors/action_rule_handler.py @@ -14,12 +14,14 @@ IterationResult, MatchedActionDetail, RuleType, + StatusText, SuggestedAction, UrlLabel, UrlLink, ) from eligibility_signposting_api.model.person import Person from eligibility_signposting_api.services.calculators.rule_calculator import RuleCalculator +from eligibility_signposting_api.config.constants import STATUS_TEXT_OVERRIDE_ACTION_TYPE class ActionRuleHandler: @@ -65,7 +67,9 @@ 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) + 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 _get_action_rules_components( @@ -102,3 +106,20 @@ def _get_actions_from_comms(action_mapper: ActionsMapper, comms: str) -> list[Su ) ) return suggested_actions + + @staticmethod + def _extract_status_text_override( + actions: list[SuggestedAction] | None, + ) -> tuple[list[SuggestedAction] | None, StatusText | None]: + if not actions: + return None, None + + override_text = None + remaining = [] + for action in actions: + if action.action_type == STATUS_TEXT_OVERRIDE_ACTION_TYPE: + override_text = StatusText(str(action.action_description)) if action.action_description else None + else: + remaining.append(action) + + return remaining or None, override_text diff --git a/tests/unit/model/test_status.py b/tests/unit/model/test_status.py index 4419542f0..21ac1b182 100644 --- a/tests/unit/model/test_status.py +++ b/tests/unit/model/test_status.py @@ -80,7 +80,7 @@ def test_matched_action_detail_existing_constructor_still_works_with_three_args( rule_name=RuleName("RuleA"), rule_priority=RulePriority(1), actions=actions, - status_text_override=StatusText(None), + status_text_override=None, ), ) diff --git a/tests/unit/services/processors/test_action_rule_handler.py b/tests/unit/services/processors/test_action_rule_handler.py index dd4827627..4e1a33c2c 100644 --- a/tests/unit/services/processors/test_action_rule_handler.py +++ b/tests/unit/services/processors/test_action_rule_handler.py @@ -1,9 +1,10 @@ from unittest.mock import Mock, call, patch import pytest -from hamcrest import assert_that, is_ +from hamcrest import assert_that, has_properties, is_ from pydantic import HttpUrl +from eligibility_signposting_api.config.constants import STATUS_TEXT_OVERRIDE_ACTION_TYPE from eligibility_signposting_api.model.campaign_config import AvailableAction, RuleName, RulePriority, RuleType from eligibility_signposting_api.model.eligibility_status import ( ActionCode, @@ -812,3 +813,97 @@ def test_handle_is_not_called_when_include_actions_is_false(mock_handle, handler ) assert_that(mock_handle.call_count, is_(0)) + + +# --- helpers --- + +def _make_action(action_type: str, description: str) -> SuggestedAction: + return SuggestedAction( + action_type=ActionType(action_type), + action_code=ActionCode("CODE"), + action_description=ActionDescription(description), + url_link=None, + url_label=None, + internal_action_code=InternalActionCode("code"), + ) + + +REGULAR_ACTION = _make_action("CareCardWithText", "You are eligible for a vaccination") +OVERRIDE_ACTION = _make_action(STATUS_TEXT_OVERRIDE_ACTION_TYPE, "You maybe eligible for a vaccination") + + +# --- _extract_status_text_override --- + +def test_extract_status_text_override_with_none_returns_none_none(): + result = ActionRuleHandler._extract_status_text_override(None) + + assert_that(result, is_((None, None))) + + +def test_extract_status_text_override_with_empty_list_returns_none_none(): + result = ActionRuleHandler._extract_status_text_override([]) + + assert_that(result, is_((None, None))) + + +def test_extract_status_text_override_with_no_override_action_leaves_actions_unchanged(): + actions = [REGULAR_ACTION] + + remaining, override_text = ActionRuleHandler._extract_status_text_override(actions) + + assert_that(remaining, is_([REGULAR_ACTION])) + assert_that(override_text, is_(None)) + + +def test_extract_status_text_override_with_only_override_action_captures_text(): + actions = [OVERRIDE_ACTION] + + remaining, override_text = ActionRuleHandler._extract_status_text_override(actions) + + assert_that(remaining, is_(None)) + assert_that(override_text, is_(StatusText("You maybe eligible for a vaccination"))) + + +def test_extract_status_text_override_with_mixed_actions_strips_override_and_keeps_others(): + actions = [REGULAR_ACTION, OVERRIDE_ACTION] + + remaining, override_text = ActionRuleHandler._extract_status_text_override(actions) + + assert_that(remaining, is_([REGULAR_ACTION])) + assert_that(override_text, is_(StatusText("You maybe eligible for a vaccination"))) + + +# --- _handle integration --- + +@patch("eligibility_signposting_api.services.processors.action_rule_handler.RuleCalculator") +@patch.object(ActionRuleHandler, "_get_actions_from_comms") +@patch.object(ActionRuleHandler, "_get_action_rules_components") +def test_handle_returns_status_text_override_in_matched_action_detail( + mock_get_action_rules_components, + mock_get_actions_from_comms, + mock_rule_calculator_class, + handler: ActionRuleHandler, +): + active_iteration = rule_builder.IterationFactory.build( + default_comms_routing="default_action_code", + actions_mapper=ActionsMapperFactory.build(), + iteration_rules=[], + ) + mock_get_action_rules_components.return_value = ( + [], + active_iteration.actions_mapper, + active_iteration.default_comms_routing, + ) + mock_get_actions_from_comms.return_value = [REGULAR_ACTION, OVERRIDE_ACTION] + + matched_action_detail = handler._handle(MOCK_PERSON, active_iteration, RuleType.redirect) + + assert_that( + matched_action_detail, + has_properties( + actions=[REGULAR_ACTION], + status_text_override=StatusText("You maybe eligible for a vaccination"), + rule_name=None, + rule_priority=None, + ), + ) From 7e7c82df8cd0a05376c05cd3fefc847f764d513e Mon Sep 17 00:00:00 2001 From: Oneeb <258801025+oneeb-nhs@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:45:51 +0100 Subject: [PATCH 2/3] updated extract status text override method to allow empty list aswell as None to allow integration test to pass --- .../services/processors/action_rule_handler.py | 7 +++++-- tests/unit/services/processors/test_action_rule_handler.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) 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 e76f1793c..8957d3a50 100644 --- a/src/eligibility_signposting_api/services/processors/action_rule_handler.py +++ b/src/eligibility_signposting_api/services/processors/action_rule_handler.py @@ -111,9 +111,12 @@ def _get_actions_from_comms(action_mapper: ActionsMapper, comms: str) -> list[Su def _extract_status_text_override( actions: list[SuggestedAction] | None, ) -> tuple[list[SuggestedAction] | None, StatusText | None]: - if not actions: + if actions is None: return None, None + if len(actions) == 0: + return [], None + override_text = None remaining = [] for action in actions: @@ -122,4 +125,4 @@ def _extract_status_text_override( else: remaining.append(action) - return remaining or None, override_text + return remaining, override_text diff --git a/tests/unit/services/processors/test_action_rule_handler.py b/tests/unit/services/processors/test_action_rule_handler.py index 4e1a33c2c..fdc9b1b09 100644 --- a/tests/unit/services/processors/test_action_rule_handler.py +++ b/tests/unit/services/processors/test_action_rule_handler.py @@ -843,7 +843,7 @@ def test_extract_status_text_override_with_none_returns_none_none(): def test_extract_status_text_override_with_empty_list_returns_none_none(): result = ActionRuleHandler._extract_status_text_override([]) - assert_that(result, is_((None, None))) + assert_that(result, is_(([], None))) def test_extract_status_text_override_with_no_override_action_leaves_actions_unchanged(): @@ -860,7 +860,7 @@ def test_extract_status_text_override_with_only_override_action_captures_text(): remaining, override_text = ActionRuleHandler._extract_status_text_override(actions) - assert_that(remaining, is_(None)) + assert_that(remaining, is_([])) assert_that(override_text, is_(StatusText("You maybe eligible for a vaccination"))) From c33f0c00130a37a83a8b95b7b401a4ebff4bb36b Mon Sep 17 00:00:00 2001 From: Oneeb <258801025+oneeb-nhs@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:05:50 +0100 Subject: [PATCH 3/3] fixed linting issue --- .../services/processors/action_rule_handler.py | 8 +++++--- .../unit/services/processors/test_action_rule_handler.py | 8 -------- 2 files changed, 5 insertions(+), 11 deletions(-) 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 8957d3a50..c80742f86 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, @@ -21,7 +22,6 @@ ) from eligibility_signposting_api.model.person import Person from eligibility_signposting_api.services.calculators.rule_calculator import RuleCalculator -from eligibility_signposting_api.config.constants import STATUS_TEXT_OVERRIDE_ACTION_TYPE class ActionRuleHandler: @@ -69,7 +69,9 @@ def _handle(self, person: Person, best_active_iteration: Iteration, rule_type: R actions, status_text_override = self._extract_status_text_override(actions) - return MatchedActionDetail(matched_action_rule_name, matched_action_rule_priority, actions, status_text_override) + return MatchedActionDetail( + matched_action_rule_name, matched_action_rule_priority, actions, status_text_override + ) @staticmethod def _get_action_rules_components( @@ -106,7 +108,7 @@ def _get_actions_from_comms(action_mapper: ActionsMapper, comms: str) -> list[Su ) ) return suggested_actions - + @staticmethod def _extract_status_text_override( actions: list[SuggestedAction] | None, diff --git a/tests/unit/services/processors/test_action_rule_handler.py b/tests/unit/services/processors/test_action_rule_handler.py index fdc9b1b09..eb01fe19f 100644 --- a/tests/unit/services/processors/test_action_rule_handler.py +++ b/tests/unit/services/processors/test_action_rule_handler.py @@ -815,8 +815,6 @@ def test_handle_is_not_called_when_include_actions_is_false(mock_handle, handler assert_that(mock_handle.call_count, is_(0)) -# --- helpers --- - def _make_action(action_type: str, description: str) -> SuggestedAction: return SuggestedAction( action_type=ActionType(action_type), @@ -832,8 +830,6 @@ def _make_action(action_type: str, description: str) -> SuggestedAction: OVERRIDE_ACTION = _make_action(STATUS_TEXT_OVERRIDE_ACTION_TYPE, "You maybe eligible for a vaccination") -# --- _extract_status_text_override --- - def test_extract_status_text_override_with_none_returns_none_none(): result = ActionRuleHandler._extract_status_text_override(None) @@ -873,15 +869,11 @@ def test_extract_status_text_override_with_mixed_actions_strips_override_and_kee assert_that(override_text, is_(StatusText("You maybe eligible for a vaccination"))) -# --- _handle integration --- - -@patch("eligibility_signposting_api.services.processors.action_rule_handler.RuleCalculator") @patch.object(ActionRuleHandler, "_get_actions_from_comms") @patch.object(ActionRuleHandler, "_get_action_rules_components") def test_handle_returns_status_text_override_in_matched_action_detail( mock_get_action_rules_components, mock_get_actions_from_comms, - mock_rule_calculator_class, handler: ActionRuleHandler, ): active_iteration = rule_builder.IterationFactory.build(