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

Add APM support for Subscriptions and tokens with split UPE with deferred intent #2883

Merged
merged 23 commits into from
Feb 15, 2024

Conversation

james-allan
Copy link
Contributor

Fixes #2872

Changes proposed in this Pull Request:

This PR adds/fixes the following:

  1. Subscriptions via iDEAL, Bancontact and SEPA Debit.
  2. Saved tokens via iDEAL, Bancontact and SEPA.
    • Using saved tokens via block checkout.
  3. Passing off-session args to the Stripe Payment Elements so the terms are shown to customers on the block checkout.
  4. Refunds via payment methods that support it.

Testing instructions

Subscriptions

  1. Enable the iDEAL, SEPA, Bancontact payment methods.
  2. Enable the Woo Subscriptions plugin.
  3. Create a subscription product if you don't have one.
  4. Add a subscription product to the cart.
  5. Go to the block and/or block checkout.
    • On add/deferred-intent only the card payment element will be shown.
    • On this branch, SEPA, Bancontact and iDEAL should all be shown.
add/deferred-intent This branch
Screenshot 2024-02-07 at 5 35 39 pm Screenshot 2024-02-07 at 5 36 05 pm
  1. Test all these additional payment methods with subscriptions in your cart.
  2. View the subscription and you should see that it has a payment method via SEPA.

Screenshot 2024-02-07 at 5 38 18 pm

Important

Keep in mind that no matter which payment method you use (SEPA, Bancontact or iDEAL) they all resolve to SEPA payment tokens as described in Stripe docs here, under "Recurring payments".

  1. Test automatic, and early renewals.
    • Note that because it's a SEPA token, final payment is processed via the webhook.

Tokens

After you've purchased a subscription using any of the APMs mentioned above, you should now have a SEPA token. These steps will walk through the process of using a SEPA token and saving a new one.

  1. Add a simple product to your cart and on the checkout select Bancontact, SEPA, or iDEAL.
  2. Choose to save the payment method.
  3. Complete checkout.
  4. Add another product to the cart and process the payment using the saved token.
    • Again, note that because it's a SEPA token, final payment is processed via the webhook.

Deleting tokens

  1. Go into your database and the wp_woocommerce_payment_tokens table.
  2. Delete all your Stripe related tokens.
  3. Refresh the checkout page and notice that all the tokens you deleted that still exist in your Stripe account have been recreated.

  • Covered with tests (or have a good reason not to test in description ☝️)
  • Added changelog entry in both changelog.txt and readme.txt (or does not apply)
  • Tested on mobile (or does not apply)

Post merge

@james-allan james-allan requested review from a team and mattallan and removed request for a team February 8, 2024 06:40
@james-allan
Copy link
Contributor Author

I'm placing this one on-hold for a bit. I think there's an issue in the way I'm setting the subscription's payment method ID to stripe even if you use SEPA. I think the right way is to set the subscriptions payment method to the individual UPE method and then make the payment methods that support subscriptions call $this->maybe_init_subscriptions(); to hook up all the relevant subscription hooks.

props.showSaveOption
) {
options.setupFutureUsage = 'off_session';
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change makes sure that when a subscription is in the cart, or if saving the method is allowed these terms are displayed to the user.

Screenshot 2024-02-09 at 2 28 40 pm

*/
if ( $is_stripe_payment_method && isset( $data['issavedtoken'] ) && $data['issavedtoken'] ) {
$context->set_payment_data( array_merge( $data, [ 'wc-stripe-is-deferred-intent' => true ] ) );
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using a saved payment token on the block checkout doesn't go via PaymentProcessor function and so this flag that we use to determine if we should process the request via the deferred intent flow isn't set.

This change makes sure we set this flag early in the request to process the block checkout.


return $is_stripe_link_enabled || $is_sepa_debit_payment;
return 'card' === $selected_payment_type && in_array( 'link', $this->get_upe_gateway()->get_upe_enabled_payment_method_ids(), true );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because Bancontact and iDEAL resolve to SEPA debit payment tokens, Stripe also requires that mandate data be set when attempting to use Bancontact and save it.

'payment_method' => $payment_information['payment_method'],
'payment_method_types' => [ $payment_information['selected_payment_type'] ],
'customer' => $payment_information['customer'],
'confirm' => 'true',
'return_url' => $payment_information['return_url'],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that Bancontact and iDEAL can be used for set up intents, we need to pass a return URL here.

@@ -712,7 +714,7 @@ private function process_payment_with_deferred_intent( int $order_id ) {

// Handle saving the payment method in the store.
// It's already attached to the Stripe customer at this point.
if ( $payment_information['save_payment_method_to_store'] ) {
if ( $payment_information['save_payment_method_to_store'] && $upe_payment_method && $upe_payment_method->get_id() === $upe_payment_method->get_retrievable_type() ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change can essentially be described as: only save the token if the payment method being used resolves to that same payment method. eg card = card 👍 bancontact = sepa ❌.

When using a Bancontact to purchase a subscription we don't save the bancontact stripe payment method id (ie pm_) because it's not reusable. After the customer returns to the store after authorising the payment, stripe will create a sepa_debit token and we save it at that point. See process_upe_redirect_payment() and process_order_for_confirmed_intent().

@james-allan
Copy link
Contributor Author

This is ready for review again. In the most recent set of changes I've changed it so when you purchase a subscription it will now set the subscription's payment gateway to the correct payment method ID. eg if you purchase it with a card, it will be set to stripe. If you purchase it with Bancontact, iDEAL or SEPA it will be set to stripe_sepa_debit.

This means the subscription will now list the right payment method type in the admin list table etc.

Screenshot 2024-02-09 at 4 34 13 pm

Copy link
Contributor

@mattallan mattallan left a comment

Choose a reason for hiding this comment

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

@james-allan I did some testing today and noticed a couple of things:

  • pm_ token stored on the subscription is different than what gets saved in the wc_payment_tokens table
    • Token saved on the subscription:
      image
    • Token saved in the woo_payment_tokens table is different
    • From stripe, the one that is saved in the WC tokens table is the setup intent one, and the one saved on the subscription is the payment intent one:
      image
  • ❌ Processing a renewal payment with an APM (ideal) results in the following fatal:
[12-Feb-2024 03:23:17 UTC] PHP Fatal error:  Uncaught Error: Call to undefined method WC_Stripe_UPE_Payment_Method_Sepa::get_intent_from_order() in /Users/matt/local/woo/wp-content/plugins/woocommerce-gateway-stripe/includes/compat/trait-wc-stripe-subscriptions.php:818
Stack trace:
#0 /Users/matt/local/woo/wp-content/plugins/woocommerce-gateway-stripe/includes/compat/trait-wc-stripe-subscriptions.php(244): WC_Stripe_UPE_Payment_Method_Sepa->has_authentication_already_failed(Object(WC_Order))
#1 /Users/matt/local/woo/wp-content/plugins/woocommerce-gateway-stripe/includes/compat/trait-wc-stripe-subscriptions.php(189): WC_Stripe_UPE_Payment_Method_Sepa->process_subscription_payment('225.00', Object(WC_Order), true, false)
#2 /Users/matt/local/woo/wp-includes/class-wp-hook.php(324): WC_Stripe_UPE_Payment_Method_Sepa->scheduled_subscription_payment('225.00', Object(WC_Order))
#3 /Users/matt/local/woo/wp-includes/class-wp-hook.php(348): WP_Hook->apply_filters('', Array)
#4 /Users/matt/local/woo/wp-includes/plugin.php(517): WP_Hook->do_action(Array)
#5 /Users/ in /Users/matt/local/woo/wp-content/plugins/woocommerce-gateway-stripe/includes/compat/trait-wc-stripe-subscriptions.php on line 818
  • ❌ On the Edit Subscription page, when you go to edit the payment method, there's two SEPA payment options. Inspecting the elements shows one is actually 'stripe' but for some reason the title is being listed as SEPA? I couldn't figure this one out.

I've been digging into the second issue a bit and it brings up a bigger question (and potential issues). If the SEPA payment method is going to be using the subscriptions trait, should it extend a gateway class, or should we refactor the subscriptions trait so that it doesn't assume the trait will be used on a gateway class by using things like WC_Stripe::get_instance()->get_main_stripe_gateway()->{gateway_method} 🤔

@james-allan
Copy link
Contributor Author

@mattallan I've pushed 5f76377 in line with the approach we discussed in a Slack huddle. I'm going to put it through some tests to make sure things are working as expected.

@james-allan
Copy link
Contributor Author

A couple of things I've noticed so far:

  • With Multiple subscriptions in the cart iDEAL and Bancontact aren't being shown on the checkout.
  • Tokens are only backfilled on the classic checkout page. If you have tokens missing in your databse, loading the block checkout doesn't backfill them for some reason.
  • Deleting token in Stripe directly doesn't delete it from token database in Woo. It's my understanding this was part of the woocommerce_get_customer_upe_payment_tokens() function's responsibility.

@mattallan
Copy link
Contributor

mattallan commented Feb 14, 2024

I noticed my APM tokens aren't getting deleted from Stripe so I've opened a separate issue for this:

EDIT: opened another issue

@james-allan
Copy link
Contributor Author

Another issue:

@mattallan
Copy link
Contributor

mattallan commented Feb 14, 2024

@james-allan I did a bit more testing today and found some issues. Let me know if you'd rather move these out to separate issues so that we can divide and conquer 😄


APM testing (iDeal):

  • Purchase simple
    • Use new card
    • Use saved card
  • Process renewal
    • New card
    • Saved card
    • ❌ If you have the SEPA payment method disabled in WC > Settings > Payments > Stripe > Payment Methods. When you purchase a subscription with iDeal, the initial purchase is fine, but processing the renewal order fails with the following error:
The PaymentMethod provided (sepa_debit) is not allowed for this PaymentIntent. Please attach a PaymentMethod of one of the following types: card, giropay, eps, bancontact, ideal, p24. Alternatively, update the allowed payment_method_types for this PaymentIntent to include "sepa_debit"
  • Renew early
    • Modal
    • Checkout
  • Change payment method
  • Purchase trial
    • Use a saved card
    • New card
  • Process switch
    • No switch cost
    • With prorated amount
  • Refund renewal order
    • ❌ WC Core is calling WC_Stripe_UPE_Payment_Method_Sepa::process_refund() but it's not calling our UPE gateway refund function (😭), instead it's calling the WC_Payment_Gateway::process_refund which does nothing but return false. Looks like the __call() function isn't getting called in this case because the base class has defined this empty function.

Standard Credit Card testing:

  • ❌ Not sure if this is a known issue but while purchasing a subscription on the checkout block with a saved card the Subscription is active but requires manual renewal. Looking in the DB, the payment_method is being set to stripe_card instead of just stripe 😢. Should I move this to a separate issue?

@james-allan
Copy link
Contributor Author

james-allan commented Feb 14, 2024

If you have the SEPA payment method disabled in WC > Settings > Payments > Stripe > Payment Methods. When you purchase a subscription with iDeal, the initial purchase is fine, but processing the renewal order fails with the following error:

Good catch. I've had a look into this one, and the issue stems from this code in the create_and_confirm_intent_for_off_session() function.

if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) {
	$payment_method_types = $this->get_upe_enabled_at_checkout_payment_method_ids();
} elseif ( isset( $prepared_source->source_object->type ) ) {
	$payment_method_types = [ $prepared_source->source_object->type ];
}

Given this function has off_session in the name, I'm not sure why it limits the allowed payment method types to those enabled at checkout. Its generally accepted that if a merchant disables a payment method, subscriptions already signed up with that payment method should still work, it just won't be available to new sign ups. ie all off session payments should be allowed right?

With that in mind, I'm thinking of removing that first if statement entirely. Thoughts?

@mattallan
Copy link
Contributor

With that in mind, I'm thinking of removing that first if statement entirely. Thoughts?

Yeah I agree. Looking at where create_and_confirm_intent_for_off_session() is called, it's only used when handling subscription and pre-order payments so it shouldn't be limited by enabled gateways only and should work for any stripe payment method imo.

@james-allan
Copy link
Contributor Author

If you have the SEPA payment method disabled in WC > Settings > Payments > Stripe > Payment Methods. When you purchase a subscription with iDeal, the initial purchase is fine, but processing the renewal order fails with the following error:

I've fixed that with b99580b.

Refund renewal order ❌

I've fixed that in b13c162.

Not sure if this is a known issue but while purchasing a subscription on the checkout block with a saved card the Subscription is active but requires manual renewal. Looking in the DB, the payment_method is being set to stripe_card instead of just stripe 😢. Should I move this to a separate issue?

I still need to look into this one.

@james-allan
Copy link
Contributor Author

Not sure if this is a known issue but while purchasing a subscription on the checkout block with a saved card the Subscription is active but requires manual renewal. Looking in the DB, the payment_method is being set to stripe_card instead of just stripe 😢. Should I move this to a separate issue?

I still need to look into this one.

Ok. I've fixed that issue in d1f7541 too. 😓

@a-danae
Copy link
Contributor

a-danae commented Feb 14, 2024

Subscriptions via iDEAL, Bancontact and SEPA Debit.

Should we also include Sofort? It's reusable and it continues to be displayed when it was enabled before.

@james-allan
Copy link
Contributor Author

james-allan commented Feb 15, 2024

Here's the full set of tests I'm going to rerun through now that this PR seems a little more stable. I may add to them as I go.

Subscription sign up

  • Simple Subscription Purchase
  • Free Trial Subscription Purchase
    • Cards
      • New
      • Saved token
      • New 3DS card
      • Saved 3DS card
    • Bancontact
      • New
      • Saved token
    • iDEAL
      • New
      • Saved token
    • SEPA
      • New
      • Saved token
    • Sofort
      • New
      • Saved token
  • Mixed carts (subscription + standard product)
    • Cards
      • New
      • Saved token
      • New 3DS card
      • Saved 3DS card
    • Bancontact
      • New
      • Saved token
    • iDEAL
      • New
      • Saved token
    • SEPA
      • New
      • Saved token
    • Sofort
      • New
      • Saved token

Renewals

  • Automatic renewals
  • Manual renewals
  • Early renewals (modal)
  • Early renewals (via cart)
  • Failed renewals (invalid token)
  • Failed renewal (insufficient funds token)

Changing payment methods

  • SEPA
    • Changing to new SEPA
    • Change to existing SEPA token
  • Card
    • Change to new Card
    • Change to existing Card token
    • Change to new 3DS Card
    • Change to existing 3DS Card token

Every other payment method will need to be part of: #2905

  • Bancontact
    • Change to new Bancontact
    • Change to existing Bancontact token
  • iDEAL
    • Change to new iDEAL
    • Change to existing iDEAL token
  • Sofort
    • Change to new iDEAL
    • Change to existing iDEAL token

Tokens

  • Deleting from My Account -> Payment Methods
  • Adding tokens from My Account -> Payment Methods.

@james-allan
Copy link
Contributor Author

Should we also include Sofort? It's reusable and it continues to be displayed when it was enabled before.

It's my understanding the Sofort has been discontinued by Stripe. There are PRs I've seen removing it from the plugin and in Stripe's docs they say that they are in the process of deprecating it but they do leave the door open to existing merchants.

So, if I've misunderstood and we do still support Sofort let me know. :)

@mattallan
Copy link
Contributor

Deleting from My Account -> Payment Methods

Just making a note that Danae has fixed this in #2902 💯

…cally (#2902)

* Check whether the deleted token belongs to a reusable gateway

Before, we were only checking it belonged to the main gateway. Since Split PE, APMs have their own gateway so we needed to change this check

* Move the try/catch blocks up in the method

* Add Bancontact, Ideal, and Sofort to the static list of reusable gateways
@a-danae
Copy link
Contributor

a-danae commented Feb 15, 2024

It's my understanding the Sofort has been discontinued by Stripe. There are PRs I've seen removing it from the plugin and in Stripe's docs they say that they are in the process of deprecating it but they do leave the door open to existing merchants.

Yup, that's what I've understood: It's been discontinued by Stripe but still functioning for existing merchants.

In the plugin, looks like we haven't removed it for merchants that still have it active (ref in UPE, ref in classic).
That makes me think we should continue to make it functional, so it continues to work for merchants that have it active. Glad to learn if that's not the case, tho!

@james-allan
Copy link
Contributor Author

That makes me think we should continue to make it functional

Ok, yeah that makes sense. I've updated the Sofort UPE method to now support subscriptions in d6690c9. I've also updated my list of tests above to also test Sofort.

FWIW and incase this is helpful to anyone else, because my Stipe settings didn't show Sofort as an available payment method, I just forced it on via the option.

$settings = get_option( 'woocommerce_stripe_settings' );

$settings['upe_checkout_experience_accepted_payments'][] = 'sofort';
update_option( 'woocommerce_stripe_settings', $settings );

Copy link
Contributor

@mattallan mattallan left a comment

Choose a reason for hiding this comment

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

@james-allan I did a bunch more testing on this branch on the latest changes today and I would say this is ready to be merged!!! 👏

I found one issue with not being able to use a saved credit card when paying for a failed subscription renewal. I was able to replicate this on the base branch add/deferred-intent so I'm going to create a separate issue to fix this.

Question:

  1. Should iDeal and Bancontact support multiple subscriptions or leave it out for now?

iDeal

  • Purchase simple
    • Use new card
    • Use saved card
  • Purchase multiple subscriptions
    • iDeal not supported ❓
  • Process renewal
    • New card
    • Saved card
    • Confirm successful renewals with iDeal even with the SEPA payment method disabled
  • Renew early
    • Modal
    • Checkout
  • Change payment method
  • Purchase trial
    • Use a saved card
    • New card
  • Process switch
    • No switch cost
    • With prorated amount
  • Refund renewal order

Credit Card

  • Purchase simple
    • Use new card
      • Checkout shortcode
      • Checkout block
    • Use saved card
      • Checkout shortcode
      • Checkout block
  • Purchase multiple subscriptions
    • Use new card
      • Checkout shortcode
      • Checkout block
    • Use saved card
      • Checkout block
      • Checkout shortcode
  • Purchase mixed checkout
    • Use new card
      • Checkout shortcode
      • Checkout block
    • Use saved card
      • Checkout block
      • Checkout shortcode
  • Process renewals
    • single purchase
    • multiple subscriptions
    • mixed checkout
  • Renew early
    • Modal
    • Checkout
  • Change payment method
  • Purchase trial
    • Use new card
      • Checkout shortcode
      • Checkout block
    • Use saved card
      • Checkout block
      • Checkout shortcode
  • Process switch
  • Refund renewal order
  • Pay for a failed renewal
    • New card
      • Checkout shortcode
      • Checkout block
    • Saved card
      • Checkout shortcode
      • Checkout block ❌
        • Error found in stripe logs: No such payment_intent: 'seti_XXXXXX'
        • Can reproduce on add/deferred-intent so I'll create a separate issue for this

SEPA Direct Debit

  • Purchase subscriptiom
    • Use new card
      • AT611904300234573201
      • AT861904300235473202 (fails)
    • Use saved card
  • Renew
  • Renew early
  • Change payment method
    • change to different SEPA card
    • change to a credit card
  • Purchase trial
  • Refund renewal order

Bancontact

  • Purchase simple
    • Use new card
    • Use saved card
  • Renew
  • Renew early
  • Purchase trial
  • Refund renewal order

@james-allan
Copy link
Contributor Author

@mattallan I'm still going through your latest response and all the tests and questions you've raised. I was just in the middle of my tests too and I found an issue with using a new 3DS card on the block checkout (I haven't tested classic checkout). The purchase goes through but the payment method on the subscriptions says N/A and the customer ID is for some reason missing.

Screenshot 2024-02-15 at 1 18 00 pm Screenshot 2024-02-15 at 1 22 15 pm

Can you just check that you can replicate that too and I'll file an issue for it.

@mattallan
Copy link
Contributor

mattallan commented Feb 15, 2024

Can you just check that you can replicate that too and I'll file an issue for it.

Yep I can reproduce it, but only on the Checkout block page and only when using a new card.

  • 👍 Existing 3DS saved card on block checkout
  • ❌ New 3DS card on block checkout
  • 👍 Existing 3DS saved card on shortcode checkout
  • 👍 New 3DS card on shortcode checkout

Payment method on My Account > View subscription is N/A, but the payment method in the DB is still set to stripe.

image

@james-allan
Copy link
Contributor Author

Thanks for confirming. I can also replicate on add/deferred-intent so will file an issue. It appears that the issue is caused by the fact that the maybe_process_upe_redirect() is never ran when using the block checkout in this scenario and that means that the completed/authorized payment source isn't processed.

@mattallan
Copy link
Contributor

Note

I've opened a new issue to address the problem I found to do with paying for a failed renewal using a saved card on the block checkout: #2904

@james-allan
Copy link
Contributor Author

Should iDeal and Bancontact support multiple subscriptions or leave it out for now?

I've patched that in e1e3926. :)

@james-allan james-allan merged commit 747df01 into add/deferred-intent Feb 15, 2024
32 checks passed
@james-allan james-allan deleted the fix/2872-dPE-APM-subscription-support branch February 15, 2024 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants