Stack guide

Laravel and Stripe Connect: refunds beyond Cashier’s comfort zone

Laravel Cashier manages subscriptions elegantly; destination-charge refunds sit mostly outside its remit, leaving teams hand-rolling the path where Connect flags live. Hand-rolled is fine — Laravel’s queue and cache primitives make the guardrails straightforward. Descriptive note: Cashier is named here as context, not criticism; its scope simply ends where marketplace refund plumbing begins.

The manual refund action

A dedicated action class carrying both flags, executed through queued jobs for anything user-initiated:

StripeRefund::create([
  'charge' => $chargeId,
  'reverse_transfer' => true,
  'refund_application_fee' => true,
], ['idempotency_key' => "refund-{$orderId}-{$attempt}"]);

Cache-based dedupe before hitting Stripe

Laravel cache atomic locks deduplicate concurrent submissions cheaply: acquire lock per order-intent, check completion marker, proceed or return early. Complements Stripe-side idempotency — cache handles same-second collisions, keys handle cross-hour retries.

Horizon visibility for clawback jobs

Failed reversal jobs must surface loudly, not silently exhaust retries. Horizon dashboards plus failure notifications make money-moving failures first-class operational events — the alternative is discovering them in month-end reconciliation, weeks late.

Common questions

Does Cashier pass reverse_transfer through?

SDK-level pass-through exists; the guardrails around it remain yours regardless of package choice.

Does this require a FeeGuard integration?

No — the page stands alone as stack guidance. FeeGuard observes your event stream externally rather than embedding in it.