Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/controllers/admin_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ def perform_unsubscribe
@users = User.where(email: emails)
@users.each do |user|
if user.on_premium_plan?
# Cancel on Stripe too, or the user stays subscribed there and keeps
# getting billed after we've unsubscribed them on our end.
SubscriptionService.cancel_stripe_subscriptions!(user)
SubscriptionService.cancel_all_existing_subscriptions(user)
UnsubscribedMailer.unsubscribed(user).deliver_now! if Rails.env.production?
end
Expand Down
54 changes: 33 additions & 21 deletions app/controllers/subscriptions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def information
@selected_plan = BillingPlan.find_by(stripe_plan_id: params['plan'], available: true)
@stripe_customer = Stripe::Customer.retrieve(current_user.stripe_customer_id)
@stripe_payment_methods = @stripe_customer.list_payment_methods(type: 'card')
@stripe_subscriptions = SubscriptionService.billable_stripe_subscriptions(current_user.stripe_customer_id)
end

# Save a payment method
Expand Down Expand Up @@ -179,10 +180,6 @@ def information_change

def delete_payment_method
stripe_customer = Stripe::Customer.retrieve current_user.stripe_customer_id

# Use safe navigation to handle customers without subscriptions
subscriptions = stripe_customer.subscriptions&.data || []
stripe_subscription = subscriptions.first

payment_methods = stripe_customer.list_payment_methods(type: 'card')
payment_methods.data.each do |payment_method|
Expand All @@ -191,17 +188,18 @@ def delete_payment_method

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

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

active_billing_plan = BillingPlan.find_by(stripe_plan_id: current_price_id)
if active_billing_plan
notice << "Your #{active_billing_plan.name} subscription will end on #{Time.at(stripe_subscription.current_period_end).strftime('%B %d')}."
end
# With no card on file, make sure every paid subscription stops rebilling
# at the end of its current period.
SubscriptionService.billable_stripe_subscriptions(current_user.stripe_customer_id).each do |stripe_subscription|
current_price_id = SubscriptionService.subscription_price_ids(stripe_subscription).first
next if current_price_id.nil? || current_price_id == 'starter'

Stripe::Subscription.update(stripe_subscription.id, { cancel_at_period_end: true })

active_billing_plan = BillingPlan.find_by(stripe_plan_id: current_price_id)
period_end = SubscriptionService.subscription_period_end(stripe_subscription)
if active_billing_plan && period_end
notice << "Your #{active_billing_plan.name} subscription will end on #{Time.at(period_end).strftime('%B %d')}."
end
end

Expand Down Expand Up @@ -281,12 +279,26 @@ def move_user_to_plan_requested(plan_id)
end

def process_plan_change(user, new_plan_id)
# General flow we're going to take here:
# 1. Cancel all existing plans, reversing their benefits
SubscriptionService.cancel_all_existing_subscriptions(user)

# 2. Add a new plan, adding its benefits
SubscriptionService.add_subscription(user, new_plan_id)
# Serialize plan changes per user with a row lock, so repeated clicks on a
# slow page (or two racing requests) can't both act on stale subscription
# state and double-subscribe the user on Stripe.
# requires_new gives us our own savepoint, so a declined card rolls back the
# local downgrade below even if we're ever called inside another transaction.
user.transaction(requires_new: true) do
user.lock!

# General flow we're going to take here:
# 1. Cancel all existing plans, reversing their benefits
SubscriptionService.cancel_all_existing_subscriptions(user)

# 2. Add a new plan, adding its benefits
SubscriptionService.add_subscription(user, new_plan_id)
end
rescue Stripe::CardError
# with_lock runs in a transaction, so letting the error escape it rolled the
# local downgrade back: the user keeps the plan and bandwidth they had
# before their card was declined.
:failed_card
end

def set_sidenav_expansion
Expand Down
17 changes: 3 additions & 14 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,9 @@ def delete_my_account # :(
return
end

# Make sure the user is set to Starter on Stripe so we don't keep charging them
stripe_customer = Stripe::Customer.retrieve(current_user.stripe_customer_id)

# Use safe navigation to handle customers without subscriptions
subscriptions = stripe_customer.subscriptions&.data || []
stripe_subscription = subscriptions.first
if stripe_subscription
# Update subscription to starter plan using modern API
Stripe::Subscription.modify(stripe_subscription.id, {
items: [{
id: stripe_subscription.items.data[0].id,
price: 'starter'
}]
})
# Cancel every billable subscription on Stripe so we don't keep charging them
SubscriptionService.billable_stripe_subscriptions(current_user.stripe_customer_id).each do |stripe_subscription|
Stripe::Subscription.cancel(stripe_subscription.id)
end

report_user_deletion_to_slack(current_user)
Expand Down
26 changes: 5 additions & 21 deletions app/models/users/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -296,27 +296,11 @@ def initialize_stripe_customer
self.stripe_customer_id = customer_data.id
self.save

# If we're creating this Customer in Stripe for the first time, we should also associate them with the free tier
# Get the customer's available payment methods (if any)
payment_methods = Stripe::PaymentMethod.list({
customer: self.stripe_customer_id,
type: 'card'
})

default_payment_method = payment_methods.data.first&.id

# Create subscription with payment method if available
subscription_params = {
customer: self.stripe_customer_id,
items: [{ price: 'starter' }]
}

# Add default payment method if available (free tier may not have payment methods)
if default_payment_method
subscription_params[:default_payment_method] = default_payment_method
end

Stripe::Subscription.create(subscription_params)
# If we're creating this Customer in Stripe for the first time, we should also associate them with the free tier.
# This goes through SubscriptionService so it obeys the same
# one-subscription-per-customer invariant as every other code path, and
# can't leave a brand new user with a parallel subscription.
SubscriptionService.sync_stripe_subscriptions_to_plan(self, 'starter')
else
# In test environment, just set a dummy customer ID
self.stripe_customer_id = 'test_customer_id'
Expand Down
Loading