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

Commit ae8ced3

Browse files
authored
feat: DTOSS-8752 - initial validators for NBSS Appointment event files (#21)
1 parent 83951fd commit ae8ced3

33 files changed

+2002
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
2+
3+
public static class ErrorCodes
4+
{
5+
public const string UnableToParseFile = "NBSSAPPT001";
6+
public const string MissingHeader = "NBSSAPPT002";
7+
public const string MissingExtractId = "NBSSAPPT003";
8+
public const string InvalidExtractId = "NBSSAPPT004";
9+
public const string MissingRecordCount = "NBSSAPPT005";
10+
public const string InvalidRecordCount = "NBSSAPPT006";
11+
public const string MissingTrailer = "NBSSAPPT007";
12+
public const string InconsistentExtractId = "NBSSAPPT008";
13+
public const string InconsistentRecordCount = "NBSSAPPT009";
14+
public const string MissingFieldHeadings = "NBSSAPPT010";
15+
public const string UnexpectedRecordCount = "NBSSAPPT011";
16+
public const string MissingSequence = "NBSSAPPT012";
17+
public const string InvalidSequence = "NBSSAPPT013";
18+
public const string MissingBso = "NBSSAPPT014";
19+
public const string InvalidBso = "NBSSAPPT015";
20+
public const string MissingAction = "NBSSAPPT016";
21+
public const string InvalidAction = "NBSSAPPT017";
22+
public const string MissingClinicCode = "NBSSAPPT018";
23+
public const string InvalidClinicCode = "NBSSAPPT019";
24+
public const string MissingHoldingClinic = "NBSSAPPT020";
25+
public const string InvalidHoldingClinic = "NBSSAPPT021";
26+
public const string MissingStatus = "NBSSAPPT022";
27+
public const string InvalidStatus = "NBSSAPPT023";
28+
public const string MissingAttendedNotScr = "NBSSAPPT024";
29+
public const string InvalidAttendedNotScr = "NBSSAPPT025";
30+
public const string MissingAppointmentId = "NBSSAPPT026";
31+
public const string InvalidAppointmentId = "NBSSAPPT027";
32+
public const string MissingNhsNum = "NBSSAPPT028";
33+
public const string InvalidNhsNum = "NBSSAPPT029";
34+
public const string InvalidNhsNumCheckDigit = "NBSSAPPT030";
35+
public const string MissingEpisodeType = "NBSSAPPT031";
36+
public const string InvalidEpisodeType = "NBSSAPPT032";
37+
public const string MissingEpisodeStart = "NBSSAPPT033";
38+
public const string InvalidEpisodeStart = "NBSSAPPT034";
39+
public const string MissingBatchId = "NBSSAPPT035";
40+
public const string InvalidBatchId = "NBSSAPPT036";
41+
public const string MissingScreenOrAsses = "NBSSAPPT037";
42+
public const string InvalidScreenOrAsses = "NBSSAPPT038";
43+
public const string MissingScreenApptNum = "NBSSAPPT039";
44+
public const string InvalidScreenApptNum = "NBSSAPPT040";
45+
public const string MissingBookedBy = "NBSSAPPT041";
46+
public const string InvalidBookedBy = "NBSSAPPT042";
47+
public const string MissingCancelledBy = "NBSSAPPT043";
48+
public const string InvalidCancelledBy = "NBSSAPPT044";
49+
public const string MissingApptDate = "NBSSAPPT045";
50+
public const string InvalidApptDate = "NBSSAPPT046";
51+
public const string MissingApptTime = "NBSSAPPT047";
52+
public const string InvalidApptTime = "NBSSAPPT048";
53+
public const string MissingLocation = "NBSSAPPT049";
54+
public const string InvalidLocation = "NBSSAPPT050";
55+
public const string MissingClinicName = "NBSSAPPT051";
56+
public const string InvalidClinicName = "NBSSAPPT052";
57+
public const string MissingClinicNameLet = "NBSSAPPT053";
58+
public const string InvalidClinicNameLet = "NBSSAPPT054";
59+
public const string MissingClinicAddress1 = "NBSSAPPT055";
60+
public const string InvalidClinicAddress1 = "NBSSAPPT056";
61+
public const string MissingClinicAddress2 = "NBSSAPPT057";
62+
public const string InvalidClinicAddress2 = "NBSSAPPT058";
63+
public const string MissingClinicAddress3 = "NBSSAPPT059";
64+
public const string InvalidClinicAddress3 = "NBSSAPPT060";
65+
public const string MissingClinicAddress4 = "NBSSAPPT061";
66+
public const string InvalidClinicAddress4 = "NBSSAPPT062";
67+
public const string MissingClinicAddress5 = "NBSSAPPT063";
68+
public const string InvalidClinicAddress5 = "NBSSAPPT064";
69+
public const string MissingPostcode = "NBSSAPPT065";
70+
public const string InvalidPostcode = "NBSSAPPT066";
71+
public const string MissingActionTimestamp = "NBSSAPPT067";
72+
public const string InvalidActionTimestamp = "NBSSAPPT068";
73+
public const string UnknownRecordTypeIdentifier = "NBSSAPPT069";
74+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
2+
3+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
4+
5+
public class MaxLengthValidator(
6+
string fieldName,
7+
int maxLength,
8+
string errorCodeMissing,
9+
string errorCodeTooLong,
10+
bool allowEmpty = false)
11+
: IRecordValidator
12+
{
13+
public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
14+
{
15+
var value = fileDataRecord[fieldName];
16+
17+
if (value == null || (!allowEmpty && string.IsNullOrWhiteSpace(value)))
18+
{
19+
var error = $"{fieldName} is missing{(allowEmpty ? "" : " or empty")}";
20+
21+
yield return new ValidationError
22+
{
23+
RowNumber = fileDataRecord.RowNumber,
24+
Field = fieldName,
25+
Error = error,
26+
Code = errorCodeMissing,
27+
};
28+
yield break;
29+
}
30+
31+
if (value.Length > maxLength)
32+
{
33+
var error = $"{fieldName} exceeds maximum length of {maxLength}";
34+
35+
yield return new ValidationError
36+
{
37+
RowNumber = fileDataRecord.RowNumber,
38+
Field = fieldName,
39+
Error = error,
40+
Code = errorCodeTooLong,
41+
};
42+
}
43+
}
44+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Text.RegularExpressions;
2+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
3+
4+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
5+
6+
public class RegexValidator(
7+
string fieldName,
8+
Regex pattern,
9+
string errorCodeMissing,
10+
string errorCodeInvalidFormat)
11+
: IRecordValidator
12+
{
13+
public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
14+
{
15+
var value = fileDataRecord[fieldName];
16+
17+
if (value == null)
18+
{
19+
yield return new ValidationError
20+
{
21+
RowNumber = fileDataRecord.RowNumber,
22+
Field = fieldName,
23+
Error = $"{fieldName} is missing",
24+
Code = errorCodeMissing,
25+
};
26+
yield break;
27+
}
28+
29+
if (!pattern.IsMatch(value))
30+
{
31+
yield return new ValidationError
32+
{
33+
RowNumber = fileDataRecord.RowNumber,
34+
Field = fieldName,
35+
Error = $"{fieldName} is in an invalid format",
36+
Code = errorCodeInvalidFormat,
37+
};
38+
}
39+
}
40+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
4+
5+
public static partial class ValidatorRegistry
6+
{
7+
public static IEnumerable<IRecordValidator> GetAllRecordValidators()
8+
{
9+
return
10+
[
11+
new RegexValidator("Sequence", SequenceRegex(), ErrorCodes.MissingSequence,
12+
ErrorCodes.InvalidSequence),
13+
new MaxLengthValidator("BSO", 3, ErrorCodes.MissingBso,
14+
ErrorCodes.InvalidBso),
15+
new RegexValidator("Action", ActionRegex(), ErrorCodes.MissingAction,
16+
ErrorCodes.InvalidAction),
17+
new MaxLengthValidator("Clinic Code", 5, ErrorCodes.MissingClinicCode,
18+
ErrorCodes.InvalidClinicCode),
19+
new RegexValidator("Holding Clinic", YesNoBlankRegex(), ErrorCodes.MissingHoldingClinic,
20+
ErrorCodes.InvalidHoldingClinic),
21+
new RegexValidator("Status", StatusRegex(), ErrorCodes.MissingStatus,
22+
ErrorCodes.InvalidStatus),
23+
new RegexValidator("Attended Not Scr", YesNoBlankRegex(), ErrorCodes.MissingAttendedNotScr,
24+
ErrorCodes.InvalidAttendedNotScr),
25+
new MaxLengthValidator("Appointment ID", 27, ErrorCodes.MissingAppointmentId,
26+
ErrorCodes.InvalidAppointmentId),
27+
new RegexValidator("Episode Type", EpisodeTypeRegex(), ErrorCodes.MissingEpisodeType,
28+
ErrorCodes.InvalidEpisodeType),
29+
new MaxLengthValidator("Batch ID", 9, ErrorCodes.MissingBatchId,
30+
ErrorCodes.InvalidBatchId),
31+
new RegexValidator("Screen or Asses", ScreenOrAssesRegex(), ErrorCodes.MissingScreenOrAsses,
32+
ErrorCodes.InvalidScreenOrAsses),
33+
new RegexValidator("Screen Appt num", ScreenApptNumRegex(), ErrorCodes.MissingScreenApptNum,
34+
ErrorCodes.InvalidScreenApptNum),
35+
new RegexValidator("Booked By", BookedByRegex(), ErrorCodes.MissingBookedBy,
36+
ErrorCodes.InvalidBookedBy),
37+
new RegexValidator("Cancelled By", CancelledByRegex(), ErrorCodes.MissingCancelledBy,
38+
ErrorCodes.InvalidCancelledBy),
39+
new MaxLengthValidator("Location", 5, ErrorCodes.MissingLocation,
40+
ErrorCodes.InvalidLocation),
41+
new MaxLengthValidator("Clinic Name", 40, ErrorCodes.MissingClinicName,
42+
ErrorCodes.InvalidClinicName, true),
43+
new MaxLengthValidator("Clinic Name (Let)", 50, ErrorCodes.MissingClinicNameLet,
44+
ErrorCodes.InvalidClinicNameLet, true),
45+
new MaxLengthValidator("Clinic Address 1", 30, ErrorCodes.MissingClinicAddress1,
46+
ErrorCodes.InvalidClinicAddress1, true),
47+
new MaxLengthValidator("Clinic Address 2", 30, ErrorCodes.MissingClinicAddress2,
48+
ErrorCodes.InvalidClinicAddress2, true),
49+
new MaxLengthValidator("Clinic Address 3", 30, ErrorCodes.MissingClinicAddress3,
50+
ErrorCodes.InvalidClinicAddress3, true),
51+
new MaxLengthValidator("Clinic Address 4", 30, ErrorCodes.MissingClinicAddress4,
52+
ErrorCodes.InvalidClinicAddress4, true),
53+
new MaxLengthValidator("Clinic Address 5", 30, ErrorCodes.MissingClinicAddress5,
54+
ErrorCodes.InvalidClinicAddress5, true),
55+
new MaxLengthValidator("Postcode", 8, ErrorCodes.MissingPostcode,
56+
ErrorCodes.InvalidPostcode, true),
57+
];
58+
}
59+
60+
public static IEnumerable<IFileValidator> GetAllFileValidators()
61+
{
62+
return [];
63+
}
64+
65+
[GeneratedRegex(@"^[BCU]$", RegexOptions.Compiled)]
66+
private static partial Regex ActionRegex();
67+
68+
[GeneratedRegex(@"^[CH]$", RegexOptions.Compiled)]
69+
private static partial Regex BookedByRegex();
70+
71+
[GeneratedRegex(@"^$|^[ CH]$", RegexOptions.Compiled)]
72+
private static partial Regex CancelledByRegex();
73+
74+
[GeneratedRegex(@"^[FGHNRST]$", RegexOptions.Compiled)]
75+
private static partial Regex EpisodeTypeRegex();
76+
77+
[GeneratedRegex(@"^[AS]$", RegexOptions.Compiled)]
78+
private static partial Regex ScreenOrAssesRegex();
79+
80+
[GeneratedRegex(@"^$|^[1-9]$", RegexOptions.Compiled)]
81+
private static partial Regex ScreenApptNumRegex();
82+
83+
[GeneratedRegex(@"^(?!000000)\d{6}$", RegexOptions.Compiled)]
84+
private static partial Regex SequenceRegex();
85+
86+
[GeneratedRegex(@"^[ABCD]$", RegexOptions.Compiled)]
87+
private static partial Regex StatusRegex();
88+
89+
[GeneratedRegex(@"^$|^[ YN]$", RegexOptions.Compiled)]
90+
private static partial Regex YesNoBlankRegex();
91+
}

src/ServiceLayer.sln.DotSettings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=nbssappt/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
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 ActionValidatorTests : ValidationTestBase
6+
{
7+
[Fact]
8+
public void Validate_ActionMissing_ReturnsValidationError()
9+
{
10+
// Arrange
11+
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Action"));
12+
13+
// Act
14+
var validationErrors = Validate(file);
15+
16+
// Assert
17+
validationErrors.ShouldContainValidationError(
18+
"Action",
19+
"Action is missing",
20+
ErrorCodes.MissingAction
21+
);
22+
}
23+
24+
[Theory]
25+
[InlineData("A")] // invalid character
26+
[InlineData("D")] // invalid character
27+
[InlineData("$")] // invalid character
28+
[InlineData("b")] // lowercase
29+
[InlineData("")] // Blank
30+
[InlineData(" ")] // Whitespace
31+
[InlineData("BC")] // Too many characters
32+
[InlineData("BCU")] // Too many characters
33+
public void Validate_ActionInvalidFormat_ReturnsValidationError(string value)
34+
{
35+
// Arrange
36+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Action"] = value);
37+
38+
// Act
39+
var validationErrors = Validate(file).ToList();
40+
41+
// Assert
42+
validationErrors.ShouldContainValidationError(
43+
"Action",
44+
"Action is in an invalid format",
45+
ErrorCodes.InvalidAction
46+
);
47+
}
48+
49+
[Theory]
50+
[InlineData("B")]
51+
[InlineData("C")]
52+
[InlineData("U")]
53+
public void Validate_ActionValidFormat_NoValidationErrorsReturned(string value)
54+
{
55+
// Arrange
56+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Action"] = value);
57+
58+
// Act
59+
var validationErrors = Validate(file).ToList();
60+
61+
// Assert
62+
Assert.Empty(validationErrors);
63+
}
64+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
2+
3+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
4+
5+
public class AppointmentIdValidatorTests : ValidationTestBase
6+
{
7+
[Fact]
8+
public void Validate_AppointmentIdMissing_ReturnsValidationError()
9+
{
10+
// Arrange
11+
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Appointment ID"));
12+
13+
// Act
14+
var validationErrors = Validate(file);
15+
16+
// Assert
17+
validationErrors.ShouldContainValidationError(
18+
"Appointment ID",
19+
"Appointment ID is missing or empty",
20+
ErrorCodes.MissingAppointmentId
21+
);
22+
}
23+
24+
[Theory]
25+
[InlineData("")]
26+
[InlineData(" ")]
27+
public void Validate_AppointmentIdBlank_ReturnsValidationError(string value)
28+
{
29+
// Arrange
30+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appointment ID"] = value);
31+
32+
// Act
33+
var validationErrors = Validate(file);
34+
35+
// Assert
36+
validationErrors.ShouldContainValidationError(
37+
"Appointment ID",
38+
"Appointment ID is missing or empty",
39+
ErrorCodes.MissingAppointmentId
40+
);
41+
}
42+
43+
[Theory]
44+
[InlineData("1234567890123456789012345678")] // 28 characters
45+
[InlineData("1234567890123456789012345678901234567890")] // 40 characters
46+
public void Validate_AppointmentIdTooLong_ReturnsValidationError(string value)
47+
{
48+
// Arrange
49+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appointment ID"] = value);
50+
51+
// Act
52+
var validationErrors = Validate(file);
53+
54+
// Assert
55+
validationErrors.ShouldContainValidationError(
56+
"Appointment ID",
57+
"Appointment ID exceeds maximum length of 27",
58+
ErrorCodes.InvalidAppointmentId
59+
);
60+
}
61+
62+
[Theory]
63+
[InlineData("AS003-67240-RA1-DN-T1315-1")]
64+
[InlineData("AS003-67240-RA1-DN-T1045-01")]
65+
public void Validate_AppointmentIdValid_NoValidationErrorsReturned(string value)
66+
{
67+
// Arrange
68+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appointment ID"] = value);
69+
70+
// Act
71+
var validationErrors = Validate(file);
72+
73+
// Assert
74+
Assert.Empty(validationErrors);
75+
}
76+
}

0 commit comments

Comments
 (0)