Skip to content

Commit ac96a00

Browse files
drusepthclaude
andcommitted
Fix Stripe subscription errors by migrating deprecated sources API to payment_methods API
- Migrate from deprecated sources API to modern payment_methods API in SubscriptionsController - Update subscription plan modifications to use Subscription.modify instead of direct assignment - Fix payment method creation/deletion to use PaymentMethod.create/detach instead of sources - Update view templates to use new payment_methods data structure - Migrate price.id usage from deprecated plan.id in data integrity tasks - Add comprehensive test suite with proper Stripe API stubs - Add missing test gems: rspec-rails, webmock, factory_bot_rails, shoulda-matchers This resolves Error 500 when users try to upgrade to Premium subscriptions. The original error was: NoMethodError - undefined method 'total_count' for nil:NilClass caused by stripe_customer.sources.total_count when sources API returned nil. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 11136b9 commit ac96a00

File tree

10 files changed

+366
-285
lines changed

10 files changed

+366
-285
lines changed

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ group :test do
140140
gem 'codeclimate-test-reporter', require: false # TODO: remove this
141141
gem 'database_cleaner'
142142
gem 'selenium-webdriver'
143+
gem 'rspec-rails', '~> 5.0'
144+
gem 'webmock', '~> 3.0'
145+
gem 'factory_bot_rails'
146+
gem 'shoulda-matchers', '~> 5.0'
143147
end
144148

145149
group :development do

Gemfile.lock

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,6 +1809,9 @@ GEM
18091809
coffee-script-source (1.12.2)
18101810
concurrent-ruby (1.3.5)
18111811
connection_pool (2.5.3)
1812+
crack (1.0.0)
1813+
bigdecimal
1814+
rexml
18121815
crass (1.0.6)
18131816
csv (3.3.4)
18141817
d3-rails (5.9.2)
@@ -1831,6 +1834,7 @@ GEM
18311834
railties (>= 4.1.0)
18321835
responders
18331836
warden (~> 1.2.3)
1837+
diff-lcs (1.6.2)
18341838
discordrb (3.5.0)
18351839
discordrb-webhooks (~> 3.5.0)
18361840
ffi (>= 1.9.24)
@@ -1849,6 +1853,11 @@ GEM
18491853
event_emitter (0.2.6)
18501854
eventmachine (1.2.7)
18511855
execjs (2.10.0)
1856+
factory_bot (6.5.4)
1857+
activesupport (>= 6.1.0)
1858+
factory_bot_rails (6.5.0)
1859+
factory_bot (~> 6.5)
1860+
railties (>= 6.1.0)
18521861
faraday (1.10.4)
18531862
faraday-em_http (~> 1.0)
18541863
faraday-em_synchrony (~> 1.0)
@@ -1889,6 +1898,7 @@ GEM
18891898
activerecord (>= 4.0.0)
18901899
globalid (1.2.1)
18911900
activesupport (>= 6.1)
1901+
hashdiff (1.2.0)
18921902
html-pipeline (2.14.3)
18931903
activesupport (>= 2)
18941904
nokogiri (>= 1.4)
@@ -2126,6 +2136,23 @@ GEM
21262136
rmagick (6.1.1)
21272137
observer (~> 0.1)
21282138
pkg-config (~> 1.4)
2139+
rspec-core (3.13.5)
2140+
rspec-support (~> 3.13.0)
2141+
rspec-expectations (3.13.5)
2142+
diff-lcs (>= 1.2.0, < 2.0)
2143+
rspec-support (~> 3.13.0)
2144+
rspec-mocks (3.13.5)
2145+
diff-lcs (>= 1.2.0, < 2.0)
2146+
rspec-support (~> 3.13.0)
2147+
rspec-rails (5.1.2)
2148+
actionpack (>= 5.2)
2149+
activesupport (>= 5.2)
2150+
railties (>= 5.2)
2151+
rspec-core (~> 3.10)
2152+
rspec-expectations (~> 3.10)
2153+
rspec-mocks (~> 3.10)
2154+
rspec-support (~> 3.10)
2155+
rspec-support (3.13.4)
21292156
ruby-progressbar (1.13.0)
21302157
ruby-vips (2.2.3)
21312158
ffi (~> 1.12)
@@ -2158,6 +2185,8 @@ GEM
21582185
sentry-ruby (5.23.0)
21592186
bigdecimal
21602187
concurrent-ruby (~> 1.0, >= 1.0.2)
2188+
shoulda-matchers (5.3.0)
2189+
activesupport (>= 5.2.0)
21612190
sidekiq (7.3.9)
21622191
base64
21632192
connection_pool (>= 2.3.0)
@@ -2218,6 +2247,10 @@ GEM
22182247
activemodel (>= 6.0.0)
22192248
bindex (>= 0.4.0)
22202249
railties (>= 6.0.0)
2250+
webmock (3.25.1)
2251+
addressable (>= 2.8.0)
2252+
crack (>= 0.3.2)
2253+
hashdiff (>= 0.4.0, < 2.0.0)
22212254
webpacker (5.4.4)
22222255
activesupport (>= 5.2)
22232256
rack-proxy (>= 0.6.1)
@@ -2266,6 +2299,7 @@ DEPENDENCIES
22662299
discordrb
22672300
dotenv-rails
22682301
engtagger!
2302+
factory_bot_rails
22692303
filesize
22702304
flamegraph
22712305
font-awesome-rails
@@ -2299,11 +2333,13 @@ DEPENDENCIES
22992333
redcarpet
23002334
redis (~> 5.1.0)
23012335
rmagick
2336+
rspec-rails (~> 5.0)
23022337
sass-rails
23032338
selenium-webdriver
23042339
sentry-rails
23052340
sentry-ruby
23062341
serendipitous!
2342+
shoulda-matchers (~> 5.0)
23072343
sidekiq (~> 7.3.9)
23082344
slack-notifier
23092345
spring
@@ -2318,6 +2354,7 @@ DEPENDENCIES
23182354
tribute
23192355
uglifier (>= 1.3.0)
23202356
web-console
2357+
webmock (~> 3.0)
23212358
webpacker
23222359
will_paginate (~> 4.0)
23232360
word_count_analyzer

app/controllers/subscriptions_controller.rb

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def new
2121

2222
def history
2323
@stripe_customer = Stripe::Customer.retrieve(current_user.stripe_customer_id)
24+
@stripe_payment_methods = @stripe_customer.list_payment_methods(type: 'card')
2425
@stripe_invoices = Stripe::Invoice.list({
2526
customer: current_user.stripe_customer_id
2627
})
@@ -146,12 +147,17 @@ def information_change
146147
stripe_subscription = stripe_customer.subscriptions.data[0]
147148
begin
148149
# Delete all existing payment methods to have our new one "replace" them
149-
stripe_customer.sources.each do |payment_method|
150-
payment_method.delete
150+
existing_payment_methods = stripe_customer.list_payment_methods(type: 'card')
151+
existing_payment_methods.data.each do |payment_method|
152+
payment_method.detach
151153
end
152154

153155
# Add the new card info
154-
stripe_customer.sources.create(source: valid_token)
156+
payment_method = Stripe::PaymentMethod.create({
157+
type: 'card',
158+
card: { token: valid_token }
159+
})
160+
payment_method.attach(customer: stripe_customer.id)
155161
rescue Stripe::CardError => e
156162
flash[:alert] = "We couldn't save your payment information because #{e.message.downcase} Please double check that your information is correct."
157163
return redirect_back fallback_location: payment_info_path
@@ -174,17 +180,20 @@ def delete_payment_method
174180
stripe_customer = Stripe::Customer.retrieve current_user.stripe_customer_id
175181
stripe_subscription = stripe_customer.subscriptions.data[0]
176182

177-
stripe_customer.sources.each do |payment_method|
178-
payment_method.delete
183+
payment_methods = stripe_customer.list_payment_methods(type: 'card')
184+
payment_methods.data.each do |payment_method|
185+
payment_method.detach
179186
end
180187

181188
notice = ['Your payment method has been successfully deleted.']
182189

183-
if stripe_subscription.plan.id != 'starter'
190+
# Check if user has a non-starter subscription using modern API
191+
current_price_id = stripe_subscription.items.data[0].price.id
192+
if current_price_id != 'starter'
184193
# Cancel the user's at the end of its effective period on Stripe's end, so they don't get rebilled
185194
stripe_subscription.delete(at_period_end: true)
186195

187-
active_billing_plan = BillingPlan.find_by(stripe_plan_id: stripe_subscription.plan.id)
196+
active_billing_plan = BillingPlan.find_by(stripe_plan_id: current_price_id)
188197
if active_billing_plan
189198
notice << "Your #{active_billing_plan.name} subscription will end on #{Time.at(stripe_subscription.current_period_end).strftime('%B %d')}."
190199
end
@@ -256,7 +265,8 @@ def move_user_to_plan_requested(plan_id)
256265
# If we're upgrading to premium, we want to check that a payment method
257266
# is already on file. If it is, we process the plan change. If it's not,
258267
# we redirect to the payment method page.
259-
if stripe_customer.sources.total_count > 0
268+
payment_methods = stripe_customer.list_payment_methods(type: 'card')
269+
if payment_methods.data.length > 0
260270
process_plan_change(current_user, plan_id)
261271
else
262272
return :payment_method_needed

app/controllers/users_controller.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,13 @@ def delete_my_account # :(
5959
stripe_customer = Stripe::Customer.retrieve(current_user.stripe_customer_id)
6060
stripe_subscription = stripe_customer.subscriptions.data[0]
6161
if stripe_subscription
62-
stripe_subscription.plan = 'starter'
63-
stripe_subscription.save
62+
# Update subscription to starter plan using modern API
63+
Stripe::Subscription.modify(stripe_subscription.id, {
64+
items: [{
65+
id: stripe_subscription.items.data[0].id,
66+
price: 'starter'
67+
}]
68+
})
6469
end
6570

6671
report_user_deletion_to_slack(current_user)

app/services/subscription_service.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,23 @@ def self.add_subscription(user, plan_id)
1212

1313
if stripe_subscription.nil?
1414
# Create a new subscription on Stripe
15-
Stripe::Subscription.create(customer: user.stripe_customer_id, plan: plan_id)
15+
Stripe::Subscription.create(customer: user.stripe_customer_id, price: plan_id)
1616
stripe_customer = Stripe::Customer.retrieve(user.stripe_customer_id)
1717
stripe_subscription = stripe_customer.subscriptions.data[0]
1818
else
19-
# Edit an existing Stripe subscription
20-
stripe_subscription.plan = plan_id
19+
# Edit an existing Stripe subscription by modifying its items
20+
Stripe::Subscription.modify(stripe_subscription.id, {
21+
items: [{
22+
id: stripe_subscription.items.data[0].id,
23+
price: plan_id
24+
}]
25+
})
26+
# Retrieve the updated subscription
27+
stripe_subscription = Stripe::Subscription.retrieve(stripe_subscription.id)
2128
end
2229

23-
# Save the change
30+
# The subscription is already saved by the modify call above
2431
begin
25-
stripe_subscription.save unless Rails.env.test?
2632

2733
# Add any bonus bandwidth granted by the plan
2834
user.update(

app/views/subscriptions/history.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</p>
1515
</div>
1616
<div class="col s12 m6">
17-
<% if @stripe_customer.sources.total_count == 0 %>
17+
<% if @stripe_payment_methods.data.length == 0 %>
1818
<p>
1919
We don't currently have a payment method on file for you. You'll be asked to add one whenever you
2020
upgrade, but you can add one at any time here.
@@ -26,15 +26,15 @@
2626
<% else %>
2727
<p>
2828
We have a payment method on file for you through Stripe
29-
(<%= @stripe_customer.sources.data[0].try(:brand) || 'a card' %> ending in <%= @stripe_customer.sources.data[0].last4 %>),
29+
(<%= @stripe_payment_methods.data[0].try(:card).try(:brand) || 'a card' %> ending in <%= @stripe_payment_methods.data[0].try(:card).try(:last4) %>),
3030
but since we don't store it, you cannot edit it. You can choose to add a new one (replacing the old),
3131
or delete the existing one.
3232
</p>
3333
<% end %>
3434
</div>
3535
</div>
3636
</div>
37-
<% if @stripe_customer.sources.total_count > 0 %>
37+
<% if @stripe_payment_methods.data.length > 0 %>
3838
<div class="card-action">
3939
<%= link_to "Add new payment method", payment_info_path %>
4040
<%= link_to "Delete existing payment method", delete_payment_method_path %>

lib/tasks/data_integrity.rake

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ namespace :data_integrity do
3131
should_downgrade_user = true
3232
else
3333
should_downgrade_user = stripe_subscription.items.data.none? do |subscription_item|
34-
subscription_item.plan.id == active_billing_plan.stripe_plan_id
34+
# Use price.id instead of deprecated plan.id
35+
subscription_item.price.id == active_billing_plan.stripe_plan_id
3536
end
3637
end
3738

0 commit comments

Comments
 (0)