Skip to content

Conversation

daledupreez
Copy link
Contributor

@daledupreez daledupreez commented Aug 8, 2025

Related to #4560

Changes proposed in this Pull Request:

This change introduces a new get_all_payment_methods() method to the WC_Stripe_Customer class as a way to perform fewer overall API calls when trying to load all saved payment methods for a customer. The basic approach mimics the implementation in the existing get_payment_methods() method, but removes the query parameter to limit the accepted payment method types. Due to Stripe's API limiting the result page size to 100 rows, the new logic will fetch in a loop. The new code also fetches all the data and caches it before applying any filtering. That way the cached data can be re-used as needed, even if filters are re-applied. It's worth noting that we're using the same underlying mechanism for the cache as for specific payment methods, including the method responsible for clearing the customer cache.

The PR also updates the logic in WC_Stripe_Payment_Tokens->woocommerce_get_customer_upe_payment_tokens() to use the new method to fetch all saved tokens for a user across the set of currently enabled payment methods. The key improvement here is that we will now make at most 1 request per customer (or 2, if they have >100 saved payment methods!), and then cache the result. Previously, we would make an API call per enabled payment method, and we'd make more API calls when Optimized Checkout was enabled, as we weren't filtering on active payment methods. The code also makes an important code change to use a more recently introduced function to identify which payment methods are currently active, which also simplifies the code for identifying the active payment methods.

From an implementation perspective, it's worth noting that the new code always includes expand[]=data.sepa_debit.generated_from.charge&expand[]=data.sepa_debit.generated_from.setup_attempt as URL parameters, just in case we have any SEPA debits created from other payment methods like Bancontact or iDEAL.

Testing instructions

Basic setup

  • Create a store with a location in Europe and using EUR as the currency
  • Connect to a Stripe test account
  • Ensure you install and activate WooCommerce Subscriptions and a tool like Debug Bar that allows you to see API calls made from the server
  • Ensure you have a subscription product available
  • Ensure that you enable iDEAL or Bancontact as payment methods, keep SEPA disabled, and that the Enable SEPA Direct Debit tokens for other methods setting for the Stripe gateway is enabled

Steps

  • Log in to the site as a user (or ensure your site will auto-create a user for you during checkout)
  • Work through the flow to purchase a subscription product
  • When you get to checkout, pick either iDEAL or Bancontact as your payment method
  • Place your order, and confirm the purchase via the Stripe UI
  • After confirming the payment, verify that you're taken to the order summary page
  • Navigate to My Account and then to Payment methods
  • Verify that your subscription payment method is listed as a SEPA payment method
  • Now clear all transients for your site, which can be done as follows in a dev environment:
npm run wp transient delete -- --all
  • Refresh the page, and verify that only one API call was made to https://api.stripe.com/v1/payment_methods (though it will have additional URL parameters).

  • Refresh the page again, and verify that we don't make another outbound HTTP call

    • For extra credit, you can also check out develop, clear transients and repeat the steps to see how many API requests we make 😬
  • Now add a simple product to your cart and navigate to checkout

  • Use a card to pay for the product, and save your card details

  • Navigate to My Account -> Payment Methods

  • Verify that your saved card is listed

  • Clear transients and refresh the page

  • Verify there is only one payment_methods API call

  • Refresh the page, and verify that we don't trigger another API call

  • Now add a product to your cart and navigate to checkout.

  • Clear transients again, and reload checkout.

  • Verify that you only see one API call to the Stripe payment_methods API

  • Reload checkout, and verify that you don't see any calls to the payment_methods API


  • Covered with tests (or have a good reason not to test in description ☝️)
  • Tested on mobile (or does not apply)

Changelog entry

  • This Pull Request does not require a changelog entry. (Comment required below)
Changelog Entry Comment

Comment

Post merge

@daledupreez daledupreez requested a review from Copilot August 8, 2025 15:14
Copilot

This comment was marked as outdated.

@daledupreez daledupreez requested review from a team, Copilot, malithsen and wjrosa and removed request for a team August 8, 2025 19:53
@daledupreez daledupreez self-assigned this Aug 8, 2025
@daledupreez daledupreez marked this pull request as ready for review August 8, 2025 19:53
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces a new method to efficiently fetch and cache all saved payment methods for a Stripe customer, reducing the number of API calls made to Stripe's payment_methods endpoint.

  • Adds a new get_all_payment_methods() method to the WC_Stripe_Customer class that fetches all payment methods in paginated requests and caches the complete result
  • Updates the payment token retrieval logic to use the new method instead of making separate API calls per payment method type
  • Implements proper pagination handling for customers with more than 100 saved payment methods

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
includes/class-wc-stripe-customer.php Adds the new get_all_payment_methods() method with pagination and caching logic
includes/payment-tokens/class-wc-stripe-payment-tokens.php Updates token retrieval to use the new unified method instead of multiple API calls
readme.txt Documents the API call reduction improvement
changelog.txt Adds changelog entry for the fix

}

$cache_key = self::PAYMENT_METHODS_TRANSIENT_KEY . '__all_' . $this->get_id();
$all_payment_methods = get_transient( $cache_key );
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we use the WC_Stripe_Database_Cache here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, but I was wary of adding new code and adding new data to the database cache before we have a cleanup mechanism (which I created an issue for earlier today in #4569).

For large stores with regular customers, using the database cache would result in a big jump in the options size, which could easily drive other performance issues on sites.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also want to pull @diegocurbelo into this thread as he flagged the persistent cache usage as well in his review.

I've implemented calling WC_Stripe_Database_Cache in #4570, mostly so we can merge that change into this branch if we decide to go in that direction.

Copy link
Member

Choose a reason for hiding this comment

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

For large stores with regular customers, using the database cache would result in a big jump in the options size, which could easily drive other performance issues on sites.

Expired transients are not automatically removed from the DB; that requires custom code (calling delete_expired_transients ()) or using a plugin... and each transient adds two items to the options table (one with the data, and one with the expiration), the DB cache uses one so it would be half the amount of entries than using transients.

I can create a quick PR that adds an additional options entry for the expiration timestamp in the private write_to_cache() method, and then add a new public method delete_expired() to WC_Stripe_Database_Cache similar to this one. And schedule it to run every 24 hours (filterable).

This would generate the same number of options entries as using transients, and provide a way for custom code to remove expired cache items, what do you think @daledupreez?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For now, I am concerned about timelines and stability, and what changes should (and should not) be included in 9.8.0. Maybe it makes sense to have a quick sync with the team to work things out?

return [];
}

if ( ! is_array( $response->data ) || [] === $response->data ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is getting lengthy, I would consider creating new private methods to handle different parts of the logic.

@diegocurbelo
Copy link
Member

This looks good @daledupreez!

My only concern is the use of transients; given the problems we had with the PMC cache, maybe we should use the DB cache instead.

And as you mentioned in Slack, we should also implement a scheduled task to remove expired items from the cache, since we use the customer_id as part of the cache key. But I don't think this is a blocker, the wp_options table has an index by option_name, and for both transients and the database cache, we can provide a snippet to clear expired items if needed.

We need to improve the database cache to fully support this use case... currently it stores the ttl and updated timestamp inside the option_value, which means reading them all, checking if they are expired, and deleting those that are. Transients, for example, use an additional option entry for the expiration timestamp, so it's possible to get the expired entries via a SQL query. Maybe the most flexible solution is to have both (saving an additional expiration entry automatically in write_to_cache)

@daledupreez
Copy link
Contributor Author

My only concern is the use of transients; given the problems we had with the PMC cache, maybe we should use the DB cache instead.

@diegocurbelo, I agree that this could be problematic, but we're still reducing the number of API calls considerably with this implementation, even if transients are not working. I am also concerned about causing the options data size to balloon, even if we're explicitly flagging these items with autoload = false in our caching layer.

So I went with the more conservative approach of continuing to use a transient and only changing the API we call.

@diegocurbelo
Copy link
Member

My only concern is the use of transients; given the problems we had with the PMC cache, maybe we should use the DB cache instead.

@diegocurbelo, I agree that this could be problematic, but we're still reducing the number of API calls considerably with this implementation, even if transients are not working. I am also concerned about causing the options data size to balloon, even if we're explicitly flagging these items with autoload = false in our caching layer.

So I went with the more conservative approach of continuing to use a transient and only changing the API we call.

Sounds good, we can migrate it to use the DB cache in the next release, after we implement a cache cleanup solution.

Copy link
Contributor

@malithsen malithsen left a comment

Choose a reason for hiding this comment

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

Code looks good to me and tests as expected. Only one API call is made to fetch the payment methods and it's cached on subsequent page refreshes.

Image

@daledupreez daledupreez merged commit e0d8375 into develop Aug 11, 2025
45 checks passed
@daledupreez daledupreez deleted the refactor/fetching-all-customer-payment-methods branch August 11, 2025 14:52
diegocurbelo pushed a commit that referenced this pull request Aug 11, 2025
#4567)

* Basic get_all_payment_methods() implementation
* Ensure we clear the new _all payment methods cache
* Always send expand flags for SEPA
* Refactor logic to get customer UPE tokens
* Refactor WC_Stripe_Subscriptions_Trait->maybe_render_subscription_payment_method() to use get_all_payment_methods()
* Revert "Refactor WC_Stripe_Subscriptions_Trait->maybe_render_subscription_payment_method() to use get_all_payment_methods()"
This reverts commit c622d76.
* Reinstate SEPA logic
* Remove unnecessary array_merge() call
* Changelog
* Use Payment Method Configuration to identify active payment methods
* Add link to Stripe docs for limit argument
* Use one-line isset() for missing customer checks
@daledupreez daledupreez added this to the 9.8.0 milestone Aug 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants