Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
09ffcff
ELI-578 consumer_id - campaign_config mapping
Karthikeyannhs Dec 16, 2025
dfcff79
ELI-578 added dummy consumer id
Karthikeyannhs Dec 18, 2025
18736fa
ELI-578 local stack configuration
Karthikeyannhs Dec 18, 2025
b9fe63b
ELI-578 wip test
Karthikeyannhs Dec 18, 2025
bcd1d8d
ELI-578 unit test fixed
Karthikeyannhs Dec 18, 2025
60b4954
ELI-578 consumer_id error validation
Karthikeyannhs Dec 19, 2025
d840389
ELI-578 intergation testing
Karthikeyannhs Dec 19, 2025
b7561a0
ELI-578 lambda
Karthikeyannhs Dec 19, 2025
aefbe33
ELI-578 integration test
Karthikeyannhs Dec 19, 2025
8f4e28a
ELI-578 Integration test to check if consumer has campaign mappings
Karthikeyannhs Dec 22, 2025
4afb122
ELI-578 Unit tests
Karthikeyannhs Dec 22, 2025
2dacd5e
added test : customer requesting for campaign that is not mapped
Karthikeyannhs Jan 2, 2026
72f6d0c
consumer with no campaign mapping is valid
Karthikeyannhs Jan 5, 2026
bcee4b1
ELI-578 consumer_id - campaign_config mapping
Karthikeyannhs Dec 16, 2025
6029398
ELI-578 added dummy consumer id
Karthikeyannhs Dec 18, 2025
714433d
ELI-578 local stack configuration
Karthikeyannhs Dec 18, 2025
fcc68be
ELI-578 wip test
Karthikeyannhs Dec 18, 2025
510df8d
ELI-578 unit test fixed
Karthikeyannhs Dec 18, 2025
91a2a09
ELI-578 consumer_id error validation
Karthikeyannhs Dec 19, 2025
4089abc
ELI-578 intergation testing
Karthikeyannhs Dec 19, 2025
1279f2d
ELI-578 lambda
Karthikeyannhs Dec 19, 2025
425f6e7
ELI-578 integration test
Karthikeyannhs Dec 19, 2025
3b58fc9
ELI-578 Integration test to check if consumer has campaign mappings
Karthikeyannhs Dec 22, 2025
7812be0
ELI-578 Unit tests
Karthikeyannhs Dec 22, 2025
569dc5f
added test : customer requesting for campaign that is not mapped
Karthikeyannhs Jan 2, 2026
fbb902d
consumer with no campaign mapping is valid
Karthikeyannhs Jan 5, 2026
d30b4df
revert
Karthikeyannhs Jan 5, 2026
6591256
Merge remote-tracking branch 'origin/ELI-578/consumer-campaign-config…
Karthikeyannhs Jan 5, 2026
bca6215
linting
Karthikeyannhs Jan 5, 2026
5160d98
Merge branch 'main' into ELI-578/consumer-campaign-config-mappings
ayeshalshukri1-nhs Jan 6, 2026
92dd24e
removed unused code
Karthikeyannhs Jan 6, 2026
f25e29e
added CONSUMER_ID_NOT_PROVIDED_ERROR back
Karthikeyannhs Jan 6, 2026
80704fe
more test cases
Karthikeyannhs Jan 6, 2026
5407820
hardcodes values are converted to fixtures
Karthikeyannhs Jan 6, 2026
ca0aef8
fixtures
Karthikeyannhs Jan 6, 2026
3f30502
fixtures
Karthikeyannhs Jan 6, 2026
7751ae8
linting
Karthikeyannhs Jan 6, 2026
b6b9242
Merge branch 'main' into ELI-578/consumer-campaign-config-mappings
ayeshalshukri1-nhs Jan 7, 2026
4fea2dc
more scenarios
Karthikeyannhs Jan 7, 2026
34827d1
added consumer-id header to the requests in unit tests
Karthikeyannhs Jan 7, 2026
3d1b970
linting
Karthikeyannhs Jan 7, 2026
a14db73
Added vacc request placeholder in tests.
ayeshalshukri1-nhs Jan 7, 2026
1b10f85
fixed linting.
ayeshalshukri1-nhs Jan 7, 2026
45522f8
added sample consumer-mapping file
Karthikeyannhs Jan 12, 2026
c507ee2
Merge branch 'main' into ELI-578/consumer-campaign-config-mappings
Karthikeyannhs Jan 12, 2026
926edc8
test_valid_response_when_consumer_has_a_valid_campaign_config_mapping…
Karthikeyannhs Jan 12, 2026
cbbae6f
multiple campaigns for same target
Karthikeyannhs Jan 12, 2026
f525d01
multiple campaigns for same target
Karthikeyannhs Jan 12, 2026
fa80df6
consumer config structure modification
Karthikeyannhs Jan 13, 2026
dbef36c
Merge branch 'main' into ELI-578/consumer-campaign-config-mappings
Karthikeyannhs Jan 13, 2026
82ee079
lint fix
Karthikeyannhs Jan 13, 2026
2045e08
consumer mapping - schema
Karthikeyannhs Jan 13, 2026
5fff049
Terraform consumer mapping bucket
Karthikeyannhs Jan 13, 2026
bcadb02
Terraform consumer mapping bucket policy
Karthikeyannhs Jan 13, 2026
6eaca30
more linting
Karthikeyannhs Jan 13, 2026
a1ad018
fix s3
Karthikeyannhs Jan 13, 2026
6c70813
Merge branch 'main' into ELI-578/consumer-campaign-config-mappings
Karthikeyannhs Jan 13, 2026
3d6b127
fix s3
Karthikeyannhs Jan 13, 2026
8afefa9
revert to S3ReadAccess
Karthikeyannhs Jan 13, 2026
34073c6
iam permissions
Karthikeyannhs Jan 13, 2026
bd8b5c2
more tests
Karthikeyannhs Jan 14, 2026
0e6c784
name correction
Karthikeyannhs Jan 14, 2026
fe9a2da
one more test scenario
Karthikeyannhs Jan 14, 2026
e7d8862
one more test scenario
Karthikeyannhs Jan 14, 2026
f513f31
Merge branch 'main' into ELI-578/consumer-campaign-config-mappings
Karthikeyannhs Jan 21, 2026
769989b
added comments
Karthikeyannhs Jan 21, 2026
f5a52ca
test case - check best status is picked from the iterations
Karthikeyannhs Jan 21, 2026
68f2cca
test cases fixed
Karthikeyannhs Jan 21, 2026
3435844
fix lambda test case
Karthikeyannhs Jan 22, 2026
21c36bd
fix lambda test case
Karthikeyannhs Jan 22, 2026
afcd901
Merge branch 'main' into ELI-578/consumer-campaign-config-mappings
ayeshalshukri1-nhs Jan 22, 2026
815b435
fix latest testcases without consumer mapping
Karthikeyannhs Jan 22, 2026
6965840
removed unsued conftest
Karthikeyannhs Jan 22, 2026
72f41b0
renamed conftests
Karthikeyannhs Jan 22, 2026
6fee706
fixed comment
Karthikeyannhs Jan 22, 2026
b2a1171
Merge branch 'main' into ELI-578/consumer-campaign-config-mappings
ayeshalshukri1-nhs Jan 22, 2026
9b3bab6
no reference to CONSUMER_ID in constant in tests
Karthikeyannhs Jan 22, 2026
3c9a534
integration tests
Karthikeyannhs Jan 22, 2026
c18ff42
integration tests
Karthikeyannhs Jan 22, 2026
d681b59
checkov suggestions
Karthikeyannhs Jan 23, 2026
37c919d
checkov suggestions
Karthikeyannhs Jan 23, 2026
684b971
Revert "checkov suggestions"
Karthikeyannhs Jan 23, 2026
a4ee06d
Revert "checkov suggestions"
Karthikeyannhs Jan 23, 2026
e0da30d
revert - checkov suggestions
Karthikeyannhs Jan 23, 2026
1d05812
checkov skips
Karthikeyannhs Jan 23, 2026
230de1d
checkov skips
Karthikeyannhs Jan 23, 2026
baf882b
renamed "Campaign" to CampaignConfigId"
Karthikeyannhs Jan 23, 2026
54482e2
"CampaignConfigID" is the campaign id in consumer mapping
Karthikeyannhs Jan 23, 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
17 changes: 9 additions & 8 deletions infrastructure/modules/lambda/lambda.tf
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONSUMER_MAPPING_BUCKET_NAME = var.eligibility_consumer_mappings_bucket_name is the change here. Rest are just lint (spacings)

Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {

environment {
variables = {
PERSON_TABLE_NAME = var.eligibility_status_table_name,
RULES_BUCKET_NAME = var.eligibility_rules_bucket_name,
KINESIS_AUDIT_STREAM_TO_S3 = var.kinesis_audit_stream_to_s3_name
ENV = var.environment
LOG_LEVEL = var.log_level
ENABLE_XRAY_PATCHING = var.enable_xray_patching
API_DOMAIN_NAME = var.api_domain_name
HASHING_SECRET_NAME = var.hashing_secret_name
PERSON_TABLE_NAME = var.eligibility_status_table_name,
RULES_BUCKET_NAME = var.eligibility_rules_bucket_name,
CONSUMER_MAPPING_BUCKET_NAME = var.eligibility_consumer_mappings_bucket_name,
KINESIS_AUDIT_STREAM_TO_S3 = var.kinesis_audit_stream_to_s3_name
ENV = var.environment
LOG_LEVEL = var.log_level
ENABLE_XRAY_PATCHING = var.enable_xray_patching
API_DOMAIN_NAME = var.api_domain_name
HASHING_SECRET_NAME = var.hashing_secret_name
}
}

Expand Down
5 changes: 5 additions & 0 deletions infrastructure/modules/lambda/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ variable "eligibility_rules_bucket_name" {
type = string
}

variable "eligibility_consumer_mappings_bucket_name" {
description = "consumer mappings bucket name"
type = string
}

variable "eligibility_status_table_name" {
description = "eligibility datastore table name"
type = string
Expand Down
97 changes: 96 additions & 1 deletion infrastructure/stacks/api-layer/iam_policies.tf
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,60 @@
}
}

# Policy doc for S3 Consumer Mappings bucket
data "aws_iam_policy_document" "s3_consumer_mapping_bucket_policy" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lambda_consumer_mapping_read_policy might be clearer? But probably needs a wider revisit to ensure all our terraform names are sensible / explain what the purpose it - a lot of my stuff from early on is likely confusing to me now....

statement {
sid = "AllowSSLRequestsOnly"
actions = [
"s3:GetObject",
"s3:ListBucket",
]
resources = [
module.s3_consumer_mappings_bucket.storage_bucket_arn,
"${module.s3_consumer_mappings_bucket.storage_bucket_arn}/*",
]
condition {
test = "Bool"
values = ["true"]
variable = "aws:SecureTransport"
}
}
}

# ensure only secure transport is allowed

resource "aws_s3_bucket_policy" "consumer_mapping_s3_bucket" {
bucket = module.s3_consumer_mappings_bucket.storage_bucket_id
policy = data.aws_iam_policy_document.consumer_mapping_s3_bucket_policy.json
}

data "aws_iam_policy_document" "consumer_mapping_s3_bucket_policy" {
statement {
sid = "AllowSslRequestsOnly"
actions = [
"s3:*",
]
effect = "Deny"
resources = [
module.s3_consumer_mappings_bucket.storage_bucket_arn,
"${module.s3_consumer_mappings_bucket.storage_bucket_arn}/*",
]
principals {
type = "*"
identifiers = ["*"]
}
condition {
test = "Bool"
values = [
"false",
]

variable = "aws:SecureTransport"
}
}
}

# audit bucket
resource "aws_s3_bucket_policy" "audit_s3_bucket" {
bucket = module.s3_audit_bucket.storage_bucket_id
policy = data.aws_iam_policy_document.audit_s3_bucket_policy.json
Expand Down Expand Up @@ -136,12 +190,18 @@
}

# Attach s3 read policy to Lambda role
resource "aws_iam_role_policy" "lambda_s3_read_policy" {
resource "aws_iam_role_policy" "lambda_s3_rules_read_policy" {
name = "S3ReadAccess"
role = aws_iam_role.eligibility_lambda_role.id
policy = data.aws_iam_policy_document.s3_rules_bucket_policy.json
}

resource "aws_iam_role_policy" "lambda_s3_mapping_read_policy" {
name = "S3ConsumerMappingReadAccess"
role = aws_iam_role.eligibility_lambda_role.id
policy = data.aws_iam_policy_document.s3_consumer_mapping_bucket_policy.json
}

# Attach s3 write policy to kinesis firehose role
resource "aws_iam_role_policy" "kinesis_firehose_s3_write_policy" {
name = "S3WriteAccess"
Expand Down Expand Up @@ -290,6 +350,41 @@
policy = data.aws_iam_policy_document.s3_rules_kms_key_policy.json
}

data "aws_iam_policy_document" "s3_consumer_mapping_kms_key_policy" {
#checkov:skip=CKV_AWS_111: Root user needs full KMS key management
#checkov:skip=CKV_AWS_356: Root user needs full KMS key management
#checkov:skip=CKV_AWS_109: Root user needs full KMS key management
statement {
sid = "EnableIamUserPermissions"
effect = "Allow"
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
}
actions = ["kms:*"]
resources = ["*"]
}

#checkov:skip=CKV_AWS_111: Permission boundary enforces restrictions for this policy
#checkov:skip=CKV_AWS_356: Permission boundary enforces resource-level controls
#checkov:skip=CKV_AWS_109: Permission boundary governs write-access constraints
statement {
sid = "AllowLambdaDecrypt"
effect = "Allow"
principals {
type = "AWS"
identifiers = [aws_iam_role.eligibility_lambda_role.arn]
}
actions = ["kms:Decrypt"]
resources = ["*"]
}
}

resource "aws_kms_key_policy" "s3_consumer_mapping_kms_key" {
key_id = module.s3_consumer_mappings_bucket.storage_bucket_kms_key_id
policy = data.aws_iam_policy_document.s3_consumer_mapping_kms_key_policy.json
}

resource "aws_iam_role_policy" "splunk_firehose_policy" {
#checkov:skip=CKV_AWS_290: Firehose requires write access to dynamic log streams without static constraints
#checkov:skip=CKV_AWS_355: Firehose logging requires wildcard resource for CloudWatch log groups/streams
Expand Down
41 changes: 21 additions & 20 deletions infrastructure/stacks/api-layer/lambda.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,28 @@ data "aws_subnet" "private_subnets" {
}

module "eligibility_signposting_lambda_function" {
source = "../../modules/lambda"
eligibility_lambda_role_arn = aws_iam_role.eligibility_lambda_role.arn
eligibility_lambda_role_name = aws_iam_role.eligibility_lambda_role.name
workspace = local.workspace
environment = var.environment
runtime = "python3.13"
lambda_func_name = "${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}eligibility_signposting_api"
source = "../../modules/lambda"
eligibility_lambda_role_arn = aws_iam_role.eligibility_lambda_role.arn
eligibility_lambda_role_name = aws_iam_role.eligibility_lambda_role.name
workspace = local.workspace
environment = var.environment
runtime = "python3.13"
lambda_func_name = "${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}eligibility_signposting_api"
security_group_ids = [data.aws_security_group.main_sg.id]
vpc_intra_subnets = [for v in data.aws_subnet.private_subnets : v.id]
file_name = "../../../dist/lambda.zip"
handler = "eligibility_signposting_api.app.lambda_handler"
eligibility_rules_bucket_name = module.s3_rules_bucket.storage_bucket_name
eligibility_status_table_name = module.eligibility_status_table.table_name
kinesis_audit_stream_to_s3_name = module.eligibility_audit_firehose_delivery_stream.firehose_stream_name
hashing_secret_name = module.secrets_manager.aws_hashing_secret_name
lambda_insights_extension_version = 38
log_level = "INFO"
enable_xray_patching = "true"
stack_name = local.stack_name
provisioned_concurrency_count = 5
api_domain_name = local.api_domain_name
vpc_intra_subnets = [for v in data.aws_subnet.private_subnets : v.id]
file_name = "../../../dist/lambda.zip"
handler = "eligibility_signposting_api.app.lambda_handler"
eligibility_rules_bucket_name = module.s3_rules_bucket.storage_bucket_name
eligibility_consumer_mappings_bucket_name = module.s3_consumer_mappings_bucket.storage_bucket_name
eligibility_status_table_name = module.eligibility_status_table.table_name
kinesis_audit_stream_to_s3_name = module.eligibility_audit_firehose_delivery_stream.firehose_stream_name
hashing_secret_name = module.secrets_manager.aws_hashing_secret_name
lambda_insights_extension_version = 38
log_level = "INFO"
enable_xray_patching = "true"
stack_name = local.stack_name
provisioned_concurrency_count = 5
api_domain_name = local.api_domain_name
}

# -----------------------------------------------------------------------------
Expand Down
9 changes: 9 additions & 0 deletions infrastructure/stacks/api-layer/s3_buckets.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ module "s3_rules_bucket" {
workspace = terraform.workspace
}

module "s3_consumer_mappings_bucket" {
source = "../../modules/s3"
bucket_name = "eli-consumer-map"
environment = var.environment
project_name = var.project_name
stack_name = local.stack_name
workspace = terraform.workspace
}

module "s3_audit_bucket" {
source = "../../modules/s3"
bucket_name = "eli-audit"
Expand Down
8 changes: 8 additions & 0 deletions src/eligibility_signposting_api/common/api_error_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,11 @@ def log_and_generate_response(
fhir_error_code=FHIRSpineErrorCode.ACCESS_DENIED,
fhir_display_message="Access has been denied to process this request.",
)

CONSUMER_ID_NOT_PROVIDED_ERROR = APIErrorResponse(
status_code=HTTPStatus.FORBIDDEN,
fhir_issue_code=FHIRIssueCode.FORBIDDEN,
fhir_issue_severity=FHIRIssueSeverity.ERROR,
fhir_error_code=FHIRSpineErrorCode.ACCESS_DENIED,
fhir_display_message="Access has been denied to process this request.",
)
10 changes: 9 additions & 1 deletion src/eligibility_signposting_api/common/request_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from flask.typing import ResponseReturnValue

from eligibility_signposting_api.common.api_error_response import (
CONSUMER_ID_NOT_PROVIDED_ERROR,
INVALID_CATEGORY_ERROR,
INVALID_CONDITION_FORMAT_ERROR,
INVALID_INCLUDE_ACTIONS_ERROR,
NHS_NUMBER_ERROR,
)
from eligibility_signposting_api.config.constants import NHS_NUMBER_HEADER
from eligibility_signposting_api.config.constants import CONSUMER_ID, NHS_NUMBER_HEADER

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -50,6 +51,13 @@ def validate_request_params() -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> ResponseReturnValue: # noqa:ANN002,ANN003
consumer_id = request.headers.get(CONSUMER_ID)
if not consumer_id:
message = "You are not authorised to request"
return CONSUMER_ID_NOT_PROVIDED_ERROR.log_and_generate_response(
log_message=message, diagnostics=message
)

path_nhs_number = str(kwargs.get("nhs_number")) if kwargs.get("nhs_number") else None

if not path_nhs_number:
Expand Down
3 changes: 3 additions & 0 deletions src/eligibility_signposting_api/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
def config() -> dict[str, Any]:
person_table_name = TableName(os.getenv("PERSON_TABLE_NAME", "test_eligibility_datastore"))
rules_bucket_name = BucketName(os.getenv("RULES_BUCKET_NAME", "test-rules-bucket"))
consumer_mapping_bucket_name = BucketName(os.getenv("CONSUMER_MAPPING_BUCKET_NAME", "test-consumer-mapping-bucket"))
audit_bucket_name = BucketName(os.getenv("AUDIT_BUCKET_NAME", "test-audit-bucket"))
hashing_secret_name = HashSecretName(os.getenv("HASHING_SECRET_NAME", "test_secret"))
aws_default_region = AwsRegion(os.getenv("AWS_DEFAULT_REGION", "eu-west-1"))
Expand All @@ -41,6 +42,7 @@ def config() -> dict[str, Any]:
"s3_endpoint": None,
"rules_bucket_name": rules_bucket_name,
"audit_bucket_name": audit_bucket_name,
"consumer_mapping_bucket_name": consumer_mapping_bucket_name,
"firehose_endpoint": None,
"kinesis_audit_stream_to_s3": kinesis_audit_stream_to_s3,
"enable_xray_patching": enable_xray_patching,
Expand All @@ -59,6 +61,7 @@ def config() -> dict[str, Any]:
"s3_endpoint": URL(os.getenv("S3_ENDPOINT", local_stack_endpoint)),
"rules_bucket_name": rules_bucket_name,
"audit_bucket_name": audit_bucket_name,
"consumer_mapping_bucket_name": consumer_mapping_bucket_name,
"firehose_endpoint": URL(os.getenv("FIREHOSE_ENDPOINT", local_stack_endpoint)),
"kinesis_audit_stream_to_s3": kinesis_audit_stream_to_s3,
"enable_xray_patching": enable_xray_patching,
Expand Down
1 change: 1 addition & 0 deletions src/eligibility_signposting_api/config/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
URL_PREFIX = "patient-check"
RULE_STOP_DEFAULT = False
NHS_NUMBER_HEADER = "nhs-login-nhs-number"
CONSUMER_ID = "nhsd-application-id" # "Nhsd-Application-Id"
ALLOWED_CONDITIONS = Literal["COVID", "FLU", "MMR", "RSV"]
17 changes: 17 additions & 0 deletions src/eligibility_signposting_api/model/consumer_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import NewType

from pydantic import BaseModel, Field, RootModel

from eligibility_signposting_api.model.campaign_config import CampaignID

ConsumerId = NewType("ConsumerId", str)


class ConsumerCampaign(BaseModel):
campaign_config_id: CampaignID = Field(alias="CampaignConfigID")
description: str | None = Field(default=None, alias="Description")


class ConsumerMapping(RootModel[dict[ConsumerId, list[ConsumerCampaign]]]):
def get(self, key: ConsumerId, default: list[ConsumerCampaign] | None = None) -> list[ConsumerCampaign] | None:
return self.root.get(key, default)
41 changes: 41 additions & 0 deletions src/eligibility_signposting_api/repos/consumer_mapping_repo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import json
from typing import Annotated, NewType

from botocore.client import BaseClient
from wireup import Inject, service

from eligibility_signposting_api.model.campaign_config import CampaignID
from eligibility_signposting_api.model.consumer_mapping import ConsumerId, ConsumerMapping

BucketName = NewType("BucketName", str)


@service
class ConsumerMappingRepo:
"""Repository class for Consumer Mapping"""

def __init__(
self,
s3_client: Annotated[BaseClient, Inject(qualifier="s3")],
bucket_name: Annotated[BucketName, Inject(param="consumer_mapping_bucket_name")],
) -> None:
super().__init__()
self.s3_client = s3_client
self.bucket_name = bucket_name

def get_permitted_campaign_ids(self, consumer_id: ConsumerId) -> list[CampaignID] | None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's probably worth caching the consumer config, it's likely to be v. slow changing

Copy link
Contributor Author

@Karthikeyannhs Karthikeyannhs Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can look into this along with the "nhs-product-id" implementation userstory.
Because there are few edgecases which we have to deal with.

Copy link
Contributor

@ayeshalshukri1-nhs ayeshalshukri1-nhs Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a good idea to look at caching for both consumer mapping, and campaign config.
I am not sure of the consequences of caching one, without the other.
Example, we cached consumer to campaign id, but campaign changes (id, maybe).

We also have to think about cache invalidation, as we don't want some warm lambda running with old config, and new lambdas running new config.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More generally, an alternative might be a new dynamoDB table - latency would hopefully be much lower as single key lookups are v speedy vs. s3

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we might look at this in terms of revisiting where we store campaign config too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an interesting alternative,
but I would suggest reading from dynamoDB be part of another story, that would maybe look at moving both consumer mapping and campaign config to dynamoDB (so we follow a consistent pattern for all read config).

objects = self.s3_client.list_objects(Bucket=self.bucket_name).get("Contents")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we set the filename as a constant (e.g. consumer_mapping.json) and avoid this list object (e.g. just do the get?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have followed the same pattern that has been used for campaign config.

I think, we will need to revisit this as part of a review on the management of consumer mapping and campaign config.
Things to consider; do we want all consumers mapped in one file, or easier to manage one file per consumer? etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree this whole thing might need a rethink. We're likely adding another 50-100ms of latency with this MR, so still just about sits under the arbitrary 200ms.

'do we want all consumers mapped in one file, or easier to manage one file per consumer? etc.' - at the moment the approach only supports the former, so your choice is to stick with the list_objects latency and rework if we want multiple files (one per consumer) or go with the single file approach and then rework to support multiple files?

I think we'd want a fast following ticket to resolve this if we leave it as is

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(my temptation is read in one file into memory and deal with it - we can be smart about managing it e.g. have a mechanism to independently manage each consumer and generate a single file)


if not objects:
return None

consumer_mappings_obj = objects[0]
response = self.s3_client.get_object(Bucket=self.bucket_name, Key=consumer_mappings_obj["Key"])
body = response["Body"].read()

mapping_result = ConsumerMapping.model_validate(json.loads(body)).get(consumer_id)

if mapping_result is None:
return None

return [item.campaign_config_id for item in mapping_result]
Loading
Loading