diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index cba5a14e..d913736d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -24,9 +24,9 @@ https://nhsd-jira.digital.nhs.uk/browse/NPA-XXXX -- [ ] -- [ ] -- [ ] +- +- +- --- diff --git a/postman/Validate Relationship Service Sandbox.postman_collection.json b/postman/Validate Relationship Service Sandbox.postman_collection.json index 4a240a9a..4e6acc84 100644 --- a/postman/Validate Relationship Service Sandbox.postman_collection.json +++ b/postman/Validate Relationship Service Sandbox.postman_collection.json @@ -578,6 +578,73 @@ }, "response": [] }, + { + "name": "Duplicate relationship", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const expectedResponseBody = {", + " \"issue\": [", + " {", + " \"code\": \"invalid\",", + " \"diagnostics\": \"Proxy role already exists.\",", + " \"details\": {", + " \"coding\": [", + " {", + " \"code\": \"DUPLICATE_RELATIONSHIP\",", + " \"display\": \"Request must be for a new proxy role.\",", + " \"system\": \"https://fhir.nhs.uk/R4/CodeSystem/ValidatedRelationships-ErrorOrWarningCode\",", + " \"version\": \"1\"", + " }", + " ]", + " },", + " \"severity\": \"error\"", + " }", + " ],", + " \"resourceType\": \"OperationOutcome\"", + "}", + "", + "pm.test(\"Status code is 409\", function () {", + " pm.response.to.have.status(409);", + "});", + "", + "pm.test(\"Should have correct response body\", () => {", + " var responseJson = pm.response.json();", + " pm.expect(responseJson).to.eql(expectedResponseBody);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"resourceType\": \"QuestionnaireResponse\",\n \"status\": \"completed\",\n \"authored\": \"2024-07-15T09:43:03.280Z\",\n \"source\": {\n \"type\": \"RelatedPerson\",\n \"identifier\": {\n \"system\": \"https://fhir.nhs.uk/Id/nhs-number\",\n \"value\": \"9000000049\"\n }\n },\n \"subject\": {\n \"type\": \"Patient\",\n \"identifier\": {\n \"system\": \"https://fhir.nhs.uk/Id/nhs-number\",\n \"value\": \"9000000006\"\n }\n },\n \"questionnaire\": \"https://api.service.nhs.uk/validated-relationships/FHIR/R4/Questionnaire/01dc6813-3421-4d14-948d-a4888241add1\",\n \"item\": [\n {\n \"linkId\": \"relatedPerson\",\n \"text\": \"Proxy details\",\n \"item\": [\n {\n \"linkId\": \"relatedPerson_identifier\",\n \"text\": \"NHS number\",\n \"answer\": [\n {\n \"valueString\": \"9000000049\"\n }\n ]\n },\n {\n \"linkId\": \"relatedPerson_basisForAccess\",\n \"text\": \"Basis for Access\",\n \"answer\": [\n {\n \"valueCoding\": {\n \"system\": \"https://fhir.hl7.org.uk/CodeSystem/UKCore-AdditionalPersonRelationshipRole\",\n \"code\": \"Personal\",\n \"display\": \"Personal relationship with the patient\"\n }\n }\n ]\n },\n {\n \"linkId\": \"relatedPerson_relationship\",\n \"text\": \"Relationship\",\n \"answer\": [\n {\n \"valueCoding\": {\n \"system\": \"http://terminology.hl7.org/CodeSystem/v3-RoleCode\",\n \"code\": \"SPS\",\n \"display\": \"Spouse\"\n }\n }\n ]\n }\n ]\n },\n {\n \"linkId\": \"patient\",\n \"text\": \"Patient details\",\n \"item\": [\n {\n \"linkId\": \"patient_identifier\",\n \"text\": \"NHS number\",\n \"answer\": [\n {\n \"valueString\": \"9000000006\"\n }\n ]\n },\n {\n \"linkId\": \"patient_name\",\n \"text\": \"Name\",\n \"item\": [\n {\n \"linkId\": \"patient_name_first\",\n \"text\": \"First name\",\n \"answer\": [\n {\n \"valueString\": \"Jill\"\n }\n ]\n },\n {\n \"linkId\": \"patient_name_family\",\n \"text\": \"Last name\",\n \"answer\": [\n {\n \"valueString\": \"Jones\"\n }\n ]\n }\n ]\n },\n {\n \"linkId\": \"patient_birthDate\",\n \"text\": \"Date of birth\",\n \"answer\": [\n {\n \"valueDate\": \"1965-01-01\"\n }\n ]\n }\n ]\n },\n {\n \"linkId\": \"requestedAccess\",\n \"text\": \"Requested access\",\n \"item\": [\n {\n \"linkId\": \"requestedAccess_accessLevel\",\n \"text\": \"Requested access level\",\n \"answer\": [\n {\n \"valueCoding\": {\n \"system\": \"https://fhir.nhs.uk/CodeSystem/Proxy-Placeholder-RequestedAccess\",\n \"code\": \"APPT\",\n \"display\": \"Appointment Booking\"\n }\n },\n {\n \"valueCoding\": {\n \"system\": \"https://fhir.nhs.uk/CodeSystem/Proxy-Placeholder-RequestedAccess\",\n \"code\": \"VACC\",\n \"display\": \"Vaccination Records\"\n }\n }\n ]\n },\n {\n \"linkId\": \"requestedAccess_reasonsForAccess\",\n \"text\": \"Reason for access\",\n \"answer\": [\n {\n \"valueCoding\": {\n \"system\": \"https://fhir.nhs.uk/CodeSystem/Proxy-Placeholder-ReasonForAccess\",\n \"code\": \"PRAC\",\n \"display\": \"Practical Reasons\"\n }\n }\n ]\n }\n ]\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{api_base_url}}/QuestionnaireResponse", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "QuestionnaireResponse" + ] + } + }, + "response": [] + }, { "name": "Adult to adult without ability to consent access request", "event": [ @@ -10183,4 +10250,4 @@ "type": "string" } ] -} \ No newline at end of file +} diff --git a/sandbox/api/constants.py b/sandbox/api/constants.py index 21094a2c..7467d933 100644 --- a/sandbox/api/constants.py +++ b/sandbox/api/constants.py @@ -93,6 +93,9 @@ # POST QuestionnaireResponse POST_QUESTIONNAIRE_RESPONSE_DIRECTORY = "./api/examples/POST_QuestionnaireResponse/" POST_QUESTIONNAIRE_RESPONSE__SUCCESS = f"{POST_QUESTIONNAIRE_RESPONSE_DIRECTORY}success.yaml" +POST_QUESTIONNAIRE_RESPONSE__DUPLICATE_RELATIONSHIP_ERROR = ( + f"{POST_QUESTIONNAIRE_RESPONSE_DIRECTORY}errors/duplicate_relationship_error.yaml" +) # GET QuestionnaireResponse GET_QUESTIONNAIRE_RESPONSE_DIRECTORY = "./api/examples/GET_QuestionnaireResponse/" diff --git a/sandbox/api/post_questionnaire_response.py b/sandbox/api/post_questionnaire_response.py index fbd955c4..fb08d136 100644 --- a/sandbox/api/post_questionnaire_response.py +++ b/sandbox/api/post_questionnaire_response.py @@ -1,7 +1,13 @@ from logging import INFO, basicConfig, getLogger from typing import Union -from .constants import INTERNAL_SERVER_ERROR_EXAMPLE, POST_QUESTIONNAIRE_RESPONSE__SUCCESS +from flask import request + +from .constants import ( + INTERNAL_SERVER_ERROR_EXAMPLE, + POST_QUESTIONNAIRE_RESPONSE__SUCCESS, + POST_QUESTIONNAIRE_RESPONSE__DUPLICATE_RELATIONSHIP_ERROR, +) from .utils import generate_response_from_example basicConfig(level=INFO, format="%(asctime)s - %(message)s") @@ -15,7 +21,24 @@ def post_questionnaire_response_response() -> Union[dict, tuple]: Union[dict, tuple]: Response for POST /QuestionnaireResponse """ try: - return generate_response_from_example(POST_QUESTIONNAIRE_RESPONSE__SUCCESS, 200) + logger.debug("Received request to POST questionnaire response") + # Validate body - beyond the scope of sandbox - assume body is valid for scenario + json = request.get_json() + source_identifier = json.get("source", {}).get("identifier", {}).get("value") + response = None + + # Successful questionnaire response + if source_identifier in ["9000000009", "9000000017"]: + response = generate_response_from_example(POST_QUESTIONNAIRE_RESPONSE__SUCCESS, 200) + # Duplicate relationship + elif source_identifier == "9000000049": + response = generate_response_from_example(POST_QUESTIONNAIRE_RESPONSE__DUPLICATE_RELATIONSHIP_ERROR, 409) + else: + # Out of scope errors + raise ValueError("Invalid Request") + + return response + except Exception: logger.exception("POST questionnaire response failed") return generate_response_from_example(INTERNAL_SERVER_ERROR_EXAMPLE, 500) diff --git a/sandbox/api/tests/test_post_questionnaire_response.py b/sandbox/api/tests/test_post_questionnaire_response.py index 5bd80ded..6cef14be 100644 --- a/sandbox/api/tests/test_post_questionnaire_response.py +++ b/sandbox/api/tests/test_post_questionnaire_response.py @@ -4,36 +4,58 @@ import pytest from flask import Response +from sandbox.api.constants import ( + POST_QUESTIONNAIRE_RESPONSE__SUCCESS, + POST_QUESTIONNAIRE_RESPONSE__DUPLICATE_RELATIONSHIP_ERROR, + INTERNAL_SERVER_ERROR_EXAMPLE, +) + QUESTIONNAIRE_RESPONSE_API_ENDPOINT = "/FHIR/R4/QuestionnaireResponse" @pytest.mark.parametrize( - ("url_path", "response_file_name", "status_code"), + ("nhs_num", "response_file_name", "status_code"), [ ( - QUESTIONNAIRE_RESPONSE_API_ENDPOINT, - "./api/examples/POST_QuestionnaireResponse/success.yaml", + "9000000009", + POST_QUESTIONNAIRE_RESPONSE__SUCCESS, + 200, + ), + ( + "9000000017", + POST_QUESTIONNAIRE_RESPONSE__SUCCESS, 200, ), + ( + "9000000049", + POST_QUESTIONNAIRE_RESPONSE__DUPLICATE_RELATIONSHIP_ERROR, + 409, + ), + ( + "INVALID_NHS_NUMBER", + INTERNAL_SERVER_ERROR_EXAMPLE, + 500, + ), ], ) @patch("sandbox.api.post_questionnaire_response.generate_response_from_example") def test_post_questionnaire_response( mock_generate_response_from_example: MagicMock, - url_path: str, + nhs_num: str, response_file_name: str, - client: object, status_code: int, + client: object, ) -> None: - """Test related_persons endpoint with identifier only.""" + """Test POST QuestionnaireResponse endpoint with different scenarios.""" # Arrange mock_generate_response_from_example.return_value = mocked_response = Response( dumps({"data": "mocked"}), status=status_code, content_type="application/json", ) + json = {"resourceType": "QuestionnaireResponse", "source": {"identifier": {"value": nhs_num}}} # Act - response = client.post(url_path, json={"data": "mocked"}) + response = client.post(QUESTIONNAIRE_RESPONSE_API_ENDPOINT, json=json) # Assert mock_generate_response_from_example.assert_called_once_with(response_file_name, status_code) assert response.status_code == status_code diff --git a/specification/examples/requests/POST_QuestionnaireResponse/duplicate_relationship.yaml b/specification/examples/requests/POST_QuestionnaireResponse/duplicate_relationship.yaml new file mode 100644 index 00000000..901c9f52 --- /dev/null +++ b/specification/examples/requests/POST_QuestionnaireResponse/duplicate_relationship.yaml @@ -0,0 +1,92 @@ +QuestionnaireResponseDuplicateRelationship: + summary: Duplicate relationship request + description: | + Example proxy access request that triggers a 409 DUPLICATE_RELATIONSHIP error due to an existing proxy role with NHS number `9000000049` requesting access to act on behalf of a patient (Jill Jones) with NHS number `9000000006`. + + Significant details to point out: + + - `source.type` should be `RelatedPerson` when a proxy is applying + - `source.identifier.value` should be the NHS number of the user completing the form - this should correlate with the Identity token in the request + - `subject.type` should be `Patient` since it is the patient that is the subject of the application + - `subject.identifier.value` should be the NHS Number of the patient to which the application relates + - `patient` demographics are present in the request as a result of being provided by the applicant + value: + resourceType: "QuestionnaireResponse" + status: "completed" + authored: "2024-07-15T09:43:03.280Z" + source: + type: "RelatedPerson" + identifier: + system: "https://fhir.nhs.uk/Id/nhs-number" + value: "9000000049" + subject: + type: "Patient" + identifier: + system: "https://fhir.nhs.uk/Id/nhs-number" + value: "9000000006" + questionnaire: "https://api.service.nhs.uk/validated-relationships/FHIR/R4/Questionnaire/01dc6813-3421-4d14-948d-a4888241add1" + item: + - linkId: "relatedPerson" + text: "Proxy details" + item: + - linkId: "relatedPerson_identifier" + text: "NHS number" + answer: + - valueString: "9000000049" + - linkId: "relatedPerson_basisForAccess" + text: "Basis for Access" + answer: + - valueCoding: + system: "https://fhir.hl7.org.uk/CodeSystem/UKCore-AdditionalPersonRelationshipRole" + code: "Personal" + display: "Personal relationship with the patient" + - linkId: "relatedPerson_relationship" + text: "Relationship" + answer: + - valueCoding: + system: "http://terminology.hl7.org/CodeSystem/v3-RoleCode" + code: "SPS" + display: "Spouse" + - linkId: "patient" + text: "Patient details" + item: + - linkId: "patient_identifier" + text: "NHS number" + answer: + - valueString: "9000000006" + - linkId: "patient_name" + text: "Name" + item: + - linkId: "patient_name_first" + text: "First name" + answer: + - valueString: "Jill" + - linkId: "patient_name_family" + text: "Last name" + answer: + - valueString: "Jones" + - linkId: "patient_birthDate" + text: "Date of birth" + answer: + - valueDate: "1965-01-01" + - linkId: "requestedAccess" + text: "Requested access" + item: + - linkId: "requestedAccess_accessLevel" + text: "Requested access level" + answer: + - valueCoding: + system: "https://fhir.nhs.uk/CodeSystem/Proxy-Placeholder-RequestedAccess" + code: "APPT" + display: "Appointment Booking" + - valueCoding: + system: "https://fhir.nhs.uk/CodeSystem/Proxy-Placeholder-RequestedAccess" + code: "VACC" + display: "Vaccination Records" + - linkId: "requestedAccess_reasonsForAccess" + text: "Reason for access" + answer: + - valueCoding: + system: "https://fhir.nhs.uk/CodeSystem/Proxy-Placeholder-ReasonForAccess" + code: "PRAC" + display: "Practical Reasons" diff --git a/specification/examples/responses/POST_QuestionnaireResponse/errors/duplicate_relationship_error.yaml b/specification/examples/responses/POST_QuestionnaireResponse/errors/duplicate_relationship_error.yaml new file mode 100644 index 00000000..d97dc7cd --- /dev/null +++ b/specification/examples/responses/POST_QuestionnaireResponse/errors/duplicate_relationship_error.yaml @@ -0,0 +1,15 @@ +PostQuestionnaireResponseDuplicateRelationshipError: + summary: Duplicate request for proxy role that already exists + description: Error response for a duplicate proxy role + value: + issue: + - code: invalid + diagnostics: "Proxy role already exists." + details: + coding: + - code: "DUPLICATE_RELATIONSHIP" + display: "Request must be for a new proxy role." + system: "https://fhir.nhs.uk/R4/CodeSystem/ValidatedRelationships-ErrorOrWarningCode" + version: '1' + severity: error + resourceType: "OperationOutcome" diff --git a/specification/validated-relationships-service-api.yaml b/specification/validated-relationships-service-api.yaml index 3d64ea9b..3a23d93e 100644 --- a/specification/validated-relationships-service-api.yaml +++ b/specification/validated-relationships-service-api.yaml @@ -141,7 +141,7 @@ info: * only covers a limited set of scenarios * is open access, so does not allow you to test authorisation - [Run In Postman](https://app.getpostman.com/run-collection/45653607-8a3d3121-ca56-4ac8-9d14-cd375ee158fc?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D45653607-8a3d3121-ca56-4ac8-9d14-cd375ee158fc%26entityType%3Dcollection%26workspaceId%3Db7b0feaf-e9be-4780-8e09-d82c6f42138c) + [Run In Postman](https://god.gw.postman.com/run-collection/46399153-5d8500b1-1bed-4494-9d55-ed8487f2898a?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D46399153-5d8500b1-1bed-4494-9d55-ed8487f2898a%26entityType%3Dcollection%26workspaceId%3Da3cfae72-2c53-419e-8850-13c2b51db16d) ### Integration testing @@ -187,9 +187,11 @@ paths: ## Sandbox test scenarios - | Scenario | Request | Response | - | --------------- | ----------------------------------------------- | -------------------------------- | - | Example request | Example questionnaire response from try it now | HTTP Status 200 Success response | + | Scenario | Request | Response | + | ---------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | + | Successful request | Valid request with performer identifier value of 9000000009 or 9000000017 | HTTP Status 200 Success response | + | Duplicate relationship | Request for relationship that already exists, with performer identifier value of 9000000049 | HTTP Status 409 and DUPLICATE_RELATIONSHIP error response | + ### Sandbox constraints - Questionnaire Response is not validated. @@ -218,6 +220,8 @@ paths: $ref: "./examples/requests/POST_QuestionnaireResponse/adult-to-adult-with-capacity.yaml#/QuestionnaireResponseAdultToAdultWithCapacityRequest" questionnaireResponseAdultNominatesAdultRequest: $ref: "./examples/requests/POST_QuestionnaireResponse/adult-nominates-adult.yaml#/QuestionnaireResponseAdultNominatesAdultRequest" + questionnaireResponseDuplicateRelationship: + $ref: "./examples/requests/POST_QuestionnaireResponse/duplicate_relationship.yaml#/QuestionnaireResponseDuplicateRelationship" application/fhir+json; charset=utf-8: schema: $ref: "#/components/schemas/QuestionnaireResponse" @@ -230,6 +234,8 @@ paths: $ref: "./examples/requests/POST_QuestionnaireResponse/adult-to-adult-with-capacity.yaml#/QuestionnaireResponseAdultToAdultWithCapacityRequest" questionnaireResponseAdultNominatesAdultRequest: $ref: "./examples/requests/POST_QuestionnaireResponse/adult-nominates-adult.yaml#/QuestionnaireResponseAdultNominatesAdultRequest" + questionnaireResponseDuplicateRelationship: + $ref: "./examples/requests/POST_QuestionnaireResponse/duplicate_relationship.yaml#/QuestionnaireResponseDuplicateRelationship" responses: "200": description: Request was received successfully for processing @@ -244,17 +250,18 @@ paths: description: | Errors will be returned for the first error encountered in the request. An error occurred as follows: - | HTTP status | Error code | Description | - | ----------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | - | 400 | `MISSING_VALUE` | Missing header or parameter. For details, see the `diagnostics` field. | - | 400 | `INVALID_VALUE` | Invalid Parameter or Invalid operation. | - | 401 | `ACCESS_DENIED` | Missing or invalid OAuth 2.0 bearer token in request. | - | 403 | `FORBIDDEN` | Access denied to resource. | - | 404 | `INVALIDATED_RESOURCE` | Resource that has been marked as invalid was requested - invalid resources cannot be retrieved | - | 405 | `METHOD_NOT_ALLOWED` | The method is not allowed. | - | 408 | `TIMEOUT` | Request timed out. | - | 415 | `UNSUPPORTED_MEDIA` | Unsupported media type. | - | 429 | `THROTTLED` | You have exceeded your application's [rate limit](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#rate-limits). | + | HTTP status | Error code | Description | + | ----------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | + | 400 | `MISSING_VALUE` | Missing header or parameter. For details, see the `diagnostics` field. | + | 400 | `INVALID_VALUE` | Invalid Parameter or Invalid operation. | + | 401 | `ACCESS_DENIED` | Missing or invalid OAuth 2.0 bearer token in request. | + | 403 | `FORBIDDEN` | Access denied to resource. | + | 404 | `INVALIDATED_RESOURCE` | Resource that has been marked as invalid was requested - invalid resources cannot be retrieved | + | 405 | `METHOD_NOT_ALLOWED` | The method is not allowed. | + | 408 | `TIMEOUT` | Request timed out. | + | 409 | `DUPLICATE_RELATIONSHIP` | A proposed proxy role already exists for this proxy/patient relationship. | + | 415 | `UNSUPPORTED_MEDIA` | Unsupported media type. | + | 429 | `THROTTLED` | You have exceeded your application's [rate limit](https://digital.nhs.uk/developer/guides-and-documentation/reference-guide#rate-limits). | content: application/fhir+json: @@ -263,6 +270,8 @@ paths: examples: accessDeniedError: $ref: "./examples/responses/errors/access-denied.yaml#/AccessDeniedError" + postQuestionnaireResponseDuplicateRelationshipError: + $ref: "./examples/responses/POST_QuestionnaireResponse/errors/duplicate_relationship_error.yaml#/PostQuestionnaireResponseDuplicateRelationshipError" "5XX": description: | Errors will be returned for the first error encountered in the request. An error occurred as follows: