OUT-3539: extract findOrMapInvoiceFromQBO helper for invoice sync resilience - #216
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…nc resilience When an invoice is manually created in QBO, subsequent webhook events (paid, void, delete) silently failed because no qb_invoice_sync mapping existed. The new findOrMapInvoiceFromQBO helper queries QBO by invoice number, resolves/creates the customer mapping, and inserts the qb_invoice_sync row so downstream handlers can proceed normally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, if invoice.created failed to sync to QBO, a FAILED CREATED row landed in qb_sync_logs but subsequent invoice.paid/voided/deleted webhooks for the same invoice returned silently, so the re-sync cron never saw them. - webhookInvoicePaid/Voided now throw APIError when the invoice is absent from both the sync table and QBO, so the webhook-level catch records a FAILED PAID/VOIDED row for re-sync. - handleInvoiceDeleted now queries QBO up front and branches on presence. When QBO doesn't have the invoice (never synced or manually deleted there), soft-delete prior sync logs, mark the local qb_invoice_sync row as DELETED, and record a pre-soft-deleted SUCCESS DELETED for audit. When QBO has it, ensure the local mapping and proceed with the existing delete flow, reusing the prefetched QBO invoice via a new optional qbInvoice param on findOrMapInvoiceFromQBO. - Add SyncLogService.softDeleteLogsByCopilotId helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
180f43c to
638068b
Compare
Greptile SummaryThis PR introduces
Confidence Score: 3/5Do not merge until the ×100 amount bug is fixed — payments created via the fallback path will be 100× too small in QBO. A P1 financial bug causes QBO payments to be created at 1/100th of the correct amount whenever the new findOrMapInvoiceFromQBO helper is the first to record the CREATED sync log. This directly corrupts payment records in QBO and must be fixed before merging. src/app/api/quickbooks/invoice/invoice.service.ts — specifically the amount and taxAmount fields in the logSync call inside findOrMapInvoiceFromQBO, and the DELETED-event log amount in handleInvoiceDeleted. Important Files Changed
Sequence DiagramsequenceDiagram
participant W as Webhook Handler
participant IS as InvoiceService
participant DB as Local DB
participant QBO as QuickBooks Online
W->>IS: webhookInvoicePaid / Voided / Deleted / Created
IS->>DB: getInvoiceByNumber()
alt mapping found
DB-->>IS: invoiceSync row
IS->>IS: proceed with normal flow
else no mapping
DB-->>IS: null
IS->>IS: findOrMapInvoiceFromQBO()
IS->>QBO: getInvoice(invoiceNumber)
alt invoice in QBO
QBO-->>IS: qbInvoice {Id, SyncToken}
IS->>DB: createQBInvoice (mapping row)
IS->>DB: logSync CREATED — amount=total.toFixed(2) missing x100
IS-->>W: invoiceSync = mappedInvoice
W->>W: paid path: amount divided by 100 → payment 100x too small
else invoice not in QBO
QBO-->>IS: null
IS-->>W: null → throw or return
end
end
Reviews (1): Last reviewed commit: "fix(OUT-3539): log subsequent invoice ev..." | Re-trigger Greptile |
…log writes - SyncService#processInvoiceVoided/Deleted: qb_sync_logs.amount is already in cents, so parseFloat(record.amount) / 100 produced a value 100x smaller than intended when rebuilding the invoice payload for retry. Drop the division. - WebhookService#pushFailedInvoiceToSyncLog: use updateOrCreateQBSyncLog instead of createQBSyncLog so repeated failures for the same (copilotId, eventType) update the existing row instead of inserting duplicates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| id: record.copilotId, | ||
| number: invNumber, | ||
| total: record.amount ? parseFloat(record.amount) / 100 : 0, // assuming amount is in cents | ||
| total: record.amount ? parseFloat(record.amount) : 0, |
There was a problem hiding this comment.
Won't this cause regression?
There was a problem hiding this comment.
This shouldn’t cause any regression. The change is only in the invoice void/delete function, and we’re not actually using the total amount for syncing anyway—it’s just there for logging.
Summary
findOrMapInvoiceFromQBO()helper that queries QBO by invoice number, resolves/creates the customer mapping, and inserts aqb_invoice_syncrow when foundcreated,paid,voided,deleted) as a fallback when no local mapping existscheckIfInvoiceExistsInQBO()to delegate to the new helper, so existing callers (syncMissedInvoices,backfillTimedOutInvoices) automatically gain mapping creationProblem
When an invoice is manually created in QBO, our app tries to create the same invoice and fails. Subsequent
invoice.paid,invoice.void, andinvoice.deleteevents also silently skip because noqb_invoice_syncmapping row exists in our database.Test plan
createdwebhook maps it and skips creationpaid/voided/deletedwebhooks with unmapped invoice → maps from QBO and proceedsfindOrMapInvoiceFromQBOtwice for the same invoice does not create duplicates🤖 Generated with Claude Code