22from datetime import datetime , timedelta
33from decimal import Decimal
44from typing import Union
5+ from datetime import datetime , date
56
67from .generic_utils import nhs_number_mod11_check , is_valid_simple_snomed
78
@@ -82,7 +83,7 @@ def for_list(
8283 raise ValueError (f"{ field_location } must be an array of non-empty objects" )
8384
8485 @staticmethod
85- def for_date (field_value : str , field_location : str ):
86+ def for_date (field_value : str , field_location : str , future_date_allowed : bool = False ):
8687 """
8788 Apply pre-validation to a date field to ensure that it is a string (JSON dates must be
8889 written as strings) containing a valid date in the format "YYYY-MM-DD"
@@ -91,12 +92,16 @@ def for_date(field_value: str, field_location: str):
9192 raise TypeError (f"{ field_location } must be a string" )
9293
9394 try :
94- datetime .strptime (field_value , "%Y-%m-%d" ).date ()
95+ parsed_date = datetime .strptime (field_value , "%Y-%m-%d" ).date ()
9596 except ValueError as value_error :
9697 raise ValueError (
9798 f'{ field_location } must be a valid date string in the format "YYYY-MM-DD"'
9899 ) from value_error
99100
101+ # Enforce future date rule using central checker after successful parse
102+ if not future_date_allowed and PreValidation .check_if_future_date (parsed_date ):
103+ raise ValueError (f"{ field_location } must not be in the future" )
104+
100105 @staticmethod
101106 def for_date_time (field_value : str , field_location : str , strict_timezone : bool = True ):
102107 """
@@ -116,11 +121,13 @@ def for_date_time(field_value: str, field_location: str, strict_timezone: bool =
116121 "- 'YYYY-MM-DD' — Full date only"
117122 "- 'YYYY-MM-DDThh:mm:ss%z' — Full date and time with timezone (e.g. +00:00 or +01:00)"
118123 "- 'YYYY-MM-DDThh:mm:ss.f%z' — Full date and time with milliseconds and timezone"
124+ "- Date must not be in the future."
119125 )
120-
121126 if strict_timezone :
122- error_message += "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n "
123- error_message += f"Note that partial dates are not allowed for { field_location } in this service."
127+ error_message += (
128+ "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n "
129+ f"Note that partial dates are not allowed for { field_location } in this service.\n "
130+ )
124131
125132 allowed_suffixes = {"+00:00" , "+01:00" , "+0000" , "+0100" ,}
126133
@@ -133,10 +140,13 @@ def for_date_time(field_value: str, field_location: str, strict_timezone: bool =
133140 for fmt in formats :
134141 try :
135142 fhir_date = datetime .strptime (field_value , fmt )
136-
143+ # Enforce future-date rule using central checker after successful parse
144+ if PreValidation .check_if_future_date (fhir_date ):
145+ raise ValueError (f"{ field_location } must not be in the future" )
146+ # After successful parse, enforce timezone and future-date rules
137147 if strict_timezone and fhir_date .tzinfo is not None :
138- if not any (field_value .endswith (suffix ) for suffix in allowed_suffixes ):
139- raise ValueError (error_message )
148+ if not any (field_value .endswith (suffix ) for suffix in allowed_suffixes ):
149+ raise ValueError (error_message )
140150 return fhir_date .isoformat ()
141151 except ValueError :
142152 continue
@@ -234,3 +244,16 @@ def for_nhs_number(nhs_number: str, field_location: str):
234244 """
235245 if not nhs_number_mod11_check (nhs_number ):
236246 raise ValueError (f"{ field_location } is not a valid NHS number" )
247+
248+ @staticmethod
249+ def check_if_future_date (parsed_value : date | datetime ):
250+ """
251+ Ensure a parsed date or datetime object is not in the future.
252+ """
253+ if isinstance (parsed_value , datetime ):
254+ now = datetime .now (parsed_value .tzinfo ) if parsed_value .tzinfo else datetime .now ()
255+ elif isinstance (parsed_value , date ):
256+ now = datetime .now ().date ()
257+ if parsed_value > now :
258+ return True
259+ return False
0 commit comments