Skip to content

OUT-3773: integration tests for payment.succeeded webhook event#256

Open
SandipBajracharya wants to merge 6 commits into
masterfrom
OUT-3773
Open

OUT-3773: integration tests for payment.succeeded webhook event#256
SandipBajracharya wants to merge 6 commits into
masterfrom
OUT-3773

Conversation

@SandipBajracharya
Copy link
Copy Markdown
Collaborator

Summary

  • 8 integration tests under test/integration/quickbooks/paymentSucceeded/ pinning every branch of WebhookService.handlePaymentSucceeded — happy path, absorbed-fee flag off, no platform-paid fee, idempotent re-delivery, Copilot invoice not found, local invoice-sync miss, QB createPurchase failure, revert-on-log-failure rollback.
  • Supporting infra: global @/utils/sleep and afterIfAvailable mocks in the integration setup, two new seed constants, expanded createMockIntuitAPI defaults (createPurchase / deletePurchase), id-echoing getAnAccount, real getInvoice default, new setupPaymentSucceededTest helper, new webhook fixture.

Production finding (separate follow-up worth filing)

webhook.service.ts:518 throws APIError(404) outside the try/catch on line 524. When Copilot returns no invoice, the handler responds 404 and leaves a PENDING claim row instead of writing FAILED. The copilotInvoiceNotFound test pins this current behavior; the fix is to move the check inside the try.

Test plan

  • npx vitest run --project integration test/integration/quickbooks/paymentSucceeded/ → 8 files / 9 tests pass
  • npx vitest run --project integration → 29 files / 40 tests pass (no regressions)
  • npx vitest run --project unit → 15 files / 238 tests pass
  • yarn lint:check clean (0 errors, pre-existing useEffect warnings only)
  • yarn prettier:check clean
  • CI Lint workflow green on push

Linear: https://linear.app/assemblycom/issue/OUT-3773

🤖 Generated with Claude Code

SandipBajracharya and others added 4 commits May 22, 2026 14:47
…ion tests

Webhook pre-claim sleeps (INVOICE_UPDATED, INVOICE_VOIDED, PAYMENT_SUCCEEDED)
would add ≥7s per test; we don't exercise race ordering at this layer.

afterIfAvailable is mocked because direct imports of SyncLogService
transitively load next/server's AsyncLocalStorage shim and corrupt
next-test-api-route-handler's request lifecycle. The shim calls the callback
directly, keeping next/server out of the module graph entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add TEST_COPILOT_PAYMENT_ID and TEST_QB_PURCHASE_ID seed constants.
- Add createPurchase and deletePurchase defaults to createMockIntuitAPI.
- Make getAnAccount echo the id back so asset/expense lookups return the
  ref that was passed in; name-only callers still get the income ref.
- Flip getInvoice default from undefined to a real invoice whose number
  matches TEST_INVOICE_NUMBER. No existing test calls getInvoice.
- New setupPaymentSucceededTest helper and paymentSucceeded fixture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eight tests pinning every branch of WebhookService.handlePaymentSucceeded:
happy path, absorbed-fee flag off, no platform-paid fee, idempotent
re-delivery, Copilot invoice not found, local invoice-sync miss, QB
createPurchase failure, and the revert-on-log-failure rollback.

copilotInvoiceNotFound pins a production quirk: the !invoice throw at
webhook.service.ts:518 sits outside the try/catch on line 524, so the
handler returns 404 with a leaked PENDING claim row instead of logging
FAILED. A follow-up ticket should move the check inside the try.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r wrap

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 22, 2026

OUT-3773

@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

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

Project Deployment Actions Updated (UTC)
quickbooks-sync Ready Ready Preview, Comment May 22, 2026 10:54am
quickbooks-sync (dev) Ready Ready Preview, Comment May 22, 2026 10:54am

Request Review

@SandipBajracharya SandipBajracharya changed the title test(OUT-3773): integration tests for payment.succeeded webhook event OUT-3773: integration tests for payment.succeeded webhook event May 22, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 22, 2026

Greptile Summary

Adds 8 integration tests covering every branch of WebhookService.handlePaymentSucceeded, plus supporting infrastructure (fixture, per-suite setup helper, seed constants, expanded mock defaults).

  • New tests in test/integration/quickbooks/paymentSucceeded/ cover: happy path, absorbed-fee flag off, no platform fee, idempotent re-delivery, Copilot invoice not found, local invoice-sync miss, QB createPurchase failure, and revert-on-log-failure rollback.
  • Infrastructure changes include a paymentSucceededPayload fixture, setupPaymentSucceededTest helper, two new seed constants (TEST_COPILOT_PAYMENT_ID, TEST_QB_PURCHASE_ID), id-echoing getAnAccount mock, real getInvoice default, and global sleep / afterIfAvailable mocks in test/integration/setup.ts.

Confidence Score: 5/5

Pure test-and-infrastructure addition with no production code changes; safe to merge.

Every changed file is in the test tree. The mock changes are backward-compatible and confirmed non-regressive by the full 40-test integration suite. The revertOnLogFailure spy is correctly scoped with try/finally to avoid leakage under isolate: false. The copilotInvoiceNotFound test deliberately pins a known production bug with clear documentation.

No files require special attention.

Important Files Changed

Filename Overview
test/integration/setup.ts Adds global sleep and afterIfAvailable mocks; the afterIfAvailable shim fires callbacks as fire-and-forget matching the real fallback path.
test/helpers/mocks.ts Changed getInvoice default to a real invoice object; replaced static getAnAccount with id-echoing implementation; added createPurchase and deletePurchase defaults.
test/helpers/paymentSucceededTestSetup.ts New per-suite setup helper using beforeEach/afterEach with clearAllMocks to preserve module-level mocks.
test/helpers/seed.ts Adds TEST_COPILOT_PAYMENT_ID and TEST_QB_PURCHASE_ID constants.
test/integration/quickbooks/paymentSucceeded/revertOnLogFailure.test.ts Correctly spies on SyncLogService.prototype.updateOrCreateQBSyncLog with mockImplementationOnce and wraps assertions in try/finally to guarantee spy restoration.
test/integration/quickbooks/paymentSucceeded/copilotInvoiceNotFound.test.ts Intentionally pins the current broken behavior (404 + PENDING log) with clear inline comments; deferred fix documented in PR description.
test/integration/quickbooks/paymentSucceeded/happyPath.test.ts Comprehensive assertions covering QB Purchase payload shape, account ref echo, and SUCCESS log fields including fee amount conversion.
test/integration/quickbooks/paymentSucceeded/idempotency.test.ts Pre-seeds a SUCCESS log to exercise the ON CONFLICT DO NOTHING idempotency path.
test/fixtures/paymentSucceeded.webhook.ts Clean fixture typed against the Zod input schema using seed constants.

Reviews (2): Last reviewed commit: "test(OUT-3773): use TEST_COPILOT_INVOICE..." | Re-trigger Greptile

Comment thread test/helpers/mocks.ts
Comment on lines +65 to +69
// payment.succeeded needs a real invoice object to proceed past the getInvoice guard (OUT-3773)
getInvoice: vi.fn().mockResolvedValue({
id: 'inv-cop-0001',
number: TEST_INVOICE_NUMBER,
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The id field in the getInvoice default is hardcoded as 'inv-cop-0001' while every other seed value in this file uses the named constant. TEST_COPILOT_INVOICE_ID already equals that string, so if the constant changes the mock silently drifts.

Suggested change
// payment.succeeded needs a real invoice object to proceed past the getInvoice guard (OUT-3773)
getInvoice: vi.fn().mockResolvedValue({
id: 'inv-cop-0001',
number: TEST_INVOICE_NUMBER,
}),
// payment.succeeded needs a real invoice object to proceed past the getInvoice guard (OUT-3773)
getInvoice: vi.fn().mockResolvedValue({
id: TEST_COPILOT_INVOICE_ID,
number: TEST_INVOICE_NUMBER,
}),

Comment thread test/helpers/mocks.ts
Comment on lines +87 to +95
getAnAccount: vi
.fn()
.mockImplementation(async (_name?: string, id?: string) => ({
Id: id ?? TEST_INCOME_ACCOUNT_REF,
Name: 'Sales of Product Income',
SyncToken: '0',
Active: true,
AccountType: 'Income',
})),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 AccountType is always 'Income' for every account — the id-echo mock returns the same type regardless of which account id is passed. checkAndUpdateAccountStatus doesn't validate AccountType today so tests pass, but a future call site or test inspecting this field for an asset or expense account will silently receive the wrong type. Consider accepting AccountType as a parameter or adding a comment noting the field is a stub.

SandipBajracharya and others added 2 commits May 22, 2026 16:36
Greptile flagged that AccountType is always 'Income' regardless of which
account id is passed. No current caller of getAnAccount inspects the field,
so behavior is unchanged — adding a comment so a future test author knows
to parameterize before relying on it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile flagged that the id field was hardcoded as 'inv-cop-0001' while
the rest of the file uses the named seed constant. The value matches today
but would silently drift if the constant is renamed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@SandipBajracharya
Copy link
Copy Markdown
Collaborator Author

@greptileai

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.

1 participant