33using Microsoft . AspNetCore . Mvc . ModelBinding ;
44using System . Text . RegularExpressions ;
55using Microsoft . Extensions . Logging ;
6+ using System . Text . Json ;
67using Task = DfE . ExternalApplications . Domain . Models . Task ;
78
89namespace DfE . ExternalApplications . Infrastructure . Services
@@ -13,10 +14,12 @@ namespace DfE.ExternalApplications.Infrastructure.Services
1314 public class FormValidationOrchestrator : IFormValidationOrchestrator
1415 {
1516 private readonly ILogger < FormValidationOrchestrator > _logger ;
17+ private readonly IConditionalLogicEngine _conditionalLogicEngine ;
1618
17- public FormValidationOrchestrator ( ILogger < FormValidationOrchestrator > logger )
19+ public FormValidationOrchestrator ( ILogger < FormValidationOrchestrator > logger , IConditionalLogicEngine conditionalLogicEngine )
1820 {
1921 _logger = logger ;
22+ _conditionalLogicEngine = conditionalLogicEngine ;
2023 }
2124
2225 /// <summary>
@@ -40,7 +43,7 @@ public bool ValidatePage(Page page, Dictionary<string, object> data, ModelStateD
4043 data . TryGetValue ( key , out var rawValue ) ;
4144 var value = rawValue ? . ToString ( ) ?? string . Empty ;
4245
43- if ( ! ValidateField ( field , value , modelState , key ) )
46+ if ( ! ValidateField ( field , value , data , modelState , key ) )
4447 {
4548 isValid = false ;
4649 }
@@ -113,6 +116,21 @@ public bool ValidateApplication(FormTemplate template, Dictionary<string, object
113116 /// <param name="fieldKey">The field key for model state</param>
114117 /// <returns>True if validation passes</returns>
115118 public bool ValidateField ( Field field , object value , ModelStateDictionary modelState , string fieldKey )
119+ {
120+ // Call the overloaded method with null data for backward compatibility
121+ return ValidateField ( field , value , null , modelState , fieldKey ) ;
122+ }
123+
124+ /// <summary>
125+ /// Validates a single field with full form data context for conditional validation
126+ /// </summary>
127+ /// <param name="field">The field to validate</param>
128+ /// <param name="value">The field value</param>
129+ /// <param name="formData">The complete form data for conditional evaluation</param>
130+ /// <param name="modelState">The model state to add errors to</param>
131+ /// <param name="fieldKey">The field key for model state</param>
132+ /// <returns>True if validation passes</returns>
133+ public bool ValidateField ( Field field , object value , Dictionary < string , object > ? formData , ModelStateDictionary modelState , string fieldKey )
116134 {
117135 if ( field ? . Validations == null )
118136 {
@@ -122,14 +140,39 @@ public bool ValidateField(Field field, object value, ModelStateDictionary modelS
122140 var stringValue = value ? . ToString ( ) ?? string . Empty ;
123141 var isValid = true ;
124142
143+ // Special handling for complex fields (upload, autocomplete, etc.)
144+ if ( field . Type == "complexField" && field . ComplexField != null )
145+ {
146+ return ValidateComplexField ( field , value , formData , modelState , fieldKey ) ;
147+ }
148+
125149 foreach ( var rule in field . Validations )
126150 {
127- // Conditional application
151+ // Check if this is a conditional validation rule
128152 if ( rule . Condition != null )
129153 {
130- // This would need access to the full data dictionary to check conditions
131- // For now, we'll skip conditional validation in this context
132- continue ;
154+ if ( formData == null )
155+ {
156+ _logger . LogWarning ( "Conditional validation rule found for field '{FieldId}' but no form data provided for evaluation. Skipping rule." , field . FieldId ) ;
157+ continue ;
158+ }
159+
160+ try
161+ {
162+ // Evaluate the condition using the conditional logic engine
163+ bool conditionMet = _conditionalLogicEngine . EvaluateCondition ( rule . Condition , formData ) ;
164+
165+ if ( ! conditionMet )
166+ {
167+ // Condition not met, skip this validation rule
168+ continue ;
169+ }
170+ }
171+ catch ( Exception ex )
172+ {
173+ _logger . LogError ( ex , "Error evaluating conditional validation rule for field '{FieldId}'. Skipping rule." , field . FieldId ) ;
174+ continue ;
175+ }
133176 }
134177
135178 switch ( rule . Type )
@@ -142,26 +185,195 @@ public bool ValidateField(Field field, object value, ModelStateDictionary modelS
142185 }
143186 break ;
144187 case "regex" :
145- if ( ! Regex . IsMatch ( stringValue , rule . Rule . ToString ( ) , RegexOptions . None , TimeSpan . FromMilliseconds ( 200 ) ) && ! string . IsNullOrWhiteSpace ( stringValue ) )
188+ var pattern = rule . Rule ? . ToString ( ) ;
189+ if ( ! string . IsNullOrWhiteSpace ( stringValue ) && ! string . IsNullOrEmpty ( pattern ) )
146190 {
147- modelState . AddModelError ( fieldKey , rule . Message ) ;
148- isValid = false ;
191+ var regexMatch = Regex . IsMatch ( stringValue , pattern , RegexOptions . None , TimeSpan . FromMilliseconds ( 200 ) ) ;
192+ if ( ! regexMatch )
193+ {
194+ modelState . AddModelError ( fieldKey , rule . Message ) ;
195+ isValid = false ;
196+ }
149197 }
150198 break ;
151199 case "maxLength" :
152- if ( stringValue . Length > int . Parse ( rule . Rule . ToString ( ) ) )
200+ var maxLengthStr = rule . Rule ? . ToString ( ) ;
201+ if ( ! string . IsNullOrEmpty ( maxLengthStr ) && int . TryParse ( maxLengthStr , out var maxLength ) )
153202 {
154- modelState . AddModelError ( fieldKey , rule . Message ) ;
155- isValid = false ;
203+ if ( stringValue . Length > maxLength )
204+ {
205+ modelState . AddModelError ( fieldKey , rule . Message ) ;
206+ isValid = false ;
207+ }
156208 }
157209 break ;
158210 default :
159- _logger . LogWarning ( "Unknown validation rule type: {RuleType}" , rule . Type ) ;
211+ _logger . LogWarning ( "Unknown validation rule type: {RuleType} for field '{FieldKey}' " , rule . Type , fieldKey ) ;
160212 break ;
161213 }
162214 }
163215
164216 return isValid ;
165217 }
218+
219+ #region Complex Field Validation
220+
221+ /// <summary>
222+ /// Validates a complex field (upload, autocomplete, etc.)
223+ /// </summary>
224+ /// <param name="field">The complex field to validate</param>
225+ /// <param name="value">The field value</param>
226+ /// <param name="formData">The complete form data for conditional evaluation</param>
227+ /// <param name="modelState">The model state to add errors to</param>
228+ /// <param name="fieldKey">The field key for model state</param>
229+ /// <returns>True if validation passes</returns>
230+ private bool ValidateComplexField ( Field field , object ? value , Dictionary < string , object > ? formData , ModelStateDictionary modelState , string fieldKey )
231+ {
232+ if ( field . Validations == null )
233+ {
234+ return true ;
235+ }
236+
237+ var stringValue = value ? . ToString ( ) ?? string . Empty ;
238+ var isValid = true ;
239+
240+ // Determine if this is an upload field
241+ bool isUploadField = field . ComplexField ! . Id . Contains ( "Upload" , StringComparison . OrdinalIgnoreCase ) ;
242+
243+ foreach ( var rule in field . Validations )
244+ {
245+ // Check if this is a conditional validation rule
246+ if ( rule . Condition != null )
247+ {
248+ if ( formData == null )
249+ {
250+ _logger . LogWarning ( "Conditional validation rule found for complex field '{FieldId}' but no form data provided for evaluation. Skipping rule." , field . FieldId ) ;
251+ continue ;
252+ }
253+
254+ try
255+ {
256+ var conditionResult = _conditionalLogicEngine . EvaluateCondition ( rule . Condition , formData ) ;
257+
258+ if ( ! conditionResult )
259+ {
260+ continue ;
261+ }
262+ }
263+ catch ( Exception ex )
264+ {
265+ _logger . LogError ( ex , "Error evaluating conditional validation rule for complex field '{FieldId}'. Skipping rule." , field . FieldId ) ;
266+ continue ;
267+ }
268+ }
269+
270+ switch ( rule . Type . ToLowerInvariant ( ) )
271+ {
272+ case "required" :
273+ if ( isUploadField )
274+ {
275+ // For upload fields, check if files are uploaded
276+ bool hasFiles = HasUploadedFiles ( stringValue ) ;
277+ if ( ! hasFiles )
278+ {
279+ // Use a more appropriate error message for upload fields if the template message is clearly wrong
280+ var errorMessage = rule . Message ;
281+ if ( errorMessage . Contains ( "phone" , StringComparison . OrdinalIgnoreCase ) ||
282+ errorMessage . Contains ( "name" , StringComparison . OrdinalIgnoreCase ) ||
283+ errorMessage . Contains ( "text" , StringComparison . OrdinalIgnoreCase ) )
284+ {
285+ errorMessage = "Please upload a file." ;
286+ }
287+
288+ modelState . AddModelError ( fieldKey , errorMessage ) ;
289+ isValid = false ;
290+ }
291+ }
292+ else
293+ {
294+ // For other complex fields (autocomplete), check if value is empty
295+ if ( string . IsNullOrWhiteSpace ( stringValue ) )
296+ {
297+ modelState . AddModelError ( fieldKey , rule . Message ) ;
298+ isValid = false ;
299+ }
300+ }
301+ break ;
302+ case "regex" :
303+ // Regex validation doesn't apply to upload fields, skip for uploads
304+ if ( ! isUploadField && ! string . IsNullOrWhiteSpace ( stringValue ) )
305+ {
306+ var pattern = rule . Rule ? . ToString ( ) ;
307+ if ( ! string . IsNullOrEmpty ( pattern ) && ! Regex . IsMatch ( stringValue , pattern , RegexOptions . None , TimeSpan . FromMilliseconds ( 200 ) ) )
308+ {
309+ modelState . AddModelError ( fieldKey , rule . Message ) ;
310+ isValid = false ;
311+ }
312+ }
313+ break ;
314+ case "maxlength" :
315+ // MaxLength validation doesn't apply to upload fields, skip for uploads
316+ if ( ! isUploadField )
317+ {
318+ if ( int . TryParse ( rule . Rule ? . ToString ( ) , out var maxLength ) )
319+ {
320+ if ( stringValue . Length > maxLength )
321+ {
322+ modelState . AddModelError ( fieldKey , rule . Message ) ;
323+ isValid = false ;
324+ }
325+ }
326+ else
327+ {
328+ _logger . LogWarning ( "Complex field maxLength validation rule has invalid rule value for field '{FieldId}': {Rule}" , field . FieldId , rule . Rule ) ;
329+ }
330+ }
331+ break ;
332+ default :
333+ _logger . LogWarning ( "Unknown complex field validation rule type '{Type}' for field '{FieldId}'" , rule . Type , field . FieldId ) ;
334+ break ;
335+ }
336+ }
337+
338+ return isValid ;
339+ }
340+
341+ /// <summary>
342+ /// Checks if an upload field has uploaded files
343+ /// </summary>
344+ /// <param name="value">The field value (JSON array or string)</param>
345+ /// <returns>True if files are uploaded</returns>
346+ private bool HasUploadedFiles ( string value )
347+ {
348+ if ( string . IsNullOrWhiteSpace ( value ) )
349+ {
350+ return false ;
351+ }
352+
353+ // Handle special session data placeholder - this indicates NO files uploaded yet
354+ if ( value == "UPLOAD_FIELD_SESSION_DATA" )
355+ {
356+ return false ;
357+ }
358+
359+ // Try to parse as JSON array
360+ try
361+ {
362+ if ( value . StartsWith ( "[" ) && value . EndsWith ( "]" ) )
363+ {
364+ var files = System . Text . Json . JsonSerializer . Deserialize < List < object > > ( value ) ;
365+ return files != null && files . Count > 0 ;
366+ }
367+ }
368+ catch ( Exception ex )
369+ {
370+ _logger . LogWarning ( ex , "Failed to parse upload field value as JSON for field value: {Value}" , value ) ;
371+ }
372+
373+ // If not JSON or parsing failed, treat non-empty as having files (except for known placeholders)
374+ return ! string . IsNullOrWhiteSpace ( value ) ;
375+ }
376+
377+ #endregion
166378 }
167379}
0 commit comments