@@ -7,11 +7,11 @@ import {
77 $LetterStatusChangeEvent ,
88 LetterStatusChangeEvent ,
99} from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events" ;
10+ import { makeIdempotent } from "@aws-lambda-powertools/idempotency" ;
1011import createSupplierAllocatorHandler from "../allocate-handler" ;
1112import * as supplierConfig from "../../services/supplier-config" ;
1213import * as supplierQuotas from "../../services/supplier-quotas" ;
1314import * as allocationConfig from "../allocation-config" ;
14-
1515import { Deps } from "../../config/deps" ;
1616import packageJson from "../../../package.json" ;
1717
@@ -24,6 +24,14 @@ jest.mock("../../services/supplier-config");
2424jest . mock ( "../../services/supplier-quotas" ) ;
2525jest . mock ( "../allocation-config" ) ;
2626
27+ jest . mock ( "@aws-lambda-powertools/idempotency" , ( ) => {
28+ const original = jest . requireActual ( "@aws-lambda-powertools/idempotency" ) ;
29+ return {
30+ ...original ,
31+ makeIdempotent : jest . fn ( ( fn , _ ) => fn ) ,
32+ } ;
33+ } ) ;
34+
2735function createSQSEvent ( records : SQSRecord [ ] ) : SQSEvent {
2836 return {
2937 Records : records ,
@@ -181,23 +189,35 @@ function setupDefaultMocks() {
181189}
182190
183191describe ( "createSupplierAllocatorHandler" , ( ) => {
184- let mockSqsClient : jest . Mocked < SQSClient > ;
185- let mockedDeps : jest . Mocked < Deps > ;
192+ const mockedDeps : jest . Mocked < Deps > = {
193+ logger : { error : jest . fn ( ) , info : jest . fn ( ) } as unknown as pino . Logger ,
194+ env : {
195+ SUPPLIER_CONFIG_TABLE_NAME : "SupplierConfigTable" ,
196+ SUPPLIER_QUOTAS_TABLE_NAME : "SupplierQuotasTable" ,
197+ IDEMPOTENCY_TABLE_NAME : "IdempotencyTable" ,
198+ } ,
199+ sqsClient : { send : jest . fn ( ) } as unknown as SQSClient ,
200+ supplierConfigRepo : {
201+ ddbClient : { } as any ,
202+ config : { } as any ,
203+ getLetterVariant : jest . fn ( ) ,
204+ getVolumeGroup : jest . fn ( ) ,
205+ getSupplierAllocationsForVolumeGroup : jest . fn ( ) ,
206+ getSuppliersDetails : jest . fn ( ) ,
207+ getSupplierPacksForPackSpecification : jest . fn ( ) ,
208+ getPackSpecification : jest . fn ( ) ,
209+ } ,
210+ supplierQuotasRepo : {
211+ ddbClient : { } as any ,
212+ config : { } as any ,
213+ getOverallAllocation : jest . fn ( ) ,
214+ updateOverallAllocation : jest . fn ( ) ,
215+ getDailyAllocation : jest . fn ( ) ,
216+ updateDailyAllocation : jest . fn ( ) ,
217+ } ,
218+ } as unknown as Deps ;
219+
186220 beforeEach ( ( ) => {
187- mockSqsClient = {
188- send : jest . fn ( ) ,
189- } as unknown as jest . Mocked < SQSClient > ;
190-
191- mockedDeps = {
192- logger : { error : jest . fn ( ) , info : jest . fn ( ) } as unknown as pino . Logger ,
193- env : {
194- SUPPLIER_CONFIG_TABLE_NAME : "SupplierConfigTable" ,
195- SUPPLIER_QUOTAS_TABLE_NAME : "SupplierQuotasTable" ,
196- } ,
197- sqsClient : mockSqsClient ,
198- supplierConfigRepo : { } as unknown as Deps [ "supplierConfigRepo" ] ,
199- supplierQuotasRepo : { } as unknown as Deps [ "supplierQuotasRepo" ] ,
200- } ;
201221 jest . clearAllMocks ( ) ;
202222 } ) ;
203223
@@ -218,8 +238,8 @@ describe("createSupplierAllocatorHandler", () => {
218238
219239 expect ( result . batchItemFailures ) . toHaveLength ( 0 ) ;
220240
221- expect ( mockSqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
222- const sendCall = ( mockSqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
241+ expect ( mockedDeps . sqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
242+ const sendCall = ( mockedDeps . sqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
223243 expect ( sendCall ) . toBeInstanceOf ( SendMessageCommand ) ;
224244
225245 const messageBody = JSON . parse ( sendCall . input . MessageBody ) ;
@@ -255,8 +275,8 @@ describe("createSupplierAllocatorHandler", () => {
255275
256276 expect ( result . batchItemFailures ) . toHaveLength ( 0 ) ;
257277
258- expect ( mockSqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
259- const sendCall = ( mockSqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
278+ expect ( mockedDeps . sqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
279+ const sendCall = ( mockedDeps . sqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
260280 expect ( sendCall ) . toBeInstanceOf ( SendMessageCommand ) ;
261281
262282 const messageBody = JSON . parse ( sendCall . input . MessageBody ) ;
@@ -289,8 +309,8 @@ describe("createSupplierAllocatorHandler", () => {
289309
290310 expect ( result . batchItemFailures ) . toHaveLength ( 0 ) ;
291311
292- expect ( mockSqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
293- const sendCall = ( mockSqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
312+ expect ( mockedDeps . sqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
313+ const sendCall = ( mockedDeps . sqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
294314 const messageBody = JSON . parse ( sendCall . input . MessageBody ) ;
295315 expect ( messageBody . allocationDetails . supplierSpec ) . toEqual ( {
296316 supplierId : "supplier1" ,
@@ -335,7 +355,7 @@ describe("createSupplierAllocatorHandler", () => {
335355 const handler = createSupplierAllocatorHandler ( mockedDeps ) ;
336356 await handler ( evt , { } as any , { } as any ) ;
337357
338- const sendCall = ( mockSqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
358+ const sendCall = ( mockedDeps . sqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
339359 const messageBody = JSON . parse ( sendCall . input . MessageBody ) ;
340360 expect ( messageBody . letterEvent . data . domainId ) . toBe ( "letter-test" ) ;
341361 } ) ;
@@ -384,7 +404,7 @@ describe("createSupplierAllocatorHandler", () => {
384404 if ( ! result ) throw new Error ( "expected BatchResponse, got void" ) ;
385405
386406 expect ( result . batchItemFailures ) . toHaveLength ( 0 ) ;
387- expect ( mockSqsClient . send ) . toHaveBeenCalledTimes ( 2 ) ;
407+ expect ( mockedDeps . sqsClient . send ) . toHaveBeenCalledTimes ( 2 ) ;
388408 } ) ;
389409
390410 test ( "returns batch failure for invalid JSON" , async ( ) => {
@@ -454,7 +474,7 @@ describe("createSupplierAllocatorHandler", () => {
454474 process . env . UPSERT_LETTERS_QUEUE_URL = "https://sqs.test.queue" ;
455475
456476 const sqsError = new Error ( "SQS send failed" ) ;
457- ( mockSqsClient . send as jest . Mock ) . mockRejectedValueOnce ( sqsError ) ;
477+ ( mockedDeps . sqsClient . send as jest . Mock ) . mockRejectedValueOnce ( sqsError ) ;
458478
459479 const handler = createSupplierAllocatorHandler ( mockedDeps ) ;
460480 const result = await handler ( evt , { } as any , { } as any ) ;
@@ -487,7 +507,7 @@ describe("createSupplierAllocatorHandler", () => {
487507 expect ( result . batchItemFailures ) . toHaveLength ( 1 ) ;
488508 expect ( result . batchItemFailures [ 0 ] . itemIdentifier ) . toBe ( "fail-msg" ) ;
489509
490- expect ( mockSqsClient . send ) . toHaveBeenCalledTimes ( 2 ) ;
510+ expect ( mockedDeps . sqsClient . send ) . toHaveBeenCalledTimes ( 2 ) ;
491511 } ) ;
492512
493513 test ( "sends correct queue URL in SQS message command" , async ( ) => {
@@ -503,7 +523,7 @@ describe("createSupplierAllocatorHandler", () => {
503523 const handler = createSupplierAllocatorHandler ( mockedDeps ) ;
504524 await handler ( evt , { } as any , { } as any ) ;
505525
506- const sendCall = ( mockSqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
526+ const sendCall = ( mockedDeps . sqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
507527 expect ( sendCall . input . QueueUrl ) . toBe ( queueUrl ) ;
508528 } ) ;
509529
@@ -531,8 +551,8 @@ describe("createSupplierAllocatorHandler", () => {
531551 variantId : "lv1" ,
532552 } ) ,
533553 ) ;
534- expect ( mockSqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
535- const sendCall = ( mockSqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
554+ expect ( mockedDeps . sqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
555+ const sendCall = ( mockedDeps . sqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
536556 expect ( sendCall ) . toBeInstanceOf ( SendMessageCommand ) ;
537557
538558 const messageBody = JSON . parse ( sendCall . input . MessageBody ) ;
@@ -641,8 +661,9 @@ describe("createSupplierAllocatorHandler", () => {
641661 variantId : "lv1" ,
642662 } ) ,
643663 ) ;
644- expect ( mockSqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
645- const sendCall = ( mockSqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
664+ expect ( mockedDeps . sqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
665+ const sendCall = ( mockedDeps . sqsClient . send as jest . Mock ) . mock
666+ . calls [ 0 ] [ 0 ] ;
646667 expect ( sendCall ) . toBeInstanceOf ( SendMessageCommand ) ;
647668
648669 const messageBody = JSON . parse ( sendCall . input . MessageBody ) ;
@@ -685,8 +706,8 @@ describe("createSupplierAllocatorHandler", () => {
685706 variantId : "lv1" ,
686707 } ) ,
687708 ) ;
688- expect ( mockSqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
689- const sendCall = ( mockSqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
709+ expect ( mockedDeps . sqsClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
710+ const sendCall = ( mockedDeps . sqsClient . send as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
690711 expect ( sendCall ) . toBeInstanceOf ( SendMessageCommand ) ;
691712
692713 const messageBody = JSON . parse ( sendCall . input . MessageBody ) ;
@@ -745,4 +766,22 @@ describe("createSupplierAllocatorHandler", () => {
745766 expect ( result . batchItemFailures ) . toHaveLength ( 0 ) ;
746767 expect ( allocationConfig . selectSupplierByFactor ) . toHaveBeenCalledTimes ( 2 ) ;
747768 } ) ;
769+
770+ test ( "does not process a message more than once due to idempotency wrapper" , async ( ) => {
771+ const preparedEvent = createPreparedV2Event ( ) ;
772+ const evt : SQSEvent = createSQSEvent ( [
773+ createSqsRecord ( "msg1" , JSON . stringify ( preparedEvent ) ) ,
774+ ] ) ;
775+
776+ process . env . UPSERT_LETTERS_QUEUE_URL = "https://sqs.test.queue" ;
777+
778+ setupDefaultMocks ( ) ;
779+ ( makeIdempotent as jest . Mock ) . mockImplementationOnce ( ( _fn ) => "supplier1" ) ;
780+
781+ const handler = createSupplierAllocatorHandler ( mockedDeps ) ;
782+ await handler ( evt , { } as any , { } as any ) ;
783+
784+ expect ( makeIdempotent ) . toHaveBeenCalledTimes ( 1 ) ;
785+ expect ( mockedDeps . sqsClient . send ) . not . toHaveBeenCalled ( ) ;
786+ } ) ;
748787} ) ;
0 commit comments