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

Commit a7ebf88

Browse files
committed
Merge branch 'main' into fix/sonarqubewarnings
2 parents 8c06d4d + 93402d6 commit a7ebf88

17 files changed

+409
-9
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Globalization;
2+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
3+
4+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
5+
6+
public class DateFormatValidator(
7+
string fieldName,
8+
string format,
9+
string errorCodeMissing,
10+
string errorCodeInvalidFormat) : IRecordValidator
11+
{
12+
public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
13+
{
14+
var value = fileDataRecord[fieldName];
15+
16+
if (value == null)
17+
{
18+
yield return new ValidationError
19+
{
20+
Scope = ValidationErrorScope.Record,
21+
RowNumber = fileDataRecord.RowNumber,
22+
Field = fieldName,
23+
Error = $"{fieldName} is missing",
24+
Code = errorCodeMissing,
25+
};
26+
yield break;
27+
}
28+
29+
if (!DateTime.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out _))
30+
{
31+
yield return new ValidationError
32+
{
33+
Scope = ValidationErrorScope.Record,
34+
RowNumber = fileDataRecord.RowNumber,
35+
Field = fieldName,
36+
Error = $"{fieldName} is in an invalid format",
37+
Code = errorCodeInvalidFormat,
38+
};
39+
}
40+
}
41+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
2020

2121
yield return new ValidationError
2222
{
23+
Scope = ValidationErrorScope.Record,
2324
RowNumber = fileDataRecord.RowNumber,
2425
Field = fieldName,
2526
Error = error,
@@ -34,6 +35,7 @@ public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
3435

3536
yield return new ValidationError
3637
{
38+
Scope = ValidationErrorScope.Record,
3739
RowNumber = fileDataRecord.RowNumber,
3840
Field = fieldName,
3941
Error = error,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ protected override IEnumerable<ValidationError> RunAdditionalChecks(int rowNumbe
1111
{
1212
yield return new ValidationError
1313
{
14+
Scope = ValidationErrorScope.Record,
1415
RowNumber = rowNumber,
1516
Field = FieldName,
1617
Error = "NHS Num has invalid check digit",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
2020
{
2121
yield return new ValidationError
2222
{
23+
Scope = ValidationErrorScope.Record,
2324
RowNumber = fileDataRecord.RowNumber,
2425
Field = FieldName,
2526
Error = $"{FieldName} is missing",
@@ -32,6 +33,7 @@ public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
3233
{
3334
yield return new ValidationError
3435
{
36+
Scope = ValidationErrorScope.Record,
3537
RowNumber = fileDataRecord.RowNumber,
3638
Field = FieldName,
3739
Error = $"{FieldName} is in an invalid format",

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Runtime.InteropServices.JavaScript;
12
using System.Text.RegularExpressions;
23

34
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
@@ -27,6 +28,8 @@ public static IEnumerable<IRecordValidator> GetAllRecordValidators()
2728
new NhsNumValidator(),
2829
new RegexValidator("Episode Type", EpisodeTypeRegex(), ErrorCodes.MissingEpisodeType,
2930
ErrorCodes.InvalidEpisodeType),
31+
new DateFormatValidator("Episode Start", "yyyyMMdd", ErrorCodes.MissingEpisodeStart,
32+
ErrorCodes.InvalidEpisodeStart),
3033
new MaxLengthValidator("Batch ID", 9, ErrorCodes.MissingBatchId,
3134
ErrorCodes.InvalidBatchId),
3235
new RegexValidator("Screen or Asses", ScreenOrAssesRegex(), ErrorCodes.MissingScreenOrAsses,
@@ -37,6 +40,10 @@ public static IEnumerable<IRecordValidator> GetAllRecordValidators()
3740
ErrorCodes.InvalidBookedBy),
3841
new RegexValidator("Cancelled By", CancelledByRegex(), ErrorCodes.MissingCancelledBy,
3942
ErrorCodes.InvalidCancelledBy),
43+
new DateFormatValidator("Appt Date", "yyyyMMdd", ErrorCodes.MissingApptDate,
44+
ErrorCodes.InvalidApptDate),
45+
new DateFormatValidator("Appt Time", "HHmm", ErrorCodes.MissingApptTime,
46+
ErrorCodes.InvalidApptTime),
4047
new MaxLengthValidator("Location", 5, ErrorCodes.MissingLocation,
4148
ErrorCodes.InvalidLocation),
4249
new MaxLengthValidator("Clinic Name", 40, ErrorCodes.MissingClinicName,
@@ -55,6 +62,8 @@ public static IEnumerable<IRecordValidator> GetAllRecordValidators()
5562
ErrorCodes.InvalidClinicAddress5, true),
5663
new MaxLengthValidator("Postcode", 8, ErrorCodes.MissingPostcode,
5764
ErrorCodes.InvalidPostcode, true),
65+
new DateFormatValidator("Action Timestamp", "yyyyMMdd-HHmmss", ErrorCodes.MissingActionTimestamp,
66+
ErrorCodes.InvalidActionTimestamp)
5867
];
5968
}
6069

src/ServiceLayer.Mesh/ValidationError.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ public class ValidationError
99
public required string Code { get; set; }
1010

1111
public required string Error { get; set; }
12+
13+
public required ValidationErrorScope Scope { get; set; }
1214
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace ServiceLayer.Mesh;
2+
3+
public enum ValidationErrorScope
4+
{
5+
File,
6+
Record,
7+
Header,
8+
Trailer
9+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
2+
3+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
4+
5+
public class ActionTimestampValidatorTests : ValidationTestBase
6+
{
7+
[Fact]
8+
public void Validate_ActionTimestampMissing_ReturnsValidationError()
9+
{
10+
// Arrange
11+
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Action Timestamp"));
12+
13+
// Act
14+
var validationErrors = Validate(file);
15+
16+
// Assert
17+
validationErrors.ShouldContainValidationError(
18+
"Action Timestamp",
19+
"Action Timestamp is missing",
20+
ErrorCodes.MissingActionTimestamp
21+
);
22+
}
23+
24+
[Theory]
25+
[InlineData("20250631-183156")] // too many days in June
26+
[InlineData("202S0630-183156")] // invalid character
27+
[InlineData("20250630-1831")] // too short
28+
[InlineData("20250630T1831")] // unexpected separator
29+
[InlineData("250630-183156")] // too short, ddMMyy
30+
[InlineData("20250630-1456")] // No seconds
31+
[InlineData("20250630-18:31:56")] // unexpected separators
32+
[InlineData("20250229-183156")] // Not a leap year
33+
public void Validate_ActionTimestampInvalidFormat_ReturnsValidationError(string value)
34+
{
35+
// Arrange
36+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Action Timestamp"] = value);
37+
38+
// Act
39+
var validationErrors = Validate(file).ToList();
40+
41+
// Assert
42+
validationErrors.ShouldContainValidationError(
43+
"Action Timestamp",
44+
"Action Timestamp is in an invalid format",
45+
ErrorCodes.InvalidActionTimestamp
46+
);
47+
}
48+
49+
[Theory]
50+
[InlineData("20250529-163243")]
51+
[InlineData("20240229-163243")]
52+
[InlineData("20250731-163243")]
53+
[InlineData("19990806-235959")]
54+
[InlineData("20561212-000000")]
55+
public void Validate_ActionTimestampValidFormat_NoValidationErrorsReturned(string value)
56+
{
57+
// Arrange
58+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Action Timestamp"] = value);
59+
60+
// Act
61+
var validationErrors = Validate(file).ToList();
62+
63+
// Assert
64+
Assert.Empty(validationErrors);
65+
}
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
2+
3+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
4+
5+
public class ApptDateValidatorTests : ValidationTestBase
6+
{
7+
[Fact]
8+
public void Validate_ApptDateMissing_ReturnsValidationError()
9+
{
10+
// Arrange
11+
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Appt Date"));
12+
13+
// Act
14+
var validationErrors = Validate(file);
15+
16+
// Assert
17+
validationErrors.ShouldContainValidationError(
18+
"Appt Date",
19+
"Appt Date is missing",
20+
ErrorCodes.MissingApptDate
21+
);
22+
}
23+
24+
[Theory]
25+
[InlineData("20250631")] // too many days in June
26+
[InlineData("202S0630")] // invalid character
27+
[InlineData("202506")] // too short
28+
[InlineData("30062025")] // ddMMyyyy and not valid as yyyyMMdd
29+
[InlineData("250630")] // too short, ddMMyy
30+
[InlineData("20250630-145621")] // Includes time
31+
[InlineData("20250229")] // Not a leap year
32+
public void Validate_ApptDateInvalidFormat_ReturnsValidationError(string value)
33+
{
34+
// Arrange
35+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appt Date"] = value);
36+
37+
// Act
38+
var validationErrors = Validate(file).ToList();
39+
40+
// Assert
41+
validationErrors.ShouldContainValidationError(
42+
"Appt Date",
43+
"Appt Date is in an invalid format",
44+
ErrorCodes.InvalidApptDate
45+
);
46+
}
47+
48+
[Theory]
49+
[InlineData("20250101")]
50+
[InlineData("20250228")]
51+
[InlineData("20250331")]
52+
[InlineData("20251231")]
53+
[InlineData("20240229")]
54+
[InlineData("19990331")]
55+
public void Validate_ApptDateValidFormat_NoValidationErrorsReturned(string value)
56+
{
57+
// Arrange
58+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appt Date"] = value);
59+
60+
// Act
61+
var validationErrors = Validate(file).ToList();
62+
63+
// Assert
64+
Assert.Empty(validationErrors);
65+
}
66+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
2+
3+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
4+
5+
public class ApptTimeValidatorTests : ValidationTestBase
6+
{
7+
[Fact]
8+
public void Validate_ApptTimeMissing_ReturnsValidationError()
9+
{
10+
// Arrange
11+
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Appt Time"));
12+
13+
// Act
14+
var validationErrors = Validate(file);
15+
16+
// Assert
17+
validationErrors.ShouldContainValidationError(
18+
"Appt Time",
19+
"Appt Time is missing",
20+
ErrorCodes.MissingApptTime
21+
);
22+
}
23+
24+
[Theory]
25+
[InlineData("2407")] // too many hours
26+
[InlineData("1960")] // too many minutes
27+
[InlineData("842")] // too short
28+
[InlineData("10435")] // too long
29+
[InlineData("193S")] // invalid characters
30+
public void Validate_ApptTimeInvalidFormat_ReturnsValidationError(string value)
31+
{
32+
// Arrange
33+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appt Time"] = value);
34+
35+
// Act
36+
var validationErrors = Validate(file).ToList();
37+
38+
// Assert
39+
validationErrors.ShouldContainValidationError(
40+
"Appt Time",
41+
"Appt Time is in an invalid format",
42+
ErrorCodes.InvalidApptTime
43+
);
44+
}
45+
46+
[Theory]
47+
[InlineData("0000")]
48+
[InlineData("2359")]
49+
[InlineData("0001")]
50+
[InlineData("2358")]
51+
[InlineData("1200")]
52+
[InlineData("1300")]
53+
public void Validate_ApptTimeValidFormat_NoValidationErrorsReturned(string value)
54+
{
55+
// Arrange
56+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appt Time"] = value);
57+
58+
// Act
59+
var validationErrors = Validate(file).ToList();
60+
61+
// Assert
62+
Assert.Empty(validationErrors);
63+
}
64+
}

0 commit comments

Comments
 (0)