88from itertools import groupby
99from typing import Any
1010
11- from hamcrest .core .string_description import StringDescription
1211from wireup import service
1312
1413from eligibility_signposting_api .model import eligibility , rules
15- from eligibility_signposting_api .services .rules . operators import OperatorRegistry
14+ from eligibility_signposting_api .services .calculators . rule_calculator import RuleCalculator
1615
1716Row = Collection [Mapping [str , Any ]]
1817
@@ -153,45 +152,22 @@ def evaluate_priority_group(
153152 eligibility .Status .not_eligible if exclude_capable_rules else eligibility .Status .actionable
154153 )
155154 for iteration_rule in exclude_capable_rules :
156- exclusion , reason = self .evaluate_exclusion (iteration_rule )
157- if exclusion :
158- best_status_so_far_for_priority_group = self .best_status (
159- iteration_rule .type , best_status_so_far_for_priority_group
155+ rule_calculator = RuleCalculator (person_data = self .person_data , rule = iteration_rule )
156+ status , reason = rule_calculator .evaluate_exclusion ()
157+ if status .is_exclusion :
158+ best_status_so_far_for_priority_group = eligibility .Status .best (
159+ status , best_status_so_far_for_priority_group
160160 )
161161 exclusion_reasons .append (reason )
162162 else :
163163 best_status_so_far_for_priority_group = eligibility .Status .actionable
164164 actionable_reasons .append (reason )
165165 return (
166- self . worst_status (best_status_so_far_for_priority_group , worst_status_so_far_for_condition ),
166+ eligibility . Status . worst (best_status_so_far_for_priority_group , worst_status_so_far_for_condition ),
167167 actionable_reasons ,
168168 exclusion_reasons ,
169169 )
170170
171- @staticmethod
172- def worst_status (* statuses : eligibility .Status ) -> eligibility .Status :
173- """Pick the worst status from those given.
174-
175- Here "worst" means furthest from being able to access vaccination, so not-eligible is "worse" than
176- not-actionable, and not-actionable is "worse" than actionable.
177- """
178- return min (statuses )
179-
180- @staticmethod
181- def best_status (rule_type : rules .RuleType , status : eligibility .Status ) -> eligibility .Status :
182- """Pick the best status between the existing status, and the status implied by
183- the rule excluding the person from vaccination.
184-
185- Here "best" means closest to being able to access vaccination, so not-actionable is "better" than
186- not-eligible, and actionable is "better" than not-actionable.
187- """
188- return max (
189- status ,
190- eligibility .Status .not_eligible
191- if rule_type == rules .RuleType .filter
192- else eligibility .Status .not_actionable ,
193- )
194-
195171 def get_base_eligible_conditions (
196172 self ,
197173 base_eligible_evaluations : Mapping [
@@ -204,48 +180,7 @@ def get_base_eligible_conditions(
204180 # for each condition for which the person is base eligible:
205181 # what is the "best" status, i.e. closest to actionable? Add the condition to the result with that status.
206182 for condition_name , reasons_by_status in base_eligible_evaluations .items ():
207- best_status = max ( reasons_by_status .keys ())
183+ best_status = eligibility . Status . best ( * list ( reasons_by_status .keys () ))
208184 self .results [condition_name ] = eligibility .Condition (
209185 condition_name = condition_name , status = best_status , reasons = reasons_by_status [best_status ]
210186 )
211-
212- def evaluate_exclusion (self , iteration_rule : rules .IterationRule ) -> tuple [bool , eligibility .Reason ]:
213- """Evaluate if a particular rule excludes this person. Return the result, and the reason for the result."""
214- attribute_value = self .get_attribute_value (iteration_rule )
215- exclusion , reason = self .evaluate_rule (iteration_rule , attribute_value )
216- reason = eligibility .Reason (
217- rule_name = eligibility .RuleName (iteration_rule .name ),
218- rule_type = eligibility .RuleType (iteration_rule .type ),
219- rule_result = eligibility .RuleResult (
220- f"Rule { iteration_rule .name !r} ({ iteration_rule .description !r} ) "
221- f"{ '' if exclusion else 'not ' } excluding - "
222- f"{ iteration_rule .attribute_name !r} { iteration_rule .comparator !r} { reason } "
223- ),
224- )
225- return exclusion , reason
226-
227- def get_attribute_value (self , iteration_rule : rules .IterationRule ) -> str | None :
228- """Pull out the correct attribute for a rule from the person's data."""
229- match iteration_rule .attribute_level :
230- case rules .RuleAttributeLevel .PERSON :
231- person : Mapping [str , str | None ] | None = next (
232- (r for r in self .person_data if r .get ("ATTRIBUTE_TYPE" , "" ) == "PERSON" ), None
233- )
234- attribute_value = person .get (iteration_rule .attribute_name ) if person else None
235- case _: # pragma: no cover
236- msg = f"{ iteration_rule .attribute_level } not implemented"
237- raise NotImplementedError (msg )
238- return attribute_value
239-
240- @staticmethod
241- def evaluate_rule (iteration_rule : rules .IterationRule , attribute_value : str | None ) -> tuple [bool , str ]:
242- """Evaluate a rule against a person data attribute. Return the result, and the reason for the result."""
243- matcher_class = OperatorRegistry .get (iteration_rule .operator )
244- matcher = matcher_class (rule_value = iteration_rule .comparator )
245-
246- reason = StringDescription ()
247- if matcher .matches (attribute_value ):
248- matcher .describe_match (attribute_value , reason )
249- return True , str (reason )
250- matcher .describe_mismatch (attribute_value , reason )
251- return False , str (reason )
0 commit comments