Skip to content

Commit c509af3

Browse files
authored
Merge branch 'master' into ved-1252-list-store-fix
2 parents 449b15d + a8bbbf0 commit c509af3

21 files changed

Lines changed: 484 additions & 182 deletions

.github/pull_request_template.md

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,7 @@
1-
## Summary
1+
## PR Description
22

3-
- Routine Change
4-
- :exclamation: Breaking Change
5-
- :robot: Operational or Infrastructure Change
6-
- :sparkles: New Feature
7-
- :warning: Potential issues that might be caused by this change
3+
Description of the changes made.
84

9-
Add any other relevant notes or explanations here. **Remove this line if you have nothing to add.**
5+
## How were the changes tested
106

11-
## Reviews Required
12-
13-
- [x] Dev
14-
- [ ] Test
15-
- [ ] Tech Author
16-
- [ ] Product Owner
17-
18-
## Review Checklist
19-
20-
:information_source: This section is to be filled in by the **reviewer**.
21-
22-
- [ ] I have reviewed the changes in this PR and they fill all of the acceptance criteria of the ticket.
23-
- [ ] If there were infrastructure, operational, or build changes, I have made sure there is sufficient evidence that the changes will work.
24-
- [ ] If there were changes that are outside of the regular release processes e.g. account infrastructure to setup, manual setup for external API integrations, secrets to set, then I have checked that the developer has flagged this to the Tech Lead as release steps.
25-
- [ ] I have checked that no Personal Identifiable Data (PID) is logged as part of the changes.
7+
Describe how the changes were tested

.github/workflows/deploy-lambda-artifact.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ jobs:
239239

240240
- name: Set up Docker Buildx
241241
if: ${{ steps.decide.outputs.deployment_mode == 'build' && !steps.build-check.outputs.existing_image_digest }}
242-
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
242+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
243243

244244
- name: Build and publish image with layer caching
245245
id: build-image

.github/workflows/quality-checks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ jobs:
257257
fi
258258
259259
- name: SonarCloud Scan
260-
uses: SonarSource/sonarqube-scan-action@299e4b793aaa83bf2aba7c9c14bedbb485688ec4
260+
uses: SonarSource/sonarqube-scan-action@55e44800a8f495208cce6e4e82f5dedb45fcf0ef
261261
env:
262262
GITHUB_TOKEN: ${{ github.token }} # Needed to get PR information, if any
263263
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# System Overview
2+
3+
This page gives a high-level view of the Immunisation FHIR API runtime architecture.
4+
5+
It focuses on the API path, batch ingestion path, outbound notification flow, runtime configuration, and NHS number change handling.
6+
7+
## High-Level Diagram
8+
9+
```mermaid
10+
flowchart LR
11+
subgraph Ingress[Ingress and API]
12+
Suppliers[Supplier systems] --> Apigee[Apigee proxy\nOAuth, rate limiting, supplier header]
13+
Apigee --> ApiGw[AWS API Gateway HTTP API]
14+
ApiGw --> Backend[Backend API Lambdas\nCRUD, search, status]
15+
Backend --> IEDS[(IEDS DynamoDB\nImmunisation event store)]
16+
end
17+
18+
subgraph Batch[Batch ingestion]
19+
SupplierFiles[Supplier batch files in S3] --> Filename[Filename Processor Lambda]
20+
Mesh[MESH mailbox bucket] --> MeshProc[Mesh Processor Lambda]
21+
MeshProc --> Filename
22+
Filename --> BatchCreated[SQS FIFO\nbatch-file-created]
23+
BatchCreated --> BatchFilter[Batch Processor Filter Lambda]
24+
BatchFilter --> SupplierQueue[SQS FIFO\nsupplier metadata queue]
25+
SupplierQueue --> BatchPipe[EventBridge Pipe]
26+
BatchPipe --> RecordProcessor[ECS Fargate Record Processor]
27+
RecordProcessor --> Kinesis[Kinesis data stream]
28+
Kinesis --> Forwarder[Record Forwarder Lambda]
29+
Forwarder --> IEDS
30+
Forwarder --> AckQueue[SQS FIFO\nack metadata queue]
31+
AckQueue --> Ack[Ack Backend Lambda]
32+
end
33+
34+
subgraph Outbound[Outbound notifications]
35+
IEDS -->|DynamoDB stream| Delta[Delta Lambda]
36+
Delta --> DeltaTable[(Delta DynamoDB)]
37+
DeltaTable -->|DynamoDB stream| MnsPipe[EventBridge Pipe]
38+
MnsPipe --> MnsQueue[SQS\nmns-outbound-events]
39+
MnsQueue --> MnsPublisher[MNS Publisher Lambda]
40+
MnsPublisher --> Subscribers[MNS subscribers]
41+
end
42+
43+
subgraph Config[Runtime config]
44+
ConfigBucket[S3 config bucket] --> RedisSync[Redis Sync Lambda]
45+
RedisSync --> Redis[(Redis cache\npermissions, disease mappings, config)]
46+
Redis --> Backend
47+
Redis --> Filename
48+
Redis --> RecordProcessor
49+
Redis --> Forwarder
50+
end
51+
52+
subgraph IdSync[Identity sync]
53+
MnsIdEvent[MNS NHS number change event] --> IdQueue[SQS\nid-sync-queue]
54+
IdQueue --> IdSyncLambda[ID Sync Lambda]
55+
IdSyncLambda --> IEDS
56+
end
57+
```
58+
59+
## Key Runtime Stores
60+
61+
| Store | Purpose |
62+
| -------------- | ------------------------------------------------------------------- |
63+
| IEDS DynamoDB | System of record for immunisation events |
64+
| Delta DynamoDB | Outbound change store derived from IEDS stream events |
65+
| Redis | Runtime cache for permissions, disease mappings, and related config |
66+
| Audit table | Batch-processing control state, deduplication, and status tracking |
67+
68+
## Design Notes
69+
70+
- The filename processor is the batch entry point for files placed in the source bucket.
71+
- The audit table is for deduplication, processing state, and ordering decisions.
72+
- The batch processor filter ensures only one event is processed at a time for a given supplier and vaccine-type combination.
73+
- The supplier metadata FIFO queue preserves ordering before work is dispatched to ECS through EventBridge Pipe.
74+
- ECS is used for record processing because batch row processing can be long-running.
75+
- The record forwarder is the component that applies processed batch changes to IEDS.
76+
- ACK creation is part of the batch lifecycle.

infrastructure/account/.terraform.lock.hcl

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infrastructure/instance/.terraform.lock.hcl

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infrastructure/mesh/.terraform.lock.hcl

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lambdas/backend/src/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE = "Unable to process request. Issue may be transient."
44
# Maximum response size for an AWS Lambda function
5-
MAX_RESPONSE_SIZE_BYTES = 6 * 1024 * 1024
5+
MAX_SEARCH_RESPONSE_SIZE_BYTES = 1 * 1024 * 1024

lambdas/backend/src/controller/fhir_controller.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from fhir.resources.R4B.identifier import Identifier
1212

1313
from common.get_service_url import get_service_url
14-
from constants import MAX_RESPONSE_SIZE_BYTES
14+
from constants import MAX_SEARCH_RESPONSE_SIZE_BYTES
1515
from controller.aws_apig_event_utils import (
1616
get_multi_value_query_params,
1717
get_path_parameter,
@@ -89,12 +89,17 @@ def create_immunization(self, aws_event: APIGatewayProxyEventV1) -> dict:
8989
except JSONDecodeError as e:
9090
raise InvalidJsonError(message=str(f"Request's body contains malformed JSON: {e}"))
9191

92-
created_resource_id = self.fhir_service.create_immunization(immunisation, supplier_system)
92+
created_resource_id, created_resource_version = self.fhir_service.create_immunization(
93+
immunisation, supplier_system
94+
)
9395

9496
return create_response(
9597
status_code=201,
9698
body=None,
97-
headers={"Location": f"{self._API_SERVICE_URL}/Immunization/{created_resource_id}", "E-Tag": "1"},
99+
headers={
100+
"Location": f"{self._API_SERVICE_URL}/Immunization/{created_resource_id}",
101+
"E-Tag": str(created_resource_version),
102+
},
98103
)
99104

100105
@fhir_api_exception_handler
@@ -208,7 +213,7 @@ def _search_immunizations_by_target_disease(self, search_params: dict[str, list[
208213
def _create_search_response(self, search_bundle: Bundle) -> dict:
209214
search_response_json = search_bundle.json(use_decimal=True)
210215

211-
if len(search_response_json) > MAX_RESPONSE_SIZE_BYTES:
216+
if len(search_response_json) > MAX_SEARCH_RESPONSE_SIZE_BYTES:
212217
raise TooManyResultsError("Search returned too many results. Please narrow down the search")
213218

214219
prepared_search_bundle = self._prepare_search_bundle(search_response_json)

lambdas/backend/src/service/fhir_service.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import os
55
import uuid
6-
from typing import Any, cast
6+
from typing import Any
77
from uuid import uuid4
88

99
from fhir.resources.R4B.bundle import (
@@ -145,7 +145,7 @@ def get_immunization_and_version_by_id(self, imms_id: str, supplier_system: str)
145145

146146
return Immunization.parse_obj(resource), str(immunization_metadata.resource_version)
147147

148-
def create_immunization(self, immunization: dict, supplier_system: str) -> Id:
148+
def create_immunization(self, immunization: dict, supplier_system: str) -> tuple[Id, int]:
149149
if immunization.get("id") is not None:
150150
raise CustomValidationError("id field must not be present for CREATE operation")
151151

@@ -156,16 +156,32 @@ def create_immunization(self, immunization: dict, supplier_system: str) -> Id:
156156
if not self.authoriser.authorise(supplier_system, ApiOperationCode.CREATE, {vaccination_type}):
157157
raise UnauthorizedVaxError()
158158

159-
# Set ID for the requested new record
160-
immunization["id"] = str(uuid.uuid4())
159+
identifier = Identifier.parse_obj(immunization["identifier"][0])
160+
duplicate_identifier = f"{identifier.system}#{identifier.value}"
161161

162-
immunization_fhir_entity = Immunization.parse_obj(immunization)
163-
identifier = cast(Identifier, immunization_fhir_entity.identifier[0])
162+
existing_immunization_resource, existing_immunization_meta = (
163+
self.immunization_repo.get_immunization_by_identifier(identifier)
164+
)
165+
if existing_immunization_resource:
166+
if not existing_immunization_meta.is_deleted:
167+
raise IdentifierDuplicationError(identifier=duplicate_identifier)
168+
169+
immunization_id = existing_immunization_resource["id"]
170+
immunization["id"] = immunization_id
171+
immunization_fhir_entity = Immunization.parse_obj(immunization)
172+
updated_version = self.immunization_repo.update_immunization(
173+
immunization_id,
174+
immunization_fhir_entity,
175+
existing_immunization_meta,
176+
supplier_system,
177+
)
178+
return immunization_id, updated_version
164179

165-
if self.immunization_repo.check_immunization_identifier_exists(identifier.system, identifier.value):
166-
raise IdentifierDuplicationError(identifier=f"{identifier.system}#{identifier.value}")
180+
immunization["id"] = str(uuid.uuid4())
181+
immunization_fhir_entity = Immunization.parse_obj(immunization)
167182

168-
return self.immunization_repo.create_immunization(immunization_fhir_entity, supplier_system)
183+
created_id = self.immunization_repo.create_immunization(immunization_fhir_entity, supplier_system)
184+
return created_id, 1
169185

170186
def update_immunization(self, imms_id: str, immunization: dict, supplier_system: str, resource_version: int) -> int:
171187
self._validate_immunization(immunization)

0 commit comments

Comments
 (0)