diff --git a/changelog.txt b/changelog.txt index 32d0e1a6e6..1ba015bcca 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,7 @@ *** Changelog *** = 9.1.0 - xxxx-xx-xx = +* Fix - Prevents duplicated credit cards to be added to the customer's account through the My Account page, the shortcode checkout and the block checkout. * Fix - Return to the correct page when redirect-based payment method fails. * Fix - Show default recipient for Payment Authentication Requested email. * Fix - Correctly handles IPP failed payments webhook calls by extracting the order ID from the payment intent metadata. diff --git a/includes/class-wc-stripe-customer.php b/includes/class-wc-stripe-customer.php index 17e20b30cc..69f8b14ae5 100644 --- a/includes/class-wc-stripe-customer.php +++ b/includes/class-wc-stripe-customer.php @@ -391,28 +391,31 @@ public function add_source( $source_id ) { $wc_token->set_token( $response->id ); $wc_token->set_gateway_id( 'stripe_sepa' ); $wc_token->set_last4( $response->sepa_debit->last4 ); + $wc_token->set_fingerprint( $response->sepa_debit->fingerprint ); break; default: if ( WC_Stripe_Helper::is_card_payment_method( $response ) ) { - $wc_token = new WC_Payment_Token_CC(); + $wc_token = new WC_Stripe_Payment_Token_CC(); $wc_token->set_token( $response->id ); $wc_token->set_gateway_id( 'stripe' ); $wc_token->set_card_type( strtolower( $response->card->brand ) ); $wc_token->set_last4( $response->card->last4 ); $wc_token->set_expiry_month( $response->card->exp_month ); $wc_token->set_expiry_year( $response->card->exp_year ); + $wc_token->set_fingerprint( $response->card->fingerprint ); } break; } } else { // Legacy. - $wc_token = new WC_Payment_Token_CC(); + $wc_token = new WC_Stripe_Payment_Token_CC(); $wc_token->set_token( $response->id ); $wc_token->set_gateway_id( 'stripe' ); $wc_token->set_card_type( strtolower( $response->brand ) ); $wc_token->set_last4( $response->last4 ); $wc_token->set_expiry_month( $response->exp_month ); $wc_token->set_expiry_year( $response->exp_year ); + $wc_token->set_fingerprint( $response->fingerprint ); } $wc_token->set_user_id( $this->get_user_id() ); diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 1d89c30b8c..01c4c39caa 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -1474,7 +1474,7 @@ public function is_payment_needed( $order_id = null ) { // Check if the cart contains a pre-order product. Ignore the cart if we're on the Pay for Order page. if ( $this->is_pre_order_item_in_cart() && ! $is_pay_for_order_page ) { - $pre_order_product = $this->get_pre_order_product_from_cart(); + $pre_order_product = $this->get_pre_order_product_from_cart(); // Only one pre-order product is allowed per cart, // so we can return if it's charged upfront. @@ -1802,7 +1802,7 @@ private function is_setup_intent_success_creation_redirection() { * @param string $setup_intent_id ID of the setup intent. * @param WP_User $user User to add token to. * - * @return WC_Payment_Token_CC|WC_Payment_Token_WCPay_SEPA The added token. + * @return WC_Payment_Token The added token. * * @since 5.8.0 * @version 5.8.0 @@ -2216,8 +2216,16 @@ protected function handle_saving_payment_method( WC_Order $order, $payment_metho $payment_method_instance = $this->payment_methods[ $payment_method_type ]; } - // Create a payment token for the user in the store. - $payment_method_instance->create_payment_token_for_user( $user->ID, $payment_method_object ); + // Searches for an existing duplicate token to update. + $found_token = WC_Stripe_Payment_Tokens::get_duplicate_token( $payment_method_object, $customer->get_user_id(), $this->id ); + + if ( $found_token ) { + // Update the token with the new payment method ID. + $payment_method_instance->update_payment_token( $found_token, $payment_method_object->id ); + } else { + // Create a payment token for the user in the store. + $payment_method_instance->create_payment_token_for_user( $user->ID, $payment_method_object ); + } // Add the payment method information to the order. $prepared_payment_method_object = $this->prepare_payment_method( $payment_method_object ); @@ -2363,7 +2371,16 @@ public function add_payment_method() { $customer = new WC_Stripe_Customer( $user->ID ); $customer->clear_cache(); - $token = $payment_method->create_payment_token_for_user( $user->ID, $payment_method_object ); + // Check if a token with the same payment method details exist. If so, just updates the payment method ID and return. + $found_token = WC_Stripe_Payment_Tokens::get_duplicate_token( $payment_method_object, $user->ID, $this->id ); + + // If we have a token found, update it and return. + if ( $found_token ) { + $token = $payment_method->update_payment_token( $found_token, $payment_method_object->id ); + } else { + // Create a new token if not. + $token = $payment_method->create_payment_token_for_user( $user->ID, $payment_method_object ); + } if ( ! is_a( $token, 'WC_Payment_Token' ) ) { throw new WC_Stripe_Exception( sprintf( 'New payment token is not an instance of WC_Payment_Token. Token: %s.', print_r( $token, true ) ) ); diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php index ec95c75da9..22a034bcc1 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php @@ -65,10 +65,10 @@ public function get_retrievable_type() { * @param string $user_id WP_User ID * @param object $payment_method Stripe payment method object * - * @return WC_Payment_Token_CC + * @return WC_Stripe_Payment_Token_CC */ public function create_payment_token_for_user( $user_id, $payment_method ) { - $token = new WC_Payment_Token_CC(); + $token = new WC_Stripe_Payment_Token_CC(); $token->set_expiry_month( $payment_method->card->exp_month ); $token->set_expiry_year( $payment_method->card->exp_year ); $token->set_card_type( strtolower( $payment_method->card->display_brand ?? $payment_method->card->networks->preferred ?? $payment_method->card->brand ) ); @@ -76,6 +76,7 @@ public function create_payment_token_for_user( $user_id, $payment_method ) { $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID ); $token->set_token( $payment_method->id ); $token->set_user_id( $user_id ); + $token->set_fingerprint( $payment_method->card->fingerprint ); $token->save(); return $token; } diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php index d10a795cc9..c26f7f0a59 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -374,6 +374,20 @@ public function create_payment_token_for_user( $user_id, $payment_method ) { $token->set_token( $payment_method->id ); $token->set_payment_method_type( $this->get_id() ); $token->set_user_id( $user_id ); + $token->set_fingerprint( $payment_method->sepa_debit->fingerprint ); + $token->save(); + return $token; + } + + /** + * Updates a payment token. + * + * @param WC_Payment_Token $token The token to update. + * @param string $payment_method_id The new payment method ID. + * @return WC_Payment_Token + */ + public function update_payment_token( $token, $payment_method_id ) { + $token->set_token( $payment_method_id ); $token->save(); return $token; } diff --git a/includes/class-wc-stripe-cash-app-pay-token.php b/includes/payment-tokens/class-wc-stripe-cash-app-payment-token.php similarity index 76% rename from includes/class-wc-stripe-cash-app-pay-token.php rename to includes/payment-tokens/class-wc-stripe-cash-app-payment-token.php index e4066b4bf0..afd79eb5ef 100644 --- a/includes/class-wc-stripe-cash-app-pay-token.php +++ b/includes/payment-tokens/class-wc-stripe-cash-app-payment-token.php @@ -13,8 +13,7 @@ // Exit if accessed directly defined( 'ABSPATH' ) || exit; -class WC_Payment_Token_CashApp extends WC_Payment_Token { - +class WC_Payment_Token_CashApp extends WC_Payment_Token implements WC_Stripe_Payment_Method_Comparison_Interface { /** * Token Type. * @@ -62,6 +61,20 @@ public function get_cashtag() { return $this->get_prop( 'cashtag' ); } + /** + * Checks if the payment method token is equal a provided payment method. + * + * @inheritDoc + */ + public function is_equal_payment_method( $payment_method ): bool { + if ( WC_Stripe_Payment_Methods::CASHAPP_PAY === $this->get_type() + && ( $payment_method->cashapp->cashtag ?? null ) === $this->get_cashtag() ) { + return true; + } + + return false; + } + /** * Returns this token's hook prefix. * diff --git a/includes/payment-tokens/class-wc-stripe-cc-payment-token.php b/includes/payment-tokens/class-wc-stripe-cc-payment-token.php new file mode 100644 index 0000000000..f22661101f --- /dev/null +++ b/includes/payment-tokens/class-wc-stripe-cc-payment-token.php @@ -0,0 +1,45 @@ +extra_data['fingerprint'] = ''; + + parent::__construct( $token ); + } + + /** + * Checks if the payment method token is equal a provided payment method. + * + * @inheritDoc + */ + public function is_equal_payment_method( $payment_method ): bool { + if ( WC_Stripe_Payment_Methods::CARD === $payment_method->type + && ( $payment_method->card->fingerprint ?? null ) === $this->get_fingerprint() ) { + return true; + } + + return false; + } +} diff --git a/includes/class-wc-stripe-link-payment-token.php b/includes/payment-tokens/class-wc-stripe-link-payment-token.php similarity index 80% rename from includes/class-wc-stripe-link-payment-token.php rename to includes/payment-tokens/class-wc-stripe-link-payment-token.php index 3d6bcd6c4e..c1aada00e8 100644 --- a/includes/class-wc-stripe-link-payment-token.php +++ b/includes/payment-tokens/class-wc-stripe-link-payment-token.php @@ -13,8 +13,7 @@ * * @class WC_Payment_Token_Link */ -class WC_Payment_Token_Link extends WC_Payment_Token { - +class WC_Payment_Token_Link extends WC_Payment_Token implements WC_Stripe_Payment_Method_Comparison_Interface { /** * Stores payment type. * @@ -92,4 +91,18 @@ public function get_email( $context = 'view' ) { public function set_email( $email ) { $this->set_prop( 'email', $email ); } + + /** + * Checks if the payment method token is equal a provided payment method. + * + * @inheritDoc + */ + public function is_equal_payment_method( $payment_method ): bool { + if ( WC_Stripe_Payment_Methods::LINK === $payment_method->type + && ( $payment_method->link->email ?? null ) === $this->get_email() ) { + return true; + } + + return false; + } } diff --git a/includes/class-wc-stripe-payment-tokens.php b/includes/payment-tokens/class-wc-stripe-payment-tokens.php similarity index 90% rename from includes/class-wc-stripe-payment-tokens.php rename to includes/payment-tokens/class-wc-stripe-payment-tokens.php index f73620caff..2369ae9938 100644 --- a/includes/class-wc-stripe-payment-tokens.php +++ b/includes/payment-tokens/class-wc-stripe-payment-tokens.php @@ -42,6 +42,7 @@ public function __construct() { add_filter( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 ); add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'get_account_saved_payment_methods_list_item' ], 10, 2 ); add_filter( 'woocommerce_get_credit_card_type_label', [ $this, 'normalize_sepa_label' ] ); + add_filter( 'woocommerce_payment_token_class', [ $this, 'woocommerce_payment_token_class' ], 10, 2 ); add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 ); add_action( 'woocommerce_payment_token_set_default', [ $this, 'woocommerce_payment_token_set_default' ] ); } @@ -173,7 +174,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom foreach ( $stripe_sources as $source ) { if ( isset( $source->type ) && WC_Stripe_Payment_Methods::CARD === $source->type ) { if ( ! isset( $stored_tokens[ $source->id ] ) ) { - $token = new WC_Payment_Token_CC(); + $token = new WC_Stripe_Payment_Token_CC(); $token->set_token( $source->id ); $token->set_gateway_id( WC_Gateway_Stripe::ID ); @@ -184,6 +185,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom $token->set_expiry_year( $source->card->exp_year ); } + $token->set_fingerprint( $source->fingerprint ); $token->set_user_id( $customer_id ); $token->save(); $tokens[ $token->get_id() ] = $token; @@ -192,7 +194,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom } } else { if ( ! isset( $stored_tokens[ $source->id ] ) && WC_Stripe_Payment_Methods::CARD === $source->object ) { - $token = new WC_Payment_Token_CC(); + $token = new WC_Stripe_Payment_Token_CC(); $token->set_token( $source->id ); $token->set_gateway_id( WC_Gateway_Stripe::ID ); $token->set_card_type( strtolower( $source->brand ) ); @@ -200,6 +202,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom $token->set_expiry_month( $source->exp_month ); $token->set_expiry_year( $source->exp_year ); $token->set_user_id( $customer_id ); + $token->set_fingerprint( $source->fingerprint ); $token->save(); $tokens[ $token->get_id() ] = $token; } else { @@ -221,6 +224,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom $token->set_gateway_id( WC_Gateway_Stripe_Sepa::ID ); $token->set_last4( $source->sepa_debit->last4 ); $token->set_user_id( $customer_id ); + $token->set_fingerprint( $source->fingerprint ); $token->save(); $tokens[ $token->get_id() ] = $token; } else { @@ -492,9 +496,9 @@ private function is_valid_payment_method_type_for_gateway( $payment_method_type, /** * Creates and add a token to an user, based on the PaymentMethod object. * - * @param array $payment_method Payment method to be added. - * @param WC_Stripe_Customer $user WC_Stripe_Customer we're processing the tokens for. - * @return WC_Payment_Token_CC|WC_Payment_Token_Link|WC_Payment_Token_SEPA The WC object for the payment token. + * @param object $payment_method Payment method to be added. + * @param WC_Stripe_Customer $customer WC_Stripe_Customer we're processing the tokens for. + * @return WC_Payment_Token The WC object for the payment token. */ private function add_token_to_user( $payment_method, WC_Stripe_Customer $customer ) { // Clear cached payment methods. @@ -503,13 +507,22 @@ private function add_token_to_user( $payment_method, WC_Stripe_Customer $custome $payment_method_type = $this->get_original_payment_method_type( $payment_method ); $gateway_id = self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ $payment_method_type ]; + $found_token = $this->get_duplicate_token( $payment_method, $customer->get_user_id(), $gateway_id ); + if ( $found_token ) { + // Update the token with the new payment method ID. + $found_token->set_token( $payment_method->id ); + $found_token->save(); + return $found_token; + } + switch ( $payment_method_type ) { case WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID: - $token = new WC_Payment_Token_CC(); + $token = new WC_Stripe_Payment_Token_CC(); $token->set_expiry_month( $payment_method->card->exp_month ); $token->set_expiry_year( $payment_method->card->exp_year ); $token->set_card_type( strtolower( $payment_method->card->display_brand ?? $payment_method->card->networks->preferred ?? $payment_method->card->brand ) ); $token->set_last4( $payment_method->card->last4 ); + $token->set_fingerprint( $payment_method->card->fingerprint ); break; case WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID: @@ -528,6 +541,7 @@ private function add_token_to_user( $payment_method, WC_Stripe_Customer $custome $token = new WC_Payment_Token_SEPA(); $token->set_last4( $payment_method->sepa_debit->last4 ); $token->set_payment_method_type( $payment_method_type ); + $token->set_fingerprint( $payment_method->sepa_debit->fingerprint ); } $token->set_gateway_id( $gateway_id ); @@ -647,6 +661,50 @@ public function is_valid_payment_method_id( $payment_method_id, $payment_method_ return 0 === strpos( $payment_method_id, 'src_' ) && WC_Stripe_Payment_Methods::CARD === $payment_method_type; } + /** + * Searches for a duplicate token in the user's saved payment methods and returns it. + * + * @param $payment_method stdClass The payment method object. + * @param $user_id int The user ID. + * @param $gateway_id string The gateway ID. + * @return WC_Payment_Token|null + */ + public static function get_duplicate_token( $payment_method, $user_id, $gateway_id ) { + // Using the base method instead of `WC_Payment_Tokens::get_customer_tokens` to avoid recursive calls to hooked filters and actions + $tokens = WC_Payment_Tokens::get_tokens( + [ + 'user_id' => $user_id, + 'gateway_id' => $gateway_id, + 'limit' => 100, + ] + ); + foreach ( $tokens as $token ) { + /** + * Token object. + * + * @var WC_Payment_Token_CashApp|WC_Stripe_Payment_Token_CC|WC_Payment_Token_Link|WC_Payment_Token_SEPA $token + */ + if ( $token->is_equal_payment_method( $payment_method ) ) { + return $token; + } + } + return null; + } + + /** + * Filters the payment token class to override the credit card class with the extension's version. + * + * @param string $class Payment token class. + * @param string $type Token type. + * @return string + */ + public function woocommerce_payment_token_class( $class, $type ) { + if ( WC_Payment_Token_CC::class === $class ) { + return WC_Stripe_Payment_Token_CC::class; + } + return $class; + } + /** * Controls the output for SEPA on the my account page. * diff --git a/includes/class-wc-stripe-sepa-payment-token.php b/includes/payment-tokens/class-wc-stripe-sepa-payment-token.php similarity index 82% rename from includes/class-wc-stripe-sepa-payment-token.php rename to includes/payment-tokens/class-wc-stripe-sepa-payment-token.php index 7df3663b07..44e52aa557 100644 --- a/includes/class-wc-stripe-sepa-payment-token.php +++ b/includes/payment-tokens/class-wc-stripe-sepa-payment-token.php @@ -15,7 +15,9 @@ * @version 4.0.0 * @since 4.0.0 */ -class WC_Payment_Token_SEPA extends WC_Payment_Token { +class WC_Payment_Token_SEPA extends WC_Payment_Token implements WC_Stripe_Payment_Method_Comparison_Interface { + + use WC_Stripe_Fingerprint_Trait; /** * Stores payment type. @@ -32,6 +34,7 @@ class WC_Payment_Token_SEPA extends WC_Payment_Token { protected $extra_data = [ 'last4' => '', 'payment_method_type' => WC_Stripe_Payment_Methods::SEPA_DEBIT, + 'fingerprint' => '', ]; /** @@ -125,4 +128,18 @@ public function set_payment_method_type( $type ) { public function get_payment_method_type( $context = 'view' ) { return $this->get_prop( 'payment_method_type', $context ); } + + /** + * Checks if the payment method token is equal a provided payment method. + * + * @inheritDoc + */ + public function is_equal_payment_method( $payment_method ): bool { + if ( WC_Stripe_Payment_Methods::SEPA_DEBIT === $payment_method->type + && ( $payment_method->sepa_debit->fingerprint ?? null ) === $this->get_fingerprint() ) { + return true; + } + + return false; + } } diff --git a/includes/payment-tokens/interface-wc-stripe-payment-method-comparison.php b/includes/payment-tokens/interface-wc-stripe-payment-method-comparison.php new file mode 100644 index 0000000000..a29f11be56 --- /dev/null +++ b/includes/payment-tokens/interface-wc-stripe-payment-method-comparison.php @@ -0,0 +1,17 @@ +get_prop( 'fingerprint', $context ); + } + + /** + * Set the token fingerprint (unique identifier). + * + * @since 9.0.0 + * @param string $fingerprint The fingerprint. + */ + public function set_fingerprint( string $fingerprint ) { + $this->set_prop( 'fingerprint', $fingerprint ); + } +} diff --git a/readme.txt b/readme.txt index 42c24f423e..215627db64 100644 --- a/readme.txt +++ b/readme.txt @@ -111,6 +111,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o == Changelog == = 9.1.0 - xxxx-xx-xx = +* Fix - Prevents duplicated credit cards to be added to the customer's account through the My Account page, the shortcode checkout and the block checkout. * Fix - Return to the correct page when redirect-based payment method fails. * Fix - Show default recipient for Payment Authentication Requested email. * Fix - Correctly handles IPP failed payments webhook calls by extracting the order ID from the payment intent metadata. diff --git a/tests/phpunit/payment-tokens/test-class-wc-stripe-payment-tokens.php b/tests/phpunit/payment-tokens/test-class-wc-stripe-payment-tokens.php new file mode 100644 index 0000000000..19ba517a44 --- /dev/null +++ b/tests/phpunit/payment-tokens/test-class-wc-stripe-payment-tokens.php @@ -0,0 +1,214 @@ +stripe_payment_tokens = new WC_Stripe_Payment_Tokens(); + } + + public function test_is_valid_payment_method_id() { + $this->assertTrue( $this->stripe_payment_tokens->is_valid_payment_method_id( 'pm_1234567890' ) ); + $this->assertTrue( $this->stripe_payment_tokens->is_valid_payment_method_id( 'pm_1234567890', 'card' ) ); + $this->assertTrue( $this->stripe_payment_tokens->is_valid_payment_method_id( 'pm_1234567890', 'sepa' ) ); + + // Test with source id (only card payment method type is valid). + $this->assertTrue( $this->stripe_payment_tokens->is_valid_payment_method_id( 'src_1234567890', 'card' ) ); + $this->assertFalse( $this->stripe_payment_tokens->is_valid_payment_method_id( 'src_1234567890', 'sepa' ) ); + $this->assertFalse( $this->stripe_payment_tokens->is_valid_payment_method_id( 'src_1234567890', 'giropay' ) ); + } + + /** + * Test for `get_duplicate_token` method. + * + * @param object $payment_method Payment method object. + * @param boolean $instance_expected Whether an instance of token is expected. + * @return void + * @dataProvider provide_test_get_duplicate_token + */ + public function test_get_duplicate_token( $payment_method, $instance_expected ) { + // CC token. + $token = new WC_Stripe_Payment_Token_CC(); + $token->set_expiry_month( '12' ); + $token->set_expiry_year( '2024' ); + $token->set_card_type( 'visa' ); + $token->set_last4( '4242' ); + $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID ); + $token->set_token( 'pm_1234' ); + $token->set_user_id( 1 ); + $token->set_fingerprint( 'Fxxxxxxxxxxxxxxx' ); + $token->save(); + + // CashApp token. + $token = new WC_Payment_Token_CashApp(); + $token->set_cashtag( '$test_cashtag' ); + $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID ); + $token->set_token( 'pm_1234' ); + $token->set_user_id( 1 ); + $token->save(); + + // SEPA token. + $token = new WC_Payment_Token_SEPA(); + $token->set_token( 'pm_1234' ); + $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID ); + $token->set_last4( '1234' ); + $token->set_fingerprint( 'Fxxxxxxxxxxxxxxx' ); + $token->set_user_id( 1 ); + $token->save(); + + // Link token. + $token = new WC_Payment_Token_Link(); + $token->set_token( 'pm_1234' ); + $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID ); + $token->set_email( 'test@example.com' ); + $token->set_user_id( 1 ); + $token->save(); + + $gateway_id = WC_Stripe_Payment_Tokens::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID ]; + + $found_token = WC_Stripe_Payment_Tokens::get_duplicate_token( $payment_method, 1, $gateway_id ); + if ( $instance_expected ) { + $this->assertInstanceOf( WC_Payment_Token::class, $found_token ); + } else { + $this->assertNull( $found_token ); + } + } + + /** + * Provider for `test_get_duplicate_token` method. + * + * @return array + */ + public function provide_test_get_duplicate_token() { + // Known CC method. + $payment_method_cc = [ + 'id' => 'pm_mock_payment_method_id', + 'type' => WC_Stripe_Payment_Methods::CARD, + WC_Stripe_Payment_Methods::CARD => [ + 'brand' => 'visa', + 'network' => 'visa', + 'exp_month' => '7', + 'exp_year' => '2099', + 'funding' => 'credit', + 'last4' => '4242', + 'fingerprint' => 'Fxxxxxxxxxxxxxxx', + ], + ]; + $payment_method_cc[ WC_Stripe_Payment_Methods::CARD ] = (object) $payment_method_cc[ WC_Stripe_Payment_Methods::CARD ]; + + // Unknown CC method. + $payment_method_cc_unknown = [ + 'id' => 'pm_mock_payment_method_id', + 'type' => WC_Stripe_Payment_Methods::CARD, + WC_Stripe_Payment_Methods::CARD => [ + 'brand' => 'visa', + 'network' => 'visa', + 'exp_month' => '7', + 'exp_year' => '2099', + 'funding' => 'credit', + 'last4' => '4242', + 'fingerprint' => 'Fxxxxxxxxxxxxxxx_unknown', + ], + ]; + $payment_method_cc_unknown[ WC_Stripe_Payment_Methods::CARD ] = (object) $payment_method_cc_unknown[ WC_Stripe_Payment_Methods::CARD ]; + + // Known CashApp method. + $payment_method_cashapp = [ + 'id' => 'pm_mock_payment_method_id', + 'type' => WC_Stripe_Payment_Methods::CASHAPP_PAY, + WC_Stripe_Payment_Methods::CASHAPP_PAY => [ + 'cashtag' => '$test_cashtag', + ], + ]; + $payment_method_cashapp[ WC_Stripe_Payment_Methods::CASHAPP_PAY ] = (object) $payment_method_cashapp[ WC_Stripe_Payment_Methods::CASHAPP_PAY ]; + + // Known Sepa method. + $payment_method_sepa = [ + 'id' => 'pm_mock_payment_method_id', + 'type' => WC_Stripe_Payment_Methods::SEPA_DEBIT, + WC_Stripe_Payment_Methods::SEPA_DEBIT => [ + 'last4' => '1234', + 'fingerprint' => 'Fxxxxxxxxxxxxxxx', + ], + ]; + $payment_method_sepa[ WC_Stripe_Payment_Methods::SEPA_DEBIT ] = (object) $payment_method_sepa[ WC_Stripe_Payment_Methods::SEPA_DEBIT ]; + + // Known Link method. + $payment_method_link = [ + 'id' => 'pm_mock_payment_method_id', + 'type' => WC_Stripe_Payment_Methods::LINK, + WC_Stripe_Payment_Methods::LINK => [ + 'email' => 'test@example.com', + ], + ]; + + return [ + 'existing CC' => [ + 'payment method' => (object) $payment_method_cc, + 'expected' => true, + ], + 'unknown CC' => [ + 'payment method' => (object) $payment_method_cc_unknown, + 'expected' => false, + ], + 'existing CashApp' => [ + 'payment method' => (object) $payment_method_cashapp, + 'expected' => true, + ], + 'existing Sepa' => [ + 'payment method' => (object) $payment_method_sepa, + 'expected' => true, + ], + 'existing Link' => [ + 'payment method' => (object) $payment_method_link, + 'expected' => false, + ], + ]; + } + + /** + * Test for `woocommerce_payment_token_class`. + * + * @return void + * @dataProvider provide_test_woocommerce_payment_token_class + */ + public function test_woocommerce_payment_token_class( $class, $expected ) { + $actual = $this->stripe_payment_tokens->woocommerce_payment_token_class( $class, '' ); + $this->assertSame( $expected, $actual ); + } + + /** + * Provider for `test_woocommerce_payment_token_class` method. + * + * @return array + */ + public function provide_test_woocommerce_payment_token_class() { + return [ + WC_Payment_Token_CC::class => [ + 'class' => WC_Payment_Token_CC::class, + 'expected' => WC_Stripe_Payment_Token_CC::class, + ], + WC_Payment_Token_CashApp::class => [ + 'class' => WC_Payment_Token_CashApp::class, + 'expected' => WC_Payment_Token_CashApp::class, + ], + WC_Payment_Token_SEPA::class => [ + 'class' => WC_Payment_Token_SEPA::class, + 'expected' => WC_Payment_Token_SEPA::class, + ], + WC_Payment_Token_Link::class => [ + 'class' => WC_Payment_Token_Link::class, + 'expected' => WC_Payment_Token_Link::class, + ], + ]; + } +} diff --git a/tests/phpunit/payment-tokens/test-interface-wc-stripe-token-comparison.php b/tests/phpunit/payment-tokens/test-interface-wc-stripe-token-comparison.php new file mode 100644 index 0000000000..6cdb02aae0 --- /dev/null +++ b/tests/phpunit/payment-tokens/test-interface-wc-stripe-token-comparison.php @@ -0,0 +1,104 @@ +set_fingerprint( '123abc' ); + break; + case WC_Stripe_Payment_Methods::LINK: + $token = new WC_Payment_Token_Link(); + $token->set_email( 'john.doe@example.com' ); + break; + case WC_Stripe_Payment_Methods::CASHAPP_PAY: + $token = new WC_Payment_Token_CashApp(); + $token->set_cashtag( '$test_cashtag' ); + break; + case 'CC': + default: + $token = new WC_Stripe_Payment_Token_CC(); + $token->set_fingerprint( '123abc' ); + } + + $this->assertEquals( $expected, $token->is_equal_payment_method( $payment_method ) ); + } + + /** + * Data provider for `test_is_equal`. + * + * @return array + */ + public function provide_test_is_equal_payment_method() { + return [ + 'Unknown method' => [ + 'token type' => 'unknown', + 'payment method' => (object) [ + 'type' => 'unknown', + ], + 'expected' => false, + ], + 'CC, not equal' => [ + 'token type' => 'CC', + 'payment_method' => (object) [ + 'type' => WC_Stripe_Payment_Methods::CARD, + 'card' => (object) [ + 'fingerprint' => '456def', + ], + ], + 'expected' => false, + ], + 'CC, equal' => [ + 'token type' => 'CC', + 'payment method' => (object) [ + 'type' => WC_Stripe_Payment_Methods::CARD, + 'card' => (object) [ + 'fingerprint' => '123abc', + ], + ], + 'expected' => true, + ], + 'SEPA, equal' => [ + 'token type' => WC_Stripe_Payment_Methods::SEPA, + 'payment method' => (object) [ + 'type' => WC_Stripe_Payment_Methods::SEPA_DEBIT, + 'sepa_debit' => (object) [ + 'fingerprint' => '123abc', + ], + ], + 'expected' => true, + ], + 'Link, equal' => [ + 'token type' => WC_Stripe_Payment_Methods::LINK, + 'payment method' => (object) [ + 'type' => WC_Stripe_Payment_Methods::LINK, + 'link' => (object) [ + 'email' => 'john.doe@example.com', + ], + ], + 'expected' => true, + ], + 'CashApp, equal' => [ + 'token type' => WC_Stripe_Payment_Methods::CASHAPP_PAY, + 'payment method' => (object) [ + 'type' => WC_Stripe_Payment_Methods::CASHAPP_PAY, + 'cashapp' => (object) [ + 'cashtag' => '$test_cashtag', + ], + ], + 'expected' => true, + ], + ]; + } +} diff --git a/tests/phpunit/payment-tokens/test-trait-wc-stripe-fingerprint.php b/tests/phpunit/payment-tokens/test-trait-wc-stripe-fingerprint.php new file mode 100644 index 0000000000..fbf1b2901e --- /dev/null +++ b/tests/phpunit/payment-tokens/test-trait-wc-stripe-fingerprint.php @@ -0,0 +1,16 @@ +set_fingerprint( '123abc' ); + $this->assertEquals( '123abc', $token->get_fingerprint() ); + } +} diff --git a/tests/phpunit/test-class-wc-stripe-payment-tokens.php b/tests/phpunit/test-class-wc-stripe-payment-tokens.php deleted file mode 100644 index 513cce96dd..0000000000 --- a/tests/phpunit/test-class-wc-stripe-payment-tokens.php +++ /dev/null @@ -1,29 +0,0 @@ -stripe_payment_tokens = new WC_Stripe_Payment_Tokens(); - } - - public function test_is_valid_payment_method_id() { - $this->assertTrue( $this->stripe_payment_tokens->is_valid_payment_method_id( 'pm_1234567890' ) ); - $this->assertTrue( $this->stripe_payment_tokens->is_valid_payment_method_id( 'pm_1234567890', 'card' ) ); - $this->assertTrue( $this->stripe_payment_tokens->is_valid_payment_method_id( 'pm_1234567890', 'sepa' ) ); - - // Test with source id (only card payment method type is valid). - $this->assertTrue( $this->stripe_payment_tokens->is_valid_payment_method_id( 'src_1234567890', 'card' ) ); - $this->assertFalse( $this->stripe_payment_tokens->is_valid_payment_method_id( 'src_1234567890', 'sepa' ) ); - $this->assertFalse( $this->stripe_payment_tokens->is_valid_payment_method_id( 'src_1234567890', 'giropay' ) ); - } -} diff --git a/tests/phpunit/test-class-wc-stripe-upe-payment-gateway.php b/tests/phpunit/test-class-wc-stripe-upe-payment-gateway.php index 679af71377..7fce98f825 100644 --- a/tests/phpunit/test-class-wc-stripe-upe-payment-gateway.php +++ b/tests/phpunit/test-class-wc-stripe-upe-payment-gateway.php @@ -35,7 +35,7 @@ class WC_Stripe_UPE_Payment_Gateway_Test extends WP_UnitTestCase { * Base template for Stripe card payment method. */ const MOCK_CARD_PAYMENT_METHOD_TEMPLATE = [ - 'type' => WC_Stripe_Payment_Methods::CARD, + 'type' => WC_Stripe_Payment_Methods::CARD, WC_Stripe_Payment_Methods::CARD => [ 'brand' => 'visa', 'networks' => [ 'preferred' => 'visa' ], @@ -49,10 +49,11 @@ class WC_Stripe_UPE_Payment_Gateway_Test extends WP_UnitTestCase { * Base template for SEPA Direct Debit payment method. */ const MOCK_SEPA_PAYMENT_METHOD_TEMPLATE = [ - 'type' => WC_Stripe_Payment_Methods::SEPA_DEBIT, - 'object' => 'payment_method', + 'type' => WC_Stripe_Payment_Methods::SEPA_DEBIT, + 'object' => 'payment_method', WC_Stripe_Payment_Methods::SEPA_DEBIT => [ - 'last4' => '7061', + 'last4' => '7061', + 'fingerprint' => 'fp_mock', ], ]; @@ -1075,7 +1076,7 @@ public function test_setup_intent_checkout_saves_sepa_generated_payment_method_t $setup_intent_mock['latest_charge'] = []; $setup_intent_mock['latest_attempt'] = [ 'payment_method_details' => [ - 'type' => WC_Stripe_Payment_Methods::BANCONTACT, + 'type' => WC_Stripe_Payment_Methods::BANCONTACT, WC_Stripe_Payment_Methods::BANCONTACT => [ 'generated_sepa_debit' => $generated_payment_method_id, ], @@ -2230,9 +2231,9 @@ public function test_process_payment_deferred_intent_with_co_branded_cc_and_pref ->willReturn( $customer_id ); $charge = [ - 'id' => 'ch_mock', - 'captured' => true, - 'status' => 'succeeded', + 'id' => 'ch_mock', + 'captured' => true, + 'status' => 'succeeded', ]; $this->mock_gateway ->expects( $this->exactly( 2 ) ) @@ -2406,8 +2407,8 @@ public function test_set_payment_method_title_for_order_custom_title() { // CARD // Set a custom title. - $payment_method_type = WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID; - $payment_method_settings = get_option( "woocommerce_stripe_{$payment_method_type}_settings", [] ); + $payment_method_type = WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID; + $payment_method_settings = get_option( "woocommerce_stripe_{$payment_method_type}_settings", [] ); $payment_method_settings['title'] = 'Custom Card Title'; update_option( "woocommerce_stripe_{$payment_method_type}_settings", $payment_method_settings ); @@ -2417,8 +2418,8 @@ public function test_set_payment_method_title_for_order_custom_title() { // SEPA // Set a custom title. - $payment_method_type = WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID; - $payment_method_settings = get_option( "woocommerce_stripe_{$payment_method_type}_settings", [] ); + $payment_method_type = WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID; + $payment_method_settings = get_option( "woocommerce_stripe_{$payment_method_type}_settings", [] ); $payment_method_settings['title'] = 'Custom SEPA Title'; update_option( "woocommerce_stripe_{$payment_method_type}_settings", $payment_method_settings ); diff --git a/tests/phpunit/test-class-wc-stripe-upe-payment-method.php b/tests/phpunit/test-class-wc-stripe-upe-payment-method.php index 9416204cc0..c5c0d30de4 100644 --- a/tests/phpunit/test-class-wc-stripe-upe-payment-method.php +++ b/tests/phpunit/test-class-wc-stripe-upe-payment-method.php @@ -17,12 +17,13 @@ class WC_Stripe_UPE_Payment_Method_Test extends WP_UnitTestCase { 'id' => 'pm_mock_payment_method_id', 'type' => WC_Stripe_Payment_Methods::CARD, WC_Stripe_Payment_Methods::CARD => [ - 'brand' => 'visa', - 'network' => 'visa', - 'exp_month' => '7', - 'exp_year' => '2099', - 'funding' => 'credit', - 'last4' => '4242', + 'brand' => 'visa', + 'network' => 'visa', + 'exp_month' => '7', + 'exp_year' => '2099', + 'funding' => 'credit', + 'last4' => '4242', + 'fingerprint' => 'Fxxxxxxxxxxxxxxx', ], ]; @@ -511,7 +512,7 @@ public function test_payment_methods_with_domestic_restrictions_are_enabled_on_c ->getMock(); WC_Stripe::get_instance()->account->method( 'get_cached_account_data' )->willReturn( [ - 'country' => 'US', + 'country' => 'US', 'default_currency' => WC_Stripe_Currency_Code::UNITED_STATES_DOLLAR, ] ); @@ -655,7 +656,7 @@ public function test_create_payment_token_for_user() { case WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID: $card_payment_method_mock = $this->array_to_object( self::MOCK_CARD_PAYMENT_METHOD_TEMPLATE ); $token = $payment_method->create_payment_token_for_user( $user_id, $card_payment_method_mock ); - $this->assertTrue( 'WC_Payment_Token_CC' === get_class( $token ) ); + $this->assertTrue( WC_Stripe_Payment_Token_CC::class === get_class( $token ) ); $this->assertSame( $token->get_last4(), $card_payment_method_mock->card->last4 ); $this->assertSame( $token->get_token(), $card_payment_method_mock->id ); // Test display brand @@ -674,19 +675,19 @@ public function test_create_payment_token_for_user() { case WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID: $link_payment_method_mock = $this->array_to_object( self::MOCK_LINK_PAYMENT_METHOD_TEMPLATE ); $token = $payment_method->create_payment_token_for_user( $user_id, $link_payment_method_mock ); - $this->assertTrue( 'WC_Payment_Token_Link' === get_class( $token ) ); + $this->assertTrue( WC_Payment_Token_Link::class === get_class( $token ) ); $this->assertSame( $token->get_email(), $link_payment_method_mock->link->email ); break; case WC_Stripe_UPE_Payment_Method_Cash_App_Pay::STRIPE_ID: $cash_app_payment_method_mock = $this->array_to_object( self::MOCK_CASH_APP_PAYMENT_METHOD_TEMPLATE ); $token = $payment_method->create_payment_token_for_user( $user_id, $cash_app_payment_method_mock ); - $this->assertTrue( 'WC_Payment_Token_CashApp' === get_class( $token ) ); + $this->assertTrue( WC_Payment_Token_CashApp::class === get_class( $token ) ); $this->assertSame( $token->get_cashtag(), $cash_app_payment_method_mock->cashapp->cashtag ); break; default: $sepa_payment_method_mock = $this->array_to_object( self::MOCK_SEPA_PAYMENT_METHOD_TEMPLATE ); $token = $payment_method->create_payment_token_for_user( $user_id, $sepa_payment_method_mock ); - $this->assertTrue( 'WC_Payment_Token_SEPA' === get_class( $token ) ); + $this->assertTrue( WC_Payment_Token_SEPA::class === get_class( $token ) ); $this->assertSame( $token->get_last4(), $sepa_payment_method_mock->sepa_debit->last4 ); $this->assertSame( $token->get_token(), $sepa_payment_method_mock->id ); @@ -694,6 +695,31 @@ public function test_create_payment_token_for_user() { } } + /** + * Test for `update_payment_token` method. + * + * @return void + */ + public function test_update_payment_token() { + $token = new WC_Stripe_Payment_Token_CC(); + $token->set_expiry_month( '12' ); + $token->set_expiry_year( '2024' ); + $token->set_card_type( 'visa' ); + $token->set_last4( '4242' ); + $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID ); + $token->set_token( 'pm_1234' ); + $token->set_user_id( 1 ); + $token->set_fingerprint( 'Lstxxxx' ); + $token->save(); + + $expected = self::MOCK_CARD_PAYMENT_METHOD_TEMPLATE['id']; + + $payment_method = new WC_Stripe_UPE_Payment_Method_CC(); + $actual = $payment_method->update_payment_token( $token, $expected ); + + $this->assertSame( $expected, $actual->get_token() ); + } + /** * Tests that UPE methods are only enabled if Stripe is enabled and the individual methods is enabled in the settings. */ diff --git a/woocommerce-gateway-stripe.php b/woocommerce-gateway-stripe.php index 4707c47820..786af27cd3 100644 --- a/woocommerce-gateway-stripe.php +++ b/woocommerce-gateway-stripe.php @@ -208,9 +208,12 @@ public function init() { require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-action-scheduler-service.php'; require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-webhook-state.php'; require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-webhook-handler.php'; - require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-sepa-payment-token.php'; - require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-link-payment-token.php'; - require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-cash-app-pay-token.php'; + require_once dirname( __FILE__ ) . '/includes/payment-tokens/trait-wc-stripe-fingerprint.php'; + require_once dirname( __FILE__ ) . '/includes/payment-tokens/interface-wc-stripe-payment-method-comparison.php'; + require_once dirname( __FILE__ ) . '/includes/payment-tokens/class-wc-stripe-cc-payment-token.php'; + require_once dirname( __FILE__ ) . '/includes/payment-tokens/class-wc-stripe-sepa-payment-token.php'; + require_once dirname( __FILE__ ) . '/includes/payment-tokens/class-wc-stripe-link-payment-token.php'; + require_once dirname( __FILE__ ) . '/includes/payment-tokens/class-wc-stripe-cash-app-payment-token.php'; require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-apple-pay-registration.php'; require_once dirname( __FILE__ ) . '/includes/class-wc-gateway-stripe.php'; require_once dirname( __FILE__ ) . '/includes/constants/class-wc-stripe-currency-code.php'; @@ -255,7 +258,7 @@ public function init() { require_once dirname( __FILE__ ) . '/includes/connect/class-wc-stripe-connect.php'; require_once dirname( __FILE__ ) . '/includes/connect/class-wc-stripe-connect-api.php'; require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-order-handler.php'; - require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-payment-tokens.php'; + require_once dirname( __FILE__ ) . '/includes/payment-tokens/class-wc-stripe-payment-tokens.php'; require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-customer.php'; require_once dirname( __FILE__ ) . '/includes/class-wc-stripe-intent-controller.php'; require_once dirname( __FILE__ ) . '/includes/admin/class-wc-stripe-inbox-notes.php';