From a7358047bf35ec3bfddb194c3b9a1ba25ae10be0 Mon Sep 17 00:00:00 2001 From: Joe Fong <127404525+joefong-nhs@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:28:36 +0100 Subject: [PATCH] PRMP-571 - Lloyd George Ingestion - PDS Matching Updates Issues (#390) * [PRMP-571] Add unicode utils method to check string starts_with and end_withs * [PRMP-571] Add helper to assert test not raising expection * [PRMP-571] Add test cases to reflect changes in PDS name match requirement * [PRMP-571] Adding unit tests, replace try except catch block with `expect_not_to_raise` * [PRMP-571] Amend implementation according to new requirements * [PRMP-571] Add mock patients to test bulk upload with new requirements * fix mistake in mock data (patient number not updated) * [PRMP-571] Update name comparison logic at frontend * edit mock patient to allow test with frontend * fix mock patient 103 missing gp ods code * set conftest.py to be exclude from coverage * [PRMP-571] Add logic to select the most recent name for patient * improve test coverage --- .../test/testDataForPdsNameValidation.ts | 92 +++++ .../utils/uploadDocumentValidation.test.ts | 135 ++++++ .../helpers/utils/uploadDocumentValidation.ts | 23 +- lambdas/models/pds_models.py | 38 +- ...M85143_gp_family_name_with_whitespace.json | 386 ++++++++++++++++++ ...103_M85143_gp_family_name_with_hyphen.json | 386 ++++++++++++++++++ ..._M85143_gp_given_name_with_whitespace.json | 386 ++++++++++++++++++ ..._given_name_with_two_separate_strings.json | 386 ++++++++++++++++++ ...t_9000000106_M85143_gp_with_temp_name.json | 386 ++++++++++++++++++ lambdas/tests/unit/conftest.py | 10 + .../data/pds/test_cases_for_date_logic.py | 35 ++ .../test_cases_for_patient_name_matching.py | 81 ++++ lambdas/tests/unit/models/test_pds_models.py | 136 ++++++ .../unit/utils/test_lloyd_george_validator.py | 177 ++++---- .../tests/unit/utils/test_unicode_utils.py | 39 ++ lambdas/utils/lloyd_george_validator.py | 31 +- lambdas/utils/unicode_utils.py | 14 + sonar-project.properties | 2 +- 18 files changed, 2636 insertions(+), 107 deletions(-) create mode 100644 app/src/helpers/test/testDataForPdsNameValidation.ts create mode 100644 app/src/helpers/utils/uploadDocumentValidation.test.ts create mode 100644 lambdas/services/mock_data/pds_patient_9000000102_M85143_gp_family_name_with_whitespace.json create mode 100644 lambdas/services/mock_data/pds_patient_9000000103_M85143_gp_family_name_with_hyphen.json create mode 100644 lambdas/services/mock_data/pds_patient_9000000104_M85143_gp_given_name_with_whitespace.json create mode 100644 lambdas/services/mock_data/pds_patient_9000000105_M85143_gp_given_name_with_two_separate_strings.json create mode 100644 lambdas/services/mock_data/pds_patient_9000000106_M85143_gp_with_temp_name.json create mode 100644 lambdas/tests/unit/helpers/data/pds/test_cases_for_date_logic.py create mode 100644 lambdas/tests/unit/helpers/data/pds/test_cases_for_patient_name_matching.py diff --git a/app/src/helpers/test/testDataForPdsNameValidation.ts b/app/src/helpers/test/testDataForPdsNameValidation.ts new file mode 100644 index 000000000..4c9d62835 --- /dev/null +++ b/app/src/helpers/test/testDataForPdsNameValidation.ts @@ -0,0 +1,92 @@ +import { PatientDetails } from '../../types/generic/patientDetails'; +import { buildPatientDetails } from './testBuilders'; +import { + DOCUMENT_TYPE, + DOCUMENT_UPLOAD_STATE as documentUploadStates, + UploadDocument, +} from '../../types/pages/UploadDocumentsPage/types'; +import { v4 as uuidv4 } from 'uuid'; + +type PdsNameMatchingTestCase = { + patientDetails: PatientDetails; + patientNameInFileName: string; + shouldAcceptName: boolean; +}; + +type TestCaseJsonFormat = { + pds_name: { + family: string; + given: string[]; + }; + accept: string[]; + reject: string[]; +}; + +export const TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME = { + pds_name: { family: 'Smith Anderson', given: ['Jane', 'Bob'] }, + accept: ['Jane Bob Smith Anderson', 'Jane Smith Anderson', 'Jane B Smith Anderson'], + reject: ['Bob Smith Anderson', 'Jane Smith', 'Jane Anderson', 'Jane Anderson Smith'], +}; + +export const TEST_CASES_FOR_FAMILY_NAME_WITH_HYPHEN = { + pds_name: { family: 'Smith-Anderson', given: ['Jane'] }, + accept: ['Jane Smith-Anderson'], + reject: ['Jane Smith Anderson', 'Jane Smith', 'Jane Anderson'], +}; + +export const TEST_CASES_FOR_TWO_WORDS_GIVEN_NAME = { + pds_name: { family: 'Smith', given: ['Jane Bob'] }, + accept: ['Jane Bob Smith'], + reject: ['Jane Smith', 'Jane B Smith', 'Jane-Bob Smith', 'Bob Smith'], +}; + +export const TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME_AND_GIVEN_NAME = { + pds_name: { family: 'Smith Anderson', given: ['Jane Bob'] }, + accept: ['Jane Bob Smith Anderson'], + reject: [ + 'Jane Smith Anderson', + 'Bob Smith Anderson', + 'Jane B Smith Anderson', + 'Jane Bob Smith', + 'Jane Bob Anderson', + ], +}; + +export function loadTestCases(testCaseJson: TestCaseJsonFormat): Array { + const patientDetails = buildPatientDetails({ + givenName: testCaseJson['pds_name']['given'], + familyName: testCaseJson['pds_name']['family'], + }); + + const testCasesForAccept = testCaseJson['accept'].map((patientNameInFileName) => ({ + patientDetails, + patientNameInFileName, + shouldAcceptName: true, + })); + + const testCasesForReject = testCaseJson['reject'].map((patientNameInFileName) => ({ + patientDetails, + patientNameInFileName, + shouldAcceptName: false, + })); + + return [...testCasesForAccept, ...testCasesForReject]; +} + +export function buildLGUploadDocsFromFilenames(filenames: string[]): UploadDocument[] { + const fileObjects = filenames.map( + (filename) => + new File(['test'], filename, { + type: 'application/pdf', + }), + ); + + return fileObjects.map((file) => ({ + file, + state: documentUploadStates.SELECTED, + progress: 0, + id: uuidv4(), + docType: DOCUMENT_TYPE.LLOYD_GEORGE, + attempts: 0, + })); +} diff --git a/app/src/helpers/utils/uploadDocumentValidation.test.ts b/app/src/helpers/utils/uploadDocumentValidation.test.ts new file mode 100644 index 000000000..1f44d406d --- /dev/null +++ b/app/src/helpers/utils/uploadDocumentValidation.test.ts @@ -0,0 +1,135 @@ +import { patientNameMatchesPds, uploadDocumentValidation } from './uploadDocumentValidation'; +import { buildPatientDetails } from '../test/testBuilders'; +import { + buildLGUploadDocsFromFilenames, + loadTestCases, + TEST_CASES_FOR_FAMILY_NAME_WITH_HYPHEN, + TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME, + TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME_AND_GIVEN_NAME, + TEST_CASES_FOR_TWO_WORDS_GIVEN_NAME, +} from '../test/testDataForPdsNameValidation'; +import { UploadFilesErrors } from '../../types/pages/UploadDocumentsPage/types'; + +describe('uploadDocumentValidation', () => { + describe('name validation', () => { + it('can handle a patient name with multiple words and special chars', () => { + const testPatient = buildPatientDetails({ + givenName: ['Jane François', 'Bob'], // NFC + familyName: "O'Brian-Jones Anderson", + nhsNumber: '9000000009', + birthDate: '2011-09-19', + }); + const patientNameInFile = "Jane François Bob O'Brian-Jones Anderson"; // NFD; + const testFileName = `1of1_Lloyd_George_Record_[${patientNameInFile}]_[9000000009]_[19-09-2011].pdf`; + const testUploadDocuments = buildLGUploadDocsFromFilenames([testFileName]); + + const expected: UploadFilesErrors[] = []; + const actual = uploadDocumentValidation(testUploadDocuments, testPatient); + + expect(actual).toEqual(expected); + }); + }); +}); + +describe('patientNameMatchesPds', () => { + it('returns true when the name in pds match patientNameInFileName', () => { + const patientDetails = buildPatientDetails({ givenName: ['Jane'], familyName: 'Smith' }); + const patientNameInFileName = 'Jane Smith'; + const expected: boolean = true; + + const actual: boolean = patientNameMatchesPds(patientNameInFileName, patientDetails); + expect(actual).toBe(expected); + }); + + it('returns false when first name not match', () => { + const patientDetails = buildPatientDetails({ givenName: ['Jane'], familyName: 'Smith' }); + const patientNameInFileName = 'Bob Smith'; + const expected: boolean = false; + + const actual: boolean = patientNameMatchesPds(patientNameInFileName, patientDetails); + expect(actual).toBe(expected); + }); + + it('returns false when last name not match', () => { + const patientDetails = buildPatientDetails({ givenName: ['Jane'], familyName: 'Smith' }); + const patientNameInFileName = 'Jane Anderson'; + const expected: boolean = false; + + const actual: boolean = patientNameMatchesPds(patientNameInFileName, patientDetails); + expect(actual).toBe(expected); + }); + + it('should be case insensitive when comparing names', () => { + const patientDetails = buildPatientDetails({ givenName: ['jane'], familyName: 'SMITH' }); + const patientNameInFileName = 'Jane Smith'; + const expected: boolean = true; + + const actual: boolean = patientNameMatchesPds(patientNameInFileName, patientDetails); + expect(actual).toBe(expected); + }); + + it('should be able to compare names with accent chars', () => { + const patientDetails = buildPatientDetails( + { givenName: ['Jàne'], familyName: 'Smïth' }, // NFD + ); + const patientNameInFileName = 'Jàne Smïth'; // NFC + const expected: boolean = true; + + const actual: boolean = patientNameMatchesPds(patientNameInFileName, patientDetails); + expect(actual).toBe(expected); + }); + + describe('Names with multiple words joined together', () => { + it.each(loadTestCases(TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME))( + 'from pds: ["Jane", "Bob"] Smith Anderson, filename: $patientNameInFileName, $shouldAcceptName', + ({ patientDetails, patientNameInFileName, shouldAcceptName }) => { + const actual: boolean = patientNameMatchesPds( + patientNameInFileName, + patientDetails, + ); + const expected: boolean = shouldAcceptName; + + expect(actual).toBe(expected); + }, + ); + + it.each(loadTestCases(TEST_CASES_FOR_FAMILY_NAME_WITH_HYPHEN))( + 'from pds: ["Jane"] Smith-Anderson, filename: $patientNameInFileName, $shouldAcceptName', + ({ patientDetails, patientNameInFileName, shouldAcceptName }) => { + const actual: boolean = patientNameMatchesPds( + patientNameInFileName, + patientDetails, + ); + const expected: boolean = shouldAcceptName; + + expect(actual).toBe(expected); + }, + ); + + it.each(loadTestCases(TEST_CASES_FOR_TWO_WORDS_GIVEN_NAME))( + 'from pds: ["Jane Bob"] Smith, filename: $patientNameInFileName, $shouldAcceptName', + ({ patientDetails, patientNameInFileName, shouldAcceptName }) => { + const actual: boolean = patientNameMatchesPds( + patientNameInFileName, + patientDetails, + ); + const expected: boolean = shouldAcceptName; + + expect(actual).toBe(expected); + }, + ); + + it.each(loadTestCases(TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME_AND_GIVEN_NAME))( + 'from pds: ["Jane Bob"] Smith Anderson, filename: $patientNameInFileName, $shouldAcceptName', + ({ patientDetails, patientNameInFileName, shouldAcceptName }) => { + const actual: boolean = patientNameMatchesPds( + patientNameInFileName, + patientDetails, + ); + const expected: boolean = shouldAcceptName; + + expect(actual).toBe(expected); + }, + ); + }); +}); diff --git a/app/src/helpers/utils/uploadDocumentValidation.ts b/app/src/helpers/utils/uploadDocumentValidation.ts index 42176205d..db5b18bff 100644 --- a/app/src/helpers/utils/uploadDocumentValidation.ts +++ b/app/src/helpers/utils/uploadDocumentValidation.ts @@ -8,7 +8,7 @@ for (let i = 0x300; i < 0x371; i++) { REGEX_ACCENT_MARKS_IN_NFD += String.fromCharCode(i); } const REGEX_ACCENT_CHARS_IN_NFC = 'À-ž'; -const REGEX_PATIENT_NAME_PATTERN = `[A-Za-z ${REGEX_ACCENT_CHARS_IN_NFC}${REGEX_ACCENT_MARKS_IN_NFD}]+`; +const REGEX_PATIENT_NAME_PATTERN = `[A-Za-z ${REGEX_ACCENT_CHARS_IN_NFC}${REGEX_ACCENT_MARKS_IN_NFD}'-]+`; const REGEX_NHS_NUMBER_REGEX = '[0-9]{10}'; const REGEX_LLOYD_GEORGE_FILENAME = new RegExp( `^[0-9]+of[0-9]+_Lloyd_George_Record_\\[(?${REGEX_PATIENT_NAME_PATTERN})]_\\[(?${REGEX_NHS_NUMBER_REGEX})]_\\[(?\\d\\d-\\d\\d-\\d\\d\\d\\d)].pdf$`, @@ -104,7 +104,6 @@ const validateWithPatientDetails = ( ): UploadFilesErrors[] => { const dateOfBirth = new Date(patientDetails.birthDate); const dateOfBirthString = moment(dateOfBirth).format('DD-MM-YYYY'); - const patientNameFromPds = [...patientDetails.givenName, patientDetails.familyName].join(' '); const nhsNumber = patientDetails.nhsNumber; const errors: UploadFilesErrors[] = []; @@ -120,16 +119,24 @@ const validateWithPatientDetails = ( } const patientNameInFilename = match?.groups?.patient_name as string; - if (!patientNameMatches(patientNameInFilename, patientNameFromPds)) { + if (!patientNameMatchesPds(patientNameInFilename, patientDetails)) { errors.push({ filename, error: fileUploadErrorMessages.patientNameError }); } return errors; }; -const patientNameMatches = (patientNameInFileName: string, patientNameFromPds: string): boolean => { - return ( - patientNameInFileName.normalize('NFD').toLowerCase() === - patientNameFromPds.normalize('NFD').toLowerCase() - ); +export const patientNameMatchesPds = ( + patientNameInFileName: string, + patientDetailsFromPds: PatientDetails, +): boolean => { + const patientNameInFileNameNormalised = patientNameInFileName.normalize('NFD').toLowerCase(); + + const firstName = patientDetailsFromPds.givenName[0].normalize('NFD').toLowerCase(); + const firstNameMatches = patientNameInFileNameNormalised.startsWith(firstName); + + const familyName = patientDetailsFromPds.familyName.normalize('NFD').toLowerCase(); + const familyNameMarches = patientNameInFileNameNormalised.endsWith(familyName); + + return firstNameMatches && familyNameMarches; }; diff --git a/lambdas/models/pds_models.py b/lambdas/models/pds_models.py index 1f08bc493..2bd4999dd 100644 --- a/lambdas/models/pds_models.py +++ b/lambdas/models/pds_models.py @@ -28,6 +28,19 @@ class Name(BaseModel): given: list[str] = [""] family: str + def is_currently_in_use(self) -> bool: + if not self.period: + return False + if self.use.lower() in ["nickname", "old"]: + return False + + today = date.today() + + name_started_already = self.period.start <= today + name_not_expired_yet = (not self.period.end) or self.period.end >= today + + return name_started_already and name_not_expired_yet + class Security(BaseModel): code: str @@ -86,18 +99,31 @@ def is_unrestricted(self) -> bool: security = self.get_security() return security.code == "U" - def get_current_usual_name(self) -> Optional[Name]: + def get_usual_name(self) -> Optional[Name]: for entry in self.name: if entry.use.lower() == "usual": return entry + def get_most_recent_name(self) -> Optional[Name]: + active_names = [name for name in self.name if name.is_currently_in_use()] + if not active_names: + return None + + sorted_by_start_date_desc = sorted( + active_names, key=lambda name: name.period.start, reverse=True + ) + return sorted_by_start_date_desc[0] + def get_current_family_name_and_given_name(self) -> Tuple[str, list[str]]: - usual_name = self.get_current_usual_name() - if not usual_name: - logger.warning("The patient does not have a usual name.") + current_name = self.get_most_recent_name() or self.get_usual_name() + if not current_name: + logger.warning( + "The patient does not have a currently active name or a usual name." + ) return "", [""] - given_name = usual_name.given - family_name = usual_name.family + + given_name = current_name.given + family_name = current_name.family if not given_name or given_name == [""]: logger.warning("The given name of patient is empty.") diff --git a/lambdas/services/mock_data/pds_patient_9000000102_M85143_gp_family_name_with_whitespace.json b/lambdas/services/mock_data/pds_patient_9000000102_M85143_gp_family_name_with_whitespace.json new file mode 100644 index 000000000..5320002c4 --- /dev/null +++ b/lambdas/services/mock_data/pds_patient_9000000102_M85143_gp_family_name_with_whitespace.json @@ -0,0 +1,386 @@ +{ + "resourceType": "Patient", + "id": "9000000102", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000102", + "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": "2025-12-31" + }, + "given": ["Jane"], + "family": "Smith Anderson", + "prefix": ["Mrs"], + "suffix": ["MBE"] + }, + { + "id": "1234", + "use": "other", + "period": { + "start": "2020-01-01", + "end": "2025-12-31" + }, + "family": "Stevens", + "prefix": ["Mr"], + "suffix": ["MBE"] + } + ], + "gender": "female", + "birthDate": "2010-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "M85143", + "period": { + "start": "2020-01-01", + "end": "2025-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": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-DeathNotificationStatus", + "extension": [ + { + "url": "deathNotificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-DeathNotificationStatus", + "version": "1.0.0", + "code": "2", + "display": "Formal - death notice received from Registrar of Deaths" + } + ] + } + }, + { + "url": "systemEffectiveDate", + "valueDateTime": "2010-10-22T00:00:00+00:00" + } + ] + }, + { + "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": "SCT", + "display": "Transferred to Scotland" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2025-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2025-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": "2025-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": "2025-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": "2025-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": "2025-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/lambdas/services/mock_data/pds_patient_9000000103_M85143_gp_family_name_with_hyphen.json b/lambdas/services/mock_data/pds_patient_9000000103_M85143_gp_family_name_with_hyphen.json new file mode 100644 index 000000000..d5194f3ce --- /dev/null +++ b/lambdas/services/mock_data/pds_patient_9000000103_M85143_gp_family_name_with_hyphen.json @@ -0,0 +1,386 @@ +{ + "resourceType": "Patient", + "id": "9000000103", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000103", + "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": "2025-12-31" + }, + "given": ["Jane"], + "family": "Smith-Anderson", + "prefix": ["Mrs"], + "suffix": ["MBE"] + }, + { + "id": "1234", + "use": "other", + "period": { + "start": "2020-01-01", + "end": "2025-12-31" + }, + "family": "Stevens", + "prefix": ["Mr"], + "suffix": ["MBE"] + } + ], + "gender": "female", + "birthDate": "2010-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "M85143", + "period": { + "start": "2020-01-01", + "end": "2025-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": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-DeathNotificationStatus", + "extension": [ + { + "url": "deathNotificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-DeathNotificationStatus", + "version": "1.0.0", + "code": "2", + "display": "Formal - death notice received from Registrar of Deaths" + } + ] + } + }, + { + "url": "systemEffectiveDate", + "valueDateTime": "2010-10-22T00:00:00+00:00" + } + ] + }, + { + "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": "SCT", + "display": "Transferred to Scotland" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2025-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2025-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": "2025-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": "2025-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": "2025-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": "2025-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/lambdas/services/mock_data/pds_patient_9000000104_M85143_gp_given_name_with_whitespace.json b/lambdas/services/mock_data/pds_patient_9000000104_M85143_gp_given_name_with_whitespace.json new file mode 100644 index 000000000..5d39b0fdb --- /dev/null +++ b/lambdas/services/mock_data/pds_patient_9000000104_M85143_gp_given_name_with_whitespace.json @@ -0,0 +1,386 @@ +{ + "resourceType": "Patient", + "id": "9000000104", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000104", + "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": "2025-12-31" + }, + "given": ["Jane Bob"], + "family": "Smith", + "prefix": ["Mrs"], + "suffix": ["MBE"] + }, + { + "id": "1234", + "use": "other", + "period": { + "start": "2020-01-01", + "end": "2025-12-31" + }, + "family": "Stevens", + "prefix": ["Mr"], + "suffix": ["MBE"] + } + ], + "gender": "female", + "birthDate": "2010-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "M85143", + "period": { + "start": "2020-01-01", + "end": "2025-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": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-DeathNotificationStatus", + "extension": [ + { + "url": "deathNotificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-DeathNotificationStatus", + "version": "1.0.0", + "code": "2", + "display": "Formal - death notice received from Registrar of Deaths" + } + ] + } + }, + { + "url": "systemEffectiveDate", + "valueDateTime": "2010-10-22T00:00:00+00:00" + } + ] + }, + { + "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": "SCT", + "display": "Transferred to Scotland" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2025-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2025-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": "2025-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": "2025-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": "2025-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": "2025-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/lambdas/services/mock_data/pds_patient_9000000105_M85143_gp_given_name_with_two_separate_strings.json b/lambdas/services/mock_data/pds_patient_9000000105_M85143_gp_given_name_with_two_separate_strings.json new file mode 100644 index 000000000..c23e695b7 --- /dev/null +++ b/lambdas/services/mock_data/pds_patient_9000000105_M85143_gp_given_name_with_two_separate_strings.json @@ -0,0 +1,386 @@ +{ + "resourceType": "Patient", + "id": "9000000105", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000105", + "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": "2025-12-31" + }, + "given": ["Jane", "Bob"], + "family": "Smith", + "prefix": ["Mrs"], + "suffix": ["MBE"] + }, + { + "id": "1234", + "use": "other", + "period": { + "start": "2020-01-01", + "end": "2025-12-31" + }, + "family": "Stevens", + "prefix": ["Mr"], + "suffix": ["MBE"] + } + ], + "gender": "female", + "birthDate": "2010-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "M85143", + "period": { + "start": "2020-01-01", + "end": "2025-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": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-DeathNotificationStatus", + "extension": [ + { + "url": "deathNotificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-DeathNotificationStatus", + "version": "1.0.0", + "code": "2", + "display": "Formal - death notice received from Registrar of Deaths" + } + ] + } + }, + { + "url": "systemEffectiveDate", + "valueDateTime": "2010-10-22T00:00:00+00:00" + } + ] + }, + { + "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": "SCT", + "display": "Transferred to Scotland" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2025-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2025-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": "2025-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": "2025-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": "2025-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": "2025-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/lambdas/services/mock_data/pds_patient_9000000106_M85143_gp_with_temp_name.json b/lambdas/services/mock_data/pds_patient_9000000106_M85143_gp_with_temp_name.json new file mode 100644 index 000000000..6eed5ba22 --- /dev/null +++ b/lambdas/services/mock_data/pds_patient_9000000106_M85143_gp_with_temp_name.json @@ -0,0 +1,386 @@ +{ + "resourceType": "Patient", + "id": "9000000106", + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000106", + "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" + }, + "given": ["Jane", "Bob"], + "family": "Smith", + "prefix": ["Mrs"], + "suffix": ["MBE"] + }, + { + "id": "234", + "use": "temp", + "period": { + "start": "2024-01-01", + "end": "2099-12-31" + }, + "given": ["Temp Jane"], + "family": "Smith", + "prefix": ["Mr"], + "suffix": ["MBE"] + } + ], + "gender": "female", + "birthDate": "2010-10-22", + "multipleBirthInteger": 1, + "generalPractitioner": [ + { + "id": "254406A3", + "type": "Organization", + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "M85143", + "period": { + "start": "2020-01-01", + "end": "2025-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": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-PreferredDispenserOrganization", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-MedicalApplianceSupplier", + "valueReference": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + } + } + }, + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-DeathNotificationStatus", + "extension": [ + { + "url": "deathNotificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-DeathNotificationStatus", + "version": "1.0.0", + "code": "2", + "display": "Formal - death notice received from Registrar of Deaths" + } + ] + } + }, + { + "url": "systemEffectiveDate", + "valueDateTime": "2010-10-22T00:00:00+00:00" + } + ] + }, + { + "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": "SCT", + "display": "Transferred to Scotland" + } + ] + } + }, + { + "url": "effectiveTime", + "valuePeriod": { + "start": "2020-01-01T00:00:00+00:00", + "end": "2025-12-31T00:00:00+00:00" + } + } + ] + } + ], + "telecom": [ + { + "id": "789", + "period": { + "start": "2020-01-01", + "end": "2025-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": "2025-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": "2025-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": "2025-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": "2025-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/lambdas/tests/unit/conftest.py b/lambdas/tests/unit/conftest.py index eed5fbc70..7f5269515 100644 --- a/lambdas/tests/unit/conftest.py +++ b/lambdas/tests/unit/conftest.py @@ -1,5 +1,6 @@ import json import tempfile +from contextlib import contextmanager from dataclasses import dataclass from enum import Enum from unittest import mock @@ -285,3 +286,12 @@ def mock_temp_folder(mocker): def mock_uuid(mocker): mocker.patch("uuid.uuid4", return_value=TEST_UUID) yield TEST_UUID + + +@contextmanager +def expect_not_to_raise(exception, message_when_fail=""): + try: + yield + except exception: + message_when_fail = message_when_fail or "DID RAISE {0}".format(exception) + raise pytest.fail(message_when_fail) diff --git a/lambdas/tests/unit/helpers/data/pds/test_cases_for_date_logic.py b/lambdas/tests/unit/helpers/data/pds/test_cases_for_date_logic.py new file mode 100644 index 000000000..83adaf727 --- /dev/null +++ b/lambdas/tests/unit/helpers/data/pds/test_cases_for_date_logic.py @@ -0,0 +1,35 @@ +from typing import Optional + +from models.pds_models import Name, Patient +from tests.unit.helpers.data.pds.pds_patient_response import PDS_PATIENT + + +def build_test_name( + given: list[str] = None, + family: str = "Smith", + start: Optional[str] = None, + end: Optional[str] = None, + use: str = "usual", +): + if not given: + given = ["Jane"] + + period = None + if start: + period = {"start": start, "end": end} + + return Name.model_validate( + { + "use": use, + "period": period, + "given": given, + "family": family, + } + ) + + +def build_test_patient_with_names(names: list[Name]) -> Patient: + patient = Patient.model_validate(PDS_PATIENT) + patient.name = names + + return patient diff --git a/lambdas/tests/unit/helpers/data/pds/test_cases_for_patient_name_matching.py b/lambdas/tests/unit/helpers/data/pds/test_cases_for_patient_name_matching.py new file mode 100644 index 000000000..eee282a06 --- /dev/null +++ b/lambdas/tests/unit/helpers/data/pds/test_cases_for_patient_name_matching.py @@ -0,0 +1,81 @@ +from datetime import date +from typing import List, NamedTuple + +from models.pds_models import PatientDetails +from tests.unit.conftest import TEST_NHS_NUMBER + + +class PdsNameMatchingTestCase(NamedTuple): + patient_details: PatientDetails + patient_name_in_file_name: str + should_accept_name: bool + + +TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME = { + "pds_name": {"family": "Smith Anderson", "given": ["Jane", "Bob"]}, + "accept": [ + "Jane Bob Smith Anderson", + "Jane Smith Anderson", + "Jane B Smith Anderson", + ], + "reject": [ + "Bob Smith Anderson", + "Jane Smith", + "Jane Anderson", + "Jane Anderson Smith", + ], +} + +TEST_CASES_FOR_FAMILY_NAME_WITH_HYPHEN = { + "pds_name": {"family": "Smith-Anderson", "given": ["Jane"]}, + "accept": ["Jane Smith-Anderson"], + "reject": ["Jane Smith Anderson", "Jane Smith", "Jane Anderson"], +} + +TEST_CASES_FOR_TWO_WORDS_GIVEN_NAME = { + "pds_name": {"family": "Smith", "given": ["Jane Bob"]}, + "accept": ["Jane Bob Smith"], + "reject": ["Jane Smith", "Jane B Smith", "Jane-Bob Smith", "Bob Smith"], +} + +TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME_AND_GIVEN_NAME = { + "pds_name": {"family": "Smith Anderson", "given": ["Jane Bob"]}, + "accept": ["Jane Bob Smith Anderson"], + "reject": [ + "Jane Smith Anderson", + "Bob Smith Anderson", + "Jane B Smith Anderson", + "Jane Bob Smith", + "Jane Bob Anderson", + ], +} + + +MOCK_DATE_OF_BIRTH = date(2010, 10, 22) + + +def load_test_cases(test_case_dict: dict) -> List[PdsNameMatchingTestCase]: + patient_detail = PatientDetails( + givenName=test_case_dict["pds_name"]["given"], + familyName=test_case_dict["pds_name"]["family"], + birthDate=MOCK_DATE_OF_BIRTH, + nhsNumber=TEST_NHS_NUMBER, + superseded=False, + restricted=False, + ) + + test_cases_for_accept = [ + PdsNameMatchingTestCase( + patient_detail, patient_name_in_file_name=test_name, should_accept_name=True + ) + for test_name in test_case_dict["accept"] + ] + test_cases_for_reject = [ + PdsNameMatchingTestCase( + patient_detail, + patient_name_in_file_name=test_file_name, + should_accept_name=False, + ) + for test_file_name in test_case_dict["reject"] + ] + return test_cases_for_accept + test_cases_for_reject diff --git a/lambdas/tests/unit/models/test_pds_models.py b/lambdas/tests/unit/models/test_pds_models.py index 029f46af9..115293386 100644 --- a/lambdas/tests/unit/models/test_pds_models.py +++ b/lambdas/tests/unit/models/test_pds_models.py @@ -12,6 +12,10 @@ PDS_PATIENT_WITHOUT_ACTIVE_GP, PDS_PATIENT_WITHOUT_ADDRESS, ) +from tests.unit.helpers.data.pds.test_cases_for_date_logic import ( + build_test_name, + build_test_patient_with_names, +) from tests.unit.helpers.data.pds.utils import create_patient from utils.utilities import validate_nhs_number @@ -163,3 +167,135 @@ def test_patient_without_period_in_general_practitioner_identifier_can_be_proces result = patient.get_patient_details(patient.id) assert expected == result + + +def test_get_patient_details_return_the_most_recent_name(): + name_1 = build_test_name(start="1990-01-01", end=None, given=["Jane"]) + name_2 = build_test_name(start="2010-02-14", end=None, given=["Jones"]) + name_3 = build_test_name(start="2000-03-25", end=None, given=["Bob"]) + + test_patient = build_test_patient_with_names([name_1, name_2, name_3]) + + expected_given_name = ["Jones"] + actual = test_patient.get_patient_details(test_patient.id).given_name + + assert actual == expected_given_name + + +def test_get_current_family_name_and_given_name_return_the_first_usual_name_if_all_names_have_no_dates_attached(): + name_1 = build_test_name(use="temp", start=None, end=None, given=["Jones"]) + name_2 = build_test_name(use="usual", start=None, end=None, given=["Jane"]) + name_3 = build_test_name(use="usual", start=None, end=None, given=["Bob"]) + + test_patient = build_test_patient_with_names([name_1, name_2, name_3]) + + expected_given_name = ["Jane"] + actual = test_patient.get_patient_details(test_patient.id).given_name + + assert actual == expected_given_name + + +def test_get_current_family_name_and_given_name_logs_a_warning_if_no_current_name_or_usual_name_found( + caplog, +): + test_patient = build_test_patient_with_names([]) + + actual = test_patient.get_patient_details(test_patient.id) + assert actual.given_name == [""] + assert actual.family_name == "" + + expected_log = "The patient does not have a currently active name or a usual name." + actual_log = caplog.records[-1].msg + + assert expected_log == actual_log + assert caplog.records[-1].levelname == "WARNING" + + +@freeze_time("2024-01-01") +def test_name_is_currently_in_use_return_false_for_expired_name(): + test_name = build_test_name(start="2023-01-01", end="2023-06-01") + expected = False + actual = test_name.is_currently_in_use() + + assert actual == expected + + +@freeze_time("2024-01-01") +def test_name_is_currently_in_use_return_false_for_name_not_started_yet(): + test_name = build_test_name(start="2024-02-01", end="2024-06-01") + expected = False + actual = test_name.is_currently_in_use() + + assert actual == expected + + +@freeze_time("2024-01-01") +def test_name_is_currently_in_use_return_true_when_name_period_includes_today(): + test_name = build_test_name(start="2023-12-31", end="2024-02-01") + expected = True + actual = test_name.is_currently_in_use() + + assert actual == expected + + +@freeze_time("2024-01-01") +def test_name_is_currently_in_use_return_false_for_name_without_a_period_field(): + test_name = build_test_name(start=None, end=None) + assert test_name.period is None + + expected = False + actual = test_name.is_currently_in_use() + + assert actual == expected + + +@freeze_time("2024-01-01") +def test_name_is_currently_in_use_can_handle_name_with_no_end_in_period(): + test_name = build_test_name(start="2023-12-31", end=None) + assert test_name.period.end is None + + expected = True + actual = test_name.is_currently_in_use() + + assert actual == expected + + +@freeze_time("2024-01-01") +def test_name_is_currently_in_use_return_false_for_nickname_or_old_name(): + test_nickname = build_test_name( + use="nickname", start="2023-01-01", end="2024-12-31" + ) + test_old_name = build_test_name(use="old", start="2023-01-01", end="2024-12-31") + + assert test_nickname.is_currently_in_use() is False + assert test_old_name.is_currently_in_use() is False + + +@freeze_time("2024-01-01") +def test_get_most_recent_name_return_the_name_with_most_recent_start_date(): + name_1 = build_test_name(start="1990-01-01", end=None, given=["Jane"]) + name_2 = build_test_name(start="2010-02-14", end=None, given=["Jones"]) + name_3 = build_test_name(start="2000-03-25", end=None, given=["Bob"]) + expired_name = build_test_name( + start="2020-04-05", end="2022-07-01", given=["Alice"] + ) + nickname = build_test_name( + use="nickname", start="2023-01-01", end=None, given=["Janie"] + ) + future_name = build_test_name(start="2047-01-01", end=None, given=["Neo Jane"]) + + test_patient = build_test_patient_with_names( + [name_1, name_2, name_3, expired_name, nickname, future_name] + ) + + expected = name_2 + actual = test_patient.get_most_recent_name() + + assert actual == expected + + +@freeze_time("2024-01-01") +def test_get_most_recent_name_return_none_if_no_active_name_found(): + test_patient = build_test_patient_with_names([]) + + assert test_patient.get_most_recent_name() is None diff --git a/lambdas/tests/unit/utils/test_lloyd_george_validator.py b/lambdas/tests/unit/utils/test_lloyd_george_validator.py index dfadee56e..0cbb83603 100644 --- a/lambdas/tests/unit/utils/test_lloyd_george_validator.py +++ b/lambdas/tests/unit/utils/test_lloyd_george_validator.py @@ -1,11 +1,11 @@ import pytest from botocore.exceptions import ClientError from enums.supported_document_types import SupportedDocumentTypes -from models.pds_models import Patient +from models.pds_models import Patient, PatientDetails from requests import Response from services.base.ssm_service import SSMService from services.document_service import DocumentService -from tests.unit.conftest import TEST_NHS_NUMBER +from tests.unit.conftest import TEST_NHS_NUMBER, expect_not_to_raise from tests.unit.helpers.data.bulk_upload.test_data import ( TEST_DOCUMENT_REFERENCE_LIST, TEST_NHS_NUMBER_FOR_BULK_UPLOAD, @@ -15,6 +15,13 @@ PDS_PATIENT, PDS_PATIENT_WITH_MIDDLE_NAME, ) +from tests.unit.helpers.data.pds.test_cases_for_patient_name_matching import ( + TEST_CASES_FOR_FAMILY_NAME_WITH_HYPHEN, + TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME, + TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME_AND_GIVEN_NAME, + TEST_CASES_FOR_TWO_WORDS_GIVEN_NAME, + load_test_cases, +) from tests.unit.models.test_document_reference import MOCK_DOCUMENT_REFERENCE from utils.common_query_filters import NotDeleted from utils.exceptions import ( @@ -50,11 +57,9 @@ def test_catching_error_when_file_type_not_pdf(): def test_valid_file_type(): - try: + with expect_not_to_raise(LGInvalidFilesException): file_type = "application/pdf" validate_lg_file_type(file_type) - except LGInvalidFilesException: - assert False, "One or more of the files do not match the required file type" def test_invalid_file_name(): @@ -64,13 +69,11 @@ def test_invalid_file_name(): def test_valid_file_name(): - try: + with expect_not_to_raise(LGInvalidFilesException): file_name = ( "1of1_Lloyd_George_Record_[Joe Bloggs]_[1111111111]_[25-12-2019].pdf" ) validate_file_name(file_name) - except LGInvalidFilesException: - assert False, "One or more of the files do not match naming convention" @pytest.mark.parametrize( @@ -87,22 +90,22 @@ def test_valid_file_name(): ], ) def test_valid_file_name_special_characters(file_name): - try: + with expect_not_to_raise( + LGInvalidFilesException, + "validate_file_name should handle patient names with special characters", + ): validate_file_name(file_name) - except LGInvalidFilesException: - assert ( - False - ), "validate_file_name should be handle patient names with special characters" def test_valid_file_name_with_apostrophe(): - try: + with expect_not_to_raise( + LGInvalidFilesException, + "validate_file_name should handle patient names with special characters and apostrophe", + ): file_name = ( "1of1_Lloyd_George_Record_[Joé Blöggê's-Glüë]_[1111111111]_[25-12-2019].pdf" ) validate_file_name(file_name) - except LGInvalidFilesException: - assert False, "One or more of the files do not match naming convention" def test_files_with_duplication(): @@ -115,14 +118,12 @@ def test_files_with_duplication(): def test_files_without_duplication(): - try: + with expect_not_to_raise(LGInvalidFilesException): lg_file_list = [ "1of2_Lloyd_George_Record_[Joe Bloggs]_[1111111111]_[25-12-2019].pdf", "2of2_Lloyd_George_Record_[Joe Bloggs]_[1111111111]_[25-12-2019].pdf", ] check_for_duplicate_files(lg_file_list) - except LGInvalidFilesException: - assert False, "One or more of the files has the same filename" def test_files_list_with_missing_files(): @@ -165,22 +166,18 @@ def test_file_name_with_apostrophe_as_name(): As part of prmdr-520 it was decided that it was acceptable to have an apostrophe accepted as a name This is because patient names will only ever come from PDS """ - try: + with expect_not_to_raise(LGInvalidFilesException): file_name = "1of1_Lloyd_George_Record_[']_[1111111111]_[25-12-2019].pdf" validate_file_name(file_name) - except LGInvalidFilesException: - assert False, "One or more of the files do not match naming convention" def test_files_without_missing_files(): - try: + with expect_not_to_raise(LGInvalidFilesException): lg_file_list = [ "1of2_Lloyd_George_Record_[Joe Bloggs]_[1111111111]_[25-12-2019].pdf", "2of2_Lloyd_George_Record_[Joe Bloggs]_[1111111111]_[25-12-2019].pdf", ] check_for_number_of_files_match_expected(lg_file_list[0], len(lg_file_list)) - except LGInvalidFilesException: - assert False, "There are missing file(s) in the request" @pytest.mark.parametrize( @@ -236,10 +233,8 @@ def test_validate_nhs_id_with_pds_service(mock_pds_patient_details): "1of2_Lloyd_George_Record_[Jane Smith]_[9000000009]_[22-10-2010].pdf", "2of2_Lloyd_George_Record_[Jane Smith]_[9000000009]_[22-10-2010].pdf", ] - try: + with expect_not_to_raise(LGInvalidFilesException): validate_filename_with_patient_details(lg_file_list, mock_pds_patient_details) - except LGInvalidFilesException: - assert False def test_mismatch_nhs_id(mocker): @@ -268,65 +263,36 @@ def test_mismatch_name_with_pds_service(mock_pds_patient_details): validate_filename_with_patient_details(lg_file_list, mock_pds_patient_details) -def test_order_names_with_pds_service(): - lg_file_list = [ - "1of2_Lloyd_George_Record_[Jake Jane Smith]_[9000000009]_[22-10-2010].pdf", - "2of2_Lloyd_George_Record_[Jake Jane Smith]_[9000000009]_[22-10-2010].pdf", - ] - patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) - patient_details = patient.get_minimum_patient_details("9000000009") - try: - validate_filename_with_patient_details(lg_file_list, patient_details) - except LGInvalidFilesException: - assert False - - def test_validate_name_with_correct_name(mock_pds_patient_details): lg_file_patient_name = "Jane Smith" - try: + with expect_not_to_raise(LGInvalidFilesException): validate_patient_name(lg_file_patient_name, mock_pds_patient_details) - except LGInvalidFilesException: - assert False def test_validate_name_with_file_missing_middle_name(): lg_file_patient_name = "Jane Smith" patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) patient_details = patient.get_minimum_patient_details("9000000009") - try: + + with expect_not_to_raise(LGInvalidFilesException): validate_patient_name(lg_file_patient_name, patient_details) - except LGInvalidFilesException: - assert False def test_validate_name_with_additional_middle_name_in_file_mismatching_pds(): lg_file_patient_name = "Jane David Smith" patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) patient_details = patient.get_minimum_patient_details("9000000009") - try: + + with expect_not_to_raise(LGInvalidFilesException): validate_patient_name(lg_file_patient_name, patient_details) - except LGInvalidFilesException: - assert False def test_validate_name_with_additional_middle_name_in_file_but_none_in_pds( mock_pds_patient_details, ): lg_file_patient_name = "Jane David Smith" - try: + with expect_not_to_raise(LGInvalidFilesException): validate_patient_name(lg_file_patient_name, mock_pds_patient_details) - except LGInvalidFilesException: - assert False - - -def test_validate_name_with_wrong_order(): - lg_file_patient_name = "Jake Jane Smith" - patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) - patient_details = patient.get_minimum_patient_details("9000000009") - try: - validate_patient_name(lg_file_patient_name, patient_details) - except LGInvalidFilesException: - assert False def test_validate_name_with_wrong_first_name(mock_pds_patient_details): @@ -344,10 +310,76 @@ def test_validate_name_with_wrong_family_name(mock_pds_patient_details): def test_validate_name_without_given_name(mock_pds_patient_details): lg_file_patient_name = "Jane Smith" mock_pds_patient_details.given_name = [""] - try: + with expect_not_to_raise(LGInvalidFilesException): validate_patient_name(lg_file_patient_name, mock_pds_patient_details) - except LGInvalidFilesException: - assert False + + +@pytest.mark.parametrize( + ["patient_details", "patient_name_in_file_name", "should_accept_name"], + load_test_cases(TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME), +) +def test_validate_patient_name_with_two_words_family_name( + patient_details: PatientDetails, + patient_name_in_file_name: str, + should_accept_name: bool, +): + if should_accept_name: + with expect_not_to_raise(LGInvalidFilesException): + validate_patient_name(patient_name_in_file_name, patient_details) + else: + with pytest.raises(LGInvalidFilesException): + validate_patient_name(patient_name_in_file_name, patient_details) + + +@pytest.mark.parametrize( + ["patient_details", "patient_name_in_file_name", "should_accept_name"], + load_test_cases(TEST_CASES_FOR_FAMILY_NAME_WITH_HYPHEN), +) +def test_validate_patient_name_with_family_name_with_hyphen( + patient_details: PatientDetails, + patient_name_in_file_name: str, + should_accept_name: bool, +): + if should_accept_name: + with expect_not_to_raise(LGInvalidFilesException): + validate_patient_name(patient_name_in_file_name, patient_details) + else: + with pytest.raises(LGInvalidFilesException): + validate_patient_name(patient_name_in_file_name, patient_details) + + +@pytest.mark.parametrize( + ["patient_details", "patient_name_in_file_name", "should_accept_name"], + load_test_cases(TEST_CASES_FOR_TWO_WORDS_GIVEN_NAME), +) +def test_validate_patient_name_with_two_words_given_name( + patient_details: PatientDetails, + patient_name_in_file_name: str, + should_accept_name: bool, +): + if should_accept_name: + with expect_not_to_raise(LGInvalidFilesException): + validate_patient_name(patient_name_in_file_name, patient_details) + else: + with pytest.raises(LGInvalidFilesException): + validate_patient_name(patient_name_in_file_name, patient_details) + + +@pytest.mark.parametrize( + ["patient_details", "patient_name_in_file_name", "should_accept_name"], + load_test_cases(TEST_CASES_FOR_TWO_WORDS_FAMILY_NAME_AND_GIVEN_NAME), +) +def test_validate_patient_name_with_two_words_family_name_and_given_name( + patient_details: PatientDetails, + patient_name_in_file_name: str, + should_accept_name: bool, +): + if should_accept_name: + with expect_not_to_raise(LGInvalidFilesException): + validate_patient_name(patient_name_in_file_name, patient_details) + else: + with pytest.raises(LGInvalidFilesException): + validate_patient_name(patient_name_in_file_name, patient_details) def test_missing_middle_name_names_with_pds_service(): @@ -357,10 +389,9 @@ def test_missing_middle_name_names_with_pds_service(): ] patient = Patient.model_validate(PDS_PATIENT_WITH_MIDDLE_NAME) patient_details = patient.get_minimum_patient_details("9000000009") - try: + + with expect_not_to_raise(LGInvalidFilesException): validate_filename_with_patient_details(lg_file_list, patient_details) - except LGInvalidFilesException: - assert False def test_mismatch_dob_with_pds_service(mock_pds_patient_details): @@ -384,10 +415,8 @@ def test_validate_date_of_birth_when_mismatch_dob_with_pds_service( def test_validate_date_of_birth_valid_with_pds_service(mock_pds_patient_details): file_date_of_birth = "22-10-2010" - try: + with expect_not_to_raise(LGInvalidFilesException): validate_patient_date_of_birth(file_date_of_birth, mock_pds_patient_details) - except LGInvalidFilesException: - assert False def test_validate_date_of_birth_none_with_pds_service(mock_pds_patient_details): @@ -463,10 +492,8 @@ def test_check_pds_response_200_status_not_raise_exception(): response = Response() response.status_code = 200 - try: + with expect_not_to_raise(LGInvalidFilesException): check_pds_response_status(response) - except LGInvalidFilesException: - assert False def test_parse_pds_response_return_the_patient_object( diff --git a/lambdas/tests/unit/utils/test_unicode_utils.py b/lambdas/tests/unit/utils/test_unicode_utils.py index d4c263e45..6eb8b006a 100644 --- a/lambdas/tests/unit/utils/test_unicode_utils.py +++ b/lambdas/tests/unit/utils/test_unicode_utils.py @@ -6,6 +6,8 @@ contains_accent_char, convert_to_nfc_form, convert_to_nfd_form, + name_ends_with, + name_starts_with, names_are_matching, remove_accent_glyphs, ) @@ -71,6 +73,43 @@ def test_names_are_matching_handles_letter_case_difference(): assert actual == expected +@pytest.mark.parametrize( + ["full_name", "partial_name", "expected"], + [ + ("Jane Bob Smith Anderson", "Jane", True), + ("Jane Bob Smith Anderson", "Bob", False), + ("Jane Bob Smith Anderson", "jane", True), + ("jane Bob Smith Anderson", "Jane Bob", True), + ("jane Bob Smith Anderson", "Jane-Bob", False), + ("Jàne Bob Smith Anderson", "Jane", False), + ("Jàne Bob Smith Anderson", "Jàne", True), # NFC <-> NFD + ("Jàne Bob Smith Anderson", "Jàne", True), # NFD <-> NFC + ], +) +def test_name_starts_with(full_name, partial_name, expected): + actual = name_starts_with(full_name, partial_name) + + assert actual == expected + + +@pytest.mark.parametrize( + ["full_name", "partial_name", "expected"], + [ + ("Jane Bob Smith Anderson", "Anderson", True), + ("Jane Bob Smith Anderson", "Smith", False), + ("Jane Bob Smith Anderson", "anderson", True), + ("jane Bob Smith Anderson", "Smith Anderson", True), + ("jane Bob Smith Anderson", "Smith-Anderson", False), + ("Jane Bob Smith Andèrson", "Anderson", False), + ("Jane Bob Smith Andèrson", "Andèrson", True), # NFC <-> NFD + ("Jane Bob Smith Andèrson", "Andèrson", True), # NFD <-> NFC + ], +) +def test_name_ends_with(full_name, partial_name, expected): + actual = name_ends_with(full_name, partial_name) + assert actual == expected + + @pytest.mark.parametrize( ["input_str", "expected"], [ diff --git a/lambdas/utils/lloyd_george_validator.py b/lambdas/utils/lloyd_george_validator.py index 1b94f6cf3..9f5f3be49 100644 --- a/lambdas/utils/lloyd_george_validator.py +++ b/lambdas/utils/lloyd_george_validator.py @@ -18,7 +18,11 @@ PatientRecordAlreadyExistException, PdsTooManyRequestsException, ) -from utils.unicode_utils import REGEX_PATIENT_NAME_PATTERN, names_are_matching +from utils.unicode_utils import ( + REGEX_PATIENT_NAME_PATTERN, + name_ends_with, + name_starts_with, +) from utils.utilities import get_pds_service logger = LoggingService(__name__) @@ -153,23 +157,16 @@ def validate_filename_with_patient_details( raise LGInvalidFilesException(e) -def validate_patient_name(file_patient_name, pds_patient_details): +def validate_patient_name(file_patient_name: str, pds_patient_details: PatientDetails): logger.info("Verifying patient name against the record in PDS...") - patient_name_split = file_patient_name.split(" ") - file_patient_first_name = patient_name_split[0] - file_patient_last_name = patient_name_split[-1] - is_file_first_name_in_patient_details = False - for patient_name in pds_patient_details.given_name: - if ( - names_are_matching(file_patient_first_name, patient_name) - or not patient_name - ): - is_file_first_name_in_patient_details = True - break - - if not is_file_first_name_in_patient_details or not names_are_matching( - file_patient_last_name, pds_patient_details.family_name - ): + + first_name_in_pds: str = pds_patient_details.given_name[0] + family_name_in_pds = pds_patient_details.family_name + + first_name_matches = name_starts_with(file_patient_name, first_name_in_pds) + family_name_matches = name_ends_with(file_patient_name, family_name_in_pds) + + if not (first_name_matches and family_name_matches): raise LGInvalidFilesException("Patient name does not match our records") diff --git a/lambdas/utils/unicode_utils.py b/lambdas/utils/unicode_utils.py index beb96a123..eadc5a660 100644 --- a/lambdas/utils/unicode_utils.py +++ b/lambdas/utils/unicode_utils.py @@ -44,6 +44,20 @@ def names_are_matching(name_a: str, name_b: str) -> bool: ) +def name_starts_with(full_name: str, partial_name: str) -> bool: + folded_full_name = convert_to_nfd_form(full_name).casefold() + folded_partial_name = convert_to_nfd_form(partial_name).casefold() + + return folded_full_name.startswith(folded_partial_name) + + +def name_ends_with(full_name: str, partial_name: str) -> bool: + folded_full_name = convert_to_nfd_form(full_name).casefold() + folded_partial_name = convert_to_nfd_form(partial_name).casefold() + + return folded_full_name.endswith(folded_partial_name) + + def convert_to_nfc_form(input_str: str) -> str: """ Convert a string to the NFC normalization form diff --git a/sonar-project.properties b/sonar-project.properties index 27633ba18..67136cb84 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -14,7 +14,7 @@ sonar.python.coverage.reportPaths=lambdas/coverage.xml sonar.sources=lambdas/,app/src/ sonar.tests=lambdas/tests/,app/src/ -sonar.exclusions=**/*.test.tsx,app/src/helpers/test/,**/*.story.tsx,**/TestPanel.tsx,lambdas/scripts/,**/*.test.ts,**/*.story.ts,lambdas/tests/* +sonar.exclusions=**/*.test.tsx,app/src/helpers/test/,**/*.story.tsx,**/TestPanel.tsx,lambdas/scripts/,**/*.test.ts,**/*.story.ts,lambdas/tests/*,**/conftest.py sonar.test.inclusions=**/*.test.tsx,app/src/helpers/test/,**/*.test.ts # Encoding of the source code. Default is default system encoding