Do not set setup_future_usage off_session again for already stored payment methods to avoid multiple mandates creation

Created on 15 January 2024, 10 months ago
Updated 9 July 2024, 5 months ago

Hi there,

I have been using PaymentElement.
Although at the time of this writing Paypal was not implemented yet, with some custom coding, I have been able to successfully pay with Paypal, with setup_future_usage set to off_session. I have been able to store the Paypal payment method as reusable. I was able to reuse this Paypal pm for on session payments multiple times without any problem.
However, when I tried to make an off session payment in order to renew client subscription in the background, I got the following error:
There are multiple mandates associated with the PaymentMethod for this PaymentIntent. A `mandate` parameter must be provided to specify which to use
I contacted the guys from Stripe on Discord and here's what we figured out together: every time we confirm a PaymentIntent with setup_future_usage set to off_session, with Paypal as payment method, a mandate will be created to allow future off session payments. If multiple mandates exist for one same Paypal PM, we need to specify which mandate must be used. However, the mandate id is not passed in the return_url after payment. Furthermore, guys from Stripe could not tell me how to check how many mandates a payment method has, nor how to retrieve their ids. They told me this does not seem to be made available by the API. So I had no way to specify which mandate to use.
However, they insisted on the fact that the correct way to go is NOT to create multiple mandates.
setup_future_usage off_session should only be set when the PM does not exist yet. Later on, when reusing this stored PM for on session and off session payments, setup_future_usage should not be set at all so no other mandate is ever created.
Even though I did not face this problem with credit_cards, I believe generally updating the code to never set setup_future_usage on an already existing payment method of any type that has already been configured for reusability should be the way to go.
Here is the code I personally added in StripeReview->buildPaneForm in order to make this all work:

      // Set the setup_future_usage parameter for the Stripe Payment Element.
      if ($stripe_plugin instanceof StripePaymentElementInterface) {
        $intent_attributes['setup_future_usage'] = $stripe_plugin->getPaymentMethodUsage();
        // start custom //
        if (!$this->order->get('payment_method')->isEmpty()) {
          unset($intent_attributes['setup_future_usage']);
        }
        // end custom //
      }

What do you think?

πŸ“Œ Task
Status

Closed: outdated

Version

1.1

Component

Payment Element

Created by

πŸ‡«πŸ‡·France nicolas bouteille

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @nicolas bouteille
  • πŸ‡«πŸ‡·France nicolas bouteille

    Because of the code above, a new specific case also needs to be dealt with: after selecting an existing payment method, the client changes his mind, comes back and finally chooses to use a new payment method (that will be added with Payment Element form at review step).

    As of now, setup_future_usage is only set in StripeReview when the PaymentIntent does not exist yet.
    The PaymentIntent is created the first time we arrive at the Review step.
    Because the client selected an already existing payment method at the Payment Information pane, with the code above, we did not set setup_future_usage (null) so that no new mandate would be created.
    Now he's gone back and selected "use a new payment method".
    So when StripeReview->buildPaneForm() is called for the second time, the PaymentIntent already exist so we never reach the code that is supposed to set setup_future_usage.

  • πŸ‡«πŸ‡·France nicolas bouteille

    Rarer scenario: the client chooses to use a new payment method (which creates the PaymentIntent with setup_future_usage to off_session) but changes his mind, goes back, chooses an already existing payment method. Now in StripeReview we need to update the PaymentIntent and set setup_future_usage to null again.

  • πŸ‡«πŸ‡·France nicolas bouteille

    Here is how I've been able to solve the problems I just told you about:
    The code below is added to StripeReview->buildPaneForm() after line 200 after we check wether the PaymentIntent already exists or not. Indeed, I believe we should update setup_future_usage every time we come back to StripeReview even if PaymentIntent already exists, because the client may have gone back and switched from using an existing payment method to using a new payment method and vice versa.

        if ($this->order->get('payment_method')->isEmpty()) { // new payment method
          // Set the setup_future_usage parameter for the Stripe Payment Element.
          if ($stripe_plugin instanceof StripePaymentElementInterface) {
            $intent = PaymentIntent::update($intent->id, [
              'setup_future_usage' => $stripe_plugin->getPaymentMethodUsage(),
            ]);
          }
        }
        else {
          // For existing payment methods, we don't set setup_future_usage at all (NULL) so we don't create multiple mandates
          if ($intent->setup_future_usage === "off_session") { // if the client previously chose to add new payment method
            // it is not possible to update PaymentIntent that already has setup_future_usage set to off_session back to NULL
            // only solution that seems possible is to cancel current PaymentIntent and create a new one without setup_future_usage
            $intent->cancel();
            $payment_process_pane = $this->checkoutFlow->getPane('payment_process');
            assert($payment_process_pane instanceof CheckoutPaneInterface);
            $intent_attributes = [
              'capture_method' => $payment_process_pane->getConfiguration()['capture'] ? 'automatic' : 'manual',
            ];
            $intent = $stripe_plugin->createPaymentIntent($this->order, $intent_attributes);
          }
    
        }
    

    Obviously, the code before line 200 that sets setup_future_usage for PaymentElement only when the PaymentIntent did not exist yet, should be deleted...

    What do you guys think?

  • πŸ‡¦πŸ‡ΉAustria agoradesign

    Side notice on this issue: I'm coming from πŸ“Œ In StripePaymentElement->onReturn do not allow PaymentGatewayException to bubble up if we know the payment was successful on Stripe's end Active because I've realized that any other payment methods than credit cards (and the American ACH banking) are currently not supported by Payment Element, like in my case PayPal was refused on the Drupal side (the Stripe transaction was successful)

    I found this issue description here a little bit weird at first, as it clearly states that PayPal payments already have worked at the time of writing this issue. This important refactoring here destroyed PayPal and other payments until a dedicated plugin is created: https://git.drupalcode.org/project/commerce_stripe/-/commit/bf118321d3fb...

  • πŸ‡«πŸ‡·France nicolas bouteille

    Your right. Let me update the description to clarify the situation.

  • πŸ‡«πŸ‡·France nicolas bouteille

    Updating the description to make it clear that using Paypal was done via custom code.

  • Status changed to Closed: outdated 5 months ago
  • πŸ‡ΊπŸ‡ΈUnited States TomTech

    This has been addressed in a separate issue. setup_future_usage is no longer set if an existing payment method is selected by the customer.

    See: https://git.drupalcode.org/project/commerce_stripe/-/blob/8.x-1.x/src/Pl...

Production build 0.71.5 2024