Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
b4728b8
[GPCAPIM-254]: Beginning of /patient/$gpc.getstructuredrecord endpoint.
davidhamill1-nhs Jan 15, 2026
2cfef13
[GPCAPIM-254]: Handle logic in request-specific class
davidhamill1-nhs Jan 19, 2026
273390d
[GPCAPIM-254]: Move to handler class
davidhamill1-nhs Jan 19, 2026
01bcd21
[GPCAPIM-254]: Remove the lambda.
davidhamill1-nhs Jan 20, 2026
28c9d5e
[GPCAPIM-254]: Clean up.
davidhamill1-nhs Jan 21, 2026
61bde79
[GPCAPIM-254]: Correct content-type header.
davidhamill1-nhs Jan 21, 2026
203935c
[GPCAPIM-254]: Handle response object, rather than just pass back dict.
davidhamill1-nhs Jan 21, 2026
124c93b
[GPCAPIM-254]: Correct content-type header for healthcheck.
davidhamill1-nhs Jan 21, 2026
614c801
Revert "[GPCAPIM-254]: Force new deployment of ecs task in preview en…
davidhamill1-nhs Jan 21, 2026
8d34def
[GPCAPIM-254]: Correct module name.
davidhamill1-nhs Jan 21, 2026
be1bc93
[GPCAPIM-254]: APIM handles CSRF through its auth design; We don't ha…
davidhamill1-nhs Jan 22, 2026
3a94ec8
[GPCAPIM-254]: Reduce fragility of code by ensuring NHS number is cor…
davidhamill1-nhs Jan 22, 2026
d5c1548
[GPCAPIM-254]: CSRF alert will be disabled in SonarQube; we do not ne…
davidhamill1-nhs Jan 26, 2026
d843f73
Change return type to flask response
Vox-Ben Jan 15, 2026
bffcfb6
Refactor things to make ruff happy
Vox-Ben Jan 16, 2026
5d7aa73
Pass the request body & multiple SDS calls
Vox-Ben Jan 19, 2026
f4fcbe1
Mypy happy, tests passing
Vox-Ben Jan 19, 2026
de05444
Tests passing. Maybe got too many tests.
Vox-Ben Jan 20, 2026
c09f866
Trim some unnecessary unit tests
Vox-Ben Jan 20, 2026
3b69682
Sort out docstrings
Vox-Ben Jan 20, 2026
78a0c7a
Add tests for coverage
Vox-Ben Jan 20, 2026
986c86b
Add tests for coverage
Vox-Ben Jan 20, 2026
8670d62
Change GP Connect to GP provider
Vox-Ben Jan 20, 2026
b18ffcc
Remove redundant parentheses
Vox-Ben Jan 20, 2026
12caa29
Fix expected response
Vox-Ben Jan 21, 2026
912b9ad
Address review comments
Vox-Ben Jan 27, 2026
e357afe
Integrate with real GpProviderClient
Vox-Ben Jan 27, 2026
e965f20
Integrate API handler with controller
Vox-Ben Jan 27, 2026
41b4ed3
One test passing with updated run signature
Vox-Ben Jan 28, 2026
862f218
Tests passing
Vox-Ben Jan 28, 2026
5b1d7cf
Tidy up todos
Vox-Ben Jan 28, 2026
bae80d4
Make cleanup more robust
Vox-Ben Jan 28, 2026
f37d118
Make mypy happy
Vox-Ben Jan 28, 2026
c1a8d94
Add missing check to ruff and fix code accordingly
Vox-Ben Jan 28, 2026
084cb79
Remove lifestyle ignore changes
Vox-Ben Jan 29, 2026
0b7d8b6
[GPCAPIM-255]: Update dependency groups and requests version
DWolfsNHS Jan 29, 2026
68ae9ea
Remove tests that need rewriting
Vox-Ben Jan 29, 2026
c5282d9
Remove acceptance, contract and schema tests pending rewrite/fix
Vox-Ben Jan 29, 2026
a4539fa
Remove handler
Vox-Ben Jan 29, 2026
59afc92
Address review comments
Vox-Ben Jan 30, 2026
e08778b
Acceptance tests now passing
Vox-Ben Jan 30, 2026
0728f28
Reinstate integration tests
Vox-Ben Feb 1, 2026
a723d2b
Reinstate contract and schema tests
Vox-Ben Feb 1, 2026
e8c6d70
Replace StubResponse with requests.Response
Vox-Ben Feb 2, 2026
caebaee
Schema tests working
Vox-Ben Feb 2, 2026
ece28bf
All tests passing, hurray!
Vox-Ben Feb 2, 2026
f2d96f3
Increase test coverage
Vox-Ben Feb 3, 2026
5bd396f
Clean up todos
Vox-Ben Feb 3, 2026
5300791
Clean up rebase mess
Vox-Ben Feb 4, 2026
790689e
Remove unnecessary fixture
Vox-Ben Feb 4, 2026
8838b4e
Add tests lost during rebase back in
Vox-Ben Feb 4, 2026
ff78d58
Add extra comment
Vox-Ben Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions gateway-api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,60 @@ paths:
type: string
enum: [application/fhir+json]
required: true
- in: header
name: Ods-from
required: true
schema:
type: string
example: "A12345"
minLength: 1
- in: header
name: Ssp-TraceID
required: true
schema:
type: string
example: "trace-1234"
minLength: 1
requestBody:
required: true
content:
application/fhir+json:
schema:
type: object
required:
- resourceType
- parameter
properties:
resourceType:
type: string
enum: ["Parameters"]
example: "Parameters"
parameter:
type: array
minItems: 1
items:
type: object
required:
- name
- valueIdentifier
properties:
name:
type: string
enum: ["patientNHSNumber"]
example: "patientNHSNumber"
valueIdentifier:
type: object
required:
- system
- value
properties:
system:
type: string
minLength: 1
example: "https://fhir.nhs.uk/Id/nhs-number"
value:
type: string
pattern: "^[0-9]{10}$"
example: "9999999999"
responses:
'200':
Expand Down Expand Up @@ -141,6 +169,78 @@ paths:
type: string
format: date
example: "1985-04-12"
'400':
description: Bad request - invalid input parameters
content:
application/fhir+json:
schema:
type: object
properties:
resourceType:
type: string
example: "OperationOutcome"
issue:
type: array
items:
type: object
properties:
severity:
type: string
example: "error"
code:
type: string
example: "invalid"
diagnostics:
type: string
example: "Invalid NHS number format"
'404':
description: Patient not found
content:
application/fhir+json:
schema:
type: object
properties:
resourceType:
type: string
example: "OperationOutcome"
issue:
type: array
items:
type: object
properties:
severity:
type: string
example: "error"
code:
type: string
example: "not-found"
diagnostics:
type: string
example: "Patient not found"
'500':
description: Internal server error
content:
application/fhir+json:
schema:
type: object
properties:
resourceType:
type: string
example: "OperationOutcome"
issue:
type: array
items:
type: object
properties:
severity:
type: string
example: "error"
code:
type: string
example: "exception"
diagnostics:
type: string
example: "Internal server error"
/health:
get:
summary: Health check
Expand Down
12 changes: 6 additions & 6 deletions gateway-api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gateway-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ requires-python = ">3.13,<4.0.0"
clinical-data-common = { git = "https://github.com/NHSDigital/clinical-data-common.git", tag = "v0.1.0" }
flask = "^3.1.2"
types-flask = "^1.1.6"
requests = "^2.32.5"

[tool.poetry]
packages = [{include = "gateway_api", from = "src"},
Expand Down Expand Up @@ -51,7 +52,6 @@ dev = [
"pytest-html (>=4.1.1,<5.0.0)",
"pact-python>=2.0.0",
"python-dotenv>=1.0.0",
"requests>=2.31.0",
"schemathesis>=4.4.1",
"types-requests (>=2.32.4.20250913,<3.0.0.0)",
"types-pyyaml (>=6.0.12.20250915,<7.0.0.0)",
Expand Down
24 changes: 22 additions & 2 deletions gateway-api/src/gateway_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from flask import Flask, request
from flask.wrappers import Response

from gateway_api.controller import Controller
from gateway_api.get_structured_record import (
GetStructuredRecordHandler,
GetStructuredRecordRequest,
RequestValidationError,
)

app = Flask(__name__)
Expand Down Expand Up @@ -37,9 +38,28 @@ def get_app_port() -> int:
def get_structured_record() -> Response:
try:
get_structured_record_request = GetStructuredRecordRequest(request)
GetStructuredRecordHandler.handle(get_structured_record_request)
except RequestValidationError as e:
response = Response(
response=str(e),
status=400,
content_type="text/plain",
)
return response
except Exception as e:
response = Response(
response=f"Internal Server Error: {e}",
status=500,
content_type="text/plain",
)
return response

try:
controller = Controller()
flask_response = controller.run(request=get_structured_record_request)
get_structured_record_request.set_response_from_flaskresponse(flask_response)
except Exception as e:
get_structured_record_request.set_negative_response(str(e))

return get_structured_record_request.build_response()


Expand Down
Empty file.
63 changes: 63 additions & 0 deletions gateway-api/src/gateway_api/common/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Shared lightweight types and helpers used across the gateway API.
"""

import re
from dataclasses import dataclass

# This project uses JSON request/response bodies as strings in the controller layer.
# The alias is used to make intent clearer in function signatures.
type json_str = str


@dataclass
class FlaskResponse:
"""
Lightweight response container returned by controller entry points.

This mirrors the minimal set of fields used by the surrounding web framework.

:param status_code: HTTP status code for the response (e.g., 200, 400, 404).
:param data: Response body as text, if any.
:param headers: Response headers, if any.
"""

status_code: int
data: str | None = None
headers: dict[str, str] | None = None


def validate_nhs_number(value: str | int) -> bool:
"""
Validate an NHS number using the NHS modulus-11 check digit algorithm.

The input may be a string or integer. Any non-digit separators in string
inputs (spaces, hyphens, etc.) are ignored.

:param value: NHS number as a string or integer. Non-digit characters
are ignored when a string is provided.
:returns: ``True`` if the number is a valid NHS number, otherwise ``False``.
"""
str_value = str(value) # Just in case they passed an integer
digits = re.sub(r"[\s-]", "", str_value or "")

if len(digits) != 10:
return False
if not digits.isdigit():
return False

first_nine = [int(ch) for ch in digits[:9]]
provided_check_digit = int(digits[9])

weights = list(range(10, 1, -1))
total = sum(d * w for d, w in zip(first_nine, weights, strict=True))

remainder = total % 11
check = 11 - remainder

if check == 11:
check = 0
if check == 10:
return False # invalid NHS number

return check == provided_check_digit
Empty file.
55 changes: 55 additions & 0 deletions gateway-api/src/gateway_api/common/test_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Unit tests for :mod:`gateway_api.common.common`.
"""

import pytest

from gateway_api.common import common


@pytest.mark.parametrize(
("nhs_number", "expected"),
[
("9434765919", True), # Just a number
("943 476 5919", True), # Spaces are permitted
("987-654-3210", True), # Hyphens are permitted
(9434765919, True), # Integer input is permitted
("", False), # Empty string is invalid
("943476591", False), # 9 digits
("94347659190", False), # 11 digits
("9434765918", False), # wrong check digit
("NOT_A_NUMB", False), # non-numeric
("943SOME_LETTERS4765919", False), # non-numeric in a valid NHS number
],
)
def test_validate_nhs_number(nhs_number: str | int, expected: bool) -> None:
"""
Validate that separators (spaces, hyphens) are ignored and valid numbers pass.
"""
assert common.validate_nhs_number(nhs_number) is expected


@pytest.mark.parametrize(
("nhs_number", "expected"),
[
# All zeros => weighted sum 0 => remainder 0 => check 11 => mapped to 0 => valid
("0000000000", True),
# First 9 digits produce remainder 1 => check 10 => invalid
("0000000060", False),
],
)
def test_validate_nhs_number_check_edge_cases_10_and_11(
nhs_number: str | int, expected: bool
) -> None:
"""
validate_nhs_number should behave correctly when the computed ``check`` value
is 10 or 11.

- If ``check`` computes to 11, it should be treated as 0, so a number with check
digit 0 should validate successfully.
- If ``check`` computes to 10, the number is invalid and validation should return
False.
"""
# All zeros => weighted sum 0 => remainder 0 => check 11 => mapped to 0 => valid
# with check digit 0
assert common.validate_nhs_number(nhs_number) is expected
Loading