1- from datetime import datetime
1+ from datetime import date , datetime
22from decimal import Decimal
33from typing import Union
44
@@ -80,7 +80,7 @@ def for_list(
8080 raise ValueError (f"{ field_location } must be an array of non-empty objects" )
8181
8282 @staticmethod
83- def for_date (field_value : str , field_location : str ):
83+ def for_date (field_value : str , field_location : str , future_date_allowed : bool = False ):
8484 """
8585 Apply pre-validation to a date field to ensure that it is a string (JSON dates must be
8686 written as strings) containing a valid date in the format "YYYY-MM-DD"
@@ -89,10 +89,14 @@ def for_date(field_value: str, field_location: str):
8989 raise TypeError (f"{ field_location } must be a string" )
9090
9191 try :
92- datetime .strptime (field_value , "%Y-%m-%d" ).date ()
92+ parsed_date = datetime .strptime (field_value , "%Y-%m-%d" ).date ()
9393 except ValueError as value_error :
9494 raise ValueError (f'{ field_location } must be a valid date string in the format "YYYY-MM-DD"' ) from value_error
9595
96+ # Enforce future date rule using central checker after successful parse
97+ if not future_date_allowed and PreValidation .check_if_future_date (parsed_date ):
98+ raise ValueError (f"{ field_location } must not be in the future" )
99+
96100 @staticmethod
97101 def for_date_time (field_value : str , field_location : str , strict_timezone : bool = True ):
98102 """
@@ -112,11 +116,13 @@ def for_date_time(field_value: str, field_location: str, strict_timezone: bool =
112116 "- 'YYYY-MM-DD' — Full date only"
113117 "- 'YYYY-MM-DDThh:mm:ss%z' — Full date and time with timezone (e.g. +00:00 or +01:00)"
114118 "- 'YYYY-MM-DDThh:mm:ss.f%z' — Full date and time with milliseconds and timezone"
119+ "- Date must not be in the future."
115120 )
116-
117121 if strict_timezone :
118- error_message += "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n "
119- error_message += f"Note that partial dates are not allowed for { field_location } in this service."
122+ error_message += (
123+ "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n "
124+ f"Note that partial dates are not allowed for { field_location } in this service.\n "
125+ )
120126
121127 allowed_suffixes = {
122128 "+00:00" ,
@@ -135,7 +141,10 @@ def for_date_time(field_value: str, field_location: str, strict_timezone: bool =
135141 for fmt in formats :
136142 try :
137143 fhir_date = datetime .strptime (field_value , fmt )
138-
144+ # Enforce future-date rule using central checker after successful parse
145+ if PreValidation .check_if_future_date (fhir_date ):
146+ raise ValueError (f"{ field_location } must not be in the future" )
147+ # After successful parse, enforce timezone and future-date rules
139148 if strict_timezone and fhir_date .tzinfo is not None :
140149 if not any (field_value .endswith (suffix ) for suffix in allowed_suffixes ):
141150 raise ValueError (error_message )
@@ -233,3 +242,16 @@ def for_nhs_number(nhs_number: str, field_location: str):
233242 """
234243 if not nhs_number_mod11_check (nhs_number ):
235244 raise ValueError (f"{ field_location } is not a valid NHS number" )
245+
246+ @staticmethod
247+ def check_if_future_date (parsed_value : date | datetime ):
248+ """
249+ Ensure a parsed date or datetime object is not in the future.
250+ """
251+ if isinstance (parsed_value , datetime ):
252+ now = datetime .now (parsed_value .tzinfo ) if parsed_value .tzinfo else datetime .now ()
253+ elif isinstance (parsed_value , date ):
254+ now = datetime .now ().date ()
255+ if parsed_value > now :
256+ return True
257+ return False
0 commit comments