Stack guide
Rails and Stripe Connect: refunds, callbacks, background jobs
Rails makes refunds dangerously easy: one model callback fires and money moves. The same convenience scatters correctness — callbacks bypass review, ActiveJob retries execute blindly, and the service object pattern exists precisely because payment logic deserves explicit boundaries. Here is the Rails-native shape of doing it right.
Service object with both flags
Refunds belong in explicit services returning result objects, not model callbacks hiding side effects. Callback-triggered refunds are the audit blind spot: discover them via grep -rn "refunds.create" across app/models before assuming your inventory is complete.
class Refunds::Create
def initialize(order:, attempt:)
@order, @attempt = order, attempt
end
def call
Stripe::Refund.create({
charge: @order.stripe_charge_id,
reverse_transfer: true,
refund_application_fee: true,
}, { idempotency_key: "refund-#{@order.id}-#{@attempt}" })
end
endActiveJob retry semantics
retry_on without intent-scoped keys re-executes entire perform methods — including successful-partial scenarios. Keys derived from order-plus-intent collapse retries; live-state reads guard the 24-hour expiry tail. Sidekiq follows identical rules with different syntax.
Version pinning and schema drift
stripe-ruby updates occasionally shift object shapes detectors rely upon; pin versions, review changelogs deliberately, and let monitoring catch drift empirically — findings dropping to zero after an upgrade is itself a signal.
Common questions
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.
Callbacks or services for refunds?
Services. Callbacks hide money movement from review and testing; explicitness is the whole point of boundaries in payment code.