Skip to content

Commit 28d2e75

Browse files
committed
eli-579 amending token processor to use new token parser
1 parent c023070 commit 28d2e75

3 files changed

Lines changed: 366 additions & 4 deletions

File tree

src/eligibility_signposting_api/services/processors/token_processor.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
from eligibility_signposting_api.config.constants import ALLOWED_CONDITIONS
99
from eligibility_signposting_api.model.person import Person
10+
from eligibility_signposting_api.services.processors.derived_values import (
11+
DerivedValueContext,
12+
DerivedValueRegistry,
13+
)
1014
from eligibility_signposting_api.services.processors.token_parser import ParsedToken, TokenParser
1115

1216
TARGET_ATTRIBUTE_LEVEL = "TARGET"
@@ -21,6 +25,7 @@
2125
"BOOKED_APPOINTMENT_PROVIDER",
2226
"LAST_INVITE_DATE",
2327
"LAST_INVITE_STATUS",
28+
"NEXT_DOSE_DUE",
2429
}
2530

2631

@@ -85,13 +90,88 @@ def get_token_replacement(token: str, person_data: list[dict], present_attribute
8590
if TokenProcessor.should_replace_with_empty(parsed_token, present_attributes):
8691
return ""
8792

93+
# Check if this is a derived value (has a function like ADD_DAYS)
94+
if parsed_token.function_name:
95+
return TokenProcessor.get_derived_value(parsed_token, person_data, present_attributes, token)
96+
8897
found_attribute, key_to_replace = TokenProcessor.find_matching_attribute(parsed_token, person_data)
8998

9099
if not found_attribute or not key_to_replace:
91100
TokenProcessor.handle_token_not_found(parsed_token, token)
101+
# handle_token_not_found always raises, but the type checker needs help
102+
msg = "Unreachable"
103+
raise RuntimeError(msg) # pragma: no cover
92104

93105
return TokenProcessor.apply_formatting(found_attribute, key_to_replace, parsed_token.format)
94106

107+
@staticmethod
108+
def get_derived_value(
109+
parsed_token: ParsedToken,
110+
person_data: list[dict],
111+
present_attributes: set,
112+
token: str,
113+
) -> str:
114+
"""Calculate a derived value using the registered handler.
115+
116+
Args:
117+
parsed_token: The parsed token containing function information
118+
person_data: List of person attribute dictionaries
119+
present_attributes: Set of attribute types present in person data
120+
token: The original token string for error messages
121+
122+
Returns:
123+
The calculated derived value as a string
124+
125+
Raises:
126+
ValueError: If no handler is registered or attribute not found
127+
"""
128+
registry = DerivedValueRegistry()
129+
130+
function_name = parsed_token.function_name
131+
if not function_name:
132+
message = f"No function specified in token '{token}'."
133+
raise ValueError(message)
134+
135+
if not registry.has_handler(function_name):
136+
message = f"Unknown function '{function_name}' in token '{token}'."
137+
raise ValueError(message)
138+
139+
# For TARGET level tokens, validate the condition is allowed
140+
if parsed_token.attribute_level == TARGET_ATTRIBUTE_LEVEL:
141+
is_allowed_condition = parsed_token.attribute_name in ALLOWED_CONDITIONS.__args__
142+
is_allowed_target_attr = parsed_token.attribute_value in ALLOWED_TARGET_ATTRIBUTES
143+
144+
# If condition is not allowed, raise error
145+
if not is_allowed_condition:
146+
TokenProcessor.handle_token_not_found(parsed_token, token)
147+
148+
# If vaccine type is not in person data but is allowed, return empty
149+
if parsed_token.attribute_name not in present_attributes:
150+
if is_allowed_target_attr:
151+
return ""
152+
TokenProcessor.handle_token_not_found(parsed_token, token)
153+
154+
try:
155+
target_attribute = parsed_token.attribute_value or parsed_token.attribute_name
156+
source_attribute = registry.get_source_attribute(function_name, target_attribute)
157+
158+
context = DerivedValueContext(
159+
person_data=person_data,
160+
attribute_name=parsed_token.attribute_name,
161+
source_attribute=source_attribute,
162+
function_args=parsed_token.function_args,
163+
date_format=parsed_token.format,
164+
)
165+
166+
return registry.calculate(
167+
function_name=function_name,
168+
context=context,
169+
)
170+
except ValueError as e:
171+
# Re-raise with more context
172+
message = f"Error calculating derived value for token '{token}': {e}"
173+
raise ValueError(message) from e
174+
95175
@staticmethod
96176
def should_replace_with_empty(parsed_token: ParsedToken, present_attributes: set) -> bool:
97177
is_target_level = parsed_token.attribute_level == TARGET_ATTRIBUTE_LEVEL

tests/unit/services/processors/test_token_processor.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,7 @@ def test_valid_token_valid_format_should_replace_with_date_formatting(self):
330330
@pytest.mark.parametrize(
331331
"token_format",
332332
[
333-
":INVALID_DATE_FORMATTER(%ABC)",
334-
":INVALID_DATE_FORMATTER(19900327)",
335333
":()",
336-
":FORMAT(DATE)",
337-
":FORMAT(BLAH)",
338334
":DATE[%d %B %Y]",
339335
":DATE(%A, (%d) %B %Y)",
340336
],
@@ -354,6 +350,31 @@ def test_valid_token_invalid_format_should_raise_error(self, token_format: str):
354350
with pytest.raises(ValueError, match="Invalid token format."):
355351
TokenProcessor.find_and_replace_tokens(person, condition)
356352

353+
@pytest.mark.parametrize(
354+
("token_format", "func_name"),
355+
[
356+
(":INVALID_DATE_FORMATTER(%ABC)", "INVALID_DATE_FORMATTER"),
357+
(":INVALID_DATE_FORMATTER(19900327)", "INVALID_DATE_FORMATTER"),
358+
(":FORMAT(DATE)", "FORMAT"),
359+
(":FORMAT(BLAH)", "FORMAT"),
360+
],
361+
)
362+
def test_unknown_function_raises_error(self, token_format: str, func_name: str):
363+
"""Test that unknown function names raise ValueError with appropriate message."""
364+
person = Person([{"ATTRIBUTE_TYPE": "PERSON", "AGE": "30", "DATE_OF_BIRTH": "19900327"}])
365+
366+
condition = Condition(
367+
condition_name=ConditionName("You had your RSV vaccine"),
368+
status=Status.actionable,
369+
status_text=StatusText(f"Your birthday is on [[PERSON.DATE_OF_BIRTH{token_format}]]"),
370+
cohort_results=[],
371+
suitability_rules=[],
372+
actions=[],
373+
)
374+
375+
with pytest.raises(ValueError, match=f"Unknown function '{func_name}'"):
376+
TokenProcessor.find_and_replace_tokens(person, condition)
377+
357378
@pytest.mark.parametrize(
358379
("token_format", "expected"),
359380
[

0 commit comments

Comments
 (0)