Skip to content

OUT-3952: integration tests for invoice.voided webhook - #68

Merged
SandipBajracharya merged 6 commits into
mainfrom
OUT-3952
Jul 10, 2026
Merged

OUT-3952: integration tests for invoice.voided webhook#68
SandipBajracharya merged 6 commits into
mainfrom
OUT-3952

Conversation

@SandipBajracharya

Copy link
Copy Markdown
Collaborator

What

Adds integration tests for the invoice.voided webhook flow on the testcontainers harness, following the OUT-3951 (invoice.paid) pattern. Ref: OUT-3952.

Drives the real /api/webhook route end-to-end, mocking only the Copilot and Xero API clients. Reuses the shared setupWebhookTest, seed helpers, and constants.

Test cases (test/integration/webhook/invoiceVoided/)

File Scenario
happyPath.test.ts Voids the Xero invoice, writes a VOIDED success log, no failed_syncs
syncDisabled.test.ts Controller short-circuits (200 "Sync is disabled"); no void call
invoiceNotFound.test.ts getInvoiceById → undefined → 404, no log, one failed_syncs row
missingXeroInvoice.test.ts xeroInvoiceId null → create-then-void; VOIDED success log
xeroVoidFails.test.ts voidInvoice throws → 500; failed VOIDED log + failed_syncs row
noPriorCreatedLog.test.ts Regression test for the fix below

Also adds test/fixtures/voidedInvoice.webhook.ts and a voidInvoice default in test/helpers/mocks.ts (additive — existing suites untouched).

Production fix

A review surfaced a real defect: voidInvoice built its sync-log payloads by spreading prevSyncLog without setting the NOT NULL entityType. When no prior invoice.created log exists, the log INSERT crashed — the happy path returned 500 even though the Xero void succeeded, and on the failure path the crash preceded addFailedSyncRecord so no failed_syncs row was written (silent drop, no retry). syncPaidInvoiceToXero's catch was already immune because it hardcodes entityType.

Fix: hardcode entityType: SyncEntityType.INVOICE in voidInvoice's success log and failure payload, matching the existing paid pattern. Covered by noPriorCreatedLog.test.ts (verified failing before the fix).

Deferred / follow-ups

  • deleteInvoice has the identical entityType defect — deferred to the invoice.deleted ticket so the fix lands with its own test.
  • Pre-existing (OUT-3951): syncPaidInvoiceToXero's success-log has the same missing entityType, inside the DB transaction after markInvoicePaid — a crash there rolls back the payment record and risks a duplicate payment on retry. Worth its own ticket.
  • Pre-existing: createMissingXeroInvoice double-writes a CREATED log with inconsistent taxAmount units.

Verification

  • pnpm test → 27 files / 32 tests pass
  • pnpm typecheck and pnpm lint clean

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Jul 9, 2026

Copy link
Copy Markdown

OUT-3952

@supabase

supabase Bot commented Jul 9, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project pkdwtcdqcefmlgxmcwmc because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
xero-integration Ready Ready Preview, Comment Jul 9, 2026 10:35am

Request Review

SandipBajracharya and others added 6 commits July 9, 2026 16:19
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ce is missing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e then voids

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oice failure

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…or created log

voidInvoice built its sync-log payloads by spreading prevSyncLog without
setting the NOT-NULL entityType. When no invoice.created log exists the log
INSERT crashed: the happy path returned 500 even though the Xero void
succeeded, and on the failure path the crash preceded addFailedSyncRecord so
no failed_syncs row was written. Hardcode entityType like syncPaidInvoiceToXero
already does, and cover it with a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@SandipBajracharya SandipBajracharya changed the title test(OUT-3952): integration tests for invoice.voided webhook OUT-3952: integration tests for invoice.voided webhook Jul 9, 2026
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a complete integration-test suite for the invoice.voided webhook flow and, as a companion fix, hardcodes entityType: SyncEntityType.INVOICE in voidInvoice's success log and failure payload — preventing a NOT NULL crash when no prior invoice.created log exists. It also backfills the invoice.paid test suite, introduces seedSyncLog / seedSyncedPayment seed helpers, and ships paidInvoice.webhook.ts and voidedInvoice.webhook.ts fixtures following the existing harness pattern.

  • Production fix (voidInvoice): adds entityType: SyncEntityType.INVOICE to both the success log and the failedSyncLogPayload, matching the pattern already used in syncPaidInvoiceToXero's catch block; covered by the new noPriorCreatedLog regression test.
  • Integration test coverage: six invoiceVoided scenarios (happy path, sync-disabled, invoice-not-found, missing-Xero-invoice, void-fails, no-prior-log) and six invoicePaid scenarios mirror each other cleanly; test isolation, seeding, and mock overrides all follow established patterns.
  • Unresolved sibling defects: the identical entityType omission remains in syncPaidInvoiceToXero's success log (inside the DB transaction, so a crash there can yield a duplicate Xero payment on retry) and in both paths of deleteInvoice; both are acknowledged in the PR description but deferred.

Confidence Score: 4/5

The voidInvoice fix and its regression test are correct and safe to merge; the unresolved syncPaidInvoiceToXero success-log omission is a real defect that can produce duplicate Xero payments on retry, but it pre-dates this PR.

The core fix and all six voided-invoice test scenarios are well-constructed. The paid-invoice happy-path test avoids the unfixed bug by seeding a prior created log, but there is no equivalent noPriorCreatedLog test for the paid path. The syncPaidInvoiceToXero success log still lacks entityType — if the DB insert fails mid-transaction the Xero payment is already gone through and a retry would duplicate it. Addressing that in the same PR (one-liner, same pattern) would complete the fix class.

src/features/invoice-sync/lib/SyncedInvoices.service.ts — the syncPaidInvoiceToXero success-log path and both deleteInvoice paths still need the entityType hardcode.

Important Files Changed

Filename Overview
src/features/invoice-sync/lib/SyncedInvoices.service.ts Fixes missing entityType in voidInvoice success log and failure payload; syncPaidInvoiceToXero success log and deleteInvoice still lack the same fix
test/integration/webhook/invoiceVoided/happyPath.test.ts New happy-path integration test; exercises voidInvoice end-to-end, verifies tenantId/xeroInvoiceId args, synced_invoices state, VOIDED success log, and no failed_syncs
test/integration/webhook/invoiceVoided/noPriorCreatedLog.test.ts Regression test for the production fix; verifies voidInvoice succeeds with entityType defaulted to INVOICE when no prior created log exists
test/integration/webhook/invoicePaid/happyPath.test.ts New happy-path integration test for invoice.paid; seeds seedSyncLog() to provide entityType — which masks the unfixed entityType bug in syncPaidInvoiceToXero's success log
test/helpers/seed.ts Adds seedSyncedPayment and seedSyncLog helpers; baseSyncLog correctly sets entityType, invoiceNumber, and customer fields for use in invoice.paid and invoice.voided tests
test/helpers/mocks.ts Adds default mocks for getInvoiceById, markInvoicePaid, and voidInvoice; additive change, existing suites unaffected
test/integration/webhook/invoiceVoided/xeroVoidFails.test.ts Correctly tests voidInvoice failure path; verifies FAILED sync log includes entityType=INVOICE, customer metadata, and failed_syncs row
test/fixtures/voidedInvoice.webhook.ts New webhook payload fixture for invoice.voided; mirrors paidInvoice.webhook.ts pattern exactly

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Webhook Client
    participant W as /api/webhook
    participant S as SyncedInvoicesService
    participant XL as SyncLogsService
    participant X as XeroAPI
    participant DB as Database

    C->>W: POST invoice.voided
    W->>S: voidInvoice(copilotInvoiceId)
    S->>DB: getOrCreateInvoiceRecord()
    alt xeroInvoiceId missing
        S->>S: createMissingXeroInvoice()
        S->>X: createInvoice()
    end
    S->>X: getInvoiceById()
    alt invoice not found (404)
        S-->>W: APIError NOT_FOUND → failed_syncs row
    else invoice found
        S->>XL: getInvoiceCreatedSyncLog()
        S->>X: voidInvoice(tenantId, xeroInvoiceId)
        alt void succeeds
            S->>XL: "createSyncLog(VOIDED, SUCCESS, entityType=INVOICE)"
            S-->>W: 200 OK
        else void throws
            S->>XL: "createSyncLog(VOIDED, FAILED, entityType=INVOICE)"
            S-->>W: 500 → failed_syncs row
        end
    end
Loading
%%{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 C as Webhook Client
    participant W as /api/webhook
    participant S as SyncedInvoicesService
    participant XL as SyncLogsService
    participant X as XeroAPI
    participant DB as Database

    C->>W: POST invoice.voided
    W->>S: voidInvoice(copilotInvoiceId)
    S->>DB: getOrCreateInvoiceRecord()
    alt xeroInvoiceId missing
        S->>S: createMissingXeroInvoice()
        S->>X: createInvoice()
    end
    S->>X: getInvoiceById()
    alt invoice not found (404)
        S-->>W: APIError NOT_FOUND → failed_syncs row
    else invoice found
        S->>XL: getInvoiceCreatedSyncLog()
        S->>X: voidInvoice(tenantId, xeroInvoiceId)
        alt void succeeds
            S->>XL: "createSyncLog(VOIDED, SUCCESS, entityType=INVOICE)"
            S-->>W: 200 OK
        else void throws
            S->>XL: "createSyncLog(VOIDED, FAILED, entityType=INVOICE)"
            S-->>W: 500 → failed_syncs row
        end
    end
Loading

Comments Outside Diff (2)

  1. src/features/invoice-sync/lib/SyncedInvoices.service.ts, line 307-314 (link)

    P1 The syncPaidInvoiceToXero success log spreads prevSyncLog without hardcoding entityType, so when no prior invoice.created log exists (prevSyncLog is undefined), the spread gives {} and the NOT NULL entityType column is missing. The DB INSERT fails, the transaction rolls back (discarding both the synced_invoices status update and the synced_payments write), but the Xero payment was already created outside the transaction — so a failed_syncs retry row is produced for a payment that already went through. On retry the idempotency check (getPaymentForInvoiceId) finds no local record and calls markInvoicePaid again, writing a duplicate payment in Xero. The identical defect was just fixed in voidInvoice in this PR; the one-liner fix applies here too.

  2. src/features/invoice-sync/lib/SyncedInvoices.service.ts, line 406-422 (link)

    P2 deleteInvoice's success log spreads prevSyncLog without entityType, and the failure payload does too — the same defect fixed in voidInvoice this PR. If no prior CREATED log exists, both paths will crash on the NOT NULL entityType constraint. The PR description defers this to the invoice.deleted ticket; flagging here so the fix lands with awareness of the full pattern.

Reviews (1): Last reviewed commit: "fix(OUT-3952): default sync-log entityTy..." | Re-trigger Greptile

@priosshrsth priosshrsth left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@SandipBajracharya
SandipBajracharya merged commit 57f3a80 into main Jul 10, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants