Skip to content

Commit 8e9ffb9

Browse files
authored
New: [AEA-5202] - Filter for sites with notifications allowed or denied (#1574)
## Summary - ✨ New Feature ### Details Adds a function on the producer side of EPS notifications that filters out sites that are blocked, whilst only allowing through enabled suppliers and sites. Case insensitive. We want to maintain these lists in version control, so I've simply added them as static data to the helper function file. There are also three test cases for us to use in testing: - Enabled test ODS code: `FA565` - Enabled test supplier name: `Internal Test System` - Blocked ODS code: `A83008`
1 parent 4b56964 commit 8e9ffb9

11 files changed

Lines changed: 272 additions & 57 deletions

File tree

SAMtemplates/functions/main.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ Parameters:
3333
Type: String
3434
Default: none
3535

36+
EnabledSiteODSCodesParam:
37+
Type: AWS::SSM::Parameter::Value<String>
38+
39+
EnabledSystemsParam:
40+
Type: AWS::SSM::Parameter::Value<String>
41+
42+
BlockedSiteODSCodesParam:
43+
Type: AWS::SSM::Parameter::Value<String>
44+
3645
LogLevel:
3746
Type: String
3847

@@ -83,6 +92,9 @@ Resources:
8392
TABLE_NAME: !Ref PrescriptionStatusUpdatesTableName
8493
NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL: !Ref NHSNotifyPrescriptionsSQSQueueUrl
8594
SQS_SALT: !Sub "{{resolve:secretsmanager:${SQSSaltSecret}:SecretString:salt}}"
95+
ENABLED_SITE_ODS_CODES: !Ref EnabledSiteODSCodesParam
96+
ENABLED_SYSTEMS: !Ref EnabledSystemsParam
97+
BLOCKED_SITE_ODS_CODES: !Ref BlockedSiteODSCodesParam
8698
LOG_LEVEL: !Ref LogLevel
8799
ENVIRONMENT: !Ref Environment
88100
TEST_PRESCRIPTIONS_1: "None"

SAMtemplates/main_template.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ Parameters:
9090
Type: String
9191

9292
Resources:
93+
Parameters:
94+
Type: AWS::Serverless::Application
95+
Properties:
96+
Location: parameters/main.yaml
97+
Parameters:
98+
StackName: !Ref AWS::StackName
99+
Environment: !Ref Environment
100+
93101
Tables:
94102
Type: AWS::Serverless::Application
95103
Properties:
@@ -136,6 +144,9 @@ Resources:
136144
PrescriptionStatusUpdatesTableName: !GetAtt Tables.Outputs.PrescriptionStatusUpdatesTableName
137145
PrescriptionNotificationStateTableName: !GetAtt Tables.Outputs.PrescriptionNotificationStateTableName
138146
NHSNotifyPrescriptionsSQSQueueUrl: !GetAtt Messaging.Outputs.NHSNotifyPrescriptionsSQSQueueUrl
147+
EnabledSiteODSCodesParam: !GetAtt Parameters.Outputs.EnabledSiteODSCodesParameterName
148+
EnabledSystemsParam: !GetAtt Parameters.Outputs.EnabledSystemsParameterName
149+
BlockedSiteODSCodesParam: !GetAtt Parameters.Outputs.BlockedSiteODSCodesParameterName
139150
LogLevel: !Ref LogLevel
140151
LogRetentionInDays: !Ref LogRetentionInDays
141152
EnableSplunk: !Ref EnableSplunk

SAMtemplates/parameters/main.yaml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Description: >-
3+
SSM Parameter Store entries. Values may differ between prod and non-prod environments
4+
5+
Parameters:
6+
StackName:
7+
Type: String
8+
9+
Environment:
10+
Type: String
11+
12+
Conditions:
13+
IsProd: !Equals [ !Ref Environment, prod ]
14+
15+
Resources:
16+
EnabledSiteODSCodesParameter:
17+
Type: AWS::SSM::Parameter
18+
Properties:
19+
Name: !Sub ${StackName}-PSUNotifyEnabledSiteODSCodes
20+
Description: "List of site ODS codes for which notifications are enabled"
21+
Type: String
22+
Value: !If
23+
- IsProd
24+
- > # Prod notification enabled
25+
FA565
26+
- > # Non-prod
27+
FA565
28+
29+
EnabledSystemsParameter:
30+
Type: AWS::SSM::Parameter
31+
Properties:
32+
Name: !Sub ${StackName}-PSUNotifyEnabledSystems
33+
Description: "List of application names for which notifications are enabled"
34+
Type: String
35+
Value: !If
36+
- IsProd
37+
- > # Prod notification enabled
38+
Apotec Ltd - Apotec CRM - Production,
39+
CrxPatientApp,
40+
nhsPrescriptionApp,
41+
Titan PSU Prod
42+
- > # Non-prod
43+
Internal Test System,
44+
Apotec Ltd - Apotec CRM - Production,
45+
CrxPatientApp,
46+
nhsPrescriptionApp,
47+
Titan PSU Prod
48+
49+
BlockedSiteODSCodesParameter:
50+
Type: AWS::SSM::Parameter
51+
Properties:
52+
Name: !Sub ${StackName}-PSUNotifyBlockedSiteODSCodes
53+
Description: "List of site ODS codes for which notifications are blocked"
54+
Type: String
55+
Value: !If
56+
- IsProd
57+
- > # Prod notification disabled
58+
A83008
59+
- > # Non-prod
60+
A83008
61+
62+
Outputs:
63+
EnabledSiteODSCodesParameterName:
64+
Description: "Name of the SSM parameter holding enabled site ODS codes"
65+
Value: !Ref EnabledSiteODSCodesParameter
66+
Export:
67+
Name: !Sub ${StackName}-PSUNotifyEnabledSiteODSCodesParam
68+
69+
EnabledSystemsParameterName:
70+
Description: "Name of the SSM parameter holding enabled system names"
71+
Value: !Ref EnabledSystemsParameter
72+
Export:
73+
Name: !Sub ${StackName}-PSUNotifyEnabledSystemsParam
74+
75+
BlockedSiteODSCodesParameterName:
76+
Description: "Name of the SSM parameter holding blocked site ODS codes"
77+
Value: !Ref BlockedSiteODSCodesParameter
78+
Export:
79+
Name: !Sub ${StackName}-PSUNotifyBlockedSiteODSCodesParam
Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
export interface PSUDataItem {
2-
LastModified: string
3-
LineItemID: string
4-
PatientNHSNumber: string
5-
PharmacyODSCode: string
6-
PrescriptionID: string
7-
RepeatNo?: number
8-
RequestID: string
9-
Status: string
10-
TaskID: string
11-
TerminalStatus: string
12-
ApplicationName: string
13-
ExpiryTime: number
14-
}
2+
LastModified: string
3+
LineItemID: string
4+
PatientNHSNumber: string
5+
PharmacyODSCode: string
6+
PrescriptionID: string
7+
RepeatNo?: number
8+
RequestID: string
9+
Status: string
10+
TaskID: string
11+
TerminalStatus: string
12+
ApplicationName: string
13+
ExpiryTime: number
14+
}

packages/updatePrescriptionStatus/.jest/setEnvVars.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL = "dummy_notify_sqs";
33
process.env.AWS_REGION = "eu-west-2";
44
process.env.SQS_SALT = "the quick brown fox something something"
5+
process.env.ENABLED_SITE_ODS_CODES = "FA565"
6+
process.env.ENABLED_SYSTEMS = "Internal Test System,Apotec Ltd - Apotec CRM - Production,CrxPatientApp,nhsPrescriptionApp,Titan PSU Prod"
7+
process.env.BLOCKED_SITE_ODS_CODES = "A83008"

packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import httpHeaderNormalizer from "@middy/http-header-normalizer"
1111
import errorHandler from "@nhs/fhir-middy-error-handler"
1212
import {Bundle, BundleEntry, Task} from "fhir/r4"
1313

14+
import {PSUDataItem} from "@PrescriptionStatusUpdate_common/commonTypes"
15+
1416
import {transactionBundle, validateEntry} from "./validation/content"
1517
import {getPreviousItem, persistDataItems} from "./utils/databaseClient"
1618
import {jobWithTimeout, hasTimedOut} from "./utils/timeoutUtils"
@@ -42,21 +44,6 @@ export const TEST_PRESCRIPTIONS_1 = (process.env.TEST_PRESCRIPTIONS_1 ?? "")
4244
export const TEST_PRESCRIPTIONS_2 = (process.env.TEST_PRESCRIPTIONS_2 ?? "")
4345
.split(",").map(item => item.trim()) || []
4446

45-
export interface DataItem {
46-
LastModified: string
47-
LineItemID: string
48-
PatientNHSNumber: string
49-
PharmacyODSCode: string
50-
PrescriptionID: string
51-
RepeatNo?: number
52-
RequestID: string
53-
Status: string
54-
TaskID: string
55-
TerminalStatus: string
56-
ApplicationName: string
57-
ExpiryTime: number
58-
}
59-
6047
const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
6148
logger.appendKeys({
6249
"nhsd-correlation-id": event.headers["nhsd-correlation-id"],
@@ -260,16 +247,16 @@ export function buildDataItems(
260247
requestEntries: Array<BundleEntry>,
261248
xRequestID: string,
262249
applicationName: string
263-
): Array<DataItem> {
264-
const dataItems: Array<DataItem> = []
250+
): Array<PSUDataItem> {
251+
const dataItems: Array<PSUDataItem> = []
265252

266253
for (const requestEntry of requestEntries) {
267254
const task = requestEntry.resource as Task
268255
logger.info("Building data item for task.", {task: task, id: task.id})
269256

270257
const repeatNo = task.input?.[0]?.valueInteger
271258

272-
const dataItem: DataItem = {
259+
const dataItem: PSUDataItem = {
273260
LastModified: task.lastModified!,
274261
LineItemID: task.focus!.identifier!.value!.toUpperCase(),
275262
PatientNHSNumber: task.for!.identifier!.value!,
@@ -300,7 +287,7 @@ function response(statusCode: number, responseEntries: Array<BundleEntry>) {
300287
}
301288
}
302289

303-
async function logTransitions(dataItems: Array<DataItem>): Promise<void> {
290+
async function logTransitions(dataItems: Array<PSUDataItem>): Promise<void> {
304291
for (const dataItem of dataItems) {
305292
try {
306293
const previousItem = await getPreviousItem(dataItem)

packages/updatePrescriptionStatus/src/utils/databaseClient.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import {
1111
} from "@aws-sdk/client-dynamodb"
1212
import {marshall, unmarshall} from "@aws-sdk/util-dynamodb"
1313

14-
import {DataItem} from "../updatePrescriptionStatus"
14+
import {PSUDataItem} from "@PrescriptionStatusUpdate_common/commonTypes"
1515
import {Timeout} from "./timeoutUtils"
1616

1717
const client = new DynamoDBClient()
1818
const tableName = process.env.TABLE_NAME ?? "PrescriptionStatusUpdates"
1919

20-
function createTransactionCommand(dataItems: Array<DataItem>, logger: Logger): TransactWriteItemsCommand {
20+
function createTransactionCommand(dataItems: Array<PSUDataItem>, logger: Logger): TransactWriteItemsCommand {
2121
logger.info("Creating transaction command to write data items.")
22-
const transactItems: Array<TransactWriteItem> = dataItems.map((d: DataItem): TransactWriteItem => {
22+
const transactItems: Array<TransactWriteItem> = dataItems.map((d: PSUDataItem): TransactWriteItem => {
2323
return {
2424
Put: {
2525
TableName: tableName,
@@ -32,7 +32,7 @@ function createTransactionCommand(dataItems: Array<DataItem>, logger: Logger): T
3232
return new TransactWriteItemsCommand({TransactItems: transactItems})
3333
}
3434

35-
export async function persistDataItems(dataItems: Array<DataItem>, logger: Logger): Promise<boolean | Timeout> {
35+
export async function persistDataItems(dataItems: Array<PSUDataItem>, logger: Logger): Promise<boolean | Timeout> {
3636
const transactionCommand = createTransactionCommand(dataItems, logger)
3737
try {
3838
logger.info("Sending TransactWriteItemsCommand to DynamoDB.", {command: transactionCommand})
@@ -72,7 +72,7 @@ export async function checkPrescriptionRecordExistence(
7272
}
7373
}
7474

75-
export async function getPreviousItem(currentItem: DataItem): Promise<DataItem | undefined> {
75+
export async function getPreviousItem(currentItem: PSUDataItem): Promise<PSUDataItem | undefined> {
7676
const query: QueryCommandInput = {
7777
TableName: tableName,
7878
KeyConditions: {
@@ -90,7 +90,7 @@ export async function getPreviousItem(currentItem: DataItem): Promise<DataItem |
9090
}
9191

9292
let lastEvaluatedKey
93-
let items: Array<DataItem> = []
93+
let items: Array<PSUDataItem> = []
9494
do {
9595
if (lastEvaluatedKey) {
9696
query.ExclusiveStartKey = lastEvaluatedKey
@@ -99,7 +99,7 @@ export async function getPreviousItem(currentItem: DataItem): Promise<DataItem |
9999
if (result.Items) {
100100
items = items.concat(
101101
result.Items
102-
.map((item) => unmarshall(item) as DataItem)
102+
.map((item) => unmarshall(item) as PSUDataItem)
103103
.filter((item) => item.TaskID !== currentItem.TaskID) // Can't do NE in the query so filter here
104104
)
105105
}

packages/updatePrescriptionStatus/src/utils/sqsClient.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import {SQSClient, SendMessageBatchCommand} from "@aws-sdk/client-sqs"
33

44
import {createHmac} from "crypto"
55

6-
import {DataItem} from "../updatePrescriptionStatus"
6+
import {PSUDataItem} from "@PrescriptionStatusUpdate_common/commonTypes"
7+
8+
import {checkSiteOrSystemIsNotifyEnabled} from "../validation/notificationSiteAndSystemFilters"
79

810
const sqsUrl: string | undefined = process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL
9-
const sqsSalt: string = process.env.SQS_SALT ?? "DEVSALT"
11+
const fallbackSalt = "DEV SALT"
12+
const sqsSalt: string = process.env.SQS_SALT ?? fallbackSalt
1013

1114
// The AWS_REGION is always defined in lambda environments
1215
const sqs = new SQSClient({region: process.env.AWS_REGION})
@@ -33,26 +36,26 @@ function chunkArray<T>(arr: Array<T>, size: number): Array<Array<T>> {
3336
* @param hashFunction - Which hash function to use. HMAC compatible. Defaults to SHA-256
3437
* @returns - A hex encoded string of the hash
3538
*/
36-
export function saltedHash(input: string, hashFunction: string = "sha256"): string {
37-
if (sqsSalt === "DEVSALT") {
38-
console.warn("Using the fallback salt value - please update the environment variable `SQS_SALT` to a random value.")
39+
export function saltedHash(logger: Logger, input: string, hashFunction: string = "sha256"): string {
40+
if (sqsSalt === fallbackSalt) {
41+
logger.warn("Using the fallback salt value - please update the environment variable `SQS_SALT` to a random value.")
3942
}
4043
return createHmac(hashFunction, sqsSalt)
4144
.update(input, "utf8")
4245
.digest("hex")
4346
}
4447

4548
/**
46-
* Pushes an array of DataItems to the notifications SQS queue
49+
* Pushes an array of PSUDataItem to the notifications SQS queue
4750
* Uses SendMessageBatch to send up to 10 at a time
4851
*
4952
* @param requestId - The x-request-id header from the incoming event
50-
* @param data - Array of DataItems to send to SQS
53+
* @param data - Array of PSUDataItem to send to SQS
5154
* @param logger - Logger instance
5255
*/
5356
export async function pushPrescriptionToNotificationSQS(
5457
requestId: string,
55-
data: Array<DataItem>,
58+
data: Array<PSUDataItem>,
5659
logger: Logger
5760
) {
5861
logger.info("Checking if any items require notifications", {numItemsToBeChecked: data.length, sqsUrl})
@@ -62,8 +65,17 @@ export async function pushPrescriptionToNotificationSQS(
6265
throw new Error("Notifications SQS URL not configured")
6366
}
6467

68+
// Only allow through sites and systems that are allowedSitesAndSystems
69+
const allowedSitesAndSystemsData = checkSiteOrSystemIsNotifyEnabled(data)
70+
logger.info(
71+
"Filtered out sites and suppliers that are not enabled, or are explicitly disabled",
72+
{
73+
numItemsAllowed: allowedSitesAndSystemsData.length
74+
}
75+
)
76+
6577
// SQS batch calls are limited to 10 messages per request, so chunk the data
66-
const batches = chunkArray(data, 10)
78+
const batches = chunkArray(allowedSitesAndSystemsData, 10)
6779

6880
// Only these statuses will be pushed to the SQS
6981
const updateStatuses: Array<string> = [
@@ -80,7 +92,7 @@ export async function pushPrescriptionToNotificationSQS(
8092
MessageBody: JSON.stringify(item),
8193
// FIFO
8294
// We dedupe on both nhs number and ods code
83-
MessageDeduplicationId: saltedHash(`${item.PatientNHSNumber}:${item.PharmacyODSCode}`),
95+
MessageDeduplicationId: saltedHash(logger, `${item.PatientNHSNumber}:${item.PharmacyODSCode}`),
8496
MessageGroupId: requestId
8597
}))
8698
// We could do a round of deduplications here, but benefits would be minimal and AWS SQS will do it for us anyway.

0 commit comments

Comments
 (0)