Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public static class ErrorCodes
{
public const string UnableToParseFile = "NBSSAPPT001";
public const string MissingHeader = "NBSSAPPT002";
public const string MissingExtractId = "NBSSAPPT003";
public const string InvalidExtractId = "NBSSAPPT004";
public const string MissingRecordCount = "NBSSAPPT005";
public const string InvalidRecordCount = "NBSSAPPT006";
public const string MissingTrailer = "NBSSAPPT007";
public const string InconsistentExtractId = "NBSSAPPT008";
public const string InconsistentRecordCount = "NBSSAPPT009";
public const string MissingFieldHeadings = "NBSSAPPT010";
public const string UnexpectedRecordCount = "NBSSAPPT011";
public const string MissingSequence = "NBSSAPPT012";
public const string InvalidSequence = "NBSSAPPT013";
public const string MissingBso = "NBSSAPPT014";
public const string InvalidBso = "NBSSAPPT015";
public const string MissingAction = "NBSSAPPT016";
public const string InvalidAction = "NBSSAPPT017";
public const string MissingClinicCode = "NBSSAPPT018";
public const string InvalidClinicCode = "NBSSAPPT019";
public const string MissingHoldingClinic = "NBSSAPPT020";
public const string InvalidHoldingClinic = "NBSSAPPT021";
public const string MissingStatus = "NBSSAPPT022";
public const string InvalidStatus = "NBSSAPPT023";
public const string MissingAttendedNotScr = "NBSSAPPT024";
public const string InvalidAttendedNotScr = "NBSSAPPT025";
public const string MissingAppointmentId = "NBSSAPPT026";
public const string InvalidAppointmentId = "NBSSAPPT027";
public const string MissingNhsNum = "NBSSAPPT028";
public const string InvalidNhsNum = "NBSSAPPT029";
public const string InvalidNhsNumCheckDigit = "NBSSAPPT030";
public const string MissingEpisodeType = "NBSSAPPT031";
public const string InvalidEpisodeType = "NBSSAPPT032";
public const string MissingEpisodeStart = "NBSSAPPT033";
public const string InvalidEpisodeStart = "NBSSAPPT034";
public const string MissingBatchId = "NBSSAPPT035";
public const string InvalidBatchId = "NBSSAPPT036";
public const string MissingScreenOrAsses = "NBSSAPPT037";
public const string InvalidScreenOrAsses = "NBSSAPPT038";
public const string MissingScreenApptNum = "NBSSAPPT039";
public const string InvalidScreenApptNum = "NBSSAPPT040";
public const string MissingBookedBy = "NBSSAPPT041";
public const string InvalidBookedBy = "NBSSAPPT042";
public const string MissingCancelledBy = "NBSSAPPT043";
public const string InvalidCancelledBy = "NBSSAPPT044";
public const string MissingApptDate = "NBSSAPPT045";
public const string InvalidApptDate = "NBSSAPPT046";
public const string MissingApptTime = "NBSSAPPT047";
public const string InvalidApptTime = "NBSSAPPT048";
public const string MissingLocation = "NBSSAPPT049";
public const string InvalidLocation = "NBSSAPPT050";
public const string MissingClinicName = "NBSSAPPT051";
public const string InvalidClinicName = "NBSSAPPT052";
public const string MissingClinicNameLet = "NBSSAPPT053";
public const string InvalidClinicNameLet = "NBSSAPPT054";
public const string MissingClinicAddress1 = "NBSSAPPT055";
public const string InvalidClinicAddress1 = "NBSSAPPT056";
public const string MissingClinicAddress2 = "NBSSAPPT057";
public const string InvalidClinicAddress2 = "NBSSAPPT058";
public const string MissingClinicAddress3 = "NBSSAPPT059";
public const string InvalidClinicAddress3 = "NBSSAPPT060";
public const string MissingClinicAddress4 = "NBSSAPPT061";
public const string InvalidClinicAddress4 = "NBSSAPPT062";
public const string MissingClinicAddress5 = "NBSSAPPT063";
public const string InvalidClinicAddress5 = "NBSSAPPT064";
public const string MissingPostcode = "NBSSAPPT065";
public const string InvalidPostcode = "NBSSAPPT066";
public const string MissingActionTimestamp = "NBSSAPPT067";
public const string InvalidActionTimestamp = "NBSSAPPT068";
public const string UnknownRecordTypeIdentifier = "NBSSAPPT069";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public class MaxLengthValidator(
string fieldName,
int maxLength,
string errorCodeMissing,
string errorCodeTooLong,
bool allowEmpty = false)
: IRecordValidator
{
public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
{
var value = fileDataRecord[fieldName];

if (value == null || (!allowEmpty && string.IsNullOrWhiteSpace(value)))
{
var error = $"{fieldName} is missing{(allowEmpty ? "" : " or empty")}";

yield return new ValidationError
{
RowNumber = fileDataRecord.RowNumber,
Field = fieldName,
Error = error,
Code = errorCodeMissing,
};
yield break;
}

if (value.Length > maxLength)
{
var error = $"{fieldName} exceeds maximum length of {maxLength}";

yield return new ValidationError
{
RowNumber = fileDataRecord.RowNumber,
Field = fieldName,
Error = error,
Code = errorCodeTooLong,
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Text.RegularExpressions;
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public class RegexValidator(
string fieldName,
Regex pattern,
string errorCodeMissing,
string errorCodeInvalidFormat)
: IRecordValidator
{
public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
{
var value = fileDataRecord[fieldName];

if (value == null)
{
yield return new ValidationError
{
RowNumber = fileDataRecord.RowNumber,
Field = fieldName,
Error = $"{fieldName} is missing",
Code = errorCodeMissing,
};
yield break;
}

if (!pattern.IsMatch(value))
{
yield return new ValidationError
{
RowNumber = fileDataRecord.RowNumber,
Field = fieldName,
Error = $"{fieldName} is in an invalid format",
Code = errorCodeInvalidFormat,
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System.Text.RegularExpressions;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public static partial class ValidatorRegistry
{
public static IEnumerable<IRecordValidator> GetAllRecordValidators()
{
return
[
new RegexValidator("Sequence", SequenceRegex(), ErrorCodes.MissingSequence,
ErrorCodes.InvalidSequence),
new MaxLengthValidator("BSO", 3, ErrorCodes.MissingBso,
ErrorCodes.InvalidBso),
new RegexValidator("Action", ActionRegex(), ErrorCodes.MissingAction,
ErrorCodes.InvalidAction),
new MaxLengthValidator("Clinic Code", 5, ErrorCodes.MissingClinicCode,
ErrorCodes.InvalidClinicCode),
new RegexValidator("Holding Clinic", YesNoBlankRegex(), ErrorCodes.MissingHoldingClinic,
ErrorCodes.InvalidHoldingClinic),
new RegexValidator("Status", StatusRegex(), ErrorCodes.MissingStatus,
ErrorCodes.InvalidStatus),
new RegexValidator("Attended Not Scr", YesNoBlankRegex(), ErrorCodes.MissingAttendedNotScr,
ErrorCodes.InvalidAttendedNotScr),
new MaxLengthValidator("Appointment ID", 27, ErrorCodes.MissingAppointmentId,
ErrorCodes.InvalidAppointmentId),
new RegexValidator("Episode Type", EpisodeTypeRegex(), ErrorCodes.MissingEpisodeType,
ErrorCodes.InvalidEpisodeType),
new MaxLengthValidator("Batch ID", 9, ErrorCodes.MissingBatchId,
ErrorCodes.InvalidBatchId),
new RegexValidator("Screen or Asses", ScreenOrAssesRegex(), ErrorCodes.MissingScreenOrAsses,
ErrorCodes.InvalidScreenOrAsses),
new RegexValidator("Screen Appt num", ScreenApptNumRegex(), ErrorCodes.MissingScreenApptNum,
ErrorCodes.InvalidScreenApptNum),
new RegexValidator("Booked By", BookedByRegex(), ErrorCodes.MissingBookedBy,
ErrorCodes.InvalidBookedBy),
new RegexValidator("Cancelled By", CancelledByRegex(), ErrorCodes.MissingCancelledBy,
ErrorCodes.InvalidCancelledBy),
new MaxLengthValidator("Location", 5, ErrorCodes.MissingLocation,
ErrorCodes.InvalidLocation),
new MaxLengthValidator("Clinic Name", 40, ErrorCodes.MissingClinicName,
ErrorCodes.InvalidClinicName, true),
new MaxLengthValidator("Clinic Name (Let)", 50, ErrorCodes.MissingClinicNameLet,
ErrorCodes.InvalidClinicNameLet, true),
new MaxLengthValidator("Clinic Address 1", 30, ErrorCodes.MissingClinicAddress1,
ErrorCodes.InvalidClinicAddress1, true),
new MaxLengthValidator("Clinic Address 2", 30, ErrorCodes.MissingClinicAddress2,
ErrorCodes.InvalidClinicAddress2, true),
new MaxLengthValidator("Clinic Address 3", 30, ErrorCodes.MissingClinicAddress3,
ErrorCodes.InvalidClinicAddress3, true),
new MaxLengthValidator("Clinic Address 4", 30, ErrorCodes.MissingClinicAddress4,
ErrorCodes.InvalidClinicAddress4, true),
new MaxLengthValidator("Clinic Address 5", 30, ErrorCodes.MissingClinicAddress5,
ErrorCodes.InvalidClinicAddress5, true),
new MaxLengthValidator("Postcode", 8, ErrorCodes.MissingPostcode,
ErrorCodes.InvalidPostcode, true),
];
}

public static IEnumerable<IFileValidator> GetAllFileValidators()
{
return [];
}

[GeneratedRegex(@"^[BCU]$", RegexOptions.Compiled)]
private static partial Regex ActionRegex();

[GeneratedRegex(@"^[CH]$", RegexOptions.Compiled)]
private static partial Regex BookedByRegex();

[GeneratedRegex(@"^$|^[ CH]$", RegexOptions.Compiled)]
private static partial Regex CancelledByRegex();

[GeneratedRegex(@"^[FGHNRST]$", RegexOptions.Compiled)]
private static partial Regex EpisodeTypeRegex();

[GeneratedRegex(@"^[AS]$", RegexOptions.Compiled)]
private static partial Regex ScreenOrAssesRegex();

[GeneratedRegex(@"^$|^[1-9]$", RegexOptions.Compiled)]
private static partial Regex ScreenApptNumRegex();

[GeneratedRegex(@"^(?!000000)\d{6}$", RegexOptions.Compiled)]
private static partial Regex SequenceRegex();

[GeneratedRegex(@"^[ABCD]$", RegexOptions.Compiled)]
private static partial Regex StatusRegex();

[GeneratedRegex(@"^$|^[ YN]$", RegexOptions.Compiled)]
private static partial Regex YesNoBlankRegex();
}
2 changes: 2 additions & 0 deletions src/ServiceLayer.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<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">
<s:Boolean x:Key="/Default/UserDictionary/Words/=nbssappt/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;

public class ActionValidatorTests : ValidationTestBase
{
[Fact]
public void Validate_ActionMissing_ReturnsValidationError()
{
// Arrange
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Action"));

// Act
var validationErrors = Validate(file);

// Assert
validationErrors.ShouldContainValidationError(
"Action",
"Action is missing",
ErrorCodes.MissingAction
);
}

[Theory]
[InlineData("A")] // invalid character
[InlineData("D")] // invalid character
[InlineData("$")] // invalid character
[InlineData("b")] // lowercase
[InlineData("")] // Blank
[InlineData(" ")] // Whitespace
[InlineData("BC")] // Too many characters
[InlineData("BCU")] // Too many characters
public void Validate_ActionInvalidFormat_ReturnsValidationError(string value)
{
// Arrange
var file = ParsedFileWithModifiedRecord(r => r.Fields["Action"] = value);

// Act
var validationErrors = Validate(file).ToList();

// Assert
validationErrors.ShouldContainValidationError(
"Action",
"Action is in an invalid format",
ErrorCodes.InvalidAction
);
}

[Theory]
[InlineData("B")]
[InlineData("C")]
[InlineData("U")]
public void Validate_ActionValidFormat_NoValidationErrorsReturned(string value)
{
// Arrange
var file = ParsedFileWithModifiedRecord(r => r.Fields["Action"] = value);

// Act
var validationErrors = Validate(file).ToList();

// Assert
Assert.Empty(validationErrors);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;

public class AppointmentIdValidatorTests : ValidationTestBase
{
[Fact]
public void Validate_AppointmentIdMissing_ReturnsValidationError()
{
// Arrange
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Appointment ID"));

// Act
var validationErrors = Validate(file);

// Assert
validationErrors.ShouldContainValidationError(
"Appointment ID",
"Appointment ID is missing or empty",
ErrorCodes.MissingAppointmentId
);
}

[Theory]
[InlineData("")]
[InlineData(" ")]
public void Validate_AppointmentIdBlank_ReturnsValidationError(string value)
{
// Arrange
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appointment ID"] = value);

// Act
var validationErrors = Validate(file);

// Assert
validationErrors.ShouldContainValidationError(
"Appointment ID",
"Appointment ID is missing or empty",
ErrorCodes.MissingAppointmentId
);
}

[Theory]
[InlineData("1234567890123456789012345678")] // 28 characters
[InlineData("1234567890123456789012345678901234567890")] // 40 characters
public void Validate_AppointmentIdTooLong_ReturnsValidationError(string value)
{
// Arrange
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appointment ID"] = value);

// Act
var validationErrors = Validate(file);

// Assert
validationErrors.ShouldContainValidationError(
"Appointment ID",
"Appointment ID exceeds maximum length of 27",
ErrorCodes.InvalidAppointmentId
);
}

[Theory]
[InlineData("AS003-67240-RA1-DN-T1315-1")]
[InlineData("AS003-67240-RA1-DN-T1045-01")]
public void Validate_AppointmentIdValid_NoValidationErrorsReturned(string value)
{
// Arrange
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appointment ID"] = value);

// Act
var validationErrors = Validate(file);

// Assert
Assert.Empty(validationErrors);
}
}
Loading
Loading