Skip to content

Commit 092f46a

Browse files
committed
ELI-351: Adds rule processor
1 parent 237fc6d commit 092f46a

3 files changed

Lines changed: 378 additions & 93 deletions

File tree

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 5 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
CampaignID,
1717
CampaignVersion,
1818
Iteration,
19-
IterationCohort,
2019
IterationRule,
2120
RuleName,
2221
RulePriority,
@@ -41,9 +40,10 @@
4140
)
4241
from eligibility_signposting_api.services.processors.campaign_evaluator import CampaignEvaluator
4342
from eligibility_signposting_api.services.processors.person_data_reader import PersonDataReader
43+
from eligibility_signposting_api.services.processors.rule_processor import RuleProcessor
4444

4545
if TYPE_CHECKING:
46-
from collections.abc import Collection, Iterable, Iterator
46+
from collections.abc import Collection
4747

4848
from eligibility_signposting_api.model.person import Person
4949

@@ -62,6 +62,7 @@ class EligibilityCalculator:
6262

6363
campaign_evaluator: CampaignEvaluator = field(default_factory=CampaignEvaluator)
6464
person_data_reader: PersonDataReader = field(default_factory=PersonDataReader)
65+
rule_processor: RuleProcessor = field(default_factory=RuleProcessor)
6566

6667
results: list[eligibility_status.Condition] = field(default_factory=list)
6768

@@ -88,16 +89,6 @@ def get_the_best_cohort_memberships(
8889

8990
return best_status, best_cohorts
9091

91-
@staticmethod
92-
def get_exclusion_rules(cohort: IterationCohort, filter_rules: Iterable[IterationRule]) -> Iterator[IterationRule]:
93-
return (
94-
ir
95-
for ir in filter_rules
96-
if ir.cohort_label is None
97-
or cohort.cohort_label == ir.cohort_label
98-
or (isinstance(ir.cohort_label, (list, set, tuple)) and cohort.cohort_label in ir.cohort_label)
99-
)
100-
10192
@staticmethod
10293
def get_rules_by_type(
10394
active_iteration: Iteration,
@@ -263,9 +254,9 @@ def get_cohort_results(self, active_iteration: Iteration) -> dict[str, CohortGro
263254
person_cohorts = self.person_data_reader.get_person_cohorts(self.person)
264255
if cohort.cohort_label in person_cohorts or cohort.is_magic_cohort:
265256
# Eligibility - check
266-
if self.is_eligible_by_filter_rules(cohort, cohort_results, filter_rules):
257+
if self.rule_processor.is_eligible(self.person, cohort, cohort_results, filter_rules):
267258
# Actionability - evaluation
268-
self.evaluate_suppression_rules(cohort, cohort_results, suppression_rules)
259+
self.rule_processor.is_actionable(self.person, cohort, cohort_results, suppression_rules)
269260

270261
# Not base eligible
271262
elif cohort.cohort_label is not None:
@@ -316,85 +307,6 @@ def build_condition_results(condition_results: dict[ConditionName, IterationResu
316307
)
317308
return conditions
318309

319-
def is_eligible_by_filter_rules(
320-
self,
321-
cohort: IterationCohort,
322-
cohort_results: dict[str, CohortGroupResult],
323-
filter_rules: Iterable[IterationRule],
324-
) -> bool:
325-
is_eligible = True
326-
priority_getter = attrgetter("priority")
327-
sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, filter_rules), key=priority_getter)
328-
329-
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
330-
status, group_exclusion_reasons, _ = self.evaluate_rules_priority_group(rule_group)
331-
if status.is_exclusion:
332-
if cohort.cohort_label is not None:
333-
cohort_results[cohort.cohort_label] = CohortGroupResult(
334-
cohort.cohort_group,
335-
Status.not_eligible,
336-
[],
337-
cohort.negative_description,
338-
group_exclusion_reasons,
339-
)
340-
is_eligible = False
341-
break
342-
return is_eligible
343-
344-
def evaluate_suppression_rules(
345-
self,
346-
cohort: IterationCohort,
347-
cohort_results: dict[str, CohortGroupResult],
348-
suppression_rules: Iterable[IterationRule],
349-
) -> None:
350-
is_actionable: bool = True
351-
priority_getter = attrgetter("priority")
352-
suppression_reasons = []
353-
354-
sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, suppression_rules), key=priority_getter)
355-
356-
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
357-
status, group_exclusion_reasons, rule_stop = self.evaluate_rules_priority_group(rule_group)
358-
if status.is_exclusion:
359-
is_actionable = False
360-
suppression_reasons.extend(group_exclusion_reasons)
361-
if rule_stop:
362-
break
363-
364-
if cohort.cohort_label is not None:
365-
key = cohort.cohort_label
366-
if is_actionable:
367-
cohort_results[key] = CohortGroupResult(
368-
cohort.cohort_group, Status.actionable, [], cohort.positive_description, suppression_reasons
369-
)
370-
else:
371-
cohort_results[key] = CohortGroupResult(
372-
cohort.cohort_group,
373-
Status.not_actionable,
374-
suppression_reasons,
375-
cohort.positive_description,
376-
suppression_reasons,
377-
)
378-
379-
def evaluate_rules_priority_group(
380-
self, rules_group: Iterator[IterationRule]
381-
) -> tuple[eligibility_status.Status, list[eligibility_status.Reason], bool]:
382-
is_rule_stop = False
383-
exclusion_reasons = []
384-
best_status = eligibility_status.Status.not_eligible
385-
386-
for rule in rules_group:
387-
is_rule_stop = rule.rule_stop or is_rule_stop
388-
rule_calculator = RuleCalculator(person=self.person, rule=rule)
389-
status, reason = rule_calculator.evaluate_exclusion()
390-
if status.is_exclusion:
391-
best_status = eligibility_status.Status.best(status, best_status)
392-
exclusion_reasons.append(reason)
393-
else:
394-
best_status = eligibility_status.Status.actionable
395-
396-
return best_status, exclusion_reasons, is_rule_stop
397-
398310
@staticmethod
399311
def get_actions_from_comms(action_mapper: ActionsMapper, comms: str) -> list[SuggestedAction] | None:
400312
suggested_actions: list[SuggestedAction] = []
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from __future__ import annotations
2+
3+
from itertools import groupby
4+
from operator import attrgetter
5+
from typing import TYPE_CHECKING
6+
7+
from wireup import service
8+
9+
from eligibility_signposting_api.model import eligibility_status
10+
from eligibility_signposting_api.model.eligibility_status import CohortGroupResult, Status
11+
from eligibility_signposting_api.services.calculators.rule_calculator import RuleCalculator
12+
13+
if TYPE_CHECKING:
14+
from collections.abc import Iterable, Iterator
15+
16+
from eligibility_signposting_api.model.campaign_config import IterationCohort, IterationRule
17+
from eligibility_signposting_api.model.person import Person
18+
19+
20+
@service
21+
class RuleProcessor:
22+
"""Handles the processing and evaluation of different rules (filter, suppression) against person data."""
23+
24+
def is_eligible(
25+
self,
26+
person: Person,
27+
cohort: IterationCohort,
28+
cohort_results: dict[str, CohortGroupResult],
29+
filter_rules: Iterable[IterationRule],
30+
) -> bool:
31+
is_eligible = True
32+
priority_getter = attrgetter("priority")
33+
sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, filter_rules), key=priority_getter)
34+
35+
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
36+
status, group_exclusion_reasons, _ = self.evaluate_rules_priority_group(person, rule_group)
37+
if status.is_exclusion:
38+
if cohort.cohort_label is not None:
39+
cohort_results[cohort.cohort_label] = CohortGroupResult(
40+
cohort.cohort_group,
41+
Status.not_eligible,
42+
[],
43+
cohort.negative_description,
44+
group_exclusion_reasons,
45+
)
46+
is_eligible = False
47+
break
48+
return is_eligible
49+
50+
def is_actionable(
51+
self,
52+
person: Person,
53+
cohort: IterationCohort,
54+
cohort_results: dict[str, CohortGroupResult],
55+
suppression_rules: Iterable[IterationRule],
56+
) -> None:
57+
is_actionable: bool = True
58+
priority_getter = attrgetter("priority")
59+
suppression_reasons = []
60+
61+
sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, suppression_rules), key=priority_getter)
62+
63+
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
64+
status, group_exclusion_reasons, rule_stop = self.evaluate_rules_priority_group(person, rule_group)
65+
if status.is_exclusion:
66+
is_actionable = False
67+
suppression_reasons.extend(group_exclusion_reasons)
68+
if rule_stop:
69+
break
70+
71+
if cohort.cohort_label is not None:
72+
key = cohort.cohort_label
73+
if is_actionable:
74+
cohort_results[key] = CohortGroupResult(
75+
cohort.cohort_group, Status.actionable, [], cohort.positive_description, suppression_reasons
76+
)
77+
else:
78+
cohort_results[key] = CohortGroupResult(
79+
cohort.cohort_group,
80+
Status.not_actionable,
81+
suppression_reasons,
82+
cohort.positive_description,
83+
suppression_reasons,
84+
)
85+
86+
def evaluate_rules_priority_group(
87+
self, person: Person, rules_group: Iterator[IterationRule]
88+
) -> tuple[eligibility_status.Status, list[eligibility_status.Reason], bool]:
89+
is_rule_stop = False
90+
exclusion_reasons = []
91+
best_status = eligibility_status.Status.not_eligible
92+
93+
for rule in rules_group:
94+
is_rule_stop = rule.rule_stop or is_rule_stop
95+
rule_calculator = RuleCalculator(person=person, rule=rule)
96+
status, reason = rule_calculator.evaluate_exclusion()
97+
if status.is_exclusion:
98+
best_status = eligibility_status.Status.best(status, best_status)
99+
exclusion_reasons.append(reason)
100+
else:
101+
best_status = eligibility_status.Status.actionable
102+
103+
return best_status, exclusion_reasons, is_rule_stop
104+
105+
@staticmethod
106+
def get_exclusion_rules(cohort: IterationCohort, filter_rules: Iterable[IterationRule]) -> Iterator[IterationRule]:
107+
return (
108+
ir
109+
for ir in filter_rules
110+
if ir.cohort_label is None
111+
or cohort.cohort_label == ir.cohort_label
112+
or (isinstance(ir.cohort_label, (list, set, tuple)) and cohort.cohort_label in ir.cohort_label)
113+
)

0 commit comments

Comments
 (0)