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: