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,30 @@ 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 ] = {}
119+ cohort_results : dict [str , CohortGroupResult ] = {}
102120
103121 filter_rules , suppression_rules = self .get_rules_by_type (active_iteration )
122+
104123 for cohort in sorted (active_iteration .iteration_cohorts , key = attrgetter ("priority" )):
105124 # Base Eligibility - check
106- if cohort .cohort_label in self .person_cohorts or cohort . cohort_label == magic_cohort :
125+ if cohort .cohort_label in self .person_cohorts or active_iteration . has_magic_cohort :
107126 # Eligibility - check
108127 if self .is_eligible_by_filter_rules (cohort , cohort_results , filter_rules ):
109128 # Actionability - evaluation
110129 self .evaluate_suppression_rules (cohort , cohort_results , suppression_rules )
111130
112131 # Not base eligible
113132 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 ,
133+ cohort_results [cohort .cohort_label ] = CohortGroupResult (
134+ ( cohort .cohort_group ) ,
116135 Status .not_eligible ,
117136 [],
118- str ( cohort .negative_description ) ,
137+ cohort .negative_description ,
119138 )
120139
121140 # Determine Result between cohorts - get the best
122- status , best_cohorts = self .get_best_cohort (cohort_results )
141+ status , best_cohorts = self .get_the_best_cohort_memberships (cohort_results )
142+
123143 iteration_results [active_iteration .name ] = IterationResult (status , best_cohorts )
124144
125145 # Determine results between iterations - get the best
@@ -130,20 +150,50 @@ def evaluate_eligibility(self) -> eligibility.EligibilityStatus:
130150 condition_results [condition_name ] = best_candidate
131151
132152 # 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- ]
153+ final_result = self .build_condition_results (condition_results )
141154 return eligibility .EligibilityStatus (conditions = final_result )
142155
156+ @staticmethod
157+ def build_condition_results (
158+ condition_results : dict [ConditionName , IterationResult ],
159+ ) -> list [Condition ]:
160+ conditions : list [Condition ] = []
161+ # iterate over conditions
162+ for condition_name , active_iteration_result in condition_results .items ():
163+ grouped_cohort_results = defaultdict (list )
164+ # iterate over cohorts and group them by status and cohort_group
165+ for cohort_result in active_iteration_result .cohort_results :
166+ if active_iteration_result .status == cohort_result .status :
167+ grouped_cohort_results [cohort_result .cohort_code ].append (cohort_result )
168+
169+ # deduplicate grouped cohort results by cohort_code
170+ deduplicated_cohort_results = [
171+ CohortGroupResult (
172+ cohort_code = group_cohort_code ,
173+ status = group [0 ].status ,
174+ # Flatten all reasons from the group
175+ reasons = [reason for cohort in group for reason in cohort .reasons ],
176+ # get the first nonempty description
177+ description = next ((c .description for c in group if c .description ), group [0 ].description ),
178+ )
179+ for group_cohort_code , group in grouped_cohort_results .items ()
180+ if group
181+ ]
182+
183+ # return condition with cohort results
184+ conditions .append (
185+ Condition (
186+ condition_name = condition_name ,
187+ status = active_iteration_result .status ,
188+ cohort_results = list (deduplicated_cohort_results ),
189+ )
190+ )
191+ return conditions
192+
143193 def is_eligible_by_filter_rules (
144194 self ,
145195 cohort : IterationCohort ,
146- cohort_results : dict [str , CohortResult ],
196+ cohort_results : dict [str , CohortGroupResult ],
147197 filter_rules : Iterable [rules .IterationRule ],
148198 ) -> bool :
149199 is_eligible = True
@@ -156,11 +206,11 @@ def is_eligible_by_filter_rules(
156206 )
157207 if status .is_exclusion :
158208 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 ,
209+ cohort_results [cohort .cohort_label ] = CohortGroupResult (
210+ ( cohort .cohort_group ) ,
161211 Status .not_eligible ,
162212 [],
163- str ( cohort .negative_description ) ,
213+ cohort .negative_description ,
164214 )
165215 is_eligible = False
166216 break
@@ -169,7 +219,7 @@ def is_eligible_by_filter_rules(
169219 def evaluate_suppression_rules (
170220 self ,
171221 cohort : IterationCohort ,
172- cohort_results : dict [str , CohortResult ],
222+ cohort_results : dict [str , CohortGroupResult ],
173223 suppression_rules : Iterable [rules .IterationRule ],
174224 ) -> None :
175225 is_actionable : bool = True
@@ -191,18 +241,18 @@ def evaluate_suppression_rules(
191241 if cohort .cohort_label is not None :
192242 key = cohort .cohort_label
193243 if is_actionable :
194- cohort_results [key ] = CohortResult (
195- cohort .cohort_group if cohort . cohort_group else key ,
244+ cohort_results [key ] = CohortGroupResult (
245+ cohort .cohort_group ,
196246 Status .actionable ,
197247 [],
198- str ( cohort .positive_description ) ,
248+ cohort .positive_description ,
199249 )
200250 else :
201- cohort_results [key ] = CohortResult (
202- cohort .cohort_group if cohort . cohort_group else key ,
251+ cohort_results [key ] = CohortGroupResult (
252+ cohort .cohort_group ,
203253 Status .not_actionable ,
204254 suppression_reasons ,
205- str ( cohort .positive_description ) ,
255+ cohort .positive_description ,
206256 )
207257
208258 def evaluate_rules_priority_group (
0 commit comments