From 67e3e2f947850419adcc3fe2dbfdbb0a1b3991ab Mon Sep 17 00:00:00 2001 From: Michael Clayson Date: Fri, 12 Dec 2025 15:53:06 +0000 Subject: [PATCH 1/9] Adding New Rules --- .../CohortManager/compose.wiremock.yaml | 1 + .../ValidateParticipant.cs | 5 +- .../TransformDataService/Program.cs | 1 + .../TransformDataService.cs | 48 ++- .../TransformDataService/transformRules.json | 38 ++ .../complete-patient-9990360855.json | 367 ++++++++++++++++++ .../complete-patient-9991970592.json | 367 ++++++++++++++++++ .../complete-patient-9998582997.json | 343 ++++++++++++++++ .../Breast_Screening_staticRules.json | 31 +- .../StaticValidation/Program.cs | 1 + .../StaticValidation/StaticValidation.cs | 32 +- .../IReasonForRemovalLookup.cs | 6 + .../ReasonForRemovalLookup.cs | 36 ++ .../Shared/Common/TransformDataRequestBody.cs | 1 + .../StaticValidation/StaticValidationTests.cs | 107 ++++- 15 files changed, 1367 insertions(+), 17 deletions(-) create mode 100644 application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9990360855.json create mode 100644 application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9991970592.json create mode 100644 application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9998582997.json create mode 100644 application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/IReasonForRemovalLookup.cs create mode 100644 application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/ReasonForRemovalLookup.cs diff --git a/application/CohortManager/compose.wiremock.yaml b/application/CohortManager/compose.wiremock.yaml index b9ed7ba76f..f3badf16d9 100644 --- a/application/CohortManager/compose.wiremock.yaml +++ b/application/CohortManager/compose.wiremock.yaml @@ -7,6 +7,7 @@ services: build: context: ./src/Functions/ dockerfile: Shared/Wiremock/Dockerfile + user: "0" ports: - "8080:8080" volumes: diff --git a/application/CohortManager/src/Functions/CohortDistributionServices/DistributeParticipant/ValidateParticipant.cs b/application/CohortManager/src/Functions/CohortDistributionServices/DistributeParticipant/ValidateParticipant.cs index 499be80380..f552e4c6e1 100644 --- a/application/CohortManager/src/Functions/CohortDistributionServices/DistributeParticipant/ValidateParticipant.cs +++ b/application/CohortManager/src/Functions/CohortDistributionServices/DistributeParticipant/ValidateParticipant.cs @@ -65,7 +65,7 @@ public ValidateParticipant(IDataServiceClient cohortDistribu await removeValidationRecordTask; validationRecord.PreviousParticipantRecord = previousRecord; - + // Lookup & Static Validation var lookupTaskOptions = TaskOptions.FromRetryPolicy(new RetryPolicy( maxNumberOfAttempts: _config.MaxLookupValidationRetries, @@ -215,6 +215,7 @@ public async Task> LookupValidation([ActivityTrigger] var transformDataRequestBody = new TransformDataRequestBody() { Participant = validationRecord.Participant, + FileName = validationRecord.FileName, // TODO: is this used? ServiceProvider = validationRecord.ServiceProvider, ExistingParticipant = validationRecord.PreviousParticipantRecord.ToCohortDistribution() @@ -258,4 +259,4 @@ public async Task HandleValidationExceptions([ActivityTrigger] ValidationExcepti } _logger.LogInformation("Created validation exception and set exception flag to 1 for participant {ParticipantId}", participantRecord.Participant.ParticipantId); } -} \ No newline at end of file +} diff --git a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/Program.cs b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/Program.cs index 7e1c4c631f..86498642fc 100644 --- a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/Program.cs +++ b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/Program.cs @@ -24,6 +24,7 @@ services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddMemoryCache(); // Register health checks diff --git a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs index 9ce78e99b4..eb06845d24 100644 --- a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs +++ b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs @@ -28,13 +28,15 @@ public class TransformDataService private readonly IExceptionHandler _exceptionHandler; private readonly ITransformReasonForRemoval _transformReasonForRemoval; private readonly ITransformDataLookupFacade _dataLookup; + private readonly IReasonForRemovalLookup _reasonForRemovalLookup; public TransformDataService( ICreateResponse createResponse, IExceptionHandler exceptionHandler, ILogger logger, ITransformReasonForRemoval transformReasonForRemoval, - ITransformDataLookupFacade dataLookup + ITransformDataLookupFacade dataLookup, + IReasonForRemovalLookup reasonForRemovalLookup ) { _createResponse = createResponse; @@ -42,6 +44,7 @@ ITransformDataLookupFacade dataLookup _logger = logger; _transformReasonForRemoval = transformReasonForRemoval; _dataLookup = dataLookup; + _reasonForRemovalLookup = reasonForRemovalLookup; } [Function("TransformDataService")] @@ -77,7 +80,7 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano participant = await transformString.TransformStringFields(participant); // Other transformation rules - participant = await TransformParticipantAsync(participant, requestBody.ExistingParticipant); + participant = await TransformParticipantAsync(participant, requestBody.ExistingParticipant,CheckManualAddFileName(requestBody.FileName)); // Name prefix transformation if (participant.NamePrefix != null) @@ -85,17 +88,25 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano participant = await _transformReasonForRemoval.ReasonForRemovalTransformations(participant, requestBody.ExistingParticipant); - if (participant.NhsNumber != null) + + + + if (participant.NhsNumber == null) { - var response = JsonSerializer.Serialize(participant); - return _createResponse.CreateHttpResponse(HttpStatusCode.OK, req, response); + return _createResponse.CreateHttpResponse(HttpStatusCode.Accepted, req, ""); + } - return _createResponse.CreateHttpResponse(HttpStatusCode.Accepted, req, ""); + + var response = JsonSerializer.Serialize(participant); + return _createResponse.CreateHttpResponse(HttpStatusCode.OK, req, response); + + + } catch (ArgumentException ex) { _logger.LogWarning(ex, "An error occurred during transformation"); - await _exceptionHandler.CreateSystemExceptionLogFromNhsNumber(ex, participant.NhsNumber, "", participant.ScreeningName, JsonSerializer.Serialize(participant)); + await _exceptionHandler.CreateSystemExceptionLogFromNhsNumber(ex, participant.NhsNumber, requestBody.FileName!, participant.ScreeningName, JsonSerializer.Serialize(participant)); return _createResponse.CreateHttpResponse(HttpStatusCode.Accepted, req); } catch (TransformationException ex) @@ -105,14 +116,14 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano } catch (Exception ex) { - await _exceptionHandler.CreateSystemExceptionLogFromNhsNumber(ex, participant.NhsNumber, "", participant.ScreeningName, JsonSerializer.Serialize(participant)); + await _exceptionHandler.CreateSystemExceptionLogFromNhsNumber(ex, participant.NhsNumber, requestBody.FileName!, participant.ScreeningName, JsonSerializer.Serialize(participant)); _logger.LogWarning(ex, "exception occurred while running transform data service"); return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req); } } public async Task TransformParticipantAsync(CohortDistributionParticipant participant, - CohortDistribution databaseParticipant) + CohortDistribution databaseParticipant, bool isManualAdd = false) { var excludedSMUList = await _dataLookup.GetCachedExcludedSMUValues(); @@ -133,7 +144,8 @@ public async Task TransformParticipantAsync(Cohor new RuleParameter("participant", participant), new RuleParameter("dbLookup", _dataLookup), new RuleParameter("excludedSMUList", excludedSMUList), - new RuleParameter("existingParticipant", existingParticipant) + new RuleParameter("existingParticipant", existingParticipant), + new RuleParameter("reasonForRemovalLkp",_reasonForRemovalLookup) }; var resultList = await re.ExecuteAllRulesAsync("Common", ruleParameters); @@ -142,6 +154,10 @@ public async Task TransformParticipantAsync(Cohor { resultList.AddRange(await re.ExecuteAllRulesAsync("Referred", ruleParameters)); } + if (isManualAdd) + { + resultList.AddRange(await re.ExecuteAllRulesAsync("ManualAdd", ruleParameters)); + } await HandleExceptions(resultList, participant); await CreateTransformExecutedExceptions(resultList, participant); @@ -216,5 +232,17 @@ private async Task CreateTransformExecutedExceptions(List except await _exceptionHandler.CreateTransformExecutedExceptions(participant, ruleName, ruleId); } } + private static bool CheckManualAddFileName(string? FileName) + { + if(string.IsNullOrEmpty(FileName)) + { + return false; + } + if (FileName.ToLower().EndsWith(".parquet")) + { + return false; + } + return true; + } } diff --git a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json index d98b271d72..2478581100 100644 --- a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json +++ b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json @@ -747,5 +747,43 @@ } } ] + }, + { + "WorkflowName": "ManualAdd", + "Rules": [ + { + "RuleName": "96.UpdateRFR.ManualAdd", + "LocalParams": [ + { + "Name": "CanTransformRfr", + "Expression": "reasonForRemovalLkp.CanRemovalReasonBeOverridden(participant.ReasonForRemoval)" + }, + { + "Name": "HasPrimaryCareProvider", + "Expression": "!string.IsNullOrEmpty(participant.PrimaryCareProvider)" + } + ], + "Expression": "CanTransformRfr && HasPrimaryCareProvider", + "Actions": { + "OnSuccess": { + "Name": "TransformAction", + "Context": { + "transformFields": [ + { + "field": "ReasonForRemoval", + "value": null, + "isExpression": false + }, + { + "field": "ReasonForRemoval", + "value": null, + "isExpression": false + } + ] + } + } + } + } + ] } ] diff --git a/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9990360855.json b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9990360855.json new file mode 100644 index 0000000000..c06c9c239d --- /dev/null +++ b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9990360855.json @@ -0,0 +1,367 @@ +{ + "resourceType": "Patient", + "id": "9990360855", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9990360855", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatus", + "version": "1.0.0", + "code": "01", + "display": "Number present and verified" + } + ] + } + } + ] + } + ], + "meta": { + "versionId": "2", + "security": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality", + "code": "U", + "display": "unrestricted" + } + ] + }, + "name": [ + { + "id": "123", + "use": "usual", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "given": [ + "Jane" + ], + "family": "Smith", + "prefix": [ + "Mrs" + ] + } + ], + "gender": "female", + "birthDate": "1980-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + } + ], + "managingOrganization": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + }, + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NominatedPharmacy", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y23456" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y34567" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSCommunication", + "extension": [ + { + "url": "language", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-HumanLanguage", + "version": "1.0.0", + "code": "fr", + "display": "French" + } + ] + } + }, + { + "url": "interpreterRequired", + "valueBoolean": true + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-ContactPreference", + "extension": [ + { + "url": "PreferredWrittenCommunicationFormat", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredWrittenCommunicationFormat", + "code": "12", + "display": "Braille" + } + ] + } + }, + { + "url": "PreferredContactMethod", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredContactMethod", + "code": "1", + "display": "Letter" + } + ] + } + }, + { + "url": "PreferredContactTimes", + "valueString": "Not after 7pm" + } + ] + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", + "valueAddress": { + "city": "Manchester", + "district": "Greater Manchester", + "country": "GBR" + } + }, + { + "url": "https://fhir.nhs.uk/StructureDefinition/Extension-PDS-RemovalFromRegistration", + "extension": [ + { + "url": "removalFromRegistrationCode", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/PDS-RemovalReasonExitCode", + "code": "AFL", + "display": "armed forces enlistment - notified locally)" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2021-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "phone", + "value": "01632960587", + "use": "home" + }, + { + "id": "790", + "period": { + "start": "2019-01-01", + "end": "2022-12-31" + }, + "system": "email", + "value": "jane.smith@example.com", + "use": "home" + }, + { + "id": "OC789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "other", + "value": "01632960587", + "use": "home", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-OtherContactSystem", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-OtherContactSystem", + "code": "textphone", + "display": "Minicom (Textphone)" + } + } + ] + } + ], + "contact": [ + { + "id": "C123", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "relationship": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0131", + "code": "C", + "display": "Emergency Contact" + } + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "01632960587" + } + ] + } + ], + "address": [ + { + "id": "456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "home", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + }, + { + "id": "T456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "temp", + "text": "Student Accommodation", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + } + ] +} diff --git a/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9991970592.json b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9991970592.json new file mode 100644 index 0000000000..7ff0daf00a --- /dev/null +++ b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9991970592.json @@ -0,0 +1,367 @@ +{ + "resourceType": "Patient", + "id": "9991970592", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9991970592", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatus", + "version": "1.0.0", + "code": "01", + "display": "Number present and verified" + } + ] + } + } + ] + } + ], + "meta": { + "versionId": "2", + "security": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality", + "code": "U", + "display": "unrestricted" + } + ] + }, + "name": [ + { + "id": "123", + "use": "usual", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "given": [ + "Jane" + ], + "family": "Smith", + "prefix": [ + "Mrs" + ] + } + ], + "gender": "female", + "birthDate": "1980-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + } + ], + "managingOrganization": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + }, + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NominatedPharmacy", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y23456" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y34567" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSCommunication", + "extension": [ + { + "url": "language", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-HumanLanguage", + "version": "1.0.0", + "code": "fr", + "display": "French" + } + ] + } + }, + { + "url": "interpreterRequired", + "valueBoolean": true + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-ContactPreference", + "extension": [ + { + "url": "PreferredWrittenCommunicationFormat", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredWrittenCommunicationFormat", + "code": "12", + "display": "Braille" + } + ] + } + }, + { + "url": "PreferredContactMethod", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredContactMethod", + "code": "1", + "display": "Letter" + } + ] + } + }, + { + "url": "PreferredContactTimes", + "valueString": "Not after 7pm" + } + ] + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", + "valueAddress": { + "city": "Manchester", + "district": "Greater Manchester", + "country": "GBR" + } + }, + { + "url": "https://fhir.nhs.uk/StructureDefinition/Extension-PDS-RemovalFromRegistration", + "extension": [ + { + "url": "removalFromRegistrationCode", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/PDS-RemovalReasonExitCode", + "code": "NIT", + "display": "Transferred to Northern Ireland" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2021-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "phone", + "value": "01632960587", + "use": "home" + }, + { + "id": "790", + "period": { + "start": "2019-01-01", + "end": "2022-12-31" + }, + "system": "email", + "value": "jane.smith@example.com", + "use": "home" + }, + { + "id": "OC789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "other", + "value": "01632960587", + "use": "home", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-OtherContactSystem", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-OtherContactSystem", + "code": "textphone", + "display": "Minicom (Textphone)" + } + } + ] + } + ], + "contact": [ + { + "id": "C123", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "relationship": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0131", + "code": "C", + "display": "Emergency Contact" + } + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "01632960587" + } + ] + } + ], + "address": [ + { + "id": "456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "home", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + }, + { + "id": "T456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "temp", + "text": "Student Accommodation", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + } + ] +} diff --git a/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9998582997.json b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9998582997.json new file mode 100644 index 0000000000..2d2cdf6396 --- /dev/null +++ b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9998582997.json @@ -0,0 +1,343 @@ +{ + "resourceType": "Patient", + "id": "9998582997", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9998582997", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatus", + "version": "1.0.0", + "code": "01", + "display": "Number present and verified" + } + ] + } + } + ] + } + ], + "meta": { + "versionId": "2", + "security": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality", + "code": "U", + "display": "unrestricted" + } + ] + }, + "name": [ + { + "id": "123", + "use": "usual", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "given": [ + "Jane" + ], + "family": "Smith", + "prefix": [ + "Mrs" + ] + } + ], + "gender": "female", + "birthDate": "1980-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + } + ], + "managingOrganization": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + }, + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NominatedPharmacy", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y23456" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y34567" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSCommunication", + "extension": [ + { + "url": "language", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-HumanLanguage", + "version": "1.0.0", + "code": "fr", + "display": "French" + } + ] + } + }, + { + "url": "interpreterRequired", + "valueBoolean": true + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-ContactPreference", + "extension": [ + { + "url": "PreferredWrittenCommunicationFormat", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredWrittenCommunicationFormat", + "code": "12", + "display": "Braille" + } + ] + } + }, + { + "url": "PreferredContactMethod", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredContactMethod", + "code": "1", + "display": "Letter" + } + ] + } + }, + { + "url": "PreferredContactTimes", + "valueString": "Not after 7pm" + } + ] + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", + "valueAddress": { + "city": "Manchester", + "district": "Greater Manchester", + "country": "GBR" + } + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "phone", + "value": "01632960587", + "use": "home" + }, + { + "id": "790", + "period": { + "start": "2019-01-01", + "end": "2022-12-31" + }, + "system": "email", + "value": "jane.smith@example.com", + "use": "home" + }, + { + "id": "OC789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "other", + "value": "01632960587", + "use": "home", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-OtherContactSystem", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-OtherContactSystem", + "code": "textphone", + "display": "Minicom (Textphone)" + } + } + ] + } + ], + "contact": [ + { + "id": "C123", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "relationship": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0131", + "code": "C", + "display": "Emergency Contact" + } + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "01632960587" + } + ] + } + ], + "address": [ + { + "id": "456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "home", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + }, + { + "id": "T456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "temp", + "text": "Student Accommodation", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + } + ] +} diff --git a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json index 9d09d462fb..6de869c374 100644 --- a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json +++ b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json @@ -93,7 +93,7 @@ }, { "RuleName": "3.PrimaryCareProviderAndReasonForRemoval.NBO.NonFatal", - "Expression": "(string.IsNullOrEmpty(participant.PrimaryCareProvider) AND !string.IsNullOrEmpty(participant.ReasonForRemoval)) OR (!string.IsNullOrEmpty(participant.PrimaryCareProvider) AND string.IsNullOrEmpty(participant.ReasonForRemoval))", + "Expression": "manualAdd OR ((string.IsNullOrEmpty(participant.PrimaryCareProvider) AND !string.IsNullOrEmpty(participant.ReasonForRemoval)) OR (!string.IsNullOrEmpty(participant.PrimaryCareProvider) AND string.IsNullOrEmpty(participant.ReasonForRemoval)))", "Actions": { "OnFailure": { "Name": "OutputExpression", @@ -174,5 +174,34 @@ } } ] + }, + { + "WorkflowName": "Manual_Add", + "Rules": [ + { + "RuleName": "97.NoGPorPCP.BSSelect.NonFatal", + "Expression": "!string.IsNullOrEmpty(participant.PrimaryCareProvider)", + "Actions": { + "OnFailure": { + "Name": "OutputExpression", + "Context": { + "Expression": "\"Missing GP details (and no accepted reason for removal)\"" + } + } + } + }, + { + "RuleName": "98.ManualAddWithBlockingRFR.BSSelect.NonFatal", + "Expression": "reasonForRemovalLkp.CanRemovalReasonBeOverridden(participant.ReasonForRemoval)", + "Actions": { + "OnFailure": { + "Name": "OutputExpression", + "Context": { + "Expression": "\"Participant cannot be sent to BS Select due to reason for removal (RfR)\"" + } + } + } + } + ] } ] diff --git a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Program.cs b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Program.cs index cf9cd8bffb..370b0a9a92 100644 --- a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Program.cs +++ b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Program.cs @@ -10,6 +10,7 @@ { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Register health checks services.AddBasicHealthCheck("StaticValidation"); }) diff --git a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs index 65a5a2c690..ffa0fef925 100644 --- a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs +++ b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs @@ -1,6 +1,7 @@ namespace NHS.CohortManager.ScreeningValidationService; using System.Net; +using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -20,15 +21,18 @@ public class StaticValidation private readonly ILogger _logger; private readonly ICreateResponse _createResponse; private readonly IReadRules _readRules; + private readonly IReasonForRemovalLookup _reasonForRemovalLookup; public StaticValidation( ILogger logger, ICreateResponse createResponse, - IReadRules readRules) + IReadRules readRules, + IReasonForRemovalLookup reasonForRemovalLookup) { _logger = logger; _createResponse = createResponse; _readRules = readRules; + _reasonForRemovalLookup = reasonForRemovalLookup; } // TODO: refactor to accept a cohort distribution participant @@ -49,6 +53,7 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano _logger.LogInformation("ruleFileName: {RuleFileName}", ruleFileName); bool routineParticipant = (participantCsvRecord.Participant.ReferralFlag ?? "").ToLower() == "false"; + bool isManualAdd = CheckManualAddFileName(participantCsvRecord.FileName); var json = await _readRules.GetRulesFromDirectory(ruleFileName); var rules = JsonSerializer.Deserialize(json); @@ -62,9 +67,13 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano var re = new RulesEngine.RulesEngine(rules, reSettings); var ruleParameters = new[] { - new RuleParameter("participant", participantCsvRecord.Participant) + new RuleParameter("participant", participantCsvRecord.Participant), + new RuleParameter("reasonForRemovalLkp", _reasonForRemovalLookup), + new RuleParameter("manualAdd",isManualAdd) }; + + var resultList = new List(); if (participantCsvRecord.Participant.RecordType != Actions.Removed) @@ -77,6 +86,12 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano } } + if (isManualAdd) + { + var manualAddResults = await re.ExecuteAllRulesAsync("Manual_Add", ruleParameters); + resultList.AddRange(manualAddResults); + } + if (re.GetAllRegisteredWorkflowNames().Contains(participantCsvRecord.Participant.RecordType)) { _logger.LogInformation("Executing workflow {RecordType}", participantCsvRecord.Participant.RecordType); @@ -100,4 +115,17 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req); } } + + private static bool CheckManualAddFileName(string FileName) + { + if(string.IsNullOrEmpty(FileName)) + { + return false; + } + if (FileName.ToLower().EndsWith(".parquet")) + { + return false; + } + return true; + } } diff --git a/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/IReasonForRemovalLookup.cs b/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/IReasonForRemovalLookup.cs new file mode 100644 index 0000000000..edd8374cb1 --- /dev/null +++ b/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/IReasonForRemovalLookup.cs @@ -0,0 +1,6 @@ +namespace Common; + +public interface IReasonForRemovalLookup +{ + bool CanRemovalReasonBeOverridden(string? reasonForRemoval); +} diff --git a/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/ReasonForRemovalLookup.cs b/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/ReasonForRemovalLookup.cs new file mode 100644 index 0000000000..f453867bdb --- /dev/null +++ b/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/ReasonForRemovalLookup.cs @@ -0,0 +1,36 @@ +namespace Common; + +public class ReasonForRemovalLookup : IReasonForRemovalLookup +{ + + private readonly List NonOverridableRFRs; + private readonly List OverridableRFRs; + public ReasonForRemovalLookup() + { + NonOverridableRFRs = new List + { + "AFL", + "AFN", + "DEA", + "LDN", + "SDL", + "SDN", + "TRA" + }; + } + public bool CanRemovalReasonBeOverridden(string? reasonForRemoval) + { + if(reasonForRemoval is null) + { + return true; + } + + if (NonOverridableRFRs.Contains(reasonForRemoval.ToUpper())) + { + return false; + } + + return true; + } + +} diff --git a/application/CohortManager/src/Functions/Shared/Common/TransformDataRequestBody.cs b/application/CohortManager/src/Functions/Shared/Common/TransformDataRequestBody.cs index 929e646f81..0df72ccb28 100644 --- a/application/CohortManager/src/Functions/Shared/Common/TransformDataRequestBody.cs +++ b/application/CohortManager/src/Functions/Shared/Common/TransformDataRequestBody.cs @@ -7,4 +7,5 @@ public class TransformDataRequestBody public CohortDistributionParticipant Participant { get; set; } public CohortDistribution ExistingParticipant { get; set; } public string? ServiceProvider { get; set; } + public string? FileName {get;set;} } diff --git a/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs b/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs index 4c32dda7b5..136f27e558 100644 --- a/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs +++ b/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs @@ -27,6 +27,7 @@ public class StaticValidationTests private readonly CreateResponse _createResponse = new(); private readonly ServiceCollection _serviceCollection = new(); private readonly ParticipantCsvRecord _participantCsvRecord; + private readonly IReasonForRemovalLookup _reasonForRemovalLookup; private readonly StaticValidation _function; public StaticValidationTests() @@ -37,10 +38,13 @@ public StaticValidationTests() _context.SetupProperty(c => c.InstanceServices, serviceProvider); + _reasonForRemovalLookup = new ReasonForRemovalLookup(); + _function = new StaticValidation( _logger.Object, _createResponse, - new ReadRules(new NullLogger()) + new ReadRules(new NullLogger()), + _reasonForRemovalLookup ); _request.Setup(r => r.CreateResponse()).Returns(() => @@ -54,7 +58,7 @@ public StaticValidationTests() _participantCsvRecord = new ParticipantCsvRecord() { - FileName = "test", + FileName = "test.parquet", Participant = new Participant() { ScreeningName = "Breast Screening", @@ -637,7 +641,106 @@ public async Task Run_CompatibleCurrentPostingAndPrimaryCareProvider_ReturnNoCon Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode); } #endregion + #region Manual add rule 97 Missing GP details (and no accepted reason for removal) + [TestMethod] + [DataRow("A12345")] + [DataRow("ZZZZYZ")] + public async Task Run_ManualAddWithGPCode_ReturnsNoContent(string primaryCareProvider) + { + // Arrange + _participantCsvRecord.Participant.CurrentPosting = "BAA"; + _participantCsvRecord.Participant.PrimaryCareProvider = primaryCareProvider; + + _participantCsvRecord.Participant.RecordType = Actions.New; + _participantCsvRecord.FileName = "CS0573848"; + var json = JsonSerializer.Serialize(_participantCsvRecord); + SetUpRequestBody(json); + + // Act + var response = await _function.RunAsync(_request.Object); + + // Assert + Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode); + + } + [TestMethod] + public async Task Run_ManualAddWithoutGPCode_ReturnValidationException() + { + // Arrange + _participantCsvRecord.Participant.CurrentPosting = "BAA"; + _participantCsvRecord.Participant.PrimaryCareProvider = null; + + _participantCsvRecord.Participant.RecordType = Actions.New; + _participantCsvRecord.FileName = "CS0573848"; + var json = JsonSerializer.Serialize(_participantCsvRecord); + SetUpRequestBody(json); + + // Act + var response = await _function.RunAsync(_request.Object); + + // Assert + string body = await AssertionHelper.ReadResponseBodyAsync(response); + Assert.AreEqual(HttpStatusCode.OK,response.StatusCode); + StringAssert.Contains(body, "97.NoGPorPCP.BSSelect.NonFatal"); + + } + + #endregion + #region Rule 98 Participant cannot be sent to BS Select due to reason for removal (RfR) + [TestMethod] + [DataRow(null)] + [DataRow("CGA")] + [DataRow("NIT")] + [DataRow("EMB")] + [DataRow("SCT")] + [DataRow("OPA")] + public async Task Run_ManualAddWithOverridableReasonForRemoval_ReturnsNoContent(string? reasonForRemoval) + { + // Arrange + _participantCsvRecord.Participant.CurrentPosting = "BAA"; + _participantCsvRecord.Participant.RecordType = Actions.New; + _participantCsvRecord.FileName = "CS0573848"; + _participantCsvRecord.Participant.ReasonForRemoval = reasonForRemoval; + var json = JsonSerializer.Serialize(_participantCsvRecord); + SetUpRequestBody(json); + + // Act + var response = await _function.RunAsync(_request.Object); + string body = await AssertionHelper.ReadResponseBodyAsync(response); + + // Assert + Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode); + } + [TestMethod] + [DataRow("AFL")] + [DataRow("AFN")] + [DataRow("DEA")] + [DataRow("LDN")] + [DataRow("SDL")] + [DataRow("SDN")] + [DataRow("TRA")] + public async Task Run_ManualAddWithoutOverridableReasonForRemoval_ReturnsValidationException(string? reasonForRemoval) + { + // Arrange + _participantCsvRecord.Participant.CurrentPosting = "BAA"; + _participantCsvRecord.Participant.RecordType = Actions.New; + _participantCsvRecord.FileName = "CS0573848"; + _participantCsvRecord.Participant.ReasonForRemoval = reasonForRemoval; + var json = JsonSerializer.Serialize(_participantCsvRecord); + SetUpRequestBody(json); + // Act + var response = await _function.RunAsync(_request.Object); + + // Assert + string body = await AssertionHelper.ReadResponseBodyAsync(response); + Assert.AreEqual(HttpStatusCode.OK,response.StatusCode); + StringAssert.Contains(body, "98.ManualAddWithBlockingRFR.BSSelect.NonFatal"); + + } + + + #endregion [TestMethod] public async Task Run_ValidParticipantFile_ReturnNoContent() { From d844b137c8550b813d91c1f41f219ac696756762 Mon Sep 17 00:00:00 2001 From: Michael Clayson Date: Mon, 15 Dec 2025 11:31:53 +0000 Subject: [PATCH 2/9] fix rule to nullify rfr effective from date --- .../TransformDataService/transformRules.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json index 2478581100..681b3dee38 100644 --- a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json +++ b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json @@ -775,7 +775,7 @@ "isExpression": false }, { - "field": "ReasonForRemoval", + "field": "ReasonForRemovalEffectiveFromDate", "value": null, "isExpression": false } From cf8d9ba40395abc0e0427b3165977f4f292935c5 Mon Sep 17 00:00:00 2001 From: Michael Clayson Date: Mon, 15 Dec 2025 13:32:50 +0000 Subject: [PATCH 3/9] transform unit tests --- .../TransformDataServiceTests.cs | 74 ++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/tests/UnitTests/TransformDataServiceTests/TransformDataServiceTests/TransformDataServiceTests.cs b/tests/UnitTests/TransformDataServiceTests/TransformDataServiceTests/TransformDataServiceTests.cs index 029de8b689..c0d566b9fa 100644 --- a/tests/UnitTests/TransformDataServiceTests/TransformDataServiceTests/TransformDataServiceTests.cs +++ b/tests/UnitTests/TransformDataServiceTests/TransformDataServiceTests/TransformDataServiceTests.cs @@ -27,6 +27,7 @@ public class TransformDataServiceTests private readonly Mock _handleException = new(); private readonly Mock _transformLookups = new(); private readonly ITransformReasonForRemoval _transformReasonForRemoval; + private readonly IReasonForRemovalLookup _reasonForRemovalLookup; public TransformDataServiceTests() { @@ -55,16 +56,19 @@ public TransformDataServiceTests() { Participant = requestParticipant, ExistingParticipant = databaseParticipant, - ServiceProvider = "1" + ServiceProvider = "1", + FileName = "test.parquet" }; + _reasonForRemovalLookup = new ReasonForRemovalLookup(); + _transformLookups.Setup(x => x.ValidateOutcode(It.IsAny())).Returns(true); _transformLookups.Setup(x => x.GetBsoCode(It.IsAny())).Returns("ELD"); _transformLookups.Setup(x => x.GetBsoCodeUsingPCP(It.IsAny())).Returns("ELD"); _transformLookups.Setup(x => x.ValidateLanguageCode(It.IsAny())).Returns(true); _transformReasonForRemoval = new TransformReasonForRemoval(_handleException.Object, _transformLookups.Object); - _function = new TransformDataService(_createResponse.Object, _handleException.Object, _logger.Object, _transformReasonForRemoval, _transformLookups.Object); + _function = new TransformDataService(_createResponse.Object, _handleException.Object, _logger.Object, _transformReasonForRemoval, _transformLookups.Object, _reasonForRemovalLookup); _request.Setup(r => r.CreateResponse()).Returns(() => { @@ -901,6 +905,72 @@ public async Task Run_ZZZSECURPostcode_TransformPostcode() Assert.AreEqual("ZZ99 3VZ", actualResponse?.Postcode); } + [TestMethod] + [DataRow("CGA")] + [DataRow("DIS")] + [DataRow("EMB")] + [DataRow("NIT")] + [DataRow("OPA")] + [DataRow("ORR")] + [DataRow("RDI")] + [DataRow("RDR")] + [DataRow("RFI")] + [DataRow("SCT")] + public async Task Run_ManualAddRemovableRfR_RemovesRFR(string ReasonForRemoval) + { + // Arrange + _requestBody.FileName = "CS0848402"; + _requestBody.Participant.PrimaryCareProvider = "ZZZXYZ"; + _requestBody.Participant.ReasonForRemoval = ReasonForRemoval; + _requestBody.Participant.ReasonForRemovalEffectiveFromDate = DateTime.UtcNow.ToString(); + + var json = JsonSerializer.Serialize(_requestBody); + SetUpRequestBody(json); + + // Act + var result = await _function.RunAsync(_request.Object); + + // Assert + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + + string responseBody = await AssertionHelper.ReadResponseBodyAsync(result); + var actualResponse = JsonSerializer.Deserialize(responseBody); + + Assert.IsNull(actualResponse!.ReasonForRemoval); + Assert.IsNull(actualResponse!.ReasonForRemovalEffectiveFromDate); + } + [TestMethod] + [DataRow("AFL")] + [DataRow("AFN")] + [DataRow("DEA")] + [DataRow("LDN")] + [DataRow("SDL")] + [DataRow("SDN")] + [DataRow("TRA")] + public async Task Run_ManualAddNonRemovableRfR_HasRFR(string ReasonForRemoval) + { + // Arrange + _requestBody.FileName = "CS0848402"; + _requestBody.Participant.PrimaryCareProvider = "ZZZXYZ"; + _requestBody.Participant.ReasonForRemoval = ReasonForRemoval; + var timestamp = DateTime.UtcNow.ToString(); + _requestBody.Participant.ReasonForRemovalEffectiveFromDate = timestamp; + + var json = JsonSerializer.Serialize(_requestBody); + SetUpRequestBody(json); + + // Act + var result = await _function.RunAsync(_request.Object); + + // Assert + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + + string responseBody = await AssertionHelper.ReadResponseBodyAsync(result); + var actualResponse = JsonSerializer.Deserialize(responseBody); + + Assert.AreEqual(ReasonForRemoval,actualResponse!.ReasonForRemoval); + Assert.IsNotNull(actualResponse!.ReasonForRemovalEffectiveFromDate); + } private void SetUpRequestBody(string json) { From dc803c61530de25040de0f9c7f340eaaf6075a04 Mon Sep 17 00:00:00 2001 From: Michael Clayson Date: Tue, 16 Dec 2025 11:52:36 +0000 Subject: [PATCH 4/9] Update category and comment for rfr static rules --- .../StaticValidation/Breast_Screening_staticRules.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json index 6de869c374..f657849f65 100644 --- a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json +++ b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json @@ -179,19 +179,19 @@ "WorkflowName": "Manual_Add", "Rules": [ { - "RuleName": "97.NoGPorPCP.BSSelect.NonFatal", + "RuleName": "97.NoDummyGPorPCP.NBO.NonFatal", "Expression": "!string.IsNullOrEmpty(participant.PrimaryCareProvider)", "Actions": { "OnFailure": { "Name": "OutputExpression", "Context": { - "Expression": "\"Missing GP details (and no accepted reason for removal)\"" + "Expression": "\"Missing GP details\"" } } } }, { - "RuleName": "98.ManualAddWithBlockingRFR.BSSelect.NonFatal", + "RuleName": "98.ManualAddWithBlockingRFR.NBO.NonFatal", "Expression": "reasonForRemovalLkp.CanRemovalReasonBeOverridden(participant.ReasonForRemoval)", "Actions": { "OnFailure": { From 9324a8ab9cdeabdd50d84eaa8e2ff0693c43a96f Mon Sep 17 00:00:00 2001 From: Michael Clayson Date: Tue, 16 Dec 2025 15:20:44 +0000 Subject: [PATCH 5/9] Update Rules to include RDR check --- .../Breast_Screening_staticRules.json | 8 +++++- .../StaticValidation/StaticValidation.cs | 4 +-- .../StaticValidation/StaticValidationTests.cs | 25 +++++++++++++------ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json index f657849f65..97deb9386c 100644 --- a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json +++ b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json @@ -180,7 +180,13 @@ "Rules": [ { "RuleName": "97.NoDummyGPorPCP.NBO.NonFatal", - "Expression": "!string.IsNullOrEmpty(participant.PrimaryCareProvider)", + "LocalParams" : [ + { + "Name": "ValidReasonForRemoval", + "Expression": "string.IsNullOrEmpty(participant.ReasonForRemoval) || participant.ReasonForRemoval == \"RDR\" || participant.ReasonForRemoval == \"RPR\" || participant.ReasonForRemoval == \"RDI\" " + } + ], + "Expression": "!string.IsNullOrEmpty(participant.PrimaryCareProvider) || ValidReasonForRemoval", "Actions": { "OnFailure": { "Name": "OutputExpression", diff --git a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs index ffa0fef925..09a67807f6 100644 --- a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs +++ b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs @@ -72,8 +72,6 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano new RuleParameter("manualAdd",isManualAdd) }; - - var resultList = new List(); if (participantCsvRecord.Participant.RecordType != Actions.Removed) @@ -88,6 +86,8 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano if (isManualAdd) { + var validrfr = string.IsNullOrEmpty(participantCsvRecord.Participant.ReasonForRemoval) || new List{"RDR","RDI","RPR"}.Contains(participantCsvRecord.Participant.ReasonForRemoval); + var res = !(string.IsNullOrEmpty(participantCsvRecord.Participant.PrimaryCareProvider) && validrfr); var manualAddResults = await re.ExecuteAllRulesAsync("Manual_Add", ruleParameters); resultList.AddRange(manualAddResults); } diff --git a/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs b/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs index 136f27e558..470bf6e0ef 100644 --- a/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs +++ b/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs @@ -643,13 +643,19 @@ public async Task Run_CompatibleCurrentPostingAndPrimaryCareProvider_ReturnNoCon #endregion #region Manual add rule 97 Missing GP details (and no accepted reason for removal) [TestMethod] - [DataRow("A12345")] - [DataRow("ZZZZYZ")] - public async Task Run_ManualAddWithGPCode_ReturnsNoContent(string primaryCareProvider) + [DataRow("A12345",null)] + [DataRow("ZZZZYZ","RPR")] + [DataRow("ZZZZYZ","RDI")] + [DataRow("ZZZZYZ","RDR")] + [DataRow(null,"RDR")] + [DataRow(null,"RDI")] + [DataRow(null,"RPR")] + public async Task Run_ManualAddWithGPCode_ReturnsNoContent(string? primaryCareProvider,string? reasonForRemoval) { // Arrange _participantCsvRecord.Participant.CurrentPosting = "BAA"; _participantCsvRecord.Participant.PrimaryCareProvider = primaryCareProvider; + _participantCsvRecord.Participant.ReasonForRemoval = reasonForRemoval; _participantCsvRecord.Participant.RecordType = Actions.New; _participantCsvRecord.FileName = "CS0573848"; @@ -658,13 +664,17 @@ public async Task Run_ManualAddWithGPCode_ReturnsNoContent(string primaryCarePro // Act var response = await _function.RunAsync(_request.Object); - + string body = await AssertionHelper.ReadResponseBodyAsync(response); // Assert Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode); } [TestMethod] - public async Task Run_ManualAddWithoutGPCode_ReturnValidationException() + [DataRow("AFL")] + [DataRow("RDI")] + [DataRow("NIT")] + [DataRow(null)] + public async Task Run_ManualAddWithoutGPCode_ReturnValidationException(string? reasonForRemoval) { // Arrange _participantCsvRecord.Participant.CurrentPosting = "BAA"; @@ -681,7 +691,7 @@ public async Task Run_ManualAddWithoutGPCode_ReturnValidationException() // Assert string body = await AssertionHelper.ReadResponseBodyAsync(response); Assert.AreEqual(HttpStatusCode.OK,response.StatusCode); - StringAssert.Contains(body, "97.NoGPorPCP.BSSelect.NonFatal"); + StringAssert.Contains(body, "97.NoDummyGPorPCP.NBO.NonFatal"); } @@ -700,6 +710,7 @@ public async Task Run_ManualAddWithOverridableReasonForRemoval_ReturnsNoContent( _participantCsvRecord.Participant.CurrentPosting = "BAA"; _participantCsvRecord.Participant.RecordType = Actions.New; _participantCsvRecord.FileName = "CS0573848"; + _participantCsvRecord.Participant.PrimaryCareProvider = "ZZZXYZ"; _participantCsvRecord.Participant.ReasonForRemoval = reasonForRemoval; var json = JsonSerializer.Serialize(_participantCsvRecord); SetUpRequestBody(json); @@ -735,7 +746,7 @@ public async Task Run_ManualAddWithoutOverridableReasonForRemoval_ReturnsValidat // Assert string body = await AssertionHelper.ReadResponseBodyAsync(response); Assert.AreEqual(HttpStatusCode.OK,response.StatusCode); - StringAssert.Contains(body, "98.ManualAddWithBlockingRFR.BSSelect.NonFatal"); + StringAssert.Contains(body, "98.ManualAddWithBlockingRFR.NBO.NonFatal"); } From e83b2859f2147da09fe596469a25a0ccc6ba48bc Mon Sep 17 00:00:00 2001 From: Michael Clayson Date: Tue, 16 Dec 2025 15:37:06 +0000 Subject: [PATCH 6/9] Correcting Logic --- .../StaticValidation/Breast_Screening_staticRules.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json index 97deb9386c..94752cc707 100644 --- a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json +++ b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/Breast_Screening_staticRules.json @@ -183,10 +183,10 @@ "LocalParams" : [ { "Name": "ValidReasonForRemoval", - "Expression": "string.IsNullOrEmpty(participant.ReasonForRemoval) || participant.ReasonForRemoval == \"RDR\" || participant.ReasonForRemoval == \"RPR\" || participant.ReasonForRemoval == \"RDI\" " + "Expression": "participant.ReasonForRemoval == \"RDR\" || participant.ReasonForRemoval == \"RPR\" || participant.ReasonForRemoval == \"RDI\" " } ], - "Expression": "!string.IsNullOrEmpty(participant.PrimaryCareProvider) || ValidReasonForRemoval", + "Expression": "!string.IsNullOrEmpty(participant.PrimaryCareProvider) || (string.IsNullOrEmpty(participant.PrimaryCareProvider) && ValidReasonForRemoval)", "Actions": { "OnFailure": { "Name": "OutputExpression", From 44ba147de1fc97f303e0132b8bed24d85f8f3e39 Mon Sep 17 00:00:00 2001 From: Michael Clayson Date: Tue, 16 Dec 2025 15:46:53 +0000 Subject: [PATCH 7/9] Address Comments --- .../TransformDataService.cs | 19 +------------------ .../StaticValidation/StaticValidation.cs | 15 +-------------- .../ReasonForRemovalLookup.cs | 2 -- .../Shared/Common/ValidationHelper.cs | 17 +++++++++++++++++ .../TransformDataServiceTests.cs | 1 + 5 files changed, 20 insertions(+), 34 deletions(-) diff --git a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs index eb06845d24..589fc3c3f9 100644 --- a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs +++ b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs @@ -80,7 +80,7 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano participant = await transformString.TransformStringFields(participant); // Other transformation rules - participant = await TransformParticipantAsync(participant, requestBody.ExistingParticipant,CheckManualAddFileName(requestBody.FileName)); + participant = await TransformParticipantAsync(participant, requestBody.ExistingParticipant,ValidationHelper.CheckManualAddFileName(requestBody.FileName)); // Name prefix transformation if (participant.NamePrefix != null) @@ -89,8 +89,6 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano participant = await _transformReasonForRemoval.ReasonForRemovalTransformations(participant, requestBody.ExistingParticipant); - - if (participant.NhsNumber == null) { return _createResponse.CreateHttpResponse(HttpStatusCode.Accepted, req, ""); @@ -100,8 +98,6 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano var response = JsonSerializer.Serialize(participant); return _createResponse.CreateHttpResponse(HttpStatusCode.OK, req, response); - - } catch (ArgumentException ex) { @@ -232,17 +228,4 @@ private async Task CreateTransformExecutedExceptions(List except await _exceptionHandler.CreateTransformExecutedExceptions(participant, ruleName, ruleId); } } - private static bool CheckManualAddFileName(string? FileName) - { - if(string.IsNullOrEmpty(FileName)) - { - return false; - } - if (FileName.ToLower().EndsWith(".parquet")) - { - return false; - } - return true; - } - } diff --git a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs index 09a67807f6..d48d865646 100644 --- a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs +++ b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs @@ -53,7 +53,7 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano _logger.LogInformation("ruleFileName: {RuleFileName}", ruleFileName); bool routineParticipant = (participantCsvRecord.Participant.ReferralFlag ?? "").ToLower() == "false"; - bool isManualAdd = CheckManualAddFileName(participantCsvRecord.FileName); + bool isManualAdd = ValidationHelper.CheckManualAddFileName(participantCsvRecord.FileName); var json = await _readRules.GetRulesFromDirectory(ruleFileName); var rules = JsonSerializer.Deserialize(json); @@ -115,17 +115,4 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req); } } - - private static bool CheckManualAddFileName(string FileName) - { - if(string.IsNullOrEmpty(FileName)) - { - return false; - } - if (FileName.ToLower().EndsWith(".parquet")) - { - return false; - } - return true; - } } diff --git a/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/ReasonForRemovalLookup.cs b/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/ReasonForRemovalLookup.cs index f453867bdb..9911e35dce 100644 --- a/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/ReasonForRemovalLookup.cs +++ b/application/CohortManager/src/Functions/Shared/Common/ReasonForRemovalLookup/ReasonForRemovalLookup.cs @@ -4,7 +4,6 @@ public class ReasonForRemovalLookup : IReasonForRemovalLookup { private readonly List NonOverridableRFRs; - private readonly List OverridableRFRs; public ReasonForRemovalLookup() { NonOverridableRFRs = new List @@ -29,7 +28,6 @@ public bool CanRemovalReasonBeOverridden(string? reasonForRemoval) { return false; } - return true; } diff --git a/application/CohortManager/src/Functions/Shared/Common/ValidationHelper.cs b/application/CohortManager/src/Functions/Shared/Common/ValidationHelper.cs index 6c2818ba64..2ddb5168eb 100644 --- a/application/CohortManager/src/Functions/Shared/Common/ValidationHelper.cs +++ b/application/CohortManager/src/Functions/Shared/Common/ValidationHelper.cs @@ -125,6 +125,23 @@ public static bool ValidatePostcode(string postcode) string outcode = match.Groups[1].Value; return outcode.ToUpper(); } + /// + /// Checks that a file name is a parquet file to see if a Routine or a manual add + /// + /// + /// true is its a manualAdd + public static bool CheckManualAddFileName(string? FileName) + { + if(string.IsNullOrEmpty(FileName)) + { + return false; + } + if (FileName.ToLower().EndsWith(".parquet")) + { + return false; + } + return true; + } private static bool ParseInt32(char value, out int integerValue) { diff --git a/tests/UnitTests/TransformDataServiceTests/TransformDataServiceTests/TransformDataServiceTests.cs b/tests/UnitTests/TransformDataServiceTests/TransformDataServiceTests/TransformDataServiceTests.cs index d6f4a6450b..ecea1d9a44 100644 --- a/tests/UnitTests/TransformDataServiceTests/TransformDataServiceTests/TransformDataServiceTests.cs +++ b/tests/UnitTests/TransformDataServiceTests/TransformDataServiceTests/TransformDataServiceTests.cs @@ -956,6 +956,7 @@ public async Task Run_ZZZSECURPostcode_TransformPostcode() [DataRow("ORR")] [DataRow("RDI")] [DataRow("RDR")] + [DataRow("RPR")] [DataRow("RFI")] [DataRow("SCT")] public async Task Run_ManualAddRemovableRfR_RemovesRFR(string ReasonForRemoval) From 7a87837aa857a48617792ec98ce92d1d16b90a80 Mon Sep 17 00:00:00 2001 From: Michael Clayson Date: Tue, 16 Dec 2025 16:00:10 +0000 Subject: [PATCH 8/9] remove unused code and sonarqube --- .../TransformDataService/TransformDataService.cs | 4 ++-- .../StaticValidation/StaticValidation.cs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs index 589fc3c3f9..db5d0039c6 100644 --- a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs +++ b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/TransformDataService.cs @@ -102,7 +102,7 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano catch (ArgumentException ex) { _logger.LogWarning(ex, "An error occurred during transformation"); - await _exceptionHandler.CreateSystemExceptionLogFromNhsNumber(ex, participant.NhsNumber, requestBody.FileName!, participant.ScreeningName, JsonSerializer.Serialize(participant)); + await _exceptionHandler.CreateSystemExceptionLogFromNhsNumber(ex, participant.NhsNumber, requestBody.FileName!, participant.ScreeningName!, JsonSerializer.Serialize(participant)); return _createResponse.CreateHttpResponse(HttpStatusCode.Accepted, req); } catch (TransformationException ex) @@ -112,7 +112,7 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano } catch (Exception ex) { - await _exceptionHandler.CreateSystemExceptionLogFromNhsNumber(ex, participant.NhsNumber, requestBody.FileName!, participant.ScreeningName, JsonSerializer.Serialize(participant)); + await _exceptionHandler.CreateSystemExceptionLogFromNhsNumber(ex, participant.NhsNumber, requestBody.FileName!, participant.ScreeningName!, JsonSerializer.Serialize(participant)); _logger.LogWarning(ex, "exception occurred while running transform data service"); return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req); } diff --git a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs index d48d865646..03ce19b294 100644 --- a/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs +++ b/application/CohortManager/src/Functions/ScreeningValidationService/StaticValidation/StaticValidation.cs @@ -86,8 +86,6 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano if (isManualAdd) { - var validrfr = string.IsNullOrEmpty(participantCsvRecord.Participant.ReasonForRemoval) || new List{"RDR","RDI","RPR"}.Contains(participantCsvRecord.Participant.ReasonForRemoval); - var res = !(string.IsNullOrEmpty(participantCsvRecord.Participant.PrimaryCareProvider) && validrfr); var manualAddResults = await re.ExecuteAllRulesAsync("Manual_Add", ruleParameters); resultList.AddRange(manualAddResults); } From 440f2f184591fd560e0f0f6c8bfa611ec707ea04 Mon Sep 17 00:00:00 2001 From: Michael Clayson Date: Wed, 17 Dec 2025 12:48:45 +0000 Subject: [PATCH 9/9] Add pds mock data for e2e tests --- .../TransformDataService/transformRules.json | 2 +- .../complete-patient-9994170503.json | 367 ++++++++++++++++++ .../complete-patient-9996666921.json | 367 ++++++++++++++++++ .../StaticValidation/StaticValidationTests.cs | 3 + 4 files changed, 738 insertions(+), 1 deletion(-) create mode 100644 application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9994170503.json create mode 100644 application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9996666921.json diff --git a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json index 2a6154d5a9..a4aea48dbb 100644 --- a/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json +++ b/application/CohortManager/src/Functions/CohortDistributionServices/TransformDataService/transformRules.json @@ -767,7 +767,7 @@ "Expression": "!string.IsNullOrEmpty(participant.PrimaryCareProvider)" } ], - "Expression": "CanTransformRfr && HasPrimaryCareProvider", + "Expression": "!string.IsNullOrEmpty(participant.ReasonForRemoval) && CanTransformRfr && HasPrimaryCareProvider", "Actions": { "OnSuccess": { "Name": "TransformAction", diff --git a/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9994170503.json b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9994170503.json new file mode 100644 index 0000000000..18b3c5c7ba --- /dev/null +++ b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9994170503.json @@ -0,0 +1,367 @@ +{ + "resourceType": "Patient", + "id": "9994170503", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9994170503", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatus", + "version": "1.0.0", + "code": "01", + "display": "Number present and verified" + } + ] + } + } + ] + } + ], + "meta": { + "versionId": "2", + "security": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality", + "code": "U", + "display": "unrestricted" + } + ] + }, + "name": [ + { + "id": "123", + "use": "usual", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "given": [ + "Jane" + ], + "family": "Smith", + "prefix": [ + "Mrs" + ] + } + ], + "gender": "female", + "birthDate": "1980-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + } + ], + "managingOrganization": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + }, + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NominatedPharmacy", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y23456" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y34567" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSCommunication", + "extension": [ + { + "url": "language", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-HumanLanguage", + "version": "1.0.0", + "code": "fr", + "display": "French" + } + ] + } + }, + { + "url": "interpreterRequired", + "valueBoolean": true + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-ContactPreference", + "extension": [ + { + "url": "PreferredWrittenCommunicationFormat", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredWrittenCommunicationFormat", + "code": "12", + "display": "Braille" + } + ] + } + }, + { + "url": "PreferredContactMethod", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredContactMethod", + "code": "1", + "display": "Letter" + } + ] + } + }, + { + "url": "PreferredContactTimes", + "valueString": "Not after 7pm" + } + ] + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", + "valueAddress": { + "city": "Manchester", + "district": "Greater Manchester", + "country": "GBR" + } + }, + { + "url": "https://fhir.nhs.uk/StructureDefinition/Extension-PDS-RemovalFromRegistration", + "extension": [ + { + "url": "removalFromRegistrationCode", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/PDS-RemovalReasonExitCode", + "code": "RDI", + "display": "Practice request immediate removal" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2021-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "phone", + "value": "01632960587", + "use": "home" + }, + { + "id": "790", + "period": { + "start": "2019-01-01", + "end": "2022-12-31" + }, + "system": "email", + "value": "jane.smith@example.com", + "use": "home" + }, + { + "id": "OC789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "other", + "value": "01632960587", + "use": "home", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-OtherContactSystem", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-OtherContactSystem", + "code": "textphone", + "display": "Minicom (Textphone)" + } + } + ] + } + ], + "contact": [ + { + "id": "C123", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "relationship": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0131", + "code": "C", + "display": "Emergency Contact" + } + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "01632960587" + } + ] + } + ], + "address": [ + { + "id": "456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "home", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + }, + { + "id": "T456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "temp", + "text": "Student Accommodation", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + } + ] +} diff --git a/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9996666921.json b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9996666921.json new file mode 100644 index 0000000000..f6cce54177 --- /dev/null +++ b/application/CohortManager/src/Functions/DemographicServices/RetrievePDSDemographic/MockedPDSData/complete-patient-9996666921.json @@ -0,0 +1,367 @@ +{ + "resourceType": "Patient", + "id": "9996666921", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9996666921", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatus", + "version": "1.0.0", + "code": "01", + "display": "Number present and verified" + } + ] + } + } + ] + } + ], + "meta": { + "versionId": "2", + "security": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality", + "code": "U", + "display": "unrestricted" + } + ] + }, + "name": [ + { + "id": "123", + "use": "usual", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "given": [ + "Jane" + ], + "family": "Smith", + "prefix": [ + "Mrs" + ] + } + ], + "gender": "female", + "birthDate": "1980-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + } + ], + "managingOrganization": { + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + } + } + }, + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NominatedPharmacy", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y23456" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y34567" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSCommunication", + "extension": [ + { + "url": "language", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-HumanLanguage", + "version": "1.0.0", + "code": "fr", + "display": "French" + } + ] + } + }, + { + "url": "interpreterRequired", + "valueBoolean": true + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-ContactPreference", + "extension": [ + { + "url": "PreferredWrittenCommunicationFormat", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredWrittenCommunicationFormat", + "code": "12", + "display": "Braille" + } + ] + } + }, + { + "url": "PreferredContactMethod", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-PreferredContactMethod", + "code": "1", + "display": "Letter" + } + ] + } + }, + { + "url": "PreferredContactTimes", + "valueString": "Not after 7pm" + } + ] + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", + "valueAddress": { + "city": "Manchester", + "district": "Greater Manchester", + "country": "GBR" + } + }, + { + "url": "https://fhir.nhs.uk/StructureDefinition/Extension-PDS-RemovalFromRegistration", + "extension": [ + { + "url": "removalFromRegistrationCode", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/PDS-RemovalReasonExitCode", + "code": "CGA", + "display": "Gone away – address not known/FP69" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2021-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "phone", + "value": "01632960587", + "use": "home" + }, + { + "id": "790", + "period": { + "start": "2019-01-01", + "end": "2022-12-31" + }, + "system": "email", + "value": "jane.smith@example.com", + "use": "home" + }, + { + "id": "OC789", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "system": "other", + "value": "01632960587", + "use": "home", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-OtherContactSystem", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-OtherContactSystem", + "code": "textphone", + "display": "Minicom (Textphone)" + } + } + ] + } + ], + "contact": [ + { + "id": "C123", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "relationship": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0131", + "code": "C", + "display": "Emergency Contact" + } + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "01632960587" + } + ] + } + ], + "address": [ + { + "id": "456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "home", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + }, + { + "id": "T456", + "period": { + "start": "2020-01-01", + "end": "2021-12-31" + }, + "use": "temp", + "text": "Student Accommodation", + "line": [ + "1 Trevelyan Square", + "Boar Lane", + "City Centre", + "Leeds", + "West Yorkshire" + ], + "postalCode": "LS1 6AE", + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "PAF" + } + }, + { + "url": "value", + "valueString": "12345678" + } + ] + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey", + "extension": [ + { + "url": "type", + "valueCoding": { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType", + "code": "UPRN" + } + }, + { + "url": "value", + "valueString": "123456789012" + } + ] + } + ] + } + ] +} diff --git a/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs b/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs index 470bf6e0ef..b0a974a82b 100644 --- a/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs +++ b/tests/UnitTests/ScreeningValidationServiceTests/StaticValidation/StaticValidationTests.cs @@ -704,6 +704,9 @@ public async Task Run_ManualAddWithoutGPCode_ReturnValidationException(string? r [DataRow("EMB")] [DataRow("SCT")] [DataRow("OPA")] + [DataRow("RDR")] + [DataRow("RPR")] + [DataRow("RPI")] public async Task Run_ManualAddWithOverridableReasonForRemoval_ReturnsNoContent(string? reasonForRemoval) { // Arrange