Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.

Commit a5f0c20

Browse files
committed
feat: DTOSS-9159 file-level validation of NBSS Appointment event files
1 parent 741cbfe commit a5f0c20

File tree

7 files changed

+381
-6
lines changed

7 files changed

+381
-6
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System.Text.RegularExpressions;
2+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
3+
4+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
5+
6+
public partial class FileValidator : IFileValidator
7+
{
8+
private readonly HeaderFieldRegexValidator _headerExtractIdValidator = new(
9+
x => x.ExtractId, "Extract ID", ExtractIdRegex(),
10+
ErrorCodes.MissingExtractId, ErrorCodes.InvalidExtractId);
11+
12+
private readonly HeaderFieldRegexValidator _headerIdRecordCountValidator = new(
13+
x => x.RecordCount, "Record count", RecordCountRegex(),
14+
ErrorCodes.MissingRecordCount, ErrorCodes.InvalidRecordCount);
15+
16+
public IEnumerable<ValidationError> Validate(ParsedFile file)
17+
{
18+
foreach (var error in ValidateHeaderPresence(file))
19+
{
20+
yield return error;
21+
}
22+
23+
foreach (var error in ValidateTrailerPresence(file))
24+
{
25+
yield return error;
26+
}
27+
28+
foreach (var error in ValidateExtractId(file))
29+
{
30+
yield return error;
31+
}
32+
33+
foreach (var error in ValidateRecordCount(file))
34+
{
35+
yield return error;
36+
}
37+
}
38+
39+
private static IEnumerable<ValidationError> ValidateHeaderPresence(ParsedFile file)
40+
{
41+
if (file.FileHeader == null)
42+
{
43+
yield return new ValidationError
44+
{
45+
Code = ErrorCodes.MissingHeader,
46+
Error = "Header is missing",
47+
Scope = ValidationErrorScope.File
48+
};
49+
}
50+
}
51+
52+
private static IEnumerable<ValidationError> ValidateTrailerPresence(ParsedFile file)
53+
{
54+
if (file.FileTrailer == null)
55+
{
56+
yield return new ValidationError
57+
{
58+
Code = ErrorCodes.MissingTrailer,
59+
Error = "Trailer is missing",
60+
Scope = ValidationErrorScope.File
61+
};
62+
}
63+
}
64+
65+
private IEnumerable<ValidationError> ValidateExtractId(ParsedFile file)
66+
{
67+
if (file.FileHeader == null) yield break;
68+
69+
foreach (var error in _headerExtractIdValidator.Validate(file))
70+
{
71+
yield return error;
72+
}
73+
74+
if (file.FileTrailer != null && file.FileHeader.ExtractId != file.FileTrailer.ExtractId)
75+
{
76+
yield return new ValidationError
77+
{
78+
Field = "Extract ID",
79+
Code = ErrorCodes.InconsistentExtractId,
80+
Error = "Extract ID does not match value in header",
81+
Scope = ValidationErrorScope.Trailer
82+
};
83+
}
84+
}
85+
86+
private IEnumerable<ValidationError> ValidateRecordCount(ParsedFile file)
87+
{
88+
if (file.FileHeader == null) yield break;
89+
90+
var headerRecordCountErrors = _headerIdRecordCountValidator.Validate(file).ToList();
91+
92+
foreach (var error in headerRecordCountErrors)
93+
{
94+
yield return error;
95+
}
96+
97+
if (file.FileTrailer != null && file.FileHeader.RecordCount != file.FileTrailer.RecordCount)
98+
{
99+
yield return new ValidationError
100+
{
101+
Field = "Record count",
102+
Code = ErrorCodes.InconsistentRecordCount,
103+
Error = "Record count does not match value in header",
104+
Scope = ValidationErrorScope.Trailer
105+
};
106+
} else if (headerRecordCountErrors.Count == 0 && file.DataRecords.Count != int.Parse(file.FileHeader.RecordCount!))
107+
{
108+
yield return new ValidationError
109+
{
110+
Code = ErrorCodes.UnexpectedRecordCount,
111+
Error = "Record count does not match value in header and trailer",
112+
Scope = ValidationErrorScope.File
113+
};
114+
}
115+
}
116+
117+
[GeneratedRegex(@"^\d{8}$")]
118+
private static partial Regex ExtractIdRegex();
119+
120+
[GeneratedRegex(@"^(?!000000)\d{6}$")]
121+
private static partial Regex RecordCountRegex();
122+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Linq.Expressions;
2+
using System.Text.RegularExpressions;
3+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
4+
5+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
6+
7+
public class HeaderFieldRegexValidator(
8+
Expression<Func<FileHeaderRecord, string?>> fieldSelector,
9+
string fieldName,
10+
Regex pattern,
11+
string errorCodeMissing,
12+
string errorCodeInvalidFormat)
13+
: IFileValidator
14+
{
15+
public IEnumerable<ValidationError> Validate(ParsedFile file)
16+
{
17+
var header = file.FileHeader!;
18+
var value = fieldSelector.Compile().Invoke(header);
19+
20+
if (value == null)
21+
{
22+
yield return new ValidationError
23+
{
24+
Scope = ValidationErrorScope.Header,
25+
Field = fieldName,
26+
Error = $"{fieldName} is missing",
27+
Code = errorCodeMissing,
28+
};
29+
yield break;
30+
}
31+
32+
if (!pattern.IsMatch(value))
33+
{
34+
yield return new ValidationError
35+
{
36+
Scope = ValidationErrorScope.Header,
37+
Field = fieldName,
38+
Error = $"{fieldName} is in an invalid format",
39+
Code = errorCodeInvalidFormat,
40+
};
41+
}
42+
}
43+
}

src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Runtime.InteropServices.JavaScript;
21
using System.Text.RegularExpressions;
32

43
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
@@ -69,7 +68,7 @@ public static IEnumerable<IRecordValidator> GetAllRecordValidators()
6968

7069
public static IEnumerable<IFileValidator> GetAllFileValidators()
7170
{
72-
return [];
71+
return [new FileValidator()];
7372
}
7473

7574
[GeneratedRegex(@"^[BCU]$", RegexOptions.Compiled)]

src/ServiceLayer.Mesh/ValidationError.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ public class ValidationError
44
{
55
public int? RowNumber { get; set; }
66

7-
public required string Field { get; set; }
7+
public string? Field { get; set; }
88

99
public required string Code { get; set; }
1010

tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/TestDataBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public static ParsedFile BuildValidParsedFile(int numberOfRecords = 3)
1212
ExtractId = "00000107",
1313
TransferStartDate = "20250519",
1414
TransferStartTime = "153806",
15-
RecordCount = numberOfRecords.ToString()
15+
RecordCount = numberOfRecords.ToString("D6")
1616
};
1717

1818
var trailer = new FileTrailerRecord
@@ -21,7 +21,7 @@ public static ParsedFile BuildValidParsedFile(int numberOfRecords = 3)
2121
ExtractId = "00000107",
2222
TransferEndDate = "20250519",
2323
TransferEndTime = "153957",
24-
RecordCount = numberOfRecords.ToString()
24+
RecordCount = numberOfRecords.ToString("D6")
2525
};
2626

2727
var dataRecords = Enumerable.Range(1, numberOfRecords)

0 commit comments

Comments
 (0)