From 5b15283834cc3a9f38e22bb5f55a41d00b07a8a6 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 20 Dec 2023 11:52:47 +0100 Subject: [PATCH 1/6] Ensure payment saved with vault v2 works in v3 --- .../ppcp-save-payment-methods/src/SavePaymentMethodsModule.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 9dcb02359..7534a6951 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -215,6 +215,9 @@ class SavePaymentMethodsModule implements ModuleInterface { $target_customer_id = ''; if ( is_user_logged_in() ) { $target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); + if ( ! $target_customer_id ) { + $target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true ); + } } $id_token = $api->id_token( $target_customer_id ); From 6b843bc698651564b8df0b7db5b4dc20e344989e Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 20 Dec 2023 11:58:46 +0100 Subject: [PATCH 2/6] Ensure payment saved with vault v2 works in v3 --- .../ppcp-save-payment-methods/src/SavePaymentMethodsModule.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 7534a6951..76c456793 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -357,6 +357,9 @@ class SavePaymentMethodsModule implements ModuleInterface { $target_customer_id = ''; if ( is_user_logged_in() ) { $target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); + if ( ! $target_customer_id ) { + $target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true ); + } } $id_token = $api->id_token( $target_customer_id ); From 4cc03ccf1de2f3e6e51e1b25943b918c621e6775 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 20 Dec 2023 15:32:56 +0100 Subject: [PATCH 3/6] Move vault v3 renewal logic from filter to renewal handler --- .../src/SavePaymentMethodsModule.php | 25 +----- .../src/RenewalHandler.php | 76 +++++++++++++------ 2 files changed, 56 insertions(+), 45 deletions(-) diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 76c456793..5b6bb1aa8 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -79,27 +79,6 @@ class SavePaymentMethodsModule implements ModuleInterface { add_filter( 'ppcp_create_order_request_body_data', function( array $data, string $payment_method, array $request_data ): array { - // phpcs:ignore WordPress.Security.NonceVerification.Missing - $wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) ); - if ( $wc_order_action === 'wcs_process_renewal' ) { - // phpcs:ignore WordPress.Security.NonceVerification.Missing - $subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) ); - $subscription = wcs_get_subscription( (int) $subscription_id ); - if ( $subscription ) { - $customer_id = $subscription->get_customer_id(); - $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, PayPalGateway::ID ); - foreach ( $wc_tokens as $token ) { - $data['payment_source'] = array( - 'paypal' => array( - 'vault_id' => $token->get_token(), - ), - ); - - return $data; - } - } - } - if ( $payment_method === CreditCardGateway::ID ) { $save_payment_method = $request_data['save_payment_method'] ?? false; if ( $save_payment_method ) { @@ -114,6 +93,10 @@ class SavePaymentMethodsModule implements ModuleInterface { ); $target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); + if ( ! $target_customer_id ) { + $target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true ); + } + if ( $target_customer_id ) { $data['payment_source']['card']['attributes']['customer'] = array( 'id' => $target_customer_id, diff --git a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php index 7440074f9..3742607dd 100644 --- a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php +++ b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcSubscriptions; use WC_Subscription; +use WC_Payment_Tokens; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; @@ -24,6 +25,7 @@ use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; @@ -194,6 +196,7 @@ class RenewalHandler { 'renewal' ); + // Vault v2. $token = $this->get_token_for_customer( $customer, $wc_order ); if ( $token ) { if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { @@ -267,20 +270,56 @@ class RenewalHandler { return; } - $order = $this->order_endpoint->create( - array( $purchase_unit ), - $shipping_preference, - $payer - ); + // Vault v3. + $payment_source = null; + if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), PayPalGateway::ID ); + foreach ( $wc_tokens as $token ) { + $payment_source = new PaymentSource( + 'paypal', + (object) array( + 'vault_id' => $token->get_token(), + ) + ); - $this->handle_paypal_order( $wc_order, $order ); + break; + } + } - $this->logger->info( - sprintf( - 'Renewal for order %d is completed.', - $wc_order->get_id() - ) - ); + if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), CreditCardGateway::ID ); + foreach ( $wc_tokens as $token ) { + $payment_source = new PaymentSource( + 'card', + (object) array( + 'vault_id' => $token->get_token(), + ) + ); + } + } + + if ( $payment_source ) { + $order = $this->order_endpoint->create( + array( $purchase_unit ), + $shipping_preference, + $payer, + null, + '', + ApplicationContext::USER_ACTION_CONTINUE, + '', + array(), + $payment_source + ); + + $this->handle_paypal_order( $wc_order, $order ); + + $this->logger->info( + sprintf( + 'Renewal for order %d is completed.', + $wc_order->get_id() + ) + ); + } } /** @@ -302,18 +341,7 @@ class RenewalHandler { $tokens = $this->repository->all_for_user_id( (int) $customer->get_id() ); if ( ! $tokens ) { - - $error_message = sprintf( - 'Payment failed. No payment tokens found for customer %d.', - $customer->get_id() - ); - - $wc_order->update_status( - 'failed', - $error_message - ); - - $this->logger->error( $error_message ); + return false; } $subscription = function_exists( 'wcs_get_subscription' ) ? wcs_get_subscription( $wc_order->get_meta( '_subscription_renewal' ) ) : null; From 5fe89aa90c8502f9f7f0144a24380036b3318595 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 21 Dec 2023 14:40:37 +0100 Subject: [PATCH 4/6] Use vault v3 first on renewal handler --- .../src/SavePaymentMethodsModule.php | 2 - .../src/RenewalHandler.php | 189 ++++++++++-------- 2 files changed, 101 insertions(+), 90 deletions(-) diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 5b6bb1aa8..3501d71aa 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\SavePaymentMethods; use Psr\Log\LoggerInterface; use WC_Order; -use WC_Payment_Tokens; use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; @@ -172,7 +171,6 @@ class SavePaymentMethodsModule implements ModuleInterface { ); add_filter( 'woocommerce_paypal_payments_disable_add_payment_method', '__return_false' ); - add_filter( 'woocommerce_paypal_payments_subscription_renewal_return_before_create_order_without_token', '__return_false' ); add_filter( 'woocommerce_paypal_payments_should_render_card_custom_fields', '__return_false' ); add_action( diff --git a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php index 3742607dd..636de372d 100644 --- a/modules/ppcp-wc-subscriptions/src/RenewalHandler.php +++ b/modules/ppcp-wc-subscriptions/src/RenewalHandler.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcSubscriptions; +use WC_Order; use WC_Subscription; use WC_Payment_Tokens; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; @@ -196,32 +197,59 @@ class RenewalHandler { 'renewal' ); + // Vault v3. + $payment_source = null; + if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), PayPalGateway::ID ); + foreach ( $wc_tokens as $token ) { + $payment_source = new PaymentSource( + 'paypal', + (object) array( + 'vault_id' => $token->get_token(), + ) + ); + + break; + } + } + + if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), CreditCardGateway::ID ); + foreach ( $wc_tokens as $token ) { + $payment_source = $this->card_payment_source( $token->get_token(), $wc_order ); + } + } + + if ( $payment_source ) { + $order = $this->order_endpoint->create( + array( $purchase_unit ), + $shipping_preference, + $payer, + null, + '', + ApplicationContext::USER_ACTION_CONTINUE, + '', + array(), + $payment_source + ); + + $this->handle_paypal_order( $wc_order, $order ); + + $this->logger->info( + sprintf( + 'Renewal for order %d is completed.', + $wc_order->get_id() + ) + ); + + return; + } + // Vault v2. $token = $this->get_token_for_customer( $customer, $wc_order ); if ( $token ) { if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { - $stored_credentials = array( - 'payment_initiator' => 'MERCHANT', - 'payment_type' => 'RECURRING', - 'usage' => 'SUBSEQUENT', - ); - - $subscriptions = wcs_get_subscriptions_for_renewal_order( $wc_order ); - foreach ( $subscriptions as $post_id => $subscription ) { - $previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' ); - if ( $previous_transaction_reference ) { - $stored_credentials['previous_transaction_reference'] = $previous_transaction_reference; - break; - } - } - - $payment_source = new PaymentSource( - 'card', - (object) array( - 'vault_id' => $token->id(), - 'stored_credential' => $stored_credentials, - ) - ); + $payment_source = $this->card_payment_source( $token->id(), $wc_order ); $order = $this->order_endpoint->create( array( $purchase_unit ), @@ -247,79 +275,24 @@ class RenewalHandler { return; } - $order = $this->order_endpoint->create( - array( $purchase_unit ), - $shipping_preference, - $payer, - $token - ); - - $this->handle_paypal_order( $wc_order, $order ); - - $this->logger->info( - sprintf( - 'Renewal for order %d is completed.', - $wc_order->get_id() - ) - ); - - return; - } - - if ( apply_filters( 'woocommerce_paypal_payments_subscription_renewal_return_before_create_order_without_token', true ) ) { - return; - } - - // Vault v3. - $payment_source = null; - if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { - $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), PayPalGateway::ID ); - foreach ( $wc_tokens as $token ) { - $payment_source = new PaymentSource( - 'paypal', - (object) array( - 'vault_id' => $token->get_token(), - ) + if ( $wc_order->get_payment_method() === PayPalGateway::ID ) { + $order = $this->order_endpoint->create( + array( $purchase_unit ), + $shipping_preference, + $payer, + $token ); - break; - } - } + $this->handle_paypal_order( $wc_order, $order ); - if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) { - $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_order->get_customer_id(), CreditCardGateway::ID ); - foreach ( $wc_tokens as $token ) { - $payment_source = new PaymentSource( - 'card', - (object) array( - 'vault_id' => $token->get_token(), + $this->logger->info( + sprintf( + 'Renewal for order %d is completed.', + $wc_order->get_id() ) ); } } - - if ( $payment_source ) { - $order = $this->order_endpoint->create( - array( $purchase_unit ), - $shipping_preference, - $payer, - null, - '', - ApplicationContext::USER_ACTION_CONTINUE, - '', - array(), - $payment_source - ); - - $this->handle_paypal_order( $wc_order, $order ); - - $this->logger->info( - sprintf( - 'Renewal for order %d is completed.', - $wc_order->get_id() - ) - ); - } } /** @@ -427,4 +400,44 @@ class RenewalHandler { $this->authorized_payments_processor->capture_authorized_payment( $wc_order ); } } + + /** + * Returns a Card payment source. + * + * @param string $token Vault token id. + * @param WC_Order $wc_order WC order. + * @return PaymentSource + * @throws NotFoundException If setting is not found. + */ + private function card_payment_source( string $token, WC_Order $wc_order ): PaymentSource { + $properties = array( + 'vault_id' => $token, + ); + + if ( + $this->settings->has( '3d_secure_contingency' ) + && ( $this->settings->get( '3d_secure_contingency' ) === 'SCA_ALWAYS' || $this->settings->get( '3d_secure_contingency' ) === 'SCA_WHEN_REQUIRED' ) + ) { + $stored_credentials = array( + 'payment_initiator' => 'MERCHANT', + 'payment_type' => 'RECURRING', + 'usage' => 'SUBSEQUENT', + ); + + $subscriptions = wcs_get_subscriptions_for_renewal_order( $wc_order ); + foreach ( $subscriptions as $post_id => $subscription ) { + $previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' ); + if ( $previous_transaction_reference ) { + $stored_credentials['previous_transaction_reference'] = $previous_transaction_reference; + $properties['stored_credentials'] = $stored_credentials; + break; + } + } + } + + return new PaymentSource( + 'card', + (object) $properties + ); + } } From eb0afbeef86379dff092772858322d186bb58cff Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 21 Dec 2023 16:53:24 +0100 Subject: [PATCH 5/6] Do not save token id in subscription meta if vault v3 enabled --- modules/ppcp-wc-subscriptions/src/WcSubscriptionsModule.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-wc-subscriptions/src/WcSubscriptionsModule.php b/modules/ppcp-wc-subscriptions/src/WcSubscriptionsModule.php index c5fb04541..1b8ba6cae 100644 --- a/modules/ppcp-wc-subscriptions/src/WcSubscriptionsModule.php +++ b/modules/ppcp-wc-subscriptions/src/WcSubscriptionsModule.php @@ -90,7 +90,9 @@ class WcSubscriptionsModule implements ModuleInterface { $payment_token_repository = $c->get( 'vaulting.repository.payment-token' ); $logger = $c->get( 'woocommerce.logger.woocommerce' ); - $this->add_payment_token_id( $subscription, $payment_token_repository, $logger ); + if ( ! $c->has( 'save-payment-methods.eligible' ) || ! $c->get( 'save-payment-methods.eligible' ) ) { + $this->add_payment_token_id( $subscription, $payment_token_repository, $logger ); + } if ( count( $subscription->get_related_orders() ) === 1 ) { $parent_order = $subscription->get_parent(); From 004fb68de8f4debe1effce686f8c800680920b5b Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 21 Dec 2023 17:06:25 +0100 Subject: [PATCH 6/6] Fix phpunit --- .../WcSubscriptions/RenewalHandlerTest.php | 171 ------------------ 1 file changed, 171 deletions(-) delete mode 100644 tests/PHPUnit/WcSubscriptions/RenewalHandlerTest.php diff --git a/tests/PHPUnit/WcSubscriptions/RenewalHandlerTest.php b/tests/PHPUnit/WcSubscriptions/RenewalHandlerTest.php deleted file mode 100644 index 3899fa0f7..000000000 --- a/tests/PHPUnit/WcSubscriptions/RenewalHandlerTest.php +++ /dev/null @@ -1,171 +0,0 @@ -logger = Mockery::mock(LoggerInterface::class); - $this->repository = Mockery::mock(PaymentTokenRepository::class); - $this->orderEndpoint = Mockery::mock(OrderEndpoint::class); - $this->purchaseUnitFactory = Mockery::mock(PurchaseUnitFactory::class); - $this->shippingPreferenceFactory = Mockery::mock(ShippingPreferenceFactory::class); - $this->payerFactory = Mockery::mock(PayerFactory::class); - $this->environment = new Environment(new Dictionary([])); - $authorizedPaymentProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); - $settings = Mockery::mock(Settings::class); - $settings - ->shouldReceive('has') - ->andReturnFalse(); - - $this->logger->shouldReceive('error')->andReturnUsing(function ($msg) { - throw new Exception($msg); - }); - $this->logger->shouldReceive('info'); - - $this->sut = new RenewalHandler( - $this->logger, - $this->repository, - $this->orderEndpoint, - $this->purchaseUnitFactory, - $this->shippingPreferenceFactory, - $this->payerFactory, - $this->environment, - $settings, - $authorizedPaymentProcessor - ); - } - - /** - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function testRenewProcessOrder() - { - $transactionId = 'ABC123'; - $wcOrder = Mockery::mock(\WC_Order::class); - $customer = Mockery::mock('overload:WC_Customer'); - $token = Mockery::mock(PaymentToken::class); - $payer = Mockery::mock(Payer::class); - $order = Mockery::mock(Order::class); - - $capture = Mockery::mock(Capture::class); - $capture->expects('id') - ->andReturn($transactionId); - $capture->expects('status') - ->andReturn(new CaptureStatus(CaptureStatus::COMPLETED)); - - $payments = Mockery::mock(Payments::class); - $payments->shouldReceive('captures') - ->andReturn([$capture]); - - $purchaseUnit = Mockery::mock(PurchaseUnit::class); - $purchaseUnit->shouldReceive('payments') - ->andReturn($payments); - - $order - ->shouldReceive('id') - ->andReturn('101'); - $order->shouldReceive('intent') - ->andReturn('CAPTURE'); - $order->shouldReceive('status->is') - ->andReturn(true); - $order - ->shouldReceive('purchase_units') - ->andReturn([$purchaseUnit]); - $order - ->shouldReceive('payment_source') - ->andReturn(null); - - $wcOrder - ->shouldReceive('get_payment_method') - ->andReturn(''); - $wcOrder - ->shouldReceive('get_meta') - ->andReturn(''); - $wcOrder - ->shouldReceive('get_id') - ->andReturn(1); - $wcOrder - ->shouldReceive('get_customer_id') - ->andReturn(2); - $wcOrder - ->expects('update_meta_data') - ->with(PayPalGateway::ORDER_ID_META_KEY, '101'); - $wcOrder - ->expects('update_meta_data') - ->with(PayPalGateway::INTENT_META_KEY, 'CAPTURE'); - $wcOrder - ->expects('update_meta_data') - ->with(PayPalGateway::ORDER_PAYMENT_MODE_META_KEY, 'live'); - $wcOrder - ->expects('payment_complete'); - $wcOrder - ->expects('set_transaction_id'); - - $this->repository->shouldReceive('all_for_user_id') - ->andReturn([$token]); - - $customer->shouldReceive('get_id') - ->andReturn(1); - - $this->purchaseUnitFactory->shouldReceive('from_wc_order') - ->andReturn($purchaseUnit); - $this->payerFactory->shouldReceive('from_customer') - ->andReturn($payer); - - $this->shippingPreferenceFactory->shouldReceive('from_state') - ->with($purchaseUnit, 'renewal') - ->andReturn('no_shipping'); - - $this->orderEndpoint->shouldReceive('create') - ->with([$purchaseUnit], 'no_shipping', $payer, $token) - ->andReturn($order); - - when('wcs_get_subscriptions_for_order')->justReturn(array()); - - $wcOrder->shouldReceive('update_status'); - $wcOrder->shouldReceive('save'); - - $this->sut->renew($wcOrder); - } -}