Detail Bug Report
https://app.detail.dev/org_ecfbc7cf-6d21-4ce1-8c61-cda1d650ccf7/bugs/bug_2956ae47-2914-4a1f-8387-bfb97fd16325
Introduced in #12097 by @psincraian on Jun 3, 2026
Summary
- Context: The
cancel() method in the payout service reverses payouts and refunds the merchant's balance.
- Bug: When canceling a pending payout that has a Stripe transfer but no bank payout (
transfer_id is not None but no payout attempt), the payout fee is NOT returned even though Stripe has not charged it.
- Actual vs. expected: Actual: cancel reverses the Stripe transfer and marks payout canceled, but leaves the platform payout fee debited from the merchant balance. Expected: if no bank payout was attempted, the payout fee should be returned to the merchant.
- Impact: Merchants are charged payout fees for canceled payouts where no bank payout occurred. The reconciliation system does NOT automatically refund these fees.
Code with Bug
# server/polar/payout/service.py (cancel())
if (
payout.processor == PayoutAccountType.stripe
and payout_transaction.transfer_id is not None
):
# Transfer was made - reverse it in Stripe
stripe_reversal = await stripe_service.reverse_transfer(...)
# <-- BUG 🔴 Fees are not reversed here; transfer existence is treated as proof payout fee was charged
else:
# No transfer ran, so the per-payout fees were only reserved, never
# paid to Stripe. Return them so the merchant is made whole.
await platform_fee_transaction_service.create_payout_fees_reversal_balances(
session, payout=payout
)
Explanation
Stripe charges the payout fee when a bank payout is triggered (i.e., stripe.payout()), not when a transfer is created (stripe.transfer()). The cancel logic uses transfer_id is not None as the gate for fee reversal, so a payout that reached “transfer created” but never reached “bank payout attempted” will have its transfer reversed but will not have its payout fee returned.
This scenario is real and modeled in tests: a pending payout can have transfer_id="STRIPE_TRANSFER_ID" with attempts=[] (no bank payout attempt). It can also occur in production when pending payouts are canceled due to organization state changes.
The repository’s own comment indicates the intended behavior (“fees were only reserved, never paid to Stripe”), but the condition is wrong: it checks for a transfer instead of checking whether a bank payout attempt occurred.
Codebase Inconsistency
create_payout_fees_reversal_balances() reverses all payout-associated platform fees and is documented as being used when a payout “never ran its Stripe transfer,” but the bug scenario is “transfer ran, bank payout did not.” The cancel logic currently never calls this reversal path in that scenario, leaving the merchant-side fee debited with no automated refund via sync_stripe_fees().
Recommended Fix
At minimum, return payout fees when there was no bank payout attempt:
- After reversing the Stripe transfer, check
if not payout.attempts: and call platform_fee_transaction_service.create_payout_fees_reversal_balances(session, payout=payout).
History
This bug was introduced in commit c495bce. The change added fee-reversal logic for held payouts but conflated “Stripe transfer ran” with “payout fee was charged,” using transfer_id is not None instead of checking for a bank payout attempt (payout.attempts).
Detail Bug Report
https://app.detail.dev/org_ecfbc7cf-6d21-4ce1-8c61-cda1d650ccf7/bugs/bug_2956ae47-2914-4a1f-8387-bfb97fd16325
Introduced in #12097 by @psincraian on Jun 3, 2026
Summary
cancel()method in the payout service reverses payouts and refunds the merchant's balance.transfer_id is not Nonebut no payout attempt), the payout fee is NOT returned even though Stripe has not charged it.Code with Bug
Explanation
Stripe charges the payout fee when a bank payout is triggered (i.e.,
stripe.payout()), not when a transfer is created (stripe.transfer()). The cancel logic usestransfer_id is not Noneas the gate for fee reversal, so a payout that reached “transfer created” but never reached “bank payout attempted” will have its transfer reversed but will not have its payout fee returned.This scenario is real and modeled in tests: a pending payout can have
transfer_id="STRIPE_TRANSFER_ID"withattempts=[](no bank payout attempt). It can also occur in production when pending payouts are canceled due to organization state changes.The repository’s own comment indicates the intended behavior (“fees were only reserved, never paid to Stripe”), but the condition is wrong: it checks for a transfer instead of checking whether a bank payout attempt occurred.
Codebase Inconsistency
create_payout_fees_reversal_balances()reverses all payout-associated platform fees and is documented as being used when a payout “never ran its Stripe transfer,” but the bug scenario is “transfer ran, bank payout did not.” The cancel logic currently never calls this reversal path in that scenario, leaving the merchant-side fee debited with no automated refund viasync_stripe_fees().Recommended Fix
At minimum, return payout fees when there was no bank payout attempt:
if not payout.attempts:and callplatform_fee_transaction_service.create_payout_fees_reversal_balances(session, payout=payout).History
This bug was introduced in commit c495bce. The change added fee-reversal logic for held payouts but conflated “Stripe transfer ran” with “payout fee was charged,” using
transfer_id is not Noneinstead of checking for a bank payout attempt (payout.attempts).