Skip to content

Commit a9194c5

Browse files
Feature/eli 295 generic text for not eligible xrules (#238)
* WIP: drafting out X and Y rules. * WIP: updated config with x and y rule idea. * WIP: Stub out x and y rules impl * WIP: stubbing out impl. * Refactored action support functions and renamed vars * WIP: Added X/Y Rule logic and test. * Added tests for eligible and actionable actions. * WIP: Added more tests for X and Y rule scenarios. * WIP: flaky tests. * WIP: Fixed failing tests for empty actions. * WIP: added audit record check to tests. * WIP: file format and added audit rule priority and name test. * Working tests. Refactored some audit logic. * Minor refactor * Addressed linting issues * WIP: fixed failing unit tests. * Format. * Added tests. * File format --------- Co-authored-by: Robert <rob.bailiff1@nhs.net>
1 parent b465617 commit a9194c5

8 files changed

Lines changed: 793 additions & 40 deletions

File tree

src/eligibility_signposting_api/audit/audit_context.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ def append_audit_condition(
6363
condition_name: ConditionName,
6464
best_results: tuple[Iteration | None, IterationResult | None, dict[str, CohortGroupResult] | None],
6565
campaign_details: tuple[CampaignID | None, CampaignVersion | None],
66-
redirect_rule_details: tuple[RulePriority | None, RuleName | None],
66+
action_rule_details: tuple[RulePriority | None, RuleName | None],
6767
) -> None:
68-
audit_eligibility_cohorts, audit_eligibility_cohort_groups = [], []
69-
audit_filter_rule, audit_suitability_rule = None, None
68+
audit_eligibility_cohorts, audit_eligibility_cohort_groups, audit_actions = [], [], []
69+
audit_filter_rule, audit_suitability_rule, audit_action_rule = None, None, None
7070
best_active_iteration = best_results[0]
7171
best_candidate = best_results[1]
7272
best_cohort_results = best_results[2]
@@ -88,7 +88,7 @@ def append_audit_condition(
8888
audit_filter_rule = AuditContext.create_audit_filter_rule(best_candidate, result)
8989
audit_suitability_rule = AuditContext.create_audit_suitability_rule(best_candidate, result)
9090

91-
audit_redirect_rule = AuditContext.get_audit_redirect_rule(best_candidate, redirect_rule_details)
91+
audit_action_rule = AuditContext.add_rule_name_and_priority_to_audit(best_candidate, action_rule_details)
9292

9393
audit_actions = AuditContext.create_audit_actions(suggested_actions)
9494

@@ -104,22 +104,26 @@ def append_audit_condition(
104104
eligibility_cohort_groups=audit_eligibility_cohort_groups,
105105
filter_rules=audit_filter_rule,
106106
suitability_rules=audit_suitability_rule,
107-
action_rule=audit_redirect_rule,
107+
action_rule=audit_action_rule,
108108
actions=audit_actions,
109109
)
110110

111111
g.audit_log.response.condition.append(audit_condition)
112112

113113
@staticmethod
114-
def get_audit_redirect_rule(
115-
best_candidate: IterationResult | None, redirect_rule_details: tuple[RulePriority | None, RuleName | None]
114+
def add_rule_name_and_priority_to_audit(
115+
best_candidate: IterationResult | None,
116+
action_rule_details: tuple[RulePriority | None, RuleName | None] | None,
116117
) -> AuditRedirectRule | None:
117-
audit_redirect_rule = None
118-
if best_candidate and best_candidate.status and best_candidate.status.name == Status.actionable.name:
119-
audit_redirect_rule = AuditRedirectRule(
120-
rule_priority=str(redirect_rule_details[0]), rule_name=redirect_rule_details[1]
121-
)
122-
return audit_redirect_rule
118+
audit_action_rule = None
119+
if best_candidate and best_candidate.status:
120+
if action_rule_details is None or (action_rule_details[0] is None and action_rule_details[1] is None):
121+
audit_action_rule = None
122+
else:
123+
audit_action_rule = AuditRedirectRule(
124+
rule_priority=str(action_rule_details[0]), rule_name=action_rule_details[1]
125+
)
126+
return audit_action_rule
123127

124128
@staticmethod
125129
def add_response_details(response_id: UUID, last_updated: datetime) -> None:

src/eligibility_signposting_api/model/eligibility.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class RuleType(StrEnum):
3131
filter = "F"
3232
suppression = "S"
3333
redirect = "R"
34+
not_eligible_actions = "X"
35+
not_actionable_actions = "Y"
3436

3537

3638
@total_ordering

src/eligibility_signposting_api/model/rules.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class RuleType(StrEnum):
4242
filter = "F"
4343
suppression = "S"
4444
redirect = "R"
45+
not_eligible_actions = "X"
46+
not_actionable_actions = "Y"
4547

4648

4749
class RuleOperator(StrEnum):
@@ -153,6 +155,8 @@ class Iteration(BaseModel):
153155
approval_maximum: int | None = Field(None, alias="ApprovalMaximum")
154156
type: Literal["A", "M", "S", "O"] = Field(..., alias="Type")
155157
default_comms_routing: str = Field(..., alias="DefaultCommsRouting")
158+
default_not_eligible_routing: str = Field(..., alias="DefaultNotEligibleRouting")
159+
default_not_actionable_routing: str = Field(..., alias="DefaultNotActionableRouting")
156160
iteration_cohorts: list[IterationCohort] = Field(..., alias="IterationCohorts")
157161
iteration_rules: list[IterationRule] = Field(..., alias="IterationRules")
158162
actions_mapper: ActionsMapper = Field(..., alias="ActionsMapper")

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
IterationCohort,
2020
RuleName,
2121
RulePriority,
22+
RuleType,
2223
)
2324

2425
from wireup import service
@@ -140,23 +141,28 @@ def get_rules_by_type(
140141
return filter_rules, suppression_rules
141142

142143
@staticmethod
143-
def get_redirect_rules(
144-
active_iteration: Iteration,
145-
) -> tuple[tuple[rules.IterationRule, ...], ActionsMapper, str]:
146-
redirect_rules = tuple(
147-
rule for rule in active_iteration.iteration_rules if rule.type in rules.RuleType.redirect
148-
)
149-
default_comms = active_iteration.default_comms_routing
144+
def get_action_rules_components(
145+
active_iteration: Iteration, rule_type: RuleType
146+
) -> tuple[tuple[rules.IterationRule, ...], ActionsMapper, str | None]:
147+
action_rules = tuple(rule for rule in active_iteration.iteration_rules if rule.type in rule_type)
148+
149+
routing_map = {
150+
rules.RuleType.redirect: active_iteration.default_comms_routing,
151+
rules.RuleType.not_eligible_actions: active_iteration.default_not_eligible_routing,
152+
rules.RuleType.not_actionable_actions: active_iteration.default_not_actionable_routing,
153+
}
154+
155+
default_comms = routing_map.get(rule_type)
150156
action_mapper = active_iteration.actions_mapper
151-
return redirect_rules, action_mapper, default_comms
157+
return action_rules, action_mapper, default_comms
152158

153159
def evaluate_eligibility(
154160
self, include_actions: str, conditions: list[str], category: str
155161
) -> eligibility.EligibilityStatus:
156162
include_actions_flag = include_actions.upper() == "Y"
157163
condition_results: dict[ConditionName, IterationResult] = {}
158164
actions: list[SuggestedAction] | None = []
159-
redirect_rule_priority, redirect_rule_name = None, None
165+
action_rule_priority, action_rule_name = None, None
160166

161167
for condition_name, campaign_group in self.campaigns_grouped_by_condition_name(conditions, category):
162168
best_active_iteration: Iteration | None
@@ -188,16 +194,26 @@ def evaluate_eligibility(
188194

189195
condition_results[condition_name] = best_candidate
190196

191-
if best_candidate.status == Status.actionable and best_active_iteration is not None:
197+
status_to_rule_type = {
198+
Status.actionable: rules.RuleType.redirect,
199+
Status.not_eligible: rules.RuleType.not_eligible_actions,
200+
Status.not_actionable: rules.RuleType.not_actionable_actions,
201+
}
202+
203+
if best_candidate.status in status_to_rule_type and best_active_iteration is not None:
192204
if include_actions_flag:
193-
actions, matched_r_rule_priority, matched_r_rule_name = self.handle_redirect_rules(
194-
best_active_iteration
205+
rule_type = status_to_rule_type[best_candidate.status]
206+
actions, matched_action_rule_priority, matched_action_rule_name = self.handle_action_rules(
207+
best_active_iteration, rule_type
195208
)
196-
redirect_rule_name = matched_r_rule_name
197-
redirect_rule_priority = matched_r_rule_priority
209+
action_rule_name = matched_action_rule_name
210+
action_rule_priority = matched_action_rule_priority
198211
else:
199212
actions = None
200213

214+
else:
215+
actions = None
216+
201217
if best_candidate.status in (Status.not_eligible, Status.not_actionable) and not include_actions_flag:
202218
actions = None
203219

@@ -212,7 +228,7 @@ def evaluate_eligibility(
212228
condition_name,
213229
(best_active_iteration, best_candidate, best_cohort_results),
214230
(best_campaign_id, best_campaign_version),
215-
(redirect_rule_priority, redirect_rule_name),
231+
(action_rule_priority, action_rule_name),
216232
)
217233

218234
# Consolidate all the results and return
@@ -240,15 +256,16 @@ def get_iteration_results(
240256
)
241257
return iteration_results
242258

243-
def handle_redirect_rules(
244-
self, best_active_iteration: Iteration
259+
def handle_action_rules(
260+
self, best_active_iteration: Iteration, rule_type: RuleType
245261
) -> tuple[list[SuggestedAction] | None, RulePriority | None, RuleName | None]:
246-
redirect_rules, action_mapper, default_comms = self.get_redirect_rules(best_active_iteration)
262+
action_rules, action_mapper, default_comms = self.get_action_rules_components(best_active_iteration, rule_type)
247263
priority_getter = attrgetter("priority")
248-
sorted_rules_by_priority = sorted(redirect_rules, key=priority_getter)
264+
sorted_rules_by_priority = sorted(action_rules, key=priority_getter)
265+
266+
actions: list[SuggestedAction] | None = self.get_actions_from_comms(action_mapper, default_comms) # pyright: ignore[reportArgumentType]
249267

250-
actions: list[SuggestedAction] | None = self.get_actions_from_comms(action_mapper, default_comms)
251-
matched_redirect_rule_priority, matched_redirect_rule_name = None, None
268+
matched_action_rule_priority, matched_action_rule_name = None, None
252269
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
253270
rule_group_list = list(rule_group)
254271
matcher_matched_list = [
@@ -261,11 +278,11 @@ def handle_redirect_rules(
261278
rule_actions = self.get_actions_from_comms(action_mapper, comms_routing)
262279
if rule_actions and len(rule_actions) > 0:
263280
actions = rule_actions
264-
matched_redirect_rule_priority = rule_group_list[0].priority
265-
matched_redirect_rule_name = rule_group_list[0].name
281+
matched_action_rule_priority = rule_group_list[0].priority
282+
matched_action_rule_name = rule_group_list[0].name
266283
break
267284

268-
return actions, matched_redirect_rule_priority, matched_redirect_rule_name
285+
return actions, matched_action_rule_priority, matched_action_rule_name
269286

270287
def get_cohort_results(self, active_iteration: rules.Iteration) -> dict[str, CohortGroupResult]:
271288
cohort_results: dict[str, CohortGroupResult] = {}

src/eligibility_signposting_api/services/calculators/rule_calculator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def evaluate_rule(self, attribute_value: str | None) -> tuple[eligibility.Status
8484
rules.RuleType.filter: eligibility.Status.not_eligible,
8585
rules.RuleType.suppression: eligibility.Status.not_actionable,
8686
rules.RuleType.redirect: eligibility.Status.actionable,
87+
rules.RuleType.not_eligible_actions: eligibility.Status.not_eligible,
88+
rules.RuleType.not_actionable_actions: eligibility.Status.not_actionable,
8789
}[self.rule.type]
8890
return status, str(reason), matcher_matched
8991
matcher.describe_mismatch(attribute_value, reason)

tests/fixtures/builders/model/rule.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,27 @@ class ICBRedirectRuleFactory(IterationRuleFactory):
173173
attribute_name = rules.RuleAttributeName("ICB")
174174
comparator = rules.RuleComparator("QE1")
175175
comms_routing = rules.CommsRouting("ActionCode1")
176+
177+
178+
class ICBNonEligibleActionRuleFactory(IterationRuleFactory):
179+
type = rules.RuleType.not_eligible_actions
180+
name = rules.RuleName("In QE1")
181+
description = rules.RuleDescription("In QE1")
182+
priority = rules.RulePriority(20)
183+
operator = rules.RuleOperator.equals
184+
attribute_level = rules.RuleAttributeLevel.PERSON
185+
attribute_name = rules.RuleAttributeName("ICB")
186+
comparator = rules.RuleComparator("QE1")
187+
comms_routing = rules.CommsRouting("ActionCode1")
188+
189+
190+
class ICBNonActionableActionRuleFactory(IterationRuleFactory):
191+
type = rules.RuleType.not_actionable_actions
192+
name = rules.RuleName("In QE1")
193+
description = rules.RuleDescription("In QE1")
194+
priority = rules.RulePriority(20)
195+
operator = rules.RuleOperator.equals
196+
attribute_level = rules.RuleAttributeLevel.PERSON
197+
attribute_name = rules.RuleAttributeName("ICB")
198+
comparator = rules.RuleComparator("QE1")
199+
comms_routing = rules.CommsRouting("ActionCode1")

tests/test_data/test_config/test_config.json

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616
{
1717
"ID": "id_100",
1818
"DefaultCommsRouting": "INTERNALCONTACTGP1",
19+
"DefaultNotActionableRouting": "INTERNALCONTACTGP1",
20+
"DefaultNotEligibleRouting": "INTERNALCONTACTGP1",
1921
"ActionsMapper": {
2022
"INTERNALCONTACTGP1": {"ExternalRoutingCode": "CONTACTGP","ActionDescription":"Contact GP Text1 description", "ActionType":"text1"},
2123
"INTERNALCONTACTGP2": {"ExternalRoutingCode": "CONTACTGP","ActionDescription":"Contact GP Link description", "ActionType":"link", "UrlLink": "link123", "UrlLabel": "link label"},
2224
"INTERNALTESCO": {"ExternalRoutingCode": "TESCO","ActionDescription":"Tesco description", "ActionType":"link", "UrlLink": "tesco link", "UrlLabel": "link label"},
23-
"INTERNALFINDWALKIN": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"}
25+
"INTERNALFINDWALKIN": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"},
26+
27+
"XRULEID1": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"},
28+
"YRULEID1": {"ExternalRoutingCode": "FINDWALKIN","ActionDescription":"Find walkin description", "ActionType":"button"}
2429
},
2530
"IterationCohorts": [
2631
{
@@ -91,6 +96,28 @@
9196
"Operator": ">",
9297
"Comparator": "19000101",
9398
"CommsRouting": "INTERNALCONTACTGP1|INTERNALTESCO"
99+
},
100+
{
101+
"Type": "X",
102+
"Name": "Test X Rule for not eligible",
103+
"Description": "Test X Rule Desc",
104+
"Priority": 20,
105+
"AttributeLevel": "PERSON",
106+
"AttributeName": "DATE_OF_BIRTH",
107+
"Operator": ">",
108+
"Comparator": "19000101",
109+
"CommsRouting": "XRULEID1|INTERNALTESCO"
110+
},
111+
{
112+
"Type": "Y",
113+
"Name": "Test Y Rule for not actionable",
114+
"Description": "Test Y Rule Desc",
115+
"Priority": 20,
116+
"AttributeLevel": "PERSON",
117+
"AttributeName": "DATE_OF_BIRTH",
118+
"Operator": ">",
119+
"Comparator": "19000101",
120+
"CommsRouting": "YRULEID1|INTERNALTESCO"
94121
}
95122
],
96123
"Version": "1",

0 commit comments

Comments
 (0)