From c8f7062943a94a7eeb25ac7639644656287e0f5c Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 8 Aug 2026 20:33:15 -0400 Subject: [PATCH] Fix charge.succeeded webhook crash on removed Charge#subscription Stripe gem 19.x pins an API version that dropped Charge#subscription, so every charge.succeeded webhook raised NoMethodError before recording the payment. Subscription (membership) charges carry an invoice and are handled via the membership/invoice flow, so detect and skip them by invoice presence instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/webhooks/stripe_charge_succeeded_processor.rb | 5 ++++- spec/webhooks/stripe_charge_succeeded_processor_spec.rb | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/webhooks/stripe_charge_succeeded_processor.rb b/app/webhooks/stripe_charge_succeeded_processor.rb index 6114a5e83..5e589cff6 100644 --- a/app/webhooks/stripe_charge_succeeded_processor.rb +++ b/app/webhooks/stripe_charge_succeeded_processor.rb @@ -2,7 +2,10 @@ class StripeChargeSucceededProcessor def call(event) stripe_charge = event.data.object return unless stripe_charge.paid - return if stripe_charge.subscription.present? + # Subscription (membership) charges carry an invoice and are recorded via the + # membership/invoice flow — skip them here. Newer Stripe API versions dropped + # Charge#subscription, so detect them by the presence of an invoice. + return if stripe_charge.invoice.present? return if Payment.exists?(stripe_charge_id: stripe_charge.id) diff --git a/spec/webhooks/stripe_charge_succeeded_processor_spec.rb b/spec/webhooks/stripe_charge_succeeded_processor_spec.rb index 9c1d3afe3..806059e51 100644 --- a/spec/webhooks/stripe_charge_succeeded_processor_spec.rb +++ b/spec/webhooks/stripe_charge_succeeded_processor_spec.rb @@ -18,7 +18,7 @@ billing_details: nil, receipt_email: nil, customer: nil, - subscription: nil, + invoice: nil, to_hash: { "id" => stripe_charge_id, "amount" => 30_00 } ) end @@ -47,8 +47,8 @@ expect { processor.call(event) }.not_to change(ExternalProcessorPayment, :count) end - it "does nothing when the charge belongs to a subscription" do - allow(stripe_charge).to receive(:subscription).and_return("sub_membership") + it "does nothing when the charge belongs to a subscription (has an invoice)" do + allow(stripe_charge).to receive(:invoice).and_return("in_membership") expect { processor.call(event) }.not_to change(ExternalProcessorPayment, :count) end