OUT-4010: freeze batched-deposit intent per invoice - #270
Conversation
Reading bankDepositFeeFlag live in both handlePaymentSucceeded and handlePayoutReconciliationCompleted desynced when the flag was toggled between a payment and its payout (fee double-booked OFF->ON, or missed and payment stranded in Undeposited Funds ON->OFF). Freeze the decision on qb_invoice_sync.is_batched_deposit at row creation; both handlers now read the frozen value. - add is_batched_deposit column (+ migration) and freeze it on invoice create / paid-on-create - resolveDepositToAccountRef takes the frozen flag; the only live read stays at the freeze point - payment.succeeded: dedupe + resolve intent before the claim; batched intent defers to the payout with zero rows - payout: per-invoice intent via getSuccessfulPaidPaymentIds; all-batched books one deposit, all-non-batched skips before claiming, mixed rejected Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- invoice.created freezes is_batched_deposit; paid-on-create deposit routing - invoice.paid + payment.succeeded route off the frozen value, not the live flag - payout: all-batched books one deposit, all-non-batched skips before the claim, mixed rejected - shared infra: getUndepositedFundsAccountId/createDeposit mocks, seedPaidInvoiceForPayout, payout fixture + setup helper Overlaps the OUT-4006 payout suite (#268) on shared infra and the removed live-flag tests; reconcile on rebase once #268 lands on the base branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ut tests
- remove the debug GET webhook route (captureWebhookEventGET) that replayed a
hardcoded payout against real QBO on any authenticated GET
- correct the unresolved-invoice comment: the join is safe because
webhookInvoicePaid throws without an invoice-sync row, not via a soft-delete
- extract repeated payload fields into locals (paymentId/invoiceId/platformFee,
payoutId)
Reconcile the OUT-4006 payout suite with the frozen-intent behavior:
- port payout tests to seedPaidInvoiceForPayout so the new invoice-sync join
resolves; update resolvePayments for the {paymentId, isBatchedDeposit} shape
- delete now-obsolete live-flag tests (bankDepositFlagNoOp, flagOff)
- fold payoutReconciliationCompleted/* into payoutReconciliation/ on the shared
payout fixture; drop the duplicate happy-path and parallel infra
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryFreezes batched-deposit routing per invoice and uses that persisted intent throughout payment and payout processing.
Confidence Score: 4/5The PR appears safe to merge, with the previously reported payment-redelivery recovery delay still outstanding. The frozen invoice intent is propagated consistently through payment and payout routing, but payment webhook redeliveries still exit on any existing FAILED or PENDING log and must wait for asynchronous recovery. Files Needing Attention: src/app/api/quickbooks/webhook/webhook.service.ts Important Files Changed
Sequence DiagramsequenceDiagram
participant I as Invoice creation
participant DB as qb_invoice_sync
participant P as payment.succeeded
participant PO as payout reconciliation
I->>DB: Persist is_batched_deposit
P->>DB: Read frozen intent
alt Batched
P-->>PO: Defer absorbed fee
PO->>DB: Resolve each invoice intent
PO->>PO: Create one bank deposit
else Non-batched
P->>P: Book fee immediately
PO->>PO: Skip all-non-batched payout
end
Reviews (2): Last reviewed commit: "style(OUT-4010): trim verbose comments t..." | Re-trigger Greptile |
… P2) The peek matches any prior claim row (PENDING/SUCCESS/FAILED), mirroring claimWebhookEvent's status-blind onConflictDoNothing. A redelivery never reprocesses either way; FAILED recovery is the resync cron's job. Reword the comment + skip log to say "already claimed", not "processed". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@greptileai review PR again |
| // All-non-batched skipped pre-claim; a non-batched invoice here = mixed. | ||
| // get() is non-null — every id passed the unresolved check above. | ||
| const allBatched = copilotInvoiceIds.every( | ||
| (id) => paymentIdByInvoice.get(id)!.isBatchedDeposit, |
There was a problem hiding this comment.
@SandipBajracharya I think better to use ? rather than ! imo.
There was a problem hiding this comment.
Converted to optional chaining
d899747
into
feature/payout-reconciliation
OUT-4010 — Freeze batched-deposit intent per invoice
https://linear.app/assemblycom/issue/OUT-4010
Problem
bankDepositFeeFlagwas read live in bothhandlePaymentSucceededandhandlePayoutReconciliationCompleted. Toggling it between a payment and its payout desynced the two paths — the fee got booked twice (OFF→ON) or missed while the payment stranded in Undeposited Funds (ON→OFF).Approach
Freeze the decision per invoice. A new
qb_invoice_sync.is_batched_depositcolumn is set from the live flag at invoice-create time; both handlers read the frozen value.resolveDepositToAccountReftakes the frozen flag; the only live read stays at the freeze point (create / paid-on-create).payment.succeeded: dedupe + resolve the frozen intent before the claim; a batched invoice defers to the payout with zero rows.payout: resolves per-invoice intent viagetSuccessfulPaidPaymentIds(now joined toqb_invoice_sync); all-batched books one deposit, all-non-batched skips before claiming, mixed is rejected.Tests
freezeBatchedIntent,statusPaidDepositRouting,frozenIntentRouting,frozenIntentDefer, plus payoutallNonBatched/mixed.seedPaidInvoiceForPayout, updatedresolvePaymentsfor the new return shape, removed the obsolete live-flag tests, and folded the duplicate happy-path/parallel infra into one directory.Notes for reviewers
false); no backfill needed since the batched-deposit feature isn't in production yet, so no pre-existing invoice was created under batched behavior.🤖 Generated with Claude Code