Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 51 additions & 22 deletions src/eligibility_signposting_api/audit/audit_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
ConditionName,
IterationResult,
MatchedActionDetail,
Reason,
Status,
SuggestedAction,
)
Expand Down Expand Up @@ -63,9 +64,9 @@ def append_audit_condition(
condition_name: ConditionName,
best_iteration_result: BestIterationResult,
action_detail: MatchedActionDetail,
cohort_results: list[CohortGroupResult],
) -> None:
audit_eligibility_cohorts, audit_eligibility_cohort_groups, audit_actions = [], [], []
audit_filter_rule, audit_suitability_rule, audit_action_rule = None, None, None
best_active_iteration = best_iteration_result.active_iteration
best_candidate = best_iteration_result.iteration_result
best_cohort_results = best_iteration_result.cohort_results
Expand All @@ -83,9 +84,15 @@ def append_audit_condition(
)
)

if result.audit_rules and best_candidate:
audit_filter_rule = AuditContext.create_audit_filter_rule(best_candidate, result)
audit_suitability_rule = AuditContext.create_audit_suitability_rule(best_candidate, result)
filter_audit_rules, suitability_audit_rules = [], []
for result in cohort_results:
if result.status.name == Status.not_eligible.name:
filter_audit_rules.extend(result.audit_rules)
if result.status.name == Status.not_actionable.name:
suitability_audit_rules.extend(result.audit_rules)

audit_filter_rule = AuditContext.create_audit_filter_rule(filter_audit_rules)
audit_suitability_rule = AuditContext.create_audit_suitability_rule(suitability_audit_rules)

audit_action_rule = AuditContext.add_rule_name_and_priority_to_audit(best_candidate, action_detail)

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

@staticmethod
def create_audit_suitability_rule(
best_candidate: IterationResult, result: CohortGroupResult
) -> AuditSuitabilityRule | None:
audit_suitability_rule = None
if best_candidate.status and best_candidate.status.name == Status.not_actionable.name:
audit_suitability_rule = AuditSuitabilityRule(
rule_priority=result.audit_rules[0].rule_priority,
rule_name=result.audit_rules[0].rule_name,
rule_message=result.audit_rules[0].rule_description,
def create_audit_suitability_rule(reasons: list[Reason]) -> list[AuditSuitabilityRule] | None:
unique_reasons = AuditContext.deduplicate_reasons(reasons)

suitability_audit = [
AuditSuitabilityRule(
rule_priority=rule.rule_priority,
rule_name=rule.rule_name,
rule_message=rule.rule_description,
)
return audit_suitability_rule
for rule in unique_reasons
]

return suitability_audit if suitability_audit else None

@staticmethod
def create_audit_filter_rule(best_candidate: IterationResult, result: CohortGroupResult) -> AuditFilterRule | None:
audit_filter_rule = None
if best_candidate.status and best_candidate.status.name == Status.not_eligible.name:
audit_filter_rule = AuditFilterRule(
rule_priority=result.audit_rules[0].rule_priority,
rule_name=result.audit_rules[0].rule_name,
)
return audit_filter_rule
def create_audit_filter_rule(reasons: list[Reason]) -> list[AuditFilterRule] | None:
unique_reasons = AuditContext.deduplicate_reasons(reasons)

filter_audit = [
AuditFilterRule(rule_priority=rule.rule_priority, rule_name=rule.rule_name) for rule in unique_reasons
]

return filter_audit if len(filter_audit) > 0 else None

@staticmethod
def deduplicate_reasons(reasons: list[Reason]) -> list[Reason]:
unique_rule_codes = set()
deduplicated_reasons = []

for reason in reasons:
if reason.rule_name not in unique_rule_codes and reason.rule_description:
unique_rule_codes.add(reason.rule_name)
deduplicated_reasons.append(
Reason(
reason.rule_type,
reason.rule_name,
reason.rule_priority,
reason.rule_description,
reason.matcher_matched,
)
)

return deduplicated_reasons
4 changes: 2 additions & 2 deletions src/eligibility_signposting_api/audit/audit_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ class AuditCondition(CamelCaseBaseModel):
status_text: str | None = None
eligibility_cohorts: list[AuditEligibilityCohorts] | None = None
eligibility_cohort_groups: list[AuditEligibilityCohortGroups] | None = None
filter_rules: AuditFilterRule | None = None
suitability_rules: AuditSuitabilityRule | None = None
filter_rules: list[AuditFilterRule] | None = None
suitability_rules: list[AuditSuitabilityRule] | None = None
action_rule: AuditRedirectRule | None = None
actions: list[AuditAction] | None = Field(default_factory=list)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def get_the_best_cohort_memberships(
def get_eligibility_status(self, include_actions: str, conditions: list[str], category: str) -> EligibilityStatus:
include_actions_flag = include_actions.upper() == "Y"
condition_results: dict[ConditionName, IterationResult] = {}
final_result = []

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

AuditContext.append_audit_condition(condition_name, best_iteration_result, matched_action_detail)
condition_result = self.build_condition_results(condition_results[condition_name], condition_name)
final_result.append(condition_result)

AuditContext.append_audit_condition(
condition_name,
best_iteration_result,
matched_action_detail,
condition_results[condition_name].cohort_results,
)

# Consolidate all the results and return
final_result = self.build_condition_results(condition_results)
return eligibility_status.EligibilityStatus(conditions=final_result)

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

@staticmethod
def build_condition_results(condition_results: dict[ConditionName, IterationResult]) -> list[Condition]:
conditions: list[Condition] = []
# iterate over conditions
for condition_name, active_iteration_result in condition_results.items():
grouped_cohort_results = defaultdict(list)
# iterate over cohorts and group them by status and cohort_group
for cohort_result in active_iteration_result.cohort_results:
if active_iteration_result.status == cohort_result.status:
grouped_cohort_results[cohort_result.cohort_code].append(cohort_result)

# deduplicate grouped cohort results by cohort_code
deduplicated_cohort_results = [
CohortGroupResult(
def build_condition_results(iteration_result: IterationResult, condition_name: ConditionName) -> Condition:
grouped_cohort_results = defaultdict(list)

for cohort_result in iteration_result.cohort_results:
if iteration_result.status == cohort_result.status:
grouped_cohort_results[cohort_result.cohort_code].append(cohort_result)

deduplicated_cohort_results = []

for group_cohort_code, group in grouped_cohort_results.items():
if group:
unique_rule_codes = set()
deduplicated_reasons = []
for cohort in group:
for reason in cohort.reasons:
if reason.rule_name not in unique_rule_codes and reason.rule_description:
unique_rule_codes.add(reason.rule_name)
deduplicated_reasons.append(reason)

non_empty_description = next((c.description for c in group if c.description), group[0].description)
cohort_group_result = CohortGroupResult(
cohort_code=group_cohort_code,
status=group[0].status,
# Flatten all reasons from the group
reasons=[reason for cohort in group for reason in cohort.reasons],
# get the first nonempty description
description=next((c.description for c in group if c.description), group[0].description),
reasons=deduplicated_reasons,
description=non_empty_description,
audit_rules=[],
)
for group_cohort_code, group in grouped_cohort_results.items()
if group
]

# return condition with cohort results
conditions.append(
Condition(
condition_name=condition_name,
status=active_iteration_result.status,
cohort_results=list(deduplicated_cohort_results),
actions=condition_results[condition_name].actions,
status_text=active_iteration_result.status.get_status_text(condition_name),
)
)
return conditions
deduplicated_cohort_results.append(cohort_group_result)

return Condition(
condition_name=condition_name,
status=iteration_result.status,
cohort_results=list(deduplicated_cohort_results),
actions=iteration_result.actions,
status_text=iteration_result.status.get_status_text(condition_name),
)
20 changes: 9 additions & 11 deletions src/eligibility_signposting_api/views/eligibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,20 +162,18 @@ def build_suitability_results(condition: Condition) -> list[eligibility_response
if condition.status != Status.not_actionable:
return []

unique_rule_codes = set()
suitability_results = []

for cohort_result in condition.cohort_results:
if cohort_result.status == Status.not_actionable:
for reason in cohort_result.reasons:
if reason.rule_name not in unique_rule_codes and reason.rule_description:
unique_rule_codes.add(reason.rule_name)
suitability_results.append(
eligibility_response.SuitabilityRule(
ruleType=eligibility_response.RuleType(reason.rule_type.value),
ruleCode=eligibility_response.RuleCode(reason.rule_name),
ruleText=eligibility_response.RuleText(reason.rule_description),
)
)
suitability_results.extend(
eligibility_response.SuitabilityRule(
ruleType=eligibility_response.RuleType(reason.rule_type.value),
ruleCode=eligibility_response.RuleCode(reason.rule_name),
ruleText=eligibility_response.RuleText(reason.rule_description),
)
for reason in cohort_result.reasons
if reason.rule_description
)

return suitability_results
12 changes: 11 additions & 1 deletion tests/fixtures/builders/model/eligibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
from polyfactory.factories import DataclassFactory

from eligibility_signposting_api.model import eligibility_status
from eligibility_signposting_api.model.eligibility_status import RuleType, UrlLink
from eligibility_signposting_api.model.eligibility_status import (
RuleDescription,
RuleName,
RulePriority,
RuleType,
UrlLink,
)


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

class ReasonFactory(DataclassFactory[eligibility_status.Reason]):
rule_type = RuleType.filter
rule_name = RuleName("name")
rule_priority = RulePriority("1")
rule_description = RuleDescription("description")
matcher_matched = False


class CohortResultFactory(DataclassFactory[eligibility_status.CohortGroupResult]):
Expand Down
27 changes: 21 additions & 6 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
from yarl import URL

from eligibility_signposting_api.model import eligibility_status
from eligibility_signposting_api.model.campaign_config import CampaignConfig, RuleType
from eligibility_signposting_api.model.campaign_config import (
CampaignConfig,
RuleType,
)
from eligibility_signposting_api.repos.campaign_repo import BucketName
from eligibility_signposting_api.repos.person_repo import TableName
from tests.fixtures.builders.model import rule
Expand Down Expand Up @@ -376,8 +379,8 @@ def persisted_person_all_cohorts(person_table: Any, faker: Faker) -> Generator[e
rows := person_rows_builder(
nhs_number,
date_of_birth=date_of_birth,
postcode="hp1",
cohorts=["cohort_label1", "cohort_label2", "cohort_label3"],
postcode="SW19",
cohorts=["cohort_label1", "cohort_label2", "cohort_label3", "cohort_label4"],
icb="QE1",
).data
):
Expand Down Expand Up @@ -488,8 +491,14 @@ def multiple_campaign_configs(s3_client: BaseClient, rules_bucket: BucketName) -

targets = ["RSV", "COVID", "FLU"]
target_rules_map = {
targets[0]: [rule.PersonAgeSuppressionRuleFactory.build(type=RuleType.filter)],
targets[1]: [rule.PersonAgeSuppressionRuleFactory.build()],
targets[0]: [
rule.PersonAgeSuppressionRuleFactory.build(type=RuleType.filter, description="TOO YOUNG"),
rule.PostcodeSuppressionRuleFactory.build(type=RuleType.filter, priority=8, cohort_label="cohort_label4"),
],
targets[1]: [
rule.PersonAgeSuppressionRuleFactory.build(description="TOO YOUNG"),
rule.PostcodeSuppressionRuleFactory.build(priority=12, cohort_label="cohort_label2"),
],
targets[2]: [rule.ICBRedirectRuleFactory.build()],
}

Expand All @@ -507,7 +516,13 @@ def multiple_campaign_configs(s3_client: BaseClient, rules_bucket: BucketName) -
cohort_group=f"cohort_group{i + 1}",
positive_description=f"positive_desc_{i + 1}",
negative_description=f"negative_desc_{i + 1}",
)
),
rule.IterationCohortFactory.build(
cohort_label="cohort_label4",
cohort_group="cohort_group4",
positive_description="positive_desc_4",
negative_description="negative_desc_4",
),
],
)
],
Expand Down
Loading