OUT-3951: default sync-log entityType when paying without a prior created log - #69
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
…r created log syncPaidInvoiceToXero built its success-log payload by spreading prevSyncLog without setting the NOT-NULL entityType. When no invoice.created log exists the log INSERT fails inside the transaction, rolling back the synced_invoices status update and the synced_payments write — but markInvoicePaid already ran outside the transaction. The resulting failed_syncs retry re-pays because the local payment record was rolled back, so getPaymentForInvoiceId finds nothing and markInvoicePaid runs again, duplicating the payment in Xero. Hardcode entityType like the catch block already does, and cover it with a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
19f4a1e to
2a584c0
Compare
Greptile SummaryFixes a latent duplicate-payment defect in
Confidence Score: 4/5Safe to merge — the one-line fix is correct, well-tested, and resolves a real data-integrity defect without touching any other code paths. The voidInvoice and deleteInvoice methods in the same file carry the identical pattern (missing entityType in both their success-path createSyncLog calls and their catch-block failedSyncLogPayload), meaning those flows can still hit the same NOT NULL failure when no prior created log exists. This PR does not introduce that gap, but it also does not close it. src/features/invoice-sync/lib/SyncedInvoices.service.ts — the voidInvoice and deleteInvoice methods are unchanged and retain the same missing-entityType pattern in both success and error paths. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant W as Webhook Handler
participant S as SyncedInvoicesService
participant X as Xero API
participant DB as Database
W->>S: syncPaidInvoiceToXero(invoiceId)
S->>DB: getInvoiceCreatedSyncLog() - prevSyncLog may be undefined
S->>DB: getPaymentForInvoiceId() - idempotency check
S->>X: markInvoicePaid() - outside transaction
Note over X: Payment created in Xero (irreversible)
S->>DB: BEGIN TRANSACTION
DB->>DB: "update syncedInvoices status=success"
DB->>DB: insert syncedPayments
DB->>DB: insert syncLogs with entityType hardcoded to INVOICE
Note over DB: Fix ensures entityType is always set
S->>DB: COMMIT
S-->>W: payment result
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant W as Webhook Handler
participant S as SyncedInvoicesService
participant X as Xero API
participant DB as Database
W->>S: syncPaidInvoiceToXero(invoiceId)
S->>DB: getInvoiceCreatedSyncLog() - prevSyncLog may be undefined
S->>DB: getPaymentForInvoiceId() - idempotency check
S->>X: markInvoicePaid() - outside transaction
Note over X: Payment created in Xero (irreversible)
S->>DB: BEGIN TRANSACTION
DB->>DB: "update syncedInvoices status=success"
DB->>DB: insert syncedPayments
DB->>DB: insert syncLogs with entityType hardcoded to INVOICE
Note over DB: Fix ensures entityType is always set
S->>DB: COMMIT
S-->>W: payment result
|
What
Fixes a latent defect in
syncPaidInvoiceToXero(invoice.paid flow) that can duplicate a payment in Xero. Follow-up to the merged PR #67; ref OUT-3951.The bug
The success-path sync log was built by spreading
prevSyncLog(fromgetInvoiceCreatedSyncLog) without hardcoding theNOT NULLentityTypecolumn:When no prior
invoice.createdlog exists,prevSyncLogisundefined, soentityTypeis missing and the INSERT fails inside the transaction. The rollback discards thesynced_invoicesstatus update and thesynced_paymentswrite — butmarkInvoicePaidalready ran outside the transaction, so the payment is real in Xero. ThehandleEventcatch then records afailed_syncsretry row. On retry,getPaymentForInvoiceIdfinds no local record (rolled back), somarkInvoicePaidruns again → duplicate payment in Xero.The catch block already hardcodes
entityType; the success path did not.The fix
One line: add
entityType: SyncEntityType.INVOICEto the success-log payload, matching the catch block. It's a no-op whenprevSyncLogis present (getInvoiceCreatedSyncLogalways filtersentityType = INVOICE).Test
invoicePaid/noPriorCreatedLog.test.ts— seeds a synced invoice with no prior created log, posts the paid webhook, and asserts 200, a committedsynced_paymentsrow, aPAIDsuccess log withentityType, and emptyfailed_syncs. Verified failing (500) before the fix.Verification
pnpm test run test/integration/webhook/invoicePaid/→ 7 files / 8 tests passpnpm typecheckandpnpm lintclean🤖 Generated with Claude Code