@@ -19,8 +19,27 @@ import { Deps } from "../config/deps";
1919type SupplierSpec = { supplierId : string ; specId : string } ;
2020type PreparedEvents = LetterRequestPreparedEventV2 | LetterRequestPreparedEvent ;
2121
22+ const SupplierSpecSchema = z . object ( {
23+ supplierId : z . string ( ) . min ( 1 ) ,
24+ specId : z . string ( ) . min ( 1 ) ,
25+ } ) ;
26+
27+ const LetterEventUnionSchema = z . discriminatedUnion ( "type" , [
28+ $LetterRequestPreparedEventV2 ,
29+ $LetterRequestPreparedEvent ,
30+ $LetterEvent ,
31+ ] ) ;
32+
33+ const QueueMessageSchema = z . union ( [
34+ $LetterEvent ,
35+ z . object ( {
36+ letterEvent : LetterEventUnionSchema ,
37+ supplierSpec : SupplierSpecSchema . optional ( ) ,
38+ } ) ,
39+ ] ) ;
40+
2241type UpsertOperation = {
23- name : "Insert" | "Update" ;
42+ name : "Insert" | "Update" | "Unknown" ;
2443 schemas : z . ZodSchema [ ] ;
2544 handler : (
2645 request : unknown ,
@@ -29,11 +48,10 @@ type UpsertOperation = {
2948 ) => Promise < void > ;
3049} ;
3150
32- // small envelope that must exist in all inputs
33- const TypeEnvelope = z . object ( { type : z . string ( ) . min ( 1 ) } ) ;
34-
3551function getOperationFromType ( type : string ) : UpsertOperation {
36- if ( type . startsWith ( "uk.nhs.notify.letter-rendering.letter-request.prepared" ) )
52+ if (
53+ type . startsWith ( "uk.nhs.notify.letter-rendering.letter-request.prepared" )
54+ ) {
3755 return {
3856 name : "Insert" ,
3957 schemas : [ $LetterRequestPreparedEventV2 , $LetterRequestPreparedEvent ] ,
@@ -55,24 +73,24 @@ function getOperationFromType(type: string): UpsertOperation {
5573 } ) ;
5674 } ,
5775 } ;
58- if ( type . startsWith ( "uk.nhs.notify.supplier-api.letter" ) )
59- return {
60- name : "Update" ,
61- schemas : [ $LetterEvent ] ,
62- handler : async ( request , supplierSpec , deps ) => {
63- const supplierEvent = request as LetterEvent ;
64- const letterToUpdate : UpdateLetter = mapToUpdateLetter ( supplierEvent ) ;
65- await deps . letterRepo . updateLetterStatus ( letterToUpdate ) ;
76+ }
77+ // if it's not an insert type, it must be an update as we've already parsed the message, but we want to have a separate operation for better logging and metrics
78+ return {
79+ name : "Update" ,
80+ schemas : [ $LetterEvent ] ,
81+ handler : async ( request , supplierSpec , deps ) => {
82+ const supplierEvent = request as LetterEvent ;
83+ const letterToUpdate : UpdateLetter = mapToUpdateLetter ( supplierEvent ) ;
84+ await deps . letterRepo . updateLetterStatus ( letterToUpdate ) ;
6685
67- deps . logger . info ( {
68- description : "Updated letter" ,
69- eventId : supplierEvent . id ,
70- letterId : letterToUpdate . id ,
71- supplierId : letterToUpdate . supplierId ,
72- } ) ;
73- } ,
74- } ;
75- throw new Error ( `Unknown operation from type=${ type } ` ) ;
86+ deps . logger . info ( {
87+ description : "Updated letter" ,
88+ eventId : supplierEvent . id ,
89+ letterId : letterToUpdate . id ,
90+ supplierId : letterToUpdate . supplierId ,
91+ } ) ;
92+ } ,
93+ } ;
7694}
7795
7896function mapToInsertLetter (
@@ -111,19 +129,6 @@ function mapToUpdateLetter(upsertRequest: LetterEvent): UpdateLetter {
111129 reasonText : upsertRequest . data . reasonText ,
112130 } ;
113131}
114- function getType ( event : unknown ) {
115- const env = TypeEnvelope . safeParse ( event ) ;
116- if ( ! env . success ) {
117- // Helpful debugging info:
118- const pretty = ( ( ) => {
119- return JSON . stringify ( event , null , 2 ) ;
120- } ) ( ) ;
121- throw new Error (
122- `Missing or invalid envelope.type field. Payload seen:\n${ pretty } ` ,
123- ) ;
124- }
125- return env . data . type ;
126- }
127132
128133async function runUpsert (
129134 operation : UpsertOperation ,
@@ -138,8 +143,6 @@ async function runUpsert(
138143 return ;
139144 }
140145 }
141- // none matched
142- throw new Error ( "No matching schema for received message" ) ;
143146}
144147
145148async function emitMetrics (
@@ -193,7 +196,24 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
193196 queueMessage,
194197 } ) ;
195198
196- const { letterEvent, supplierSpec } = queueMessage ;
199+ const result = QueueMessageSchema . safeParse ( queueMessage ) ;
200+ if ( ! result . success ) {
201+ throw new Error (
202+ `Message did not match expected schema: ${ JSON . stringify (
203+ result . error . issues ,
204+ ) } `,
205+ ) ;
206+ }
207+ let letterEvent : any ;
208+ let supplierSpec : SupplierSpec | undefined ;
209+
210+ if ( "letterEvent" in result . data ) {
211+ letterEvent = result . data . letterEvent ;
212+ supplierSpec = result . data . supplierSpec ;
213+ } else {
214+ letterEvent = result . data ;
215+ supplierSpec = undefined ;
216+ }
197217
198218 deps . logger . info ( {
199219 description : "Extracted letter event" ,
@@ -207,11 +227,14 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
207227 ? getSupplierIdFromEvent ( letterEvent )
208228 : supplierSpec . supplierId ;
209229
210- const type = getType ( letterEvent ) ;
230+ const operation = getOperationFromType ( letterEvent . type ) ;
211231
212- const operation = getOperationFromType ( type ) ;
213-
214- await runUpsert ( operation , letterEvent , supplierSpec , deps ) ;
232+ await runUpsert (
233+ operation ,
234+ letterEvent ,
235+ supplierSpec ?? { supplierId : "unknown" , specId : "unknown" } ,
236+ deps ,
237+ ) ;
215238
216239 perSupplierSuccess . set (
217240 supplier ,
0 commit comments