From bdb08ea7aa8dd92ed2592232296fcdf4e5157b1b Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:27:05 +0100 Subject: [PATCH 01/19] WIP: drafting out X and Y rules. --- tests/test_data/test_config/test_config.json | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_data/test_config/test_config.json b/tests/test_data/test_config/test_config.json index ab2672df5..fb32c87c4 100644 --- a/tests/test_data/test_config/test_config.json +++ b/tests/test_data/test_config/test_config.json @@ -16,6 +16,8 @@ { "ID": "id_100", "DefaultCommsRouting": "INTERNALCONTACTGP1", + "DefaultNotActionableRouting": "INTERNALCONTACTGP1", + "DefaultNotEligibleRouting": "INTERNALCONTACTGP1", "ActionsMapper": { "INTERNALCONTACTGP1": {"ExternalRoutingCode": "CONTACTGP","ActionDescription":"Contact GP Text1 description", "ActionType":"text1"}, "INTERNALCONTACTGP2": {"ExternalRoutingCode": "CONTACTGP","ActionDescription":"Contact GP Link description", "ActionType":"link", "UrlLink": "link123", "UrlLabel": "link label"}, @@ -91,6 +93,28 @@ "Operator": ">", "Comparator": "19000101", "CommsRouting": "INTERNALCONTACTGP1|INTERNALTESCO" + }, + { + "Type": "X", + "Name": "Test X Rule for not actionable", + "Description": "Test X Rule Desc", + "Priority": 20, + "AttributeLevel": "PERSON", + "AttributeName": "DATE_OF_BIRTH", + "Operator": ">", + "Comparator": "19000101", + "CommsRouting": "INTERNALCONTACTGP1|INTERNALTESCO" + }, + { + "Type": "Y", + "Name": "Test Y Rule for not eligible", + "Description": "Test Y Rule Desc", + "Priority": 20, + "AttributeLevel": "PERSON", + "AttributeName": "DATE_OF_BIRTH", + "Operator": ">", + "Comparator": "19000101", + "CommsRouting": "INTERNALCONTACTGP1|INTERNALTESCO" } ], "Version": "1", From 03b3c53e70db2d929981a81897c3afefc204860d Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:07:59 +0100 Subject: [PATCH 02/19] WIP: updated config with x and y rule idea. --- tests/test_data/test_config/test_config.json | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_data/test_config/test_config.json b/tests/test_data/test_config/test_config.json index fb32c87c4..6e225e198 100644 --- a/tests/test_data/test_config/test_config.json +++ b/tests/test_data/test_config/test_config.json @@ -22,7 +22,11 @@ "INTERNALCONTACTGP1": {"ExternalRoutingCode": "CONTACTGP","ActionDescription":"Contact GP Text1 description", "ActionType":"text1"}, "INTERNALCONTACTGP2": {"ExternalRoutingCode": "CONTACTGP","ActionDescription":"Contact GP Link description", "ActionType":"link", "UrlLink": "link123", "UrlLabel": "link label"}, "INTERNALTESCO": {"ExternalRoutingCode": "TESCO","ActionDescription":"Tesco description", "ActionType":"link", "UrlLink": "tesco link", "UrlLabel": "link label"}, - "INTERNALFINDWALKIN": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"} + "INTERNALFINDWALKIN": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"}, + + "XRULEID1": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"}, + "YRULEID1": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"} + }, "IterationCohorts": [ { @@ -96,25 +100,25 @@ }, { "Type": "X", - "Name": "Test X Rule for not actionable", + "Name": "Test X Rule for not eligible", "Description": "Test X Rule Desc", "Priority": 20, "AttributeLevel": "PERSON", "AttributeName": "DATE_OF_BIRTH", "Operator": ">", "Comparator": "19000101", - "CommsRouting": "INTERNALCONTACTGP1|INTERNALTESCO" + "CommsRouting": "XRULEID1|INTERNALTESCO" }, { "Type": "Y", - "Name": "Test Y Rule for not eligible", + "Name": "Test Y Rule for not actionable", "Description": "Test Y Rule Desc", "Priority": 20, "AttributeLevel": "PERSON", "AttributeName": "DATE_OF_BIRTH", "Operator": ">", "Comparator": "19000101", - "CommsRouting": "INTERNALCONTACTGP1|INTERNALTESCO" + "CommsRouting": "YRULEID1|INTERNALTESCO" } ], "Version": "1", From 62c64f42f6b1c1ce2b3676d6d87bdf23093af0c8 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:48:50 +0100 Subject: [PATCH 03/19] WIP: Stub out x and y rules impl --- src/eligibility_signposting_api/model/rules.py | 5 ++++- .../services/calculators/eligibility_calculator.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/eligibility_signposting_api/model/rules.py b/src/eligibility_signposting_api/model/rules.py index 541db6263..831c3e4a7 100644 --- a/src/eligibility_signposting_api/model/rules.py +++ b/src/eligibility_signposting_api/model/rules.py @@ -42,7 +42,8 @@ class RuleType(StrEnum): filter = "F" suppression = "S" redirect = "R" - + not_eligible = "X" + not_actionable = "Y" class RuleOperator(StrEnum): equals = "=" @@ -153,6 +154,8 @@ class Iteration(BaseModel): approval_maximum: int | None = Field(None, alias="ApprovalMaximum") type: Literal["A", "M", "S", "O"] = Field(..., alias="Type") default_comms_routing: str = Field(..., alias="DefaultCommsRouting") + default_not_eligible_routing: str = Field(..., alias="DefaultNotEligibleRouting") + default_not_actionable_routing: str = Field(..., alias="DefaultNotActionableRouting") iteration_cohorts: list[IterationCohort] = Field(..., alias="IterationCohorts") iteration_rules: list[IterationRule] = Field(..., alias="IterationRules") actions_mapper: ActionsMapper = Field(..., alias="ActionsMapper") diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index bc060bbef..2b3808415 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -202,6 +202,19 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil if best_candidate.status in (Status.not_eligible, Status.not_actionable) and not include_actions_flag: actions = None + # if best_candidate.status == Status.not_eligible + # if include_actions_flag: + # ... = self.handle_X_rules(best_active_iteration) + # else: + # actions = None + + # if best_candidate.status == Status.not_actionable + # if include_actions_flag: + # ... = self.handle_Y_rules(best_active_iteration) + # else: + # actions = None + + # add actions to condition results condition_results[condition_name].actions = actions # reset actions for the next condition From ab068a4ce3b713718f8d7ad46c031a9a71d042c3 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:51:20 +0100 Subject: [PATCH 04/19] WIP: stubbing out impl. --- .../audit/audit_context.py | 4 ++++ .../calculators/eligibility_calculator.py | 21 +++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index 09094d95b..4cddb9f72 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -98,11 +98,15 @@ def append_audit_condition( rule_message=value.audit_rules[0].rule_description, ) + # todo + # if actionflag + # if best_candidate and best_candidate.status and actionflag 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] ) + if suggested_actions is None: audit_actions = None elif len(suggested_actions) > 0: diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index 2b3808415..121ab431c 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -199,21 +199,31 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil else: actions = None - if best_candidate.status in (Status.not_eligible, Status.not_actionable) and not include_actions_flag: - actions = None + # todo naming decisions + # handle_redirect_rules NEW NAME!! + # x rules = ? + # y rules = ? - # if best_candidate.status == Status.not_eligible + # elif best_candidate.status == Status.not_eligible and best_active_iteration is not None: # if include_actions_flag: # ... = self.handle_X_rules(best_active_iteration) # else: # actions = None - # if best_candidate.status == Status.not_actionable + # elif best_candidate.status == Status.not_actionable and best_active_iteration is not None: # if include_actions_flag: # ... = self.handle_Y_rules(best_active_iteration) # else: # actions = None + # else: + # actions = None + + if best_candidate.status in (Status.not_eligible, Status.not_actionable) and not include_actions_flag: + actions = None + + + # add actions to condition results condition_results[condition_name].actions = actions @@ -229,6 +239,9 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil (redirect_rule_priority, redirect_rule_name), ) + #todo check if need reset + #redirect_rule_priority, redirect_rule_name = None, None + # Consolidate all the results and return final_result = self.build_condition_results(condition_results) return eligibility.EligibilityStatus(conditions=final_result) From 88651237a8e2cd43542b37e73c2c9a80661d0d59 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 9 Jul 2025 11:59:40 +0100 Subject: [PATCH 05/19] Refactored action support functions and renamed vars --- .../audit/audit_context.py | 3 +- .../model/rules.py | 5 +- .../calculators/eligibility_calculator.py | 58 ++++++++++--------- .../test_eligibility_calculator.py | 6 +- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index 4cddb9f72..c2de05db2 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -98,7 +98,7 @@ def append_audit_condition( rule_message=value.audit_rules[0].rule_description, ) - # todo + # TODO # if actionflag # if best_candidate and best_candidate.status and actionflag if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: @@ -106,7 +106,6 @@ def append_audit_condition( rule_priority=str(redirect_rule_details[0]), rule_name=redirect_rule_details[1] ) - if suggested_actions is None: audit_actions = None elif len(suggested_actions) > 0: diff --git a/src/eligibility_signposting_api/model/rules.py b/src/eligibility_signposting_api/model/rules.py index 831c3e4a7..b7409baea 100644 --- a/src/eligibility_signposting_api/model/rules.py +++ b/src/eligibility_signposting_api/model/rules.py @@ -42,8 +42,9 @@ class RuleType(StrEnum): filter = "F" suppression = "S" redirect = "R" - not_eligible = "X" - not_actionable = "Y" + not_eligible_actions = "X" + not_actionable_actions = "Y" + class RuleOperator(StrEnum): equals = "=" diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index 121ab431c..75ceb106a 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -18,6 +18,7 @@ IterationCohort, RuleName, RulePriority, + RuleType, ) from wireup import service @@ -126,22 +127,28 @@ def get_rules_by_type( ) return filter_rules, suppression_rules + # TODO rename function get_action_rules_action_mapper(active_iteration, rules.RuleType) @staticmethod - def get_redirect_rules( - active_iteration: Iteration, + def get_action_rules_components( + active_iteration: Iteration, rule_type: RuleType ) -> tuple[tuple[rules.IterationRule, ...], ActionsMapper, str]: - redirect_rules = tuple( - rule for rule in active_iteration.iteration_rules if rule.type in rules.RuleType.redirect - ) - default_comms = active_iteration.default_comms_routing + action_rules = tuple(rule for rule in active_iteration.iteration_rules if rule.type in rule_type) + + routing_map = { + rules.RuleType.redirect: active_iteration.default_comms_routing, + rules.RuleType.not_eligible_actions: active_iteration.default_not_eligible_routing, + rules.RuleType.not_actionable_actions: active_iteration.default_not_actionable_routing, + } + + default_comms = routing_map.get(rule_type) action_mapper = active_iteration.actions_mapper - return redirect_rules, action_mapper, default_comms + return action_rules, action_mapper, default_comms def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibility.EligibilityStatus: """Iterates over campaign groups, evaluates eligibility, and returns a consolidated status.""" condition_results: dict[ConditionName, IterationResult] = {} actions: list[SuggestedAction] | None = [] - redirect_rule_priority, redirect_rule_name = None, None + action_rule_priority, action_rule_name = None, None for condition_name, campaign_group in self.campaigns_grouped_by_condition_name: best_active_iteration: Iteration | None @@ -191,15 +198,15 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil if best_candidate.status == Status.actionable and best_active_iteration is not None: if include_actions_flag: - actions, matched_r_rule_priority, matched_r_rule_name = self.handle_redirect_rules( - best_active_iteration + actions, matched_action_rule_priority, matched_action_rule_name = self.handle_action_rules( + best_active_iteration, rules.RuleType.redirect ) - redirect_rule_name = matched_r_rule_name - redirect_rule_priority = matched_r_rule_priority + action_rule_name = matched_action_rule_name + action_rule_priority = matched_action_rule_priority else: actions = None - # todo naming decisions + # TODO naming decisions # handle_redirect_rules NEW NAME!! # x rules = ? # y rules = ? @@ -222,9 +229,6 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil if best_candidate.status in (Status.not_eligible, Status.not_actionable) and not include_actions_flag: actions = None - - - # add actions to condition results condition_results[condition_name].actions = actions # reset actions for the next condition @@ -236,25 +240,25 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil condition_name, (best_active_iteration, best_candidate, best_cohort_results), (best_campaign_id, best_campaign_version), - (redirect_rule_priority, redirect_rule_name), + (action_rule_priority, action_rule_name), ) - #todo check if need reset - #redirect_rule_priority, redirect_rule_name = None, None + # TODO check if need reset + # action_rule_priority, action_rule_name = None, None # Consolidate all the results and return final_result = self.build_condition_results(condition_results) return eligibility.EligibilityStatus(conditions=final_result) - def handle_redirect_rules( - self, best_active_iteration: Iteration + def handle_action_rules( + self, best_active_iteration: Iteration, rule_type: RuleType ) -> tuple[list[SuggestedAction] | None, RulePriority | None, RuleName | None]: - redirect_rules, action_mapper, default_comms = self.get_redirect_rules(best_active_iteration) + action_rules, action_mapper, default_comms = self.get_action_rules_components(best_active_iteration, rule_type) priority_getter = attrgetter("priority") - sorted_rules_by_priority = sorted(redirect_rules, key=priority_getter) + sorted_rules_by_priority = sorted(action_rules, key=priority_getter) actions: list[SuggestedAction] | None = self.get_actions_from_comms(action_mapper, default_comms) - matched_redirect_rule_priority, matched_redirect_rule_name = None, None + matched_action_rule_priority, matched_action_rule_name = None, None for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter): rule_group_list = list(rule_group) matcher_matched_list = [ @@ -267,11 +271,11 @@ def handle_redirect_rules( rule_actions = self.get_actions_from_comms(action_mapper, comms_routing) if rule_actions and len(rule_actions) > 0: actions = rule_actions - matched_redirect_rule_priority = rule_group_list[0].priority - matched_redirect_rule_name = rule_group_list[0].name + matched_action_rule_priority = rule_group_list[0].priority + matched_action_rule_name = rule_group_list[0].name break - return actions, matched_redirect_rule_priority, matched_redirect_rule_name + return actions, matched_action_rule_priority, matched_action_rule_name def get_cohort_results(self, active_iteration: rules.Iteration) -> dict[str, CohortGroupResult]: cohort_results: dict[str, CohortGroupResult] = {} diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 86d7ff487..26728fd48 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -39,7 +39,7 @@ class TestEligibilityCalculator: @staticmethod - def test_get_redirect_rules(): + def test_get_action_rules_components(): # Given iteration = rule_builder.IterationFactory.build( @@ -67,7 +67,9 @@ def test_get_redirect_rules(): ) # when - actual_rules, actual_action_mapper, actual_default_comms = EligibilityCalculator.get_redirect_rules(iteration) + actual_rules, actual_action_mapper, actual_default_comms = EligibilityCalculator.get_action_rules_components( + iteration, rules.RuleType.redirect + ) # then assert_that(actual_rules, has_item(is_iteration_rule().with_name(iteration.iteration_rules[0].name))) From a5d56a4028fa3a6cffe8ab96d694a1dfc7eed58f Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:50:49 +0100 Subject: [PATCH 06/19] WIP: Added X/Y Rule logic and test. --- .../model/eligibility.py | 3 +- .../calculators/eligibility_calculator.py | 40 +++++++----- .../services/calculators/rule_calculator.py | 2 + tests/fixtures/builders/model/rule.py | 11 ++++ .../test_eligibility_calculator.py | 64 +++++++++++++++++++ 5 files changed, 103 insertions(+), 17 deletions(-) diff --git a/src/eligibility_signposting_api/model/eligibility.py b/src/eligibility_signposting_api/model/eligibility.py index bad948361..815c572ef 100644 --- a/src/eligibility_signposting_api/model/eligibility.py +++ b/src/eligibility_signposting_api/model/eligibility.py @@ -29,7 +29,8 @@ class RuleType(StrEnum): filter = "F" suppression = "S" redirect = "R" - + not_eligible_actions = "X" + not_actionable_actions = "Y" @total_ordering class Status(Enum): diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index 75ceb106a..158e92645 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -196,6 +196,7 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil condition_results[condition_name] = best_candidate + # Redirect action rules if best_candidate.status == Status.actionable and best_active_iteration is not None: if include_actions_flag: actions, matched_action_rule_priority, matched_action_rule_name = self.handle_action_rules( @@ -206,29 +207,36 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil else: actions = None - # TODO naming decisions - # handle_redirect_rules NEW NAME!! - # x rules = ? - # y rules = ? + # Not Eligible action rules (Xrules) + elif best_candidate.status == Status.not_eligible and best_active_iteration is not None: + if include_actions_flag: + actions, matched_action_rule_priority, matched_action_rule_name = self.handle_action_rules( + best_active_iteration, rules.RuleType.not_eligible_actions + ) + action_rule_name = matched_action_rule_name + action_rule_priority = matched_action_rule_priority + else: + actions = None - # elif best_candidate.status == Status.not_eligible and best_active_iteration is not None: - # if include_actions_flag: - # ... = self.handle_X_rules(best_active_iteration) - # else: - # actions = None + # Not Actionable action rules (Yrules) + elif best_candidate.status == Status.not_actionable and best_active_iteration is not None: + if include_actions_flag: + actions, matched_action_rule_priority, matched_action_rule_name = self.handle_action_rules( + best_active_iteration, rules.RuleType.not_actionable_actions + ) + action_rule_name = matched_action_rule_name + action_rule_priority = matched_action_rule_priority + else: + actions = None - # elif best_candidate.status == Status.not_actionable and best_active_iteration is not None: - # if include_actions_flag: - # ... = self.handle_Y_rules(best_active_iteration) - # else: - # actions = None + else: + actions = None - # else: - # actions = None if best_candidate.status in (Status.not_eligible, Status.not_actionable) and not include_actions_flag: actions = None + # add actions to condition results condition_results[condition_name].actions = actions # reset actions for the next condition diff --git a/src/eligibility_signposting_api/services/calculators/rule_calculator.py b/src/eligibility_signposting_api/services/calculators/rule_calculator.py index 145a1e89f..03641e3be 100644 --- a/src/eligibility_signposting_api/services/calculators/rule_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/rule_calculator.py @@ -84,6 +84,8 @@ def evaluate_rule(self, attribute_value: str | None) -> tuple[eligibility.Status rules.RuleType.filter: eligibility.Status.not_eligible, rules.RuleType.suppression: eligibility.Status.not_actionable, rules.RuleType.redirect: eligibility.Status.actionable, + rules.RuleType.not_eligible_actions: eligibility.Status.not_eligible, + rules.RuleType.not_actionable_actions: eligibility.Status.not_actionable, }[self.rule.type] return status, str(reason), matcher_matched matcher.describe_mismatch(attribute_value, reason) diff --git a/tests/fixtures/builders/model/rule.py b/tests/fixtures/builders/model/rule.py index 5388b113b..444e2f970 100644 --- a/tests/fixtures/builders/model/rule.py +++ b/tests/fixtures/builders/model/rule.py @@ -173,3 +173,14 @@ class ICBRedirectRuleFactory(IterationRuleFactory): attribute_name = rules.RuleAttributeName("ICB") comparator = rules.RuleComparator("QE1") comms_routing = rules.CommsRouting("ActionCode1") + +class ICBNonEligibleActionRuleFactory(IterationRuleFactory): + type = rules.RuleType.not_eligible_actions + name = rules.RuleName("In QE1") + description = rules.RuleDescription("In QE1") + priority = rules.RulePriority(20) + operator = rules.RuleOperator.equals + attribute_level = rules.RuleAttributeLevel.PERSON + attribute_name = rules.RuleAttributeName("ICB") + comparator = rules.RuleComparator("QE1") + comms_routing = rules.CommsRouting("ActionCode1") diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 26728fd48..9377baaaf 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2374,3 +2374,67 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat ) ), ) + + +def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR0913 + faker: Faker, +): + # Given + nhs_number = NHSNumber(faker.nhs_number()) + + person_rows = person_rows_builder(nhs_number, cohorts=["NotEligibleCohort"], icb="QE1") + + campaign_configs = [ + ( + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + #default_comms_routing=default_comms_routing, + actions_mapper=rule_builder.ActionsMapperFactory.build( + root={"ActionCode1": + AvailableAction( + ActionType="InfoText", + ExternalRoutingCode="HealthcareProInfo", + ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", + ) + }), + iteration_rules=[rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing="ActionCode1")], + ) + ], + ) + ) + ] + + expected_actions = [ + SuggestedAction( + internal_action_code=InternalActionCode("ActionCode1"), + action_type=ActionType("InfoText"), + action_code=ActionCode("HealthcareProInfo"), + action_description=ActionDescription("Speak to your healthcare professional if you think you should be offered this vaccination."), + url_link=None, + url_label=None, + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_eligible)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) + + + From 9758439972ab17a8f773de8744e96ffbafa7cca6 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Thu, 10 Jul 2025 12:03:39 +0100 Subject: [PATCH 07/19] Added tests for eligible and actionable actions. --- tests/fixtures/builders/model/rule.py | 11 + tests/test_data/test_config/test_config.json | 1 - .../test_eligibility_calculator.py | 220 ++++++++++++++++-- 3 files changed, 211 insertions(+), 21 deletions(-) diff --git a/tests/fixtures/builders/model/rule.py b/tests/fixtures/builders/model/rule.py index 444e2f970..bd1c5e4b0 100644 --- a/tests/fixtures/builders/model/rule.py +++ b/tests/fixtures/builders/model/rule.py @@ -184,3 +184,14 @@ class ICBNonEligibleActionRuleFactory(IterationRuleFactory): attribute_name = rules.RuleAttributeName("ICB") comparator = rules.RuleComparator("QE1") comms_routing = rules.CommsRouting("ActionCode1") + +class ICBNonActionableActionRuleFactory(IterationRuleFactory): + type = rules.RuleType.not_actionable_actions + name = rules.RuleName("In QE1") + description = rules.RuleDescription("In QE1") + priority = rules.RulePriority(20) + operator = rules.RuleOperator.equals + attribute_level = rules.RuleAttributeLevel.PERSON + attribute_name = rules.RuleAttributeName("ICB") + comparator = rules.RuleComparator("QE1") + comms_routing = rules.CommsRouting("ActionCode1") diff --git a/tests/test_data/test_config/test_config.json b/tests/test_data/test_config/test_config.json index 6e225e198..643cbce2a 100644 --- a/tests/test_data/test_config/test_config.json +++ b/tests/test_data/test_config/test_config.json @@ -26,7 +26,6 @@ "XRULEID1": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"}, "YRULEID1": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"} - }, "IterationCohorts": [ { diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 9377baaaf..872aba376 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2375,14 +2375,89 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat ), ) - +@pytest.mark.parametrize( + ("test_comment", "person_icb", "default_comms_routing", "comms_routing", "actions_mapper", "expected_actions"), + [ + ( + """Not eligible person with matching NonEligibleActionRule""", + "QE1", + "", + "ActionCode1", + {"ActionCode1": + AvailableAction( + ActionType="InfoText", + ExternalRoutingCode="HealthcareProInfo", + ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", + )}, + [ + SuggestedAction( + internal_action_code=InternalActionCode("ActionCode1"), + action_type=ActionType("InfoText"), + action_code=ActionCode("HealthcareProInfo"), + action_description=ActionDescription( + "Speak to your healthcare professional if you think you should be offered this vaccination."), + url_link=None, + url_label=None, + ) + ], + ), + ( + """Not eligible person with NON matching NonEligibleActionRule""", + "WS3", + "defaultCommsCode", + "ActionCode1", + {"defaultCommsCode": + AvailableAction( + ActionType="DefaultInfoText", + ExternalRoutingCode="DefaultHealthcareProInfo", + ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + )}, + [ + SuggestedAction( + internal_action_code=InternalActionCode("defaultCommsCode"), + action_type=ActionType("DefaultInfoText"), + action_code=ActionCode("DefaultHealthcareProInfo"), + action_description=ActionDescription( + "Default Speak to your healthcare professional if you think you should be offered this vaccination."), + url_link=None, + url_label=None, + ) + ], + ), + ( + """Not eligible person with matching but missing NonEligibleActionRule, fall back to default comms""", + "QE1", + "defaultCommsCode", + "ActionCode1", + {"defaultCommsCode": + AvailableAction( + ActionType="DefaultInfoText", + ExternalRoutingCode="DefaultHealthcareProInfo", + ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + ) + }, + [ + SuggestedAction( + internal_action_code=InternalActionCode("defaultCommsCode"), + action_type=ActionType("DefaultInfoText"), + action_code=ActionCode("DefaultHealthcareProInfo"), + action_description=ActionDescription( + "Default Speak to your healthcare professional if you think you should be offered this vaccination."), + url_link=None, + url_label=None, + ) + ], + ) + ] +) def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR0913 +test_comment, person_icb, default_comms_routing, comms_routing, actions_mapper, expected_actions, faker: Faker, ): # Given nhs_number = NHSNumber(faker.nhs_number()) - person_rows = person_rows_builder(nhs_number, cohorts=["NotEligibleCohort"], icb="QE1") + person_rows = person_rows_builder(nhs_number, cohorts=["NotEligibleCohort"], icb=person_icb) campaign_configs = [ ( @@ -2391,30 +2466,135 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR iterations=[ rule_builder.IterationFactory.build( iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - #default_comms_routing=default_comms_routing, + default_not_eligible_routing=default_comms_routing, actions_mapper=rule_builder.ActionsMapperFactory.build( - root={"ActionCode1": - AvailableAction( - ActionType="InfoText", - ExternalRoutingCode="HealthcareProInfo", - ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", - ) - }), - iteration_rules=[rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing="ActionCode1")], + root=actions_mapper), + iteration_rules=[rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=comms_routing)], ) ], ) ) ] - expected_actions = [ - SuggestedAction( - internal_action_code=InternalActionCode("ActionCode1"), - action_type=ActionType("InfoText"), - action_code=ActionCode("HealthcareProInfo"), - action_description=ActionDescription("Speak to your healthcare professional if you think you should be offered this vaccination."), - url_link=None, - url_label=None, + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_eligible)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) + +@pytest.mark.parametrize( + ("test_comment", "person_icb", "default_comms_routing", "comms_routing", "actions_mapper", "expected_actions"), + [ + ( + """Not actionable person with matching NonActionableActionRule""", + "QE1", + "", + "ActionCode1", + {"ActionCode1": + AvailableAction( + ActionType="InfoText", + ExternalRoutingCode="HealthcareProInfo", + ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", + )}, + [ + SuggestedAction( + internal_action_code=InternalActionCode("ActionCode1"), + action_type=ActionType("InfoText"), + action_code=ActionCode("HealthcareProInfo"), + action_description=ActionDescription( + "Speak to your healthcare professional if you think you should be offered this vaccination."), + url_link=None, + url_label=None, + ) + ], + ), + ( + """Not actionable person with NON matching NonActionableActionRule""", + "WS3", + "defaultCommsCode", + "ActionCode1", + {"defaultCommsCode": + AvailableAction( + ActionType="DefaultInfoText", + ExternalRoutingCode="DefaultHealthcareProInfo", + ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + )}, + [ + SuggestedAction( + internal_action_code=InternalActionCode("defaultCommsCode"), + action_type=ActionType("DefaultInfoText"), + action_code=ActionCode("DefaultHealthcareProInfo"), + action_description=ActionDescription( + "Default Speak to your healthcare professional if you think you should be offered this vaccination."), + url_link=None, + url_label=None, + ) + ], + ), + ( + """Not actionable person with matching but missing NonActionableActionRule, fall back to default comms""", + "QE1", + "defaultCommsCode", + "ActionCode1", + {"defaultCommsCode": + AvailableAction( + ActionType="DefaultInfoText", + ExternalRoutingCode="DefaultHealthcareProInfo", + ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + ) + }, + [ + SuggestedAction( + internal_action_code=InternalActionCode("defaultCommsCode"), + action_type=ActionType("DefaultInfoText"), + action_code=ActionCode("DefaultHealthcareProInfo"), + action_description=ActionDescription( + "Default Speak to your healthcare professional if you think you should be offered this vaccination."), + url_link=None, + url_label=None, + ) + ], + ) + ] +) +def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: PLR0913 +test_comment, person_icb, default_comms_routing, comms_routing, actions_mapper, expected_actions, + faker: Faker, +): + # Given + nhs_number = NHSNumber(faker.nhs_number()) + + person_rows = person_rows_builder(nhs_number, cohorts=["cohort1"], icb=person_icb, de=True) + + campaign_configs = [ + ( + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + default_not_actionable_routing=default_comms_routing, + actions_mapper=rule_builder.ActionsMapperFactory.build( + root=actions_mapper), + iteration_rules=[ + rule_builder.DetainedEstateSuppressionRuleFactory.build(), + rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=comms_routing) + ], + ) + ], + ) ) ] @@ -2430,7 +2610,7 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR has_items( is_condition() .with_condition_name(ConditionName("RSV")) - .and_status(equal_to(Status.not_eligible)) + .and_status(equal_to(Status.not_actionable)) .and_actions(equal_to(expected_actions)) ) ), From 7b5b5235f501a7ccd6c320053e682efe9c5647d8 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:57:07 +0100 Subject: [PATCH 08/19] WIP: Added more tests for X and Y rule scenarios. --- .../lambda/test_app_running_as_lambda.py | 4 +- .../test_eligibility_calculator.py | 87 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/tests/integration/lambda/test_app_running_as_lambda.py b/tests/integration/lambda/test_app_running_as_lambda.py index 8253e9a1a..cb30e0ea7 100644 --- a/tests/integration/lambda/test_app_running_as_lambda.py +++ b/tests/integration/lambda/test_app_running_as_lambda.py @@ -318,7 +318,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationId": rsv_campaign.iterations[0].id, "iterationVersion": rsv_campaign.iterations[0].version, "conditionName": rsv_campaign.target, - "status": "not_eligible", + "status": "not_eligible", #TODO is not_eligible status with no actions valid (ELI-295) "statusText": "not_eligible", "eligibilityCohorts": [{"cohortCode": "cohort_group1", "cohortStatus": "not_eligible"}], "eligibilityCohortGroups": [ @@ -339,7 +339,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationId": covid_campaign.iterations[0].id, "iterationVersion": covid_campaign.iterations[0].version, "conditionName": covid_campaign.target, - "status": "not_actionable", + "status": "not_actionable", #TODO is not_actionable status with no actions valid (ELI-295) "statusText": "not_actionable", "eligibilityCohorts": [{"cohortCode": "cohort_group2", "cohortStatus": "not_actionable"}], "eligibilityCohortGroups": [ diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 872aba376..53c416f5e 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2494,6 +2494,51 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR ), ) +def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_given( # noqa: PLR0913 + faker: Faker, +): + # ELI-295 Campaign config without NonEligibleActions (X rules) should not return any actions/default actions for NonEligible status + + # Given + nhs_number = NHSNumber(faker.nhs_number()) + + person_rows = person_rows_builder(nhs_number, cohorts=["NotEligibleCohort"]) + + campaign_configs = [ + ( + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + actions_mapper=[], + iteration_rules=[] + ) + ], + ) + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + expected_actions=[] + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_eligible)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) + + @pytest.mark.parametrize( ("test_comment", "person_icb", "default_comms_routing", "comms_routing", "actions_mapper", "expected_actions"), [ @@ -2616,5 +2661,47 @@ def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: P ), ) +def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_given( # noqa: PLR0913 + faker: Faker, +): + # ELI-295 Campaign config without NonActionableActions (Y rules) should not return any actions/default actions for NonActionable status + + # Given + nhs_number = NHSNumber(faker.nhs_number()) + + person_rows = person_rows_builder(nhs_number, cohorts=["cohort1"]) + + campaign_configs = [ + ( + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_rules=[ + rule_builder.DetainedEstateSuppressionRuleFactory.build() + ], + ) + ], + ) + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + # When + actual = calculator.evaluate_eligibility() + # Then + expected_actions=[] + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_actionable)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) From 2e8b7b5f28d4df5f8871485575de42c64ab2a679 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:47:50 +0100 Subject: [PATCH 09/19] WIP: flaky tests. --- .../test_eligibility_calculator.py | 172 +++++++++--------- 1 file changed, 87 insertions(+), 85 deletions(-) diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 53c416f5e..ba04a1850 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2494,49 +2494,50 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR ), ) -def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_given( # noqa: PLR0913 - faker: Faker, -): - # ELI-295 Campaign config without NonEligibleActions (X rules) should not return any actions/default actions for NonEligible status - - # Given - nhs_number = NHSNumber(faker.nhs_number()) - - person_rows = person_rows_builder(nhs_number, cohorts=["NotEligibleCohort"]) - - campaign_configs = [ - ( - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - actions_mapper=[], - iteration_rules=[] - ) - ], - ) - ) - ] - calculator = EligibilityCalculator(person_rows, campaign_configs) - - # When - actual = calculator.evaluate_eligibility() - - # Then - expected_actions=[] - assert_that( - actual, - is_eligibility_status().with_conditions( - has_items( - is_condition() - .with_condition_name(ConditionName("RSV")) - .and_status(equal_to(Status.not_eligible)) - .and_actions(equal_to(expected_actions)) - ) - ), - ) +# def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_given( # noqa: PLR0913 +# faker: Faker, +# ): +# # ELI-295 Campaign config without NonEligibleActions (X rules) should not return any actions/default actions for NonEligible status +# +# # Given +# nhs_number = NHSNumber(faker.nhs_number()) +# +# person_rows = person_rows_builder(nhs_number, cohorts=["NotEligibleCohort"]) +# +# campaign_configs = [ +# ( +# rule_builder.CampaignConfigFactory.build( +# target="RSV", +# iterations=[ +# rule_builder.IterationFactory.build( +# iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], +# actions_mapper=[], +# iteration_rules=[] +# ) +# ], +# ) +# ) +# ] +# +# calculator = EligibilityCalculator(person_rows, campaign_configs) +# +# # When +# actual = calculator.evaluate_eligibility() +# +# # Then +# expected_actions=[] +# assert_that( +# actual, +# is_eligibility_status().with_conditions( +# has_items( +# is_condition() +# .with_condition_name(ConditionName("RSV")) +# .and_status(equal_to(Status.not_eligible)) +# .and_actions(equal_to(expected_actions)) +# ) +# ), +# ) @pytest.mark.parametrize( @@ -2661,47 +2662,48 @@ def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: P ), ) -def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_given( # noqa: PLR0913 - faker: Faker, -): - # ELI-295 Campaign config without NonActionableActions (Y rules) should not return any actions/default actions for NonActionable status - # Given - nhs_number = NHSNumber(faker.nhs_number()) - - person_rows = person_rows_builder(nhs_number, cohorts=["cohort1"]) - - campaign_configs = [ - ( - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_rules=[ - rule_builder.DetainedEstateSuppressionRuleFactory.build() - ], - ) - ], - ) - ) - ] - - calculator = EligibilityCalculator(person_rows, campaign_configs) - - # When - actual = calculator.evaluate_eligibility() - - # Then - expected_actions=[] - assert_that( - actual, - is_eligibility_status().with_conditions( - has_items( - is_condition() - .with_condition_name(ConditionName("RSV")) - .and_status(equal_to(Status.not_actionable)) - .and_actions(equal_to(expected_actions)) - ) - ), - ) +# def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_given( # noqa: PLR0913 +# faker: Faker, +# ): +# # ELI-295 Campaign config without NonActionableActions (Y rules) should not return any actions/default actions for NonActionable status +# +# # Given +# nhs_number = NHSNumber(faker.nhs_number()) +# +# person_rows = person_rows_builder(nhs_number, cohorts=["cohort1"]) +# +# campaign_configs = [ +# ( +# rule_builder.CampaignConfigFactory.build( +# target="RSV", +# iterations=[ +# rule_builder.IterationFactory.build( +# iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], +# iteration_rules=[ +# rule_builder.DetainedEstateSuppressionRuleFactory.build() +# ], +# ) +# ], +# ) +# ) +# ] +# +# calculator = EligibilityCalculator(person_rows, campaign_configs) +# +# # When +# actual = calculator.evaluate_eligibility() +# +# # Then +# expected_actions=[] +# assert_that( +# actual, +# is_eligibility_status().with_conditions( +# has_items( +# is_condition() +# .with_condition_name(ConditionName("RSV")) +# .and_status(equal_to(Status.not_actionable)) +# .and_actions(equal_to(expected_actions)) +# ) +# ), +# ) From 950f6a61b8fc23df921dad4485031e93bcf280b4 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:16:24 +0100 Subject: [PATCH 10/19] WIP: Fixed failing tests for empty actions. --- .../test_eligibility_calculator.py | 175 +++++++++--------- 1 file changed, 88 insertions(+), 87 deletions(-) diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index ba04a1850..578ae90ca 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2495,49 +2495,49 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR ) -# def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_given( # noqa: PLR0913 -# faker: Faker, -# ): -# # ELI-295 Campaign config without NonEligibleActions (X rules) should not return any actions/default actions for NonEligible status -# -# # Given -# nhs_number = NHSNumber(faker.nhs_number()) -# -# person_rows = person_rows_builder(nhs_number, cohorts=["NotEligibleCohort"]) -# -# campaign_configs = [ -# ( -# rule_builder.CampaignConfigFactory.build( -# target="RSV", -# iterations=[ -# rule_builder.IterationFactory.build( -# iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], -# actions_mapper=[], -# iteration_rules=[] -# ) -# ], -# ) -# ) -# ] -# -# calculator = EligibilityCalculator(person_rows, campaign_configs) -# -# # When -# actual = calculator.evaluate_eligibility() -# -# # Then -# expected_actions=[] -# assert_that( -# actual, -# is_eligibility_status().with_conditions( -# has_items( -# is_condition() -# .with_condition_name(ConditionName("RSV")) -# .and_status(equal_to(Status.not_eligible)) -# .and_actions(equal_to(expected_actions)) -# ) -# ), -# ) +def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_given( # noqa: PLR0913 + faker: Faker, +): + # ELI-295 Campaign config without NonEligibleActions (X rules) should not return any actions/default actions for NonEligible status + + # Given + nhs_number = NHSNumber(faker.nhs_number()) + + person_rows = person_rows_builder(nhs_number, cohorts=["NotEligibleCohort"]) + + campaign_configs = [ + ( + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + actions_mapper={}, + iteration_rules=[] + ) + ], + ) + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + expected_actions=[] + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_eligible)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) @pytest.mark.parametrize( @@ -2663,47 +2663,48 @@ def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: P ) -# def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_given( # noqa: PLR0913 -# faker: Faker, -# ): -# # ELI-295 Campaign config without NonActionableActions (Y rules) should not return any actions/default actions for NonActionable status -# -# # Given -# nhs_number = NHSNumber(faker.nhs_number()) -# -# person_rows = person_rows_builder(nhs_number, cohorts=["cohort1"]) -# -# campaign_configs = [ -# ( -# rule_builder.CampaignConfigFactory.build( -# target="RSV", -# iterations=[ -# rule_builder.IterationFactory.build( -# iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], -# iteration_rules=[ -# rule_builder.DetainedEstateSuppressionRuleFactory.build() -# ], -# ) -# ], -# ) -# ) -# ] -# -# calculator = EligibilityCalculator(person_rows, campaign_configs) -# -# # When -# actual = calculator.evaluate_eligibility() -# -# # Then -# expected_actions=[] -# assert_that( -# actual, -# is_eligibility_status().with_conditions( -# has_items( -# is_condition() -# .with_condition_name(ConditionName("RSV")) -# .and_status(equal_to(Status.not_actionable)) -# .and_actions(equal_to(expected_actions)) -# ) -# ), -# ) +def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_given( # noqa: PLR0913 + faker: Faker, +): + # ELI-295 Campaign config without NonActionableActions (Y rules) should not return any actions/default actions for NonActionable status + + # Given + nhs_number = NHSNumber(faker.nhs_number()) + + person_rows = person_rows_builder(nhs_number, cohorts=["cohort1"], de=True) + + campaign_configs = [ + ( + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + actions_mapper={}, + iteration_rules=[ + rule_builder.DetainedEstateSuppressionRuleFactory.build() + ], + ) + ], + ) + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + expected_actions=[] + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_actionable)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) From af6fc3d08791de431f0005a97b3b4c402c3f9343 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:25:07 +0100 Subject: [PATCH 11/19] WIP: added audit record check to tests. --- .../test_eligibility_calculator.py | 215 +++++++++++++----- 1 file changed, 155 insertions(+), 60 deletions(-) diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 578ae90ca..294a303c1 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -7,6 +7,7 @@ from hamcrest import assert_that, contains_exactly, contains_inanyorder, equal_to, has_item, has_items, is_in from pydantic import HttpUrl, ValidationError +from eligibility_signposting_api.audit.audit_models import AuditEvent, AuditAction from eligibility_signposting_api.model import rules from eligibility_signposting_api.model import rules as rules_model from eligibility_signposting_api.model.eligibility import ( @@ -35,7 +36,11 @@ is_reason, ) from tests.fixtures.matchers.rules import is_iteration_rule +from flask import g, Flask +@pytest.fixture +def app(): + return Flask(__name__) class TestEligibilityCalculator: @staticmethod @@ -2376,7 +2381,7 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat ) @pytest.mark.parametrize( - ("test_comment", "person_icb", "default_comms_routing", "comms_routing", "actions_mapper", "expected_actions"), + ("test_comment", "person_icb", "default_comms_routing", "comms_routing", "actions_mapper", "expected_actions", "expected_audit_actions"), [ ( """Not eligible person with matching NonEligibleActionRule""", @@ -2400,6 +2405,16 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat url_label=None, ) ], + [ + AuditAction( + internal_action_code="ActionCode1", + action_code="HealthcareProInfo", + action_type="InfoText", + action_description="Speak to your healthcare professional if you think you should be offered this vaccination.", + action_url=None, + action_url_label=None, + ) + ] ), ( """Not eligible person with NON matching NonEligibleActionRule""", @@ -2423,6 +2438,16 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat url_label=None, ) ], + [ + AuditAction( + internal_action_code="defaultCommsCode", + action_code="DefaultHealthcareProInfo", + action_type="DefaultInfoText", + action_description="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + action_url=None, + action_url_label=None, + ) + ] ), ( """Not eligible person with matching but missing NonEligibleActionRule, fall back to default comms""", @@ -2447,11 +2472,21 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat url_label=None, ) ], + [ + AuditAction( + internal_action_code="defaultCommsCode", + action_code="DefaultHealthcareProInfo", + action_type="DefaultInfoText", + action_description="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + action_url=None, + action_url_label=None, + ) + ] ) ] ) def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR0913 -test_comment, person_icb, default_comms_routing, comms_routing, actions_mapper, expected_actions, +app, test_comment, person_icb, default_comms_routing, comms_routing, actions_mapper, expected_actions, expected_audit_actions, faker: Faker, ): # Given @@ -2479,24 +2514,29 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR calculator = EligibilityCalculator(person_rows, campaign_configs) # When - actual = calculator.evaluate_eligibility() + with app.app_context(): + g.audit_log = AuditEvent() + + actual = calculator.evaluate_eligibility() + + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_eligible)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_items( - is_condition() - .with_condition_name(ConditionName("RSV")) - .and_status(equal_to(Status.not_eligible)) - .and_actions(equal_to(expected_actions)) - ) - ), - ) + cond = g.audit_log.response.condition[0] + assert cond.actions == expected_audit_actions def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_given( # noqa: PLR0913 - faker: Faker, + app, faker: Faker, ): # ELI-295 Campaign config without NonEligibleActions (X rules) should not return any actions/default actions for NonEligible status @@ -2523,25 +2563,33 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give calculator = EligibilityCalculator(person_rows, campaign_configs) # When - actual = calculator.evaluate_eligibility() + with app.app_context(): + g.audit_log = AuditEvent() + + actual = calculator.evaluate_eligibility() + + # Then + expected_actions=[] + expected_audit_action=[] + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_eligible)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) + + cond = g.audit_log.response.condition[0] + assert cond.actions == expected_audit_action - # Then - expected_actions=[] - assert_that( - actual, - is_eligibility_status().with_conditions( - has_items( - is_condition() - .with_condition_name(ConditionName("RSV")) - .and_status(equal_to(Status.not_eligible)) - .and_actions(equal_to(expected_actions)) - ) - ), - ) @pytest.mark.parametrize( - ("test_comment", "person_icb", "default_comms_routing", "comms_routing", "actions_mapper", "expected_actions"), + ("test_comment", "person_icb", "default_comms_routing", "comms_routing", "actions_mapper", "expected_actions", "expected_audit_actions"), [ ( """Not actionable person with matching NonActionableActionRule""", @@ -2565,6 +2613,16 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give url_label=None, ) ], + [ + AuditAction( + internal_action_code="ActionCode1", + action_code="HealthcareProInfo", + action_type="InfoText", + action_description="Speak to your healthcare professional if you think you should be offered this vaccination.", + action_url=None, + action_url_label=None, + ) + ] ), ( """Not actionable person with NON matching NonActionableActionRule""", @@ -2588,6 +2646,16 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give url_label=None, ) ], + [ + AuditAction( + internal_action_code="defaultCommsCode", + action_code="DefaultHealthcareProInfo", + action_type="DefaultInfoText", + action_description="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + action_url=None, + action_url_label=None, + ) + ] ), ( """Not actionable person with matching but missing NonActionableActionRule, fall back to default comms""", @@ -2612,11 +2680,21 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give url_label=None, ) ], + [ + AuditAction( + internal_action_code="defaultCommsCode", + action_code="DefaultHealthcareProInfo", + action_type="DefaultInfoText", + action_description="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + action_url=None, + action_url_label=None, + ) + ] ) ] ) def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: PLR0913 -test_comment, person_icb, default_comms_routing, comms_routing, actions_mapper, expected_actions, +app, test_comment, person_icb, default_comms_routing, comms_routing, actions_mapper, expected_actions, expected_audit_actions, faker: Faker, ): # Given @@ -2647,24 +2725,31 @@ def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: P calculator = EligibilityCalculator(person_rows, campaign_configs) # When - actual = calculator.evaluate_eligibility() + with app.app_context(): + g.audit_log = AuditEvent() + + actual = calculator.evaluate_eligibility() + + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_actionable)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) + + cond = g.audit_log.response.condition[0] + assert cond.actions == expected_audit_actions + - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_items( - is_condition() - .with_condition_name(ConditionName("RSV")) - .and_status(equal_to(Status.not_actionable)) - .and_actions(equal_to(expected_actions)) - ) - ), - ) def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_given( # noqa: PLR0913 - faker: Faker, + app, faker: Faker, ): # ELI-295 Campaign config without NonActionableActions (Y rules) should not return any actions/default actions for NonActionable status @@ -2693,18 +2778,28 @@ def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_gi calculator = EligibilityCalculator(person_rows, campaign_configs) # When - actual = calculator.evaluate_eligibility() + with app.app_context(): + g.audit_log = AuditEvent() + + actual = calculator.evaluate_eligibility() + + # Then + expected_actions=[] + expected_audit_action=[] + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_actionable)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) + + cond = g.audit_log.response.condition[0] + assert cond.actions == expected_audit_action + + - # Then - expected_actions=[] - assert_that( - actual, - is_eligibility_status().with_conditions( - has_items( - is_condition() - .with_condition_name(ConditionName("RSV")) - .and_status(equal_to(Status.not_actionable)) - .and_actions(equal_to(expected_actions)) - ) - ), - ) From 41d99f0d4c1ca68ea7998dc0a0bae118c2e9e354 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:53:05 +0100 Subject: [PATCH 12/19] WIP: file format and added audit rule priority and name test. --- .../audit/audit_context.py | 1 + .../model/eligibility.py | 1 + .../calculators/eligibility_calculator.py | 2 - tests/fixtures/builders/model/rule.py | 2 + .../lambda/test_app_running_as_lambda.py | 4 +- .../test_eligibility_calculator.py | 180 +++++++++++------- 6 files changed, 117 insertions(+), 73 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index c2de05db2..dea27d564 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -106,6 +106,7 @@ def append_audit_condition( rule_priority=str(redirect_rule_details[0]), rule_name=redirect_rule_details[1] ) + if suggested_actions is None: audit_actions = None elif len(suggested_actions) > 0: diff --git a/src/eligibility_signposting_api/model/eligibility.py b/src/eligibility_signposting_api/model/eligibility.py index 815c572ef..3dedeb4bf 100644 --- a/src/eligibility_signposting_api/model/eligibility.py +++ b/src/eligibility_signposting_api/model/eligibility.py @@ -32,6 +32,7 @@ class RuleType(StrEnum): not_eligible_actions = "X" not_actionable_actions = "Y" + @total_ordering class Status(Enum): not_eligible = auto() diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index 158e92645..a6e5f1ab2 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -232,11 +232,9 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil else: actions = None - if best_candidate.status in (Status.not_eligible, Status.not_actionable) and not include_actions_flag: actions = None - # add actions to condition results condition_results[condition_name].actions = actions # reset actions for the next condition diff --git a/tests/fixtures/builders/model/rule.py b/tests/fixtures/builders/model/rule.py index bd1c5e4b0..f0d7010b4 100644 --- a/tests/fixtures/builders/model/rule.py +++ b/tests/fixtures/builders/model/rule.py @@ -174,6 +174,7 @@ class ICBRedirectRuleFactory(IterationRuleFactory): comparator = rules.RuleComparator("QE1") comms_routing = rules.CommsRouting("ActionCode1") + class ICBNonEligibleActionRuleFactory(IterationRuleFactory): type = rules.RuleType.not_eligible_actions name = rules.RuleName("In QE1") @@ -185,6 +186,7 @@ class ICBNonEligibleActionRuleFactory(IterationRuleFactory): comparator = rules.RuleComparator("QE1") comms_routing = rules.CommsRouting("ActionCode1") + class ICBNonActionableActionRuleFactory(IterationRuleFactory): type = rules.RuleType.not_actionable_actions name = rules.RuleName("In QE1") diff --git a/tests/integration/lambda/test_app_running_as_lambda.py b/tests/integration/lambda/test_app_running_as_lambda.py index cb30e0ea7..3abaad124 100644 --- a/tests/integration/lambda/test_app_running_as_lambda.py +++ b/tests/integration/lambda/test_app_running_as_lambda.py @@ -318,7 +318,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationId": rsv_campaign.iterations[0].id, "iterationVersion": rsv_campaign.iterations[0].version, "conditionName": rsv_campaign.target, - "status": "not_eligible", #TODO is not_eligible status with no actions valid (ELI-295) + "status": "not_eligible", # TODO is not_eligible status with no actions valid (ELI-295) "statusText": "not_eligible", "eligibilityCohorts": [{"cohortCode": "cohort_group1", "cohortStatus": "not_eligible"}], "eligibilityCohortGroups": [ @@ -339,7 +339,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationId": covid_campaign.iterations[0].id, "iterationVersion": covid_campaign.iterations[0].version, "conditionName": covid_campaign.target, - "status": "not_actionable", #TODO is not_actionable status with no actions valid (ELI-295) + "status": "not_actionable", # TODO is not_actionable status with no actions valid (ELI-295) "statusText": "not_actionable", "eligibilityCohorts": [{"cohortCode": "cohort_group2", "cohortStatus": "not_actionable"}], "eligibilityCohortGroups": [ diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 294a303c1..d9957c819 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -3,11 +3,12 @@ import pytest from faker import Faker +from flask import Flask, g from freezegun import freeze_time from hamcrest import assert_that, contains_exactly, contains_inanyorder, equal_to, has_item, has_items, is_in from pydantic import HttpUrl, ValidationError -from eligibility_signposting_api.audit.audit_models import AuditEvent, AuditAction +from eligibility_signposting_api.audit.audit_models import AuditAction, AuditEvent from eligibility_signposting_api.model import rules from eligibility_signposting_api.model import rules as rules_model from eligibility_signposting_api.model.eligibility import ( @@ -36,12 +37,13 @@ is_reason, ) from tests.fixtures.matchers.rules import is_iteration_rule -from flask import g, Flask + @pytest.fixture def app(): return Flask(__name__) + class TestEligibilityCalculator: @staticmethod def test_get_action_rules_components(): @@ -2380,27 +2382,38 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat ), ) + @pytest.mark.parametrize( - ("test_comment", "person_icb", "default_comms_routing", "comms_routing", "actions_mapper", "expected_actions", "expected_audit_actions"), + ( + "test_comment", + "person_icb", + "default_comms_routing", + "comms_routing", + "actions_mapper", + "expected_actions", + "expected_audit_actions", + ), [ ( """Not eligible person with matching NonEligibleActionRule""", "QE1", "", "ActionCode1", - {"ActionCode1": - AvailableAction( - ActionType="InfoText", - ExternalRoutingCode="HealthcareProInfo", - ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", - )}, + { + "ActionCode1": AvailableAction( + ActionType="InfoText", + ExternalRoutingCode="HealthcareProInfo", + ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", + ) + }, [ SuggestedAction( internal_action_code=InternalActionCode("ActionCode1"), action_type=ActionType("InfoText"), action_code=ActionCode("HealthcareProInfo"), action_description=ActionDescription( - "Speak to your healthcare professional if you think you should be offered this vaccination."), + "Speak to your healthcare professional if you think you should be offered this vaccination." + ), url_link=None, url_label=None, ) @@ -2414,26 +2427,28 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_url=None, action_url_label=None, ) - ] + ], ), ( """Not eligible person with NON matching NonEligibleActionRule""", "WS3", "defaultCommsCode", "ActionCode1", - {"defaultCommsCode": - AvailableAction( + { + "defaultCommsCode": AvailableAction( ActionType="DefaultInfoText", ExternalRoutingCode="DefaultHealthcareProInfo", ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", - )}, + ) + }, [ SuggestedAction( internal_action_code=InternalActionCode("defaultCommsCode"), action_type=ActionType("DefaultInfoText"), action_code=ActionCode("DefaultHealthcareProInfo"), action_description=ActionDescription( - "Default Speak to your healthcare professional if you think you should be offered this vaccination."), + "Default Speak to your healthcare professional if you think you should be offered this vaccination." + ), url_link=None, url_label=None, ) @@ -2447,15 +2462,15 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_url=None, action_url_label=None, ) - ] + ], ), ( """Not eligible person with matching but missing NonEligibleActionRule, fall back to default comms""", "QE1", "defaultCommsCode", "ActionCode1", - {"defaultCommsCode": - AvailableAction( + { + "defaultCommsCode": AvailableAction( ActionType="DefaultInfoText", ExternalRoutingCode="DefaultHealthcareProInfo", ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", @@ -2467,7 +2482,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_type=ActionType("DefaultInfoText"), action_code=ActionCode("DefaultHealthcareProInfo"), action_description=ActionDescription( - "Default Speak to your healthcare professional if you think you should be offered this vaccination."), + "Default Speak to your healthcare professional if you think you should be offered this vaccination." + ), url_link=None, url_label=None, ) @@ -2481,12 +2497,19 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_url=None, action_url_label=None, ) - ] - ) - ] + ], + ), + ], ) def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR0913 -app, test_comment, person_icb, default_comms_routing, comms_routing, actions_mapper, expected_actions, expected_audit_actions, + app, + test_comment, + person_icb, + default_comms_routing, + comms_routing, + actions_mapper, + expected_actions, + expected_audit_actions, faker: Faker, ): # Given @@ -2502,9 +2525,10 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR rule_builder.IterationFactory.build( iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], default_not_eligible_routing=default_comms_routing, - actions_mapper=rule_builder.ActionsMapperFactory.build( - root=actions_mapper), - iteration_rules=[rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=comms_routing)], + actions_mapper=rule_builder.ActionsMapperFactory.build(root=actions_mapper), + iteration_rules=[ + rule_builder.ICBNonEligibleActionRuleFactory.build(comms_routing=comms_routing) + ], ) ], ) @@ -2529,14 +2553,20 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR .and_actions(equal_to(expected_actions)) ) ), + test_comment ) cond = g.audit_log.response.condition[0] assert cond.actions == expected_audit_actions + assert cond.action_rule.rule_priority == str(campaign_configs[0].iterations[0].iteration_rules[0].priority) + assert cond.action_rule.rule_name == str(campaign_configs[0].iterations[0].iteration_rules[0].name) + -def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_given( # noqa: PLR0913 - app, faker: Faker, + +def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_given( + app, + faker: Faker, ): # ELI-295 Campaign config without NonEligibleActions (X rules) should not return any actions/default actions for NonEligible status @@ -2553,7 +2583,7 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give rule_builder.IterationFactory.build( iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], actions_mapper={}, - iteration_rules=[] + iteration_rules=[], ) ], ) @@ -2569,8 +2599,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give actual = calculator.evaluate_eligibility() # Then - expected_actions=[] - expected_audit_action=[] + expected_actions = [] + expected_audit_action = [] assert_that( actual, is_eligibility_status().with_conditions( @@ -2587,28 +2617,37 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give assert cond.actions == expected_audit_action - @pytest.mark.parametrize( - ("test_comment", "person_icb", "default_comms_routing", "comms_routing", "actions_mapper", "expected_actions", "expected_audit_actions"), + ( + "test_comment", + "person_icb", + "default_comms_routing", + "comms_routing", + "actions_mapper", + "expected_actions", + "expected_audit_actions", + ), [ ( """Not actionable person with matching NonActionableActionRule""", "QE1", "", "ActionCode1", - {"ActionCode1": - AvailableAction( - ActionType="InfoText", - ExternalRoutingCode="HealthcareProInfo", - ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", - )}, + { + "ActionCode1": AvailableAction( + ActionType="InfoText", + ExternalRoutingCode="HealthcareProInfo", + ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", + ) + }, [ SuggestedAction( internal_action_code=InternalActionCode("ActionCode1"), action_type=ActionType("InfoText"), action_code=ActionCode("HealthcareProInfo"), action_description=ActionDescription( - "Speak to your healthcare professional if you think you should be offered this vaccination."), + "Speak to your healthcare professional if you think you should be offered this vaccination." + ), url_link=None, url_label=None, ) @@ -2622,26 +2661,28 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give action_url=None, action_url_label=None, ) - ] + ], ), ( """Not actionable person with NON matching NonActionableActionRule""", "WS3", "defaultCommsCode", "ActionCode1", - {"defaultCommsCode": - AvailableAction( + { + "defaultCommsCode": AvailableAction( ActionType="DefaultInfoText", ExternalRoutingCode="DefaultHealthcareProInfo", ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", - )}, + ) + }, [ SuggestedAction( internal_action_code=InternalActionCode("defaultCommsCode"), action_type=ActionType("DefaultInfoText"), action_code=ActionCode("DefaultHealthcareProInfo"), action_description=ActionDescription( - "Default Speak to your healthcare professional if you think you should be offered this vaccination."), + "Default Speak to your healthcare professional if you think you should be offered this vaccination." + ), url_link=None, url_label=None, ) @@ -2655,15 +2696,15 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give action_url=None, action_url_label=None, ) - ] + ], ), ( """Not actionable person with matching but missing NonActionableActionRule, fall back to default comms""", "QE1", "defaultCommsCode", "ActionCode1", - {"defaultCommsCode": - AvailableAction( + { + "defaultCommsCode": AvailableAction( ActionType="DefaultInfoText", ExternalRoutingCode="DefaultHealthcareProInfo", ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", @@ -2675,7 +2716,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give action_type=ActionType("DefaultInfoText"), action_code=ActionCode("DefaultHealthcareProInfo"), action_description=ActionDescription( - "Default Speak to your healthcare professional if you think you should be offered this vaccination."), + "Default Speak to your healthcare professional if you think you should be offered this vaccination." + ), url_link=None, url_label=None, ) @@ -2689,12 +2731,19 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give action_url=None, action_url_label=None, ) - ] - ) - ] + ], + ), + ], ) def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: PLR0913 -app, test_comment, person_icb, default_comms_routing, comms_routing, actions_mapper, expected_actions, expected_audit_actions, + app, + test_comment, + person_icb, + default_comms_routing, + comms_routing, + actions_mapper, + expected_actions, + expected_audit_actions, faker: Faker, ): # Given @@ -2710,12 +2759,11 @@ def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: P rule_builder.IterationFactory.build( iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], default_not_actionable_routing=default_comms_routing, - actions_mapper=rule_builder.ActionsMapperFactory.build( - root=actions_mapper), + actions_mapper=rule_builder.ActionsMapperFactory.build(root=actions_mapper), iteration_rules=[ rule_builder.DetainedEstateSuppressionRuleFactory.build(), - rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=comms_routing) - ], + rule_builder.ICBNonActionableActionRuleFactory.build(comms_routing=comms_routing), + ], ) ], ) @@ -2746,10 +2794,9 @@ def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: P assert cond.actions == expected_audit_actions - - -def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_given( # noqa: PLR0913 - app, faker: Faker, +def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_given( + app, + faker: Faker, ): # ELI-295 Campaign config without NonActionableActions (Y rules) should not return any actions/default actions for NonActionable status @@ -2766,9 +2813,7 @@ def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_gi rule_builder.IterationFactory.build( iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], actions_mapper={}, - iteration_rules=[ - rule_builder.DetainedEstateSuppressionRuleFactory.build() - ], + iteration_rules=[rule_builder.DetainedEstateSuppressionRuleFactory.build()], ) ], ) @@ -2784,8 +2829,8 @@ def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_gi actual = calculator.evaluate_eligibility() # Then - expected_actions=[] - expected_audit_action=[] + expected_actions = [] + expected_audit_action = [] assert_that( actual, is_eligibility_status().with_conditions( @@ -2800,6 +2845,3 @@ def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_gi cond = g.audit_log.response.condition[0] assert cond.actions == expected_audit_action - - - From 1b253a1628e4c74c0255a3a4d260eed197469d33 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:10:32 +0100 Subject: [PATCH 13/19] Working tests. Refactored some audit logic. --- .../audit/audit_context.py | 43 +++++++++++++++---- .../test_eligibility_calculator.py | 13 +++++- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index dea27d564..33a5bad6e 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -64,7 +64,7 @@ def append_audit_condition( condition_name: ConditionName, best_results: tuple[Iteration | None, IterationResult | None, dict[str, CohortGroupResult] | None], campaign_details: tuple[CampaignID | None, CampaignVersion | None], - redirect_rule_details: tuple[RulePriority | None, RuleName | None], + action_rule_details: tuple[RulePriority | None, RuleName | None], ) -> None: audit_eligibility_cohorts, audit_eligibility_cohort_groups, audit_actions = [], [], [] audit_filter_rule, audit_suitability_rule, audit_redirect_rule = None, None, None @@ -99,12 +99,39 @@ def append_audit_condition( ) # TODO - # if actionflag - # if best_candidate and best_candidate.status and actionflag - 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] - ) + #if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: + #if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: + # audit_action_rule = AuditRedirectRule( + # rule_priority=str(action_rule_details[0]), + # #rule_priority=str(action_rule_details[0]) if action_rule_details[0] is not None else None, + # rule_name=action_rule_details[1] + # ) + # # if action_rule_details is not None else None + + if best_candidate and best_candidate.status: + if action_rule_details[0] is None and action_rule_details[0] is None: + audit_action_rule = None + else: + audit_action_rule = AuditRedirectRule( + rule_priority=str(action_rule_details[0]), + rule_name=action_rule_details[1] + ) + + # elif best_candidate and best_candidate.status and best_candidate.status.name == Status.not_actionable.name: + # if action_rule_details[0] is None and action_rule_details[0] is None: + # audit_action_rule = None + # else: + # audit_action_rule = AuditRedirectRule( + # rule_priority=str(action_rule_details[0]), + # rule_name=action_rule_details[1] + # ) + # elif best_candidate and best_candidate.status and best_candidate.status.name == Status.not_eligible.name: + # audit_action_rule = AuditRedirectRule( + # rule_priority=str(action_rule_details[0]), + # rule_name=action_rule_details[1] + # ) + # else: + # audit_action_rule = None if suggested_actions is None: @@ -134,7 +161,7 @@ def append_audit_condition( eligibility_cohort_groups=audit_eligibility_cohort_groups, filter_rules=audit_filter_rule, suitability_rules=audit_suitability_rule, - action_rule=audit_redirect_rule, + action_rule=audit_action_rule, actions=audit_actions, ) diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index d9957c819..36c7941b4 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2392,6 +2392,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat "actions_mapper", "expected_actions", "expected_audit_actions", + "expected_rule_priority", + "expected_rule_name" ), [ ( @@ -2428,6 +2430,7 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_url_label=None, ) ], + '20', "In QE1" ), ( """Not eligible person with NON matching NonEligibleActionRule""", @@ -2463,6 +2466,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_url_label=None, ) ], + None, + None ), ( """Not eligible person with matching but missing NonEligibleActionRule, fall back to default comms""", @@ -2498,6 +2503,7 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_url_label=None, ) ], + '20', "In QE1" ), ], ) @@ -2510,6 +2516,8 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR actions_mapper, expected_actions, expected_audit_actions, + expected_rule_priority, + expected_rule_name, faker: Faker, ): # Given @@ -2559,8 +2567,9 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR cond = g.audit_log.response.condition[0] assert cond.actions == expected_audit_actions - assert cond.action_rule.rule_priority == str(campaign_configs[0].iterations[0].iteration_rules[0].priority) - assert cond.action_rule.rule_name == str(campaign_configs[0].iterations[0].iteration_rules[0].name) + assert getattr(cond.action_rule, "rule_priority", None) == expected_rule_priority + assert getattr(cond.action_rule, "rule_name", None) == expected_rule_name + From c3effeebe2c08495d572f923d98097b609c8d4a2 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 14 Jul 2025 16:46:55 +0100 Subject: [PATCH 14/19] Minor refactor --- .../audit/audit_context.py | 20 ++++++------ .../calculators/eligibility_calculator.py | 32 +++++-------------- .../test_eligibility_calculator.py | 14 ++++---- 3 files changed, 24 insertions(+), 42 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index 33a5bad6e..2208481cb 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -99,22 +99,21 @@ def append_audit_condition( ) # TODO - #if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: - #if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: - # audit_action_rule = AuditRedirectRule( - # rule_priority=str(action_rule_details[0]), - # #rule_priority=str(action_rule_details[0]) if action_rule_details[0] is not None else None, - # rule_name=action_rule_details[1] - # ) - # # if action_rule_details is not None else None + # if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: + # if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: + # audit_action_rule = AuditRedirectRule( + # rule_priority=str(action_rule_details[0]), + # #rule_priority=str(action_rule_details[0]) if action_rule_details[0] is not None else None, + # rule_name=action_rule_details[1] + # ) + # # if action_rule_details is not None else None if best_candidate and best_candidate.status: if action_rule_details[0] is None and action_rule_details[0] is None: audit_action_rule = None else: audit_action_rule = AuditRedirectRule( - rule_priority=str(action_rule_details[0]), - rule_name=action_rule_details[1] + rule_priority=str(action_rule_details[0]), rule_name=action_rule_details[1] ) # elif best_candidate and best_candidate.status and best_candidate.status.name == Status.not_actionable.name: @@ -133,7 +132,6 @@ def append_audit_condition( # else: # audit_action_rule = None - if suggested_actions is None: audit_actions = None elif len(suggested_actions) > 0: diff --git a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py index a6e5f1ab2..e069d39b8 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -196,33 +196,17 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil condition_results[condition_name] = best_candidate - # Redirect action rules - if best_candidate.status == Status.actionable and best_active_iteration is not None: - if include_actions_flag: - actions, matched_action_rule_priority, matched_action_rule_name = self.handle_action_rules( - best_active_iteration, rules.RuleType.redirect - ) - action_rule_name = matched_action_rule_name - action_rule_priority = matched_action_rule_priority - else: - actions = None - - # Not Eligible action rules (Xrules) - elif best_candidate.status == Status.not_eligible and best_active_iteration is not None: - if include_actions_flag: - actions, matched_action_rule_priority, matched_action_rule_name = self.handle_action_rules( - best_active_iteration, rules.RuleType.not_eligible_actions - ) - action_rule_name = matched_action_rule_name - action_rule_priority = matched_action_rule_priority - else: - actions = None + status_to_rule_type = { + Status.actionable: rules.RuleType.redirect, + Status.not_eligible: rules.RuleType.not_eligible_actions, + Status.not_actionable: rules.RuleType.not_actionable_actions, + } - # Not Actionable action rules (Yrules) - elif best_candidate.status == Status.not_actionable and best_active_iteration is not None: + if best_candidate.status in status_to_rule_type and best_active_iteration is not None: if include_actions_flag: + rule_type = status_to_rule_type[best_candidate.status] actions, matched_action_rule_priority, matched_action_rule_name = self.handle_action_rules( - best_active_iteration, rules.RuleType.not_actionable_actions + best_active_iteration, rule_type ) action_rule_name = matched_action_rule_name action_rule_priority = matched_action_rule_priority diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 36c7941b4..ac68548eb 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2393,7 +2393,7 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat "expected_actions", "expected_audit_actions", "expected_rule_priority", - "expected_rule_name" + "expected_rule_name", ), [ ( @@ -2430,7 +2430,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_url_label=None, ) ], - '20', "In QE1" + "20", + "In QE1", ), ( """Not eligible person with NON matching NonEligibleActionRule""", @@ -2467,7 +2468,7 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat ) ], None, - None + None, ), ( """Not eligible person with matching but missing NonEligibleActionRule, fall back to default comms""", @@ -2503,7 +2504,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_url_label=None, ) ], - '20', "In QE1" + "20", + "In QE1", ), ], ) @@ -2561,7 +2563,7 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR .and_actions(equal_to(expected_actions)) ) ), - test_comment + test_comment, ) cond = g.audit_log.response.condition[0] @@ -2571,8 +2573,6 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR assert getattr(cond.action_rule, "rule_name", None) == expected_rule_name - - def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_given( app, faker: Faker, From 125a330d31c60a768964777e91755f5761ba1f34 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 15 Jul 2025 16:13:49 +0100 Subject: [PATCH 15/19] Addressed linting issues --- .../audit/audit_context.py | 43 +++--------- .../calculators/eligibility_calculator.py | 9 +-- .../lambda/test_app_running_as_lambda.py | 4 +- .../test_eligibility_calculator.py | 65 +++++++++++++------ 4 files changed, 60 insertions(+), 61 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index 2208481cb..545570dd2 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -67,7 +67,7 @@ def append_audit_condition( action_rule_details: tuple[RulePriority | None, RuleName | None], ) -> None: audit_eligibility_cohorts, audit_eligibility_cohort_groups, audit_actions = [], [], [] - audit_filter_rule, audit_suitability_rule, audit_redirect_rule = None, None, None + audit_filter_rule, audit_suitability_rule, audit_action_rule = None, None, None best_active_iteration = best_results[0] best_candidate = best_results[1] best_cohort_results = best_results[2] @@ -98,39 +98,8 @@ def append_audit_condition( rule_message=value.audit_rules[0].rule_description, ) - # TODO - # if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: - # if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name: - # audit_action_rule = AuditRedirectRule( - # rule_priority=str(action_rule_details[0]), - # #rule_priority=str(action_rule_details[0]) if action_rule_details[0] is not None else None, - # rule_name=action_rule_details[1] - # ) - # # if action_rule_details is not None else None - if best_candidate and best_candidate.status: - if action_rule_details[0] is None and action_rule_details[0] is None: - audit_action_rule = None - else: - audit_action_rule = AuditRedirectRule( - rule_priority=str(action_rule_details[0]), rule_name=action_rule_details[1] - ) - - # elif best_candidate and best_candidate.status and best_candidate.status.name == Status.not_actionable.name: - # if action_rule_details[0] is None and action_rule_details[0] is None: - # audit_action_rule = None - # else: - # audit_action_rule = AuditRedirectRule( - # rule_priority=str(action_rule_details[0]), - # rule_name=action_rule_details[1] - # ) - # elif best_candidate and best_candidate.status and best_candidate.status.name == Status.not_eligible.name: - # audit_action_rule = AuditRedirectRule( - # rule_priority=str(action_rule_details[0]), - # rule_name=action_rule_details[1] - # ) - # else: - # audit_action_rule = None + audit_action_rule = AuditContext.add_rule_name_and_priority_to_audit(action_rule_details) if suggested_actions is None: audit_actions = None @@ -165,6 +134,14 @@ def append_audit_condition( g.audit_log.response.condition.append(audit_condition) + @staticmethod + def add_rule_name_and_priority_to_audit( + action_rule_details: tuple[RulePriority | None, RuleName | None] | None, + ) -> AuditRedirectRule | None: + if action_rule_details is None or (action_rule_details[0] is None and action_rule_details[1] is None): + return None + return AuditRedirectRule(rule_priority=str(action_rule_details[0]), rule_name=action_rule_details[1]) + @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 e069d39b8..fce483e03 100644 --- a/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py +++ b/src/eligibility_signposting_api/services/calculators/eligibility_calculator.py @@ -127,11 +127,10 @@ def get_rules_by_type( ) return filter_rules, suppression_rules - # TODO rename function get_action_rules_action_mapper(active_iteration, rules.RuleType) @staticmethod def get_action_rules_components( active_iteration: Iteration, rule_type: RuleType - ) -> tuple[tuple[rules.IterationRule, ...], ActionsMapper, str]: + ) -> tuple[tuple[rules.IterationRule, ...], ActionsMapper, str | None]: action_rules = tuple(rule for rule in active_iteration.iteration_rules if rule.type in rule_type) routing_map = { @@ -233,9 +232,6 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil (action_rule_priority, action_rule_name), ) - # TODO check if need reset - # action_rule_priority, action_rule_name = None, None - # Consolidate all the results and return final_result = self.build_condition_results(condition_results) return eligibility.EligibilityStatus(conditions=final_result) @@ -247,7 +243,8 @@ def handle_action_rules( priority_getter = attrgetter("priority") sorted_rules_by_priority = sorted(action_rules, key=priority_getter) - actions: list[SuggestedAction] | None = self.get_actions_from_comms(action_mapper, default_comms) + actions: list[SuggestedAction] | None = self.get_actions_from_comms(action_mapper, default_comms) # pyright: ignore[reportArgumentType] + matched_action_rule_priority, matched_action_rule_name = None, None for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter): rule_group_list = list(rule_group) diff --git a/tests/integration/lambda/test_app_running_as_lambda.py b/tests/integration/lambda/test_app_running_as_lambda.py index 3abaad124..8253e9a1a 100644 --- a/tests/integration/lambda/test_app_running_as_lambda.py +++ b/tests/integration/lambda/test_app_running_as_lambda.py @@ -318,7 +318,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationId": rsv_campaign.iterations[0].id, "iterationVersion": rsv_campaign.iterations[0].version, "conditionName": rsv_campaign.target, - "status": "not_eligible", # TODO is not_eligible status with no actions valid (ELI-295) + "status": "not_eligible", "statusText": "not_eligible", "eligibilityCohorts": [{"cohortCode": "cohort_group1", "cohortStatus": "not_eligible"}], "eligibilityCohortGroups": [ @@ -339,7 +339,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # "iterationId": covid_campaign.iterations[0].id, "iterationVersion": covid_campaign.iterations[0].version, "conditionName": covid_campaign.target, - "status": "not_actionable", # TODO is not_actionable status with no actions valid (ELI-295) + "status": "not_actionable", "statusText": "not_actionable", "eligibilityCohorts": [{"cohortCode": "cohort_group2", "cohortStatus": "not_actionable"}], "eligibilityCohortGroups": [ diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index ac68548eb..c52d01ff3 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2405,7 +2405,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat "ActionCode1": AvailableAction( ActionType="InfoText", ExternalRoutingCode="HealthcareProInfo", - ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", + ActionDescription="""Speak to your healthcare professional if you think + you should be offered this vaccination.""", ) }, [ @@ -2414,7 +2415,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_type=ActionType("InfoText"), action_code=ActionCode("HealthcareProInfo"), action_description=ActionDescription( - "Speak to your healthcare professional if you think you should be offered this vaccination." + """Speak to your healthcare professional if you think + you should be offered this vaccination.""" ), url_link=None, url_label=None, @@ -2425,7 +2427,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat internal_action_code="ActionCode1", action_code="HealthcareProInfo", action_type="InfoText", - action_description="Speak to your healthcare professional if you think you should be offered this vaccination.", + action_description="""Speak to your healthcare professional if you think + you should be offered this vaccination.""", action_url=None, action_url_label=None, ) @@ -2442,7 +2445,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat "defaultCommsCode": AvailableAction( ActionType="DefaultInfoText", ExternalRoutingCode="DefaultHealthcareProInfo", - ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + ActionDescription="""Default Speak to your healthcare professional if you think + you should be offered this vaccination.""", ) }, [ @@ -2451,7 +2455,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_type=ActionType("DefaultInfoText"), action_code=ActionCode("DefaultHealthcareProInfo"), action_description=ActionDescription( - "Default Speak to your healthcare professional if you think you should be offered this vaccination." + """Default Speak to your healthcare professional if you think + you should be offered this vaccination.""" ), url_link=None, url_label=None, @@ -2462,7 +2467,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat internal_action_code="defaultCommsCode", action_code="DefaultHealthcareProInfo", action_type="DefaultInfoText", - action_description="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + action_description="""Default Speak to your healthcare professional if you think + you should be offered this vaccination.""", action_url=None, action_url_label=None, ) @@ -2479,7 +2485,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat "defaultCommsCode": AvailableAction( ActionType="DefaultInfoText", ExternalRoutingCode="DefaultHealthcareProInfo", - ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + ActionDescription="""Default Speak to your healthcare professional if you think + you should be offered this vaccination.""", ) }, [ @@ -2488,7 +2495,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat action_type=ActionType("DefaultInfoText"), action_code=ActionCode("DefaultHealthcareProInfo"), action_description=ActionDescription( - "Default Speak to your healthcare professional if you think you should be offered this vaccination." + """Default Speak to your healthcare professional if you think + you should be offered this vaccination.""" ), url_link=None, url_label=None, @@ -2499,7 +2507,8 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat internal_action_code="defaultCommsCode", action_code="DefaultHealthcareProInfo", action_type="DefaultInfoText", - action_description="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + action_description="""Default Speak to your healthcare professional if you think + you should be offered this vaccination.""", action_url=None, action_url_label=None, ) @@ -2577,7 +2586,10 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give app, faker: Faker, ): - # ELI-295 Campaign config without NonEligibleActions (X rules) should not return any actions/default actions for NonEligible status + """ + ELI-295 - Campaign config without NonEligibleActions (X rules) should not return + any actions/default actions for NonEligible status + """ # Given nhs_number = NHSNumber(faker.nhs_number()) @@ -2646,7 +2658,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give "ActionCode1": AvailableAction( ActionType="InfoText", ExternalRoutingCode="HealthcareProInfo", - ActionDescription="Speak to your healthcare professional if you think you should be offered this vaccination.", + ActionDescription="""Speak to your healthcare professional if you think + you should be offered this vaccination.""", ) }, [ @@ -2655,7 +2668,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give action_type=ActionType("InfoText"), action_code=ActionCode("HealthcareProInfo"), action_description=ActionDescription( - "Speak to your healthcare professional if you think you should be offered this vaccination." + """Speak to your healthcare professional if you think + you should be offered this vaccination.""" ), url_link=None, url_label=None, @@ -2666,7 +2680,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give internal_action_code="ActionCode1", action_code="HealthcareProInfo", action_type="InfoText", - action_description="Speak to your healthcare professional if you think you should be offered this vaccination.", + action_description="""Speak to your healthcare professional if you think + you should be offered this vaccination.""", action_url=None, action_url_label=None, ) @@ -2681,7 +2696,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give "defaultCommsCode": AvailableAction( ActionType="DefaultInfoText", ExternalRoutingCode="DefaultHealthcareProInfo", - ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + ActionDescription="""Default Speak to your healthcare professional if you think + you should be offered this vaccination.""", ) }, [ @@ -2690,7 +2706,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give action_type=ActionType("DefaultInfoText"), action_code=ActionCode("DefaultHealthcareProInfo"), action_description=ActionDescription( - "Default Speak to your healthcare professional if you think you should be offered this vaccination." + """Default Speak to your healthcare professional if you think + you should be offered this vaccination.""" ), url_link=None, url_label=None, @@ -2701,7 +2718,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give internal_action_code="defaultCommsCode", action_code="DefaultHealthcareProInfo", action_type="DefaultInfoText", - action_description="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + action_description="""Default Speak to your healthcare professional if you think + you should be offered this vaccination.""", action_url=None, action_url_label=None, ) @@ -2716,7 +2734,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give "defaultCommsCode": AvailableAction( ActionType="DefaultInfoText", ExternalRoutingCode="DefaultHealthcareProInfo", - ActionDescription="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + ActionDescription="""Default Speak to your healthcare professional if you think + you should be offered this vaccination.""", ) }, [ @@ -2725,7 +2744,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give action_type=ActionType("DefaultInfoText"), action_code=ActionCode("DefaultHealthcareProInfo"), action_description=ActionDescription( - "Default Speak to your healthcare professional if you think you should be offered this vaccination." + """Default Speak to your healthcare professional if you think + you should be offered this vaccination.""" ), url_link=None, url_label=None, @@ -2736,7 +2756,8 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give internal_action_code="defaultCommsCode", action_code="DefaultHealthcareProInfo", action_type="DefaultInfoText", - action_description="Default Speak to your healthcare professional if you think you should be offered this vaccination.", + action_description="""Default Speak to your healthcare professional if you think + you should be offered this vaccination.""", action_url=None, action_url_label=None, ) @@ -2797,6 +2818,7 @@ def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: P .and_actions(equal_to(expected_actions)) ) ), + test_comment, ) cond = g.audit_log.response.condition[0] @@ -2807,7 +2829,10 @@ def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_gi app, faker: Faker, ): - # ELI-295 Campaign config without NonActionableActions (Y rules) should not return any actions/default actions for NonActionable status + """ + ELI-295 - Campaign config without NonActionableActions (Y rules) should not return + any actions/default actions for NonActionable status + """ # Given nhs_number = NHSNumber(faker.nhs_number()) From 0884a33c24cfb1ded32a50fb9feef9c9469e576a Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:20:18 +0100 Subject: [PATCH 16/19] WIP: fixed failing unit tests. --- src/eligibility_signposting_api/audit/audit_context.py | 3 ++- .../services/calculators/test_eligibility_calculator.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index 8d68d354c..d449f5310 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -119,7 +119,8 @@ def add_rule_name_and_priority_to_audit( if best_candidate and best_candidate.status: if action_rule_details is None or (action_rule_details[0] is None and action_rule_details[1] is None): audit_action_rule = None - audit_action_rule = AuditRedirectRule(rule_priority=str(action_rule_details[0]), rule_name=action_rule_details[1]) + else: + audit_action_rule = AuditRedirectRule(rule_priority=str(action_rule_details[0]), rule_name=action_rule_details[1]) return audit_action_rule @staticmethod diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 055c57516..e18d4c89e 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2591,7 +2591,7 @@ def test_correct_actions_determined_from_not_eligible_action_rules( # noqa: PLR with app.app_context(): g.audit_log = AuditEvent() - actual = calculator.evaluate_eligibility() + actual = calculator.evaluate_eligibility("Y", ["ALL"], "ALL") assert_that( actual, @@ -2648,7 +2648,7 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give with app.app_context(): g.audit_log = AuditEvent() - actual = calculator.evaluate_eligibility() + actual = calculator.evaluate_eligibility("Y", ["ALL"], "ALL") # Then expected_actions = [] @@ -2837,7 +2837,7 @@ def test_correct_actions_determined_from_not_actionable_action_rules( # noqa: P with app.app_context(): g.audit_log = AuditEvent() - actual = calculator.evaluate_eligibility() + actual = calculator.evaluate_eligibility("Y", ["ALL"], "ALL") assert_that( actual, @@ -2891,7 +2891,7 @@ def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_gi with app.app_context(): g.audit_log = AuditEvent() - actual = calculator.evaluate_eligibility() + actual = calculator.evaluate_eligibility("Y", ["ALL"], "ALL") # Then expected_actions = [] From 46027046774c1e90d551a151ac09958743be386c Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:26:14 +0100 Subject: [PATCH 17/19] Format. --- src/eligibility_signposting_api/audit/audit_context.py | 6 ++++-- .../services/calculators/test_eligibility_calculator.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/eligibility_signposting_api/audit/audit_context.py b/src/eligibility_signposting_api/audit/audit_context.py index d449f5310..b54276cbb 100644 --- a/src/eligibility_signposting_api/audit/audit_context.py +++ b/src/eligibility_signposting_api/audit/audit_context.py @@ -118,9 +118,11 @@ def add_rule_name_and_priority_to_audit( audit_action_rule = None if best_candidate and best_candidate.status: if action_rule_details is None or (action_rule_details[0] is None and action_rule_details[1] is None): - audit_action_rule = None + audit_action_rule = None else: - audit_action_rule = AuditRedirectRule(rule_priority=str(action_rule_details[0]), rule_name=action_rule_details[1]) + audit_action_rule = AuditRedirectRule( + rule_priority=str(action_rule_details[0]), rule_name=action_rule_details[1] + ) return audit_action_rule @staticmethod diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index e18d4c89e..de0d91d9c 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2382,6 +2382,7 @@ def test_should_not_include_actions_when_include_actions_flag_is_false_when_stat ), ) + @pytest.mark.parametrize( ("campaign_target", "campaign_type", "conditions_filter", "category_filter", "expected_result"), [ From 65a92627bd2f012e92587e3ed7b59b7a180d83d7 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:51:02 +0100 Subject: [PATCH 18/19] Added tests. --- .../test_eligibility_calculator.py | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index de0d91d9c..f2e714f72 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2670,6 +2670,89 @@ def test_no_actions_returned_when_non_eligible_actions_and_defaultcomms_not_give assert cond.actions == expected_audit_action +def test_actions_returned_when_non_eligible_actions_not_given_and_defaultcomms_given( + app, + faker: Faker, +): + """ + ELI-295 - Campaign config without NonEligibleActions (X rules) but with default comms routing + should return the default comms actions + """ + + # Given + nhs_number = NHSNumber(faker.nhs_number()) + + person_rows = person_rows_builder(nhs_number, cohorts=["NotEligibleCohort"]) + + campaign_configs = [ + ( + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + default_not_eligible_routing="defaultCommsCode", + actions_mapper=rule_builder.ActionsMapperFactory.build(root={ + "defaultCommsCode": AvailableAction( + ActionType="DefaultInfoText", + ExternalRoutingCode="DefaultHealthcareProInfo", + ActionDescription="Default Speak to your healthcare professional.", + ) + }), + iteration_rules=[], + ) + ], + ) + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + with app.app_context(): + g.audit_log = AuditEvent() + + actual = calculator.evaluate_eligibility("Y", ["ALL"], "ALL") + + # Then + expected_actions = [ + SuggestedAction( + internal_action_code=InternalActionCode("defaultCommsCode"), + action_type=ActionType("DefaultInfoText"), + action_code=ActionCode("DefaultHealthcareProInfo"), + action_description=ActionDescription( + "Default Speak to your healthcare professional." + ), + url_link=None, + url_label=None, + ) + ] + expected_audit_action = [ + AuditAction( + internal_action_code="defaultCommsCode", + action_code="DefaultHealthcareProInfo", + action_type="DefaultInfoText", + action_description="Default Speak to your healthcare professional.", + action_url=None, + action_url_label=None, + ) + ] + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_eligible)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) + + cond = g.audit_log.response.condition[0] + assert cond.actions == expected_audit_action + + @pytest.mark.parametrize( ( "test_comment", @@ -2911,3 +2994,86 @@ def test_no_actions_returned_when_non_actionable_actions_and_defaultcomms_not_gi cond = g.audit_log.response.condition[0] assert cond.actions == expected_audit_action + + +def test_actions_returned_when_non_actionable_actions_not_given_and_defaultcomms_given( + app, + faker: Faker, +): + """ + ELI-295 - Campaign config without NonActionableActions (Y rules) with default comms routing + should return default comms actions + """ + + # Given + nhs_number = NHSNumber(faker.nhs_number()) + + person_rows = person_rows_builder(nhs_number, cohorts=["cohort1"], de=True) + + campaign_configs = [ + ( + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + default_not_actionable_routing="defaultCommsCode", + actions_mapper=rule_builder.ActionsMapperFactory.build(root={ + "defaultCommsCode": AvailableAction( + ActionType="DefaultInfoText", + ExternalRoutingCode="DefaultHealthcareProInfo", + ActionDescription="Default Speak to your healthcare professional.", + ) + }), + iteration_rules=[rule_builder.DetainedEstateSuppressionRuleFactory.build()], + ) + ], + ) + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + with app.app_context(): + g.audit_log = AuditEvent() + + actual = calculator.evaluate_eligibility("Y", ["ALL"], "ALL") + + # Then + expected_actions = [ + SuggestedAction( + internal_action_code=InternalActionCode("defaultCommsCode"), + action_type=ActionType("DefaultInfoText"), + action_code=ActionCode("DefaultHealthcareProInfo"), + action_description=ActionDescription( + "Default Speak to your healthcare professional." + ), + url_link=None, + url_label=None, + ) + ] + expected_audit_action = [ + AuditAction( + internal_action_code="defaultCommsCode", + action_code="DefaultHealthcareProInfo", + action_type="DefaultInfoText", + action_description="Default Speak to your healthcare professional.", + action_url=None, + action_url_label=None, + ) + ] + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition() + .with_condition_name(ConditionName("RSV")) + .and_status(equal_to(Status.not_actionable)) + .and_actions(equal_to(expected_actions)) + ) + ), + ) + + cond = g.audit_log.response.condition[0] + assert cond.actions == expected_audit_action From c2456a1c4abf565bf2d9ef3dfa86a1b6a295c603 Mon Sep 17 00:00:00 2001 From: ayeshalshukri1-nhs <112615598+ayeshalshukri1-nhs@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:05:10 +0100 Subject: [PATCH 19/19] File format --- .../test_eligibility_calculator.py | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index f2e714f72..baa81b8b7 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -2692,13 +2692,15 @@ def test_actions_returned_when_non_eligible_actions_not_given_and_defaultcomms_g rule_builder.IterationFactory.build( iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], default_not_eligible_routing="defaultCommsCode", - actions_mapper=rule_builder.ActionsMapperFactory.build(root={ - "defaultCommsCode": AvailableAction( - ActionType="DefaultInfoText", - ExternalRoutingCode="DefaultHealthcareProInfo", - ActionDescription="Default Speak to your healthcare professional.", - ) - }), + actions_mapper=rule_builder.ActionsMapperFactory.build( + root={ + "defaultCommsCode": AvailableAction( + ActionType="DefaultInfoText", + ExternalRoutingCode="DefaultHealthcareProInfo", + ActionDescription="Default Speak to your healthcare professional.", + ) + } + ), iteration_rules=[], ) ], @@ -2716,27 +2718,25 @@ def test_actions_returned_when_non_eligible_actions_not_given_and_defaultcomms_g # Then expected_actions = [ - SuggestedAction( - internal_action_code=InternalActionCode("defaultCommsCode"), - action_type=ActionType("DefaultInfoText"), - action_code=ActionCode("DefaultHealthcareProInfo"), - action_description=ActionDescription( - "Default Speak to your healthcare professional." - ), - url_link=None, - url_label=None, - ) - ] + SuggestedAction( + internal_action_code=InternalActionCode("defaultCommsCode"), + action_type=ActionType("DefaultInfoText"), + action_code=ActionCode("DefaultHealthcareProInfo"), + action_description=ActionDescription("Default Speak to your healthcare professional."), + url_link=None, + url_label=None, + ) + ] expected_audit_action = [ - AuditAction( - internal_action_code="defaultCommsCode", - action_code="DefaultHealthcareProInfo", - action_type="DefaultInfoText", - action_description="Default Speak to your healthcare professional.", - action_url=None, - action_url_label=None, - ) - ] + AuditAction( + internal_action_code="defaultCommsCode", + action_code="DefaultHealthcareProInfo", + action_type="DefaultInfoText", + action_description="Default Speak to your healthcare professional.", + action_url=None, + action_url_label=None, + ) + ] assert_that( actual, is_eligibility_status().with_conditions( @@ -3018,13 +3018,15 @@ def test_actions_returned_when_non_actionable_actions_not_given_and_defaultcomms rule_builder.IterationFactory.build( iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], default_not_actionable_routing="defaultCommsCode", - actions_mapper=rule_builder.ActionsMapperFactory.build(root={ - "defaultCommsCode": AvailableAction( - ActionType="DefaultInfoText", - ExternalRoutingCode="DefaultHealthcareProInfo", - ActionDescription="Default Speak to your healthcare professional.", - ) - }), + actions_mapper=rule_builder.ActionsMapperFactory.build( + root={ + "defaultCommsCode": AvailableAction( + ActionType="DefaultInfoText", + ExternalRoutingCode="DefaultHealthcareProInfo", + ActionDescription="Default Speak to your healthcare professional.", + ) + } + ), iteration_rules=[rule_builder.DetainedEstateSuppressionRuleFactory.build()], ) ], @@ -3046,9 +3048,7 @@ def test_actions_returned_when_non_actionable_actions_not_given_and_defaultcomms internal_action_code=InternalActionCode("defaultCommsCode"), action_type=ActionType("DefaultInfoText"), action_code=ActionCode("DefaultHealthcareProInfo"), - action_description=ActionDescription( - "Default Speak to your healthcare professional." - ), + action_description=ActionDescription("Default Speak to your healthcare professional."), url_link=None, url_label=None, )