11from __future__ import annotations
22
33from _operator import attrgetter
4+ from collections import defaultdict
45from collections .abc import Collection , Iterable , Iterator , Mapping
56from dataclasses import dataclass , field
67from itertools import groupby
1314
1415from eligibility_signposting_api .model import eligibility , rules
1516from eligibility_signposting_api .model .eligibility import (
16- CohortResult ,
17+ CohortGroupResult ,
1718 Condition ,
1819 ConditionName ,
1920 IterationResult ,
2021 Status ,
2122)
22- from eligibility_signposting_api .services .calculators .rule_calculator import RuleCalculator
23+ from eligibility_signposting_api .services .calculators .rule_calculator import (
24+ RuleCalculator ,
25+ )
2326
2427Row = Collection [Mapping [str , Any ]]
25- magic_cohort = "elid_all_people"
2628
2729
2830@service
@@ -49,23 +51,39 @@ def campaigns_grouped_by_condition_name(
4951 ) -> Iterator [tuple [eligibility .ConditionName , list [rules .CampaignConfig ]]]:
5052 """Generator function to iterate over campaign groups by condition name."""
5153 for condition_name , campaign_group in groupby (
52- sorted (self .active_campaigns , key = attrgetter ("target" )), key = attrgetter ("target" )
54+ sorted (self .active_campaigns , key = attrgetter ("target" )),
55+ key = attrgetter ("target" ),
5356 ):
5457 yield condition_name , list (campaign_group )
5558
5659 @property
5760 def person_cohorts (self ) -> set [str ]:
5861 cohorts_row : Mapping [str , dict [str , dict [str , dict [str , Any ]]]] = next (
59- (row for row in self .person_data if row .get ("ATTRIBUTE_TYPE" ) == "COHORTS" ), {}
62+ (row for row in self .person_data if row .get ("ATTRIBUTE_TYPE" ) == "COHORTS" ),
63+ {},
6064 )
6165 return set (cohorts_row .get ("COHORT_MAP" , {}).get ("cohorts" , {}).get ("M" , {}).keys ())
6266
6367 @staticmethod
64- def get_best_cohort (cohort_results : dict [str , CohortResult ]) -> tuple [Status , list [CohortResult ]]:
68+ def get_the_best_cohort_memberships (
69+ cohort_results : dict [str , CohortGroupResult ],
70+ ) -> tuple [Status , list [CohortGroupResult ]]:
6571 if not cohort_results :
6672 return eligibility .Status .not_eligible , []
73+
6774 best_status = eligibility .Status .best (* [result .status for result in cohort_results .values ()])
6875 best_cohorts = [result for result in cohort_results .values () if result .status == best_status ]
76+
77+ best_cohorts = [
78+ CohortGroupResult (
79+ cohort_code = cc .cohort_code ,
80+ status = cc .status ,
81+ reasons = cc .reasons ,
82+ description = (cc .description or "" ).strip () if cc .description else "" ,
83+ )
84+ for cc in best_cohorts
85+ ]
86+
6987 return best_status , best_cohorts
7088
7189 @staticmethod
@@ -98,28 +116,11 @@ def evaluate_eligibility(self) -> eligibility.EligibilityStatus:
98116 iteration_results : dict [str , IterationResult ] = {}
99117
100118 for active_iteration in [cc .current_iteration for cc in campaign_group ]:
101- cohort_results : dict [str , CohortResult ] = {}
102-
103- filter_rules , suppression_rules = self .get_rules_by_type (active_iteration )
104- for cohort in sorted (active_iteration .iteration_cohorts , key = attrgetter ("priority" )):
105- # Base Eligibility - check
106- if cohort .cohort_label in self .person_cohorts or cohort .cohort_label == magic_cohort :
107- # Eligibility - check
108- if self .is_eligible_by_filter_rules (cohort , cohort_results , filter_rules ):
109- # Actionability - evaluation
110- self .evaluate_suppression_rules (cohort , cohort_results , suppression_rules )
111-
112- # Not base eligible
113- elif cohort .cohort_label is not None :
114- cohort_results [cohort .cohort_label ] = CohortResult (
115- cohort .cohort_group if cohort .cohort_group else cohort .cohort_label ,
116- Status .not_eligible ,
117- [],
118- str (cohort .negative_description ),
119- )
119+ cohort_results : dict [str , CohortGroupResult ] = self .get_cohort_results (active_iteration )
120120
121121 # Determine Result between cohorts - get the best
122- status , best_cohorts = self .get_best_cohort (cohort_results )
122+ status , best_cohorts = self .get_the_best_cohort_memberships (cohort_results )
123+
123124 iteration_results [active_iteration .name ] = IterationResult (status , best_cohorts )
124125
125126 # Determine results between iterations - get the best
@@ -130,20 +131,71 @@ def evaluate_eligibility(self) -> eligibility.EligibilityStatus:
130131 condition_results [condition_name ] = best_candidate
131132
132133 # Consolidate all the results and return
133- final_result = [
134- Condition (
135- condition_name = condition_name ,
136- status = active_iteration_result .status ,
137- cohort_results = active_iteration_result .cohort_results ,
138- )
139- for condition_name , active_iteration_result in condition_results .items ()
140- ]
134+ final_result = self .build_condition_results (condition_results )
141135 return eligibility .EligibilityStatus (conditions = final_result )
142136
137+ def get_cohort_results (self , active_iteration : rules .Iteration ) -> dict [str , CohortGroupResult ]:
138+ cohort_results : dict [str , CohortGroupResult ] = {}
139+ filter_rules , suppression_rules = self .get_rules_by_type (active_iteration )
140+ for cohort in sorted (active_iteration .iteration_cohorts , key = attrgetter ("priority" )):
141+ # Base Eligibility - check
142+ if cohort .cohort_label in self .person_cohorts or cohort .is_magic_cohort :
143+ # Eligibility - check
144+ if self .is_eligible_by_filter_rules (cohort , cohort_results , filter_rules ):
145+ # Actionability - evaluation
146+ self .evaluate_suppression_rules (cohort , cohort_results , suppression_rules )
147+
148+ # Not base eligible
149+ elif cohort .cohort_label is not None :
150+ cohort_results [cohort .cohort_label ] = CohortGroupResult (
151+ (cohort .cohort_group ),
152+ Status .not_eligible ,
153+ [],
154+ cohort .negative_description ,
155+ )
156+ return cohort_results
157+
158+ @staticmethod
159+ def build_condition_results (
160+ condition_results : dict [ConditionName , IterationResult ],
161+ ) -> list [Condition ]:
162+ conditions : list [Condition ] = []
163+ # iterate over conditions
164+ for condition_name , active_iteration_result in condition_results .items ():
165+ grouped_cohort_results = defaultdict (list )
166+ # iterate over cohorts and group them by status and cohort_group
167+ for cohort_result in active_iteration_result .cohort_results :
168+ if active_iteration_result .status == cohort_result .status :
169+ grouped_cohort_results [cohort_result .cohort_code ].append (cohort_result )
170+
171+ # deduplicate grouped cohort results by cohort_code
172+ deduplicated_cohort_results = [
173+ CohortGroupResult (
174+ cohort_code = group_cohort_code ,
175+ status = group [0 ].status ,
176+ # Flatten all reasons from the group
177+ reasons = [reason for cohort in group for reason in cohort .reasons ],
178+ # get the first nonempty description
179+ description = next ((c .description for c in group if c .description ), group [0 ].description ),
180+ )
181+ for group_cohort_code , group in grouped_cohort_results .items ()
182+ if group
183+ ]
184+
185+ # return condition with cohort results
186+ conditions .append (
187+ Condition (
188+ condition_name = condition_name ,
189+ status = active_iteration_result .status ,
190+ cohort_results = list (deduplicated_cohort_results ),
191+ )
192+ )
193+ return conditions
194+
143195 def is_eligible_by_filter_rules (
144196 self ,
145197 cohort : IterationCohort ,
146- cohort_results : dict [str , CohortResult ],
198+ cohort_results : dict [str , CohortGroupResult ],
147199 filter_rules : Iterable [rules .IterationRule ],
148200 ) -> bool :
149201 is_eligible = True
@@ -156,11 +208,11 @@ def is_eligible_by_filter_rules(
156208 )
157209 if status .is_exclusion :
158210 if cohort .cohort_label is not None :
159- cohort_results [str ( cohort .cohort_label ) ] = CohortResult (
160- cohort .cohort_group if cohort . cohort_group else cohort . cohort_label ,
211+ cohort_results [cohort .cohort_label ] = CohortGroupResult (
212+ ( cohort .cohort_group ) ,
161213 Status .not_eligible ,
162214 [],
163- str ( cohort .negative_description ) ,
215+ cohort .negative_description ,
164216 )
165217 is_eligible = False
166218 break
@@ -169,7 +221,7 @@ def is_eligible_by_filter_rules(
169221 def evaluate_suppression_rules (
170222 self ,
171223 cohort : IterationCohort ,
172- cohort_results : dict [str , CohortResult ],
224+ cohort_results : dict [str , CohortGroupResult ],
173225 suppression_rules : Iterable [rules .IterationRule ],
174226 ) -> None :
175227 is_actionable : bool = True
@@ -191,18 +243,18 @@ def evaluate_suppression_rules(
191243 if cohort .cohort_label is not None :
192244 key = cohort .cohort_label
193245 if is_actionable :
194- cohort_results [key ] = CohortResult (
195- cohort .cohort_group if cohort . cohort_group else key ,
246+ cohort_results [key ] = CohortGroupResult (
247+ cohort .cohort_group ,
196248 Status .actionable ,
197249 [],
198- str ( cohort .positive_description ) ,
250+ cohort .positive_description ,
199251 )
200252 else :
201- cohort_results [key ] = CohortResult (
202- cohort .cohort_group if cohort . cohort_group else key ,
253+ cohort_results [key ] = CohortGroupResult (
254+ cohort .cohort_group ,
203255 Status .not_actionable ,
204256 suppression_reasons ,
205- str ( cohort .positive_description ) ,
257+ cohort .positive_description ,
206258 )
207259
208260 def evaluate_rules_priority_group (
0 commit comments