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

Commit 93402d6

Browse files
authored
feat: DTOSS-9161 - validation for date/time-related fields (#35)
1 parent 65a9c09 commit 93402d6

File tree

8 files changed

+384
-1
lines changed

8 files changed

+384
-1
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/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

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+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
2+
using System.Text.RegularExpressions;
3+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
4+
5+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
6+
7+
public class DateFormatValidatorTests
8+
{
9+
private const string FieldName = "TestField";
10+
private const string MissingCode = "ERR001";
11+
private const string InvalidFormatCode = "ERR002";
12+
private const string Format = "yyyyMMdd";
13+
14+
[Fact]
15+
public void Validate_NullValue_ShouldReturnMissingError()
16+
{
17+
// Arrange
18+
var record = new FileDataRecord
19+
{
20+
RowNumber = 1
21+
};
22+
record.Fields.Clear();
23+
24+
var validator = new DateFormatValidator(FieldName, Format, MissingCode, InvalidFormatCode);
25+
26+
// Act
27+
var errors = validator.Validate(record).ToList();
28+
29+
// Assert
30+
errors.ShouldContainValidationError(FieldName, $"{FieldName} is missing", MissingCode, ValidationErrorScope.Record, 1);
31+
}
32+
33+
[Theory]
34+
[InlineData("")]
35+
[InlineData("20250631")]
36+
public void Validate_ValueNotMatchingPattern_ShouldReturnInvalidFormatError(string invalidValue)
37+
{
38+
// Arrange
39+
var record = new FileDataRecord
40+
{
41+
RowNumber = 2
42+
};
43+
record.Fields.Add(FieldName, invalidValue);
44+
45+
var validator = new DateFormatValidator(FieldName, Format, MissingCode, InvalidFormatCode);
46+
47+
// Act
48+
var errors = validator.Validate(record).ToList();
49+
50+
// Assert
51+
errors.ShouldContainValidationError(FieldName, $"{FieldName} is in an invalid format", InvalidFormatCode,ValidationErrorScope.Record, 2);
52+
}
53+
54+
[Theory]
55+
[InlineData("20250630")]
56+
[InlineData("19990807")]
57+
public void Validate_ValueMatchingPattern_ShouldReturnNoErrors(string validValue)
58+
{
59+
var record = new FileDataRecord
60+
{
61+
RowNumber = 3
62+
};
63+
record.Fields.Add(FieldName, validValue);
64+
65+
var validator = new DateFormatValidator(FieldName, Format, MissingCode, InvalidFormatCode);
66+
67+
var errors = validator.Validate(record).ToList();
68+
69+
Assert.Empty(errors);
70+
}
71+
}

0 commit comments

Comments
 (0)