Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.0] Payment Intents #667

Merged
merged 8 commits into from
Jul 5, 2019
Merged

[10.0] Payment Intents #667

merged 8 commits into from
Jul 5, 2019

Conversation

driesvints
Copy link
Member

@driesvints driesvints commented May 10, 2019

This PR aimes at updating the subscription based billing and single charges with the payment intents API to verify payments using 3D secure, amongst else.

Todo

  • incomplete state for subscriptions
  • Implement a generic way to verify payment intents
  • Update plan swapping
  • Recurring charges for subscriptions
  • Update single charges
  • Update invoicing
  • Design payment view
  • Tests
  • Final run-through/cleanup/comments

Changes

Payment Intents

These changes bring support for the new payment intents api to charges and subscriptions. As there are quite some breaking changes here let's go over the most prominent ones below:

Any payment action will now throw an exception when a payment either fails or when the payment requires a secondary action in order to be completed. This goes for single charges, invoicing customers directly, subscribing to a new plan or swapping plans. Developers can catch these exceptions and decide for themselves how to handle these by either letting Stripe handle everything for them (to be set up in the Stripe dashboard) or use the custom built-in solution which will be added in the next commit.

A new status column is introduced for subscriptions as well. Whenever an attempt is made to subscribe to a plan but a secondary payment action is required, the subscription will be put into a state of "incomplete" while payment confirmation is awaited. As soon as payment has been properly processed, a webhook will update the subscription's status to active.

After these changes, webhooks will be a fundamental part of how Cashier works and they're now required in order to properly handle any payment confirmations and off-session updates to subscriptions & customers.

The charge method now only accepts a payment method instead of a token. Developers will need to update their JS integration to retrieve a payment method id instead of a source token. These changes were done because this is now the recommended way by Stripe to work with payment methods. More info about that can be found here: https://stripe.com/docs/payments/payment-methods#transitioning
In an upcoming update all card methods as well as the create method on the subscription builder will be updated as well.

Dedicated Payment Page

These changes add a built-in payment page to Cashier. This offers a custom way for users to provide payment confirmations using, for example, 3D Secure.

Developers can redirect to this page after catching an instance of an IncompletePayment exception. They can also pass a redirect url to which the user will be redirected after they've confirmed their payment.

Screenshots for this page can be found here: #667 (comment)

Payment Confirmation Email

These changes add a built-in way for Cashier to send reminder emails to the customer when payment confirmation is needed for off-session. For example, when a subscription renews.

Note that a limitation of this addition is that emails will be sent out even when they're on-session during a payment that requires an extra action since there's no way to know for Stripe that the payment was done on or off session. But a customer will never be charged twice and will simply see a "Payment Successful" message if they visit the payment page again.

Resources

Concerns

Use incomplete status as well for failed payments? ✅

At the moment we immediately cancel a subscription when its initial payment fails. But maybe we should allow the customer a chance to use a different payment method first? At the moment his causes Stripe to be polluted with failed subscriptions and voided invoices. It would be better if the invoice just stayed open until paid.

Update 17/05: the new payment view can handle these now as well. A separate PaymentFailure exception will be thrown to inform the user about the specific failure and let them act accordingly.

How to mark subscriptions as incomplete? ✅

When a subscription is incomplete we need to mark it as incomplete as well in Cashier. I first wanted to just set the ends_at to "now" but that won't work since it'll be marked as cancelled, which isn't really wanted. I think the only solution will be to introduce a new status column on the subscriptions table and handle all the use cases in the Subscription model.

Update 20/05: I've now implemented a new status column as suggested above. It's checked in the active method on a subscription and properly cascades to the subscribed method on the billable entity.

Also catch Stripe card errors when performing single charges? ✅

At the moment card errors are caught when paying a subscription invoice, swapping plans or creating a new subscription. They're wrapped in a PaymentFailure exception which is thrown. But Stripe card errors are still thrown as normal when performing single charges. I already implemented the ActionRequired exception for single charges but I'm not sure if we should also catch Stripe card errors in the same way we do for the other parts of the app.

Update 28/06: decided to leave it as is for now. We can always decide to implement this in a future release. The most important part is that the PaymentActionRequired exception is thrown so developers can redirect customers to the payment page.

@driesvints driesvints force-pushed the billing-sca branch 2 times, most recently from ae72f18 to a0dc130 Compare May 17, 2019 17:17
@driesvints driesvints mentioned this pull request May 17, 2019
@driesvints driesvints force-pushed the billing-sca branch 5 times, most recently from aacb31d to f51e430 Compare May 21, 2019 21:33
@driesvints driesvints force-pushed the billing-sca branch 3 times, most recently from 643b132 to 5a32e06 Compare May 31, 2019 15:55
@driesvints
Copy link
Member Author

Just pushed a design for the payment page:

Screen Shot 2019-06-03 at 12 07 51

@driesvints
Copy link
Member Author

driesvints commented Jun 3, 2019

This is the view you'll see if a payment was cancelled or already successful. I feel that this can still be improved 🤔

Screen Shot 2019-06-03 at 13 35 16

@christophrumpel
Copy link

Except the "<" I really like it. Good work!

Is the "<" used somewhere else already? If you not you can maybe just remove it.

@driesvints
Copy link
Member Author

@christophrumpel thanks! I removed it.

@ipalaus
Copy link
Contributor

ipalaus commented Jun 3, 2019

I wonder if Continue instead of Back will make more sense as a copy in the payment confirmation.

@driesvints
Copy link
Member Author

@ipalaus good idea. It now looks as the following:

Screen Shot 2019-06-03 at 13 44 59

The copy "back" is still the same for the main payment view when confirmation is still needed.

@ipalaus
Copy link
Contributor

ipalaus commented Jun 3, 2019

Yeah, that looks good!

Checking again, maybe it would look better if you center-aligned paragraphs "Extra confirmation..." and "This payment was already...". Both the title and button text are centered, and especially in the confirmation, it's just too short to look good left aligned. Up to you, it's a personal taste at the end. Feel free to ignore this one :)

Also, awesome work. Thank you!

@driesvints
Copy link
Member Author

@ipalaus gonna leave it as so because the text from the main confirmation page isn't centered either :)

@driesvints
Copy link
Member Author

I can also add the amount on the page if needed with its currency sign 🤔

@ipalaus
Copy link
Contributor

ipalaus commented Jun 3, 2019

That would give more context to the view for sure 👍

@driesvints driesvints changed the title [10.0] Billing SCA [10.0] Payment Intents Jun 4, 2019
@driesvints
Copy link
Member Author

Latest iteration:

Screen Shot 2019-06-04 at 21 19 47

@driesvints
Copy link
Member Author

Just pushed a commit that allows cashier to send payment confirmations by email when extra payment action is needed. This sends an email with a button which links to the payment page above.

Screen Shot 2019-06-04 at 23 29 57

@driesvints driesvints force-pushed the billing-sca branch 2 times, most recently from b922c64 to bccfedf Compare June 6, 2019 14:07
@Reached
Copy link

Reached commented Jun 17, 2019

Great work @driesvints :)

@ricoxor
Copy link

ricoxor commented Jun 19, 2019

Hello,

Thanks for the quick update for the new stripe version.
The question will seem silly but how can I integrate the Stripe SCA on my site under Laravel?
Currently I am using Laravel version 5.8: https://laravel.com/docs/5.8/billing#subscriptions
And https://checkout.stripe.com/checkout.js

Thank you in advance for your help.

@ejblom
Copy link

ejblom commented Jun 19, 2019

Hello,

Thanks for the quick update for the new stripe version.
The question will seem silly but how can I integrate the Stripe SCA on my site under Laravel?
Currently I am using Laravel version 5.8: https://laravel.com/docs/5.8/billing#subscriptions
And https://checkout.stripe.com/checkout.js

Thank you in advance for your help.

And I would be interested in hearing which version of Spark will get the new Stripe updates :-)

} catch (IncompletePayment $exception) {
$this->markAsIncomplete();

throw $exception;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is maybe not totally the expected logic I would want to see. Imagine a user has a monthly Forge plan and 15 days into their subscription they decide to swap to yearly. If the payment is incomplete, this marks the whole subscription as incomplete and the user would no longer be considered as having an "active" subscription, even though they have already paid for the whole month.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, you're correct. I hadn't considered that scenario. In that regard I think we should also update the webhooks since it will now always place a subscription in an incomplete state when its Stripe status changes to incomplete or incomplete_expired. I'll check into this and make some changes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@taylorotwell I've checked into this and can confirm that no exception is being thrown when downgrading or when proration applies. Stripe doesn't throws an exception in this case. Stripe will mark the subscription as "past_due" in this case which means Stripe will try to re-charge the card in the intervals set in the dashboard. When a "next action" is required for the payment the invoice.payment_action_required is fired which the email setting in this PR is for.

In the other situations during a card error or "next action" payment status when you swap to a plan and need to immediately pay for an invoice the subscription is put into an incomplete state, just like when creating one. But I believe that's wanted, right?

@@ -254,6 +259,7 @@ protected function buildPayload()
return array_filter([
'billing_cycle_anchor' => $this->billingCycleAnchor,
'coupon' => $this->coupon,
'expand' => ['latest_invoice.payment_intent'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes use of the expand functionality from the Stripe API to retrieve data from objects in one request. By default when creating a new subscription, the latest_invoice parameter contains a string with its ID. By using the expand here we retrieve the actual object as well as the payment intent object so we can immediately use it below when validating the payment.

@driesvints driesvints requested a review from taylorotwell July 1, 2019 15:17
driesvints and others added 7 commits July 2, 2019 14:06
These changes bring support for the new payment intents api to charges and subscriptions. As there are quite some breaking changes here let's go over the most prominent ones below:

Any payment action will now throw an exception when a payment either fails or when the payment requires a secondary action in order to be completed. This goes for single charges, invoicing customers directly, subscribing to a new plan or swapping plans. Developers can catch these exceptions and decide for themselves how to handle these by either letting Stripe handle everything for them (to be set up in the Stripe dashboard) or use the custom built-in solution which will be added in the next commit.

A new status column is introduced for subscriptions as well. Whenever an attempt is made to subscribe to a plan but a secondary payment action is required, the subscription will be put into a state of "incomplete" while payment confirmation is awaited. As soon as payment has been properly processed, a webhook will update the subscription's status to active.

After these changes, webhooks will be a fundamental part of how Cashier works and they're now required in order to properly handle any payment confirmations and off-session updates to subscriptions & customers.

The charge method now only accepts a payment method instead of a token. Developers will need to update their JS integration to retrieve a payment method id instead of a source token. These changes were done because this is now the recommended way by Stripe to work with payment methods. More info about that can be found here: https://stripe.com/docs/payments/payment-methods#transitioning
In an upcoming update all card methods as well as the create method on the subscription builder will be updated as well.

Closes #636
These changes add a built-in payment page to Cashier. This offers a custom way for users to provide payment confirmations using, for example, 3D Secure.

Developers can redirect to this page after catching an instance of an IncompletePayment exception. They can also pass a redirect url to which the user will be redirected after they've confirmed their payment.
These changes add a built-in way for Cashier to send reminder emails to the customer when payment confirmation is needed for off-session. For example, when a subscription renews.

Not that a limitation of this addition is that emails will be sent out even when they're on-session during a payment that requires an extra action since there's no way to know for Stripe that the payment was done on or off session. But a customer will never be charged twice and will simply see a "Payment Successful" message if they visit the payment page again.
When a plan downgrade fails because of a next action requirement for the payment or a faulty card, we'll not put the subscription into an incomplete state because it could be that the person is upgrading to a yearly plan with a prorated invoice. This would mean that the customer still paid for the current period and would be allowed to continue on their subscription. These scenarios are now covered by these tests.
@mrsimonbennett
Copy link
Contributor

Not sure where it comes in with your new workflow if at all but Stripe documents refer to this quote to display users to help gain permission for off-session payment

I authorise [your business name] to send instructions to the financial institution that issued my card to take payments from my card account in accordance with the terms of my agreement with you.

Getting permission to save a card

@azimidev
Copy link

azimidev commented Jul 2, 2019

Is that all or do we still have to wait for Stripe to add more?

@driesvints
Copy link
Member Author

@mrsimonbennett heya thanks, I saw. There's a separate issue about this here: #649

@davidrushton
Copy link

@driesvints Thanks for your work on this 🙌. I'm building Payment Intents into our e-commerce framework and it's been great for inspiration and notes.

One thing I noticed that might be an issue on your Payment class is the cancelled check - https://github.com/laravel/cashier/blob/billing-sca/src/Payment.php#L96. For some reason Stripe refer to this state as canceled - https://stripe.com/docs/payments/intents

@driesvints
Copy link
Member Author

@davidrushton that's a very good spot. Thanks!

@taylorotwell taylorotwell merged commit 80d5461 into master Jul 5, 2019
@driesvints driesvints deleted the billing-sca branch July 6, 2019 17:44
@coatesap
Copy link

@driesvints For those of us wanting to jump the gun and implement this straight away, are you able to provide very brief overview of the intended payment flow for setting up new subscriptions? For instance, where should the ID that gets passed to PaymentController be generated?

@mrsimonbennett
Copy link
Contributor

@driesvints For those of us wanting to jump the gun and implement this straight away, are you able to provide very brief overview of the intended payment flow for setting up new subscriptions? For instance, where should the ID that gets passed to PaymentController be generated?

I think @coatesap you need to be passing the StripePaymentIntent ID to it, which is generated by stripe when you attempt payment see step 4 https://stripe.com/docs/billing/subscriptions/payment

@coatesap
Copy link

Thanks @mrsimonbennett - that makes sense. I was thinking more in terms of whether that should take place on the back end / front end / etc. My current code seems to work ok, but wanted to check whether it followed the intended usage:

try {
    $user->newSubscription('default', $stripeRef)->create();
} catch (PaymentActionRequired $exception) {
    return redirect()->route('cashier.payment', ['id' => $exception->payment->asStripePaymentIntent()->id]);
}

@coatesap
Copy link

For anyone else browsing this thread, https://github.com/laravel/cashier/blob/upgrade-guide/UPGRADE.md has some useful pointers.

@driesvints
Copy link
Member Author

@coatesap that's the correct flow.

I'm also working on new docs here: laravel/docs#5313

There have been some newer changes from last Thursday and Friday which I haven't gotten into the new upgrade guide or docs yet so beware of that.

@pierrocknroll
Copy link

I don't know where to put that so let's say here :
@driesvints a big thank you for all this work for the 10.0 version

@azimidev
Copy link

Yeah, man, a big thank you to @driesvints and @taylorotwell this was a massive job.

@petersuhm
Copy link

THANKS! 😍

@laravel laravel deleted a comment from damayantinama Sep 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.