Skip to content

feat(OUT-4005): reconcile payouts via webhook + resync [3/4] - #275

Merged
SandipBajracharya merged 2 commits into
OUT-4005-split-2from
OUT-4005-split-3
Aug 3, 2026
Merged

feat(OUT-4005): reconcile payouts via webhook + resync [3/4]#275
SandipBajracharya merged 2 commits into
OUT-4005-split-2from
OUT-4005-split-3

Conversation

@SandipBajracharya

Copy link
Copy Markdown
Collaborator

Stack 3/4 — base: OUT-4005-split-2

Webhook delegates payout handling to PayoutService (saving the payout row before claiming). Resync claims the row FAILEDPENDING before work so overlapping runs can't double-deposit, and skips the absorbed-fee expense for batched invoices. Routes token-exchange resync through afterIfAvailable to keep next/server out of the service graph. Includes the payout integration + resync tests.

Part of the OUT-4005 payout-reconciliation stack:

  1. schema + migration
  2. reconciliation service + deposit lookup
  3. webhook + resync wiring ← this PR
  4. typecheck tooling + chore

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Jul 31, 2026

Copy link
Copy Markdown

OUT-4005

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
quickbooks-sync (dev) Ready Ready Preview Aug 3, 2026 8:43am

Request Review

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Greptile Summary

This PR wires payout reconciliation into webhook handling and failed-record resync.

  • Persists payout context before webhook claims so failed reconciliations can be retried.
  • Atomically claims failed payout logs before reconciliation and records successful or retryable outcomes.
  • Avoids creating absorbed-fee expenses for invoices included in batched deposits.
  • Defers reconnect-triggered resync through afterIfAvailable.
  • Adds integration coverage for transient failures, unresolved invoices, deleted bank accounts, idempotency, and payout resync.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code defect identified.

The payout webhook and resync paths preserve stored deposit identifiers, use database-backed claims to prevent concurrent reconciliation, and exercise the principal recovery and idempotency cases in integration tests.

Important Files Changed

Filename Overview
src/app/api/quickbooks/webhook/webhook.service.ts Delegates payout processing to PayoutService, persists retry context before claiming, and records retryability based on payout-specific errors.
src/app/api/quickbooks/sync/sync.service.ts Adds atomically claimed payout resync and suppresses duplicate absorbed-fee expenses for batched invoices.
src/app/api/quickbooks/auth/auth.service.ts Uses the environment-aware deferred-work helper for reconnect-triggered resync.
test/integration/quickbooks/payoutResync/resync.test.ts Covers transient recovery, delayed invoice context, existing-deposit idempotency, and terminal missing-context behavior.

Sequence Diagram

sequenceDiagram
    participant Stripe as Payout webhook
    participant Webhook as WebhookService
    participant DB as Sync tables
    participant Payout as PayoutService
    participant QBO as QuickBooks
    participant Resync as SyncService

    Stripe->>Webhook: payout settled
    Webhook->>DB: Upsert payout context
    Webhook->>DB: Claim webhook event
    alt Claim acquired
        Webhook->>Payout: Reconcile payout
        Payout->>QBO: Create or locate deposit
        Payout->>DB: Save deposit ID
        Webhook->>DB: Mark log SUCCESS
    else Reconciliation fails
        Webhook->>DB: Mark log FAILED and retryable
        Resync->>DB: Claim FAILED to PENDING
        Resync->>Payout: Reconcile with idempotency check
        Payout->>QBO: Locate or create deposit
        Resync->>DB: Mark log SUCCESS or FAILED
    end
Loading

Reviews (1): Last reviewed commit: "feat(OUT-4005): reconcile payouts into b..." | 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 except for minor nitpick

Comment thread test/helpers/seed.ts Outdated
Comment on lines +239 to +259
await db.insert(QBPayoutSync).values({
portalId: TEST_PORTAL_ID,
payoutId: opts.payoutId,
lineItems: opts.lineItems,
netAmount: opts.netAmount,
feeAmount: opts.feeCents,
arrivalDate: opts.arrivalDate,
qbDepositId: opts.qbDepositId ?? null,
})
await db.insert(QBSyncLog).values({
portalId: TEST_PORTAL_ID,
entityType: EntityType.PAYOUT,
eventType: EventType.SETTLED,
status: LogStatus.FAILED,
copilotId: opts.payoutId,
// Cents-as-string, matching what the webhook writes for a payout log.
amount: opts.netAmount.toFixed(2),
feeAmount: opts.feeCents.toFixed(2),
errorMessage: opts.errorMessage ?? 'QuickBooks timed out',
shouldRetry: true,
})

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.

Minor nitpick. But this can happen in parallel right?

…nd resync

Webhook delegates to PayoutService (saving the payout row before claiming).
Resync claims the row FAILED->PENDING before work so overlapping runs can't
double-deposit, and skips the absorbed-fee expense for batched invoices.
Routes token-exchange resync through afterIfAvailable to keep next/server out
of the service graph.

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

The qb_payout_sync and qb_sync_logs inserts are independent (no FK), so
run them together with Promise.all instead of sequentially.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@SandipBajracharya
SandipBajracharya merged commit f1bf9d1 into feature/payout-reconciliation Aug 3, 2026
4 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