Skip to content

OUT-3951: integration tests for the invoice.paid webhook - #67

Merged
SandipBajracharya merged 7 commits into
mainfrom
OUT-3951
Jul 9, 2026
Merged

OUT-3951: integration tests for the invoice.paid webhook#67
SandipBajracharya merged 7 commits into
mainfrom
OUT-3951

Conversation

@SandipBajracharya

Copy link
Copy Markdown
Collaborator

Summary

Adds integration tests for the invoice.paid webhook flow on the testcontainers harness (parent OUT-3949, follows the invoice.created suite from OUT-3950). Drives the real /api/webhook route end to end with mocked Copilot + Xero clients. Test-only — no production changes.

Linear: OUT-3951

Coverage (7 tests)

Test Scenario
happyPath (US & AU) Creates the Xero payment, records synced_payments, writes a paid success log; asserts the region sales code + invoice total flow into markInvoicePaid
idempotency Already success + existing payment → skips markInvoicePaid, no new payment/log
syncDisabled isSyncEnabled=false → controller 200, handler never runs
xeroMarkPaidFails markInvoicePaid throws → failed sync_log + failed_syncs + 500, invoice row untouched
missingXeroInvoice Row lacks xeroInvoiceId → creates the Xero invoice first, then pays
invoiceNotFound getInvoiceById → undefined → NOT_FOUND (404), failed_syncs recorded, no payment/log

Harness additions: getInvoiceById / markInvoicePaid mock defaults, paidInvoice fixture, seedSyncedPayment / seedSyncLog (all additive — existing suites unaffected).

Verification

  • invoice.paid suite: 7/7 green; full suite 26/26
  • pnpm lint and pnpm typecheck: clean

Follow-ups found during this work (not in this PR)

  • OUT-3960 — redundant/racing contact resolution in createMissingXeroInvoice (duplicate Xero contact risk on the recovery path).
  • Not yet ticketed: syncPaidInvoiceToXero builds its success sync log via { ...prevSyncLog } and relies on the spread for the NOT-NULL entityType. If no prior invoice.created success log exists, the transaction rolls back after markInvoicePaid already committed in Xero → a duplicate payment can be created on each retry. Worth a production ticket (fix: hardcode entityType/eventType in the success branch like the catch block does).

🤖 Generated with Claude Code

SandipBajracharya and others added 6 commits July 8, 2026 18:17
…and AU

Extend the webhook test harness for the invoice.paid flow: getInvoiceById
and markInvoicePaid mock defaults, a paidInvoice webhook fixture, and
seedSyncedPayment / seedSyncLog helpers. The happy path is parametrized over
US and AU and asserts the region sales code and invoice total flow into
markInvoicePaid, a synced_payments row is written, and a paid success log is
recorded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A success invoice with an existing synced_payments row short-circuits before
markInvoicePaid, adds no new payment row, and writes no new paid sync log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When isSyncEnabled is false the controller returns 200 without invoking the
handler, so no Xero call is made and no payment row is written.

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

When markInvoicePaid throws, the flow writes a failed paid sync log and a
failed_syncs row, returns 500, and leaves the invoice row untouched with no
payment recorded.

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

When the synced row has no xeroInvoiceId, the flow re-fetches the Copilot
invoice, creates it in Xero, then marks it paid: the row ends mapped and
success, with a payment row and a paid success log.

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

When getInvoiceById returns undefined the flow throws NOT_FOUND before any
payment work: no payment row, no paid sync log, but a failed_syncs row is
recorded and 404 is surfaced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 8, 2026

Copy link
Copy Markdown

OUT-3951

@vercel

vercel Bot commented Jul 8, 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 8, 2026 2:58pm

Request Review

@supabase

supabase Bot commented Jul 8, 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 ↗︎.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

Test-only PR that adds a full integration-test suite (7 tests) for the invoice.paid webhook flow, following the same testcontainers harness established by the invoice.created suite. Both previous review comments on xeroMarkPaidFails (missing seedSyncLog and missing metadata assertions on the failed log) have been addressed in this revision.

  • Happy path: describe.each correctly exercises US and AU regions independently; call-argument assertions on markInvoicePaid validate the sales-code and amount routing end to end.
  • Helpers: seedSyncedPayment, seedSyncLog, and the getInvoiceById/markInvoicePaid mock defaults are additive and do not affect existing suites.
  • Known gap (out of scope): The missingXeroInvoice recovery path writes two CREATED sync logs (one from syncInvoiceToXero, one from createMissingXeroInvoice itself); the test asserts only on the PAID log count, leaving that double-write undetected.

Confidence Score: 5/5

Test-only change with no production code modifications; safe to merge.

All changed files are test helpers and integration tests. The previous review comments on xeroMarkPaidFails are fully addressed. The one remaining gap (unbounded sync-log count in the missingXeroInvoice test) is a coverage nuance with no impact on the production path.

test/integration/webhook/invoicePaid/missingXeroInvoice.test.ts — the total sync_log count for the recovery path is not asserted.

Important Files Changed

Filename Overview
test/integration/webhook/invoicePaid/happyPath.test.ts Happy-path tests for US and AU regions; uses describe.each correctly, asserts Xero call args (tenantId, xeroInvoiceId, amount, salesCode), DB state (synced_payments, sync_logs), and absence of failedSyncs.
test/integration/webhook/invoicePaid/idempotency.test.ts Correctly seeds an existing syncedPayment and verifies markInvoicePaid is skipped and no new payment or paid sync_log is added.
test/integration/webhook/invoicePaid/missingXeroInvoice.test.ts Verifies the recovery path (null xeroInvoiceId → createMissingXeroInvoice → markInvoicePaid). Asserts call counts and final DB state, but only checks paidLog count without bounding the total sync_log count.
test/integration/webhook/invoicePaid/xeroMarkPaidFails.test.ts Now seeds a prior invoice.created log via seedSyncLog() and asserts invoiceNumber, customerName, customerEmail on the FAILED paid log; addresses both previous review comments.
test/integration/webhook/invoicePaid/invoiceNotFound.test.ts Correctly mocks getInvoiceById → undefined, asserts 404, no payment/log, and a failed_syncs row.
test/helpers/seed.ts Adds seedSyncedPayment and seedSyncLog helpers using typed base objects with constant overrides; additive, existing helpers unaffected.
test/helpers/mocks.ts Adds getInvoiceById and markInvoicePaid defaults to createMockXeroAPI; types guarded by MockMethodOverrides.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant W as Webhook Route
    participant WS as WebhookService
    participant SIS as SyncedInvoicesService
    participant SPS as SyncedPaymentsService
    participant SLS as SyncLogsService
    participant Xero as XeroAPI (mock)
    participant DB as Postgres

    W->>WS: handleEvent(invoice.paid)
    WS->>WS: getRegionConfig()
    WS->>SIS: syncPaidInvoiceToXero(copilotInvoiceId)

    alt xeroInvoiceId is null
        SIS->>SIS: createMissingXeroInvoice()
        SIS->>Xero: createInvoice()
        SIS->>DB: UPDATE syncedInvoices
        SIS->>DB: INSERT syncLogs (CREATED)
    end

    SIS->>Xero: getInvoiceById()
    SIS->>SLS: getInvoiceCreatedSyncLog()
    DB-->>SIS: prevSyncLog

    alt invoice already paid
        SIS-->>WS: return (idempotent skip)
    else proceed with payment
        SIS->>Xero: markInvoicePaid(tenantId, xeroInvoiceId, amount, salesCode)
        Xero-->>SIS: "payment { paymentID }"
        SIS->>DB: BEGIN TRANSACTION
        SIS->>DB: UPDATE syncedInvoices
        SPS->>DB: INSERT syncedPayments
        SLS->>DB: INSERT syncLogs (PAID SUCCESS)
        SIS->>DB: COMMIT
    end

    alt markInvoicePaid throws
        WS->>SLS: createSyncLog(PAID, FAILED)
        WS->>DB: INSERT failedSyncs
        WS-->>W: throw 500
    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 W as Webhook Route
    participant WS as WebhookService
    participant SIS as SyncedInvoicesService
    participant SPS as SyncedPaymentsService
    participant SLS as SyncLogsService
    participant Xero as XeroAPI (mock)
    participant DB as Postgres

    W->>WS: handleEvent(invoice.paid)
    WS->>WS: getRegionConfig()
    WS->>SIS: syncPaidInvoiceToXero(copilotInvoiceId)

    alt xeroInvoiceId is null
        SIS->>SIS: createMissingXeroInvoice()
        SIS->>Xero: createInvoice()
        SIS->>DB: UPDATE syncedInvoices
        SIS->>DB: INSERT syncLogs (CREATED)
    end

    SIS->>Xero: getInvoiceById()
    SIS->>SLS: getInvoiceCreatedSyncLog()
    DB-->>SIS: prevSyncLog

    alt invoice already paid
        SIS-->>WS: return (idempotent skip)
    else proceed with payment
        SIS->>Xero: markInvoicePaid(tenantId, xeroInvoiceId, amount, salesCode)
        Xero-->>SIS: "payment { paymentID }"
        SIS->>DB: BEGIN TRANSACTION
        SIS->>DB: UPDATE syncedInvoices
        SPS->>DB: INSERT syncedPayments
        SLS->>DB: INSERT syncLogs (PAID SUCCESS)
        SIS->>DB: COMMIT
    end

    alt markInvoicePaid throws
        WS->>SLS: createSyncLog(PAID, FAILED)
        WS->>DB: INSERT failedSyncs
        WS-->>W: throw 500
    end
Loading

Reviews (2): Last reviewed commit: "test(OUT-3951): assert failed paid log c..." | Re-trigger Greptile

Comment thread test/integration/webhook/invoicePaid/xeroMarkPaidFails.test.ts
Comment thread test/integration/webhook/invoicePaid/xeroMarkPaidFails.test.ts
Seed a prior invoice.created success log and assert the failed paid log
propagates invoiceNumber, customerName, and customerEmail via the
failedSyncLogPayload spread, so a regression dropping those fields is caught.
Matches the real scenario where a payment failure follows a successful create.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@SandipBajracharya

Copy link
Copy Markdown
Collaborator Author

@greptileai re-reivew the changes

@SandipBajracharya SandipBajracharya changed the title test(OUT-3951): integration tests for the invoice.paid webhook OUT-3951: integration tests for the invoice.paid webhook Jul 8, 2026

@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 df21f7b into main Jul 9, 2026
7 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