@@ -34,32 +34,57 @@ function estimateSize(obj: unknown) {
3434}
3535
3636/**
37- * Returns the original data, updated with the status returned by NHS notify.
38- * Does not return data for messages that failed to send.
39- *
40- * @param logger AWS logging object
41- * @param routingPlanId The Notify routing plan ID with which to process the data
42- * @param data The details for the notification
37+ * Create and configure an axios instance for Notify
38+ * @param logger
39+ * @param notifyBaseUrl - The base URL for the Notify API. DOES NOT include /comms, or anything after that.
40+ * @param bearerToken - The CIS2 OAuth bearer token to use for authentication
41+ * @returns
4342 */
44- export async function makeBatchNotifyRequest (
43+ function setupAxios ( logger : Logger , notifyBaseUrl : string , bearerToken : string ) : ReturnType < typeof axios . create > {
44+
45+ const axiosInstance = axios . create ( {
46+ baseURL : notifyBaseUrl + "/comms" ,
47+ headers : {
48+ Accept : "*/*" ,
49+ "Content-Type" : "application/vnd.api+json" ,
50+ Authorization : `Bearer ${ bearerToken } `
51+ }
52+ } )
53+
54+ // Retry configuration for rate limiting
55+ const onAxiosRetry = ( retryCount : number , error : unknown ) => {
56+ logger . warn ( `Call to notify failed - retrying. Retry count ${ retryCount } ` , { error} )
57+ }
58+
59+ // Axios-retry respects the `Retry-After` header OOTB
60+ axiosRetry ( axiosInstance , {
61+ retries : 5 ,
62+ onRetry : onAxiosRetry
63+ } )
64+
65+ return axiosInstance
66+ }
67+
68+ /**
69+ * Handles making requests to NHS Notify for a batch of messages.
70+ * Decides whether to make real requests or fake ones based on config.
71+ * @param logger
72+ * @param routingPlanId - The Notify routing plan ID with which to process the data
73+ * @param data - PSU SQS messages to process
74+ * @returns
75+ */
76+ export async function handleNotifyRequests (
4577 logger : Logger ,
4678 routingPlanId : string ,
4779 data : Array < NotifyDataItemMessage >
4880) : Promise < Array < NotifyDataItemMessage > > {
49- const { makeRealNotifyRequests, notifyApiBaseUrlRaw} = await loadConfig ( )
50-
51- if ( ! notifyApiBaseUrlRaw ) throw new Error ( "NOTIFY_API_BASE_URL is not defined in the environment variables!" )
52-
53- // Just to be safe, trim any whitespace. Also, secrets may be bytes, so make sure it's a string
54- const BASE_URL = notifyApiBaseUrlRaw . trim ( )
5581
5682 // Early break for empty data
5783 if ( data . length === 0 ) {
5884 return [ ]
5985 }
6086
61- // Shared between all messages in this batch
62- const messageBatchReference = v4 ( )
87+ const configPromise = loadConfig ( )
6388
6489 // Map the NotifyDataItems into the structure needed for notify
6590 const messages : Array < MessageBatchItem > = data . flatMap ( item => {
@@ -73,10 +98,80 @@ export async function makeBatchNotifyRequest(
7398 messageReference : item . messageReference ,
7499 recipient : { nhsNumber : item . PSUDataItem . PatientNHSNumber } ,
75100 originator : { odsCode : item . PSUDataItem . PharmacyODSCode } ,
76- personalisation : { }
101+ personalisation : { } ,
102+ PSURequestId : item . PSUDataItem . RequestID
77103 } ]
78104 } )
79105
106+ // Check if we should make real requests
107+ const { makeRealNotifyRequestsFlag, notifyApiBaseUrlRaw} = await configPromise
108+ if ( ! makeRealNotifyRequestsFlag || ! notifyApiBaseUrlRaw ) return await makeFakeNotifyRequest ( logger , data , messages )
109+
110+ if ( ! notifyApiBaseUrlRaw ) throw new Error ( "NOTIFY_API_BASE_URL is not defined in the environment variables!" )
111+ // Just to be safe, trim any whitespace. Also, secrets may be bytes, so make sure it's a string
112+ const notifyBaseUrl = notifyApiBaseUrlRaw . trim ( )
113+
114+ return await makeRealNotifyRequest ( logger , routingPlanId , notifyBaseUrl , data , messages )
115+ }
116+
117+ /**
118+ * Simulates making requests to NHS Notify for a batch of messages.
119+ * Waits a short time, then returns "successful" responses for all messages.
120+ */
121+ async function makeFakeNotifyRequest (
122+ logger : Logger ,
123+ data : Array < NotifyDataItemMessage > ,
124+ messages : Array < MessageBatchItem >
125+ ) : Promise < Array < NotifyDataItemMessage > > {
126+
127+ logger . info ( "Not doing real Notify requests. Simply waiting for some time and returning success on all messages" )
128+ await new Promise ( f => setTimeout ( f , DUMMY_NOTIFY_DELAY_MS ) )
129+
130+ const messageStatus = "silent running"
131+ const messageBatchReference = v4 ( )
132+
133+ logger . info ( "Requested notifications OK!" , {
134+ messageBatchReference,
135+ messageReferences : messages . map ( e => ( {
136+ nhsNumber : e . recipient . nhsNumber ,
137+ messageReference : e . messageReference ,
138+ psuRequestId : data . find ( ( el ) => el . messageReference === e . messageReference ) ?. PSUDataItem . RequestID
139+ } ) ) ,
140+ deliveryStatus : messageStatus // TODO: change splunk report query to messageStatus
141+ } )
142+
143+ // Map each input item to a "successful" NotifyDataItemMessage
144+ return data . map ( item => {
145+ return {
146+ ...item ,
147+ messageBatchReference,
148+ messageStatus,
149+ notifyMessageId : v4 ( ) // Create a dummy UUID
150+ }
151+ } )
152+ }
153+
154+ /**
155+ * Makes real requests to NHS Notify for a batch of messages.
156+ * Handles splitting large batches into smaller ones as needed.
157+ *
158+ * @param logger - AWS logging object
159+ * @param routingPlanId - The Notify routing plan ID with which to process the data
160+ * @param data - The details for the notification
161+ */
162+ export async function makeRealNotifyRequest (
163+ logger : Logger ,
164+ routingPlanId : string ,
165+ notifyBaseUrl : string ,
166+ data : Array < NotifyDataItemMessage > ,
167+ messages : Array < MessageBatchItem > ,
168+ bearerToken ?: string ,
169+ axiosInstance ?: ReturnType < typeof axios . create >
170+ ) : Promise < Array < NotifyDataItemMessage > > {
171+
172+ // Shared between all messages in this batch
173+ const messageBatchReference = v4 ( )
174+
80175 const body : CreateMessageBatchRequest = {
81176 data : {
82177 type : "MessageBatch" as const ,
@@ -88,80 +183,35 @@ export async function makeBatchNotifyRequest(
88183 }
89184 }
90185
186+ // Lazily get the bearer token and axios instance, so we only do it once even if we recurse
187+ if ( ! bearerToken ) bearerToken = await tokenExchange ( logger , notifyBaseUrl )
188+ if ( ! axiosInstance ) axiosInstance = setupAxios ( logger , notifyBaseUrl , bearerToken )
189+
91190 // Recursive split if too large
92- if ( data . length >= NOTIFY_REQUEST_MAX_ITEMS || estimateSize ( body ) > NOTIFY_REQUEST_MAX_BYTES ) {
191+ if ( messages . length >= NOTIFY_REQUEST_MAX_ITEMS || estimateSize ( body ) > NOTIFY_REQUEST_MAX_BYTES ) {
93192 logger . info ( "Received a large payload - splitting in half and trying again" ,
94- { messageCount : data . length , estimatedSize : estimateSize ( body ) }
193+ { messageCount : messages . length , estimatedSize : estimateSize ( body ) }
95194 )
96- const mid = Math . floor ( data . length / 2 )
97- const firstHalf = data . slice ( 0 , mid )
98- const secondHalf = data . slice ( mid )
195+ const mid = Math . floor ( messages . length / 2 )
196+ const firstHalf = messages . slice ( 0 , mid )
197+ const secondHalf = messages . slice ( mid )
198+
99199 // send both halves in parallel
100200 const [ res1 , res2 ] = await Promise . all ( [
101- makeBatchNotifyRequest ( logger , routingPlanId , firstHalf ) ,
102- makeBatchNotifyRequest ( logger , routingPlanId , secondHalf )
201+ makeRealNotifyRequest ( logger , routingPlanId , notifyBaseUrl , data , firstHalf , bearerToken , axiosInstance ) ,
202+ makeRealNotifyRequest ( logger , routingPlanId , notifyBaseUrl , data , secondHalf , bearerToken , axiosInstance )
103203 ] )
104204 return [ ...res1 , ...res2 ]
105205 }
106206
107- if ( ! makeRealNotifyRequests ) {
108- logger . info ( "Not doing real Notify requests. Simply waiting for some time and returning success on all messages" )
109- await new Promise ( f => setTimeout ( f , DUMMY_NOTIFY_DELAY_MS ) )
110-
111- const messageStatus = "silent running"
112-
113- logger . info ( "Requested notifications OK!" , {
114- messageBatchReference,
115- messageReferences : messages . map ( e => ( {
116- nhsNumber : e . recipient . nhsNumber ,
117- messageReference : e . messageReference ,
118- psuRequestId : data . find ( ( el ) => el . messageReference === e . messageReference ) ?. PSUDataItem . RequestID
119- } ) ) ,
120- deliveryStatus : messageStatus // TODO: change splunk report query to messageStatus
121- } )
122-
123- // Map each input item to a "successful" NotifyDataItemMessage
124- return data . map ( item => {
125- return {
126- ...item ,
127- messageBatchReference,
128- messageStatus,
129- notifyMessageId : v4 ( ) // Create a dummy UUID
130- }
131- } )
132- }
133-
134- // This is actually going to hit notify, so get the bearer token
135- const bearerToken = await tokenExchange ( logger , BASE_URL )
136-
137- logger . info ( "Making a request for notifications to NHS notify" , { count : data . length , routingPlanId} )
138-
139- // Create an axios instance configured for Notify
140- const axiosInstance = axios . create ( {
141- baseURL : BASE_URL + "/comms" ,
142- headers : {
143- Accept : "*/*" ,
144- "Content-Type" : "application/vnd.api+json" ,
145- Authorization : `Bearer ${ bearerToken } `
146- }
147- } )
148-
149- // Retry configuration for rate limiting
150- const onAxiosRetry = ( retryCount : number , error : unknown ) => {
151- logger . warn ( `Call to notify failed - retrying. Retry count ${ retryCount } ` , { error} )
152- }
153-
154- // Axios-retry respects the `Retry-After` header
155- axiosRetry ( axiosInstance , {
156- retries : 5 ,
157- onRetry : onAxiosRetry
158- } )
207+ logger . info ( "Making a request for notifications to NHS notify" , { count : messages . length , routingPlanId} )
159208
160209 try {
161210 const resp = await axiosInstance . post < CreateMessageBatchResponse > ( "/v1/message-batches" , body )
162211
212+ // From here is just logging stuff for reporting, and mapping the response back to the input data
213+
163214 if ( resp . status === 201 ) {
164- const returnedMessages = resp . data . data . attributes . messages
165215 logger . info ( "Requested notifications OK!" , {
166216 messageBatchReference,
167217 messageReferences : messages . map ( e => ( {
@@ -172,20 +222,24 @@ export async function makeBatchNotifyRequest(
172222 deliveryStatus : "requested" // TODO: change splunk report query to messageStatus
173223 } )
174224
175- // Map each input item to a NotifyDataItemMessage, marking success and attaching the notify ID
176- return data . map ( item => {
177- const match = returnedMessages . find (
178- m => m . messageReference === item . messageReference
179- )
180-
181- // SUCCESS
182- return {
183- ...item ,
184- messageBatchReference,
185- messageStatus : match ? "requested" : "notify request failed" ,
186- notifyMessageId : match ?. id
187- }
188- } )
225+ // Map each input item to a NotifyDataItemMessage, marking success and attaching the notify ID.
226+ // Only return items that belong to *this* batch (so we handle recursive splits correctly).
227+ const batchRefs = new Set ( messages . map ( m => m . messageReference ) )
228+ const returnedByRef = new Map (
229+ resp . data . data . attributes . messages . map ( m => [ m . messageReference , m ] )
230+ )
231+
232+ return data
233+ . filter ( item => batchRefs . has ( item . messageReference ) )
234+ . map ( item => {
235+ const match = returnedByRef . get ( item . messageReference )
236+ return {
237+ ...item ,
238+ messageBatchReference,
239+ messageStatus : match ? "requested" : "notify request failed" ,
240+ notifyMessageId : match ?. id
241+ }
242+ } )
189243
190244 } else {
191245 logger . error ( "Notify batch request failed" , {
0 commit comments