diff --git a/changelog/WOOPMNT-4879 b/changelog/WOOPMNT-4879 new file mode 100644 index 00000000000..53a925becd0 --- /dev/null +++ b/changelog/WOOPMNT-4879 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Stop using dynamic keys for the database cache and move cached payment methods to user meta. diff --git a/includes/class-database-cache.php b/includes/class-database-cache.php index 2a976de4430..2a5a9c11872 100644 --- a/includes/class-database-cache.php +++ b/includes/class-database-cache.php @@ -19,26 +19,10 @@ class Database_Cache implements MultiCurrencyCacheInterface { const ACCOUNT_KEY = 'wcpay_account_data'; const ONBOARDING_FIELDS_DATA_KEY = 'wcpay_onboarding_fields_data'; const BUSINESS_TYPES_KEY = 'wcpay_business_types_data'; - const PAYMENT_PROCESS_FACTORS_KEY = 'wcpay_payment_process_factors'; const FRAUD_SERVICES_KEY = 'wcpay_fraud_services_data'; const RECOMMENDED_PAYMENT_METHODS = 'wcpay_recommended_payment_methods'; const ADDRESS_AUTOCOMPLETE_JWT_KEY = 'wcpay_address_autocomplete_jwt'; - /** - * Refresh during AJAX calls is avoided, but white-listing - * a key here will allow the refresh to happen. - * - * @var string[] - */ - const AJAX_ALLOWED_KEYS = [ - self::PAYMENT_PROCESS_FACTORS_KEY, - ]; - - /** - * Payment methods cache key prefix. Used in conjunction with the customer_id to cache a customer's payment methods. - */ - const PAYMENT_METHODS_KEY_PREFIX = 'wcpay_pm_'; - /** * Dispute status counts cache key. * @@ -86,6 +70,26 @@ class Database_Cache implements MultiCurrencyCacheInterface { */ const TRACKING_INFO_KEY = 'wcpay_tracking_info_cache'; + /** + * All cache keys. + * + * @var string[] + */ + const ALL_KEYS = [ + self::ACCOUNT_KEY, + self::ONBOARDING_FIELDS_DATA_KEY, + self::BUSINESS_TYPES_KEY, + self::FRAUD_SERVICES_KEY, + self::RECOMMENDED_PAYMENT_METHODS, + self::DISPUTE_STATUS_COUNTS_KEY, + self::DISPUTE_STATUS_COUNTS_KEY_TEST_MODE, + self::ACTIVE_DISPUTES_KEY, + self::AUTHORIZATION_SUMMARY_KEY, + self::AUTHORIZATION_SUMMARY_KEY_TEST_MODE, + self::CONNECT_INCENTIVE_KEY, + self::TRACKING_INFO_KEY, + ]; + /** * Refresh disabled flag, controlling the behaviour of the get_or_add function. * @@ -214,72 +218,13 @@ public function delete( string $key ) { } } - /** - * Deletes all cache entries that are keyed with a certain prefix. - * - * This is useful when you use dynamic cache keys. - * - * Note: Only key prefixes with known, static prefixes are allowed, for protection purposes. - * - * @param string $key_prefix The cache key prefix. - * - * @return void - */ - public function delete_by_prefix( string $key_prefix ) { - // Protection against accidentally deleting all options or options that are not related to WCPay caching. - // Feel free to update this statement as more prefix cache keys are used. - $allowed_base_prefixes = [ - self::PAYMENT_METHODS_KEY_PREFIX, - self::ONBOARDING_FIELDS_DATA_KEY, - self::RECOMMENDED_PAYMENT_METHODS, - ]; - $is_allowed = false; - foreach ( $allowed_base_prefixes as $allowed_base_prefix ) { - if ( strncmp( $key_prefix, $allowed_base_prefix, strlen( $allowed_base_prefix ) ) === 0 ) { - $is_allowed = true; - break; - } - } - if ( ! $is_allowed ) { - return; // Maybe throw exception here... - } - - global $wpdb; - - $options = $wpdb->get_results( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", $key_prefix . '%' ) ); - foreach ( $options as $option ) { - $this->delete( $option->option_name ); - } - } - /** * Delete all known cache entries. */ public function delete_all() { - $keys = [ - self::ACCOUNT_KEY, - self::ONBOARDING_FIELDS_DATA_KEY, - self::BUSINESS_TYPES_KEY, - self::PAYMENT_PROCESS_FACTORS_KEY, - self::FRAUD_SERVICES_KEY, - self::RECOMMENDED_PAYMENT_METHODS, - self::DISPUTE_STATUS_COUNTS_KEY, - self::DISPUTE_STATUS_COUNTS_KEY_TEST_MODE, - self::ACTIVE_DISPUTES_KEY, - self::AUTHORIZATION_SUMMARY_KEY, - self::AUTHORIZATION_SUMMARY_KEY_TEST_MODE, - self::CONNECT_INCENTIVE_KEY, - self::TRACKING_INFO_KEY, - ]; - - foreach ( $keys as $key ) { + foreach ( self::ALL_KEYS as $key ) { $this->delete( $key ); } - - // Delete prefix-based keys. - $this->delete_by_prefix( self::PAYMENT_METHODS_KEY_PREFIX ); - $this->delete_by_prefix( self::ONBOARDING_FIELDS_DATA_KEY ); // It can be prefixed with the locale. - $this->delete_by_prefix( self::RECOMMENDED_PAYMENT_METHODS ); // It can be prefixed with the locale. } /** @@ -325,7 +270,7 @@ private function should_refresh_cache( string $key, $cache_contents, callable $v // Do not refresh if doing ajax or the refresh has been disabled (running an AS job). if ( defined( 'DOING_CRON' ) - || ( wp_doing_ajax() && ! in_array( $key, self::AJAX_ALLOWED_KEYS, true ) ) + || ( wp_doing_ajax() ) || $this->refresh_disabled ) { return false; } @@ -484,9 +429,6 @@ private function get_ttl( string $key, array $cache_contents ): int { // If no orders, cache for an hour to check again soon. $ttl = $cache_contents['data'] ? DAY_IN_SECONDS * 90 : HOUR_IN_SECONDS; break; - case self::PAYMENT_PROCESS_FACTORS_KEY: - $ttl = 2 * HOUR_IN_SECONDS; - break; case self::TRACKING_INFO_KEY: $ttl = $cache_contents['errored'] ? 2 * MINUTE_IN_SECONDS : MONTH_IN_SECONDS; break; diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 78780e977c0..c85d2654a92 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -1872,7 +1872,7 @@ public function maybe_process_upe_redirect() { if ( $this->is_setup_intent_success_creation_redirection() ) { wc_add_notice( __( 'Payment method successfully added.', 'woocommerce-payments' ) ); $user = wp_get_current_user(); - $this->customer_service->clear_cached_payment_methods_for_user( $user->ID ); + $this->token_service->clear_cached_payment_methods_for_user( $user->ID ); } return; } diff --git a/includes/class-wc-payments-customer-service.php b/includes/class-wc-payments-customer-service.php index d0f97e061c0..ff1a9ab5a68 100644 --- a/includes/class-wc-payments-customer-service.php +++ b/includes/class-wc-payments-customer-service.php @@ -5,7 +5,6 @@ * @package WooCommerce\Payments */ -use WCPay\Database_Cache; use WCPay\Exceptions\API_Exception; use WCPay\Logger; use WCPay\Constants\Payment_Method; @@ -57,13 +56,6 @@ class WC_Payments_Customer_Service { */ private $account; - /** - * Database_Cache instance to get information about the account - * - * @var Database_Cache - */ - private $database_cache; - /** * WC_Payments_Session_Service instance for working with session information * @@ -83,20 +75,17 @@ class WC_Payments_Customer_Service { * * @param WC_Payments_API_Client $payments_api_client Payments API client. * @param WC_Payments_Account $account WC_Payments_Account instance. - * @param Database_Cache $database_cache Database_Cache instance. * @param WC_Payments_Session_Service $session_service Session Service class instance. * @param WC_Payments_Order_Service $order_service Order Service class instance. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account, - Database_Cache $database_cache, WC_Payments_Session_Service $session_service, WC_Payments_Order_Service $order_service ) { $this->payments_api_client = $payments_api_client; $this->account = $account; - $this->database_cache = $database_cache; $this->session_service = $session_service; $this->order_service = $order_service; } @@ -262,23 +251,8 @@ public function get_payment_methods_for_customer( $customer_id, $type = 'card' ) return []; } - $cache_payment_methods = ! WC_Payments::is_network_saved_cards_enabled(); - $cache_key = Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_' . $type; - - if ( $cache_payment_methods ) { - $payment_methods = $this->database_cache->get( $cache_key ); - if ( is_array( $payment_methods ) ) { - return $payment_methods; - } - } - try { - $payment_methods = $this->payments_api_client->get_payment_methods( $customer_id, $type )['data']; - if ( $cache_payment_methods ) { - $this->database_cache->add( $cache_key, $payment_methods ); - } - return $payment_methods; - + return $this->payments_api_client->get_payment_methods( $customer_id, $type )['data']; } catch ( API_Exception $e ) { // If we failed to find the payment methods, we can simply return empty payment methods as this customer // will be recreated when the user successfully adds a payment method. @@ -310,23 +284,6 @@ public function update_payment_method_with_billing_details_from_order( $payment_ } } - /** - * Clear payment methods cache for a user. - * - * @param int $user_id WC user ID. - */ - public function clear_cached_payment_methods_for_user( $user_id ) { - if ( WC_Payments::is_network_saved_cards_enabled() ) { - return; // No need to do anything, payment methods will never be cached in this case. - } - - $retrievable_payment_method_types = [ Payment_Method::CARD, Payment_Method::LINK, Payment_Method::SEPA ]; - $customer_id = $this->get_customer_id_by_user_id( $user_id ); - foreach ( $retrievable_payment_method_types as $type ) { - $this->database_cache->delete( Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_' . $type ); - } - } - /** * Given a WC_Order or WC_Customer, returns an array representing a Stripe customer object. * At least one parameter has to not be null. @@ -387,15 +344,6 @@ public static function map_customer_data( ?WC_Order $wc_order = null, ?WC_Custom return $data; } - /** - * Delete all saved payment methods that are stored inside the database cache driver. - * - * @return void - */ - public function delete_cached_payment_methods() { - $this->database_cache->delete_by_prefix( Database_Cache::PAYMENT_METHODS_KEY_PREFIX ); - } - /** * Recreates the customer for this user. * diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index 83ded781b16..86262d02066 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -124,23 +124,16 @@ public function init_hooks() { * The data is retrieved from the server and is cached. If we can't retrieve, we will use whatever data we have. * * @param string $locale The locale to use to i18n the data. - * @param bool $force_refresh Forces data to be fetched from the server, rather than using the cache. - * * @return ?array Fields data, or NULL if failed to retrieve. */ - public function get_fields_data( string $locale = '', bool $force_refresh = false ): ?array { + public function get_fields_data( string $locale = '' ): ?array { // If we don't have a server connection, return what data we currently have, regardless of expiry. if ( ! $this->payments_api_client->is_server_connected() ) { return $this->database_cache->get( Database_Cache::ONBOARDING_FIELDS_DATA_KEY, true ); } - $cache_key = Database_Cache::ONBOARDING_FIELDS_DATA_KEY; - if ( ! empty( $locale ) ) { - $cache_key .= '__' . $locale; - } - return $this->database_cache->get_or_add( - $cache_key, + Database_Cache::ONBOARDING_FIELDS_DATA_KEY, function () use ( $locale ) { try { // We will use the language for the current user (defaults to the site language). @@ -150,10 +143,19 @@ function () use ( $locale ) { return null; } + // Store the locale, so if a different one is requested, we can invalidate the cache. + $fields_data['__locale'] = $locale; + return $fields_data; }, - '__return_true', - $force_refresh + function ( $data ) use ( $locale ) { + // The locale used to be part of a dynamic key. If it is not set, the data is old & invalid. + return ( + is_array( $data ) + && isset( $data['__locale'] ) + && $data['__locale'] === $locale + ); + } ); } @@ -167,23 +169,38 @@ function () use ( $locale ) { * NULL on retrieval or validation error. */ public function get_recommended_payment_methods( string $country_code, string $locale = '' ): ?array { - $cache_key = Database_Cache::RECOMMENDED_PAYMENT_METHODS . '__' . $country_code; - if ( ! empty( $locale ) ) { - $cache_key .= '__' . $locale; - } - - return \WC_Payments::get_database_cache()->get_or_add( - $cache_key, + $cached_data = \WC_Payments::get_database_cache()->get_or_add( + Database_Cache::RECOMMENDED_PAYMENT_METHODS, function () use ( $country_code, $locale ) { try { - return $this->payments_api_client->get_recommended_payment_methods( $country_code, $locale ); + $payment_methods = $this->payments_api_client->get_recommended_payment_methods( $country_code, $locale ); + + // Indicate that the cached value is specific for the given locale and country code. + return [ + 'payment_methods' => $payment_methods, + '__locale' => $locale, + '__country_code' => $country_code, + ]; } catch ( API_Exception $e ) { // Return NULL to signal retrieval error. return null; } }, - 'is_array' + function ( $data ) use ( $locale, $country_code ) { + // The locale and country code used to be part of a dynamic key. + // If either is not set, the data is old & invalid. + return ( + is_array( $data ) + && isset( $data['payment_methods'] ) + && isset( $data['__locale'] ) + && isset( $data['__country_code'] ) + && $data['__locale'] === $locale + && $data['__country_code'] === $country_code + ); + } ); + + return $cached_data['payment_methods'] ?? null; } /** @@ -418,17 +435,13 @@ public function finalize_embedded_kyc( string $locale, string $source, array $ac /** * Gets and caches the business types per country from the server. * - * @param bool $force_refresh Forces data to be fetched from the server, rather than using the cache. - * * @return array|bool Business types, or false if failed to retrieve. */ - public function get_cached_business_types( bool $force_refresh = false ) { + public function get_cached_business_types() { if ( ! $this->payments_api_client->is_server_connected() ) { return []; } - $refreshed = false; - $business_types = $this->database_cache->get_or_add( Database_Cache::BUSINESS_TYPES_KEY, function () { @@ -445,9 +458,7 @@ function () { return $business_types; }, - [ $this, 'is_valid_cached_business_types' ], - $force_refresh, - $refreshed + [ $this, 'is_valid_cached_business_types' ] ); if ( null === $business_types ) { @@ -908,12 +919,9 @@ public function cleanup_on_account_reset() { */ public function cleanup_on_account_onboarded() { // Delete the onboarding fields data since it is used only during the initial onboarding. - // Delete it by prefix since it can have entries suffixed with the user locale. - $this->database_cache->delete_by_prefix( Database_Cache::ONBOARDING_FIELDS_DATA_KEY ); - + $this->database_cache->delete( Database_Cache::ONBOARDING_FIELDS_DATA_KEY ); $this->database_cache->delete( Database_Cache::BUSINESS_TYPES_KEY ); - // Delete it by prefix since it can have entries suffixed with the user locale. - $this->database_cache->delete_by_prefix( Database_Cache::RECOMMENDED_PAYMENT_METHODS ); + $this->database_cache->delete( Database_Cache::RECOMMENDED_PAYMENT_METHODS ); } /** diff --git a/includes/class-wc-payments-token-service.php b/includes/class-wc-payments-token-service.php index 283a0d7851a..d49149ebefd 100644 --- a/includes/class-wc-payments-token-service.php +++ b/includes/class-wc-payments-token-service.php @@ -24,6 +24,8 @@ class WC_Payments_Token_Service { Payment_Method::LINK => WC_Payment_Gateway_WCPay::GATEWAY_ID, ]; + const CACHED_PAYMENT_METHODS_META_KEY = '_wcpay_payment_methods'; + /** * Client for making requests to the WooCommerce Payments API * @@ -71,7 +73,7 @@ public function init_hooks() { */ public function add_token_to_user( $payment_method, $user ) { // Clear cached payment methods. - $this->customer_service->clear_cached_payment_methods_for_user( $user->ID ); + $this->clear_cached_payment_methods_for_user( $user->ID ); switch ( $payment_method['type'] ) { case Payment_Method::SEPA: @@ -133,6 +135,46 @@ public function is_valid_payment_method_type_for_gateway( $payment_method_type, return self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ $payment_method_type ] === $gateway_id; } + /** + * Clear payment methods cache for a user. + * + * @param int $user_id WC user ID. + */ + public function clear_cached_payment_methods_for_user( $user_id ) { + if ( WC_Payments::is_network_saved_cards_enabled() ) { + return; // No need to do anything, payment methods will never be cached in this case. + } + + delete_user_meta( $user_id, self::CACHED_PAYMENT_METHODS_META_KEY ); + } + + /** + * Clear all cached payment methods. + * Used when account data is updated and all payment method caches need to be cleared. + */ + public function clear_all_cached_payment_methods() { + global $wpdb; + + if ( WC_Payments::is_network_saved_cards_enabled() ) { + return; // No need to do anything, payment methods will never be cached in this case. + } + + // Tap straight into the database and delete the meta key for all users. + $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE meta_key = %s", self::CACHED_PAYMENT_METHODS_META_KEY ) ); + + /** + * Legacy: Payment methods were stored in the database cache with the `wcpay_pm_` prefix. + * When cleaning up cached payment methods, we need to flush the database from old cached data as well. + * + * This method gets called for account updates. Even though those are rare, they should be a + * good opportunity to clean up old cached data. + */ + $options = $wpdb->get_results( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE 'wcpay_pm_%'" ); + foreach ( $options as $option ) { + delete_option( $option->option_name ); + } + } + /** * Gets saved tokens from API if they don't already exist in WooCommerce. * @@ -168,16 +210,7 @@ public function woocommerce_get_customer_payment_tokens( $tokens, $user_id, $gat } } - $retrievable_payment_method_types = $this->get_retrievable_payment_method_types( $gateway_id ); - - $payment_methods = []; - - foreach ( $retrievable_payment_method_types as $type ) { - $payment_methods[] = $this->customer_service->get_payment_methods_for_customer( $customer_id, $type ); - } - - $payment_methods = array_merge( ...$payment_methods ); - + $payment_methods = $this->get_payment_methods_from_stripe( $user_id, $customer_id, $gateway_id ); } catch ( Exception $e ) { Logger::error( 'Failed to fetch payment methods for customer.' . $e ); return $tokens; @@ -210,6 +243,63 @@ public function woocommerce_get_customer_payment_tokens( $tokens, $user_id, $gat return $tokens; } + /** + * Gets payment methods from Stripe. + * + * @param string $user_id WP user ID. + * @param string $customer_id WC customer ID. + * @param string $gateway_id WC gateway ID. + * @return array Payment methods. + */ + private function get_payment_methods_from_stripe( $user_id, $customer_id, $gateway_id ) { + $cache_key = 'payment_methods_' . $gateway_id; + $cached_data = get_user_meta( $user_id, self::CACHED_PAYMENT_METHODS_META_KEY, true ); + + // Start by checking the customer ID. If it is different, bust the cache. + if ( + is_array( $cached_data ) + && ( + ( isset( $cached_data['customer_id'] ) && $cached_data['customer_id'] !== $customer_id ) + || ! isset( $cached_data['customer_id'] ) + ) + ) { + $cached_data = []; + } + + // Now, if there is cached data, then it is for the right customer. Check for the gateway-specific cache key. + if ( + is_array( $cached_data ) + && isset( $cached_data[ $cache_key ] ) + && is_array( $cached_data[ $cache_key ] ) + ) { + return $cached_data[ $cache_key ]; + } + + // Proceed loading payment methods. + $retrievable_payment_method_types = $this->get_retrievable_payment_method_types( $gateway_id ); + + $payment_methods = []; + foreach ( $retrievable_payment_method_types as $type ) { + $type_methods = $this->customer_service->get_payment_methods_for_customer( $customer_id, $type ); + if ( is_array( $type_methods ) ) { + $payment_methods = array_merge( $payment_methods, $type_methods ); + } + } + + // Cache the payment methods. Combine with existing data in case there are cached PMs for other gateway IDs. + $new_cache = [ + 'customer_id' => $customer_id, + $cache_key => $payment_methods, + ]; + update_user_meta( + $user_id, + self::CACHED_PAYMENT_METHODS_META_KEY, + array_merge( $cached_data ? $cached_data : [], $new_cache ) + ); + + return $payment_methods; + } + /** * Retrieves the payment method types for which tokens should be retrieved. * @@ -310,7 +400,7 @@ public function woocommerce_payment_token_deleted( $token_id, $token ) { try { $this->payments_api_client->detach_payment_method( $token->get_token() ); // Clear cached payment methods. - $this->customer_service->clear_cached_payment_methods_for_user( $token->get_user_id() ); + $this->clear_cached_payment_methods_for_user( $token->get_user_id() ); } catch ( Exception $e ) { Logger::log( 'Error detaching payment method:' . $e->getMessage() ); } @@ -329,7 +419,7 @@ public function woocommerce_payment_token_set_default( $token_id, $token ) { if ( $customer_id ) { $this->customer_service->set_default_payment_method_for_customer( $customer_id, $token->get_token() ); // Clear cached payment methods. - $this->customer_service->clear_cached_payment_methods_for_user( $token->get_user_id() ); + $this->clear_cached_payment_methods_for_user( $token->get_user_id() ); } } } diff --git a/includes/class-wc-payments-webhook-processing-service.php b/includes/class-wc-payments-webhook-processing-service.php index 37218744de6..bcf4bc6bc7e 100644 --- a/includes/class-wc-payments-webhook-processing-service.php +++ b/includes/class-wc-payments-webhook-processing-service.php @@ -74,13 +74,6 @@ class WC_Payments_Webhook_Processing_Service { */ private $wcpay_gateway; - /** - * WC_Payment_Gateway_WCPay - * - * @var WC_Payments_Customer_Service - */ - private $customer_service; - /** * Database_Cache instance. * @@ -95,6 +88,13 @@ class WC_Payments_Webhook_Processing_Service { */ private $onboarding_service; + /** + * WC_Payments_Token_Service instance. + * + * @var WC_Payments_Token_Service + */ + private $token_service; + /** * WC_Payments_Webhook_Processing_Service constructor. * @@ -105,9 +105,9 @@ class WC_Payments_Webhook_Processing_Service { * @param WC_Payments_Order_Service $order_service WC_Payments_Order_Service instance. * @param WC_Payments_In_Person_Payments_Receipts_Service $receipt_service WC_Payments_In_Person_Payments_Receipts_Service instance. * @param WC_Payment_Gateway_WCPay $wcpay_gateway WC_Payment_Gateway_WCPay instance. - * @param WC_Payments_Customer_Service $customer_service WC_Payments_Customer_Service instance. * @param Database_Cache $database_cache Database_Cache instance. * @param WC_Payments_Onboarding_Service $onboarding_service WC_Payments_Onboarding_Service instance. + * @param WC_Payments_Token_Service $token_service WC_Payments_Token_Service instance. */ public function __construct( WC_Payments_API_Client $api_client, @@ -117,9 +117,9 @@ public function __construct( WC_Payments_Order_Service $order_service, WC_Payments_In_Person_Payments_Receipts_Service $receipt_service, WC_Payment_Gateway_WCPay $wcpay_gateway, - WC_Payments_Customer_Service $customer_service, Database_Cache $database_cache, - WC_Payments_Onboarding_Service $onboarding_service + WC_Payments_Onboarding_Service $onboarding_service, + WC_Payments_Token_Service $token_service ) { $this->wcpay_db = $wcpay_db; $this->account = $account; @@ -128,9 +128,9 @@ public function __construct( $this->api_client = $api_client; $this->receipt_service = $receipt_service; $this->wcpay_gateway = $wcpay_gateway; - $this->customer_service = $customer_service; $this->database_cache = $database_cache; $this->onboarding_service = $onboarding_service; + $this->token_service = $token_service; } /** @@ -192,7 +192,7 @@ public function process( array $event_body ) { break; case 'account.updated': $this->account->refresh_account_data(); - $this->customer_service->delete_cached_payment_methods(); + $this->token_service->clear_all_cached_payment_methods(); break; case 'account.deleted': $this->onboarding_service->cleanup_on_account_reset(); @@ -204,6 +204,9 @@ public function process( array $event_body ) { // Refetch the account data to allow the platform to drive the available next steps. $this->account->refresh_account_data(); + + // Use the opportunity to clear cached payment methods. + $this->token_service->clear_all_cached_payment_methods(); break; case 'wcpay.notification': $this->process_wcpay_notification( $event_body ); diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 530a043a2a4..893f2de881a 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -550,7 +550,7 @@ public static function init() { self::$redirect_service = new WC_Payments_Redirect_Service( self::$api_client ); self::$onboarding_service = new WC_Payments_Onboarding_Service( self::$api_client, self::$database_cache, self::$session_service ); self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service, self::$onboarding_service, self::$redirect_service ); - self::$customer_service = new WC_Payments_Customer_Service( self::$api_client, self::$account, self::$database_cache, self::$session_service, self::$order_service ); + self::$customer_service = new WC_Payments_Customer_Service( self::$api_client, self::$account, self::$session_service, self::$order_service ); self::$token_service = new WC_Payments_Token_Service( self::$api_client, self::$customer_service ); self::$remote_note_service = new WC_Payments_Remote_Note_Service( WC_Data_Store::load( 'admin-note' ) ); self::$fraud_service = new WC_Payments_Fraud_Service( self::$api_client, self::$customer_service, self::$account, self::$session_service, self::$database_cache ); @@ -636,10 +636,10 @@ public static function init() { self::$card_gateway->init_hooks(); self::$wc_payments_checkout->init_hooks(); - self::$webhook_processing_service = new WC_Payments_Webhook_Processing_Service( self::$api_client, self::$db_helper, self::$account, self::$remote_note_service, self::$order_service, self::$in_person_payments_receipts_service, self::get_gateway(), self::$customer_service, self::$database_cache, self::$onboarding_service ); + self::$webhook_processing_service = new WC_Payments_Webhook_Processing_Service( self::$api_client, self::$db_helper, self::$account, self::$remote_note_service, self::$order_service, self::$in_person_payments_receipts_service, self::get_gateway(), self::$database_cache, self::$onboarding_service, self::$token_service ); self::$webhook_reliability_service = new WC_Payments_Webhook_Reliability_Service( self::$api_client, self::$action_scheduler_service, self::$webhook_processing_service ); - self::$customer_service_api = new WC_Payments_Customer_Service_API( self::$customer_service ); + self::$customer_service_api = new WC_Payments_Customer_Service_API( self::$customer_service, self::$token_service ); self::$currency_manager = new WC_Payments_Currency_Manager( self::get_gateway() ); self::$currency_manager->init_hooks(); diff --git a/includes/core/service/class-wc-payments-customer-service-api.php b/includes/core/service/class-wc-payments-customer-service-api.php index b8d70deea58..e55ea5c5167 100644 --- a/includes/core/service/class-wc-payments-customer-service-api.php +++ b/includes/core/service/class-wc-payments-customer-service-api.php @@ -12,6 +12,7 @@ use WP_User; use WC_Customer; use WC_Order; +use WC_Payments_Token_Service; defined( 'ABSPATH' ) || exit; @@ -27,13 +28,25 @@ class WC_Payments_Customer_Service_API { */ private $customer_service; + /** + * Internal Token_Service instance to invoke. + * + * @var WC_Payments_Token_Service + */ + private $token_service; + /** * Class constructor * * @param WC_Payments_Customer_Service $customer_service Customer Service instance. + * @param WC_Payments_Token_Service $token_service Token Service instance. */ - public function __construct( WC_Payments_Customer_Service $customer_service ) { + public function __construct( + WC_Payments_Customer_Service $customer_service, + WC_Payments_Token_Service $token_service + ) { $this->customer_service = $customer_service; + $this->token_service = $token_service; } /** @@ -114,7 +127,7 @@ public function update_payment_method_with_billing_details_from_order( $payment_ * @param int $user_id WC user ID. */ public function clear_cached_payment_methods_for_user( $user_id ) { - return $this->customer_service->clear_cached_payment_methods_for_user( $user_id ); + return $this->token_service->clear_cached_payment_methods_for_user( $user_id ); } /** @@ -136,7 +149,7 @@ public static function map_customer_data( WC_Order $wc_order, WC_Customer $wc_cu * @return void */ public function delete_cached_payment_methods() { - $this->customer_service->delete_cached_payment_methods(); + $this->token_service->clear_all_cached_payment_methods(); } /** diff --git a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php index 99854d40ef1..9f8b409dd9b 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php @@ -129,7 +129,7 @@ public function set_up() { $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); $order_service = new WC_Payments_Order_Service( $this->mock_api_client ); - $customer_service = new WC_Payments_Customer_Service( $this->mock_api_client, $this->mock_wcpay_account, $this->mock_cache, $this->mock_session_service, $order_service ); + $customer_service = new WC_Payments_Customer_Service( $this->mock_api_client, $this->mock_wcpay_account, $this->mock_session_service, $order_service ); $token_service = new WC_Payments_Token_Service( $this->mock_api_client, $customer_service ); $compatibility_service = new Compatibility_Service( $this->mock_api_client ); $action_scheduler_service = new WC_Payments_Action_Scheduler_Service( $this->mock_api_client, $order_service, $compatibility_service ); diff --git a/tests/unit/admin/test-class-wc-rest-payments-tos-controller.php b/tests/unit/admin/test-class-wc-rest-payments-tos-controller.php index 08b6d5a0f4f..9fc5da21581 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-tos-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-tos-controller.php @@ -62,7 +62,7 @@ public function set_up() { $mock_db_cache = $this->createMock( Database_Cache::class ); $mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); $order_service = new WC_Payments_Order_Service( $this->createMock( WC_Payments_API_Client::class ) ); - $customer_service = new WC_Payments_Customer_Service( $mock_api_client, $mock_wcpay_account, $mock_db_cache, $mock_session_service, $order_service ); + $customer_service = new WC_Payments_Customer_Service( $mock_api_client, $mock_wcpay_account, $mock_session_service, $order_service ); $token_service = new WC_Payments_Token_Service( $mock_api_client, $customer_service ); $mock_compatibility_service = $this->createMock( Compatibility_Service::class ); $action_scheduler_service = new WC_Payments_Action_Scheduler_Service( $mock_api_client, $order_service, $mock_compatibility_service ); diff --git a/tests/unit/core/service/test-class-wc-payments-customer-service-api.php b/tests/unit/core/service/test-class-wc-payments-customer-service-api.php index 3e7e515187e..5b70a212fa7 100644 --- a/tests/unit/core/service/test-class-wc-payments-customer-service-api.php +++ b/tests/unit/core/service/test-class-wc-payments-customer-service-api.php @@ -66,8 +66,8 @@ public function set_up() { 'wc_payments_http', [ $this, 'replace_http_client' ] ); - $this->customer_service = new WC_Payments_Customer_Service( WC_Payments::create_api_client(), WC_Payments::get_account_service(), WC_Payments::get_database_cache(), WC_Payments::get_session_service(), WC_Payments::get_order_service() ); - $this->customer_service_api = new WC_Payments_Customer_Service_API( $this->customer_service ); + $this->customer_service = new WC_Payments_Customer_Service( WC_Payments::create_api_client(), WC_Payments::get_account_service(), WC_Payments::get_session_service(), WC_Payments::get_order_service() ); + $this->customer_service_api = new WC_Payments_Customer_Service_API( $this->customer_service, WC_Payments::get_token_service() ); } /** @@ -80,7 +80,7 @@ public function tear_down() { ); // Clear the cache after each test. - $this->customer_service->delete_cached_payment_methods(); + $this->customer_service_api->delete_cached_payment_methods(); parent::tear_down(); } @@ -415,57 +415,6 @@ function ( $data ): bool { $this->customer_service_api->update_payment_method_with_billing_details_from_order( 'pm_mock', $order ); } - - /** - * Test clearing cached payment methods. - * - * @return void - */ - public function test_clear_cached_payment_methods_for_user() { - // get payment methods for a customer so that it gets cached. - $mock_payment_methods = [ - [ 'id' => 'pm_mock1' ], - [ 'id' => 'pm_mock2' ], - ]; - $this->mock_http_client - ->expects( $this->exactly( 1 ) ) - ->method( 'remote_request' ) - ->with( - $this->callback( - function ( $data ): bool { - $this->assertSame( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/payment_methods?test_mode=0&customer=cus_123&type=card&limit=100', $data['url'] ); - $this->assertSame( 'GET', $data['method'] ); - return true; - } - ) - )->willReturn( - [ - 'body' => wp_json_encode( [ 'data' => $mock_payment_methods ] ), - 'response' => [ - 'code' => 200, - 'message' => 'OK', - ], - ] - ); - $response = $this->customer_service_api->get_payment_methods_for_customer( 'cus_123' ); - $this->assertEquals( $mock_payment_methods, $response ); - - // check if we can retrieve from cache. - $db_cache = WC_Payments::get_database_cache(); - $cache_response = $db_cache->get( Database_Cache::PAYMENT_METHODS_KEY_PREFIX . 'cus_123_card' ); - $this->assertEquals( $mock_payment_methods, $cache_response ); - - // set up the user for customer. - update_user_option( 1, self::CUSTOMER_LIVE_META_KEY, 'cus_123' ); - - // run the method. - $this->customer_service_api->clear_cached_payment_methods_for_user( 1 ); - - // check that cache is empty. - $cache_response = $db_cache->get( Database_Cache::PAYMENT_METHODS_KEY_PREFIX . 'cus_123_card' ); - $this->assertEquals( null, $cache_response ); - } - /** * Get mock customer data. * diff --git a/tests/unit/test-class-database-cache.php b/tests/unit/test-class-database-cache.php index 4b23eece139..70ceb8ce104 100644 --- a/tests/unit/test-class-database-cache.php +++ b/tests/unit/test-class-database-cache.php @@ -542,27 +542,6 @@ function () use ( $value ) { $this->assert_cache_contains( $old ); } - public function test_delete_cache_by_prefix_will_not_delete_values_that_are_not_cache_keys() { - $cache_value = 'foo'; - $this->write_mock_cache( $cache_value, time() + YEAR_IN_SECONDS ); - - $this->database_cache->delete_by_prefix( self::MOCK_KEY ); - - $this->assert_cache_contains( $cache_value ); - } - - public function test_delete_cache_by_prefix_will_delete_cached_data_that_starts_with_prefix() { - $payment_method_cache_key_one = Database_Cache::PAYMENT_METHODS_KEY_PREFIX . '1'; - $payment_method_cache_key_two = Database_Cache::PAYMENT_METHODS_KEY_PREFIX . '2'; - $this->database_cache->add( $payment_method_cache_key_one, 'foo' ); - $this->database_cache->add( $payment_method_cache_key_two, 'bar' ); - - $this->database_cache->delete_by_prefix( Database_Cache::PAYMENT_METHODS_KEY_PREFIX ); - - $this->assertNull( $this->database_cache->get( $payment_method_cache_key_one ) ); - $this->assertNull( $this->database_cache->get( $payment_method_cache_key_two ) ); - } - private function write_mock_cache( $data, ?int $fetch_time = null, bool $errored = false ) { update_option( self::MOCK_KEY, diff --git a/tests/unit/test-class-wc-payments-customer-service.php b/tests/unit/test-class-wc-payments-customer-service.php index 54180bb22dc..1adc45122f8 100644 --- a/tests/unit/test-class-wc-payments-customer-service.php +++ b/tests/unit/test-class-wc-payments-customer-service.php @@ -39,13 +39,6 @@ class WC_Payments_Customer_Service_Test extends WCPAY_UnitTestCase { */ private $mock_account; - /** - * Mock Database_Cache. - * - * @var Database_Cache|MockObject - */ - private $mock_db_cache; - /** * Mock WC_Payments_Session_Service. * @@ -68,10 +61,9 @@ public function set_up() { $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); $this->mock_account = $this->createMock( WC_Payments_Account::class ); - $this->mock_db_cache = $this->createMock( Database_Cache::class ); $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class ); - $this->customer_service = new WC_Payments_Customer_Service( $this->mock_api_client, $this->mock_account, $this->mock_db_cache, $this->mock_session_service, WC_Payments::get_order_service() ); + $this->customer_service = new WC_Payments_Customer_Service( $this->mock_api_client, $this->mock_account, $this->mock_session_service, WC_Payments::get_order_service() ); } /** @@ -440,39 +432,6 @@ public function test_get_payment_methods_for_customer_fetches_from_api() { $this->assertEquals( $mock_payment_methods, $response ); } - public function test_get_payment_methods_for_customer_fetches_from_database_cache() { - $mock_payment_methods = [ - [ 'id' => 'pm_mock1' ], - [ 'id' => 'pm_mock2' ], - ]; - $customer_id = 'cus_12345'; - $payment_method_name = 'card'; - $cache_key = Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_' . $payment_method_name; - - $this->mock_api_client - ->expects( $this->once() ) - ->method( 'get_payment_methods' ) - ->with( $customer_id, $payment_method_name ) - ->willReturn( [ 'data' => $mock_payment_methods ] ); - - $this->mock_db_cache - ->expects( $this->exactly( 2 ) ) - ->method( 'get' ) - ->withConsecutive( [ $cache_key ], [ $cache_key ] ) - ->willReturnOnConsecutiveCalls( null, $mock_payment_methods ); - - $this->mock_db_cache - ->expects( $this->once() ) - ->method( 'add' ) - ->with( $cache_key, $mock_payment_methods ); - - $response = $this->customer_service->get_payment_methods_for_customer( $customer_id ); - $this->assertEquals( $mock_payment_methods, $response ); - - $response = $this->customer_service->get_payment_methods_for_customer( $customer_id ); - $this->assertEquals( $mock_payment_methods, $response ); - } - public function test_get_payment_methods_for_customer_no_customer() { $this->mock_api_client ->expects( $this->never() ) @@ -735,23 +694,4 @@ public function test_get_customer_id_for_order() { $this->assertEquals( $this->customer_service->get_customer_id_for_order( $order ), 'wcpay_cus_test12345' ); } - - public function test_clear_cached_payment_methods_for_user() { - update_user_option( 1, self::CUSTOMER_LIVE_META_KEY, 'cus_test12345' ); - $customer_id = $this->customer_service->get_customer_id_by_user_id( 1 ); - - $expected_card_cache_key = Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_card'; - $expected_link_cache_key = Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_link'; - $expected_sepa_cache_key = Database_Cache::PAYMENT_METHODS_KEY_PREFIX . $customer_id . '_sepa_debit'; - - $this->mock_db_cache - ->expects( $this->exactly( 3 ) ) - ->method( 'delete' ) - ->withConsecutive( - [ $expected_card_cache_key ], - [ $expected_link_cache_key ], - [ $expected_sepa_cache_key ] - ); - $this->customer_service->clear_cached_payment_methods_for_user( 1 ); - } } diff --git a/tests/unit/test-class-wc-payments-token-service.php b/tests/unit/test-class-wc-payments-token-service.php index 3345964a002..09e53fe191c 100644 --- a/tests/unit/test-class-wc-payments-token-service.php +++ b/tests/unit/test-class-wc-payments-token-service.php @@ -44,12 +44,19 @@ class WC_Payments_Token_Service_Test extends WCPAY_UnitTestCase { */ protected $mock_cache; + /** + * @var WC_Payment_Gateway_WCPay + */ + private $original_gateway; + /** * Pre-test setup */ public function set_up() { parent::set_up(); + $this->original_gateway = WC_Payments::get_gateway(); + $this->user_id = get_current_user_id(); wp_set_current_user( 1 ); @@ -70,6 +77,7 @@ public function tear_down() { wp_set_current_user( $this->user_id ); // Restore the cache service in the main class. WC_Payments::set_database_cache( $this->_cache ); + WC_Payments::set_gateway( $this->original_gateway ); parent::tear_down(); } @@ -799,4 +807,280 @@ private function generate_link_token( $stripe_id, $wp_id = 0 ) { $token->save(); return $token; } + + /** + * Test clear_cached_payment_methods_for_user method. + */ + public function test_clear_cached_payment_methods_for_user() { + $user_id = 1; + $cache_key = '_wcpay_payment_methods'; + $cached_data = [ + 'customer_id' => 'cus_12345', + 'payment_methods_woocommerce_payments' => [ + $this->generate_card_pm_response( 'pm_test1' ), + $this->generate_card_pm_response( 'pm_test2' ), + ], + ]; + + // Add cached data to user meta. + update_user_meta( $user_id, $cache_key, $cached_data ); + + // Clear cached payment methods for user. + $this->token_service->clear_cached_payment_methods_for_user( $user_id ); + + // Verify cached data is cleared. + $this->assertEmpty( get_user_meta( $user_id, $cache_key, true ) ); + } + + public function provider_clearing_with_network_saved_cards_enabled(): array { + return [ + 'clear_cached_payment_methods_for_user' => [ + 'user_id' => 1, // specific user. + ], + 'clear_all_cached_payment_methods' => [ + 'user_id' => null, // all users. + ], + ]; + } + + /** + * Test `clear_cached_payment_methods_for_user` and `clear_all_cached_payment_methods` with network saved cards enabled. + * + * @dataProvider provider_clearing_with_network_saved_cards_enabled + * @param int|null $user_id The user ID. + */ + public function test_clearing_with_network_saved_cards_enabled( ?int $user_id = null ) { + $user_id = 1; + $cached_data = [ + 'customer_id' => 'cus_12345', + 'payment_methods_woocommerce_payments' => [ + $this->generate_card_pm_response( 'pm_test1' ), + ], + ]; + + // Add cached data to user meta. + update_user_meta( $user_id, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, $cached_data ); + + // Mock network saved cards enabled using the filter. + add_filter( 'wcpay_force_network_saved_cards', '__return_true' ); + + if ( $user_id > 0 ) { + // Clear cached payment methods for user. + $this->token_service->clear_cached_payment_methods_for_user( $user_id ); + } else { + // Clear all cached payment methods for all users. + $this->token_service->clear_all_cached_payment_methods(); + } + + // Verify cached data still exists (should not be cleared when network saved cards is enabled). + $this->assertEquals( $cached_data, get_user_meta( $user_id, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, true ) ); + + // Clean up the filter. + remove_filter( 'wcpay_force_network_saved_cards', '__return_true' ); + } + + /** + * Test clear_all_cached_payment_methods method. + */ + public function test_clear_all_cached_payment_methods() { + $user_id_1 = 1; + $user_id_2 = 2; + $cached_data_1 = [ + 'customer_id' => 'cus_12345', + 'payment_methods_woocommerce_payments' => [ + $this->generate_card_pm_response( 'pm_test1' ), + ], + ]; + $cached_data_2 = [ + 'customer_id' => 'cus_67890', + 'payment_methods_woocommerce_payments' => [ + $this->generate_card_pm_response( 'pm_test2' ), + ], + ]; + + // Add cached data to multiple users. + update_user_meta( $user_id_1, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, $cached_data_1 ); + update_user_meta( $user_id_2, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, $cached_data_2 ); + + // Add some legacy cached data in options table. + update_option( 'wcpay_pm_legacy_1', 'legacy_data_1' ); + update_option( 'wcpay_pm_legacy_2', 'legacy_data_2' ); + update_option( 'wcpay_other_option', 'should_not_be_deleted' ); + + // Clear all cached payment methods. + $this->token_service->clear_all_cached_payment_methods(); + + // Clear WordPress object cache to ensure we get fresh data from database. + wp_cache_flush(); + + // Verify all cached data is cleared. + $this->assertEmpty( get_user_meta( $user_id_1, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, true ) ); + $this->assertEmpty( get_user_meta( $user_id_2, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, true ) ); + $this->assertFalse( get_option( 'wcpay_pm_legacy_1' ) ); + $this->assertFalse( get_option( 'wcpay_pm_legacy_2' ) ); + // Verify non-payment method options are not deleted. + $this->assertEquals( 'should_not_be_deleted', get_option( 'wcpay_other_option' ) ); + } + + /** + * Test get_payment_methods_from_stripe method with cached data. + */ + public function test_get_payment_methods_from_stripe_with_cached_data() { + $user_id = 1; + $customer_id = 'cus_12345'; + $gateway_id = 'woocommerce_payments'; + $cached_payment_methods = [ + $this->generate_card_pm_response( 'pm_cached1' ), + $this->generate_card_pm_response( 'pm_cached2' ), + ]; + $cached_data = [ + 'customer_id' => $customer_id, + 'payment_methods_' . $gateway_id => $cached_payment_methods, + ]; + + // Add cached data to user meta. + update_user_meta( $user_id, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, $cached_data ); + + // Verify customer service is not called (since we're using cached data). + $this->mock_customer_service + ->expects( $this->never() ) + ->method( 'get_payment_methods_for_customer' ); + + $result = $this->call_sut_method( 'get_payment_methods_from_stripe', $user_id, $customer_id, $gateway_id ); + + // Verify cached data is returned. + $this->assertEquals( $cached_payment_methods, $result ); + } + + /** + * Test get_payment_methods_from_stripe method with different customer ID (cache miss). + */ + public function test_get_payment_methods_from_stripe_with_different_customer_id() { + $user_id = 1; + $customer_id = 'cus_12345'; + $new_customer_id = 'cus_67890'; + $gateway_id = 'woocommerce_payments'; + $cached_payment_methods = [ + $this->generate_card_pm_response( 'pm_cached1' ), + ]; + $cached_data = [ + 'customer_id' => $customer_id, + 'payment_methods_' . $gateway_id => $cached_payment_methods, + ]; + + // Add cached data with different customer ID. + update_user_meta( $user_id, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, $cached_data ); + + $new_payment_methods = [ + $this->generate_card_pm_response( 'pm_new1' ), + $this->generate_card_pm_response( 'pm_new2' ), + ]; + + // Mock gateway to return enabled payment methods. + $mock_gateway = $this->createMock( WC_Payment_Gateway_WCPay::class ); + $mock_gateway->method( 'get_upe_enabled_payment_method_ids' )->willReturn( [ Payment_Method::CARD ] ); + WC_Payments::set_gateway( $mock_gateway ); + + // Mock customer service to return new payment methods. + $this->mock_customer_service + ->expects( $this->once() ) + ->method( 'get_payment_methods_for_customer' ) + ->with( $new_customer_id, Payment_Method::CARD ) + ->willReturn( $new_payment_methods ); + + $result = $this->call_sut_method( 'get_payment_methods_from_stripe', $user_id, $new_customer_id, $gateway_id ); + + // Verify new payment methods are returned. + $this->assertEquals( $new_payment_methods, $result ); + + // Verify cache is updated with new customer ID and payment methods. + $updated_cache = get_user_meta( $user_id, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, true ); + $this->assertEquals( $new_customer_id, $updated_cache['customer_id'] ); + $this->assertEquals( $new_payment_methods, $updated_cache[ 'payment_methods_' . $gateway_id ] ); + } + + /** + * Test get_payment_methods_from_stripe method with no cached data. + */ + public function test_get_payment_methods_from_stripe_with_no_cached_data() { + $user_id = 1; + $customer_id = 'cus_12345'; + $gateway_id = 'woocommerce_payments'; + $payment_methods = [ + $this->generate_card_pm_response( 'pm_new1' ), + $this->generate_link_pm_response( 'pm_new2' ), + ]; + + // Mock gateway to return enabled payment methods. + $mock_gateway = $this->createMock( WC_Payment_Gateway_WCPay::class ); + $mock_gateway->method( 'get_upe_enabled_payment_method_ids' )->willReturn( [ Payment_Method::CARD, Payment_Method::LINK ] ); + WC_Payments::set_gateway( $mock_gateway ); + + // Mock customer service to return payment methods. + $this->mock_customer_service + ->expects( $this->exactly( 2 ) ) + ->method( 'get_payment_methods_for_customer' ) + ->withConsecutive( + [ $customer_id, Payment_Method::CARD ], + [ $customer_id, Payment_Method::LINK ] + ) + ->willReturnOnConsecutiveCalls( + [ $payment_methods[0] ], + [ $payment_methods[1] ] + ); + + $result = $this->call_sut_method( 'get_payment_methods_from_stripe', $user_id, $customer_id, $gateway_id ); + + // Verify payment methods are returned. + $this->assertEquals( $payment_methods, $result ); + + // Verify cache is created with payment methods. + $cached_data = get_user_meta( $user_id, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, true ); + $this->assertEquals( $customer_id, $cached_data['customer_id'] ); + $this->assertEquals( $payment_methods, $cached_data[ 'payment_methods_' . $gateway_id ] ); + } + + /** + * Test get_payment_methods_from_stripe method with SEPA gateway. + */ + public function test_get_payment_methods_from_stripe_with_sepa_gateway() { + $user_id = 1; + $customer_id = 'cus_12345'; + $gateway_id = 'woocommerce_payments_sepa_debit'; + $payment_methods = [ + $this->generate_sepa_pm_response( 'pm_sepa1' ), + ]; + + // Mock gateway to return enabled payment methods. + $mock_gateway = $this->createMock( WC_Payment_Gateway_WCPay::class ); + $mock_gateway->method( 'get_upe_enabled_payment_method_ids' )->willReturn( [ Payment_Method::SEPA ] ); + WC_Payments::set_gateway( $mock_gateway ); + + // Mock customer service to return SEPA payment methods. + $this->mock_customer_service + ->expects( $this->once() ) + ->method( 'get_payment_methods_for_customer' ) + ->with( $customer_id, Payment_Method::SEPA ) + ->willReturn( $payment_methods ); + + $result = $this->call_sut_method( 'get_payment_methods_from_stripe', $user_id, $customer_id, $gateway_id ); + + // Verify SEPA payment methods are returned. + $this->assertEquals( $payment_methods, $result ); + + // Verify cache is created with correct gateway key. + $cached_data = get_user_meta( $user_id, WC_Payments_Token_Service::CACHED_PAYMENT_METHODS_META_KEY, true ); + $this->assertEquals( $customer_id, $cached_data['customer_id'] ); + $this->assertEquals( $payment_methods, $cached_data[ 'payment_methods_' . $gateway_id ] ); + } + + private function call_sut_method( $method_name, $user_id, $customer_id, $gateway_id ) { + // Use reflection to access private method. + $reflection = new ReflectionClass( $this->token_service ); + $method = $reflection->getMethod( $method_name ); + $method->setAccessible( true ); + + // Call the private method. + return $method->invoke( $this->token_service, $user_id, $customer_id, $gateway_id ); + } } diff --git a/tests/unit/test-class-wc-payments-webhook-processing-service.php b/tests/unit/test-class-wc-payments-webhook-processing-service.php index b2df16065c9..6290c7cd919 100644 --- a/tests/unit/test-class-wc-payments-webhook-processing-service.php +++ b/tests/unit/test-class-wc-payments-webhook-processing-service.php @@ -82,6 +82,13 @@ class WC_Payments_Webhook_Processing_Service_Test extends WCPAY_UnitTestCase { */ private $mock_onboarding_service; + /** + * Mock token service. + * + * @var WC_Payments_Token_Service&MockObject + */ + private $mock_token_service; + /** * @var array */ @@ -139,6 +146,8 @@ public function set_up() { $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class ); + $this->mock_token_service = $this->createMock( WC_Payments_Token_Service::class ); + $this->webhook_processing_service = new WC_Payments_Webhook_Processing_Service( $this->mock_api_client, $this->mock_db_wrapper, @@ -147,9 +156,9 @@ public function set_up() { $this->order_service, $this->mock_receipt_service, $this->mock_wcpay_gateway, - $this->mock_customer_service, $this->mock_database_cache, - $this->mock_onboarding_service + $this->mock_onboarding_service, + $this->mock_token_service ); // Build the event body data. @@ -2161,9 +2170,9 @@ public function test_account_updated_webhook() { ->expects( $this->once() ) ->method( 'refresh_account_data' ); - $this->mock_customer_service + $this->mock_token_service ->expects( $this->once() ) - ->method( 'delete_cached_payment_methods' ); + ->method( 'clear_all_cached_payment_methods' ); // Create webhook processing service with the mocked account. $webhook_processing_service = new WC_Payments_Webhook_Processing_Service( @@ -2174,9 +2183,9 @@ public function test_account_updated_webhook() { $this->order_service, $this->mock_receipt_service, $this->mock_wcpay_gateway, - $this->mock_customer_service, $this->mock_database_cache, - $this->mock_onboarding_service + $this->mock_onboarding_service, + $this->mock_token_service ); // Run the test. @@ -2218,9 +2227,9 @@ public function test_account_deleted_webhook() { $this->order_service, $this->mock_receipt_service, $this->mock_wcpay_gateway, - $this->mock_customer_service, $this->mock_database_cache, - $this->mock_onboarding_service + $this->mock_onboarding_service, + $this->mock_token_service ); // Run the test.