Skip to content

Commit 8c0ead3

Browse files
Merge pull request #1072 from NHSDigital/feature/made14-NRL-1631-multi-pointer-warnings
NRL-1631 Add warning log for unexpected multiple pointers
2 parents df3ca99 + c1da942 commit 8c0ead3

File tree

11 files changed

+571
-6
lines changed

11 files changed

+571
-6
lines changed

api/producer/createDocumentReference/create_document_reference.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from nrlf.core.constants import (
55
PERMISSION_AUDIT_DATES_FROM_PAYLOAD,
66
PERMISSION_SUPERSEDE_IGNORE_DELETE_FAIL,
7+
TYPES_WITH_MULTIPLES,
78
)
89
from nrlf.core.decorators import request_handler
910
from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository
@@ -255,6 +256,24 @@ def handler(
255256
logger.log(LogReference.PROCREATE999)
256257
return NRLResponse.RESOURCE_SUPERSEDED(resource_id=result.resource.id)
257258

259+
pointer_type = core_model.type
260+
if pointer_type not in TYPES_WITH_MULTIPLES:
261+
nhs_number = core_model.nhs_number
262+
existing_pointers_count = repository.count_by_nhs_number(
263+
nhs_number, [pointer_type]
264+
)
265+
266+
if existing_pointers_count > 0:
267+
logger.log(
268+
LogReference.PROCREATE012,
269+
new_pointer_id=core_model.id,
270+
new_pointer_master_id=core_model.master_identifier,
271+
pointer_type=pointer_type,
272+
nhs_number=nhs_number,
273+
custodian=core_model.custodian,
274+
existing_pointers_count=existing_pointers_count,
275+
)
276+
258277
logger.log(LogReference.PROCREATE009, pointer_id=result.resource.id)
259278
repository.create(core_model)
260279
logger.log(LogReference.PROCREATE999)

api/producer/createDocumentReference/tests/test_create_document_reference.py

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import json
2-
from unittest.mock import patch
2+
from unittest.mock import Mock, patch
33

44
from freeze_uuid import freeze_uuid
55
from freezegun import freeze_time
@@ -1734,3 +1734,144 @@ def test__set_create_time_fields_when_no_date_but_perms():
17341734
},
17351735
"date": test_time,
17361736
}
1737+
1738+
1739+
@mock_aws
1740+
@mock_repository
1741+
@freeze_uuid("00000000-0000-0000-0000-000000000001")
1742+
@patch("api.producer.createDocumentReference.create_document_reference.logger")
1743+
def test_create_logs_for_unexpected_multi_pointer(
1744+
mock_logger: Mock,
1745+
repository: DocumentPointerRepository,
1746+
):
1747+
doc_ref = load_document_reference("Y05868-736253002-Valid-with-master-id")
1748+
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
1749+
repository.create(doc_pointer)
1750+
1751+
event = create_test_api_gateway_event(
1752+
headers=create_headers(),
1753+
body=doc_ref.model_dump_json(exclude_none=True),
1754+
)
1755+
1756+
result = handler(event, create_mock_context())
1757+
body = result.pop("body")
1758+
1759+
assert result == {
1760+
"statusCode": "201",
1761+
"headers": {
1762+
"Location": "/DocumentReference/Y05868-00000000-0000-0000-0000-000000000001",
1763+
**default_response_headers(),
1764+
},
1765+
"isBase64Encoded": False,
1766+
}
1767+
1768+
parsed_body = json.loads(body)
1769+
1770+
assert parsed_body == {
1771+
"resourceType": "OperationOutcome",
1772+
"issue": [
1773+
{
1774+
"severity": "information",
1775+
"code": "informational",
1776+
"details": {
1777+
"coding": [
1778+
{
1779+
"code": "RESOURCE_CREATED",
1780+
"display": "Resource created",
1781+
"system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode",
1782+
}
1783+
]
1784+
},
1785+
"diagnostics": "The document has been created",
1786+
}
1787+
],
1788+
}
1789+
1790+
assert any(
1791+
call[0][0].name == "PROCREATE012" for call in mock_logger.log.call_args_list
1792+
)
1793+
1794+
assert {
1795+
"existing_pointers_count": 1,
1796+
"nhs_number": (
1797+
doc_ref.subject.identifier.value
1798+
if doc_ref.subject and doc_ref.subject.identifier
1799+
else None
1800+
),
1801+
"pointer_type": (
1802+
f"{doc_ref.type.coding[0].system}|{doc_ref.type.coding[0].code}"
1803+
if doc_ref.type and doc_ref.type.coding
1804+
else None
1805+
),
1806+
"custodian": (
1807+
doc_ref.custodian.identifier.value
1808+
if doc_ref.custodian and doc_ref.custodian.identifier
1809+
else None
1810+
),
1811+
"new_pointer_id": "Y05868-00000000-0000-0000-0000-000000000001",
1812+
"new_pointer_master_id": (
1813+
doc_ref.masterIdentifier.value if doc_ref.masterIdentifier else None
1814+
),
1815+
} == [
1816+
call[1:][0]
1817+
for call in mock_logger.log.call_args_list
1818+
if call[0][0].name == "PROCREATE012"
1819+
][
1820+
0
1821+
]
1822+
1823+
1824+
@mock_aws
1825+
@mock_repository
1826+
@freeze_uuid("00000000-0000-0000-0000-000000000001")
1827+
@patch("api.producer.createDocumentReference.create_document_reference.logger")
1828+
def test_create_logs_for_expected_multi_pointer(
1829+
mock_logger: Mock,
1830+
repository: DocumentPointerRepository,
1831+
):
1832+
doc_ref = load_document_reference("Y05868-Appointment-Valid")
1833+
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
1834+
repository.create(doc_pointer)
1835+
1836+
event = create_test_api_gateway_event(
1837+
headers=create_headers(),
1838+
body=doc_ref.model_dump_json(exclude_none=True),
1839+
)
1840+
1841+
result = handler(event, create_mock_context())
1842+
body = result.pop("body")
1843+
1844+
assert result == {
1845+
"statusCode": "201",
1846+
"headers": {
1847+
"Location": "/DocumentReference/Y05868-00000000-0000-0000-0000-000000000001",
1848+
**default_response_headers(),
1849+
},
1850+
"isBase64Encoded": False,
1851+
}
1852+
1853+
parsed_body = json.loads(body)
1854+
1855+
assert parsed_body == {
1856+
"resourceType": "OperationOutcome",
1857+
"issue": [
1858+
{
1859+
"severity": "information",
1860+
"code": "informational",
1861+
"details": {
1862+
"coding": [
1863+
{
1864+
"code": "RESOURCE_CREATED",
1865+
"display": "Resource created",
1866+
"system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode",
1867+
}
1868+
]
1869+
},
1870+
"diagnostics": "The document has been created",
1871+
}
1872+
],
1873+
}
1874+
1875+
assert not any(
1876+
call[0][0].name == "PROCREATE012" for call in mock_logger.log.call_args_list
1877+
)

api/producer/upsertDocumentReference/tests/test_upsert_document_reference.py

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import json
2-
from unittest.mock import patch
2+
from unittest.mock import Mock, patch
33

44
from freezegun import freeze_time
55
from moto import mock_aws
@@ -1699,3 +1699,146 @@ def test__set_create_time_fields_when_no_date_but_perms():
16991699
},
17001700
"date": test_time,
17011701
}
1702+
1703+
1704+
@mock_aws
1705+
@mock_repository
1706+
@patch("api.producer.upsertDocumentReference.upsert_document_reference.logger")
1707+
def test_upsert_logs_for_unexpected_multi_pointer(
1708+
mock_logger: Mock,
1709+
repository: DocumentPointerRepository,
1710+
):
1711+
doc_ref = load_document_reference("Y05868-736253002-Valid-with-master-id")
1712+
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
1713+
repository.create(doc_pointer)
1714+
1715+
doc_ref.id = "Y05868-99999-99999-999999-02"
1716+
1717+
event = create_test_api_gateway_event(
1718+
headers=create_headers(),
1719+
body=doc_ref.model_dump_json(exclude_none=True),
1720+
)
1721+
1722+
result = handler(event, create_mock_context())
1723+
body = result.pop("body")
1724+
1725+
assert result == {
1726+
"statusCode": "201",
1727+
"headers": {
1728+
"Location": "/DocumentReference/Y05868-99999-99999-999999-02",
1729+
**default_response_headers(),
1730+
},
1731+
"isBase64Encoded": False,
1732+
}
1733+
1734+
parsed_body = json.loads(body)
1735+
1736+
assert parsed_body == {
1737+
"resourceType": "OperationOutcome",
1738+
"issue": [
1739+
{
1740+
"severity": "information",
1741+
"code": "informational",
1742+
"details": {
1743+
"coding": [
1744+
{
1745+
"code": "RESOURCE_CREATED",
1746+
"display": "Resource created",
1747+
"system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode",
1748+
}
1749+
]
1750+
},
1751+
"diagnostics": "The document has been created",
1752+
}
1753+
],
1754+
}
1755+
1756+
assert any(
1757+
call[0][0].name == "PROUPSERT012" for call in mock_logger.log.call_args_list
1758+
)
1759+
1760+
assert {
1761+
"existing_pointers_count": 1,
1762+
"nhs_number": (
1763+
doc_ref.subject.identifier.value
1764+
if doc_ref.subject and doc_ref.subject.identifier
1765+
else None
1766+
),
1767+
"pointer_type": (
1768+
f"{doc_ref.type.coding[0].system}|{doc_ref.type.coding[0].code}"
1769+
if doc_ref.type and doc_ref.type.coding
1770+
else None
1771+
),
1772+
"custodian": (
1773+
doc_ref.custodian.identifier.value
1774+
if doc_ref.custodian and doc_ref.custodian.identifier
1775+
else None
1776+
),
1777+
"new_pointer_id": doc_ref.id,
1778+
"new_pointer_master_id": (
1779+
doc_ref.masterIdentifier.value if doc_ref.masterIdentifier else None
1780+
),
1781+
} == [
1782+
call[1:][0]
1783+
for call in mock_logger.log.call_args_list
1784+
if call[0][0].name == "PROUPSERT012"
1785+
][
1786+
0
1787+
]
1788+
1789+
1790+
@mock_aws
1791+
@mock_repository
1792+
@patch("api.producer.upsertDocumentReference.upsert_document_reference.logger")
1793+
def test_upsert_logs_for_expected_multi_pointer(
1794+
mock_logger: Mock,
1795+
repository: DocumentPointerRepository,
1796+
):
1797+
doc_ref = load_document_reference("Y05868-Appointment-Valid")
1798+
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
1799+
repository.create(doc_pointer)
1800+
1801+
doc_ref.id = "Y05868-99999-99999-999999-02"
1802+
1803+
event = create_test_api_gateway_event(
1804+
headers=create_headers(),
1805+
body=doc_ref.model_dump_json(exclude_none=True),
1806+
)
1807+
1808+
result = handler(event, create_mock_context())
1809+
body = result.pop("body")
1810+
1811+
assert result == {
1812+
"statusCode": "201",
1813+
"headers": {
1814+
"Location": "/DocumentReference/Y05868-99999-99999-999999-02",
1815+
**default_response_headers(),
1816+
},
1817+
"isBase64Encoded": False,
1818+
}
1819+
1820+
parsed_body = json.loads(body)
1821+
1822+
assert parsed_body == {
1823+
"resourceType": "OperationOutcome",
1824+
"issue": [
1825+
{
1826+
"severity": "information",
1827+
"code": "informational",
1828+
"details": {
1829+
"coding": [
1830+
{
1831+
"code": "RESOURCE_CREATED",
1832+
"display": "Resource created",
1833+
"system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode",
1834+
}
1835+
]
1836+
},
1837+
"diagnostics": "The document has been created",
1838+
}
1839+
],
1840+
}
1841+
1842+
assert not any(
1843+
call[0][0].name == "PROUPSERT012" for call in mock_logger.log.call_args_list
1844+
)

api/producer/upsertDocumentReference/upsert_document_reference.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from nrlf.core.constants import (
33
PERMISSION_AUDIT_DATES_FROM_PAYLOAD,
44
PERMISSION_SUPERSEDE_IGNORE_DELETE_FAIL,
5+
TYPES_WITH_MULTIPLES,
56
)
67
from nrlf.core.decorators import request_handler
78
from nrlf.core.dynamodb.repository import DocumentPointer, DocumentPointerRepository
@@ -262,6 +263,24 @@ def handler(
262263
logger.log(LogReference.PROUPSERT999)
263264
return NRLResponse.RESOURCE_SUPERSEDED(resource_id=saved_model.id)
264265

266+
pointer_type = core_model.type
267+
if pointer_type not in TYPES_WITH_MULTIPLES:
268+
nhs_number = core_model.nhs_number
269+
existing_pointers_count = repository.count_by_nhs_number(
270+
nhs_number, [pointer_type]
271+
)
272+
273+
if existing_pointers_count > 0:
274+
logger.log(
275+
LogReference.PROUPSERT012,
276+
new_pointer_id=core_model.id,
277+
new_pointer_master_id=core_model.master_identifier,
278+
pointer_type=pointer_type,
279+
nhs_number=nhs_number,
280+
custodian=core_model.custodian,
281+
existing_pointers_count=existing_pointers_count,
282+
)
283+
265284
logger.log(LogReference.PROUPSERT009, pointer_id=result.resource.id)
266285
saved_model = repository.create(core_model)
267286
logger.log(LogReference.PROUPSERT999)

layer/nrlf/core/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,14 @@ def coding_value(self):
200200
PointerTypes.SHARED_CARE_RECORD.value: Categories.RECORD_HEADINGS.value,
201201
}
202202

203+
#
204+
# Pointer types that can have multiple pointers for a single patient
205+
TYPES_WITH_MULTIPLES = [
206+
PointerTypes.MRA_UPPER_LIMB_ARTERY.value,
207+
PointerTypes.MRI_AXILLA_BOTH.value,
208+
PointerTypes.APPOINTMENT.value,
209+
]
210+
203211
PRACTICE_SETTING_VALUE_SET_URL = (
204212
"https://fhir.nhs.uk/England/ValueSet/England-PracticeSetting"
205213
)

0 commit comments

Comments
 (0)