Skip to content

Commit 812b27b

Browse files
committed
CCM-12614: add call to pdm and various other bits
1 parent bf0685a commit 812b27b

File tree

18 files changed

+334
-37
lines changed

18 files changed

+334
-37
lines changed

infrastructure/terraform/components/dl/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ No requirements.
1111
|------|-------------|------|---------|:--------:|
1212
| <a name="input_apim_auth_token_schedule"></a> [apim\_auth\_token\_schedule](#input\_apim\_auth\_token\_schedule) | Schedule to renew the APIM auth token | `string` | `"rate(9 minutes)"` | no |
1313
| <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 |
14-
| <a name="input_apim_base_url"></a> [apim\_base\_url](#input\_apim\_base\_url) | The URL used to send requests to Notify and PDM | `string` | `"https://sandbox.api.service.nhs.uk"` | no |
14+
| <a name="input_apim_base_url"></a> [apim\_base\_url](#input\_apim\_base\_url) | The URL used to send requests to Notify and PDM | `string` | `"https://int.api.service.nhs.uk"` | no |
1515
| <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 |
1616
| <a name="input_aws_account_id"></a> [aws\_account\_id](#input\_aws\_account\_id) | The AWS Account ID (numeric) | `string` | n/a | yes |
1717
| <a name="input_component"></a> [component](#input\_component) | The variable encapsulating the name of this component | `string` | `"dl"` | no |
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
locals {
2-
aws_lambda_functions_dir_path = "../../../../lambdas"
3-
log_destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"
2+
aws_lambda_functions_dir_path = "../../../../lambdas"
3+
log_destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"
44
apim_access_token_ssm_parameter_name = "/${var.component}/${var.environment}/apim/access_token"
55
apim_api_key_ssm_parameter_name = "/${var.component}/${var.environment}/apim/api_key"
66
apim_private_key_ssm_parameter_name = "/${var.component}/${var.environment}/apim/private_key"
77
apim_keystore_s3_bucket = "nhs-${var.aws_account_id}-${var.region}-${var.environment}-${var.component}-static-assets"
88
root_domain_name = "${var.environment}.${local.acct.route53_zone_names["digital-letters"]}"
99
root_domain_id = local.acct.route53_zone_ids["digital-letters"]
10-
ttl_shard_count = 3
10+
ttl_shard_count = 3
1111
}

infrastructure/terraform/components/dl/module_lambda_pdm_poll.tf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,25 @@ module "pdm_poll" {
4040
"APIM_ACCESS_TOKEN_SSM_PARAMETER_NAME" = local.apim_access_token_ssm_parameter_name
4141
"EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn
4242
"EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url
43+
"POLL_MAX_RETRIES" = 10
4344
}
4445
}
4546

4647
data "aws_iam_policy_document" "pdm_poll_lambda" {
48+
statement {
49+
sid = "AllowSSMParam"
50+
effect = "Allow"
51+
52+
actions = [
53+
"ssm:GetParameter",
54+
"ssm:GetParameters",
55+
"ssm:GetParametersByPath"
56+
]
57+
58+
resources = [
59+
"arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter/${var.component}/${var.environment}/apim/*"
60+
]
61+
}
4762
statement {
4863
sid = "PutEvents"
4964
effect = "Allow"

infrastructure/terraform/components/dl/variables.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ variable "ttl_poll_schedule" {
119119
variable "apim_base_url" {
120120
type = string
121121
description = "The URL used to send requests to Notify and PDM"
122-
default = "https://sandbox.api.service.nhs.uk"
122+
default = "https://int.api.service.nhs.uk"
123123
}
124124

125125
variable "apim_auth_token_url" {

lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ mockDate.mockReturnValue('2023-06-20T12:00:00.250Z');
2424

2525
const handler = createHandler({
2626
eventPublisher,
27-
pdm,
2827
logger,
28+
pdm,
29+
pollMaxRetries: 10,
2930
});
3031

3132
describe('SQS Handler', () => {
@@ -71,7 +72,7 @@ describe('SQS Handler', () => {
7172
type: 'uk.nhs.notify.digital.letters.pdm.resource.unavailable.v1',
7273
data: {
7374
...pdmResourceSubmittedEvent.data,
74-
retryCount: 1,
75+
retryCount: 0,
7576
},
7677
},
7778
]);

lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,59 @@
11
import { mock } from 'jest-mock-extended';
2-
import { Logger } from 'utils';
2+
import { IPdmClient, Logger } from 'utils';
33
import { Pdm, PdmDependencies } from 'app/pdm';
44
import { pdmResourceSubmittedEvent } from '__tests__/test-data';
55

66
const logger = mock<Logger>();
7+
const pdmClient = mock<IPdmClient>();
78
const validConfig = (): PdmDependencies => ({
8-
pdmUrl: 'https://example.com/pdm',
9+
pdmClient,
910
logger,
1011
});
1112

13+
const availableResponse = {
14+
resourceType: 'DocumentReference',
15+
id: '4c5af7c3-ca21-31b8-924b-fa526db5379b',
16+
meta: {
17+
versionId: '1',
18+
lastUpdated: '2025-12-10T09:00:47.068021Z'
19+
},
20+
status: 'current',
21+
subject: {
22+
identifier: {
23+
system: 'https://fhir.nhs.uk/Id/nhs-number',
24+
value: '9912003071'
25+
}
26+
},
27+
content: [
28+
{
29+
attachment: {
30+
contentType: 'application/pdf',
31+
data: 'base64-encoded-pdf',
32+
title: 'Dummy PDF'
33+
}
34+
}
35+
]
36+
}
37+
1238
describe('Pdm', () => {
1339
describe('constructor', () => {
1440
it('is created when required deps are provided', () => {
1541
const cfg = validConfig();
1642
expect(() => new Pdm(cfg)).not.toThrow();
1743
});
1844

19-
it('throws if pdmUrl is not provided', () => {
45+
it('throws if pdmClient is not provided', () => {
2046
const cfg = {
2147
logger,
2248
} as unknown as PdmDependencies;
2349

24-
expect(() => new Pdm(cfg)).toThrow('pdmUrl has not been specified');
50+
expect(() => new Pdm(cfg)).toThrow('pdmClient has not been specified');
2551
});
2652

2753
it('throws if logger is not provided', () => {
2854
const cfg = {
29-
pdmUrl: 'https://example.com/pdm',
30-
} as PdmDependencies;
55+
pdmClient,
56+
} as unknown as PdmDependencies;
3157

3258
expect(() => new Pdm(cfg)).toThrow('logger has not been provided');
3359
});
@@ -36,6 +62,8 @@ describe('Pdm', () => {
3662
describe('poll', () => {
3763
it('returns available when the document is ready', async () => {
3864
const cfg = validConfig();
65+
pdmClient.getDocumentReference.mockResolvedValue(availableResponse);
66+
3967
const pdm = new Pdm(cfg);
4068

4169
const result = await pdm.poll(pdmResourceSubmittedEvent);
@@ -45,21 +73,28 @@ describe('Pdm', () => {
4573

4674
it('returns unavailable when the document is not ready', async () => {
4775
const cfg = validConfig();
48-
const pdm = new Pdm(cfg);
76+
const unavailableResponse = {
77+
...availableResponse,
78+
content: [{
79+
attachment: {
80+
contentType: 'application/pdf',
81+
title: 'Dummy PDF'
82+
}
83+
}]
84+
};
85+
pdmClient.getDocumentReference.mockResolvedValue(unavailableResponse);
4986

50-
pdmResourceSubmittedEvent.data.messageReference = 'ref2';
87+
const pdm = new Pdm(cfg);
5188

5289
const result = await pdm.poll(pdmResourceSubmittedEvent);
5390

5491
expect(result).toBe('unavailable');
5592
});
5693

57-
it('returns failed and logs error when logger.info throws', async () => {
94+
it('logs and throws error when error from PDM', async () => {
5895
const cfg = validConfig();
59-
const thrown = new Error('logger failure');
60-
cfg.logger.info = jest.fn(() => {
61-
throw thrown;
62-
});
96+
const thrown = new Error('pdm failure');
97+
pdmClient.getDocumentReference.mockRejectedValueOnce(thrown)
6398

6499
const pdm = new Pdm(cfg);
65100

@@ -69,7 +104,7 @@ describe('Pdm', () => {
69104
expect(logger.error).toHaveBeenCalledWith(
70105
expect.objectContaining({
71106
description: 'Error getting document resource from PDM',
72-
err: thrown,
107+
err: new Error('pdm failure'),
73108
}),
74109
);
75110
});

lambdas/pdm-poll-lambda/src/__tests__/container.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@ jest.mock('infra/config', () => ({
66
apimAccessTokenSsmParameterName: 'test-ssm-parameter-name',
77
eventPublisherDlqUrl: 'test-url',
88
eventPublisherEventBusArn: 'test-arn',
9+
maxPollCount: 10,
910
})),
1011
}));
1112

1213
jest.mock('utils', () => ({
13-
EventPublisher: jest.fn(() => ({})),
14+
createGetApimAccessToken: jest.fn(() => ({})),
1415
eventBridgeClient: {},
16+
EventPublisher: jest.fn(() => ({})),
1517
logger: {},
16-
sqsClient: {},
1718
ParameterStoreCache: jest.fn(() => ({})),
18-
createGetApimAccessToken: jest.fn(() => ({})),
19+
PdmClient: jest.fn(() => ({})),
20+
sqsClient: {},
1921
}));
2022

2123
describe('container', () => {

lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ export interface HandlerDependencies {
1212
eventPublisher: EventPublisher;
1313
logger: Logger;
1414
pdm: Pdm;
15+
pollMaxRetries: number;
1516
}
1617

1718
export const createHandler = ({
1819
eventPublisher,
1920
logger,
2021
pdm,
22+
pollMaxRetries,
2123
}: HandlerDependencies) =>
2224
async function handler(sqsEvent: SQSEvent): Promise<SQSBatchResponse> {
2325
const receivedItemCount = sqsEvent.Records.length;
@@ -34,14 +36,14 @@ export const createHandler = ({
3436

3537
const result = await pdm.poll(eventDetail);
3638

37-
const retries = (eventDetail.data?.retryCount ?? 0) + 1;
39+
const retries = (eventDetail.data?.retryCount ?? -1) + 1;
3840
const eventTime = new Date().toISOString();
3941
let eventType =
4042
'uk.nhs.notify.digital.letters.pdm.resource.available.v1';
4143

4244
if (result === 'unavailable') {
4345
eventType =
44-
retries >= 10
46+
retries >= pollMaxRetries
4547
? 'uk.nhs.notify.digital.letters.pdm.resource.retries.exceeded.v1'
4648
: 'uk.nhs.notify.digital.letters.pdm.resource.unavailable.v1';
4749
}

lambdas/pdm-poll-lambda/src/app/pdm.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,44 @@
1-
import { Logger } from 'utils';
1+
import { IPdmClient, Logger } from 'utils';
22

33
export type PdmOutcome = 'available' | 'unavailable';
44

55
export interface PdmDependencies {
6-
pdmUrl: string;
6+
pdmClient: IPdmClient;
77
logger: Logger;
88
}
99

1010
export class Pdm {
11-
private readonly pdmUrl: string;
11+
private readonly pdmClient: IPdmClient;
1212

1313
private readonly logger: Logger;
1414

1515
constructor(config: PdmDependencies) {
16-
if (!config.pdmUrl) {
17-
throw new Error('pdmUrl has not been specified');
16+
if (!config.pdmClient) {
17+
throw new Error('pdmClient has not been specified');
1818
}
1919
if (!config.logger) {
2020
throw new Error('logger has not been provided');
2121
}
2222

23-
this.pdmUrl = config.pdmUrl;
23+
this.pdmClient = config.pdmClient;
2424
this.logger = config.logger;
2525
}
2626

2727
async poll(item: any): Promise<PdmOutcome> {
2828
try {
2929
this.logger.info(item);
30-
if (item.data.messageReference === 'ref1') {
30+
31+
const requestId = crypto.randomUUID();
32+
33+
const response = await this.pdmClient.getDocumentReference(
34+
item.data.resourceId,
35+
requestId,
36+
item.id,
37+
);
38+
39+
this.logger.info(response);
40+
41+
if (response.content[0].attachment.data) {
3142
return 'available';
3243
}
3344
return 'unavailable';
Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import { HandlerDependencies } from 'apis/sqs-handler';
22
import { Pdm } from 'app/pdm';
33
import { loadConfig } from 'infra/config';
4-
import { EventPublisher, eventBridgeClient, logger, sqsClient } from 'utils';
4+
import {
5+
EventPublisher,
6+
ParameterStoreCache,
7+
PdmClient,
8+
createGetApimAccessToken,
9+
eventBridgeClient,
10+
logger,
11+
sqsClient,
12+
} from 'utils';
513

614
export const createContainer = (): HandlerDependencies => {
7-
const { eventPublisherDlqUrl, eventPublisherEventBusArn } = loadConfig();
15+
const {
16+
apimAccessTokenSsmParameterName,
17+
apimBaseUrl,
18+
eventPublisherDlqUrl,
19+
eventPublisherEventBusArn,
20+
pollMaxRetries,
21+
} = loadConfig();
822

923
const eventPublisher = new EventPublisher({
1024
eventBusArn: eventPublisherEventBusArn,
@@ -14,12 +28,24 @@ export const createContainer = (): HandlerDependencies => {
1428
eventBridgeClient,
1529
});
1630

31+
const parameterStore = new ParameterStoreCache();
32+
33+
const accessTokenRepository = {
34+
getAccessToken: createGetApimAccessToken(
35+
apimAccessTokenSsmParameterName,
36+
logger,
37+
parameterStore,
38+
),
39+
};
40+
41+
const pdmClient = new PdmClient(accessTokenRepository, apimBaseUrl, logger);
42+
1743
const pdm = new Pdm({
18-
pdmUrl: 'pdmUrl',
44+
pdmClient,
1945
logger,
2046
});
2147

22-
return { eventPublisher, pdm, logger };
48+
return { eventPublisher, logger, pdm, pollMaxRetries };
2349
};
2450

2551
export default createContainer;

0 commit comments

Comments
 (0)