Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions infrastructure/terraform/components/callbacks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

| Name | Source | Version |
|------|--------|---------|
| <a name="module_client_config_bucket"></a> [client\_config\_bucket](#module\_client\_config\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-s3bucket.zip | n/a |
| <a name="module_client_destination"></a> [client\_destination](#module\_client\_destination) | ../../modules/client-destination | n/a |
| <a name="module_client_transform_filter_lambda"></a> [client\_transform\_filter\_lambda](#module\_client\_transform\_filter\_lambda) | git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/lambda | v2.0.29 |
| <a name="module_kms"></a> [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-kms.zip | n/a |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module "client_transform_filter_lambda" {
kms_key_arn = module.kms.key_arn ## Requires shared kms module

iam_policy_document = {
body = data.aws_iam_policy_document.example_lambda.json
body = data.aws_iam_policy_document.client_transform_filter_lambda.json
}

function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
Expand All @@ -38,7 +38,7 @@ module "client_transform_filter_lambda" {
}
}

data "aws_iam_policy_document" "example_lambda" {
data "aws_iam_policy_document" "client_transform_filter_lambda" {
statement {
sid = "KMSPermissions"
effect = "Allow"
Expand All @@ -52,4 +52,17 @@ data "aws_iam_policy_document" "example_lambda" {
module.kms.key_arn, ## Requires shared kms module
]
}

statement {
sid = "S3ClientConfigReadAccess"
effect = "Allow"

actions = [
"s3:GetObject",
]

resources = [
"${module.client_config_bucket.arn}/*",
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
##
# S3 Bucket for Client Subscription Configuration
#
# Storage location for client subscription configurations loaded by Transform & Filter Lambda.
# Files are named {clientId}.json and contain ClientSubscriptionConfiguration arrays.
##

module "client_config_bucket" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-s3bucket.zip"

name = "subscription-config"

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

default_tags = merge(
local.default_tags,
{
Description = "Client subscription configuration storage"
}
)

kms_key_arn = module.kms.key_arn
force_destroy = false
versioning = true
object_ownership = "BucketOwnerPreferred"
bucket_key_enabled = true

policy_documents = [
data.aws_iam_policy_document.client_config_bucket.json
]
}

##
# S3 Bucket Policy
#
# Allows Transform & Filter Lambda to read configuration files
##

data "aws_iam_policy_document" "client_config_bucket" {
statement {
sid = "AllowLambdaReadAccess"
effect = "Allow"

principals {
type = "AWS"
identifiers = [module.client_transform_filter_lambda.iam_role_arn]
}

actions = [
"s3:GetObject",
]

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

statement {
sid = "DenyInsecureTransport"
effect = "Deny"

principals {
type = "*"
identifiers = ["*"]
}

actions = [
"s3:*",
]

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

condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
}
3 changes: 3 additions & 0 deletions lambdas/client-transform-filter-lambda/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const utilsJestConfig = {
...(baseJestConfig.coveragePathIgnorePatterns ?? []),
"zod-validators.ts",
],

// Mirror tsconfig's baseUrl: "src" - automatically resolves non-relative imports
modulePaths: ["<rootDir>/src"],
};

export default utilsJestConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { EventTypes } from "models/status-transition-event";

// coverage purposes
describe("EventTypes", () => {
it("should match the expected event type values", () => {
expect(EventTypes).toEqual({
MESSAGE_STATUS_TRANSITIONED:
"uk.nhs.notify.client-callbacks.message.status.transitioned.v1",
CHANNEL_STATUS_TRANSITIONED:
"uk.nhs.notify.client-callbacks.channel.status.transitioned.v1",
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Channel-level status transition event data.
*/
import type { Channel } from "models/channel-types";
import type { ChannelStatus, SupplierStatus } from "models/status-types";

export interface ChannelStatusData {
messageId: string;
messageReference: string;
channel: Channel;
channelStatus: ChannelStatus;
channelStatusDescription?: string;
channelFailureReasonCode?: string;
supplierStatus: SupplierStatus;
cascadeType: "primary" | "secondary";
cascadeOrder: number;
timestamp: string;
retryCount: number;

clientId: string;
previousChannelStatus?: ChannelStatus;
previousSupplierStatus?: SupplierStatus;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Communication channel types
*/
export type Channel = "NHSAPP" | "EMAIL" | "SMS" | "LETTER";
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Message/Channel Status Callback payload delivered to client webhooks.
*/

import type { RoutingPlan } from "models/routing-plan";
import type { Channel } from "models/channel-types";
import type {
ChannelStatus,
MessageStatus,
SupplierStatus,
} from "models/status-types";

export type ClientChannel = Lowercase<Channel>;
export type ClientMessageStatus = Lowercase<MessageStatus>;
export type ClientChannelStatus = Lowercase<ChannelStatus>;
export type ClientSupplierStatus = Lowercase<SupplierStatus>;

export interface ClientCallbackPayload {
data: CallbackItem[];
}

export interface CallbackItem {
type: "MessageStatus" | "ChannelStatus";
attributes: MessageStatusAttributes | ChannelStatusAttributes;
links: {
message: string;
};
meta: {
idempotencyKey: string;
};
}

export interface MessageStatusAttributes {
messageId: string;
messageReference: string;
messageStatus: ClientMessageStatus;
messageStatusDescription?: string;
messageFailureReasonCode?: string;
channels: {
type: ClientChannel;
channelStatus: ClientChannelStatus;
}[];
timestamp: string;
routingPlan: RoutingPlan;
}

export interface ChannelStatusAttributes {
messageId: string;
messageReference: string;
cascadeType: "primary" | "secondary";
cascadeOrder: number;
channel: ClientChannel;
channelStatus: ClientChannelStatus;
channelStatusDescription?: string;
channelFailureReasonCode?: string;
supplierStatus: ClientSupplierStatus;
timestamp: string;
retryCount: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Client callback subscription configuration.
* Array of subscription rules (one per event type/channel combination).
*/

export type ClientSubscriptionConfiguration = (
| MessageStatusSubscriptionConfiguration
| ChannelStatusSubscriptionConfiguration
)[];

interface SubscriptionConfigurationBase {
Name: string;
ClientId: string;
Description: string;
EventSource: string;
EventDetail: string;
Targets: {
Type: "API";
TargetId: string;
Name: string;
InputTransformer: {
InputPaths: string;
InputHeaders: {
"x-hmac-sha256-signature": string;
};
};
InvocationEndpoint: string;
InvocationMethod: "POST";
InvocationRateLimit: number;
APIKey: {
HeaderName: string;
HeaderValue: string;
};
}[];
}

export interface MessageStatusSubscriptionConfiguration
extends SubscriptionConfigurationBase {
SubscriptionType: "MessageStatus";
Statuses: string[];
}

export interface ChannelStatusSubscriptionConfiguration
extends SubscriptionConfigurationBase {
SubscriptionType: "ChannelStatus";
ChannelType: string;
ChannelStatuses: string[];
SupplierStatuses: string[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Message-level status transition event data.
*/
import type { RoutingPlan } from "models/routing-plan";
import type { Channel } from "models/channel-types";
import type { MessageStatus } from "models/status-types";

export interface MessageStatusData {
messageId: string;
messageReference: string;
messageStatus: MessageStatus;
messageStatusDescription?: string;
messageFailureReasonCode?: string;
channels: {
type: Channel;
channelStatus: string;
}[];
timestamp: string;
routingPlan: RoutingPlan;

clientId: string;
previousMessageStatus?: MessageStatus;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface RoutingPlan {
id: string;
name: string;
version: string;
createdDate: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { MessageStatusData } from "models/message-status-data";
import type { ChannelStatusData } from "models/channel-status-data";

/**
* Status transition event following CloudEvents 2025-11-draft standard.
* Represents a status transition notification published by Core domain to the Shared Event Bus.
*
* @template T - The type of data payload (MessageStatusData or ChannelStatusData)
*/
export interface StatusTransitionEvent<
T = MessageStatusData | ChannelStatusData,
> {
specversion: string;
id: string;
source: string;
subject: string;
type: string;
time: string;
recordedtime: string;
datacontenttype: string;
dataschema: string;
traceparent: string;

data: T;
}

export const EventTypes = {
MESSAGE_STATUS_TRANSITIONED:
"uk.nhs.notify.client-callbacks.message.status.transitioned.v1",
CHANNEL_STATUS_TRANSITIONED:
"uk.nhs.notify.client-callbacks.channel.status.transitioned.v1",
} as const;

export { type MessageStatusData } from "./message-status-data";

export { type ChannelStatusData } from "models/channel-status-data";
Loading
Loading