Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4906c7d
CCM-13303: Update Generate Report schema to require a reportDate
simonlabarere Feb 4, 2026
e6d2b58
CCM-13304: Initial commit
simonlabarere Feb 10, 2026
a07bc6c
Merge branch 'main' into feature/CCM-13304_generate_reports
simonlabarere Feb 10, 2026
be5fef3
CCM-13304: Fix existing test
simonlabarere Feb 10, 2026
5c2c8e8
CCM-13304: Fix existing test
simonlabarere Feb 10, 2026
5b64e91
Merge branch 'main' into feature/CCM-13304_generate_reports
simonlabarere Feb 10, 2026
d753b4e
CCM-13304: Unit tests
simonlabarere Feb 10, 2026
b3cd51d
CCM-13304: Unit tests
simonlabarere Feb 10, 2026
83fc83e
CCM-13304: Fix terraform
simonlabarere Feb 10, 2026
6ab35e2
CCM-13304: Bundle SQL in lambda
simonlabarere Feb 11, 2026
4c987cb
CCM-13304: Bundle SQL in lambda
simonlabarere Feb 11, 2026
0a9383d
CCM-13304: Add permission to run Athen queries
simonlabarere Feb 11, 2026
52dd47a
CCM-13304: Add letter status to athena table
simonlabarere Feb 11, 2026
af74a2a
CCM-13304: Fix Athena Workgroup arn
simonlabarere Feb 11, 2026
9f0938f
CCM-13304: Update report bucket permission for Athena
simonlabarere Feb 11, 2026
e6f00cb
CCM-13304: Add letter status to athena table
simonlabarere Feb 11, 2026
94ba7f7
CCM-13304: Update query
simonlabarere Feb 11, 2026
0277b56
CCM-13304: Update query
simonlabarere Feb 11, 2026
b8bb5bc
CCM-13304: Query fix attempt
simonlabarere Feb 11, 2026
ee60d9e
CCM-13304: Query fix attempt
simonlabarere Feb 11, 2026
8a521d2
CCM-13304: update permissions to run athena query
simonlabarere Feb 11, 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
4 changes: 4 additions & 0 deletions infrastructure/terraform/components/dl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ No requirements.
| <a name="input_apim_auth_token_url"></a> [apim\_auth\_token\_url](#input\_apim\_auth\_token\_url) | URL to generate an APIM auth token | `string` | `"https://int.api.service.nhs.uk/oauth2/token"` | no |
| <a name="input_apim_base_url"></a> [apim\_base\_url](#input\_apim\_base\_url) | The URL used to send requests to PDM | `string` | `"https://int.api.service.nhs.uk"` | no |
| <a name="input_apim_keygen_schedule"></a> [apim\_keygen\_schedule](#input\_apim\_keygen\_schedule) | Schedule to refresh key pairs if necessary | `string` | `"cron(0 14 * * ? *)"` | no |
| <a name="input_athena_query_max_polling_attemps"></a> [athena\_query\_max\_polling\_attemps](#input\_athena\_query\_max\_polling\_attemps) | The number of times athena will be polled to check if a query is completed | `number` | `50` | no |
| <a name="input_athena_query_polling_time_seconds"></a> [athena\_query\_polling\_time\_seconds](#input\_athena\_query\_polling\_time\_seconds) | The amount of time in seconds to wait between each athena poll | `number` | `15` | no |
| <a name="input_aws_account_id"></a> [aws\_account\_id](#input\_aws\_account\_id) | The AWS Account ID (numeric) | `string` | n/a | yes |
| <a name="input_component"></a> [component](#input\_component) | The variable encapsulating the name of this component | `string` | `"dl"` | no |
| <a name="input_core_notify_url"></a> [core\_notify\_url](#input\_core\_notify\_url) | The URL used to send requests to Notify | `string` | `"https://sandbox.api.service.nhs.uk"` | no |
Expand Down Expand Up @@ -59,6 +61,7 @@ No requirements.
| <a name="module_print_analyser"></a> [print\_analyser](#module\_print\_analyser) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_print_status_handler"></a> [print\_status\_handler](#module\_print\_status\_handler) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_report_event_transformer"></a> [report\_event\_transformer](#module\_report\_event\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_report_generator"></a> [report\_generator](#module\_report\_generator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-lambda.zip | n/a |
| <a name="module_report_scheduler"></a> [report\_scheduler](#module\_report\_scheduler) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| <a name="module_s3bucket_cf_logs"></a> [s3bucket\_cf\_logs](#module\_s3bucket\_cf\_logs) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-s3bucket.zip | n/a |
| <a name="module_s3bucket_file_quarantine"></a> [s3bucket\_file\_quarantine](#module\_s3bucket\_file\_quarantine) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-s3bucket.zip | n/a |
Expand All @@ -77,6 +80,7 @@ No requirements.
| <a name="module_sqs_pdm_uploader"></a> [sqs\_pdm\_uploader](#module\_sqs\_pdm\_uploader) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| <a name="module_sqs_print_analyser"></a> [sqs\_print\_analyser](#module\_sqs\_print\_analyser) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| <a name="module_sqs_print_status_handler"></a> [sqs\_print\_status\_handler](#module\_sqs\_print\_status\_handler) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| <a name="module_sqs_report_generator"></a> [sqs\_report\_generator](#module\_sqs\_report\_generator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| <a name="module_sqs_scanner"></a> [sqs\_scanner](#module\_sqs\_scanner) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| <a name="module_sqs_ttl"></a> [sqs\_ttl](#module\_sqs\_ttl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| <a name="module_sqs_ttl_handle_expiry_errors"></a> [sqs\_ttl\_handle\_expiry\_errors](#module\_sqs\_ttl\_handle\_expiry\_errors) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
resource "aws_cloudwatch_event_rule" "generate_report" {
name = "${local.csi}-generate-report"
description = "Generate Report event rule"
event_bus_name = aws_cloudwatch_event_bus.main.name
event_pattern = jsonencode({
"detail" : {
"type" : [
"uk.nhs.notify.digital.letters.reporting.generate.report.v1"
],
}
})
}

resource "aws_cloudwatch_event_target" "generate_report_report_generator" {
rule = aws_cloudwatch_event_rule.generate_report.name
arn = module.sqs_report_generator.sqs_queue_arn
event_bus_name = aws_cloudwatch_event_bus.main.name
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ resource "aws_glue_catalog_table" "event_record" {
name = "type"
type = "string"
}
columns {
name = "letterstatus"
type = "string"
}
}

partition_keys {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
resource "aws_lambda_event_source_mapping" "report_generator" {
event_source_arn = module.sqs_report_generator.sqs_queue_arn
function_name = module.report_generator.function_name
batch_size = var.queue_batch_size
maximum_batching_window_in_seconds = var.queue_batch_window_seconds

function_response_types = [
"ReportBatchItemFailures"
]
}
1 change: 1 addition & 0 deletions infrastructure/terraform/components/dl/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ locals {
apim_api_key_ssm_parameter_name = "/${var.component}/${var.environment}/apim/api_key"
apim_keystore_s3_bucket = "nhs-${var.aws_account_id}-${var.region}-${var.environment}-${var.component}-static-assets"
apim_private_key_ssm_parameter_name = "/${var.component}/${var.environment}/apim/private_key"
athena_reporting_database = "${local.csi}-reporting"
aws_lambda_functions_dir_path = "../../../../lambdas"
deploy_pdm_mock = var.enable_pdm_mock
firehose_output_path_prefix = "kinesis-firehose-output"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
module "report_generator" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-lambda.zip"

function_name = "report-generator"
description = "A function to generator reports from an event"

aws_account_id = var.aws_account_id
component = local.component
environment = var.environment
project = var.project
region = var.region
group = var.group

log_retention_in_days = var.log_retention_in_days
kms_key_arn = module.kms.key_arn

iam_policy_document = {
body = data.aws_iam_policy_document.report_generator_lambda.json
}

function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
function_code_base_path = local.aws_lambda_functions_dir_path
function_code_dir = "report-generator/dist"
function_include_common = true
handler_function_name = "handler"
runtime = "nodejs22.x"
memory = 128
timeout = 60
log_level = var.log_level

force_lambda_code_deploy = var.force_lambda_code_deploy
enable_lambda_insights = false

log_destination_arn = local.log_destination_arn
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = {
"ATHENA_WORKGROUP" = aws_athena_workgroup.reporting.name
"ATHENA_DATABASE" = local.athena_reporting_database
"EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn
"EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url
"MAX_POLL_LIMIT" = var.athena_query_max_polling_attemps
"REPORTING_BUCKET" = module.s3bucket_reporting.bucket
"REPORT_NAME" = "completed_communications"
"WAIT_FOR_IN_SECONDS" = var.athena_query_polling_time_seconds
}
}

data "aws_iam_policy_document" "report_generator_lambda" {
statement {
sid = "AllowS3Get"
effect = "Allow"

actions = [
"s3:PutObject",
"s3:GetObject",
"s3:GetBucketLocation",
"s3:ListBucket"
]

resources = [
"${module.s3bucket_reporting.arn}/*",
"${module.s3bucket_reporting.arn}"
]
}

statement {
sid = "KMSPermissions"
effect = "Allow"

actions = [
"kms:Decrypt",
"kms:GenerateDataKey",
]

resources = [
module.kms.key_arn,
]
}

statement {
sid = "AllowAthenaAccess"
effect = "Allow"

actions = [
"athena:StartQueryExecution",
"athena:GetQueryResults",
"athena:GetQueryExecution"
]

resources = [
"arn:aws:athena:${var.region}:${var.aws_account_id}:workgroup/${aws_athena_workgroup.reporting.name}"
]
}

statement {
sid = "AllowGlueAccess"
effect = "Allow"

actions = [
"glue:GetTable",
"glue:GetDatabase",
"glue:GetPartition",
"glue:GetPartitions",
]

resources = [
"arn:aws:glue:${var.region}:${var.aws_account_id}:catalog",
"arn:aws:glue:${var.region}:${var.aws_account_id}:database/${local.athena_reporting_database}",
"arn:aws:glue:${var.region}:${var.aws_account_id}:table/${local.athena_reporting_database}/*"
]
}

statement {
sid = "SQSPermissionsReportGeneratorQueue"
effect = "Allow"

actions = [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes",
"sqs:GetQueueUrl",
]

resources = [
module.sqs_report_generator.sqs_queue_arn,
]
}

statement {
sid = "PutEvents"
effect = "Allow"

actions = [
"events:PutEvents",
]

resources = [
aws_cloudwatch_event_bus.main.arn,
]
}

statement {
sid = "SQSPermissionsEventPublisherDLQ"
effect = "Allow"

actions = [
"sqs:SendMessage",
"sqs:SendMessageBatch",
]

resources = [
module.sqs_event_publisher_errors.sqs_queue_arn,
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module "sqs_report_generator" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip"

aws_account_id = var.aws_account_id
component = local.component
environment = var.environment
project = var.project
region = var.region
name = "report-generator"

sqs_kms_key_arn = module.kms.key_arn

visibility_timeout_seconds = 60

create_dlq = true

sqs_policy_overload = data.aws_iam_policy_document.sqs_report_generator.json
}

data "aws_iam_policy_document" "sqs_report_generator" {
statement {
sid = "AllowEventBridgeToSendMessage"
effect = "Allow"

principals {
type = "Service"
identifiers = ["events.amazonaws.com"]
}

actions = [
"sqs:SendMessage"
]

resources = [
"arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-report-generator-queue"
]

condition {
test = "ArnEquals"
variable = "aws:SourceArn"
values = [aws_cloudwatch_event_rule.generate_report.arn]
}
}
}
12 changes: 12 additions & 0 deletions infrastructure/terraform/components/dl/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,15 @@ variable "default_cloudwatch_event_bus_name" {
description = "The name of the default cloudwatch event bus. This is needed as GuardDuty Scan Result events are sent to the default bus"
default = "default"
}

variable "athena_query_max_polling_attemps" {
type = number
description = "The number of times athena will be polled to check if a query is completed"
default = 50
}

variable "athena_query_polling_time_seconds" {
type = number
description = "The amount of time in seconds to wait between each athena poll"
default = 15
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('Firehose Handler', () => {
senderId: digitalLettersEvent.data.senderId,
pageCount: digitalLettersEvent.data.pageCount,
supplierId: digitalLettersEvent.data.supplierId,
letterStatus: digitalLettersEvent.data.status,
time: digitalLettersEvent.time,
type: digitalLettersEvent.type,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const baseEvent = {
resourceId: 'a2bcbb42-ab7e-42b6-88d6-74f8d3ca4a09',
messageReference: 'ref1',
senderId: 'sender1',
status: 'DISPATCHED',
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function validateRecord(
}

function generateReportEvent(validatedRecord: ValidatedRecord): ReportEvent {
const { messageReference, pageCount, senderId, supplierId } =
const { messageReference, pageCount, senderId, status, supplierId } =
validatedRecord.event.data;
const { time, type } = validatedRecord.event;
const eventTime = new Date(time);
Expand All @@ -66,6 +66,7 @@ function generateReportEvent(validatedRecord: ValidatedRecord): ReportEvent {
senderId,
pageCount,
supplierId,
letterStatus: status,
time,
type,
};
Expand Down
2 changes: 2 additions & 0 deletions lambdas/report-event-transformer/src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const $DigitalLettersEvent = z.object({
senderId: z.string(),
pageCount: z.number().optional(),
supplierId: z.string().optional(),
status: z.string().optional(),
}),
time: z.string(),
type: z.string(),
Expand All @@ -18,6 +19,7 @@ export type FlatDigitalLettersEvent = {
senderId: string;
pageCount?: number;
supplierId?: string;
letterStatus?: string;
time: string;
type: string;
};
Expand Down
14 changes: 14 additions & 0 deletions lambdas/report-generator/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { baseJestConfig } from '../../jest.config.base';

const config = baseJestConfig;

config.coverageThreshold = {
global: {
branches: 84,
functions: 100,
lines: 95,
statements: -10,
},
};

export default config;
24 changes: 24 additions & 0 deletions lambdas/report-generator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"dependencies": {
"@aws-sdk/client-athena": "^3.984.0",
"digital-letters-events": "^0.0.1",
"utils": "^0.0.1"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
"@types/aws-lambda": "^8.10.155",
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"typescript": "^5.9.3"
},
"name": "nhs-notify-digital-letters-report-generator",
"private": true,
"scripts": {
"lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts && cp -r src/queries dist/",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test:unit": "jest",
"typecheck": "tsc --noEmit"
},
"version": "0.0.1"
}
Loading
Loading