Skip to content

OUT-3604: Stripe payout → batched QBO bank deposit#266

Merged
priosshrsth merged 7 commits into
feature/payout-reconciliationfrom
OUT-3604-new-re
Jul 23, 2026
Merged

OUT-3604: Stripe payout → batched QBO bank deposit#266
priosshrsth merged 7 commits into
feature/payout-reconciliationfrom
OUT-3604-new-re

Conversation

@SandipBajracharya

Copy link
Copy Markdown
Collaborator

What

On payout.reconciliation_completed, create one QBO Bank Deposit per Stripe payout whose net matches the bank-feed entry, so QBO auto-suggests a one-click reconciliation match. Replaces the never-shipped per-payment deposit path.

Assembly listens to Stripe's payout.reconciliation_completed, resolves each charge to a Copilot invoice, and forwards an enriched webhook. This app resolves each invoice → QBO Payment via qb_sync_logs, asserts sum(gross) − sum(fee) == payout.netAmount (in cents), and creates one Bank Deposit: N payment-linked lines + one negative aggregated-fee line.

Commits

  • apply migrations one-per-transaction (db:migrate) — drizzle batches all pending migrations into one transaction, which breaks enum-add-then-use; migratePerFile applies one file per transaction (wired into build.sh).
  • payout/settled enums, bank-deposit columns, idempotency indexPAYOUT/SETTLED, the bank_deposit_fee_flag + bank_account_ref columns (schema + migration together), the one-shot unique index + claimWebhookEvent predicate extended for payout/settled, getSuccessfulPaidPaymentIds.
  • type the QBO createDeposit responseQBDepositResponseSchema + assertNotQBFault/Zod-parse.
  • resolve the bank account refAccountTypeObj.Bank (reactivate archived, throw on deleted), bankAccountRef threaded through all token-construction sites.
  • create one batched bank deposit — the payout handler, batched deposit builder, payment.succeeded no-op in batched mode, invoice.paid → Undeposited Funds routing, resync skip for payouts.

Behavior (bankDepositFeeFlag on)

Event Action
invoice.paid Payment → Undeposited Funds
payment.succeeded no-op (no per-payment expense/deposit)
payout.reconciliation_completed one batched Bank Deposit (net of fees)

Aborts with a FAILED log (never a partial/unbalanced deposit) on: refund lines, negative aggregate fee, duplicate/unresolved invoices, or a sum mismatch.

Notes / follow-ups

  • Test files (integration suite + fixtures + the captureWebhookEventGET harness) are managed separately and are not in these commits.
  • Payout failures are terminal (shouldRetry: false) — no cron resync path yet; auto-recovery + the settings UI (bank-account picker, dedicated toggle) are tracked in OUT-4003.
  • Fee credits / refunds are deferred (guarded: aborts if present).

🤖 Generated with Claude Code

SandipBajracharya and others added 5 commits July 22, 2026 21:11
drizzle's migrate() runs all pending files in a single transaction, which
fails when one migration adds an enum value and a later one uses it in DDL
("unsafe use of new value"). Add migratePerFile (one commit per journal
entry) and a db:migrate runner, and switch build.sh to use it.

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

Add PAYOUT entity + SETTLED event, the bank_deposit_fee_flag and
bank_account_ref columns (schema + migration together), extend the one-shot
unique index and claimWebhookEvent predicate to cover payout/settled
(byte-equivalent), and add getSuccessfulPaidPaymentIds. Stale payout claims
flip terminal (no resync path). Retire the unused DEPOSITED enum value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add QBDepositResponseSchema and refactor _createDeposit to the standard
assertNotQBFault + Zod-parse pattern (returning a typed response), and
parse the Undeposited Funds lookup, removing untyped {} property access.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add AccountTypeObj.Bank so checkAndUpdateAccountStatus reactivates an
archived bank account; a deleted one throws (never auto-restore a deposit
destination). Thread bankAccountRef through every IntuitAPITokensType
construction site (extractTokens, getRefreshedQbTokenInfo, auth exchange +
emptyTokens, getPortalTokens, rename-accounts cmd) so the now-required
field is always populated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Handle payout.reconciliation_completed: resolve each invoice to its QBO
Payment, assert sum(gross)-sum(fee)==netAmount in cents, and create one
Bank Deposit (N payment lines + one fee line). Abort with a FAILED log on
refund lines, negative aggregate fee, duplicate/unresolved invoices, or a
mismatch. Reshape createBankDepositForPayment to the batched N-line form,
drop the never-shipped per-payment deposit path (payment.succeeded no-ops
in batched mode), and skip payouts in the resync dispatcher for now.

On invoice.paid, route the QBO Payment through Undeposited Funds when
batched mode is on (DepositToAccountRef) so the payout deposit can link
and sweep it.

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

linear-code Bot commented Jul 22, 2026

Copy link
Copy Markdown

OUT-3604

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
quickbooks-sync Error Error Jul 23, 2026 7:24am
quickbooks-sync (dev) Ready Ready Preview, Comment Jul 23, 2026 7:24am

Request Review

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds payout-level Stripe reconciliation through batched QuickBooks Bank Deposits. The main changes are:

  • Routes eligible invoice payments through Undeposited Funds.
  • Creates one net Bank Deposit for each reconciled Stripe payout.
  • Adds payout idempotency, validation, account resolution, and failure logging.
  • Adds the required settings, connection fields, enums, and database indexes.
  • Applies migrations one file at a time and serializes concurrent migration runners.

Confidence Score: 5/5

This looks safe to merge.

  • The integration setup now applies enum migrations in separate transactions.
  • The migration runner uses a single database connection and an advisory lock to serialize concurrent runs.
  • No blocking issue was confirmed in the latest fixes.

Important Files Changed

Filename Overview
src/db/migratePerFile.ts Adds per-file migration execution and an advisory lock for concurrent runners.
src/db/migrate.ts Adds the production entry point for the new migration runner.
test/integration/globalSetup.ts Uses the per-file runner when preparing integration databases.
src/app/api/quickbooks/webhook/webhook.service.ts Adds payout validation, claiming, batched-deposit creation, and sync-log updates.
src/app/api/quickbooks/payment/payment.service.ts Builds and submits payment-linked QuickBooks Bank Deposits with an aggregate fee line.

Reviews (2): Last reviewed commit: "fix(OUT-3604): serialize per-file migrat..." | Re-trigger Greptile

Comment thread src/app/api/quickbooks/webhook/webhook.service.ts
Comment thread src/app/api/quickbooks/webhook/webhook.service.ts
Comment thread src/app/api/quickbooks/webhook/webhook.service.ts
Comment thread src/db/migrations/20260721083213_add_payout_settled_enums.sql
Comment thread src/db/migratePerFile.ts
Greptile PR #266 review:
- migratePerFile now wraps its loop in a Postgres advisory lock so
  concurrent deploy runners can't race the same pending migration
  (e.g. one dropping an index before the other's DROP).
- globalSetup now applies migrations via migratePerFile instead of
  drizzle's batched migrate(), so a fresh integration DB can apply the
  enum-add-then-use sequence (was failing at setup).

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

Copy link
Copy Markdown
Collaborator Author

@greptileai re-review whole feature

@SandipBajracharya
SandipBajracharya changed the base branch from master to feature/payout-reconciliation July 23, 2026 05:16
@SandipBajracharya SandipBajracharya changed the title feat(OUT-3604): Stripe payout → batched QBO bank deposit OUT-3604: Stripe payout → batched QBO bank deposit Jul 23, 2026
Comment thread src/app/api/quickbooks/invoice/invoice.service.ts Outdated
Comment thread src/app/api/quickbooks/payment/payment.service.ts Outdated
Comment thread src/app/api/quickbooks/payment/payment.service.ts
Comment thread src/app/api/quickbooks/webhook/webhook.service.ts
Comment thread src/db/migratePerFile.ts
- invoice.service: simplify Undeposited-Funds ref to a ternary.
- payment.service: type paymentLines and build the fee line via
  push when feeTotal > 0 instead of a spread.
- webhook.service: accumulate gross/fee cents in a single reduce.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@priosshrsth
priosshrsth merged commit 20acfaa into feature/payout-reconciliation Jul 23, 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