Conversation
A connected Stripe account holds a separate balance per currency, and Stripe settles a destination charge in the currency the buyer was charged in, so an account whose own currency is `huf` can hold real `eur` money. `is_balance_payable` required `holding_currency == merchant_account.currency`, so those rows were never claimed by a payout run: they sat `unpaid` forever while the payouts page kept advertising them (gumroad-private#2693). A balance is now payable when its own connected account can actually pay that currency — it holds a positive Stripe balance in it and has a bank account there to receive it. Claimed balances are grouped per (account, currency) and paid as one Payment per group, each in its own currency, with the CURRENCY_MISMATCH guard still refusing a mixed or unpayable group. A payout whose currency is not the seller's Gumroad bank-account currency omits `destination` so Stripe pays the account's own bank account for that currency, and the payout webhook now matches Gumroad-created payouts by their Payment's currency instead of the account's.
|
| @@ -92,16 +96,20 @@ def execute! | |||
| return :flagged | |||
There was a problem hiding this comment.
Successful payouts remain undispatched
When one currency group fails preparation, this loop returns or raises before the dispatch loop at lines 99–121. The failed payment returns only its own balances to unpaid, while any successful sibling has already been marked processing but is never sent or enqueued. That payment then blocks later payouts until stale-payout recovery handles it.
Knowledge Base Used: Balances and payouts
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/models/scheduled_payout.rb
Line: 86-96
Comment:
**Successful payouts remain undispatched**
When one currency group fails preparation, this loop returns or raises before the dispatch loop at lines 99–121. The failed payment returns only its own balances to `unpaid`, while any successful sibling has already been marked `processing` but is never sent or enqueued. That payment then blocks later payouts until stale-payout recovery handles it.
**Knowledge Base Used:** [Balances and payouts](https://app.greptile.com/gumroad/-/custom-context/knowledge-base/antiwork/gumroad/-/docs/balances-and-payouts.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| payment_errors = if payout_processor.respond_to?(:payout_groups) | ||
| payout_processor.prepare_payment_and_set_amount( | ||
| payment, group_balances, merchant_account, payout_currency | ||
| ) | ||
| else | ||
| payout_processor.prepare_payment_and_set_amount(payment, group_balances) | ||
| end | ||
| raise save_error | ||
| # The payout processor can mark the payment as failed while preparing it (for example when | ||
| # no valid merchant account exists, or a balance's holding currency does not match the payout | ||
| # destination). A failed payment cannot transition to processing, so only mark it processing | ||
| # when preparation left it in a payable state — otherwise return the failed payment along | ||
| # with the preparation errors and let the caller handle it. | ||
| payment.mark_processing! unless payment.failed? |
There was a problem hiding this comment.
Partial groups become stranded
Each earlier group is saved and marked processing before the next group is prepared, but preparation is outside the save-error unwind. If a later Stripe group raises an authentication, connection, or preparation error, filter_map never returns the earlier payments to either caller. Those payments are not dispatched, and their balances remain processing until stale-payout recovery runs days later.
Knowledge Base Used: Balances and payouts
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/business/payments/payouts/payouts.rb
Line: 500-512
Comment:
**Partial groups become stranded**
Each earlier group is saved and marked `processing` before the next group is prepared, but preparation is outside the save-error unwind. If a later Stripe group raises an authentication, connection, or preparation error, `filter_map` never returns the earlier payments to either caller. Those payments are not dispatched, and their balances remain `processing` until stale-payout recovery runs days later.
**Knowledge Base Used:** [Balances and payouts](https://app.greptile.com/gumroad/-/custom-context/knowledge-base/antiwork/gumroad/-/docs/balances-and-payouts.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| # Public: Whether the connected account can actually pay funds out in `currency`. | ||
| # | ||
| # A Stripe account holds a separate balance per currency and Stripe settles a destination charge | ||
| # in the currency the buyer was charged in, so an account whose own currency is `huf` can still | ||
| # hold real `eur` money (gumroad-private#2693). Payouts need both sides to line up: Stripe rejects | ||
| # a payout in a currency the account holds no funds in, and one with no bank account to receive it. | ||
| # Requiring both keeps a balance we cannot pay `unpaid` (it rolls forward, as before) instead of | ||
| # creating a payout Stripe rejects. | ||
| # | ||
| # The account's own currency is answered without a Stripe call — that's the overwhelmingly common | ||
| # path — and anything else comes from `pay_out_currencies`. | ||
| def self.pay_out_currency?(merchant_account, currency) |
There was a problem hiding this comment.
Comments violate repository guidance
This twelve-line method comment repeats implementation details and includes issue-history narration. The repository requires comments to retain only non-obvious reasons, ordering constraints, and traps, while removing company history and line-by-line restatement and reconsidering comments longer than roughly three lines. This requirement must be satisfied before merging; the same pattern also appears around pay_out_currencies and payout_groups.
Context Used: CLAUDE.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/business/payments/payouts/processor/stripe/stripe_payout_processor.rb
Line: 120-131
Comment:
**Comments violate repository guidance**
This twelve-line method comment repeats implementation details and includes issue-history narration. The repository requires comments to retain only non-obvious reasons, ordering constraints, and traps, while removing company history and line-by-line restatement and reconsidering comments longer than roughly three lines. This requirement must be satisfied before merging; the same pattern also appears around `pay_out_currencies` and `payout_groups`.
**Context Used:** CLAUDE.md ([source](https://github.com/antiwork/gumroad/blob/main/CLAUDE.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Live evidence from the production account this fix targets, for the record: the EUR-held balance was released as a real Stripe payout on the connected account (EUR 4,602.07, status pending, arrival 2026-09-18) with no |
perform_payment decided whether to name the seller's bank account by comparing the payout currency with the BankAccount record's currency, but an ACH-type record reports usd whatever country it is in, so every cad payout on a Canadian managed account silently stopped passing destination. Compare the payout currency with the connected account's own currency instead: a foreign-currency group (eur held on a huf account) still omits it, which is the case this branch needs, and every account-currency payout keeps the destination it had before. Also fixes the rubocop offenses on this branch (Performance/SelectMap in pay_out_currencies, Layout/FirstArrayElementIndentation in the new test).
What / why
Pay Stripe-held balances in their holding currency, with one Payment per connected account and currency. Previously, balances in a currency different from the account default were excluded from every payout run.
Before / after
Foreign-currency groups become payable only when Stripe reports available funds and an external account in that currency. Preparation, destination selection, and webhook reconciliation use the Payment currency; unsupported groups remain unpaid. The existing payout preview is unchanged.
Existing live evidence, collected before this revision: a EUR 4,602.07 payout was accepted without a destination override or currency conversion, with the matching Payment and balances reconciled to the cent. Stripe reported pending with arrival September 18. Account and ledger identifiers are confined to the private tracker. No production payout is being run for this revision.
Test results
Revision in progress: addressing group-failure isolation, stale single-payment test expectations, and Ruby lint. Local regression tests, mutation proofs, exact-head panel review, and CI results will be recorded here before this draft is made ready.
AI assistance: GPT-6 Astra. Prompt: “Get 7740 green and ready”; repair the reviewed payout failure modes, verify tests and mutations, and hand this high-risk change to a human without automerge.