We've moved discussions to Discord

How to handle failed payments and canceled subscriptions?

Ugurcan Kaya
What sort of  lifecycle does pay gem follow for failed payments? 

I see that in Jumpstart template there is a flash message for failed payments and when payment fails current_team.subscribed? returns false even the the subscription is not canceled in Stripe.  I guess it is not the best method for status check.

I also could not see a flash message for the case of canceled subscriptions. What do users see when they login but subscription is canceled in the default template? It seems like Teams::SubscriptionStatus only checks the status but does not do anything else.

What would be the best way to handle a failed payment and canceled subscription?   
Chris Oliver
I use ProfitWell's Retain for emailing customers about failed payments. Baremetrics has a recover feature, I've used stunning.co in the past and there's a handful other options too. My friend Steve built JustDunning.com as well.

I like not having to deal with those things myself, but you can listen to the invoice.payment_failed event from Stripe and do things yourself if you would like.
Ugurcan Kaya
Chris Oliver   isn't that something that should be part of the Jumpstart template though? Emails may not be super important at this point but it would be useful to have some flash warnings and so on.

We can build something simple that will redirect the user to the payment screen and block access if an invoice failed and subscription canceled. It will just require a couple of additional helpers.

 

Chris Oliver
I dunno, I mean I've never built it into my apps. Stripe also has dunning email functionality built-in that you can enable. I don't think that Braintree has quite the same level of detail.

Most services are lenient if a payment failed, since your card might be stolen and they don't actually cancel the subscription until several attempts to recover fail.

It might be something we should add. I'd need to figure out exactly what should be added before I can tackle it. 
Chris Oliver
Stripe also has the "incomplete" status on a subscription that we're already handling in Jumpstart Pro.

https://gitlab.com/gorails/jumpstart-pro/-/blob/master/app/views/shared/_flash.html.erb#L15
Ugurcan Kaya
Chris Oliver   In Stripe settings, there is a setting for when subscription should be canceled. I guess it is set to three failed payments by default. After that, the subscription gets canceled. Until then it is still active.

There is  a status value in the subscription object: https://stripe.com/docs/api/subscriptions/object

We need to have a status field in the jumpstart database which needs to sycn to the Stripe value and it will get updated by webhooks. 

After that, the only thing we need to do is to expand the current_team.subscribed? method. It currently returns false after the first failed payment.  It should check for the actual subscription status instead of charges.

Not sure how Braintree works but perhaps Pay gem might set the status if they don't provide a status value.

Then we just need to add another helper method to the application_controller that will handle the redirect and triggers flash messages. 



Chris Oliver
The status field has been in the database since the SCA update. 
Ugurcan Kaya
Chris Oliver   ok great, I guess we just need some helpers and filters then. I can push some code if you are interested adding this.
Chris Oliver
Definitely, I've thought about including some dunning functionality in Pay so people don't have to pay for a separate service.
Ugurcan Kaya
Chris Oliver  Is current_account.subscribed? method coming from Pay gem or SubscriptionStatus module?

What is that concern is for?
Chris Oliver
Comes from Pay. The concern is helpful methods for your controllers and views.
Ugurcan Kaya
when I call current_account.subscribed? it calls for this helper method in the concern right? which includes subscribed method that comes from Pay and user_signed_in check.

So it sort of overrides and expands that method?
Chris Oliver
No, current_account is an Account instance. It's calling the method on Account, which comes from Pay's include Pay::Billable. 
Ugurcan Kaya
what is concerns > accounts > subscription_status.rb file is for then? 

current_account.subscribed? would work without it, wouldn't it? It is already available due to Pay gem
Chris Oliver
It's helpers for any views that need to check if the current account is subscribed. You don't have to have all those conditionals in your views and remember them each time by using the helper.
Ugurcan Kaya
what I am trying to confirm is whether that causes any conflicts anywhere.

If this helper did not exist, current_account.subscribed? would return the method on Account that comes from Pay.

But now current_account.subscribed? also checks for

user_signed_in? && current_account && current_account.subscribed?

doesnt it? how and where does rails make the distinction between the two?

 
Chris Oliver
I'm not sure where the confusion is. Let me try explaining a different way.

SubscriptionStatus lets you write if subscribed? in the view. That's the equivalent of if user_signed_in && current_account && current_account.subscribed?.

current_account.subscribed? is the same as calling Account.last.subscribed? which asks Pay if the user is subscribed.

So the subscribed? helper just makes sure you're logged in before asking Pay if the account is subscribed.

No conflicts, just wrapping the method so it doesn't fail.
Ugurcan Kaya
Ok I see. I thought the view helper was the same since it was under concerns > accounts folder my bad. So it is if subscribed? vs current_account.subscribed?

👍👍👍
Chris Oliver
If it's in controllers/concerns, then it's for using in controllers/views. models/concerns would be something added to models, which in that case you're right in that it would conflict.
Ugurcan Kaya
Chris Oliver   When you delete an account, does the subscription automatically get canceled? Is that something pay gem handles?
Chris Oliver
Don't think so just yet. I was thinking about this the other day. I'm not sure if we want Pay to handle it or Jumpstart. I'm leaning towards Jumpstart, but there could be a good reason to do it before_destroy or something on Pay::Subscription.
Ugurcan Kaya
Chris Oliver  Ideally it should be part of Pay gem. But if thats too much work, we can add a single line of code to account#destroy as a quick solution
Ugurcan Kaya
Chris Oliver   Just noticed another issue.

If subscription gets canceled (after three payments), when you reactivate, it creates the subscription with trial time again.
Chris Oliver
Ah yeah, that's a bug. Want to add it to gitlab?
Ugurcan Kaya
done 👍
Ugurcan Kaya
Chris Oliver I also found this today: https://support.stripe.com/questions/2020-visa-trial-subscription-requirement-changes-guide

Stripe has built-in email settings to comply with this. Does Braintree have anything similar?

Also can jumpstart handle 3D secure? Is the recent upgrade you released was for that? I could not be sure if I should turn on 3D secure in Stripe.
Chris Oliver
I'm not sure about Braintree. 

Everything in Pay v2+ is SCA compatible with is 3D secure v2.
Ugurcan Kaya
I am also getting this error

NoMethodError in SubscriptionsController#update

undefined method `jumpstart_swap' for #<Pay::Subscription:0x00007f344226a228

What piece of code am I missing?
Ugurcan Kaya
never mind, found it :) 
Ugurcan Kaya
Chris Oliver  

I am also getting Validation failed: Owner must exist error in Stripe webhooks

what is that owner value is for? How can I fix existing records for that?


Chris Oliver
Share some details like the stacktrace and I'll take a look.
Ugurcan Kaya
ActiveRecord::RecordInvalid
Validation failed: Owner must exist
/GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/validations.rb:81→ raise_validation_error /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/validations.rb:53→ save! /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/transactions.rb:319→ block in save! /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/transactions.rb:375→ block in with_transaction_returning_status /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/connection_adapters/abstract/database_statements.rb:279→ transaction /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/transactions.rb:212→ transaction /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/transactions.rb:366→ with_transaction_returning_status /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/transactions.rb:319→ save! /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/suppressor.rb:48→ save! /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/persistence.rb:635→ block in update! /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/transactions.rb:375→ block in with_transaction_returning_status /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/connection_adapters/abstract/database_statements.rb:281→ block in transaction /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/connection_adapters/abstract/transaction.rb:280→ block in within_new_transaction /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/connection_adapters/abstract/transaction.rb:278→ synchronize /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/connection_adapters/abstract/transaction.rb:278→ within_new_transaction /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/connection_adapters/abstract/database_statements.rb:281→ transaction /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/transactions.rb:212→ transaction /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/transactions.rb:366→ with_transaction_returning_status /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/persistence.rb:633→ update! /GEM_ROOT/gems/pay-2.1.1/lib/pay/stripe/webhooks/subscription_deleted.rb:14→ call /GEM_ROOT/gems/stripe_event-2.3.0/lib/stripe_event.rb:61→ call /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications/fanout.rb:189→ finish /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications/fanout.rb:62→ block in finish /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications/fanout.rb:62→ each /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications/fanout.rb:62→ finish /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications/instrumenter.rb:45→ finish_with_state /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications/instrumenter.rb:30→ instrument /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications.rb:180→ instrument /GEM_ROOT/gems/stripe_event-2.3.0/lib/stripe_event.rb:18→ instrument /GEM_ROOT/gems/stripe_event-2.3.0/app/controllers/stripe_event/webhook_controller.rb:8→ event /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_controller/metal/basic_implicit_render.rb:6→ send_action /GEM_ROOT/gems/actionpack-6.0.2.2/lib/abstract_controller/base.rb:196→ process_action /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_controller/metal/rendering.rb:30→ process_action /GEM_ROOT/gems/actionpack-6.0.2.2/lib/abstract_controller/callbacks.rb:42→ block in process_action /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/callbacks.rb:135→ run_callbacks /GEM_ROOT/gems/actionpack-6.0.2.2/lib/abstract_controller/callbacks.rb:41→ process_action /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_controller/metal/rescue.rb:22→ process_action /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_controller/metal/instrumentation.rb:33→ block in process_action /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications.rb:180→ block in instrument /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications/instrumenter.rb:24→ instrument /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/notifications.rb:180→ instrument /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_controller/metal/instrumentation.rb:32→ process_action /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_controller/metal/params_wrapper.rb:245→ process_action /GEM_ROOT/gems/activerecord-6.0.2.2/lib/active_record/railties/controller_runtime.rb:27→ process_action /GEM_ROOT/gems/actionpack-6.0.2.2/lib/abstract_controller/base.rb:136→ process /GEM_ROOT/gems/actionview-6.0.2.2/lib/action_view/rendering.rb:39→ process /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_controller/metal.rb:191→ dispatch /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_controller/metal.rb:252→ dispatch /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/routing/route_set.rb:51→ dispatch /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/routing/route_set.rb:33→ serve /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/journey/router.rb:49→ block in serve /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/journey/router.rb:32→ each /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/journey/router.rb:32→ serve /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/routing/route_set.rb:837→ call /GEM_ROOT/gems/railties-6.0.2.2/lib/rails/engine.rb:526→ call /GEM_ROOT/gems/railties-6.0.2.2/lib/rails/railtie.rb:190→ public_send /GEM_ROOT/gems/railties-6.0.2.2/lib/rails/railtie.rb:190→ method_missing /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/routing/mapper.rb:19→ block in <class:Constraints> /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/routing/mapper.rb:48→ serve /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/journey/router.rb:49→ block in serve /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/journey/router.rb:32→ each /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/journey/router.rb:32→ serve /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/routing/route_set.rb:837→ call /GEM_ROOT/gems/omniauth-1.9.1/lib/omniauth/strategy.rb:192→ call! /GEM_ROOT/gems/omniauth-1.9.1/lib/omniauth/strategy.rb:169→ call /GEM_ROOT/gems/omniauth-1.9.1/lib/omniauth/strategy.rb:192→ call! /GEM_ROOT/gems/omniauth-1.9.1/lib/omniauth/strategy.rb:169→ call /GEM_ROOT/gems/omniauth-1.9.1/lib/omniauth/builder.rb:45→ call /GEM_ROOT/gems/warden-1.2.8/lib/warden/manager.rb:36→ block in call /GEM_ROOT/gems/warden-1.2.8/lib/warden/manager.rb:34→ catch /GEM_ROOT/gems/warden-1.2.8/lib/warden/manager.rb:34→ call /GEM_ROOT/gems/rack-2.2.2/lib/rack/tempfile_reaper.rb:15→ call /GEM_ROOT/gems/rack-2.2.2/lib/rack/etag.rb:27→ call /GEM_ROOT/gems/rack-2.2.2/lib/rack/conditional_get.rb:40→ call /GEM_ROOT/gems/rack-2.2.2/lib/rack/head.rb:12→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/http/content_security_policy.rb:18→ call /GEM_ROOT/gems/rack-2.2.2/lib/rack/session/abstract/id.rb:266→ context /GEM_ROOT/gems/rack-2.2.2/lib/rack/session/abstract/id.rb:260→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/cookies.rb:648→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/callbacks.rb:27→ block in call /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/callbacks.rb:101→ run_callbacks /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/callbacks.rb:26→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/actionable_exceptions.rb:17→ call /GEM_ROOT/gems/airbrake-10.0.1/lib/airbrake/rack/middleware.rb:34→ call! /GEM_ROOT/gems/airbrake-10.0.1/lib/airbrake/rack/middleware.rb:23→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/debug_exceptions.rb:32→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/show_exceptions.rb:33→ call /GEM_ROOT/gems/turbolinks_render-0.9.17/lib/turbolinks_render/middleware.rb:77→ call /GEM_ROOT/gems/railties-6.0.2.2/lib/rails/rack/logger.rb:38→ call_app /GEM_ROOT/gems/railties-6.0.2.2/lib/rails/rack/logger.rb:26→ block in call /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/tagged_logging.rb:80→ block in tagged /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/tagged_logging.rb:28→ tagged /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/tagged_logging.rb:80→ tagged /GEM_ROOT/gems/railties-6.0.2.2/lib/rails/rack/logger.rb:26→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/remote_ip.rb:81→ call /GEM_ROOT/gems/request_store-1.5.0/lib/request_store/middleware.rb:19→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/request_id.rb:27→ call /GEM_ROOT/gems/rack-2.2.2/lib/rack/method_override.rb:24→ call /GEM_ROOT/gems/shopify_app-13.1.0/lib/shopify_app/middleware/same_site_cookie_middleware.rb:11→ call /GEM_ROOT/gems/rack-2.2.2/lib/rack/runtime.rb:22→ call /GEM_ROOT/gems/activesupport-6.0.2.2/lib/active_support/cache/strategy/local_cache_middleware.rb:29→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/executor.rb:14→ call /GEM_ROOT/gems/rack-2.2.2/lib/rack/sendfile.rb:110→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/ssl.rb:74→ call /GEM_ROOT/gems/actionpack-6.0.2.2/lib/action_dispatch/middleware/host_authorization.rb:77→ call /GEM_ROOT/gems/rack-cors-1.1.1/lib/rack/cors.rb:100→ call /GEM_ROOT/gems/railties-6.0.2.2/lib/rails/engine.rb:526→ call /GEM_ROOT/gems/puma-4.3.3/lib/puma/configuration.rb:228→ call /GEM_ROOT/gems/puma-4.3.3/lib/puma/server.rb:682→ handle_request /GEM_ROOT/gems/puma-4.3.3/lib/puma/server.rb:472→ process_client /GEM_ROOT/gems/puma-4.3.3/lib/puma/server.rb:328→ block in run /GEM_ROOT/gems/puma-4.3.3/lib/puma/thread_pool.rb:134→ block in spawn_thread
Ugurcan Kaya
happens when subscription canceled event received
Ugurcan Kaya
stripe api version 2019-02-19

Chris Oliver
Since this is the line of code that's failing to update the Subscription record, it sounds like the record in your database doesn't have an Owner associated with it currently.

https://github.com/pay-rails/pay/blob/master/lib/pay/stripe/webhooks/subscription_deleted.rb#L14

Did you backfill the existing subscriptions for the Pay 2.1 update? It should be setting all the owner_type columns to "Account".
Ugurcan Kaya
I guess I did not or something prevented it in production. Will do

By the when an account is deleted, subscription is not deleted right? I see canceled subscriptions with no owner in database.

thanks so much for being so responsive.
Chris Oliver
If you deleted an account, but not their subscriptions, then the webhook would come in and try to update an invalid subscription. Maybe that's what's happening here?

You probably want to delete the subscriptions dependent destroy on the account deletion.
Notifications
You’re not receiving notifications from this thread.