77 UpdateCommand ,
88 UpdateCommandOutput ,
99} from "@aws-sdk/lib-dynamodb" ;
10+ import { ConditionalCheckFailedException } from "@aws-sdk/client-dynamodb" ;
1011import { Logger } from "pino" ;
1112import { z } from "zod" ;
1213import {
@@ -163,32 +164,16 @@ export class LetterRepository {
163164 } ;
164165 }
165166
166- async updateLetterStatus ( letterToUpdate : UpdateLetter ) : Promise < Letter > {
167+ async updateLetterStatus (
168+ letterToUpdate : UpdateLetter ,
169+ ) : Promise < Letter | undefined > {
167170 this . log . debug (
168171 `Updating letter ${ letterToUpdate . id } to status ${ letterToUpdate . status } ` ,
169172 ) ;
170173 let result : UpdateCommandOutput ;
171174 try {
172- let updateExpression =
173- "set #status = :status, updatedAt = :updatedAt, supplierStatus = :supplierStatus, #ttl = :ttl" ;
174- const expressionAttributeValues : Record < string , any > = {
175- ":status" : letterToUpdate . status ,
176- ":updatedAt" : new Date ( ) . toISOString ( ) ,
177- ":supplierStatus" : `${ letterToUpdate . supplierId } #${ letterToUpdate . status } ` ,
178- ":ttl" : Math . floor (
179- Date . now ( ) / 1000 + 60 * 60 * this . config . lettersTtlHours ,
180- ) ,
181- } ;
182-
183- if ( letterToUpdate . reasonCode ) {
184- updateExpression += ", reasonCode = :reasonCode" ;
185- expressionAttributeValues [ ":reasonCode" ] = letterToUpdate . reasonCode ;
186- }
187-
188- if ( letterToUpdate . reasonText ) {
189- updateExpression += ", reasonText = :reasonText" ;
190- expressionAttributeValues [ ":reasonText" ] = letterToUpdate . reasonText ;
191- }
175+ const { expressionAttributeValues, updateExpression } =
176+ this . buildUpdateExpression ( letterToUpdate ) ;
192177
193178 result = await this . ddbClient . send (
194179 new UpdateCommand ( {
@@ -198,31 +183,61 @@ export class LetterRepository {
198183 supplierId : letterToUpdate . supplierId ,
199184 } ,
200185 UpdateExpression : updateExpression ,
201- ConditionExpression : "attribute_exists(id)" , // Ensure letter exists
186+ ConditionExpression :
187+ "attribute_exists(id) AND (attribute_not_exists(eventId) OR eventId <> :eventId)" ,
202188 ExpressionAttributeNames : {
203189 "#status" : "status" ,
204190 "#ttl" : "ttl" ,
205191 } ,
206192 ExpressionAttributeValues : expressionAttributeValues ,
207193 ReturnValues : "ALL_NEW" ,
194+ ReturnValuesOnConditionCheckFailure : "ALL_OLD" ,
208195 } ) ,
209196 ) ;
197+
198+ this . log . debug (
199+ `Updated letter ${ letterToUpdate . id } to status ${ letterToUpdate . status } ` ,
200+ ) ;
201+ return LetterSchema . parse ( result . Attributes ) ;
210202 } catch ( error ) {
211- if (
212- error instanceof Error &&
213- error . name === "ConditionalCheckFailedException"
214- ) {
203+ if ( error instanceof ConditionalCheckFailedException ) {
204+ if ( error . Item ?. eventId . S === letterToUpdate . eventId ) {
205+ this . log . warn (
206+ `Skipping update for letter ${ letterToUpdate . id } : eventId ${ letterToUpdate . eventId } already processed` ,
207+ ) ;
208+ return undefined ;
209+ }
215210 throw new Error (
216211 `Letter with id ${ letterToUpdate . id } not found for supplier ${ letterToUpdate . supplierId } ` ,
217212 ) ;
218213 }
219214 throw error ;
220215 }
216+ }
221217
222- this . log . debug (
223- `Updated letter ${ letterToUpdate . id } to status ${ letterToUpdate . status } ` ,
224- ) ;
225- return LetterSchema . parse ( result . Attributes ) ;
218+ private buildUpdateExpression ( letterToUpdate : UpdateLetter ) {
219+ let updateExpression =
220+ "set #status = :status, updatedAt = :updatedAt, supplierStatus = :supplierStatus, #ttl = :ttl, eventId = :eventId" ;
221+ const expressionAttributeValues : Record < string , any > = {
222+ ":status" : letterToUpdate . status ,
223+ ":updatedAt" : new Date ( ) . toISOString ( ) ,
224+ ":supplierStatus" : `${ letterToUpdate . supplierId } #${ letterToUpdate . status } ` ,
225+ ":ttl" : Math . floor (
226+ Date . now ( ) / 1000 + 60 * 60 * this . config . lettersTtlHours ,
227+ ) ,
228+ ":eventId" : letterToUpdate . eventId ,
229+ } ;
230+
231+ if ( letterToUpdate . reasonCode ) {
232+ updateExpression += ", reasonCode = :reasonCode" ;
233+ expressionAttributeValues [ ":reasonCode" ] = letterToUpdate . reasonCode ;
234+ }
235+
236+ if ( letterToUpdate . reasonText ) {
237+ updateExpression += ", reasonText = :reasonText" ;
238+ expressionAttributeValues [ ":reasonText" ] = letterToUpdate . reasonText ;
239+ }
240+ return { updateExpression, expressionAttributeValues } ;
226241 }
227242
228243 async getLettersBySupplier (
0 commit comments