Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
a8b45df
[GPCAPIM-254]: Lift and shift of lambda functionality in to a Flask app.
davidhamill1-nhs Jan 14, 2026
73cdf4d
[GPCAPIM-254]: Lambda is no longer being used; move actions away from…
davidhamill1-nhs Jan 14, 2026
4e1ea55
[GPCAPIM-254]: Github not picking up unindented input
davidhamill1-nhs Jan 14, 2026
f89e719
[GPCAPIM-254]: Github not picking up unindented input
davidhamill1-nhs Jan 14, 2026
c321366
[GPCAPIM-254]: Add type hinting
davidhamill1-nhs Jan 14, 2026
bf390d8
[GPCAPIM-254]: Beginning of /patient/$gpc.getstructuredrecord endpoint.
davidhamill1-nhs Jan 15, 2026
4c7beef
[GPCAPIM-254]: Handle logic in request-specific class
davidhamill1-nhs Jan 19, 2026
92485cc
[GPCAPIM-254]: Move to handler class
davidhamill1-nhs Jan 19, 2026
8417c72
[GPCAPIM-254]: Update healthcheck endpoint to return simplier body.
davidhamill1-nhs Jan 20, 2026
8317bb7
[GPCAPIM-254]: Remove the lambda.
davidhamill1-nhs Jan 20, 2026
5e060c5
[GPCAPIM-254]: Clean up github actions.
davidhamill1-nhs Jan 20, 2026
5c5c82c
[GPCAPIM-254]: Clean up.
davidhamill1-nhs Jan 21, 2026
33326b5
[GPCAPIM-254]: Unit tests no loonger exist in the top level.
davidhamill1-nhs Jan 21, 2026
a78aa6b
[GPCAPIM-254]: Correct content-type header.
davidhamill1-nhs Jan 21, 2026
2f4f347
[GPCAPIM-254]: Handle response object, rather than just pass back dict.
davidhamill1-nhs Jan 21, 2026
d6d1018
[GPCAPIM-254]: Correct content-type header for healthcheck.
davidhamill1-nhs Jan 21, 2026
976dd3a
[GPCAPIM-254]: Add error hanlding in app.
davidhamill1-nhs Jan 21, 2026
859e54a
Revert "[GPCAPIM-254]: Force new deployment of ecs task in preview en…
davidhamill1-nhs Jan 21, 2026
cd6e96b
[GPCAPIM-254]: Make it clear which version is deployed in the health …
davidhamill1-nhs Jan 21, 2026
00f676a
[GPCAPIM-254]: Correct module name.
davidhamill1-nhs Jan 21, 2026
170a3cb
[GPCAPIM-254]: Use tech radars preferred alpine and run thorugh non-r…
davidhamill1-nhs Jan 21, 2026
6c0cbf0
[GPCAPIM-254]: APIM handles CSRF through its auth design; We don't ha…
davidhamill1-nhs Jan 22, 2026
0ed1834
[GPCAPIM-254]: Reduce fragility of code by pushing environment variab…
davidhamill1-nhs Jan 22, 2026
e0243b5
[GPCAPIM-254]: Reduce fragility of code by ensuring headers are corre…
davidhamill1-nhs Jan 22, 2026
2c80997
[GPCAPIM-254]: Reduce fragility of code by ensuring NHS number is cor…
davidhamill1-nhs Jan 22, 2026
8e85261
[GPCAPIM-254]: CSRF alert will be disabled in SonarQube; we do not ne…
davidhamill1-nhs Jan 26, 2026
82653af
[GPCAPIM-254]: Mock Request.geT_json() method by return the request_b…
davidhamill1-nhs Jan 26, 2026
34d02bb
[GPCAPIM-254]: Pytest, by default, runs all test_*.py files. Passing …
davidhamill1-nhs Jan 26, 2026
09ff41d
[GPCAPIM-254]: Move print lines in to test covered functions to stop …
davidhamill1-nhs Jan 26, 2026
1b3c02f
[GPCAPIM-254]: Make the behaviour of the payload more explicit.
davidhamill1-nhs Jan 26, 2026
fda4de3
[GPCAPIM-254]: Constants should be UPPERCASE for clarity.
davidhamill1-nhs Jan 26, 2026
d2a2617
[GPCAPIM-254]: No longer SSP-from/SSP-to headers; moving towards ODS-…
davidhamill1-nhs Jan 26, 2026
4fbda56
[GPCAPIM-254]: It's a numbers game.
davidhamill1-nhs Jan 26, 2026
47d4592
[GPCAPIM-254]: Correct name of test.
davidhamill1-nhs Jan 26, 2026
3238cd5
[GPCAPIM-254]: Correct step name.
davidhamill1-nhs Jan 26, 2026
76ee8f2
Change return type to flask response
Vox-Ben Jan 15, 2026
45dce95
Refactor things to make ruff happy
Vox-Ben Jan 16, 2026
9c928d7
Pass the request body & multiple SDS calls
Vox-Ben Jan 19, 2026
323d62a
Mypy happy, tests passing
Vox-Ben Jan 19, 2026
8ad53a6
Tests passing. Maybe got too many tests.
Vox-Ben Jan 20, 2026
700e4b1
Trim some unnecessary unit tests
Vox-Ben Jan 20, 2026
035faad
Sort out docstrings
Vox-Ben Jan 20, 2026
69a8bc2
Add tests for coverage
Vox-Ben Jan 20, 2026
2a0678e
Add tests for coverage
Vox-Ben Jan 20, 2026
57eccc3
Change GP Connect to GP provider
Vox-Ben Jan 20, 2026
3cd5111
Remove redundant parentheses
Vox-Ben Jan 20, 2026
ce8fe94
Fix expected response
Vox-Ben Jan 21, 2026
2c4f962
Address review comments
Vox-Ben Jan 27, 2026
a4d95ab
Integrate with real GpProviderClient
Vox-Ben Jan 27, 2026
89ebb4d
Integrate API handler with controller
Vox-Ben Jan 27, 2026
95adf64
One test passing with updated run signature
Vox-Ben Jan 28, 2026
857ae0e
Tests passing
Vox-Ben Jan 28, 2026
32bb69c
Tidy up todos
Vox-Ben Jan 28, 2026
5e5ec97
Make cleanup more robust
Vox-Ben Jan 28, 2026
0dd24bd
Make mypy happy
Vox-Ben Jan 28, 2026
7bf07be
Merge branch 'main' into feature/GPCAPIM-255_controller_integration_c…
Vox-Ben Jan 28, 2026
6569a2f
Add missing check to ruff and fix code accordingly
Vox-Ben Jan 28, 2026
d6f27f0
Merge branch 'feature/GPCAPIM-255_controller_integration_cherrypick' …
Vox-Ben Jan 28, 2026
7ce8ef3
Remove lifestyle ignore changes
Vox-Ben Jan 29, 2026
591ece4
[GPCAPIM-255]: Update dependency groups and requests version
DWolfsNHS Jan 29, 2026
2f2fa3f
Remove tests that need rewriting
Vox-Ben Jan 29, 2026
a072259
Remove acceptance, contract and schema tests pending rewrite/fix
Vox-Ben Jan 29, 2026
df080ee
Remove handler
Vox-Ben Jan 29, 2026
9bb0e40
Address review comments
Vox-Ben Jan 30, 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
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:
response = Response(
response="Internal Server Error",
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_NUMBER", 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
Loading