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
- To have wrong Stripe customer id in db table
user__commerce_remote_idon columncommerce_remote_id_remote_id - Try to make payment
- 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.
| Comment | File | Size | Author |
|---|---|---|---|
| #14 | commerce_stripe-3326812-7.patch | 2.93 KB | abdelrahman khlefat |
Issue fork commerce_stripe-3326812
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
Comment #2
alen simonyan commentedComment #3
alen simonyan commentedComment #6
alen simonyan commentedComment #7
jonathanshawI 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.
Comment #8
abdelrahman khlefat commentedThis change was added to be able to lookup for the remote customer id in drupal by checking the customer email
Comment #9
abdelrahman khlefat commentedComment #10
abdelrahman khlefat commentedComment #11
abdelrahman khlefat commentedComment #12
abdelrahman khlefat commentedComment #13
abdelrahman khlefat commentedComment #14
abdelrahman khlefat commentedComment #15
loze commentedI 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.
Comment #16
loze commentedOk 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_idfield. 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):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.
Comment #17
loze commentedWe 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.
Comment #18
loze commentedComment #20
loze commentedMR188 is my attempt to fix this. I'm checking if the ID is valid before the payment intent request is made.
Comment #21
joe huggansCan 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.