OUT-3644: add idempotency guards on qb_invoice_sync writes - #236
Conversation
Concurrent webhook deliveries for the same invoice could both pass the existence check and insert duplicate qb_invoice_sync rows. Add a partial unique index on (portal_id, invoice_number) WHERE deleted_at IS NULL, swallow conflicts in createQBInvoice via onConflictDoNothing, re-check the mapping inside findOrMapInvoiceFromQBO after the QBO fetch, and skip the redundant logSync when the race is lost. Also filter isNull(deletedAt) in the sync-log lookups so updateOrCreateQBSyncLog can't revive a soft-deleted log row in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds idempotency guards to
Confidence Score: 3/5Not safe to merge as-is — the webhookInvoiceCreated path has a missing race-loss guard that can corrupt sync log data. A single P1 finding on the primary webhook code path caps the score at 4, and the inconsistency between two code paths handling the same race (one correctly, one not) warrants pulling the score below the ceiling. src/app/api/quickbooks/invoice/invoice.service.ts — specifically the webhookInvoiceCreated path around line 788 where the createQBInvoice result is not checked before logSync is called. Important Files Changed
|
…eCreated When createQBInvoice's onConflictDoNothing skips the insert (concurrent delivery won the race), logSync would still run and overwrite the winner's CREATED log row with this webhook's qbInvoiceId, leaving qb_invoice_sync.qb_invoice_id and qb_sync_logs.quickbooks_id pointing at different QBO objects. Same hazard applied to the paid-path payment creation. Mirror the guard already in findOrMapInvoiceFromQBO: capture the insert result and short-circuit on race loss. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
priosshrsth
left a comment
There was a problem hiding this comment.
src/app/api/quickbooks/invoice/invoice.service.ts is becoming too large, don't you think?
Approved. But maybe we should also think about simplifying this in the future.
Summary
qb_invoice_sync (portal_id, invoice_number) WHERE deleted_at IS NULL+onConflictDoNothingincreateQBInvoice— concurrent webhook deliveries can no longer insert duplicate mapping rows.findOrMapInvoiceFromQBOafter the QBO fetch; race-loss path skips redundantlogSync.getOneByCopilotIdAndEventTypeand the conditions branch ofupdateOrCreateQBSyncLognow filterisNull(deletedAt)so soft-deleted log rows can't be revived in-place.qb_sync_logsunique index was descoped — production has too many historical duplicates to add the constraint cleanly. Webhook-entry idempotency tracked in OUT-3655.Linear: https://linear.app/assemblycom/issue/OUT-3644
Test plan
qb_invoice_syncinvoice.createdwebhooks in parallel for the same invoice — verify only oneqb_invoice_syncrow, only oneqb_sync_logsCREATED rowqb_sync_logsrow, then trigger a sync that would normally update it — verify a new row is created instead of reviving the soft-deleted one🤖 Generated with Claude Code