Skip to content

Commit 81bb071

Browse files
authored
ELI-376: Audit record should log multiple F and S rules (#275)
* ELI-376: Audit record should log multiple F and S rules * ELI-376: Fixing int test
1 parent acb6428 commit 81bb071

File tree

10 files changed

+238
-360
lines changed

10 files changed

+238
-360
lines changed

src/eligibility_signposting_api/audit/audit_context.py

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
ConditionName,
2525
IterationResult,
2626
MatchedActionDetail,
27+
Reason,
2728
Status,
2829
SuggestedAction,
2930
)
@@ -63,9 +64,9 @@ def append_audit_condition(
6364
condition_name: ConditionName,
6465
best_iteration_result: BestIterationResult,
6566
action_detail: MatchedActionDetail,
67+
cohort_results: list[CohortGroupResult],
6668
) -> None:
6769
audit_eligibility_cohorts, audit_eligibility_cohort_groups, audit_actions = [], [], []
68-
audit_filter_rule, audit_suitability_rule, audit_action_rule = None, None, None
6970
best_active_iteration = best_iteration_result.active_iteration
7071
best_candidate = best_iteration_result.iteration_result
7172
best_cohort_results = best_iteration_result.cohort_results
@@ -83,9 +84,15 @@ def append_audit_condition(
8384
)
8485
)
8586

86-
if result.audit_rules and best_candidate:
87-
audit_filter_rule = AuditContext.create_audit_filter_rule(best_candidate, result)
88-
audit_suitability_rule = AuditContext.create_audit_suitability_rule(best_candidate, result)
87+
filter_audit_rules, suitability_audit_rules = [], []
88+
for result in cohort_results:
89+
if result.status.name == Status.not_eligible.name:
90+
filter_audit_rules.extend(result.audit_rules)
91+
if result.status.name == Status.not_actionable.name:
92+
suitability_audit_rules.extend(result.audit_rules)
93+
94+
audit_filter_rule = AuditContext.create_audit_filter_rule(filter_audit_rules)
95+
audit_suitability_rule = AuditContext.create_audit_suitability_rule(suitability_audit_rules)
8996

9097
audit_action_rule = AuditContext.add_rule_name_and_priority_to_audit(best_candidate, action_detail)
9198

@@ -153,24 +160,46 @@ def create_audit_actions(suggested_actions: list[SuggestedAction] | None) -> lis
153160
return audit_actions
154161

155162
@staticmethod
156-
def create_audit_suitability_rule(
157-
best_candidate: IterationResult, result: CohortGroupResult
158-
) -> AuditSuitabilityRule | None:
159-
audit_suitability_rule = None
160-
if best_candidate.status and best_candidate.status.name == Status.not_actionable.name:
161-
audit_suitability_rule = AuditSuitabilityRule(
162-
rule_priority=result.audit_rules[0].rule_priority,
163-
rule_name=result.audit_rules[0].rule_name,
164-
rule_message=result.audit_rules[0].rule_description,
163+
def create_audit_suitability_rule(reasons: list[Reason]) -> list[AuditSuitabilityRule] | None:
164+
unique_reasons = AuditContext.deduplicate_reasons(reasons)
165+
166+
suitability_audit = [
167+
AuditSuitabilityRule(
168+
rule_priority=rule.rule_priority,
169+
rule_name=rule.rule_name,
170+
rule_message=rule.rule_description,
165171
)
166-
return audit_suitability_rule
172+
for rule in unique_reasons
173+
]
174+
175+
return suitability_audit if suitability_audit else None
167176

168177
@staticmethod
169-
def create_audit_filter_rule(best_candidate: IterationResult, result: CohortGroupResult) -> AuditFilterRule | None:
170-
audit_filter_rule = None
171-
if best_candidate.status and best_candidate.status.name == Status.not_eligible.name:
172-
audit_filter_rule = AuditFilterRule(
173-
rule_priority=result.audit_rules[0].rule_priority,
174-
rule_name=result.audit_rules[0].rule_name,
175-
)
176-
return audit_filter_rule
178+
def create_audit_filter_rule(reasons: list[Reason]) -> list[AuditFilterRule] | None:
179+
unique_reasons = AuditContext.deduplicate_reasons(reasons)
180+
181+
filter_audit = [
182+
AuditFilterRule(rule_priority=rule.rule_priority, rule_name=rule.rule_name) for rule in unique_reasons
183+
]
184+
185+
return filter_audit if len(filter_audit) > 0 else None
186+
187+
@staticmethod
188+
def deduplicate_reasons(reasons: list[Reason]) -> list[Reason]:
189+
unique_rule_codes = set()
190+
deduplicated_reasons = []
191+
192+
for reason in reasons:
193+
if reason.rule_name not in unique_rule_codes and reason.rule_description:
194+
unique_rule_codes.add(reason.rule_name)
195+
deduplicated_reasons.append(
196+
Reason(
197+
reason.rule_type,
198+
reason.rule_name,
199+
reason.rule_priority,
200+
reason.rule_description,
201+
reason.matcher_matched,
202+
)
203+
)
204+
205+
return deduplicated_reasons

src/eligibility_signposting_api/audit/audit_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ class AuditCondition(CamelCaseBaseModel):
7878
status_text: str | None = None
7979
eligibility_cohorts: list[AuditEligibilityCohorts] | None = None
8080
eligibility_cohort_groups: list[AuditEligibilityCohortGroups] | None = None
81-
filter_rules: AuditFilterRule | None = None
82-
suitability_rules: AuditSuitabilityRule | None = None
81+
filter_rules: list[AuditFilterRule] | None = None
82+
suitability_rules: list[AuditSuitabilityRule] | None = None
8383
action_rule: AuditRedirectRule | None = None
8484
actions: list[AuditAction] | None = Field(default_factory=list)
8585

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def get_the_best_cohort_memberships(
7676
def get_eligibility_status(self, include_actions: str, conditions: list[str], category: str) -> EligibilityStatus:
7777
include_actions_flag = include_actions.upper() == "Y"
7878
condition_results: dict[ConditionName, IterationResult] = {}
79+
final_result = []
7980

8081
requested_grouped_campaigns = self.campaign_evaluator.get_requested_grouped_campaigns(
8182
self.campaign_configs, conditions, category
@@ -93,10 +94,17 @@ def get_eligibility_status(self, include_actions: str, conditions: list[str], ca
9394
condition_results[condition_name] = best_iteration_result.iteration_result
9495
condition_results[condition_name].actions = matched_action_detail.actions
9596

96-
AuditContext.append_audit_condition(condition_name, best_iteration_result, matched_action_detail)
97+
condition_result = self.build_condition_results(condition_results[condition_name], condition_name)
98+
final_result.append(condition_result)
99+
100+
AuditContext.append_audit_condition(
101+
condition_name,
102+
best_iteration_result,
103+
matched_action_detail,
104+
condition_results[condition_name].cohort_results,
105+
)
97106

98107
# Consolidate all the results and return
99-
final_result = self.build_condition_results(condition_results)
100108
return eligibility_status.EligibilityStatus(conditions=final_result)
101109

102110
def get_best_iteration_result(self, campaign_group: list[CampaignConfig]) -> BestIterationResult:
@@ -133,39 +141,39 @@ def get_iteration_results(self, campaign_group: list[CampaignConfig]) -> dict[It
133141
return iteration_results
134142

135143
@staticmethod
136-
def build_condition_results(condition_results: dict[ConditionName, IterationResult]) -> list[Condition]:
137-
conditions: list[Condition] = []
138-
# iterate over conditions
139-
for condition_name, active_iteration_result in condition_results.items():
140-
grouped_cohort_results = defaultdict(list)
141-
# iterate over cohorts and group them by status and cohort_group
142-
for cohort_result in active_iteration_result.cohort_results:
143-
if active_iteration_result.status == cohort_result.status:
144-
grouped_cohort_results[cohort_result.cohort_code].append(cohort_result)
145-
146-
# deduplicate grouped cohort results by cohort_code
147-
deduplicated_cohort_results = [
148-
CohortGroupResult(
144+
def build_condition_results(iteration_result: IterationResult, condition_name: ConditionName) -> Condition:
145+
grouped_cohort_results = defaultdict(list)
146+
147+
for cohort_result in iteration_result.cohort_results:
148+
if iteration_result.status == cohort_result.status:
149+
grouped_cohort_results[cohort_result.cohort_code].append(cohort_result)
150+
151+
deduplicated_cohort_results = []
152+
153+
for group_cohort_code, group in grouped_cohort_results.items():
154+
if group:
155+
unique_rule_codes = set()
156+
deduplicated_reasons = []
157+
for cohort in group:
158+
for reason in cohort.reasons:
159+
if reason.rule_name not in unique_rule_codes and reason.rule_description:
160+
unique_rule_codes.add(reason.rule_name)
161+
deduplicated_reasons.append(reason)
162+
163+
non_empty_description = next((c.description for c in group if c.description), group[0].description)
164+
cohort_group_result = CohortGroupResult(
149165
cohort_code=group_cohort_code,
150166
status=group[0].status,
151-
# Flatten all reasons from the group
152-
reasons=[reason for cohort in group for reason in cohort.reasons],
153-
# get the first nonempty description
154-
description=next((c.description for c in group if c.description), group[0].description),
167+
reasons=deduplicated_reasons,
168+
description=non_empty_description,
155169
audit_rules=[],
156170
)
157-
for group_cohort_code, group in grouped_cohort_results.items()
158-
if group
159-
]
160-
161-
# return condition with cohort results
162-
conditions.append(
163-
Condition(
164-
condition_name=condition_name,
165-
status=active_iteration_result.status,
166-
cohort_results=list(deduplicated_cohort_results),
167-
actions=condition_results[condition_name].actions,
168-
status_text=active_iteration_result.status.get_status_text(condition_name),
169-
)
170-
)
171-
return conditions
171+
deduplicated_cohort_results.append(cohort_group_result)
172+
173+
return Condition(
174+
condition_name=condition_name,
175+
status=iteration_result.status,
176+
cohort_results=list(deduplicated_cohort_results),
177+
actions=iteration_result.actions,
178+
status_text=iteration_result.status.get_status_text(condition_name),
179+
)

src/eligibility_signposting_api/views/eligibility.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -162,20 +162,18 @@ def build_suitability_results(condition: Condition) -> list[eligibility_response
162162
if condition.status != Status.not_actionable:
163163
return []
164164

165-
unique_rule_codes = set()
166165
suitability_results = []
167166

168167
for cohort_result in condition.cohort_results:
169168
if cohort_result.status == Status.not_actionable:
170-
for reason in cohort_result.reasons:
171-
if reason.rule_name not in unique_rule_codes and reason.rule_description:
172-
unique_rule_codes.add(reason.rule_name)
173-
suitability_results.append(
174-
eligibility_response.SuitabilityRule(
175-
ruleType=eligibility_response.RuleType(reason.rule_type.value),
176-
ruleCode=eligibility_response.RuleCode(reason.rule_name),
177-
ruleText=eligibility_response.RuleText(reason.rule_description),
178-
)
179-
)
169+
suitability_results.extend(
170+
eligibility_response.SuitabilityRule(
171+
ruleType=eligibility_response.RuleType(reason.rule_type.value),
172+
ruleCode=eligibility_response.RuleCode(reason.rule_name),
173+
ruleText=eligibility_response.RuleText(reason.rule_description),
174+
)
175+
for reason in cohort_result.reasons
176+
if reason.rule_description
177+
)
180178

181179
return suitability_results

tests/fixtures/builders/model/eligibility.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
from polyfactory.factories import DataclassFactory
66

77
from eligibility_signposting_api.model import eligibility_status
8-
from eligibility_signposting_api.model.eligibility_status import RuleType, UrlLink
8+
from eligibility_signposting_api.model.eligibility_status import (
9+
RuleDescription,
10+
RuleName,
11+
RulePriority,
12+
RuleType,
13+
UrlLink,
14+
)
915

1016

1117
class SuggestedActionFactory(DataclassFactory[eligibility_status.SuggestedAction]):
@@ -14,6 +20,10 @@ class SuggestedActionFactory(DataclassFactory[eligibility_status.SuggestedAction
1420

1521
class ReasonFactory(DataclassFactory[eligibility_status.Reason]):
1622
rule_type = RuleType.filter
23+
rule_name = RuleName("name")
24+
rule_priority = RulePriority("1")
25+
rule_description = RuleDescription("description")
26+
matcher_matched = False
1727

1828

1929
class CohortResultFactory(DataclassFactory[eligibility_status.CohortGroupResult]):

tests/integration/conftest.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
from yarl import URL
1818

1919
from eligibility_signposting_api.model import eligibility_status
20-
from eligibility_signposting_api.model.campaign_config import CampaignConfig, RuleType
20+
from eligibility_signposting_api.model.campaign_config import (
21+
CampaignConfig,
22+
RuleType,
23+
)
2124
from eligibility_signposting_api.repos.campaign_repo import BucketName
2225
from eligibility_signposting_api.repos.person_repo import TableName
2326
from tests.fixtures.builders.model import rule
@@ -376,8 +379,8 @@ def persisted_person_all_cohorts(person_table: Any, faker: Faker) -> Generator[e
376379
rows := person_rows_builder(
377380
nhs_number,
378381
date_of_birth=date_of_birth,
379-
postcode="hp1",
380-
cohorts=["cohort_label1", "cohort_label2", "cohort_label3"],
382+
postcode="SW19",
383+
cohorts=["cohort_label1", "cohort_label2", "cohort_label3", "cohort_label4"],
381384
icb="QE1",
382385
).data
383386
):
@@ -488,8 +491,14 @@ def multiple_campaign_configs(s3_client: BaseClient, rules_bucket: BucketName) -
488491

489492
targets = ["RSV", "COVID", "FLU"]
490493
target_rules_map = {
491-
targets[0]: [rule.PersonAgeSuppressionRuleFactory.build(type=RuleType.filter)],
492-
targets[1]: [rule.PersonAgeSuppressionRuleFactory.build()],
494+
targets[0]: [
495+
rule.PersonAgeSuppressionRuleFactory.build(type=RuleType.filter, description="TOO YOUNG"),
496+
rule.PostcodeSuppressionRuleFactory.build(type=RuleType.filter, priority=8, cohort_label="cohort_label4"),
497+
],
498+
targets[1]: [
499+
rule.PersonAgeSuppressionRuleFactory.build(description="TOO YOUNG"),
500+
rule.PostcodeSuppressionRuleFactory.build(priority=12, cohort_label="cohort_label2"),
501+
],
493502
targets[2]: [rule.ICBRedirectRuleFactory.build()],
494503
}
495504

@@ -507,7 +516,13 @@ def multiple_campaign_configs(s3_client: BaseClient, rules_bucket: BucketName) -
507516
cohort_group=f"cohort_group{i + 1}",
508517
positive_description=f"positive_desc_{i + 1}",
509518
negative_description=f"negative_desc_{i + 1}",
510-
)
519+
),
520+
rule.IterationCohortFactory.build(
521+
cohort_label="cohort_label4",
522+
cohort_group="cohort_group4",
523+
positive_description="positive_desc_4",
524+
negative_description="negative_desc_4",
525+
),
511526
],
512527
)
513528
],

0 commit comments

Comments
 (0)