@@ -182,6 +182,10 @@ describe('POST /webhooks', () => {
182
182
183
183
const webhookData = JSON . parse ( payload ) ;
184
184
webhookData . type = webhookType ;
185
+
186
+ const webhookTimestamp = new Date ( '2025-01-15T10:30:00.000Z' ) . toISOString ( ) ;
187
+ webhookData . created_at = webhookTimestamp ;
188
+
185
189
payload = JSON . stringify ( webhookData ) ;
186
190
187
191
const invoiceId = webhookData . invoice . id ;
@@ -200,6 +204,10 @@ describe('POST /webhooks', () => {
200
204
expect ( invoice . total ) . toBe ( webhookData . invoice . amount_due ) ;
201
205
expect ( invoice . currency ) . toBe ( webhookData . invoice . currency ) ;
202
206
expect ( invoice . status ) . toBe ( webhookData . invoice . status ) ;
207
+
208
+ // Verify that last_synced_at gets set to the webhook timestamp for new invoices
209
+ expect ( invoice . last_synced_at ) . toBeDefined ( ) ;
210
+ expect ( new Date ( invoice . last_synced_at ) . toISOString ( ) ) . toBe ( webhookTimestamp ) ;
203
211
} ) ;
204
212
205
213
it ( 'should update an existing invoice when webhook arrives' , async ( ) => {
@@ -229,8 +237,14 @@ describe('POST /webhooks', () => {
229
237
status : initialStatus ,
230
238
} ;
231
239
232
- // Store the initial invoice in the database
233
- await syncInvoices ( postgresClient , [ initialInvoiceData ] ) ;
240
+ // Store the initial invoice in the database with an old timestamp
241
+ const oldTimestamp = new Date ( '2025-01-10T10:00:00.000Z' ) . toISOString ( ) ;
242
+ await syncInvoices ( postgresClient , [ initialInvoiceData ] , oldTimestamp ) ;
243
+
244
+ // Verify the initial invoice was created with the old timestamp
245
+ const [ initialInvoice ] = await fetchInvoicesFromDatabase ( orbSync . postgresClient , [ invoiceId ] ) ;
246
+ expect ( initialInvoice ) . toBeDefined ( ) ;
247
+ expect ( new Date ( initialInvoice . last_synced_at ) . toISOString ( ) ) . toBe ( oldTimestamp ) ;
234
248
235
249
// Now update the webhook data with new values
236
250
const updatedAmount = 1500 ;
@@ -240,6 +254,10 @@ describe('POST /webhooks', () => {
240
254
webhookData . invoice . status = updatedStatus ;
241
255
webhookData . invoice . paid_at = new Date ( ) . toISOString ( ) ;
242
256
257
+ // Set a newer webhook timestamp that should trigger an update
258
+ const newWebhookTimestamp = new Date ( '2025-01-15T10:30:00.000Z' ) . toISOString ( ) ;
259
+ webhookData . created_at = newWebhookTimestamp ;
260
+
243
261
payload = JSON . stringify ( webhookData ) ;
244
262
245
263
// Send the webhook with updated data
@@ -252,13 +270,80 @@ describe('POST /webhooks', () => {
252
270
} ) ;
253
271
254
272
// Verify that the invoice was updated in the database
255
- const [ invoice ] = await fetchInvoicesFromDatabase ( orbSync . postgresClient , [ invoiceId ] ) ;
256
- expect ( invoice ) . toBeDefined ( ) ;
257
- expect ( Number ( invoice . total ) ) . toBe ( updatedAmount ) ;
258
- expect ( invoice . status ) . toBe ( updatedStatus ) ;
273
+ const [ updatedInvoice ] = await fetchInvoicesFromDatabase ( orbSync . postgresClient , [ invoiceId ] ) ;
274
+ expect ( updatedInvoice ) . toBeDefined ( ) ;
275
+ expect ( Number ( updatedInvoice . total ) ) . toBe ( updatedAmount ) ;
276
+ expect ( updatedInvoice . status ) . toBe ( updatedStatus ) ;
259
277
260
278
// Verify that the updated_at timestamp was changed
261
- expect ( invoice . updated_at ) . toBeDefined ( ) ;
262
- expect ( new Date ( invoice . updated_at ) . getTime ( ) ) . toBeGreaterThan ( new Date ( webhookData . invoice . created_at ) . getTime ( ) ) ;
279
+ expect ( updatedInvoice . updated_at ) . toBeDefined ( ) ;
280
+ expect ( new Date ( updatedInvoice . updated_at ) . getTime ( ) ) . toBeGreaterThan ( new Date ( webhookData . invoice . created_at ) . getTime ( ) ) ;
281
+
282
+ // Verify that last_synced_at was updated to the new webhook timestamp
283
+ expect ( updatedInvoice . last_synced_at ) . toBeDefined ( ) ;
284
+ expect ( new Date ( updatedInvoice . last_synced_at ) . toISOString ( ) ) . toBe ( newWebhookTimestamp ) ;
285
+
286
+ // Verify that the new timestamp is newer than the old timestamp
287
+ expect ( new Date ( updatedInvoice . last_synced_at ) . getTime ( ) ) . toBeGreaterThan ( new Date ( oldTimestamp ) . getTime ( ) ) ;
288
+ } ) ;
289
+
290
+ it ( 'should NOT update invoice when webhook timestamp is older than last_synced_at' , async ( ) => {
291
+ let payload = loadWebhookPayload ( 'invoice' ) ;
292
+ const postgresClient = orbSync . postgresClient ;
293
+
294
+ const webhookData = JSON . parse ( payload ) ;
295
+ const invoiceId = webhookData . invoice . id ;
296
+ await deleteTestData ( orbSync . postgresClient , 'invoices' , [ invoiceId ] ) ;
297
+
298
+ webhookData . type = 'invoice.payment_succeeded' ;
299
+
300
+ // Insert an invoice with a "new" timestamp and known values
301
+ const originalAmount = 2000 ;
302
+ const originalStatus = 'paid' ;
303
+ webhookData . invoice . amount_due = originalAmount . toString ( ) ;
304
+ webhookData . invoice . total = originalAmount . toString ( ) ;
305
+ webhookData . invoice . status = originalStatus ;
306
+
307
+ const newTimestamp = new Date ( '2025-01-15T10:30:00.000Z' ) . toISOString ( ) ;
308
+ const initialInvoiceData = {
309
+ ...webhookData . invoice ,
310
+ amount_due : originalAmount . toString ( ) ,
311
+ total : originalAmount . toString ( ) ,
312
+ status : originalStatus ,
313
+ } ;
314
+ await syncInvoices ( postgresClient , [ initialInvoiceData ] , newTimestamp ) ;
315
+
316
+ // Verify the invoice was created with the new timestamp
317
+ const [ initialInvoice ] = await fetchInvoicesFromDatabase ( orbSync . postgresClient , [ invoiceId ] ) ;
318
+ expect ( initialInvoice ) . toBeDefined ( ) ;
319
+ expect ( Number ( initialInvoice . total ) ) . toBe ( originalAmount ) ;
320
+ expect ( initialInvoice . status ) . toBe ( originalStatus ) ;
321
+ expect ( new Date ( initialInvoice . last_synced_at ) . toISOString ( ) ) . toBe ( newTimestamp ) ;
322
+
323
+ // Now attempt to update with an older webhook timestamp and different values
324
+ const outdatedAmount = 1000 ;
325
+ const outdatedStatus = 'pending' ;
326
+ webhookData . invoice . amount_due = outdatedAmount . toString ( ) ;
327
+ webhookData . invoice . total = outdatedAmount . toString ( ) ;
328
+ webhookData . invoice . status = outdatedStatus ;
329
+ webhookData . invoice . paid_at = undefined ;
330
+ const oldWebhookTimestamp = new Date ( '2025-01-10T10:00:00.000Z' ) . toISOString ( ) ;
331
+ webhookData . created_at = oldWebhookTimestamp ;
332
+ payload = JSON . stringify ( webhookData ) ;
333
+
334
+ // Send the webhook with the outdated data
335
+ const response = await sendWebhookRequest ( payload ) ;
336
+ expect ( response . statusCode ) . toBe ( 200 ) ;
337
+ const data = response . json ( ) ;
338
+ expect ( data ) . toMatchObject ( { received : true } ) ;
339
+
340
+ // Fetch the invoice again and verify it was NOT updated
341
+ const [ afterWebhookInvoice ] = await fetchInvoicesFromDatabase ( orbSync . postgresClient , [ invoiceId ] ) ;
342
+ expect ( afterWebhookInvoice ) . toBeDefined ( ) ;
343
+ // Data should remain unchanged
344
+ expect ( Number ( afterWebhookInvoice . total ) ) . toBe ( originalAmount ) ;
345
+ expect ( afterWebhookInvoice . status ) . toBe ( originalStatus ) ;
346
+ // last_synced_at should remain the new timestamp
347
+ expect ( new Date ( afterWebhookInvoice . last_synced_at ) . toISOString ( ) ) . toBe ( newTimestamp ) ;
263
348
} ) ;
264
349
} ) ;
0 commit comments