Skip to content

Commit b7ee129

Browse files
CCM-14974: Added failure reasons to events
1 parent ffbc2ee commit b7ee129

30 files changed

Lines changed: 439 additions & 47 deletions

eslint.config.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,17 @@ export default defineConfig([
218218
},
219219
},
220220
{
221-
files: ['utils/**', '**/jest.config.ts'],
221+
files: ['utils/**', '**/jest.config.ts', '**/*.test.ts', '**/*.spec.ts'],
222222
rules: {
223223
'no-relative-import-paths/no-relative-import-paths': 0,
224224
'import-x/no-relative-packages': 0,
225225
},
226226
},
227227
{
228-
files: ['scripts/**'],
228+
files: ['scripts/**', '**/generate-csv.ts'],
229229
rules: {
230+
'no-relative-import-paths/no-relative-import-paths': 0,
231+
'no-console': 0,
230232
'import-x/no-extraneous-dependencies': [
231233
'error',
232234
{ devDependencies: true },
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
code,description
2+
DL_PDMV_001,Letter rejected by PDM
3+
DL_PDMV_002,Timeout waiting for letter storage
4+
DL_CLIV_003,Attachment contains a virus
5+
DL_INTE_001,Request rejected by Core API
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
resource "aws_glue_catalog_table" "failure_code_lookup" {
2+
name = "failure_code_lookup"
3+
description = "Lookup table for failure code descriptions"
4+
database_name = aws_glue_catalog_database.reporting.name
5+
6+
table_type = "EXTERNAL_TABLE"
7+
8+
storage_descriptor {
9+
location = "s3://${module.s3bucket_reporting.bucket}/reference-data/failure_codes/"
10+
11+
input_format = "org.apache.hadoop.mapred.TextInputFormat"
12+
output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat"
13+
14+
ser_de_info {
15+
name = "csv"
16+
serialization_library = "org.apache.hadoop.hive.serde2.OpenCSVSerde"
17+
18+
parameters = {
19+
"separatorChar" = ","
20+
"skip.header.line.count" = "1"
21+
}
22+
}
23+
24+
columns {
25+
name = "code"
26+
type = "string"
27+
}
28+
29+
columns {
30+
name = "description"
31+
type = "string"
32+
}
33+
}
34+
35+
parameters = {
36+
EXTERNAL = "TRUE"
37+
classification = "csv"
38+
}
39+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Auto-generated CSV containing failure code definitions
2+
# Source: src/digital-letters-events/failure-codes.ts
3+
# Build: make build / make generate (runs generate-dependencies)
4+
resource "aws_s3_object" "failure_codes" {
5+
bucket = module.s3bucket_reporting.bucket
6+
key = "reference-data/failure_codes/failure_codes.csv"
7+
source = "${path.module}/data/failure_codes.csv"
8+
content_type = "text/csv"
9+
etag = filemd5("${path.module}/data/failure_codes.csv")
10+
11+
tags = merge(
12+
local.default_tags,
13+
{
14+
Name = "${local.csi}-failure-codes-csv"
15+
}
16+
)
17+
}

infrastructure/terraform/components/dl/scripts/sql/reports/daily_report.sql

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,17 @@ WITH vars AS (
2525
WHEN e.letterstatus = 'FAILED' THEN 'Failed'
2626
WHEN e.letterstatus = 'DISPATCHED' THEN 'Dispatched'
2727
WHEN e.letterstatus = 'REJECTED' THEN 'Rejected' ELSE NULL
28-
END as status
28+
END as status,
29+
e.reasoncode,
30+
COALESCE(
31+
CASE WHEN e.type LIKE '%.messages.request.rejected.%' THEN e.reasontext END,
32+
fcl.description,
33+
e.reasontext,
34+
e.reasoncode
35+
) as reasontext
2936
FROM event_record e
3037
CROSS JOIN vars v
38+
LEFT JOIN failure_code_lookup fcl ON e.reasoncode = fcl.code
3139
WHERE e.senderid = v.senderid
3240
AND e.__year = year(v.dt)
3341
AND e.__month = month(v.dt)
@@ -52,14 +60,18 @@ WITH vars AS (
5260
te.messagereference,
5361
te.time,
5462
te.communicationtype,
55-
te.status
63+
te.status,
64+
te.reasoncode,
65+
te.reasontext
5666
FROM "translated_events" AS te
5767
WHERE te.status IS NOT NULL
5868
AND te.communicationtype IS NOT NULL
5969
)
6070
SELECT oe.messagereference as "Message Reference",
6171
oe.time as "Time",
6272
oe.communicationtype as "Communication Type",
63-
oe.status as "Status"
73+
oe.status as "Status",
74+
oe.reasoncode as "Reason Code",
75+
oe.reasontext as "Reason"
6476
FROM "ordered_events" AS oe
6577
WHERE oe.row_number = 1

lambdas/core-notifier-lambda/src/__tests__/apis/sqs-handler.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,13 @@ describe('createHandler', () => {
256256
const handler = createHandler(dependencies);
257257
const { messageId } = sqsEvent.Records[0];
258258
const errorCode = 'VALIDATION_ERROR';
259+
const failureReason = 'Request validation failed';
259260
const correlationId = 'corr-123';
260261
const error = new RequestNotifyError(
261262
new Error('Validation failed'),
262263
correlationId,
263264
errorCode,
265+
failureReason,
264266
);
265267
// Add messageReference property dynamically to trigger the terminal error path
266268
(error as any).messageReference = messageReference;

lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,15 @@ describe('mapper', () => {
209209
describe('mapPdmEventToMessageRequestRejected', () => {
210210
it('correctly maps PDM event to MessageRequestRejected', () => {
211211
const failureCode = 'INVALID_NHS_NUMBER';
212+
const failureReason = 'NHS number is not valid';
212213
const mockDate = new Date('2024-01-15T12:00:00Z');
213214
jest.spyOn(globalThis, 'Date').mockImplementation(() => mockDate as any);
214215

215216
const result = mapPdmEventToMessageRequestRejected(
216217
mockPdmEvent,
217218
mockSender,
218219
failureCode,
220+
failureReason,
219221
);
220222

221223
expect(result).toEqual({
@@ -232,40 +234,62 @@ describe('mapper', () => {
232234
failureCode: 'INVALID_NHS_NUMBER',
233235
messageUri:
234236
'https://www.nhsapp.service.nhs.uk/digital-letters?letterid=resource-789',
237+
reasonCode: 'DL_INTE_001',
238+
reasonText: 'NHS number is not valid',
235239
},
236240
});
237241

238242
expect(mockRandomUUID).toHaveBeenCalled();
239243
});
240244

245+
it('includes reasonCode and reasonText for reporting', () => {
246+
const failureCode = 'CM_DUPLICATE_REQUEST';
247+
const failureReason = 'This request has already been received';
248+
const result = mapPdmEventToMessageRequestRejected(
249+
mockPdmEvent,
250+
mockSender,
251+
failureCode,
252+
failureReason,
253+
);
254+
255+
expect(result.data.reasonCode).toBe('DL_INTE_001');
256+
expect(result.data.reasonText).toBe('This request has already been received');
257+
});
258+
241259
it('generates new UUID for event', () => {
242260
const failureCode = 'VALIDATION_ERROR';
261+
const failureReason = 'Request validation failed';
243262
mapPdmEventToMessageRequestRejected(
244263
mockPdmEvent,
245264
mockSender,
246265
failureCode,
266+
failureReason,
247267
);
248268

249269
expect(mockRandomUUID).toHaveBeenCalledTimes(1);
250270
});
251271

252272
it('includes failureCode in data', () => {
253273
const failureCode = 'ROUTING_FAILED';
274+
const failureReason = 'Unable to route message';
254275
const result = mapPdmEventToMessageRequestRejected(
255276
mockPdmEvent,
256277
mockSender,
257278
failureCode,
279+
failureReason,
258280
);
259281

260282
expect(result.data.failureCode).toBe('ROUTING_FAILED');
261283
});
262284

263285
it('includes messageUri with resource ID', () => {
264286
const failureCode = 'TIMEOUT';
287+
const failureReason = 'Request timed out';
265288
const result = mapPdmEventToMessageRequestRejected(
266289
mockPdmEvent,
267290
mockSender,
268291
failureCode,
292+
failureReason,
269293
);
270294

271295
expect(result.data.messageUri).toBe(
@@ -275,32 +299,38 @@ describe('mapper', () => {
275299

276300
it('uses sender senderId in data', () => {
277301
const failureCode = 'UNKNOWN_ERROR';
302+
const failureReason = 'An unknown error occurred';
278303
const result = mapPdmEventToMessageRequestRejected(
279304
mockPdmEvent,
280305
mockSender,
281306
failureCode,
307+
failureReason,
282308
);
283309

284310
expect(result.data.senderId).toBe('test-sender-id');
285311
});
286312

287313
it('uses messageReference from PDM event', () => {
288314
const failureCode = 'DUPLICATE_REQUEST';
315+
const failureReason = 'Duplicate request detected';
289316
const result = mapPdmEventToMessageRequestRejected(
290317
mockPdmEvent,
291318
mockSender,
292319
failureCode,
320+
failureReason,
293321
);
294322

295323
expect(result.data.messageReference).toBe('msg-ref-123');
296324
});
297325

298326
it('sets correct event type', () => {
299327
const failureCode = 'SYSTEM_ERROR';
328+
const failureReason = 'System error occurred';
300329
const result = mapPdmEventToMessageRequestRejected(
301330
mockPdmEvent,
302331
mockSender,
303332
failureCode,
333+
failureReason,
304334
);
305335

306336
expect(result.type).toBe(
@@ -310,10 +340,12 @@ describe('mapper', () => {
310340

311341
it('sets correct dataschema', () => {
312342
const failureCode = 'CONFIG_ERROR';
343+
const failureReason = 'Configuration error';
313344
const result = mapPdmEventToMessageRequestRejected(
314345
mockPdmEvent,
315346
mockSender,
316347
failureCode,
348+
failureReason,
317349
);
318350

319351
expect(result.dataschema).toBe(
@@ -323,10 +355,12 @@ describe('mapper', () => {
323355

324356
it('preserves CloudEvents properties from PDM event', () => {
325357
const failureCode = 'NETWORK_ERROR';
358+
const failureReason = 'Network connection failed';
326359
const result = mapPdmEventToMessageRequestRejected(
327360
mockPdmEvent,
328361
mockSender,
329362
failureCode,
363+
failureReason,
330364
);
331365

332366
expect(result.specversion).toBe('1.0');

lambdas/core-notifier-lambda/src/apis/sqs-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ async function processSqsRecord(
125125
incoming,
126126
sender,
127127
error.errorCode,
128+
error.failureReason,
128129
);
129130
} else {
130131
// this might be a transient error so we notify the queue to retry

lambdas/core-notifier-lambda/src/app/notify-api-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export class NotifyClient implements INotifyClient {
109109
error,
110110
correlationId,
111111
errorBody?.errors[0].code,
112+
errorBody?.errors[0].detail,
112113
);
113114
}
114115
}

lambdas/core-notifier-lambda/src/domain/mapper.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
} from 'digital-letters-events';
99
import type { SingleMessageRequest } from 'domain/request';
1010

11+
const CORE_API_FAILURE_CODE = 'DL_INTE_001';
12+
1113
const DIGITAL_LETTER_URL =
1214
'https://www.nhsapp.service.nhs.uk/digital-letters?letterid=';
1315

@@ -99,6 +101,7 @@ export function mapPdmEventToMessageRequestRejected(
99101
pdmResourceAvailable: PDMResourceAvailable,
100102
sender: Sender,
101103
notifyFailureCode: string,
104+
failureReason: string,
102105
): MessageRequestRejected {
103106
const { data } = pdmResourceAvailable;
104107
const { messageReference } = data;
@@ -117,6 +120,8 @@ export function mapPdmEventToMessageRequestRejected(
117120
senderId: sender.senderId,
118121
failureCode: notifyFailureCode,
119122
messageUri: `${DIGITAL_LETTER_URL}${data.resourceId}`,
123+
reasonCode: CORE_API_FAILURE_CODE,
124+
reasonText: failureReason,
120125
},
121126
};
122127
}

0 commit comments

Comments
 (0)