Skip to content

Commit fe7dce9

Browse files
committed
Add test case to prevent invoice updates with outdated webhook timestamps, ensuring last_synced_at remains unchanged
1 parent a9f135b commit fe7dce9

File tree

1 file changed

+60
-0
lines changed

1 file changed

+60
-0
lines changed

apps/node-fastify/src/test/webhooks.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,64 @@ describe('POST /webhooks', () => {
286286
// Verify that the new timestamp is newer than the old timestamp
287287
expect(new Date(updatedInvoice.last_synced_at).getTime()).toBeGreaterThan(new Date(oldTimestamp).getTime());
288288
});
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);
348+
});
289349
});

0 commit comments

Comments
 (0)