From b1feb82e50adf6d011105f6ce7db065bf243057c Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:21:22 +0100 Subject: [PATCH 1/9] NPA-6546: Add permission for logged in user in TPP + spec update + e2e testing update --- app/api/infrastructure/tpp/client.py | 14 ++- .../tpp/data/mocked_response.xml | 14 +++ .../infrastructure/tpp/tests/test_client.py | 78 +++++++++++++++ specification/im1-pfs-auth-api.yaml | 94 +++++++++--------- .../end_to_end/authenticate/POST/test_201.py | 97 +++++++++++++++++++ 5 files changed, 249 insertions(+), 48 deletions(-) diff --git a/app/api/infrastructure/tpp/client.py b/app/api/infrastructure/tpp/client.py index 6869e02..ed7d8e5 100644 --- a/app/api/infrastructure/tpp/client.py +++ b/app/api/infrastructure/tpp/client.py @@ -104,6 +104,9 @@ def transform_response(self, response: dict) -> ForwardResponse: return SessionResponse( sessionId=response.get("@suid"), supplier=self.supplier, + permissions=self._parse_permissions( + proxy_person.get("EffectiveServiceAccess", []) + ), proxy=Demographics( firstName=proxy_person.get("PersonName", {}).get("@firstName"), surname=proxy_person.get("PersonName", {}).get("@surname"), @@ -143,9 +146,7 @@ def _parse_patients(self, data: dict) -> list[Patient]: parsed_patients = [] for patient in patient_links: person = patient["Person"] - raw_permissions = person.get("EffectiveServiceAccess", []).get( - "ServiceAccess", [] - ) + raw_permissions = person.get("EffectiveServiceAccess", []) parsed_patients.append( Patient( firstName=person.get("PersonName", {}).get("@firstName"), @@ -157,6 +158,11 @@ def _parse_patients(self, data: dict) -> list[Patient]: return parsed_patients def _parse_permissions(self, raw_permissions: dict) -> list[ServiceAccess]: + service_access = ( + [permission.get("ServiceAccess", {}) for permission in raw_permissions] + if isinstance(raw_permissions, list) + else raw_permissions.get("ServiceAccess", {}) + ) return [ ServiceAccess( description=ServiceAccessDescription(service["@description"]), @@ -166,5 +172,5 @@ def _parse_permissions(self, raw_permissions: dict) -> list[ServiceAccess]: service["@statusDesc"] ), ) - for service in raw_permissions + for service in service_access ] diff --git a/app/api/infrastructure/tpp/data/mocked_response.xml b/app/api/infrastructure/tpp/data/mocked_response.xml index bbda5c0..f143191 100644 --- a/app/api/infrastructure/tpp/data/mocked_response.xml +++ b/app/api/infrastructure/tpp/data/mocked_response.xml @@ -9,6 +9,20 @@ + + + + + + + + + + + + + + diff --git a/app/api/infrastructure/tpp/tests/test_client.py b/app/api/infrastructure/tpp/tests/test_client.py index d91dbea..49f83e0 100644 --- a/app/api/infrastructure/tpp/tests/test_client.py +++ b/app/api/infrastructure/tpp/tests/test_client.py @@ -145,6 +145,84 @@ def test_tpp_client_transform_response(client: TPPClient) -> None: assert actual_result == SessionResponse( sessionId="xhvE9/jCjdafytcXBq8LMKMgc4wA/w5k/O5C4ip0Fs9GPbIQ/WRIZi4Och1Spmg7aYJR2CZVLHfu6cRVv84aEVrRE8xahJbT4TPAr8N/CYix6TBquQsZibYXYMxJktXcYKwDhBH8yr3iJYnyevP3hV76oTjVmKieBtYzSSZAOu4=", supplier="TPP", + permissions=[ + ServiceAccess( + description=ServiceAccessDescription("Full Clinical Record"), + serviceIdentifier=1, + status=ServiceAccessStatus("U"), + statusDescription=ServiceAccessStatusDescription("Unavailable"), + ), + ServiceAccess( + serviceIdentifier=2, + description=ServiceAccessDescription("Appointments"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), + ), + ServiceAccess( + serviceIdentifier=4, + description=ServiceAccessDescription("Request Medication"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), + ), + ServiceAccess( + serviceIdentifier=8, + description=ServiceAccessDescription("Questionnaires"), + status=ServiceAccessStatus("N"), + statusDescription=ServiceAccessStatusDescription("Not offered by unit"), + ), + ServiceAccess( + serviceIdentifier=64, + description=ServiceAccessDescription("Summary Record"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), + ), + ServiceAccess( + serviceIdentifier=128, + description=ServiceAccessDescription("Detailed Coded Record"), + status=ServiceAccessStatus("U"), + statusDescription=ServiceAccessStatusDescription("Unavailable"), + ), + ServiceAccess( + serviceIdentifier=512, + description=ServiceAccessDescription("Messaging"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), + ), + ServiceAccess( + serviceIdentifier=1024, + description=ServiceAccessDescription("View Sharing Status"), + status=ServiceAccessStatus("N"), + statusDescription=ServiceAccessStatusDescription("Not offered by unit"), + ), + ServiceAccess( + serviceIdentifier=2048, + description=ServiceAccessDescription("Record Audit"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), + ), + ServiceAccess( + serviceIdentifier=4096, + description=ServiceAccessDescription("Change Pharmacy"), + status=ServiceAccessStatus("N"), + statusDescription=ServiceAccessStatusDescription("Not offered by unit"), + ), + ServiceAccess( + serviceIdentifier=8192, + description=ServiceAccessDescription( + "Manage Sharing Rules And Requests" + ), + status=ServiceAccessStatus("G"), + statusDescription=ServiceAccessStatusDescription( + "Only available to GMS registered patients" + ), + ), + ServiceAccess( + serviceIdentifier=65536, + description=ServiceAccessDescription("Access SystmConnect"), + status=ServiceAccessStatus("O"), + statusDescription=ServiceAccessStatusDescription("Other"), + ), + ], proxy=Demographics(firstName="Sam", surname="Jones", title="Mr"), patients=[ Patient( diff --git a/specification/im1-pfs-auth-api.yaml b/specification/im1-pfs-auth-api.yaml index ad9f6db..89ce60e 100644 --- a/specification/im1-pfs-auth-api.yaml +++ b/specification/im1-pfs-auth-api.yaml @@ -212,6 +212,7 @@ components: - sessionId endUserSessionId supplier + permissions proxy patients type: object @@ -224,6 +225,8 @@ components: supplier: enum: [EMIS] type: string + permissions: + $ref: "#/components/schemas/EMISPermissionsModel" proxy: $ref: "#/components/schemas/DemographicsModel" patients: @@ -236,6 +239,7 @@ components: required: - sessionId supplier + permissions proxy patients type: object @@ -246,6 +250,8 @@ components: supplier: enum: [TPP] type: string + permissions: + $ref: "#/components/schemas/TPPSPermissionsModel" proxy: $ref: "#/components/schemas/DemographicsModel" patients: @@ -357,50 +363,50 @@ components: type: object properties: permissions: - type: array - items: - $ref: "#/components/schemas/TPPServiceAccess" - - TPPServiceAccess: - required: - - description - serviceIdentifier - status - statusDescription - type: object - properties: - description: - type: string - enum: - [ - "Full Clinical Record", - "Appointments", - "Request Medication", - "Questionnaires", - "Summary Record", - "Detailed Coded Record", - "Messaging", - "View Sharing Status", - "Record Audit", - "Change Pharmacy", - "Manage Sharing Rules And Requests", - "Access SystmConnect", - ] - serviceIdentifier: - type: integer - status: - type: string - enum: ["A", "U", "N", "G", "O"] - statusDescription: - type: string - enum: - [ - "Available", - "Unavailable", - "Not offered by unit", - "Only available to GMS registered patients", - "Other", - ] + $ref: "#/components/schemas/TPPSPermissionsModel" + + TPPSPermissionsModel: + type: array + items: + required: + - description + serviceIdentifier + status + statusDescription + type: object + properties: + description: + type: string + enum: + [ + "Full Clinical Record", + "Appointments", + "Request Medication", + "Questionnaires", + "Summary Record", + "Detailed Coded Record", + "Messaging", + "View Sharing Status", + "Record Audit", + "Change Pharmacy", + "Manage Sharing Rules And Requests", + "Access SystmConnect", + ] + serviceIdentifier: + type: integer + status: + type: string + enum: ["A", "U", "N", "G", "O"] + statusDescription: + type: string + enum: + [ + "Available", + "Unavailable", + "Not offered by unit", + "Only available to GMS registered patients", + "Other", + ] OperationOutcome: type: object diff --git a/tests/end_to_end/authenticate/POST/test_201.py b/tests/end_to_end/authenticate/POST/test_201.py index 506ef4a..7fee4ea 100644 --- a/tests/end_to_end/authenticate/POST/test_201.py +++ b/tests/end_to_end/authenticate/POST/test_201.py @@ -99,6 +99,27 @@ }, ], "proxy": {"firstName": "Alex", "surname": "Taylor", "title": "Mr"}, + "permissions": { + "appointmentsEnabled": True, + "demographicsUpdateEnabled": True, + "epsEnabled": True, + "medicalRecordEnabled": True, + "onlineTriageEnabled": False, + "practicePatientCommunicationEnabled": False, + "prescribingEnabled": True, + "recordSharingEnabled": False, + "recordViewAuditEnabled": True, + "medicalRecord": { + "recordAccessScheme": "DetailedCodedCareRecord", + "allergiesEnabled": True, + "consultationsEnabled": True, + "immunisationsEnabled": True, + "documentsEnabled": True, + "medicationEnabled": True, + "problemsEnabled": True, + "testResultsEnabled": True, + }, + }, "sessionId": "SID_2qZ9yJpVxHq4N3b", "endUserSessionId": "SESS_mDq6nE2b8R7KQ0v", "supplier": "EMIS", @@ -191,6 +212,82 @@ }, ], "proxy": {"firstName": "Sam", "surname": "Jones", "title": "Mr"}, + "permissions": [ + { + "description": "Full Clinical Record", + "statusDescription": "Unavailable", + "serviceIdentifier": 1, + "status": "U", + }, + { + "serviceIdentifier": 2, + "statusDescription": "Available", + "description": "Appointments", + "status": "A", + }, + { + "serviceIdentifier": 4, + "statusDescription": "Available", + "description": "Request Medication", + "status": "A", + }, + { + "serviceIdentifier": 8, + "description": "Questionnaires", + "status": "N", + "statusDescription": "Not offered by unit", + }, + { + "serviceIdentifier": 64, + "statusDescription": "Available", + "description": "Summary Record", + "status": "A", + }, + { + "serviceIdentifier": 128, + "statusDescription": "Unavailable", + "description": "Detailed Coded Record", + "status": "U", + }, + { + "serviceIdentifier": 512, + "statusDescription": "Available", + "description": "Messaging", + "status": "A", + }, + { + "serviceIdentifier": 1024, + "statusDescription": "Not offered by unit", + "description": "View Sharing Status", + "status": "N", + }, + { + "serviceIdentifier": 2048, + "statusDescription": "Available", + "description": "Record Audit", + "status": "A", + }, + { + "serviceIdentifier": 4096, + "statusDescription": "Not offered by unit", + "description": "Change Pharmacy", + "status": "N", + }, + { + "serviceIdentifier": 8192, + "statusDescription": ( + "Only available to GMS registered patients" + ), + "description": "Manage Sharing Rules And Requests", + "status": "G", + }, + { + "serviceIdentifier": 65536, + "statusDescription": "Other", + "description": "Access SystmConnect", + "status": "O", + }, + ], "sessionId": "xhvE9/jCjdafytcXBq8LMKMgc4wA/w5k/O5C4ip0Fs9GPbIQ/WRIZi4Och1Spmg7aYJR2CZVLHfu6cRVv84aEVrRE8xahJbT4TPAr8N/CYix6TBquQsZibYXYMxJktXcYKwDhBH8yr3iJYnyevP3hV76oTjVmKieBtYzSSZAOu4=", # noqa: E501 "supplier": "TPP", }, From 0ad50e9df577e44db28a2375035f1c739232f68d Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:33:19 +0100 Subject: [PATCH 2/9] NPA-6546: Added permissions in response for EMIS --- Makefile | 2 +- app/api/infrastructure/emis/client.py | 27 +++- app/api/infrastructure/emis/models.py | 1 + .../infrastructure/emis/tests/test_client.py | 123 +++++++++--------- app/api/infrastructure/tpp/models.py | 1 + 5 files changed, 89 insertions(+), 65 deletions(-) diff --git a/Makefile b/Makefile index 52f9cfd..30cf5ef 100644 --- a/Makefile +++ b/Makefile @@ -135,7 +135,7 @@ app-docker-run: docker run -p 9000:9000 "$(PROXYGEN_DOCKER_REGISTRY_URL):$(CONTAINER_TAG)" app-unit-test: - uv run pytest app --cov=app --cov-fail-under=80 + uv run pytest app --cov=app --cov-fail-under=80 $(PYTEST_ARGS) # ============================================================================== # Sandbox Commands diff --git a/app/api/infrastructure/emis/client.py b/app/api/infrastructure/emis/client.py index 9ff6a60..8febf22 100644 --- a/app/api/infrastructure/emis/client.py +++ b/app/api/infrastructure/emis/client.py @@ -96,16 +96,33 @@ def transform_response(self, response: dict) -> SessionResponse: Returns: SessionResponse: Homogenised response with other clients """ + self_patient_links = [ + patient_link + for patient_link in response.get("UserPatientLinks", []) + if patient_link.get("AssociationType") == "Self" + ] + + proxy_patient_links = [ + patient_link + for patient_link in response.get("UserPatientLinks", []) + if patient_link.get("AssociationType") == "Proxy" + ] + return SessionResponse( sessionId=response.get("SessionId"), endUserSessionId=response.get("EndUserSessionId"), supplier=self.supplier, + permissions=self._parse_permissions( + self_patient_links[0].get("EffectiveServices", {}) + if self_patient_links + else {} + ), proxy=Demographics( firstName=response.get("FirstName"), surname=response.get("Surname"), title=response.get("Title"), ), - patients=self._parse_patients(response), + patients=self._parse_patients(proxy_patient_links), ) def _mock_response(self) -> dict: @@ -117,17 +134,15 @@ def _mock_response(self) -> dict: with Path((BASE_DIR) / "data" / "mocked_response.json").open("r") as f: return load(f) - def _parse_patients(self, data: dict) -> list[Patient]: + def _parse_patients(self, patient_links: list) -> list[Patient]: """Parsing raw data from Client into structual model. Args: - data (dict): Raw data containing information about multiple patients + patient_links (dict): Raw data containing information about patients Returns: - list[Patient]: Parsed information about multiple patients + list[Patient]: Parsed information about patients """ - # Extra Patient data - patient_links = data.get("UserPatientLinks", []) parsed_patients = [] for patient in patient_links: raw_permissions = patient.get("EffectiveServices", {}) diff --git a/app/api/infrastructure/emis/models.py b/app/api/infrastructure/emis/models.py index a6a746d..ce0004a 100644 --- a/app/api/infrastructure/emis/models.py +++ b/app/api/infrastructure/emis/models.py @@ -91,4 +91,5 @@ class SessionResponse(ForwardResponse): model_config = ConfigDict(alias_generator=to_camel) end_user_session_id: str + permissions: Permissions patients: list[Patient] diff --git a/app/api/infrastructure/emis/tests/test_client.py b/app/api/infrastructure/emis/tests/test_client.py index 2ff7e51..2570f50 100644 --- a/app/api/infrastructure/emis/tests/test_client.py +++ b/app/api/infrastructure/emis/tests/test_client.py @@ -140,33 +140,28 @@ def test_emis_client_transform_response(client: EmisClient) -> None: endUserSessionId="SESS_mDq6nE2b8R7KQ0v", supplier="EMIS", proxy=Demographics(firstName="Alex", surname="Taylor", title="Mr"), - patients=[ - Patient( - firstName="Alex", - surname="Taylor", - title="Mr", - permissions=Permissions( - appointmentsEnabled=True, - demographicsUpdateEnabled=True, - epsEnabled=True, - medicalRecordEnabled=True, - onlineTriageEnabled=False, - practicePatientCommunicationEnabled=False, - prescribingEnabled=True, - recordSharingEnabled=False, - recordViewAuditEnabled=True, - medicalRecord=MedicalRecordPermissions( - recordAccessScheme="DetailedCodedCareRecord", - allergiesEnabled=True, - consultationsEnabled=True, - immunisationsEnabled=True, - documentsEnabled=True, - medicationEnabled=True, - problemsEnabled=True, - testResultsEnabled=True, - ), - ), + permissions=Permissions( + appointmentsEnabled=True, + demographicsUpdateEnabled=True, + epsEnabled=True, + medicalRecordEnabled=True, + onlineTriageEnabled=False, + practicePatientCommunicationEnabled=False, + prescribingEnabled=True, + recordSharingEnabled=False, + recordViewAuditEnabled=True, + medicalRecord=MedicalRecordPermissions( + recordAccessScheme="DetailedCodedCareRecord", + allergiesEnabled=True, + consultationsEnabled=True, + immunisationsEnabled=True, + documentsEnabled=True, + medicationEnabled=True, + problemsEnabled=True, + testResultsEnabled=True, ), + ), + patients=[ Patient( firstName="Jane", surname="Doe", @@ -224,44 +219,56 @@ def test_emis_client_transform_response(client: EmisClient) -> None: @pytest.mark.parametrize( - "response", + ("response", "expected_error"), [ - {}, - { # Missing UserPatientLints - "SessionId": "some session", - "FirstName": "someone's first name", - "Surname": "someone's surname", - "Title": "someone's title", - }, - { # Missing Proxy Demographic information - "SessionId": "some session", - "UserPatientLinks": [ - { - "FirstName": "someone's first name", - "Surname": "someone's surname", - "Title": "someone's title", - } - ], - }, - { # Missing Session Id - "FirstName": "someone's first name", - "Surname": "someone's surname", - "Title": "someone's title", - "UserPatientLinks": [ - { - "FirstName": "someone's first name", - "Surname": "someone's surname", - "Title": "someone's title", - } - ], - }, + ({}, ValueError), + ( + { # Missing UserPatientLints + "SessionId": "some session", + "FirstName": "someone's first name", + "Surname": "someone's surname", + "Title": "someone's title", + }, + ValueError, + ), + ( + { # Missing Proxy Demographic information + "SessionId": "some session", + "UserPatientLinks": [ + { + "FirstName": "someone's first name", + "Surname": "someone's surname", + "Title": "someone's title", + "AssociationType": "Self", + } + ], + }, + ValidationError, + ), + ( + { # Missing Session Id + "FirstName": "someone's first name", + "Surname": "someone's surname", + "Title": "someone's title", + "UserPatientLinks": [ + { + "FirstName": "someone's first name", + "Surname": "someone's surname", + "Title": "someone's title", + "AssociationType": "Self", + } + ], + }, + ValidationError, + ), ], ) -def test_emis_client_transform_response_raise_validation_error( +def test_emis_client_transform_response_raise_error( response: dict, + expected_error: Exception, client: EmisClient, ) -> None: """Test the EmisClient transform_response function raises validation error.""" # Act & Assert - with pytest.raises(ValidationError): + with pytest.raises(expected_error): client.transform_response(response) diff --git a/app/api/infrastructure/tpp/models.py b/app/api/infrastructure/tpp/models.py index 8727115..55448e1 100644 --- a/app/api/infrastructure/tpp/models.py +++ b/app/api/infrastructure/tpp/models.py @@ -124,4 +124,5 @@ class SessionResponse(ForwardResponse): model_config = ConfigDict(alias_generator=to_camel) + permissions: list[ServiceAccess] patients: list[Patient] From 98b6ba2f7e85a6e685c26f0ae8e26f20c3dce172 Mon Sep 17 00:00:00 2001 From: Ellie Bound <175816742+ellie-bound1-NHSD@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:44:48 +0100 Subject: [PATCH 3/9] NPA-6546: Update proxy in response body to be user --- app/api/domain/forward_response_model.py | 7 ++++- .../tests/test_forward_response_model.py | 9 ++++-- app/api/infrastructure/emis/client.py | 8 ++--- app/api/infrastructure/emis/models.py | 10 +++++-- .../infrastructure/emis/tests/test_client.py | 10 +++---- app/api/infrastructure/tpp/client.py | 2 +- app/api/infrastructure/tpp/models.py | 8 +++-- .../infrastructure/tpp/tests/test_client.py | 2 +- specification/im1-pfs-auth-api.yaml | 8 ++--- .../end_to_end/authenticate/POST/test_201.py | 30 ++----------------- 10 files changed, 43 insertions(+), 51 deletions(-) diff --git a/app/api/domain/forward_response_model.py b/app/api/domain/forward_response_model.py index ac694f6..29aeb3c 100644 --- a/app/api/domain/forward_response_model.py +++ b/app/api/domain/forward_response_model.py @@ -12,6 +12,10 @@ class Demographics(BaseModel): title: str +class Permissions(BaseModel): + """A data model that encapsulates all the essential permissions data.""" + + class ForwardResponse(BaseModel): """All the essential information needed to forward a external backend system response to the client.""" # noqa: E501 @@ -19,7 +23,8 @@ class ForwardResponse(BaseModel): session_id: str supplier: str - proxy: Demographics + user: Demographics + permissions: Permissions patients: list[Demographics] @field_validator("patients") diff --git a/app/api/domain/tests/test_forward_response_model.py b/app/api/domain/tests/test_forward_response_model.py index cc95317..ac8a68c 100644 --- a/app/api/domain/tests/test_forward_response_model.py +++ b/app/api/domain/tests/test_forward_response_model.py @@ -1,4 +1,8 @@ -from app.api.domain.forward_response_model import Demographics, ForwardResponse +from app.api.domain.forward_response_model import ( + Demographics, + ForwardResponse, + Permissions, +) def test_forward_response() -> None: @@ -7,6 +11,7 @@ def test_forward_response() -> None: ForwardResponse( sessionId="some session id", supplier="some supplier", - proxy=Demographics(firstName="Betty", surname="Jones", title="Ms"), + user=Demographics(firstName="Betty", surname="Jones", title="Ms"), + permissions=Permissions(), patients=[Demographics(firstName="John", surname="Jones", title="Mr")], ) diff --git a/app/api/infrastructure/emis/client.py b/app/api/infrastructure/emis/client.py index 8febf22..edbb075 100644 --- a/app/api/infrastructure/emis/client.py +++ b/app/api/infrastructure/emis/client.py @@ -12,10 +12,10 @@ ) from app.api.domain.forward_response_model import Demographics from app.api.infrastructure.emis.models import ( + EffectiveServices, Identifier, MedicalRecordPermissions, Patient, - Permissions, SessionRequestData, SessionRequestHeaders, SessionResponse, @@ -117,7 +117,7 @@ def transform_response(self, response: dict) -> SessionResponse: if self_patient_links else {} ), - proxy=Demographics( + user=Demographics( firstName=response.get("FirstName"), surname=response.get("Surname"), title=response.get("Title"), @@ -156,8 +156,8 @@ def _parse_patients(self, patient_links: list) -> list[Patient]: ) return parsed_patients - def _parse_permissions(self, raw_permissions: dict) -> Permissions: - return Permissions( + def _parse_permissions(self, raw_permissions: dict) -> EffectiveServices: + return EffectiveServices( appointmentsEnabled=raw_permissions.get("AppointmentsEnabled"), demographicsUpdateEnabled=raw_permissions.get("DemographicsUpdateEnabled"), epsEnabled=raw_permissions.get("EpsEnabled"), diff --git a/app/api/infrastructure/emis/models.py b/app/api/infrastructure/emis/models.py index ce0004a..13d7d30 100644 --- a/app/api/infrastructure/emis/models.py +++ b/app/api/infrastructure/emis/models.py @@ -1,7 +1,11 @@ from pydantic import BaseModel, ConfigDict from pydantic.alias_generators import to_camel -from app.api.domain.forward_response_model import Demographics, ForwardResponse +from app.api.domain.forward_response_model import ( + Demographics, + ForwardResponse, + Permissions, +) class Identifier(BaseModel): @@ -62,7 +66,7 @@ class MedicalRecordPermissions(BaseModel): test_results_enabled: bool -class Permissions(BaseModel): +class EffectiveServices(Permissions): """Base Model for Permissions.""" model_config = ConfigDict(alias_generator=to_camel) @@ -82,7 +86,7 @@ class Permissions(BaseModel): class Patient(Demographics): """Base Model for Patient.""" - permissions: Permissions + permissions: EffectiveServices class SessionResponse(ForwardResponse): diff --git a/app/api/infrastructure/emis/tests/test_client.py b/app/api/infrastructure/emis/tests/test_client.py index 2570f50..6a4aeb1 100644 --- a/app/api/infrastructure/emis/tests/test_client.py +++ b/app/api/infrastructure/emis/tests/test_client.py @@ -16,9 +16,9 @@ from app.api.domain.forward_response_model import Demographics from app.api.infrastructure.emis.client import EmisClient from app.api.infrastructure.emis.models import ( + EffectiveServices, MedicalRecordPermissions, Patient, - Permissions, SessionResponse, ) @@ -139,8 +139,8 @@ def test_emis_client_transform_response(client: EmisClient) -> None: sessionId="SID_2qZ9yJpVxHq4N3b", endUserSessionId="SESS_mDq6nE2b8R7KQ0v", supplier="EMIS", - proxy=Demographics(firstName="Alex", surname="Taylor", title="Mr"), - permissions=Permissions( + user=Demographics(firstName="Alex", surname="Taylor", title="Mr"), + permissions=EffectiveServices( appointmentsEnabled=True, demographicsUpdateEnabled=True, epsEnabled=True, @@ -166,7 +166,7 @@ def test_emis_client_transform_response(client: EmisClient) -> None: firstName="Jane", surname="Doe", title="Mrs", - permissions=Permissions( + permissions=EffectiveServices( appointmentsEnabled=False, demographicsUpdateEnabled=True, epsEnabled=False, @@ -192,7 +192,7 @@ def test_emis_client_transform_response(client: EmisClient) -> None: firstName="Ella", surname="Taylor", title="Ms", - permissions=Permissions( + permissions=EffectiveServices( appointmentsEnabled=True, demographicsUpdateEnabled=True, epsEnabled=False, diff --git a/app/api/infrastructure/tpp/client.py b/app/api/infrastructure/tpp/client.py index ed7d8e5..3095162 100644 --- a/app/api/infrastructure/tpp/client.py +++ b/app/api/infrastructure/tpp/client.py @@ -107,7 +107,7 @@ def transform_response(self, response: dict) -> ForwardResponse: permissions=self._parse_permissions( proxy_person.get("EffectiveServiceAccess", []) ), - proxy=Demographics( + user=Demographics( firstName=proxy_person.get("PersonName", {}).get("@firstName"), surname=proxy_person.get("PersonName", {}).get("@surname"), title=proxy_person.get("PersonName", {}).get("@title"), diff --git a/app/api/infrastructure/tpp/models.py b/app/api/infrastructure/tpp/models.py index 55448e1..036793b 100644 --- a/app/api/infrastructure/tpp/models.py +++ b/app/api/infrastructure/tpp/models.py @@ -4,7 +4,11 @@ from pydantic import BaseModel, ConfigDict from pydantic.alias_generators import to_camel -from app.api.domain.forward_response_model import Demographics, ForwardResponse +from app.api.domain.forward_response_model import ( + Demographics, + ForwardResponse, + Permissions, +) class Application(BaseModel): @@ -102,7 +106,7 @@ class ServiceAccessStatusDescription(Enum): OTHER = "Other" -class ServiceAccess(BaseModel): +class ServiceAccess(Permissions): """Base Model for Service Access which holds data per permission.""" model_config = ConfigDict(alias_generator=to_camel) diff --git a/app/api/infrastructure/tpp/tests/test_client.py b/app/api/infrastructure/tpp/tests/test_client.py index 49f83e0..d3a1938 100644 --- a/app/api/infrastructure/tpp/tests/test_client.py +++ b/app/api/infrastructure/tpp/tests/test_client.py @@ -223,7 +223,7 @@ def test_tpp_client_transform_response(client: TPPClient) -> None: statusDescription=ServiceAccessStatusDescription("Other"), ), ], - proxy=Demographics(firstName="Sam", surname="Jones", title="Mr"), + user=Demographics(firstName="Sam", surname="Jones", title="Mr"), patients=[ Patient( firstName="Clare", diff --git a/specification/im1-pfs-auth-api.yaml b/specification/im1-pfs-auth-api.yaml index 89ce60e..aefcf1d 100644 --- a/specification/im1-pfs-auth-api.yaml +++ b/specification/im1-pfs-auth-api.yaml @@ -213,7 +213,7 @@ components: endUserSessionId supplier permissions - proxy + user patients type: object properties: @@ -227,7 +227,7 @@ components: type: string permissions: $ref: "#/components/schemas/EMISPermissionsModel" - proxy: + user: $ref: "#/components/schemas/DemographicsModel" patients: type: array @@ -240,7 +240,7 @@ components: - sessionId supplier permissions - proxy + user patients type: object properties: @@ -252,7 +252,7 @@ components: type: string permissions: $ref: "#/components/schemas/TPPSPermissionsModel" - proxy: + user: $ref: "#/components/schemas/DemographicsModel" patients: type: array diff --git a/tests/end_to_end/authenticate/POST/test_201.py b/tests/end_to_end/authenticate/POST/test_201.py index 7fee4ea..b9f577b 100644 --- a/tests/end_to_end/authenticate/POST/test_201.py +++ b/tests/end_to_end/authenticate/POST/test_201.py @@ -19,32 +19,6 @@ "https://nhs70apptest.emishealth.com", { "patients": [ - { - "firstName": "Alex", - "surname": "Taylor", - "title": "Mr", - "permissions": { - "appointmentsEnabled": True, - "demographicsUpdateEnabled": True, - "epsEnabled": True, - "medicalRecordEnabled": True, - "onlineTriageEnabled": False, - "practicePatientCommunicationEnabled": False, - "prescribingEnabled": True, - "recordSharingEnabled": False, - "recordViewAuditEnabled": True, - "medicalRecord": { - "recordAccessScheme": "DetailedCodedCareRecord", - "allergiesEnabled": True, - "consultationsEnabled": True, - "immunisationsEnabled": True, - "documentsEnabled": True, - "medicationEnabled": True, - "problemsEnabled": True, - "testResultsEnabled": True, - }, - }, - }, { "firstName": "Jane", "surname": "Doe", @@ -98,7 +72,7 @@ }, }, ], - "proxy": {"firstName": "Alex", "surname": "Taylor", "title": "Mr"}, + "user": {"firstName": "Alex", "surname": "Taylor", "title": "Mr"}, "permissions": { "appointmentsEnabled": True, "demographicsUpdateEnabled": True, @@ -211,7 +185,7 @@ ], }, ], - "proxy": {"firstName": "Sam", "surname": "Jones", "title": "Mr"}, + "user": {"firstName": "Sam", "surname": "Jones", "title": "Mr"}, "permissions": [ { "description": "Full Clinical Record", From df0ecd59aa1f0aab00bc5cca74723021171a251d Mon Sep 17 00:00:00 2001 From: Tom Knapp Date: Fri, 10 Apr 2026 14:01:16 +0100 Subject: [PATCH 4/9] NPA-6546: Refactor permissions to patient models and additional user and patient properties --- app/api/domain/forward_response_model.py | 12 +- .../tests/test_forward_response_model.py | 20 +- app/api/infrastructure/emis/client.py | 37 +++- app/api/infrastructure/emis/models.py | 8 +- .../infrastructure/emis/tests/test_client.py | 60 ++++-- app/api/infrastructure/tpp/client.py | 37 +++- app/api/infrastructure/tpp/models.py | 9 +- .../infrastructure/tpp/tests/test_client.py | 184 ++++++++++-------- specification/im1-pfs-auth-api.yaml | 113 +++++++---- 9 files changed, 318 insertions(+), 162 deletions(-) diff --git a/app/api/domain/forward_response_model.py b/app/api/domain/forward_response_model.py index 29aeb3c..0a82706 100644 --- a/app/api/domain/forward_response_model.py +++ b/app/api/domain/forward_response_model.py @@ -2,6 +2,10 @@ from pydantic.alias_generators import to_camel +class Permissions(BaseModel): + """A data model that encapsulates all the essential permissions data.""" + + class Demographics(BaseModel): """A data model that encapsulates all the essential demographic data.""" @@ -10,10 +14,8 @@ class Demographics(BaseModel): first_name: str surname: str title: str - - -class Permissions(BaseModel): - """A data model that encapsulates all the essential permissions data.""" + date_of_birth: str + permissions: Permissions class ForwardResponse(BaseModel): @@ -23,8 +25,8 @@ class ForwardResponse(BaseModel): session_id: str supplier: str + ods_code: str user: Demographics - permissions: Permissions patients: list[Demographics] @field_validator("patients") diff --git a/app/api/domain/tests/test_forward_response_model.py b/app/api/domain/tests/test_forward_response_model.py index ac8a68c..c307b09 100644 --- a/app/api/domain/tests/test_forward_response_model.py +++ b/app/api/domain/tests/test_forward_response_model.py @@ -11,7 +11,21 @@ def test_forward_response() -> None: ForwardResponse( sessionId="some session id", supplier="some supplier", - user=Demographics(firstName="Betty", surname="Jones", title="Ms"), - permissions=Permissions(), - patients=[Demographics(firstName="John", surname="Jones", title="Mr")], + odsCode="some ods code", + user=Demographics( + firstName="Betty", + surname="Jones", + title="Ms", + dateOfBirth="12/03/1965", + permissions=Permissions(), + ), + patients=[ + Demographics( + firstName="John", + surname="Jones", + title="Mr", + dateOfBirth="18/05/1966", + permissions=Permissions(), + ), + ], ) diff --git a/app/api/infrastructure/emis/client.py b/app/api/infrastructure/emis/client.py index edbb075..845edce 100644 --- a/app/api/infrastructure/emis/client.py +++ b/app/api/infrastructure/emis/client.py @@ -10,7 +10,6 @@ InvalidValueError, NotFoundError, ) -from app.api.domain.forward_response_model import Demographics from app.api.infrastructure.emis.models import ( EffectiveServices, Identifier, @@ -112,15 +111,25 @@ def transform_response(self, response: dict) -> SessionResponse: sessionId=response.get("SessionId"), endUserSessionId=response.get("EndUserSessionId"), supplier=self.supplier, - permissions=self._parse_permissions( - self_patient_links[0].get("EffectiveServices", {}) - if self_patient_links - else {} - ), - user=Demographics( + odsCode=self.request.patient_ods_code, + user=Patient( firstName=response.get("FirstName"), surname=response.get("Surname"), title=response.get("Title"), + dateOfBirth=self_patient_links[0].get("DateOfBirth") + if self_patient_links + else None, + userPatientLinkToken=self_patient_links[0].get("UserPatientLinkToken") + if self_patient_links + else None, + patientIdentifiers=self._parse_identifiers( + response.get("UserPatientIdentifiers", []) + ), + permissions=self._parse_permissions( + self_patient_links[0].get("EffectiveServices", {}) + if self_patient_links + else {} + ), ), patients=self._parse_patients(proxy_patient_links), ) @@ -151,6 +160,11 @@ def _parse_patients(self, patient_links: list) -> list[Patient]: firstName=patient.get("FirstName"), surname=patient.get("Surname"), title=patient.get("Title"), + dateOfBirth=patient.get("DateOfBirth"), + userPatientLinkToken=patient.get("UserPatientLinkToken"), + patientIdentifiers=self._parse_identifiers( + patient.get("PatientIdentifiers", []) + ), permissions=self._parse_permissions(raw_permissions), ) ) @@ -196,3 +210,12 @@ def _parse_permissions(self, raw_permissions: dict) -> EffectiveServices: ), ), ) + + def _parse_identifiers(self, raw_identifiers: list) -> list[Identifier]: + return [ + Identifier( + value=identifier.get("IdentifierValue"), + type=identifier.get("IdentifierType"), + ) + for identifier in raw_identifiers + ] diff --git a/app/api/infrastructure/emis/models.py b/app/api/infrastructure/emis/models.py index 13d7d30..b2cc49c 100644 --- a/app/api/infrastructure/emis/models.py +++ b/app/api/infrastructure/emis/models.py @@ -84,8 +84,12 @@ class EffectiveServices(Permissions): class Patient(Demographics): - """Base Model for Patient.""" + """Base Model for User and Patient.""" + model_config = ConfigDict(alias_generator=to_camel) + + user_patient_link_token: str + patient_identifiers: list[Identifier] permissions: EffectiveServices @@ -95,5 +99,5 @@ class SessionResponse(ForwardResponse): model_config = ConfigDict(alias_generator=to_camel) end_user_session_id: str - permissions: Permissions + user: Patient patients: list[Patient] diff --git a/app/api/infrastructure/emis/tests/test_client.py b/app/api/infrastructure/emis/tests/test_client.py index 6a4aeb1..8accad9 100644 --- a/app/api/infrastructure/emis/tests/test_client.py +++ b/app/api/infrastructure/emis/tests/test_client.py @@ -13,10 +13,10 @@ NotFoundError, ) from app.api.domain.forward_request_model import ForwardRequest -from app.api.domain.forward_response_model import Demographics from app.api.infrastructure.emis.client import EmisClient from app.api.infrastructure.emis.models import ( EffectiveServices, + Identifier, MedicalRecordPermissions, Patient, SessionResponse, @@ -139,26 +139,34 @@ def test_emis_client_transform_response(client: EmisClient) -> None: sessionId="SID_2qZ9yJpVxHq4N3b", endUserSessionId="SESS_mDq6nE2b8R7KQ0v", supplier="EMIS", - user=Demographics(firstName="Alex", surname="Taylor", title="Mr"), - permissions=EffectiveServices( - appointmentsEnabled=True, - demographicsUpdateEnabled=True, - epsEnabled=True, - medicalRecordEnabled=True, - onlineTriageEnabled=False, - practicePatientCommunicationEnabled=False, - prescribingEnabled=True, - recordSharingEnabled=False, - recordViewAuditEnabled=True, - medicalRecord=MedicalRecordPermissions( - recordAccessScheme="DetailedCodedCareRecord", - allergiesEnabled=True, - consultationsEnabled=True, - immunisationsEnabled=True, - documentsEnabled=True, - medicationEnabled=True, - problemsEnabled=True, - testResultsEnabled=True, + odsCode="some patient ods code", + user=Patient( + firstName="Alex", + surname="Taylor", + title="Mr", + dateOfBirth="1985-06-25", + userPatientLinkToken="link_self_9aLw3G7kVQ", + patientIdentifiers=[Identifier(value="9434765919", type="NhsNumber")], + permissions=EffectiveServices( + appointmentsEnabled=True, + demographicsUpdateEnabled=True, + epsEnabled=True, + medicalRecordEnabled=True, + onlineTriageEnabled=False, + practicePatientCommunicationEnabled=False, + prescribingEnabled=True, + recordSharingEnabled=False, + recordViewAuditEnabled=True, + medicalRecord=MedicalRecordPermissions( + recordAccessScheme="DetailedCodedCareRecord", + allergiesEnabled=True, + consultationsEnabled=True, + immunisationsEnabled=True, + documentsEnabled=True, + medicationEnabled=True, + problemsEnabled=True, + testResultsEnabled=True, + ), ), ), patients=[ @@ -166,6 +174,11 @@ def test_emis_client_transform_response(client: EmisClient) -> None: firstName="Jane", surname="Doe", title="Mrs", + dateOfBirth="1979-01-15", + userPatientLinkToken="link_proxy_jane_5QJw7r2m", + patientIdentifiers=[ + Identifier(value="2222222222", type="NhsNumber"), + ], permissions=EffectiveServices( appointmentsEnabled=False, demographicsUpdateEnabled=True, @@ -192,6 +205,11 @@ def test_emis_client_transform_response(client: EmisClient) -> None: firstName="Ella", surname="Taylor", title="Ms", + dateOfBirth="2010-03-02", + userPatientLinkToken="link_proxy_ella_Z01r8yPa", + patientIdentifiers=[ + Identifier(value="3333333333", type="NhsNumber"), + ], permissions=EffectiveServices( appointmentsEnabled=True, demographicsUpdateEnabled=True, diff --git a/app/api/infrastructure/tpp/client.py b/app/api/infrastructure/tpp/client.py index 3095162..4f0e7d8 100644 --- a/app/api/infrastructure/tpp/client.py +++ b/app/api/infrastructure/tpp/client.py @@ -10,7 +10,7 @@ InvalidValueError, NotFoundError, ) -from app.api.domain.forward_response_model import Demographics, ForwardResponse +from app.api.domain.forward_response_model import ForwardResponse from app.api.infrastructure.tpp.models import ( Application, Identifier, @@ -101,16 +101,24 @@ def transform_response(self, response: dict) -> ForwardResponse: response = response.get("CreateSessionReply", {}) proxy_link = response.get("User", {}) proxy_person = proxy_link.get("Person", {}) + return SessionResponse( sessionId=response.get("@suid"), supplier=self.supplier, - permissions=self._parse_permissions( - proxy_person.get("EffectiveServiceAccess", []) - ), - user=Demographics( + odsCode=self.request.patient_ods_code, + onlineUserId=proxy_link.get("@onlineUserId"), + user=Patient( firstName=proxy_person.get("PersonName", {}).get("@firstName"), surname=proxy_person.get("PersonName", {}).get("@surname"), title=proxy_person.get("PersonName", {}).get("@title"), + dateOfBirth=proxy_person.get("@dateOfBirth"), + patientId=proxy_person.get("@patientId"), + patientIdentifiers=self._parse_identifiers( + proxy_person.get("NationalIdentifiers", []) + ), + permissions=self._parse_permissions( + proxy_person.get("EffectiveServiceAccess", []) + ), ), patients=self._parse_patients(response), ) @@ -152,12 +160,17 @@ def _parse_patients(self, data: dict) -> list[Patient]: firstName=person.get("PersonName", {}).get("@firstName"), surname=person.get("PersonName", {}).get("@surname"), title=person.get("PersonName", {}).get("@title"), + dateOfBirth=person.get("@dateOfBirth"), + patientId=person.get("@patientId"), + patientIdentifiers=self._parse_identifiers( + person.get("NationalIdentifiers", []) + ), permissions=self._parse_permissions(raw_permissions), ) ) return parsed_patients - def _parse_permissions(self, raw_permissions: dict) -> list[ServiceAccess]: + def _parse_permissions(self, raw_permissions: list) -> list[ServiceAccess]: service_access = ( [permission.get("ServiceAccess", {}) for permission in raw_permissions] if isinstance(raw_permissions, list) @@ -174,3 +187,15 @@ def _parse_permissions(self, raw_permissions: dict) -> list[ServiceAccess]: ) for service in service_access ] + + def _parse_identifiers(self, raw_identifiers: list) -> list[Identifier]: + if isinstance(raw_identifiers, dict): + # if only one identifier xmltodict will not register this an array + raw_identifiers = [raw_identifiers] + return [ + Identifier( + value=identifier.get("Identifier", {}).get("@value"), + type=identifier.get("Identifier", {}).get("@type"), + ) + for identifier in raw_identifiers + ] diff --git a/app/api/infrastructure/tpp/models.py b/app/api/infrastructure/tpp/models.py index 036793b..33af3fb 100644 --- a/app/api/infrastructure/tpp/models.py +++ b/app/api/infrastructure/tpp/models.py @@ -118,8 +118,12 @@ class ServiceAccess(Permissions): class Patient(Demographics): - """Base Model for Patient.""" + """Base Model for User and Patient.""" + model_config = ConfigDict(alias_generator=to_camel) + + patient_id: str | None # Not necessary for the user in cross practice proxy roles + patient_identifiers: list[Identifier] permissions: list[ServiceAccess] @@ -128,5 +132,6 @@ class SessionResponse(ForwardResponse): model_config = ConfigDict(alias_generator=to_camel) - permissions: list[ServiceAccess] + online_user_id: str + user: Patient patients: list[Patient] diff --git a/app/api/infrastructure/tpp/tests/test_client.py b/app/api/infrastructure/tpp/tests/test_client.py index d3a1938..f5e3422 100644 --- a/app/api/infrastructure/tpp/tests/test_client.py +++ b/app/api/infrastructure/tpp/tests/test_client.py @@ -13,9 +13,9 @@ NotFoundError, ) from app.api.domain.forward_request_model import ForwardRequest -from app.api.domain.forward_response_model import Demographics from app.api.infrastructure.tpp.client import TPPClient from app.api.infrastructure.tpp.models import ( + Identifier, Patient, ServiceAccess, ServiceAccessDescription, @@ -145,90 +145,111 @@ def test_tpp_client_transform_response(client: TPPClient) -> None: assert actual_result == SessionResponse( sessionId="xhvE9/jCjdafytcXBq8LMKMgc4wA/w5k/O5C4ip0Fs9GPbIQ/WRIZi4Och1Spmg7aYJR2CZVLHfu6cRVv84aEVrRE8xahJbT4TPAr8N/CYix6TBquQsZibYXYMxJktXcYKwDhBH8yr3iJYnyevP3hV76oTjVmKieBtYzSSZAOu4=", supplier="TPP", - permissions=[ - ServiceAccess( - description=ServiceAccessDescription("Full Clinical Record"), - serviceIdentifier=1, - status=ServiceAccessStatus("U"), - statusDescription=ServiceAccessStatusDescription("Unavailable"), - ), - ServiceAccess( - serviceIdentifier=2, - description=ServiceAccessDescription("Appointments"), - status=ServiceAccessStatus("A"), - statusDescription=ServiceAccessStatusDescription("Available"), - ), - ServiceAccess( - serviceIdentifier=4, - description=ServiceAccessDescription("Request Medication"), - status=ServiceAccessStatus("A"), - statusDescription=ServiceAccessStatusDescription("Available"), - ), - ServiceAccess( - serviceIdentifier=8, - description=ServiceAccessDescription("Questionnaires"), - status=ServiceAccessStatus("N"), - statusDescription=ServiceAccessStatusDescription("Not offered by unit"), - ), - ServiceAccess( - serviceIdentifier=64, - description=ServiceAccessDescription("Summary Record"), - status=ServiceAccessStatus("A"), - statusDescription=ServiceAccessStatusDescription("Available"), - ), - ServiceAccess( - serviceIdentifier=128, - description=ServiceAccessDescription("Detailed Coded Record"), - status=ServiceAccessStatus("U"), - statusDescription=ServiceAccessStatusDescription("Unavailable"), - ), - ServiceAccess( - serviceIdentifier=512, - description=ServiceAccessDescription("Messaging"), - status=ServiceAccessStatus("A"), - statusDescription=ServiceAccessStatusDescription("Available"), - ), - ServiceAccess( - serviceIdentifier=1024, - description=ServiceAccessDescription("View Sharing Status"), - status=ServiceAccessStatus("N"), - statusDescription=ServiceAccessStatusDescription("Not offered by unit"), - ), - ServiceAccess( - serviceIdentifier=2048, - description=ServiceAccessDescription("Record Audit"), - status=ServiceAccessStatus("A"), - statusDescription=ServiceAccessStatusDescription("Available"), - ), - ServiceAccess( - serviceIdentifier=4096, - description=ServiceAccessDescription("Change Pharmacy"), - status=ServiceAccessStatus("N"), - statusDescription=ServiceAccessStatusDescription("Not offered by unit"), - ), - ServiceAccess( - serviceIdentifier=8192, - description=ServiceAccessDescription( - "Manage Sharing Rules And Requests" + odsCode="some patient ods code", + onlineUserId="9cbf400000000000", + user=Patient( + firstName="Sam", + surname="Jones", + title="Mr", + dateOfBirth="1990-11-05", + permissions=[ + ServiceAccess( + description=ServiceAccessDescription("Full Clinical Record"), + serviceIdentifier=1, + status=ServiceAccessStatus("U"), + statusDescription=ServiceAccessStatusDescription("Unavailable"), ), - status=ServiceAccessStatus("G"), - statusDescription=ServiceAccessStatusDescription( - "Only available to GMS registered patients" + ServiceAccess( + serviceIdentifier=2, + description=ServiceAccessDescription("Appointments"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), ), - ), - ServiceAccess( - serviceIdentifier=65536, - description=ServiceAccessDescription("Access SystmConnect"), - status=ServiceAccessStatus("O"), - statusDescription=ServiceAccessStatusDescription("Other"), - ), - ], - user=Demographics(firstName="Sam", surname="Jones", title="Mr"), + ServiceAccess( + serviceIdentifier=4, + description=ServiceAccessDescription("Request Medication"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), + ), + ServiceAccess( + serviceIdentifier=8, + description=ServiceAccessDescription("Questionnaires"), + status=ServiceAccessStatus("N"), + statusDescription=ServiceAccessStatusDescription( + "Not offered by unit" + ), + ), + ServiceAccess( + serviceIdentifier=64, + description=ServiceAccessDescription("Summary Record"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), + ), + ServiceAccess( + serviceIdentifier=128, + description=ServiceAccessDescription("Detailed Coded Record"), + status=ServiceAccessStatus("U"), + statusDescription=ServiceAccessStatusDescription("Unavailable"), + ), + ServiceAccess( + serviceIdentifier=512, + description=ServiceAccessDescription("Messaging"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), + ), + ServiceAccess( + serviceIdentifier=1024, + description=ServiceAccessDescription("View Sharing Status"), + status=ServiceAccessStatus("N"), + statusDescription=ServiceAccessStatusDescription( + "Not offered by unit" + ), + ), + ServiceAccess( + serviceIdentifier=2048, + description=ServiceAccessDescription("Record Audit"), + status=ServiceAccessStatus("A"), + statusDescription=ServiceAccessStatusDescription("Available"), + ), + ServiceAccess( + serviceIdentifier=4096, + description=ServiceAccessDescription("Change Pharmacy"), + status=ServiceAccessStatus("N"), + statusDescription=ServiceAccessStatusDescription( + "Not offered by unit" + ), + ), + ServiceAccess( + serviceIdentifier=8192, + description=ServiceAccessDescription( + "Manage Sharing Rules And Requests" + ), + status=ServiceAccessStatus("G"), + statusDescription=ServiceAccessStatusDescription( + "Only available to GMS registered patients" + ), + ), + ServiceAccess( + serviceIdentifier=65536, + description=ServiceAccessDescription("Access SystmConnect"), + status=ServiceAccessStatus("O"), + statusDescription=ServiceAccessStatusDescription("Other"), + ), + ], + patientId=None, + patientIdentifiers=[ + Identifier( + value="1111111111", + type="NhsNumber", + ) + ], + ), patients=[ Patient( firstName="Clare", surname="Jones", title="Mrs", + dateOfBirth="1975-04-21", permissions=[ ServiceAccess( description=ServiceAccessDescription("Full Clinical Record"), @@ -313,6 +334,13 @@ def test_tpp_client_transform_response(client: TPPClient) -> None: statusDescription=ServiceAccessStatusDescription("Other"), ), ], + patientId="82f3500000000000", + patientIdentifiers=[ + Identifier( + value="2222222222", + type="NhsNumber", + ) + ], ) ], ) diff --git a/specification/im1-pfs-auth-api.yaml b/specification/im1-pfs-auth-api.yaml index aefcf1d..4d84c7b 100644 --- a/specification/im1-pfs-auth-api.yaml +++ b/specification/im1-pfs-auth-api.yaml @@ -210,11 +210,12 @@ components: EMISResponseModel: required: - sessionId - endUserSessionId - supplier - permissions - user - patients + - endUserSessionId + - supplier + - odsCode + - permissions + - user + - patients type: object properties: sessionId: @@ -225,10 +226,10 @@ components: supplier: enum: [EMIS] type: string - permissions: - $ref: "#/components/schemas/EMISPermissionsModel" + odsCode: + type: string user: - $ref: "#/components/schemas/DemographicsModel" + $ref: "#/components/schemas/EMISPatientModel" patients: type: array items: @@ -238,10 +239,11 @@ components: TPPResponseModel: required: - sessionId - supplier - permissions - user - patients + - supplier + - odsCode + - onlineUserId + - user + - patients type: object properties: sessionId: @@ -250,10 +252,12 @@ components: supplier: enum: [TPP] type: string - permissions: - $ref: "#/components/schemas/TPPSPermissionsModel" + odsCode: + type: string + onlineUserId: + type: string user: - $ref: "#/components/schemas/DemographicsModel" + $ref: "#/components/schemas/TPPPatientModel" patients: type: array items: @@ -263,8 +267,9 @@ components: DemographicsModel: required: - firstName - surname - title + - surname + - title + - dateOfBirth type: object properties: firstName: @@ -273,29 +278,50 @@ components: type: string title: type: string + dateOfBirth: + type: string + + PatientIdentifierModel: + required: + - value + - type + type: object + properties: + value: + type: string + type: + type: string EMISPatientModel: allOf: - $ref: "#/components/schemas/DemographicsModel" - required: + - userPatientLinkToken + - patientIdentifiers - permissions type: object properties: + userPatientLinkToken: + type: string + patientIdentifiers: + type: array + items: + $ref: "#/components/schemas/PatientIdentifierModel" permissions: $ref: "#/components/schemas/EMISPermissionsModel" EMISPermissionsModel: required: - appointmentsEnabled - demographicsUpdateEnabled - epsEnabled - medicalRecordEnabled - onlineTriageEnabled - practicePatientCommunicationEnabled - prescribingEnabled - recordSharingEnabled - recordViewAuditEnabled - medicalRecord + - demographicsUpdateEnabled + - epsEnabled + - medicalRecordEnabled + - onlineTriageEnabled + - practicePatientCommunicationEnabled + - prescribingEnabled + - recordSharingEnabled + - recordViewAuditEnabled + - medicalRecord type: object properties: appointmentsEnabled: @@ -322,13 +348,13 @@ components: EMISMedicalRecordModel: required: - recordAccessScheme - allergiesEnabled - consultationsEnabled - immunisationsEnabled - documentsEnabled - medicationEnabled - problemsEnabled - testResultsEnabled + - allergiesEnabled + - consultationsEnabled + - immunisationsEnabled + - documentsEnabled + - medicationEnabled + - problemsEnabled + - testResultsEnabled type: object properties: recordAccessScheme: @@ -359,20 +385,31 @@ components: allOf: - $ref: "#/components/schemas/DemographicsModel" - required: + - patientId + - patientIdentifiers - permissions type: object properties: + patientId: + type: string + nullable: true + patientIdentifiers: + type: array + items: + $ref: "#/components/schemas/PatientIdentifierModel" permissions: - $ref: "#/components/schemas/TPPSPermissionsModel" + type: array + items: + $ref: "#/components/schemas/TPPPermissionsModel" - TPPSPermissionsModel: + TPPPermissionsModel: type: array items: required: - description - serviceIdentifier - status - statusDescription + - serviceIdentifier + - status + - statusDescription type: object properties: description: From b5ebf19ab47317919de5b3881f096a5a3355333f Mon Sep 17 00:00:00 2001 From: Tom Knapp Date: Mon, 13 Apr 2026 11:09:38 +0100 Subject: [PATCH 5/9] NPA-6546: Update End to End test happy path to include new fields --- .../end_to_end/authenticate/POST/test_201.py | 244 ++++++++++-------- 1 file changed, 140 insertions(+), 104 deletions(-) diff --git a/tests/end_to_end/authenticate/POST/test_201.py b/tests/end_to_end/authenticate/POST/test_201.py index b9f577b..7ad46a8 100644 --- a/tests/end_to_end/authenticate/POST/test_201.py +++ b/tests/end_to_end/authenticate/POST/test_201.py @@ -18,11 +18,51 @@ ( "https://nhs70apptest.emishealth.com", { + "sessionId": "SID_2qZ9yJpVxHq4N3b", + "endUserSessionId": "SESS_mDq6nE2b8R7KQ0v", + "supplier": "EMIS", + "odsCode": "ODS123", + "user": { + "firstName": "Alex", + "surname": "Taylor", + "title": "Mr", + "dateOfBirth": "1985-06-25", + "userPatientLinkToken": "link_self_9aLw3G7kVQ", + "patientIdentifiers": [ + {"value": "9434765919", "type": "NhsNumber"}, + ], + "permissions": { + "appointmentsEnabled": True, + "demographicsUpdateEnabled": True, + "epsEnabled": True, + "medicalRecordEnabled": True, + "onlineTriageEnabled": False, + "practicePatientCommunicationEnabled": False, + "prescribingEnabled": True, + "recordSharingEnabled": False, + "recordViewAuditEnabled": True, + "medicalRecord": { + "recordAccessScheme": "DetailedCodedCareRecord", + "allergiesEnabled": True, + "consultationsEnabled": True, + "immunisationsEnabled": True, + "documentsEnabled": True, + "medicationEnabled": True, + "problemsEnabled": True, + "testResultsEnabled": True, + }, + }, + }, "patients": [ { "firstName": "Jane", "surname": "Doe", "title": "Mrs", + "dateOfBirth": "1979-01-15", + "userPatientLinkToken": "link_proxy_jane_5QJw7r2m", + "patientIdentifiers": [ + {"value": "2222222222", "type": "NhsNumber"}, + ], "permissions": { "appointmentsEnabled": False, "demographicsUpdateEnabled": True, @@ -49,6 +89,11 @@ "firstName": "Ella", "surname": "Taylor", "title": "Ms", + "dateOfBirth": "2010-03-02", + "userPatientLinkToken": "link_proxy_ella_Z01r8yPa", + "patientIdentifiers": [ + {"value": "3333333333", "type": "NhsNumber"}, + ], "permissions": { "appointmentsEnabled": True, "demographicsUpdateEnabled": True, @@ -72,41 +117,111 @@ }, }, ], - "user": {"firstName": "Alex", "surname": "Taylor", "title": "Mr"}, - "permissions": { - "appointmentsEnabled": True, - "demographicsUpdateEnabled": True, - "epsEnabled": True, - "medicalRecordEnabled": True, - "onlineTriageEnabled": False, - "practicePatientCommunicationEnabled": False, - "prescribingEnabled": True, - "recordSharingEnabled": False, - "recordViewAuditEnabled": True, - "medicalRecord": { - "recordAccessScheme": "DetailedCodedCareRecord", - "allergiesEnabled": True, - "consultationsEnabled": True, - "immunisationsEnabled": True, - "documentsEnabled": True, - "medicationEnabled": True, - "problemsEnabled": True, - "testResultsEnabled": True, - }, - }, - "sessionId": "SID_2qZ9yJpVxHq4N3b", - "endUserSessionId": "SESS_mDq6nE2b8R7KQ0v", - "supplier": "EMIS", }, ), ( "https://systmonline2.tpp-uk.com", { + "sessionId": "xhvE9/jCjdafytcXBq8LMKMgc4wA/w5k/O5C4ip0Fs9GPbIQ/WRIZi4Och1Spmg7aYJR2CZVLHfu6cRVv84aEVrRE8xahJbT4TPAr8N/CYix6TBquQsZibYXYMxJktXcYKwDhBH8yr3iJYnyevP3hV76oTjVmKieBtYzSSZAOu4=", # noqa: E501 + "supplier": "TPP", + "odsCode": "ODS123", + "onlineUserId": "9cbf400000000000", + "user": { + "firstName": "Sam", + "surname": "Jones", + "title": "Mr", + "dateOfBirth": "1990-11-05", + "patientId": None, + "patientIdentifiers": [ + {"value": "1111111111", "type": "NhsNumber"}, + ], + "permissions": [ + { + "description": "Full Clinical Record", + "statusDescription": "Unavailable", + "serviceIdentifier": 1, + "status": "U", + }, + { + "serviceIdentifier": 2, + "statusDescription": "Available", + "description": "Appointments", + "status": "A", + }, + { + "serviceIdentifier": 4, + "statusDescription": "Available", + "description": "Request Medication", + "status": "A", + }, + { + "serviceIdentifier": 8, + "description": "Questionnaires", + "status": "N", + "statusDescription": "Not offered by unit", + }, + { + "serviceIdentifier": 64, + "statusDescription": "Available", + "description": "Summary Record", + "status": "A", + }, + { + "serviceIdentifier": 128, + "statusDescription": "Unavailable", + "description": "Detailed Coded Record", + "status": "U", + }, + { + "serviceIdentifier": 512, + "statusDescription": "Available", + "description": "Messaging", + "status": "A", + }, + { + "serviceIdentifier": 1024, + "statusDescription": "Not offered by unit", + "description": "View Sharing Status", + "status": "N", + }, + { + "serviceIdentifier": 2048, + "statusDescription": "Available", + "description": "Record Audit", + "status": "A", + }, + { + "serviceIdentifier": 4096, + "statusDescription": "Not offered by unit", + "description": "Change Pharmacy", + "status": "N", + }, + { + "serviceIdentifier": 8192, + "statusDescription": ( + "Only available to GMS registered patients" + ), + "description": "Manage Sharing Rules And Requests", + "status": "G", + }, + { + "serviceIdentifier": 65536, + "statusDescription": "Other", + "description": "Access SystmConnect", + "status": "O", + }, + ], + }, "patients": [ { "firstName": "Clare", "surname": "Jones", "title": "Mrs", + "dateOfBirth": "1975-04-21", + "patientId": "82f3500000000000", + "patientIdentifiers": [ + {"value": "2222222222", "type": "NhsNumber"}, + ], "permissions": [ { "description": "Full Clinical Record", @@ -185,85 +300,6 @@ ], }, ], - "user": {"firstName": "Sam", "surname": "Jones", "title": "Mr"}, - "permissions": [ - { - "description": "Full Clinical Record", - "statusDescription": "Unavailable", - "serviceIdentifier": 1, - "status": "U", - }, - { - "serviceIdentifier": 2, - "statusDescription": "Available", - "description": "Appointments", - "status": "A", - }, - { - "serviceIdentifier": 4, - "statusDescription": "Available", - "description": "Request Medication", - "status": "A", - }, - { - "serviceIdentifier": 8, - "description": "Questionnaires", - "status": "N", - "statusDescription": "Not offered by unit", - }, - { - "serviceIdentifier": 64, - "statusDescription": "Available", - "description": "Summary Record", - "status": "A", - }, - { - "serviceIdentifier": 128, - "statusDescription": "Unavailable", - "description": "Detailed Coded Record", - "status": "U", - }, - { - "serviceIdentifier": 512, - "statusDescription": "Available", - "description": "Messaging", - "status": "A", - }, - { - "serviceIdentifier": 1024, - "statusDescription": "Not offered by unit", - "description": "View Sharing Status", - "status": "N", - }, - { - "serviceIdentifier": 2048, - "statusDescription": "Available", - "description": "Record Audit", - "status": "A", - }, - { - "serviceIdentifier": 4096, - "statusDescription": "Not offered by unit", - "description": "Change Pharmacy", - "status": "N", - }, - { - "serviceIdentifier": 8192, - "statusDescription": ( - "Only available to GMS registered patients" - ), - "description": "Manage Sharing Rules And Requests", - "status": "G", - }, - { - "serviceIdentifier": 65536, - "statusDescription": "Other", - "description": "Access SystmConnect", - "status": "O", - }, - ], - "sessionId": "xhvE9/jCjdafytcXBq8LMKMgc4wA/w5k/O5C4ip0Fs9GPbIQ/WRIZi4Och1Spmg7aYJR2CZVLHfu6cRVv84aEVrRE8xahJbT4TPAr8N/CYix6TBquQsZibYXYMxJktXcYKwDhBH8yr3iJYnyevP3hV76oTjVmKieBtYzSSZAOu4=", # noqa: E501 - "supplier": "TPP", }, ), ], From 9a49ede6dddd15a32c30678724d0b91f40474b40 Mon Sep 17 00:00:00 2001 From: Tom Knapp Date: Mon, 13 Apr 2026 11:14:00 +0100 Subject: [PATCH 6/9] NPA-6546: Remove permissions from EMISResponseModel, now in user/patient model --- specification/im1-pfs-auth-api.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/specification/im1-pfs-auth-api.yaml b/specification/im1-pfs-auth-api.yaml index 4d84c7b..3b30c99 100644 --- a/specification/im1-pfs-auth-api.yaml +++ b/specification/im1-pfs-auth-api.yaml @@ -213,7 +213,6 @@ components: - endUserSessionId - supplier - odsCode - - permissions - user - patients type: object From 4fe29b5ad599c2828fb119f71f466dca5b2cf53e Mon Sep 17 00:00:00 2001 From: Tom Knapp Date: Mon, 13 Apr 2026 11:21:05 +0100 Subject: [PATCH 7/9] NPA-6546: TPP Permissions model define as object --- specification/im1-pfs-auth-api.yaml | 80 ++++++++++++++--------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/specification/im1-pfs-auth-api.yaml b/specification/im1-pfs-auth-api.yaml index 3b30c99..301a82e 100644 --- a/specification/im1-pfs-auth-api.yaml +++ b/specification/im1-pfs-auth-api.yaml @@ -402,47 +402,45 @@ components: $ref: "#/components/schemas/TPPPermissionsModel" TPPPermissionsModel: - type: array - items: - required: - - description - - serviceIdentifier - - status - - statusDescription - type: object - properties: - description: - type: string - enum: - [ - "Full Clinical Record", - "Appointments", - "Request Medication", - "Questionnaires", - "Summary Record", - "Detailed Coded Record", - "Messaging", - "View Sharing Status", - "Record Audit", - "Change Pharmacy", - "Manage Sharing Rules And Requests", - "Access SystmConnect", - ] - serviceIdentifier: - type: integer - status: - type: string - enum: ["A", "U", "N", "G", "O"] - statusDescription: - type: string - enum: - [ - "Available", - "Unavailable", - "Not offered by unit", - "Only available to GMS registered patients", - "Other", - ] + required: + - description + - serviceIdentifier + - status + - statusDescription + type: object + properties: + description: + type: string + enum: + [ + "Full Clinical Record", + "Appointments", + "Request Medication", + "Questionnaires", + "Summary Record", + "Detailed Coded Record", + "Messaging", + "View Sharing Status", + "Record Audit", + "Change Pharmacy", + "Manage Sharing Rules And Requests", + "Access SystmConnect", + ] + serviceIdentifier: + type: integer + status: + type: string + enum: ["A", "U", "N", "G", "O"] + statusDescription: + type: string + enum: + [ + "Available", + "Unavailable", + "Not offered by unit", + "Only available to GMS registered patients", + "Other", + ] OperationOutcome: type: object From 8ec801693696e802fd5894d81bbfc6c4884ab169 Mon Sep 17 00:00:00 2001 From: Tom Knapp Date: Mon, 13 Apr 2026 11:55:14 +0100 Subject: [PATCH 8/9] NPA-6546: tpp client fix handling for xmltodict singleton object handling --- app/api/infrastructure/tpp/client.py | 42 ++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/app/api/infrastructure/tpp/client.py b/app/api/infrastructure/tpp/client.py index 4f0e7d8..8fd42df 100644 --- a/app/api/infrastructure/tpp/client.py +++ b/app/api/infrastructure/tpp/client.py @@ -170,12 +170,21 @@ def _parse_patients(self, data: dict) -> list[Patient]: ) return parsed_patients - def _parse_permissions(self, raw_permissions: list) -> list[ServiceAccess]: + def _parse_permissions(self, raw_permissions: dict | list) -> list[ServiceAccess]: + if not raw_permissions: + return [] + # xmltodict gives us {"ServiceAccess": {...}} for one element and + # {"ServiceAccess": [{...}, {...}]} for multiple — extract the inner value first service_access = ( - [permission.get("ServiceAccess", {}) for permission in raw_permissions] - if isinstance(raw_permissions, list) - else raw_permissions.get("ServiceAccess", {}) + raw_permissions.get("ServiceAccess") + if isinstance(raw_permissions, dict) + else [] ) + if not service_access: + return [] + if isinstance(service_access, dict): + # Single element — normalise to a list + service_access = [service_access] return [ ServiceAccess( description=ServiceAccessDescription(service["@description"]), @@ -188,14 +197,25 @@ def _parse_permissions(self, raw_permissions: list) -> list[ServiceAccess]: for service in service_access ] - def _parse_identifiers(self, raw_identifiers: list) -> list[Identifier]: - if isinstance(raw_identifiers, dict): - # if only one identifier xmltodict will not register this an array - raw_identifiers = [raw_identifiers] + def _parse_identifiers(self, raw_identifiers: dict | list) -> list[Identifier]: + if not raw_identifiers: + return [] + # xmltodict gives us {"Identifier": {...}} for one element and + # {"Identifier": [{...}, {...}]} for multiple — extract the inner value first + identifiers = ( + raw_identifiers.get("Identifier") + if isinstance(raw_identifiers, dict) + else [] + ) + if not identifiers: + return [] + if isinstance(identifiers, dict): + # Single element — normalise to a list + identifiers = [identifiers] return [ Identifier( - value=identifier.get("Identifier", {}).get("@value"), - type=identifier.get("Identifier", {}).get("@type"), + value=identifier.get("@value"), + type=identifier.get("@type"), ) - for identifier in raw_identifiers + for identifier in identifiers ] From 6f645cc8cd603e014ac2aded76046fd9c29422fe Mon Sep 17 00:00:00 2001 From: Tom Knapp Date: Mon, 13 Apr 2026 16:51:47 +0100 Subject: [PATCH 9/9] NPA-6546: Rename Patient to Person to cover users and patients --- app/api/infrastructure/emis/client.py | 20 +++++++------ app/api/infrastructure/tpp/client.py | 28 +++++++++---------- app/api/infrastructure/tpp/models.py | 6 ++-- .../infrastructure/tpp/tests/test_client.py | 6 ++-- specification/im1-pfs-auth-api.yaml | 12 ++++---- 5 files changed, 37 insertions(+), 35 deletions(-) diff --git a/app/api/infrastructure/emis/client.py b/app/api/infrastructure/emis/client.py index 845edce..c4011e9 100644 --- a/app/api/infrastructure/emis/client.py +++ b/app/api/infrastructure/emis/client.py @@ -95,13 +95,15 @@ def transform_response(self, response: dict) -> SessionResponse: Returns: SessionResponse: Homogenised response with other clients """ - self_patient_links = [ + # UserPatientLinks relating the user to their patient details + user_self_links = [ patient_link for patient_link in response.get("UserPatientLinks", []) if patient_link.get("AssociationType") == "Self" ] - proxy_patient_links = [ + # UserPatientLinks relating the user to patients they can act on behalf of + user_patient_links = [ patient_link for patient_link in response.get("UserPatientLinks", []) if patient_link.get("AssociationType") == "Proxy" @@ -116,22 +118,22 @@ def transform_response(self, response: dict) -> SessionResponse: firstName=response.get("FirstName"), surname=response.get("Surname"), title=response.get("Title"), - dateOfBirth=self_patient_links[0].get("DateOfBirth") - if self_patient_links + dateOfBirth=user_self_links[0].get("DateOfBirth") + if user_self_links else None, - userPatientLinkToken=self_patient_links[0].get("UserPatientLinkToken") - if self_patient_links + userPatientLinkToken=user_self_links[0].get("UserPatientLinkToken") + if user_self_links else None, patientIdentifiers=self._parse_identifiers( response.get("UserPatientIdentifiers", []) ), permissions=self._parse_permissions( - self_patient_links[0].get("EffectiveServices", {}) - if self_patient_links + user_self_links[0].get("EffectiveServices", {}) + if user_self_links else {} ), ), - patients=self._parse_patients(proxy_patient_links), + patients=self._parse_patients(user_patient_links), ) def _mock_response(self) -> dict: diff --git a/app/api/infrastructure/tpp/client.py b/app/api/infrastructure/tpp/client.py index 8fd42df..88f6760 100644 --- a/app/api/infrastructure/tpp/client.py +++ b/app/api/infrastructure/tpp/client.py @@ -14,7 +14,7 @@ from app.api.infrastructure.tpp.models import ( Application, Identifier, - Patient, + Person, ServiceAccess, ServiceAccessDescription, ServiceAccessStatus, @@ -99,25 +99,25 @@ def transform_response(self, response: dict) -> ForwardResponse: ForwardResponse: Homogenised response with other clients """ response = response.get("CreateSessionReply", {}) - proxy_link = response.get("User", {}) - proxy_person = proxy_link.get("Person", {}) + user_link = response.get("User", {}) + user_person = user_link.get("Person", {}) return SessionResponse( sessionId=response.get("@suid"), supplier=self.supplier, odsCode=self.request.patient_ods_code, - onlineUserId=proxy_link.get("@onlineUserId"), - user=Patient( - firstName=proxy_person.get("PersonName", {}).get("@firstName"), - surname=proxy_person.get("PersonName", {}).get("@surname"), - title=proxy_person.get("PersonName", {}).get("@title"), - dateOfBirth=proxy_person.get("@dateOfBirth"), - patientId=proxy_person.get("@patientId"), + onlineUserId=user_link.get("@onlineUserId"), + user=Person( + firstName=user_person.get("PersonName", {}).get("@firstName"), + surname=user_person.get("PersonName", {}).get("@surname"), + title=user_person.get("PersonName", {}).get("@title"), + dateOfBirth=user_person.get("@dateOfBirth"), + patientId=user_person.get("@patientId"), patientIdentifiers=self._parse_identifiers( - proxy_person.get("NationalIdentifiers", []) + user_person.get("NationalIdentifiers", []) ), permissions=self._parse_permissions( - proxy_person.get("EffectiveServiceAccess", []) + user_person.get("EffectiveServiceAccess", []) ), ), patients=self._parse_patients(response), @@ -135,7 +135,7 @@ def _mock_response(self) -> dict: mocked_response = f.read() return xmltodict.parse(mocked_response) - def _parse_patients(self, data: dict) -> list[Patient]: + def _parse_patients(self, data: dict) -> list[Person]: """Parsing raw data from Client into structual model. Args: @@ -156,7 +156,7 @@ def _parse_patients(self, data: dict) -> list[Patient]: person = patient["Person"] raw_permissions = person.get("EffectiveServiceAccess", []) parsed_patients.append( - Patient( + Person( firstName=person.get("PersonName", {}).get("@firstName"), surname=person.get("PersonName", {}).get("@surname"), title=person.get("PersonName", {}).get("@title"), diff --git a/app/api/infrastructure/tpp/models.py b/app/api/infrastructure/tpp/models.py index 33af3fb..c982ad2 100644 --- a/app/api/infrastructure/tpp/models.py +++ b/app/api/infrastructure/tpp/models.py @@ -117,7 +117,7 @@ class ServiceAccess(Permissions): status_description: ServiceAccessStatusDescription -class Patient(Demographics): +class Person(Demographics): """Base Model for User and Patient.""" model_config = ConfigDict(alias_generator=to_camel) @@ -133,5 +133,5 @@ class SessionResponse(ForwardResponse): model_config = ConfigDict(alias_generator=to_camel) online_user_id: str - user: Patient - patients: list[Patient] + user: Person + patients: list[Person] diff --git a/app/api/infrastructure/tpp/tests/test_client.py b/app/api/infrastructure/tpp/tests/test_client.py index f5e3422..88db7d7 100644 --- a/app/api/infrastructure/tpp/tests/test_client.py +++ b/app/api/infrastructure/tpp/tests/test_client.py @@ -16,7 +16,7 @@ from app.api.infrastructure.tpp.client import TPPClient from app.api.infrastructure.tpp.models import ( Identifier, - Patient, + Person, ServiceAccess, ServiceAccessDescription, ServiceAccessStatus, @@ -147,7 +147,7 @@ def test_tpp_client_transform_response(client: TPPClient) -> None: supplier="TPP", odsCode="some patient ods code", onlineUserId="9cbf400000000000", - user=Patient( + user=Person( firstName="Sam", surname="Jones", title="Mr", @@ -245,7 +245,7 @@ def test_tpp_client_transform_response(client: TPPClient) -> None: ], ), patients=[ - Patient( + Person( firstName="Clare", surname="Jones", title="Mrs", diff --git a/specification/im1-pfs-auth-api.yaml b/specification/im1-pfs-auth-api.yaml index 301a82e..894d255 100644 --- a/specification/im1-pfs-auth-api.yaml +++ b/specification/im1-pfs-auth-api.yaml @@ -228,11 +228,11 @@ components: odsCode: type: string user: - $ref: "#/components/schemas/EMISPatientModel" + $ref: "#/components/schemas/EMISPersonModel" patients: type: array items: - $ref: "#/components/schemas/EMISPatientModel" + $ref: "#/components/schemas/EMISPersonModel" additionalProperties: false TPPResponseModel: @@ -256,11 +256,11 @@ components: onlineUserId: type: string user: - $ref: "#/components/schemas/TPPPatientModel" + $ref: "#/components/schemas/TPPPersonModel" patients: type: array items: - $ref: "#/components/schemas/TPPPatientModel" + $ref: "#/components/schemas/TPPPersonModel" additionalProperties: false DemographicsModel: @@ -291,7 +291,7 @@ components: type: type: string - EMISPatientModel: + EMISPersonModel: allOf: - $ref: "#/components/schemas/DemographicsModel" - required: @@ -380,7 +380,7 @@ components: testResultsEnabled: type: boolean - TPPPatientModel: + TPPPersonModel: allOf: - $ref: "#/components/schemas/DemographicsModel" - required: