From 9e4279455a85d1e45072c3a8061dc7070525d930 Mon Sep 17 00:00:00 2001 From: Ian Nelson Date: Thu, 29 May 2025 09:52:39 +0100 Subject: [PATCH] feat: DTOSS-9160 - NHS Num validator --- .../Validation/NhsNumValidator.cs | 38 +++++++++ .../Validation/RegexValidator.cs | 23 ++++-- .../Validation/ValidatorRegistry.cs | 1 + .../Validation/NhsNumValidatorTests.cs | 82 +++++++++++++++++++ 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/NhsNumValidator.cs create mode 100644 tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/NhsNumValidatorTests.cs diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/NhsNumValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/NhsNumValidator.cs new file mode 100644 index 0000000..0899944 --- /dev/null +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/NhsNumValidator.cs @@ -0,0 +1,38 @@ +using System.Text.RegularExpressions; + +namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +public partial class NhsNumValidator() : + RegexValidator("NHS Num", NhsNumberRegex(), ErrorCodes.MissingNhsNum, ErrorCodes.InvalidNhsNum) +{ + protected override IEnumerable RunAdditionalChecks(int rowNumber, string value) + { + if (!HasValidCheckDigit(value)) + { + yield return new ValidationError + { + RowNumber = rowNumber, + Field = FieldName, + Error = "NHS Num has invalid check digit", + Code = ErrorCodes.InvalidNhsNumCheckDigit + }; + } + } + + private static bool HasValidCheckDigit(string value) + { + var weightedSum = 0; + for (var i = 0; i < 9; i++) + { + weightedSum += (10 - i) * (value[i] - '0'); + } + + var remainder = weightedSum % 11; + var expectedCheckDigit = (11 - remainder) % 11; + + return expectedCheckDigit == value[9] - '0'; + } + + [GeneratedRegex(@"^\d{10}$", RegexOptions.Compiled)] + private static partial Regex NhsNumberRegex(); +} diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/RegexValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/RegexValidator.cs index 0bf7834..4cb6779 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/RegexValidator.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/RegexValidator.cs @@ -10,17 +10,19 @@ public class RegexValidator( string errorCodeInvalidFormat) : IRecordValidator { + protected string FieldName { get; } = fieldName; + public IEnumerable Validate(FileDataRecord fileDataRecord) { - var value = fileDataRecord[fieldName]; + var value = fileDataRecord[FieldName]; if (value == null) { yield return new ValidationError { RowNumber = fileDataRecord.RowNumber, - Field = fieldName, - Error = $"{fieldName} is missing", + Field = FieldName, + Error = $"{FieldName} is missing", Code = errorCodeMissing, }; yield break; @@ -31,10 +33,21 @@ public IEnumerable Validate(FileDataRecord fileDataRecord) yield return new ValidationError { RowNumber = fileDataRecord.RowNumber, - Field = fieldName, - Error = $"{fieldName} is in an invalid format", + Field = FieldName, + Error = $"{FieldName} is in an invalid format", Code = errorCodeInvalidFormat, }; + yield break; + } + + foreach (var additionalError in RunAdditionalChecks(fileDataRecord.RowNumber, value)) + { + yield return additionalError; } } + + protected virtual IEnumerable RunAdditionalChecks(int rowNumber, string value) + { + yield break; + } } diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs index 5096d19..76baf02 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs @@ -24,6 +24,7 @@ public static IEnumerable GetAllRecordValidators() ErrorCodes.InvalidAttendedNotScr), new MaxLengthValidator("Appointment ID", 27, ErrorCodes.MissingAppointmentId, ErrorCodes.InvalidAppointmentId), + new NhsNumValidator(), new RegexValidator("Episode Type", EpisodeTypeRegex(), ErrorCodes.MissingEpisodeType, ErrorCodes.InvalidEpisodeType), new MaxLengthValidator("Batch ID", 9, ErrorCodes.MissingBatchId, diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/NhsNumValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/NhsNumValidatorTests.cs new file mode 100644 index 0000000..0a9372a --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/NhsNumValidatorTests.cs @@ -0,0 +1,82 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class NhsNumValidatorTests : ValidationTestBase +{ + [Fact] + public void Validate_NhsNumMissing_ReturnsValidationError() + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("NHS Num")); + + // Act + var validationErrors = Validate(file); + + // Assert + validationErrors.ShouldContainValidationError( + "NHS Num", + "NHS Num is missing", + ErrorCodes.MissingNhsNum + ); + } + + [Theory] + [InlineData("308 407 5425")] // we don't anticipate spaces + [InlineData("857320211")] // too few character + [InlineData("90238807571")] // Too many characters + [InlineData("159278895S")] // invalid characters + public void Validate_NhsNumInvalidFormat_ReturnsValidationError(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["NHS Num"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + validationErrors.ShouldContainValidationError( + "NHS Num", + "NHS Num is in an invalid format", + ErrorCodes.InvalidNhsNum + ); + } + + [Theory] + [InlineData("3244700471")] + [InlineData("7326012282")] + [InlineData("6245827145")] + [InlineData("4745895257")] + public void Validate_NhsNumInvalidCheckDigit_ReturnsValidationError(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["NHS Num"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + validationErrors.ShouldContainValidationError( + "NHS Num", + "NHS Num has invalid check digit", + ErrorCodes.InvalidNhsNumCheckDigit + ); + } + + [Theory] + [InlineData("4941273230")] + [InlineData("6451357219")] + [InlineData("3365582983")] + [InlineData("8799244780")] + public void Validate_NHSNumValidFormat_NoValidationErrorsReturned(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["NHS Num"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + Assert.Empty(validationErrors); + } +}