22
33from _operator import attrgetter
44from collections import defaultdict
5- from collections .abc import Collection , Iterable , Iterator , Mapping
65from dataclasses import dataclass , field
76from itertools import groupby
8- from typing import TYPE_CHECKING , Any
9-
10- from eligibility_signposting_api .audit .audit_context import AuditContext
11- from eligibility_signposting_api .services .processors .campaign_evaluator import CampaignEvaluator
12- from eligibility_signposting_api .services .processors .person_data_reader import PersonDataReader
13-
14- if TYPE_CHECKING :
15- from eligibility_signposting_api .model .rules import (
16- ActionsMapper ,
17- CampaignConfig ,
18- CampaignID ,
19- CampaignVersion ,
20- Iteration ,
21- IterationCohort ,
22- RuleName ,
23- RulePriority ,
24- RuleType ,
25- )
7+ from typing import TYPE_CHECKING
268
279from wireup import service
2810
29- from eligibility_signposting_api .model import eligibility_status , rules
11+ from eligibility_signposting_api .audit .audit_context import AuditContext
12+ from eligibility_signposting_api .model import eligibility_status
13+ from eligibility_signposting_api .model .campaign_config import (
14+ ActionsMapper ,
15+ CampaignConfig ,
16+ CampaignID ,
17+ CampaignVersion ,
18+ Iteration ,
19+ IterationRule ,
20+ RuleName ,
21+ RulePriority ,
22+ RuleType ,
23+ )
3024from eligibility_signposting_api .model .eligibility_status import (
3125 ActionCode ,
3226 ActionDescription ,
4438from eligibility_signposting_api .services .calculators .rule_calculator import (
4539 RuleCalculator ,
4640)
41+ from eligibility_signposting_api .services .processors .campaign_evaluator import CampaignEvaluator
42+ from eligibility_signposting_api .services .processors .rule_processor import RuleProcessor
43+
44+ if TYPE_CHECKING :
45+ from collections .abc import Collection
4746
48- Row = Collection [ Mapping [ str , Any ]]
47+ from eligibility_signposting_api . model . person import Person
4948
5049
5150@service
5251class EligibilityCalculatorFactory :
5352 @staticmethod
54- def get (person_data : Row , campaign_configs : Collection [rules . CampaignConfig ]) -> EligibilityCalculator :
55- return EligibilityCalculator (person_data = person_data , campaign_configs = campaign_configs )
53+ def get (person : Person , campaign_configs : Collection [CampaignConfig ]) -> EligibilityCalculator :
54+ return EligibilityCalculator (person = person , campaign_configs = campaign_configs )
5655
5756
5857@dataclass
5958class EligibilityCalculator :
60- person_data : Row
61- campaign_configs : Collection [rules . CampaignConfig ]
59+ person : Person
60+ campaign_configs : Collection [CampaignConfig ]
6261
6362 campaign_evaluator : CampaignEvaluator = field (default_factory = CampaignEvaluator )
64- person_data_reader : PersonDataReader = field (default_factory = PersonDataReader )
63+ rule_processor : RuleProcessor = field (default_factory = RuleProcessor )
6564
6665 results : list [eligibility_status .Condition ] = field (default_factory = list )
6766
@@ -88,56 +87,34 @@ def get_the_best_cohort_memberships(
8887
8988 return best_status , best_cohorts
9089
91- @staticmethod
92- def get_exclusion_rules (
93- cohort : IterationCohort , filter_rules : Iterable [rules .IterationRule ]
94- ) -> Iterator [rules .IterationRule ]:
95- return (
96- ir
97- for ir in filter_rules
98- if ir .cohort_label is None
99- or cohort .cohort_label == ir .cohort_label
100- or (isinstance (ir .cohort_label , (list , set , tuple )) and cohort .cohort_label in ir .cohort_label )
101- )
102-
103- @staticmethod
104- def get_rules_by_type (
105- active_iteration : Iteration ,
106- ) -> tuple [tuple [rules .IterationRule , ...], tuple [rules .IterationRule , ...]]:
107- filter_rules , suppression_rules = (
108- tuple (rule for rule in active_iteration .iteration_rules if attrgetter ("type" )(rule ) == rule_type )
109- for rule_type in (rules .RuleType .filter , rules .RuleType .suppression )
110- )
111- return filter_rules , suppression_rules
112-
11390 @staticmethod
11491 def get_action_rules_components (
11592 active_iteration : Iteration , rule_type : RuleType
116- ) -> tuple [tuple [rules . IterationRule , ...], ActionsMapper , str | None ]:
93+ ) -> tuple [tuple [IterationRule , ...], ActionsMapper , str | None ]:
11794 action_rules = tuple (rule for rule in active_iteration .iteration_rules if rule .type in rule_type )
11895
11996 routing_map = {
120- rules . RuleType .redirect : active_iteration .default_comms_routing ,
121- rules . RuleType .not_eligible_actions : active_iteration .default_not_eligible_routing ,
122- rules . RuleType .not_actionable_actions : active_iteration .default_not_actionable_routing ,
97+ RuleType .redirect : active_iteration .default_comms_routing ,
98+ RuleType .not_eligible_actions : active_iteration .default_not_eligible_routing ,
99+ RuleType .not_actionable_actions : active_iteration .default_not_actionable_routing ,
123100 }
124101
125102 default_comms = routing_map .get (rule_type )
126103 action_mapper = active_iteration .actions_mapper
127104 return action_rules , action_mapper , default_comms
128105
129- def evaluate_eligibility (
106+ def get_eligibility_status (
130107 self , include_actions : str , conditions : list [str ], category : str
131108 ) -> eligibility_status .EligibilityStatus :
132109 include_actions_flag = include_actions .upper () == "Y"
133110 condition_results : dict [ConditionName , IterationResult ] = {}
134- actions : list [SuggestedAction ] | None = []
135111 action_rule_priority , action_rule_name = None , None
136112
137113 requested_grouped_campaigns = self .campaign_evaluator .get_requested_grouped_campaigns (
138114 self .campaign_configs , conditions , category
139115 )
140116 for condition_name , campaign_group in requested_grouped_campaigns :
117+ actions : list [SuggestedAction ] | None = []
141118 best_active_iteration : Iteration | None
142119 best_candidate : IterationResult
143120 best_campaign_id : CampaignID | None
@@ -168,9 +145,9 @@ def evaluate_eligibility(
168145 condition_results [condition_name ] = best_candidate
169146
170147 status_to_rule_type = {
171- Status .actionable : rules . RuleType .redirect ,
172- Status .not_eligible : rules . RuleType .not_eligible_actions ,
173- Status .not_actionable : rules . RuleType .not_actionable_actions ,
148+ Status .actionable : RuleType .redirect ,
149+ Status .not_eligible : RuleType .not_eligible_actions ,
150+ Status .not_actionable : RuleType .not_actionable_actions ,
174151 }
175152
176153 if best_candidate .status in status_to_rule_type and best_active_iteration is not None :
@@ -192,8 +169,6 @@ def evaluate_eligibility(
192169
193170 # add actions to condition results
194171 condition_results [condition_name ].actions = actions
195- # reset actions for the next condition
196- actions : list [SuggestedAction ] | None = []
197172
198173 # add audit data
199174 AuditContext .append_audit_condition (
@@ -216,7 +191,9 @@ def get_iteration_results(
216191 ] = {}
217192 for cc in campaign_group :
218193 active_iteration = cc .current_iteration
219- cohort_results : dict [str , CohortGroupResult ] = self .get_cohort_results (active_iteration )
194+ cohort_results : dict [str , CohortGroupResult ] = self .rule_processor .get_cohort_group_results (
195+ self .person , active_iteration
196+ )
220197
221198 # Determine Result between cohorts - get the best
222199 status , best_cohorts = self .get_the_best_cohort_memberships (cohort_results )
@@ -242,7 +219,7 @@ def handle_action_rules(
242219 for _ , rule_group in groupby (sorted_rules_by_priority , key = priority_getter ):
243220 rule_group_list = list (rule_group )
244221 matcher_matched_list = [
245- RuleCalculator (person_data = self .person_data , rule = rule ).evaluate_exclusion ()[1 ].matcher_matched
222+ RuleCalculator (person = self .person , rule = rule ).evaluate_exclusion ()[1 ].matcher_matched
246223 for rule in rule_group_list
247224 ]
248225
@@ -257,29 +234,6 @@ def handle_action_rules(
257234
258235 return actions , matched_action_rule_priority , matched_action_rule_name
259236
260- def get_cohort_results (self , active_iteration : rules .Iteration ) -> dict [str , CohortGroupResult ]:
261- cohort_results : dict [str , CohortGroupResult ] = {}
262- filter_rules , suppression_rules = self .get_rules_by_type (active_iteration )
263- for cohort in sorted (active_iteration .iteration_cohorts , key = attrgetter ("priority" )):
264- # Base Eligibility - check
265- person_cohorts = self .person_data_reader .get_person_cohorts (self .person_data )
266- if cohort .cohort_label in person_cohorts or cohort .is_magic_cohort :
267- # Eligibility - check
268- if self .is_eligible_by_filter_rules (cohort , cohort_results , filter_rules ):
269- # Actionability - evaluation
270- self .evaluate_suppression_rules (cohort , cohort_results , suppression_rules )
271-
272- # Not base eligible
273- elif cohort .cohort_label is not None :
274- cohort_results [cohort .cohort_label ] = CohortGroupResult (
275- cohort .cohort_group ,
276- Status .not_eligible ,
277- [],
278- cohort .negative_description ,
279- [],
280- )
281- return cohort_results
282-
283237 @staticmethod
284238 def build_condition_results (condition_results : dict [ConditionName , IterationResult ]) -> list [Condition ]:
285239 conditions : list [Condition ] = []
@@ -318,85 +272,6 @@ def build_condition_results(condition_results: dict[ConditionName, IterationResu
318272 )
319273 return conditions
320274
321- def is_eligible_by_filter_rules (
322- self ,
323- cohort : IterationCohort ,
324- cohort_results : dict [str , CohortGroupResult ],
325- filter_rules : Iterable [rules .IterationRule ],
326- ) -> bool :
327- is_eligible = True
328- priority_getter = attrgetter ("priority" )
329- sorted_rules_by_priority = sorted (self .get_exclusion_rules (cohort , filter_rules ), key = priority_getter )
330-
331- for _ , rule_group in groupby (sorted_rules_by_priority , key = priority_getter ):
332- status , group_exclusion_reasons , _ = self .evaluate_rules_priority_group (rule_group )
333- if status .is_exclusion :
334- if cohort .cohort_label is not None :
335- cohort_results [cohort .cohort_label ] = CohortGroupResult (
336- (cohort .cohort_group ),
337- Status .not_eligible ,
338- [],
339- cohort .negative_description ,
340- group_exclusion_reasons ,
341- )
342- is_eligible = False
343- break
344- return is_eligible
345-
346- def evaluate_suppression_rules (
347- self ,
348- cohort : IterationCohort ,
349- cohort_results : dict [str , CohortGroupResult ],
350- suppression_rules : Iterable [rules .IterationRule ],
351- ) -> None :
352- is_actionable : bool = True
353- priority_getter = attrgetter ("priority" )
354- suppression_reasons = []
355-
356- sorted_rules_by_priority = sorted (self .get_exclusion_rules (cohort , suppression_rules ), key = priority_getter )
357-
358- for _ , rule_group in groupby (sorted_rules_by_priority , key = priority_getter ):
359- status , group_exclusion_reasons , rule_stop = self .evaluate_rules_priority_group (rule_group )
360- if status .is_exclusion :
361- is_actionable = False
362- suppression_reasons .extend (group_exclusion_reasons )
363- if rule_stop :
364- break
365-
366- if cohort .cohort_label is not None :
367- key = cohort .cohort_label
368- if is_actionable :
369- cohort_results [key ] = CohortGroupResult (
370- cohort .cohort_group , Status .actionable , [], cohort .positive_description , suppression_reasons
371- )
372- else :
373- cohort_results [key ] = CohortGroupResult (
374- cohort .cohort_group ,
375- Status .not_actionable ,
376- suppression_reasons ,
377- cohort .positive_description ,
378- suppression_reasons ,
379- )
380-
381- def evaluate_rules_priority_group (
382- self , rules_group : Iterator [rules .IterationRule ]
383- ) -> tuple [eligibility_status .Status , list [eligibility_status .Reason ], bool ]:
384- is_rule_stop = False
385- exclusion_reasons = []
386- best_status = eligibility_status .Status .not_eligible
387-
388- for rule in rules_group :
389- is_rule_stop = rule .rule_stop or is_rule_stop
390- rule_calculator = RuleCalculator (person_data = self .person_data , rule = rule )
391- status , reason = rule_calculator .evaluate_exclusion ()
392- if status .is_exclusion :
393- best_status = eligibility_status .Status .best (status , best_status )
394- exclusion_reasons .append (reason )
395- else :
396- best_status = eligibility_status .Status .actionable
397-
398- return best_status , exclusion_reasons , is_rule_stop
399-
400275 @staticmethod
401276 def get_actions_from_comms (action_mapper : ActionsMapper , comms : str ) -> list [SuggestedAction ] | None :
402277 suggested_actions : list [SuggestedAction ] = []
0 commit comments