Problem/Motivation

The problem is that when Stripe is trying to create a payment method Drupal checks in DB (in user__commerce_remote_id table) if the user has a customer_id from Stripe and when it is found in DB it tries to attach that customer to a payment method, but at that point, the customer_id does not exist in Stripe (it was deleted for some reason) and the remaining execution of script was stopped.

/modules/contrib/commerce_stripe/src/Plugin/Commerce/PaymentGateway/Stripe.php

protected function doCreatePaymentMethod(PaymentMethodInterface $payment_method, array $payment_details) {
  $stripe_payment_method_id = $payment_details['stripe_payment_method_id'];
  $owner = $payment_method->getOwner();
  $customer_id = NULL;
  if ($owner && $owner->isAuthenticated()) {
    $customer_id = $this->getRemoteCustomerId($owner); // Here it checks for commerce_remote_id, see the 'getRemoteCustomerId' function below.
  }
  try {
    $stripe_payment_method = PaymentMethod::retrieve($stripe_payment_method_id);
    if ($customer_id) {
      $stripe_payment_method->attach(['customer' => $customer_id]);
      $email = $owner->getEmail();
    }
    // If the user is authenticated, created a Stripe customer to attach the
    // payment method to.
    elseif ($owner && $owner->isAuthenticated()) {
      $email = $owner->getEmail();
      $customer = Customer::create([
        'email' => $email,
        'description' => $this->t('Customer for :mail', [':mail' => $email]),
        'payment_method' => $stripe_payment_method_id,
      ]);
      $customer_id = $customer->id;
      $this->setRemoteCustomerId($owner, $customer_id);
      $owner->save();
    }
    else {
      $email = NULL;
    }

    if ($customer_id && $email) {
      $payment_method_data = [
        'email' => $email,
      ];
      if ($billing_profile = $payment_method->getBillingProfile()) {
        $billing_address = $billing_profile->get('address')->first()->toArray();
        $payment_method_data['address'] = [
          'city' => $billing_address['locality'],
          'country' => $billing_address['country_code'],
          'line1' => $billing_address['address_line1'],
          'line2' => $billing_address['address_line2'],
          'postal_code' => $billing_address['postal_code'],
          'state' => $billing_address['administrative_area'],
        ];
        $payment_method_data['name'] = $billing_address['given_name'] . ' ' . $billing_address['family_name'];
      }
      PaymentMethod::update($stripe_payment_method_id, ['billing_details' => $payment_method_data]);
    }
  }
  catch (ApiErrorException $e) {
    ErrorHelper::handleException($e);
  }
  return $stripe_payment_method->card;
}
/modules/contrib/commerce/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php

public function getRemoteCustomerId(UserInterface $account) {
  $remote_id = NULL;
  if ($account->isAuthenticated()) {
    $provider = $this->parentEntity->id() . '|' . $this->getMode();
    /** @var \Drupal\commerce\Plugin\Field\FieldType\RemoteIdFieldItemListInterface $remote_ids */
    $remote_ids = $account->get('commerce_remote_id');
    $remote_id = $remote_ids->getByProvider($provider);
    // Gateways used to key customer IDs by module name, migrate that data.
    if (!$remote_id) {
      $remote_id = $remote_ids->getByProvider($this->pluginDefinition['provider']);
      if ($remote_id) {
        $remote_ids->setByProvider($this->pluginDefinition['provider'], NULL);
        $remote_ids->setByProvider($provider, $remote_id);
        $account->save();
      }
    }
  }

  return $remote_id;
}

Steps to reproduce

  1. To have wrong Stripe customer id in db table user__commerce_remote_id on column commerce_remote_id_remote_id
  2. Try to make payment
  3. Observe result

Proposed solution

The solution will be to check if the customer exists in Stripe, if not create a new customer and then continue further.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Alen Simonyan created an issue. See original summary.

alen simonyan’s picture

Issue summary: View changes
alen simonyan’s picture

Issue summary: View changes

alen simonyan’s picture

Issue summary: View changes
jonathanshaw’s picture

I dislike the way the solution here always loads the remote customer; that's a call against Stripe's remote API and therefore slow.

I'd prefer it if we could try/catch the error that happens if the customer does not exist, and fix & retry.

abdelrahman khlefat’s picture

StatusFileSize
new3.64 KB

This change was added to be able to lookup for the remote customer id in drupal by checking the customer email

abdelrahman khlefat’s picture

abdelrahman khlefat’s picture

StatusFileSize
new2.49 KB
abdelrahman khlefat’s picture

StatusFileSize
new3.33 KB
abdelrahman khlefat’s picture

StatusFileSize
new3.05 KB
abdelrahman khlefat’s picture

StatusFileSize
new3.01 KB
abdelrahman khlefat’s picture

StatusFileSize
new2.93 KB
loze’s picture

I too am getting Stripe error 'resource_missing': No such customer: 'cus_PEyPSXQ0VjqlCE' errors when clicking "continue to review" and can't checkout using Payment Element.

The patch here did not apply for me.

loze’s picture

Title: No such customer » "No such customer" error when commerce_remote_id contains invalid Stripe customer ID

Ok so, I manually applied the code changes, but it did not resolve the issue.

For me this is happening because I had an invalid customer ID stored in the commerce_remote_id field. It was left over from testing with a different Stripe account. So this customer ID didn't exist in my current sandbox. But this could happen If a customer was deleted in stripe, or if you switch stripe accounts.

For me at least, this is happening when creating a payment intent, and the patch and MR above did not address this.

@jonathanshaw mentioned that instead of checking if it's a valid customer, we should try to intercept it in the exception and fix it. We could do something like this inside createPaymentIntent (untested):

...
} catch (ApiErrorException $e) {
  if ($e instanceof StripeInvalidRequestException) {
    if ($e->getStripeCode() === 'resource_missing' && $e->getStripeParam() === 'customer') {
      $user = $order->getCustomer();
      $this->setRemoteCustomerId($user, NULL);
      $user->save();
    }
  }
  ErrorHelper::handleException($e, $payment ?? $payment_method);
}

Which would clear out the invalid customer ID and then leave it up to the user to resubmit the form. Or should we be retrying it ourselves? I'm not really sure where the best place to check for and fix this is.
Some guidance would be appreciated. Thanks.

loze’s picture

We could avoid the error altogether if we checked for a valid customer before making the payment intent request, but this would require an API call to Stripe which @jonathanshaw felt was not needed.

An alternative could be to check before the payment intent request, and if it's not a valid customer, clear the ID from the user so subsequent calls won't check since there will be no customer ID on the user entity. But this would require at least one additional API call for any user with a customer ID.

This does feel better from a UX perspective so the user doesn't see an error and need to submit again.

loze’s picture

Version: 8.x-1.0-rc6 » 2.x-dev
Assigned: alen simonyan » Unassigned

loze’s picture

Status: Active » Needs review

MR188 is my attempt to fix this. I'm checking if the ID is valid before the payment intent request is made.

joe huggans’s picture

Can we be sure that when the ApiErrorException is thrown that it is definitely because the customer ID is not found in Stripe? Because we don't want the user's remote customer ID to be nullified in any other circumstances.

The Stripe docs state for ApiErrorException that it means - "Something went wrong on Stripe’s end. (These are rare.)" and "Treat the result of the API call as indeterminate. That is, don’t assume that it succeeded or that it failed."

https://docs.stripe.com/error-handling#api-errors

This answer on Stack Overflow suggests checking for the resource_missing code.
https://stackoverflow.com/questions/51822190/stripe-error-catching-for-g...

Also should we avoid duplicate code, would the IntentHelper class be an appropriate place to put the try catch logic?

I think we'll need tests for this work also, happy to help if you need.