From 632c928ac329cfa61384ed3d6c7a98dfeda8dc73 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Dec 2021 11:43:04 +0100 Subject: [PATCH 01/16] Check if current payment exists in payment tokens --- .../src/Factory/PurchaseUnitFactory.php | 2 +- .../src/PaymentTokenRepository.php | 21 +++++++++++ .../src/Gateway/ProcessPaymentTrait.php | 6 ++-- .../Vaulting/PaymentTokenRepositoryTest.php | 36 +++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php diff --git a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php index 16d884973..29ac944bf 100644 --- a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php +++ b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -132,7 +132,7 @@ class PurchaseUnitFactory { $description, $payee, $custom_id, - $invoice_id, + $invoice_id . $retry, $soft_descriptor ); return apply_filters( diff --git a/modules/ppcp-vaulting/src/PaymentTokenRepository.php b/modules/ppcp-vaulting/src/PaymentTokenRepository.php index d159aae41..f045c3a0a 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenRepository.php +++ b/modules/ppcp-vaulting/src/PaymentTokenRepository.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Vaulting; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory; @@ -121,6 +122,26 @@ class PaymentTokenRepository { return $this->token_contains_source( $tokens, 'paypal' ); } + /** + * Check if tokens has the given payment source. + * + * @param PaymentToken[] $tokens The tokens. + * @param PaymentSource|null $payment_source The payment source. + * @return bool Whether tokens contains payment source or not. + */ + public function tokens_contains_payment_source( array $tokens, PaymentSource $payment_source ): bool { + + if ( $this->tokens_contains_card( $tokens ) ) { + foreach ( $tokens as $token ) { + if ( $payment_source->card()->last_digits() === $token->source()->card->last_digits ) { + return true; + } + } + } + + return false; + } + /** * Fetch PaymentToken from PayPal for a user. * diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index 70a3e7f32..7d3e1e9a2 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -173,10 +173,12 @@ trait ProcessPaymentTrait { if ( $this->order_processor->process( $wc_order ) ) { if ( $this->subscription_helper->has_subscription( $order_id ) ) { - $this->logger->info( "Trying to save payment for subscription parent order #{$order_id}." ); + $this->logger->info( "Checking if payment for subscription parent order #{$order_id} is saved." ); $tokens = $this->payment_token_repository->all_for_user_id( $wc_order->get_customer_id() ); - if ( $tokens ) { + $current_payment_source = $this->session_handler->order()->payment_source(); + if ( $tokens && $this->payment_token_repository->tokens_contains_payment_source($tokens, $current_payment_source) ) { + $this->logger->info( "Payment for subscription parent order #{$order_id} was saved correctly." ); if ( $this->config->has( 'intent' ) && strtoupper( (string) $this->config->get( 'intent' ) ) === 'CAPTURE' ) { diff --git a/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php b/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php new file mode 100644 index 000000000..35697f599 --- /dev/null +++ b/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php @@ -0,0 +1,36 @@ +shouldReceive('card->last_digits')->andReturn('1234'); + + $token = Mockery::mock(PaymentToken::class); + $source = (object)[ + 'card' => (object)[ + 'last_digits' => '1234', + ], + ]; + $token->shouldReceive('source')->andReturn($source); + $tokens = [$token]; + + $factory = Mockery::mock(PaymentTokenFactory::class); + $endpoint = Mockery::mock(PaymentTokenEndpoint::class); + $testee = new PaymentTokenRepository($factory, $endpoint); + + self::assertTrue($testee->tokens_contains_payment_source($tokens, $paymentSource)); + } +} From 6cbb9fe54df67590e1cc245dad72532e819e5751 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Dec 2021 11:56:51 +0100 Subject: [PATCH 02/16] Add `brand` to card payment conditional --- modules/ppcp-vaulting/src/PaymentTokenRepository.php | 5 ++++- tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-vaulting/src/PaymentTokenRepository.php b/modules/ppcp-vaulting/src/PaymentTokenRepository.php index f045c3a0a..debfdeebc 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenRepository.php +++ b/modules/ppcp-vaulting/src/PaymentTokenRepository.php @@ -133,7 +133,10 @@ class PaymentTokenRepository { if ( $this->tokens_contains_card( $tokens ) ) { foreach ( $tokens as $token ) { - if ( $payment_source->card()->last_digits() === $token->source()->card->last_digits ) { + if ( + $payment_source->card()->last_digits() === $token->source()->card->last_digits + && $payment_source->card()->brand() === $token->source()->card->brand + ) { return true; } } diff --git a/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php b/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php index 35697f599..45f03281e 100644 --- a/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php +++ b/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php @@ -17,13 +17,16 @@ class PaymentTokenRepositoryTest extends TestCase { $paymentSource = Mockery::mock(PaymentSource::class); $paymentSource->shouldReceive('card->last_digits')->andReturn('1234'); + $paymentSource->shouldReceive('card->brand')->andReturn('VISA'); $token = Mockery::mock(PaymentToken::class); $source = (object)[ 'card' => (object)[ 'last_digits' => '1234', + 'brand' => 'VISA', ], ]; + $token->shouldReceive('source')->andReturn($source); $tokens = [$token]; From 30a74b78d8f6661ef2ff2181f604c4196bb30146 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Dec 2021 14:46:42 +0100 Subject: [PATCH 03/16] Add check if payment source is null condition --- modules/ppcp-vaulting/src/PaymentTokenRepository.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-vaulting/src/PaymentTokenRepository.php b/modules/ppcp-vaulting/src/PaymentTokenRepository.php index debfdeebc..5a64fc390 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenRepository.php +++ b/modules/ppcp-vaulting/src/PaymentTokenRepository.php @@ -129,7 +129,11 @@ class PaymentTokenRepository { * @param PaymentSource|null $payment_source The payment source. * @return bool Whether tokens contains payment source or not. */ - public function tokens_contains_payment_source( array $tokens, PaymentSource $payment_source ): bool { + public function tokens_contains_payment_source( array $tokens, ?PaymentSource $payment_source ): bool { + + if ( null === $payment_source ) { + return false; + } if ( $this->tokens_contains_card( $tokens ) ) { foreach ( $tokens as $token ) { From 39ec9f14a91c1942a7a325bed750abdeb4db6def Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Dec 2021 15:00:10 +0100 Subject: [PATCH 04/16] Ensure payment source is not null --- .../ppcp-vaulting/src/PaymentTokenRepository.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-vaulting/src/PaymentTokenRepository.php b/modules/ppcp-vaulting/src/PaymentTokenRepository.php index 5a64fc390..3b6bbd10c 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenRepository.php +++ b/modules/ppcp-vaulting/src/PaymentTokenRepository.php @@ -136,10 +136,19 @@ class PaymentTokenRepository { } if ( $this->tokens_contains_card( $tokens ) ) { + + $card = $payment_source->card(); + if ( null === $card ) { + return false; + } + + $last_digits = $card->last_digits(); + $brand = $card->brand(); + foreach ( $tokens as $token ) { if ( - $payment_source->card()->last_digits() === $token->source()->card->last_digits - && $payment_source->card()->brand() === $token->source()->card->brand + $last_digits && $last_digits === $token->source()->card->last_digits + && $brand && $brand === $token->source()->card->brand ) { return true; } From 1092298038aa402ea595553b12c813c7bb2a8bb8 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Dec 2021 15:10:33 +0100 Subject: [PATCH 05/16] Fix phpcs error --- modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index 7d3e1e9a2..aaca3c829 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -175,9 +175,9 @@ trait ProcessPaymentTrait { if ( $this->subscription_helper->has_subscription( $order_id ) ) { $this->logger->info( "Checking if payment for subscription parent order #{$order_id} is saved." ); - $tokens = $this->payment_token_repository->all_for_user_id( $wc_order->get_customer_id() ); + $tokens = $this->payment_token_repository->all_for_user_id( $wc_order->get_customer_id() ); $current_payment_source = $this->session_handler->order()->payment_source(); - if ( $tokens && $this->payment_token_repository->tokens_contains_payment_source($tokens, $current_payment_source) ) { + if ( $tokens && $this->payment_token_repository->tokens_contains_payment_source( $tokens, $current_payment_source ) ) { $this->logger->info( "Payment for subscription parent order #{$order_id} was saved correctly." ); From 2ba986b34280f31fe23474465ec3a7e1409af000 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 20 Dec 2021 09:43:36 +0100 Subject: [PATCH 06/16] Introduce vaulting payment token created webhook (wip) --- modules/ppcp-wc-gateway/services.php | 11 ++- .../Processor/AuthorizedPaymentsProcessor.php | 38 +++++++++- modules/ppcp-webhooks/services.php | 5 ++ .../src/Handler/VaultCreditCardCreated.php | 55 +++++++++++++++ .../src/Handler/VaultPaymentTokenCreated.php | 70 +++++++++++++++++++ 5 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 modules/ppcp-webhooks/src/Handler/VaultCreditCardCreated.php create mode 100644 modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 73239fc66..7368b9706 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -234,7 +234,16 @@ return array( $payments_endpoint = $container->get( 'api.endpoint.payments' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); $notice = $container->get( 'wcgateway.notice.authorize-order-action' ); - return new AuthorizedPaymentsProcessor( $order_endpoint, $payments_endpoint, $logger, $notice ); + $settings = $container->get( 'wcgateway.settings' ); + $subscription_helper = $container->get( 'subscription.helper' ); + return new AuthorizedPaymentsProcessor( + $order_endpoint, + $payments_endpoint, + $logger, + $notice, + $settings, + $subscription_helper + ); }, 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); diff --git a/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php b/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php index b6bd4e461..345d8ab8b 100644 --- a/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Processor; use Exception; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; @@ -18,6 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; +use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; @@ -72,6 +74,18 @@ class AuthorizedPaymentsProcessor { */ private $notice; + /** + * The settings. + * + * @var ContainerInterface + */ + private $config; + + /** + * @var SubscriptionHelper + */ + private $subscription_helper; + /** * AuthorizedPaymentsProcessor constructor. * @@ -79,18 +93,23 @@ class AuthorizedPaymentsProcessor { * @param PaymentsEndpoint $payments_endpoint The Payments endpoint. * @param LoggerInterface $logger The logger. * @param AuthorizeOrderActionNotice $notice The notice. + * @param ContainerInterface $config The settings. */ public function __construct( OrderEndpoint $order_endpoint, PaymentsEndpoint $payments_endpoint, LoggerInterface $logger, - AuthorizeOrderActionNotice $notice + AuthorizeOrderActionNotice $notice, + ContainerInterface $config, + SubscriptionHelper $subscription_helper ) { $this->order_endpoint = $order_endpoint; $this->payments_endpoint = $payments_endpoint; $this->logger = $logger; $this->notice = $notice; + $this->config = $config; + $this->subscription_helper = $subscription_helper; } /** @@ -188,6 +207,23 @@ class AuthorizedPaymentsProcessor { return false; } + public function capture_authorized_payments_for_customer(int $customer_id) { + + $wc_orders = wc_get_orders(array( + 'customer_id' => $customer_id, + 'status' => array('wc-on-hold'), + 'limit' => -1, + )); + + if ($this->config->has('intent') && strtoupper((string)$this->config->get('intent')) === 'CAPTURE') { + foreach ($wc_orders as $wc_order) { + if( $this->subscription_helper->has_subscription( $wc_order->get_id() ) ) { + $this->capture_authorized_payment($wc_order); + } + } + } + } + /** * Displays the notice for a status. * diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php index 13b0b5e54..f6bd410e9 100644 --- a/modules/ppcp-webhooks/services.php +++ b/modules/ppcp-webhooks/services.php @@ -23,6 +23,8 @@ use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureCompleted; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureRefunded; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureReversed; use Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultCreditCardCreated; +use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultPaymentTokenCreated; use WooCommerce\PayPalCommerce\Webhooks\Status\Assets\WebhooksStatusPageAssets; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation; @@ -67,12 +69,15 @@ return array( $logger = $container->get( 'woocommerce.logger.woocommerce' ); $prefix = $container->get( 'api.prefix' ); $order_endpoint = $container->get( 'api.endpoint.order' ); + $authorized_payments_processor = $container->get('wcgateway.processor.authorized-payments'); return array( new CheckoutOrderApproved( $logger, $prefix, $order_endpoint ), new CheckoutOrderCompleted( $logger, $prefix ), new PaymentCaptureRefunded( $logger, $prefix ), new PaymentCaptureReversed( $logger, $prefix ), new PaymentCaptureCompleted( $logger, $prefix, $order_endpoint ), + new VaultPaymentTokenCreated($logger, $prefix, $authorized_payments_processor), + new VaultCreditCardCreated($logger, $prefix), ); }, diff --git a/modules/ppcp-webhooks/src/Handler/VaultCreditCardCreated.php b/modules/ppcp-webhooks/src/Handler/VaultCreditCardCreated.php new file mode 100644 index 000000000..3950e4881 --- /dev/null +++ b/modules/ppcp-webhooks/src/Handler/VaultCreditCardCreated.php @@ -0,0 +1,55 @@ +logger = $logger; + $this->prefix = $prefix; + } + + public function event_types(): array + { + return array( + 'VAULT.CREDIT-CARD.CREATED', + ); + } + + public function responsible_for_request(\WP_REST_Request $request): bool + { + return in_array( $request['event_type'], $this->event_types(), true ); + } + + public function handle_request(\WP_REST_Request $request): \WP_REST_Response + { + $message = 'VAULT.CREDIT-CARD.CREATED received.'; + $this->logger->log('info', $message); + $response = array( + 'success' => true, + 'message' => $message, + ); + + return rest_ensure_response($response); + } +} diff --git a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php new file mode 100644 index 000000000..24f84705e --- /dev/null +++ b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php @@ -0,0 +1,70 @@ +logger = $logger; + $this->prefix = $prefix; + $this->authorized_payments_processor = $authorized_payments_processor; + } + + public function event_types(): array + { + return array( + 'VAULT.PAYMENT-TOKEN.CREATED', + ); + } + + public function responsible_for_request(\WP_REST_Request $request): bool + { + return in_array( $request['event_type'], $this->event_types(), true ); + } + + public function handle_request(\WP_REST_Request $request): \WP_REST_Response + { + $response = array( 'success' => false ); + $webhook_id = (string) ( $request['id'] ?? '' ); + + $customer_id = $resource['customer_id'] ?? ''; + if(!$customer_id) { + $message = sprintf( 'No customer id for webhook event %s was found.', $webhook_id ); + $this->logger->warning( $message, array( 'request' => $request ) ); + $response['message'] = $message; + return new \WP_REST_Response( $response ); + } + + $customer_id_parts = explode('-', $customer_id); + $this->authorized_payments_processor->capture_authorized_payments_for_customer((int) end($customer_id_parts)); + + $response['success'] = true; + return rest_ensure_response($response); + } +} From f03f3e5fb1468c6dabf4d0e031a8f495a1405808 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 20 Dec 2021 11:50:26 +0100 Subject: [PATCH 07/16] Do not rely on existing payment tokens after purchase but on webhooks --- .../src/PaymentTokenRepository.php | 37 ------- .../src/Gateway/ProcessPaymentTrait.php | 96 ------------------- .../Vaulting/PaymentTokenRepositoryTest.php | 39 -------- 3 files changed, 172 deletions(-) delete mode 100644 tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php diff --git a/modules/ppcp-vaulting/src/PaymentTokenRepository.php b/modules/ppcp-vaulting/src/PaymentTokenRepository.php index 3b6bbd10c..d159aae41 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenRepository.php +++ b/modules/ppcp-vaulting/src/PaymentTokenRepository.php @@ -10,7 +10,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Vaulting; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory; @@ -122,42 +121,6 @@ class PaymentTokenRepository { return $this->token_contains_source( $tokens, 'paypal' ); } - /** - * Check if tokens has the given payment source. - * - * @param PaymentToken[] $tokens The tokens. - * @param PaymentSource|null $payment_source The payment source. - * @return bool Whether tokens contains payment source or not. - */ - public function tokens_contains_payment_source( array $tokens, ?PaymentSource $payment_source ): bool { - - if ( null === $payment_source ) { - return false; - } - - if ( $this->tokens_contains_card( $tokens ) ) { - - $card = $payment_source->card(); - if ( null === $card ) { - return false; - } - - $last_digits = $card->last_digits(); - $brand = $card->brand(); - - foreach ( $tokens as $token ) { - if ( - $last_digits && $last_digits === $token->source()->card->last_digits - && $brand && $brand === $token->source()->card->brand - ) { - return true; - } - } - } - - return false; - } - /** * Fetch PaymentToken from PayPal for a user. * diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index 5f8130299..abf1e7b34 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -10,8 +10,6 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Exception; -use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; -use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; @@ -173,100 +171,6 @@ trait ProcessPaymentTrait { try { if ( $this->order_processor->process( $wc_order ) ) { - - if ( $this->subscription_helper->has_subscription( $order_id ) ) { - $this->logger->info( "Checking if payment for subscription parent order #{$order_id} is saved." ); - - $tokens = $this->payment_token_repository->all_for_user_id( $wc_order->get_customer_id() ); - $current_payment_source = $this->session_handler->order()->payment_source(); - if ( $tokens && $this->payment_token_repository->tokens_contains_payment_source( $tokens, $current_payment_source ) ) { - - $this->logger->info( "Payment for subscription parent order #{$order_id} was saved correctly." ); - - if ( $this->config->has( 'intent' ) && strtoupper( (string) $this->config->get( 'intent' ) ) === 'CAPTURE' ) { - $this->authorized_payments_processor->capture_authorized_payment( $wc_order ); - } - - $this->session_handler->destroy_session_data(); - - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - - $this->logger->error( "Payment for subscription parent order #{$order_id} was not saved." ); - - $paypal_order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ); - if ( ! $paypal_order_id ) { - throw new RuntimeException( 'PayPal order ID not found in meta.' ); - } - $order = $this->order_endpoint->order( $paypal_order_id ); - - $purchase_units = $order->purchase_units(); - if ( ! $purchase_units ) { - throw new RuntimeException( 'No purchase units.' ); - } - - $payments = $purchase_units[0]->payments(); - if ( ! $payments ) { - throw new RuntimeException( 'No payments.' ); - } - - $this->logger->debug( - sprintf( - 'Trying to void order %1$s, payments: %2$s.', - $order->id(), - wp_json_encode( $payments->to_array() ) - ) - ); - - $voidable_authorizations = array_filter( - $payments->authorizations(), - function ( Authorization $authorization ): bool { - return $authorization->is_voidable(); - } - ); - if ( ! $voidable_authorizations ) { - throw new RuntimeException( 'No voidable authorizations.' ); - } - - foreach ( $voidable_authorizations as $authorization ) { - $this->payments_endpoint->void( $authorization ); - } - - $this->logger->debug( - sprintf( - 'Order %1$s voided successfully.', - $order->id() - ) - ); - - $error_message = __( 'Could not process order because it was not possible to save the payment.', 'woocommerce-paypal-payments' ); - $wc_order->update_status( 'failed', $error_message ); - - $subscriptions = wcs_get_subscriptions_for_order( $order_id ); - foreach ( $subscriptions as $key => $subscription ) { - if ( $subscription->get_parent_id() === $order_id ) { - try { - $subscription->update_status( 'cancelled' ); - break; - } catch ( Exception $exception ) { - $this->logger->error( "Could not update cancelled status on subscription #{$subscription->get_id()} " . $exception->getMessage() ); - } - } - } - - // Adds retry counter meta to avoid duplicate invoice id error on consequent tries. - $wc_order->update_meta_data( 'ppcp-retry', (int) $wc_order->get_meta( 'ppcp-retry' ) + 1 ); - $wc_order->save_meta_data(); - - $this->session_handler->destroy_session_data(); - wc_add_notice( $error_message, 'error' ); - - return $failure_data; - } - WC()->cart->empty_cart(); $this->session_handler->destroy_session_data(); diff --git a/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php b/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php deleted file mode 100644 index 45f03281e..000000000 --- a/tests/PHPUnit/Vaulting/PaymentTokenRepositoryTest.php +++ /dev/null @@ -1,39 +0,0 @@ -shouldReceive('card->last_digits')->andReturn('1234'); - $paymentSource->shouldReceive('card->brand')->andReturn('VISA'); - - $token = Mockery::mock(PaymentToken::class); - $source = (object)[ - 'card' => (object)[ - 'last_digits' => '1234', - 'brand' => 'VISA', - ], - ]; - - $token->shouldReceive('source')->andReturn($source); - $tokens = [$token]; - - $factory = Mockery::mock(PaymentTokenFactory::class); - $endpoint = Mockery::mock(PaymentTokenEndpoint::class); - $testee = new PaymentTokenRepository($factory, $endpoint); - - self::assertTrue($testee->tokens_contains_payment_source($tokens, $paymentSource)); - } -} From 572c62c74c8d1e34be79355cecaf38fc667e1c21 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 20 Dec 2021 15:34:55 +0100 Subject: [PATCH 08/16] Check for captured vault webhook order meta --- modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php | 5 +++++ .../src/Processor/AuthorizedPaymentsProcessor.php | 3 ++- .../ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index abf1e7b34..c36499c4e 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -171,6 +171,11 @@ trait ProcessPaymentTrait { try { if ( $this->order_processor->process( $wc_order ) ) { + + if($this->subscription_helper->has_subscription( $wc_order->get_id() )) { + $wc_order->update_meta_data('_ppcp_captured_vault_webhook', false); + } + WC()->cart->empty_cart(); $this->session_handler->destroy_session_data(); diff --git a/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php b/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php index 345d8ab8b..a3c902481 100644 --- a/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php @@ -217,8 +217,9 @@ class AuthorizedPaymentsProcessor { if ($this->config->has('intent') && strtoupper((string)$this->config->get('intent')) === 'CAPTURE') { foreach ($wc_orders as $wc_order) { - if( $this->subscription_helper->has_subscription( $wc_order->get_id() ) ) { + if( $this->subscription_helper->has_subscription( $wc_order->get_id() ) && $wc_order->get_meta( '_ppcp_captured_vault_webhook' ) === false ) { $this->capture_authorized_payment($wc_order); + $wc_order->update_meta_data('_ppcp_captured_vault_webhook', true); } } } diff --git a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php index 24f84705e..49cd07232 100644 --- a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php +++ b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php @@ -53,7 +53,7 @@ class VaultPaymentTokenCreated implements RequestHandler $response = array( 'success' => false ); $webhook_id = (string) ( $request['id'] ?? '' ); - $customer_id = $resource['customer_id'] ?? ''; + $customer_id = $request['customer_id'] ?? ''; if(!$customer_id) { $message = sprintf( 'No customer id for webhook event %s was found.', $webhook_id ); $this->logger->warning( $message, array( 'request' => $request ) ); From 465855d0616b4517d09feb9458c4bd2570dbf7eb Mon Sep 17 00:00:00 2001 From: dinamiko Date: Wed, 22 Dec 2021 12:18:17 +0100 Subject: [PATCH 09/16] Check for captured vault webhook order meta --- modules/ppcp-wc-gateway/services.php | 4 +++- .../src/Gateway/ProcessPaymentTrait.php | 4 ---- .../src/Processor/AuthorizedPaymentsProcessor.php | 7 +++++-- .../src/Processor/OrderProcessor.php | 14 +++++++++++++- .../src/Handler/VaultPaymentTokenCreated.php | 5 +++-- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index f53ffd6e3..d5ec74e23 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -215,6 +215,7 @@ return array( $settings = $container->get( 'wcgateway.settings' ); $environment = $container->get( 'onboarding.environment' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); + $subscription_helper = $container->get( 'subscription.helper' ); return new OrderProcessor( $session_handler, $order_endpoint, @@ -223,7 +224,8 @@ return array( $authorized_payments_processor, $settings, $logger, - $environment + $environment, + $subscription_helper ); }, 'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor { diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index c36499c4e..e9d209053 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -172,10 +172,6 @@ trait ProcessPaymentTrait { try { if ( $this->order_processor->process( $wc_order ) ) { - if($this->subscription_helper->has_subscription( $wc_order->get_id() )) { - $wc_order->update_meta_data('_ppcp_captured_vault_webhook', false); - } - WC()->cart->empty_cart(); $this->session_handler->destroy_session_data(); diff --git a/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php b/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php index a3c902481..b632014a3 100644 --- a/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/AuthorizedPaymentsProcessor.php @@ -217,9 +217,12 @@ class AuthorizedPaymentsProcessor { if ($this->config->has('intent') && strtoupper((string)$this->config->get('intent')) === 'CAPTURE') { foreach ($wc_orders as $wc_order) { - if( $this->subscription_helper->has_subscription( $wc_order->get_id() ) && $wc_order->get_meta( '_ppcp_captured_vault_webhook' ) === false ) { + if( + $this->subscription_helper->has_subscription( $wc_order->get_id() ) + && $wc_order->get_meta('_ppcp_captured_vault_webhook') === 'false' + ) { $this->capture_authorized_payment($wc_order); - $wc_order->update_meta_data('_ppcp_captured_vault_webhook', true); + $wc_order->update_meta_data('_ppcp_captured_vault_webhook', 'true'); } } } diff --git a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php index 22b06ae1a..d7d911b40 100644 --- a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php @@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -98,6 +99,11 @@ class OrderProcessor { */ private $logger; + /** + * @var SubscriptionHelper + */ + private $subscription_helper; + /** * OrderProcessor constructor. * @@ -118,7 +124,8 @@ class OrderProcessor { AuthorizedPaymentsProcessor $authorized_payments_processor, Settings $settings, LoggerInterface $logger, - Environment $environment + Environment $environment, + SubscriptionHelper $subscription_helper ) { $this->session_handler = $session_handler; @@ -129,6 +136,7 @@ class OrderProcessor { $this->settings = $settings; $this->environment = $environment; $this->logger = $logger; + $this->subscription_helper = $subscription_helper; } /** @@ -172,6 +180,10 @@ class OrderProcessor { $order = $this->order_endpoint->authorize( $order ); $wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' ); + + if($this->subscription_helper->has_subscription( $wc_order->get_id() )) { + $wc_order->update_meta_data('_ppcp_captured_vault_webhook', 'false'); + } } $transaction_id = $this->get_paypal_order_transaction_id( $order ); diff --git a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php index 49cd07232..e83ca7381 100644 --- a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php +++ b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php @@ -53,7 +53,7 @@ class VaultPaymentTokenCreated implements RequestHandler $response = array( 'success' => false ); $webhook_id = (string) ( $request['id'] ?? '' ); - $customer_id = $request['customer_id'] ?? ''; + $customer_id = $request['resource']['customer_id'] ?? ''; if(!$customer_id) { $message = sprintf( 'No customer id for webhook event %s was found.', $webhook_id ); $this->logger->warning( $message, array( 'request' => $request ) ); @@ -62,7 +62,8 @@ class VaultPaymentTokenCreated implements RequestHandler } $customer_id_parts = explode('-', $customer_id); - $this->authorized_payments_processor->capture_authorized_payments_for_customer((int) end($customer_id_parts)); + $wc_customer_id = (int) end($customer_id_parts); + $this->authorized_payments_processor->capture_authorized_payments_for_customer($wc_customer_id); $response['success'] = true; return rest_ensure_response($response); From a8a8121d3c71802262f3558110e37cc0c21d217c Mon Sep 17 00:00:00 2001 From: dinamiko Date: Fri, 25 Feb 2022 09:37:05 +0100 Subject: [PATCH 10/16] Update readme --- readme.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.txt b/readme.txt index 9c84a2903..9965ddf72 100644 --- a/readme.txt +++ b/readme.txt @@ -86,6 +86,7 @@ Follow the steps below to connect the plugin to your PayPal account: * Fix - Multi-currency broke #481 * Fix - Address information from PayPal shortcut flow not loaded #451 * Fix - WooCommerce as mu-plugin is not detected as active #461 +* Fix - Check if PayPal Payments is an available gateway before displaying it on Product/Cart pages #447 * Enhancement - Improve onboarding flow, allow no card processing #443 #508 #510 * Enhancement - Add Germany to supported ACDC countries #459 * Enhancement - Add filters to allow ACDC for countries #437 From 89392e43bd92a4ef297d616d78275912ebb83368 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 28 Feb 2022 10:06:05 +0100 Subject: [PATCH 11/16] Display error message for when error on hosted fields approve payment --- .../resources/js/modules/Renderer/CreditCardRenderer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index a4eb8f146..c67caa236 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -216,8 +216,11 @@ class CreditCardRenderer { this.spinner.unblock(); return contextConfig.onApprove(payload); }).catch(err => { - console.error(err); this.spinner.unblock(); + this.errorHandler.clear(); + if (err.details.length > 0) { + this.errorHandler.message(err.details.map(d => `${d.issue} ${d.description}`).join('
'), true); + } }); } else { this.spinner.unblock(); From 21013adbca1e554a0adee5c0099b55b4ad2e3226 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 28 Feb 2022 12:56:44 +0100 Subject: [PATCH 12/16] Do not display buttons in minicart if subscription in the cart and vaulting disabled --- .../js/modules/ContextBootstrap/MiniCartBootstap.js | 11 +++++++---- .../js/modules/Renderer/CreditCardRenderer.js | 2 +- modules/ppcp-button/src/Assets/SmartButton.php | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js index 090707ff3..1942417a0 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js @@ -22,9 +22,12 @@ class MiniCartBootstap { } shouldRender() { - return document.querySelector(this.gateway.button.mini_cart_wrapper) !== - null || document.querySelector(this.gateway.hosted_fields.mini_cart_wrapper) !== - null; + if (!this.gateway.can_save_vault_token) { + return; + } + + return document.querySelector(this.gateway.button.mini_cart_wrapper) !== null + || document.querySelector(this.gateway.hosted_fields.mini_cart_wrapper) !== null; } render() { @@ -40,4 +43,4 @@ class MiniCartBootstap { } } -export default MiniCartBootstap; \ No newline at end of file +export default MiniCartBootstap; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index a4eb8f146..c22945073 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -181,7 +181,7 @@ class CreditCardRenderer { this.errorHandler.clear(); if (this.formValid && this.cardValid) { - const save_card = this.defaultConfig.save_card ? true : false; + const save_card = this.defaultConfig.can_save_vault_token ? true : false; let vault = document.getElementById('ppcp-credit-card-vault') ? document.getElementById('ppcp-credit-card-vault').checked : save_card; if (this.defaultConfig.enforce_vault) { diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 6b15df926..cdb40ec6c 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -686,7 +686,7 @@ class SmartButton implements SmartButtonInterface { ), ), 'enforce_vault' => $this->has_subscriptions(), - 'save_card' => $this->can_save_vault_token(), + 'can_save_vault_token' => $this->can_save_vault_token(), 'bn_codes' => $this->bn_codes(), 'payer' => $this->payerData(), 'button' => array( From 5110209cfe7323a986a474e78240f1a7e9eaae05 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 28 Feb 2022 14:14:14 +0100 Subject: [PATCH 13/16] Format code style --- modules/ppcp-button/src/Assets/SmartButton.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index cdb40ec6c..7c362d91e 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -686,7 +686,7 @@ class SmartButton implements SmartButtonInterface { ), ), 'enforce_vault' => $this->has_subscriptions(), - 'can_save_vault_token' => $this->can_save_vault_token(), + 'can_save_vault_token' => $this->can_save_vault_token(), 'bn_codes' => $this->bn_codes(), 'payer' => $this->payerData(), 'button' => array( From e00df49ef3cbfe9152ec33e64ec41b6eb47c5071 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 28 Feb 2022 14:25:12 +0100 Subject: [PATCH 14/16] Remove changelog entry from readme, it does not belong to this PR --- readme.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/readme.txt b/readme.txt index 9965ddf72..9c84a2903 100644 --- a/readme.txt +++ b/readme.txt @@ -86,7 +86,6 @@ Follow the steps below to connect the plugin to your PayPal account: * Fix - Multi-currency broke #481 * Fix - Address information from PayPal shortcut flow not loaded #451 * Fix - WooCommerce as mu-plugin is not detected as active #461 -* Fix - Check if PayPal Payments is an available gateway before displaying it on Product/Cart pages #447 * Enhancement - Improve onboarding flow, allow no card processing #443 #508 #510 * Enhancement - Add Germany to supported ACDC countries #459 * Enhancement - Add filters to allow ACDC for countries #437 From af1fc4845a919b193fcc4f68f756c73c17b47727 Mon Sep 17 00:00:00 2001 From: Danae Millan Date: Mon, 28 Feb 2022 22:50:31 -0500 Subject: [PATCH 15/16] Update release date for 1.7.0 --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 43b324f43..e93997ad8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ *** Changelog *** -= 1.7.0 - TBD = += 1.7.0 - 2022-02-28 = * Fix - DCC orders randomly failing #503 * Fix - Multi-currency broke #481 * Fix - Address information from PayPal shortcut flow not loaded #451 From f1b11d74d398dc95b44338a7bc3480e5403daf0e Mon Sep 17 00:00:00 2001 From: dinamiko Date: Wed, 2 Mar 2022 12:18:46 +0100 Subject: [PATCH 16/16] Remove unused variable --- modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php index 9ffca1f75..37c371fb0 100644 --- a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php +++ b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -131,7 +131,7 @@ class PurchaseUnitFactory { $description, $payee, $custom_id, - $invoice_id . $retry, + $invoice_id, $soft_descriptor ); /**