Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
01fa03c
build base schema
Akol125 Feb 16, 2026
953d861
setup base_schema from vaccs data
Akol125 Feb 17, 2026
32330cc
make pds call and move pds details to shared folder
Akol125 Feb 18, 2026
dcf31ef
fetch details from pds and add tests
Akol125 Feb 18, 2026
5de42d2
Merge remote-tracking branch 'origin/staging/VED-16-mns-vacc-event-no…
Akol125 Feb 18, 2026
8337ff0
fix terraform and test issues
Akol125 Feb 18, 2026
02945a1
add test for lambda
Akol125 Feb 18, 2026
2a946c3
refactor pds fetch details
Akol125 Feb 19, 2026
f31e8c2
add publish mns notification
Akol125 Feb 19, 2026
66c6d1e
refactor modules
Akol125 Feb 20, 2026
de2b372
fix tf duplication
Akol125 Feb 20, 2026
4d76908
bump test
Akol125 Feb 20, 2026
6290ba5
add terraform and revert id_sync
Akol125 Feb 23, 2026
44d469c
fix mns, test, pds
Akol125 Feb 23, 2026
565a765
add gp identifier
Akol125 Feb 23, 2026
9a58846
make payload dynamic
Akol125 Feb 23, 2026
9cc38bd
add typedicts, remove recursion
Akol125 Feb 24, 2026
ecdf384
refactor lambda and add env vars
Akol125 Feb 24, 2026
afe7bc9
fix sonar and terraform issues
Akol125 Feb 24, 2026
0297ee5
remove pdsSync found in id_sync
Akol125 Feb 24, 2026
a96582a
resolve type hinting and remove try catch
Akol125 Feb 24, 2026
a925afd
add secrets for tf and resolve sonar issues
Akol125 Feb 25, 2026
1ef1124
secret configurations
Akol125 Feb 25, 2026
96a4a13
fix e2e as base isn't master
Akol125 Feb 25, 2026
a8c3782
reflect new permission in e2e test
Akol125 Feb 25, 2026
1c92884
add conditional for patient_detials
Akol125 Feb 25, 2026
22fb4ed
remove test for attribute error
Akol125 Feb 25, 2026
f13b423
check logs to pinpoint pds bad request
Akol125 Feb 26, 2026
023942a
remove dynamo utils and add tests
Akol125 Feb 26, 2026
9305138
fail fast with no nhs_no and lambda int test
Akol125 Feb 27, 2026
7ab3bb3
integration test
Akol125 Mar 3, 2026
8e22a92
change subject age to integer in test
Akol125 Mar 3, 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "dev"
immunisation_account_id = "345594581768"
dspp_core_account_id = "603871901111"
pds_environment = "int"
mns_environment = "int"
error_alarm_notifications_enabled = true
create_mesh_processor = false
has_sub_environment_scope = true
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "dev"
immunisation_account_id = "345594581768"
dspp_core_account_id = "603871901111"
pds_environment = "int"
mns_environment = "int"
error_alarm_notifications_enabled = false
mns_publisher_feature_enabled = true
create_mesh_processor = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "dev"
immunisation_account_id = "345594581768"
dspp_core_account_id = "603871901111"
pds_environment = "int"
mns_environment = "int"
error_alarm_notifications_enabled = false
mns_publisher_feature_enabled = true # Switch this off once tested fully e2e in Lambda branch
create_mesh_processor = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "dev"
immunisation_account_id = "345594581768"
dspp_core_account_id = "603871901111"
pds_environment = "ref"
mns_environment = "int"
error_alarm_notifications_enabled = true
create_mesh_processor = false
has_sub_environment_scope = true
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "preprod"
immunisation_account_id = "084828561157"
dspp_core_account_id = "603871901111"
pds_environment = "int"
mns_environment = "int"
error_alarm_notifications_enabled = true
mns_publisher_feature_enabled = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ environment = "preprod"
immunisation_account_id = "084828561157"
dspp_core_account_id = "603871901111"
pds_environment = "int"
mns_environment = "int"
error_alarm_notifications_enabled = true
mns_publisher_feature_enabled = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ immunisation_account_id = "664418956997"
dspp_core_account_id = "232116723729"
mns_account_id = "758334270304"
pds_environment = "prod"
mns_environment = "prod"
error_alarm_notifications_enabled = true
mns_publisher_feature_enabled = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ immunisation_account_id = "664418956997"
dspp_core_account_id = "232116723729"
mns_account_id = "758334270304"
pds_environment = "prod"
mns_environment = "prod"
error_alarm_notifications_enabled = true
mns_publisher_feature_enabled = true

Expand Down
6 changes: 6 additions & 0 deletions infrastructure/instance/mns_publisher.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ module "mns_publisher" {
enable_lambda_alarm = var.error_alarm_notifications_enabled # consider just INT and PROD
immunisation_account_id = var.immunisation_account_id
is_temp = local.is_temp
resource_scope = local.resource_scope
imms_base_path = strcontains(var.sub_environment, "pr-") ? "immunisation-fhir-api/FHIR/R4-${var.sub_environment}" : "immunisation-fhir-api/FHIR/R4"
lambda_kms_encryption_key_arn = data.aws_kms_key.existing_lambda_encryption_key.arn
mns_publisher_resource_name_prefix = "${local.resource_scope}-mns-outbound-events"
secrets_manager_policy_path = "${local.policy_path}/secret_manager.json"
account_id = data.aws_caller_identity.current.account_id
pds_environment = var.pds_environment
mns_environment = var.mns_environment

private_subnet_ids = local.private_subnet_ids
security_group_id = data.aws_security_group.existing_securitygroup.id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,11 @@ resource "aws_lambda_function" "mns_publisher_lambda" {

environment {
variables = {
SPLUNK_FIREHOSE_NAME = var.splunk_firehose_stream_name
SPLUNK_FIREHOSE_NAME = var.splunk_firehose_stream_name
IMMUNIZATION_ENV = var.resource_scope,
IMMUNIZATION_BASE_PATH = var.imms_base_path
PDS_ENV = var.pds_environment
MNS_ENV = var.mns_environment
}
}

Expand All @@ -203,6 +207,30 @@ resource "aws_lambda_function" "mns_publisher_lambda" {
]
}


data "aws_iam_policy_document" "mns_publisher_secrets_policy_document" {
source_policy_documents = [
templatefile("${var.secrets_manager_policy_path}", {
"account_id" : var.account_id,
"pds_environment" : var.pds_environment
}),
]
}

resource "aws_iam_policy" "mns_publisher_lambda_secrets_policy" {
name = "${local.mns_publisher_lambda_name}-secrets-policy"
description = "Allow Lambda to access Secrets Manager"
policy = data.aws_iam_policy_document.mns_publisher_secrets_policy_document.json
}


# Attach the secrets/dynamodb access policy to the Lambda role
resource "aws_iam_role_policy_attachment" "mns_publisher_lambda_secrets_policy_attachment" {
role = aws_iam_role.mns_publisher_lambda_exec_role.name
policy_arn = aws_iam_policy.mns_publisher_lambda_secrets_policy.arn
}


resource "aws_cloudwatch_log_group" "mns_publisher_lambda_log_group" {
name = "/aws/lambda/${local.mns_publisher_lambda_name}"
retention_in_days = 30
Expand All @@ -213,6 +241,9 @@ resource "aws_lambda_event_source_mapping" "mns_outbound_event_sqs_to_lambda" {
function_name = aws_lambda_function.mns_publisher_lambda.arn
batch_size = 10
enabled = true

# Enables partial batch responses using `batchItemFailures`
function_response_types = ["ReportBatchItemFailures"]
}

resource "aws_cloudwatch_log_metric_filter" "mns_publisher_error_logs" {
Expand Down
31 changes: 31 additions & 0 deletions infrastructure/instance/modules/mns_publisher/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,34 @@ variable "system_alarm_sns_topic_arn" {
description = "The ARN of the SNS Topic used for raising alerts to Slack for CW alarms."
}

variable "resource_scope" {
type = string
description = <<EOT
The effective deployment scope used for resource naming and isolation.
This resolves to either the base environment (e.g., dev, pre-prod, prod) or a
sub-environment (e.g., int-blue/int-green) when sub-environment scoping is enabled.
EOT
}

variable "imms_base_path" {
type = string
description = "Base path for the Immunisation FHIR API. Used to construct environment-specific routes (e.g. PR preview paths or default R4 path)."
}

variable "mns_environment" {
type = string
}

variable "pds_environment" {
type = string
}

variable "account_id" {
type = string
description = "AWS account ID used for IAM policy templating (e.g., Secrets Manager ARNs)."
}

variable "secrets_manager_policy_path" {
type = string
description = "Path to the IAM policy JSON template for Secrets Manager access (e.g., ./policies/secret_manager.json)."
}
5 changes: 5 additions & 0 deletions infrastructure/instance/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ variable "pds_environment" {
default = "int"
}

variable "mns_environment" {
type = string
default = "int"
}

variable "mesh_no_invocation_period_seconds" {
description = "The maximum duration the MESH Processor Lambda can go without being invoked before the no-invocation alarm is triggered."
type = number
Expand Down
3 changes: 2 additions & 1 deletion lambdas/backend/src/controller/fhir_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from fhir.resources.R4B.bundle import Bundle
from fhir.resources.R4B.identifier import Identifier

from common.get_service_url import get_service_url
from constants import MAX_RESPONSE_SIZE_BYTES
from controller.aws_apig_event_utils import (
get_multi_value_query_params,
Expand All @@ -33,7 +34,7 @@
TooManyResultsError,
)
from repository.fhir_repository import ImmunizationRepository, create_table
from service.fhir_service import FhirService, get_service_url
from service.fhir_service import FhirService

IMMUNIZATION_ENV = os.getenv("IMMUNIZATION_ENV")
IMMUNIZATION_BASE_PATH = os.getenv("IMMUNIZATION_BASE_PATH")
Expand Down
3 changes: 2 additions & 1 deletion lambdas/backend/src/service/fhir_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from authorisation.api_operation_code import ApiOperationCode
from authorisation.authoriser import Authoriser
from common.get_service_url import get_service_url
from common.models.constants import Constants
from common.models.errors import (
Code,
Expand All @@ -45,7 +46,7 @@
from filter import Filter
from models.errors import UnauthorizedVaxError
from repository.fhir_repository import ImmunizationRepository
from service.search_url_helper import create_url_for_bundle_link, get_service_url
from service.search_url_helper import create_url_for_bundle_link

logging.basicConfig(level="INFO")
logger = logging.getLogger()
Expand Down
25 changes: 1 addition & 24 deletions lambdas/backend/src/service/search_url_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,9 @@
import urllib.parse
from typing import Optional

from common.get_service_url import get_service_url
from controller.constants import IMMUNIZATION_TARGET_LEGACY_KEY_NAME, ImmunizationSearchParameterName
from controller.parameter_parser import PATIENT_IDENTIFIER_SYSTEM
from service.constants import DEFAULT_BASE_PATH, PR_ENV_PREFIX


def get_service_url(service_env: Optional[str], service_base_path: Optional[str]) -> str:
"""Sets the service URL based on service parameters derived from env vars. PR environments use internal-dev while
we also default to this environment. The only other exceptions are preprod which maps to the Apigee int environment
and prod which does not have a subdomain."""
if not service_base_path:
service_base_path = DEFAULT_BASE_PATH

if service_env is None or is_pr_env(service_env):
subdomain = "internal-dev."
elif service_env == "preprod":
subdomain = "int."
elif service_env == "prod":
subdomain = ""
else:
subdomain = f"{service_env}."

return f"https://{subdomain}api.service.nhs.uk/{service_base_path}"


def is_pr_env(service_env: Optional[str]) -> bool:
return service_env is not None and service_env.startswith(PR_ENV_PREFIX)


def create_url_for_bundle_link(
Expand Down
29 changes: 0 additions & 29 deletions lambdas/backend/tests/service/test_search_url_helper.py
Original file line number Diff line number Diff line change
@@ -1,29 +0,0 @@
"""Tests for the search_url_helper file"""

import unittest

from service.search_url_helper import get_service_url


class TestServiceUrl(unittest.TestCase):
def test_get_service_url(self):
"""it should create service url"""
test_cases = [
("pr-123", "https://internal-dev.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4"),
(None, "https://internal-dev.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4"),
("preprod", "https://int.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4"),
("prod", "https://api.service.nhs.uk/immunisation-fhir-api/FHIR/R4"),
("ref", "https://ref.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4"),
("internal-dev", "https://internal-dev.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4"),
("internal-qa", "https://internal-qa.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4"),
]
mock_base_path = "immunisation-fhir-api/FHIR/R4"

for mock_env, expected in test_cases:
with self.subTest(mock_env=mock_env, expected=expected):
self.assertEqual(get_service_url(mock_env, mock_base_path), expected)

def test_get_service_url_uses_default_path_when_not_provided(self):
self.assertEqual(
get_service_url(None, None), "https://internal-dev.api.service.nhs.uk/immunisation-fhir-api/FHIR/R4"
)
2 changes: 1 addition & 1 deletion lambdas/id_sync/src/pds_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from os_vars import get_pds_env

pds_env = get_pds_env()
safe_tmp_dir = tempfile.mkdtemp(dir="/tmp") # NOSONAR(S5443)
safe_tmp_dir = tempfile.mkdtemp(dir="/tmp")


# Get Patient details from external service PDS using NHS number from MNS notification
Expand Down
3 changes: 2 additions & 1 deletion lambdas/id_sync/src/record_processor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from typing import Any, Dict

from common.api_clients.get_pds_details import pds_get_patient_details
from common.clients import logger
from exceptions.id_sync_exception import IdSyncException
from ieds_db_operations import (
Expand All @@ -9,7 +10,7 @@
get_items_from_patient_id,
ieds_update_patient_id,
)
from pds_details import get_nhs_number_from_pds_resource, pds_get_patient_details
from pds_details import get_nhs_number_from_pds_resource
from utils import make_status


Expand Down
Loading