From 08a2b15fcb853781de434a1172365a651be458bf Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 10 Jul 2023 12:32:31 +0200 Subject: [PATCH 01/60] Log order data on order does not contain intent error --- modules/ppcp-api-client/services.php | 4 +++- .../ppcp-api-client/src/Factory/OrderFactory.php | 14 +++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index ce363dabc..689561449 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -361,12 +361,14 @@ return array( $application_context_repository = $container->get( 'api.repository.application-context' ); $application_context_factory = $container->get( 'api.factory.application-context' ); $payment_source_factory = $container->get( 'api.factory.payment-source' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new OrderFactory( $purchase_unit_factory, $payer_factory, $application_context_repository, $application_context_factory, - $payment_source_factory + $payment_source_factory, + $logger ); }, 'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory { diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index 7a7f057db..7ffbffcb9 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Factory; +use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; @@ -55,6 +56,13 @@ class OrderFactory { */ private $payment_source_factory; + /** + * The logger. + * + * @var LoggerInterface + */ + private LoggerInterface $logger; + /** * OrderFactory constructor. * @@ -63,13 +71,15 @@ class OrderFactory { * @param ApplicationContextRepository $application_context_repository The Application Context repository. * @param ApplicationContextFactory $application_context_factory The Application Context factory. * @param PaymentSourceFactory $payment_source_factory The Payment Source factory. + * @param LoggerInterface $logger The logger. */ public function __construct( PurchaseUnitFactory $purchase_unit_factory, PayerFactory $payer_factory, ApplicationContextRepository $application_context_repository, ApplicationContextFactory $application_context_factory, - PaymentSourceFactory $payment_source_factory + PaymentSourceFactory $payment_source_factory, + LoggerInterface $logger ) { $this->purchase_unit_factory = $purchase_unit_factory; @@ -77,6 +87,7 @@ class OrderFactory { $this->application_context_repository = $application_context_repository; $this->application_context_factory = $application_context_factory; $this->payment_source_factory = $payment_source_factory; + $this->logger = $logger; } /** @@ -128,6 +139,7 @@ class OrderFactory { ); } if ( ! isset( $order_data->intent ) ) { + $this->logger->info(wc_print_r($order_data, true)); throw new RuntimeException( __( 'Order does not contain intent.', 'woocommerce-paypal-payments' ) ); From 38bf05577e6100bb9b836580895a864dc4dbbf1d Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 10 Jul 2023 16:15:00 +0200 Subject: [PATCH 02/60] Move SCA payment indicator from `application_context` to `payment_source` --- modules/ppcp-api-client/services.php | 4 +-- .../src/Factory/OrderFactory.php | 13 +-------- .../src/SubscriptionModule.php | 27 +++++-------------- 3 files changed, 9 insertions(+), 35 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 689561449..ce363dabc 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -361,14 +361,12 @@ return array( $application_context_repository = $container->get( 'api.repository.application-context' ); $application_context_factory = $container->get( 'api.factory.application-context' ); $payment_source_factory = $container->get( 'api.factory.payment-source' ); - $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new OrderFactory( $purchase_unit_factory, $payer_factory, $application_context_repository, $application_context_factory, - $payment_source_factory, - $logger + $payment_source_factory ); }, 'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory { diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index 7ffbffcb9..50540ea95 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Factory; -use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; @@ -56,13 +55,6 @@ class OrderFactory { */ private $payment_source_factory; - /** - * The logger. - * - * @var LoggerInterface - */ - private LoggerInterface $logger; - /** * OrderFactory constructor. * @@ -71,15 +63,13 @@ class OrderFactory { * @param ApplicationContextRepository $application_context_repository The Application Context repository. * @param ApplicationContextFactory $application_context_factory The Application Context factory. * @param PaymentSourceFactory $payment_source_factory The Payment Source factory. - * @param LoggerInterface $logger The logger. */ public function __construct( PurchaseUnitFactory $purchase_unit_factory, PayerFactory $payer_factory, ApplicationContextRepository $application_context_repository, ApplicationContextFactory $application_context_factory, - PaymentSourceFactory $payment_source_factory, - LoggerInterface $logger + PaymentSourceFactory $payment_source_factory ) { $this->purchase_unit_factory = $purchase_unit_factory; @@ -87,7 +77,6 @@ class OrderFactory { $this->application_context_repository = $application_context_repository; $this->application_context_factory = $application_context_factory; $this->payment_source_factory = $payment_source_factory; - $this->logger = $logger; } /** diff --git a/modules/ppcp-subscription/src/SubscriptionModule.php b/modules/ppcp-subscription/src/SubscriptionModule.php index 099ee18ff..018fbccfe 100644 --- a/modules/ppcp-subscription/src/SubscriptionModule.php +++ b/modules/ppcp-subscription/src/SubscriptionModule.php @@ -123,29 +123,16 @@ class SubscriptionModule implements ModuleInterface { && isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN' && isset( $data['payment_source']['token']['source']->card ) ) { - $renewal_order_id = absint( $data['purchase_units'][0]['custom_id'] ); - $subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order_id ); - $subscriptions_values = array_values( $subscriptions ); - $latest_subscription = array_shift( $subscriptions_values ); - if ( is_a( $latest_subscription, WC_Subscription::class ) ) { - $related_renewal_orders = $latest_subscription->get_related_orders( 'ids', 'renewal' ); - $latest_order_id_with_transaction = array_slice( $related_renewal_orders, 1, 1, false ); - $order_id = ! empty( $latest_order_id_with_transaction ) ? $latest_order_id_with_transaction[0] : 0; - if ( count( $related_renewal_orders ) === 1 ) { - $order_id = $latest_subscription->get_parent_id(); - } - - $wc_order = wc_get_order( $order_id ); - if ( is_a( $wc_order, WC_Order::class ) ) { - $transaction_id = $wc_order->get_transaction_id(); - $data['application_context']['stored_payment_source'] = array( + $data['payment_source'] = array( + 'card' => array( + 'vault_id' => $data['payment_source']['token']['id'], + 'stored_credential' => array( 'payment_initiator' => 'MERCHANT', 'payment_type' => 'RECURRING', 'usage' => 'SUBSEQUENT', - 'previous_transaction_reference' => $transaction_id, - ); - } - } + ), + ), + ); } return $data; From 518651b6a93e50858c5ca3ebbeb348e7be0b4bc8 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 10 Jul 2023 17:37:20 +0200 Subject: [PATCH 03/60] Add `previous_transaction_reference` into subscription order meta --- modules/ppcp-subscription/src/RenewalHandler.php | 6 ++++++ modules/ppcp-subscription/src/SubscriptionModule.php | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/modules/ppcp-subscription/src/RenewalHandler.php b/modules/ppcp-subscription/src/RenewalHandler.php index 3253c2f54..b47fb43a8 100644 --- a/modules/ppcp-subscription/src/RenewalHandler.php +++ b/modules/ppcp-subscription/src/RenewalHandler.php @@ -204,6 +204,12 @@ class RenewalHandler { $transaction_id = $this->get_paypal_order_transaction_id( $order ); if ( $transaction_id ) { $this->update_transaction_id( $transaction_id, $wc_order ); + + $subscriptions = wcs_get_subscriptions_for_order( $wc_order->get_id(), array( 'order_type' => 'any' ) ); + foreach ( $subscriptions as $id => $subscription ) { + $subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id ); + $subscription->save(); + } } $this->handle_new_order_status( $order, $wc_order ); diff --git a/modules/ppcp-subscription/src/SubscriptionModule.php b/modules/ppcp-subscription/src/SubscriptionModule.php index 018fbccfe..3d14b4663 100644 --- a/modules/ppcp-subscription/src/SubscriptionModule.php +++ b/modules/ppcp-subscription/src/SubscriptionModule.php @@ -133,6 +133,11 @@ class SubscriptionModule implements ModuleInterface { ), ), ); + + $previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' ); + if ( $previous_transaction_reference ) { + $data['payment_source']['card']['stored_credential']['previous_transaction_reference'] = $previous_transaction_reference; + } } return $data; From 56d16a5d8a58345f6780c77dc5fdfc18cda2d2c7 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 11 Jul 2023 11:19:50 +0200 Subject: [PATCH 04/60] Add previous transaction to parent order (WIP) --- modules/ppcp-api-client/src/Factory/OrderFactory.php | 1 - modules/ppcp-subscription/src/SubscriptionModule.php | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index 50540ea95..7a7f057db 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -128,7 +128,6 @@ class OrderFactory { ); } if ( ! isset( $order_data->intent ) ) { - $this->logger->info(wc_print_r($order_data, true)); throw new RuntimeException( __( 'Order does not contain intent.', 'woocommerce-paypal-payments' ) ); diff --git a/modules/ppcp-subscription/src/SubscriptionModule.php b/modules/ppcp-subscription/src/SubscriptionModule.php index 3d14b4663..4f971e13e 100644 --- a/modules/ppcp-subscription/src/SubscriptionModule.php +++ b/modules/ppcp-subscription/src/SubscriptionModule.php @@ -73,6 +73,12 @@ class SubscriptionModule implements ModuleInterface { $logger = $c->get( 'woocommerce.logger.woocommerce' ); $this->add_payment_token_id( $subscription, $payment_token_repository, $logger ); + + if(count($subscription->get_related_orders()) === 1) { + $parent_order = $subscription->get_parent(); + // get transaction id + // add trans as meta to sub + } } ); From ab1880f0da2d8b8c3d958d12f4cf90ff802c700a Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 11 Jul 2023 12:44:30 +0200 Subject: [PATCH 05/60] Add parent order previous transaction id to subscription --- .../ppcp-subscription/src/SubscriptionModule.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-subscription/src/SubscriptionModule.php b/modules/ppcp-subscription/src/SubscriptionModule.php index 4f971e13e..a33c189e8 100644 --- a/modules/ppcp-subscription/src/SubscriptionModule.php +++ b/modules/ppcp-subscription/src/SubscriptionModule.php @@ -13,6 +13,7 @@ use Exception; use WC_Product; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; @@ -26,6 +27,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; @@ -34,6 +36,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; */ class SubscriptionModule implements ModuleInterface { + use TransactionIdHandlingTrait; + /** * {@inheritDoc} */ @@ -76,8 +80,15 @@ class SubscriptionModule implements ModuleInterface { if(count($subscription->get_related_orders()) === 1) { $parent_order = $subscription->get_parent(); - // get transaction id - // add trans as meta to sub + if ( is_a( $parent_order, WC_Order::class ) ) { + $order_repository = $c->get('api.repository.order'); + $order = $order_repository->for_wc_order($parent_order); + $transaction_id = $this->get_paypal_order_transaction_id( $order ); + if ( $transaction_id ) { + $subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id ); + $subscription->save(); + } + } } } ); From f1eda7d3e31299287cb9ea725a57b61e976a18b2 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 11 Jul 2023 14:20:11 +0200 Subject: [PATCH 06/60] Fix phpunit --- tests/PHPUnit/Subscription/RenewalHandlerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/PHPUnit/Subscription/RenewalHandlerTest.php b/tests/PHPUnit/Subscription/RenewalHandlerTest.php index d079012c3..a73ad7767 100644 --- a/tests/PHPUnit/Subscription/RenewalHandlerTest.php +++ b/tests/PHPUnit/Subscription/RenewalHandlerTest.php @@ -25,6 +25,7 @@ use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; +use function Brain\Monkey\Functions\when; class RenewalHandlerTest extends TestCase { @@ -154,6 +155,8 @@ class RenewalHandlerTest extends TestCase ->with([$purchaseUnit], 'no_shipping', $payer, $token) ->andReturn($order); + when('wcs_get_subscriptions_for_order')->justReturn(array()); + $wcOrder->shouldReceive('update_status'); $wcOrder->shouldReceive('save'); From 3dc6d3622f0d2ded4f77610e9afc2960f8bd84c1 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 11 Jul 2023 14:24:40 +0200 Subject: [PATCH 07/60] Fix phpcs --- modules/ppcp-subscription/src/SubscriptionModule.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-subscription/src/SubscriptionModule.php b/modules/ppcp-subscription/src/SubscriptionModule.php index a33c189e8..a59713228 100644 --- a/modules/ppcp-subscription/src/SubscriptionModule.php +++ b/modules/ppcp-subscription/src/SubscriptionModule.php @@ -78,12 +78,12 @@ class SubscriptionModule implements ModuleInterface { $this->add_payment_token_id( $subscription, $payment_token_repository, $logger ); - if(count($subscription->get_related_orders()) === 1) { + if ( count( $subscription->get_related_orders() ) === 1 ) { $parent_order = $subscription->get_parent(); if ( is_a( $parent_order, WC_Order::class ) ) { - $order_repository = $c->get('api.repository.order'); - $order = $order_repository->for_wc_order($parent_order); - $transaction_id = $this->get_paypal_order_transaction_id( $order ); + $order_repository = $c->get( 'api.repository.order' ); + $order = $order_repository->for_wc_order( $parent_order ); + $transaction_id = $this->get_paypal_order_transaction_id( $order ); if ( $transaction_id ) { $subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id ); $subscription->save(); From 81a735751baed22146fae08ca028aaca720111d2 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Fri, 21 Jul 2023 13:39:48 +0200 Subject: [PATCH 08/60] Use guest id if exist in user meta --- .../src/Handler/VaultPaymentTokenCreated.php | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php index 8f2b1abce..03417025c 100644 --- a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php +++ b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php @@ -107,12 +107,18 @@ class VaultPaymentTokenCreated implements RequestHandler { $customer_id = null !== $request['resource'] && isset( $request['resource']['customer_id'] ) ? $request['resource']['customer_id'] : ''; + if ( ! $customer_id ) { $message = 'No customer id was found.'; return $this->failure_response( $message ); } - $wc_customer_id = (int) str_replace( $this->prefix, '', $customer_id ); + $wc_customer_id = $this->wc_customer_id_from( $customer_id ); + if ( ! $wc_customer_id ) { + $message = "No WC customer id was found from PayPal customer id {$customer_id}"; + return $this->failure_response( $message ); + } + $this->authorized_payments_processor->capture_authorized_payments_for_customer( $wc_customer_id ); if ( ! is_null( $request['resource'] ) && isset( $request['resource']['id'] ) ) { @@ -149,4 +155,29 @@ class VaultPaymentTokenCreated implements RequestHandler { return $this->success_response(); } + + /** + * Returns WC customer id from PayPal customer id. + * + * @param string $customer_id The customer ID from PayPal. + * @return int + */ + private function wc_customer_id_from( string $customer_id ): int { + $customers = get_users( + array( + 'meta_key' => 'ppcp_guest_customer_id', + 'meta_value' => $customer_id, + 'fields' => 'ids', + 'number' => 1, + ) + ); + + $wc_customer_id = $customers[0] ?? ''; + if ( $wc_customer_id ) { + return (int) $wc_customer_id; + } + + $id = str_replace( $this->prefix, '', $customer_id ); + return is_numeric( $id ) ? (int) $id : 0; + } } From ff7e5050286a618aabb2c97a3ef22927de5de72e Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Fri, 21 Jul 2023 15:49:32 +0200 Subject: [PATCH 09/60] Increase saved payment checking time to allow webhook to arrive before --- modules/ppcp-vaulting/src/VaultingModule.php | 1 + modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index 5b7251abb..7386dae42 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -72,6 +72,7 @@ class VaultingModule implements ModuleInterface { 'woocommerce_paypal_payments_check_saved_payment', function ( int $order_id, int $customer_id, string $intent ) use ( $container ) { $payment_token_checker = $container->get( 'vaulting.payment-token-checker' ); + assert( $payment_token_checker instanceof PaymentTokenChecker ); $payment_token_checker->check_and_update( $order_id, $customer_id, $intent ); }, 10, diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index 1772b2838..ab16ec178 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -40,7 +40,7 @@ trait ProcessPaymentTrait { * @param int $customer_id The customer ID. */ protected function schedule_saved_payment_check( int $wc_order_id, int $customer_id ): void { - $timestamp = 1 * MINUTE_IN_SECONDS; + $timestamp = 3 * MINUTE_IN_SECONDS; if ( $this->config->has( 'subscription_behavior_when_vault_fails' ) && $this->config->get( 'subscription_behavior_when_vault_fails' ) === 'capture_auth' From 0278db9787513dfe57f1641898b673ded0b98d81 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 24 Jul 2023 12:04:03 +0200 Subject: [PATCH 10/60] Ignore phpcs warnings --- .../ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php index 03417025c..152932556 100644 --- a/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php +++ b/modules/ppcp-webhooks/src/Handler/VaultPaymentTokenCreated.php @@ -165,8 +165,8 @@ class VaultPaymentTokenCreated implements RequestHandler { private function wc_customer_id_from( string $customer_id ): int { $customers = get_users( array( - 'meta_key' => 'ppcp_guest_customer_id', - 'meta_value' => $customer_id, + 'meta_key' => 'ppcp_guest_customer_id', //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => $customer_id, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'fields' => 'ids', 'number' => 1, ) From c15b8ee463cc9ec2b9c6db80cd3feafc3e18e811 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 2 Aug 2023 17:09:06 +0100 Subject: [PATCH 11/60] Refactor purchase_item ditching behaviour to allow for extra line item with roundings. --- modules/ppcp-api-client/src/Entity/Item.php | 5 +- modules/ppcp-api-client/src/Entity/Money.php | 19 +- .../src/Entity/PurchaseUnit.php | 117 +------- .../src/Helper/MoneyFormatter.php | 21 +- .../src/Helper/PurchaseUnitSanitizer.php | 270 ++++++++++++++++++ 5 files changed, 317 insertions(+), 115 deletions(-) create mode 100644 modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php diff --git a/modules/ppcp-api-client/src/Entity/Item.php b/modules/ppcp-api-client/src/Entity/Item.php index efcaa179d..13f4702ba 100644 --- a/modules/ppcp-api-client/src/Entity/Item.php +++ b/modules/ppcp-api-client/src/Entity/Item.php @@ -201,12 +201,13 @@ class Item { /** * Returns the object as array. * + * @param bool $round_to_floor If value rounding should be floor. * @return array */ - public function to_array() { + public function to_array( bool $round_to_floor = false ): array { $item = array( 'name' => $this->name(), - 'unit_amount' => $this->unit_amount()->to_array(), + 'unit_amount' => $this->unit_amount()->to_array( $round_to_floor ), 'quantity' => $this->quantity(), 'description' => $this->description(), 'sku' => $this->sku(), diff --git a/modules/ppcp-api-client/src/Entity/Money.php b/modules/ppcp-api-client/src/Entity/Money.php index 5ba49dc43..e8f638feb 100644 --- a/modules/ppcp-api-client/src/Entity/Money.php +++ b/modules/ppcp-api-client/src/Entity/Money.php @@ -62,10 +62,11 @@ class Money { /** * The value formatted as string for API requests. * + * @param bool $round_to_floor If value rounding should be floor. * @return string */ - public function value_str(): string { - return $this->money_formatter->format( $this->value, $this->currency_code ); + public function value_str( bool $round_to_floor = false ): string { + return $this->money_formatter->format( $this->value, $this->currency_code, $round_to_floor ); } /** @@ -80,12 +81,22 @@ class Money { /** * Returns the object as array. * + * @param bool $round_to_floor If value rounding should be floor. * @return array */ - public function to_array(): array { + public function to_array( bool $round_to_floor = false ): array { return array( 'currency_code' => $this->currency_code(), - 'value' => $this->value_str(), + 'value' => $this->value_str( $round_to_floor ), ); } + + /** + * Indicates if the default rounding for this value is rounding up. + * + * @return bool + */ + public function is_rounding_up(): bool { + return (float) $this->value_str() > $this->value; + } } diff --git a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php index f2c64c73a..9359b422b 100644 --- a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php +++ b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Entity; +use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer; + /** * Class PurchaseUnit */ @@ -277,11 +279,11 @@ class PurchaseUnit { /** * Returns the object as array. * - * @param bool $ditch_items_when_mismatch Whether ditch items when mismatch or not. + * @param bool $sanitize_output Whether output should be sanitized for PayPal consumption. * * @return array */ - public function to_array( bool $ditch_items_when_mismatch = true ): array { + public function to_array( bool $sanitize_output = true ): array { $purchase_unit = array( 'reference_id' => $this->reference_id(), 'amount' => $this->amount()->to_array(), @@ -294,17 +296,6 @@ class PurchaseUnit { ), ); - $ditch = $ditch_items_when_mismatch && $this->ditch_items_when_mismatch( $this->amount(), ...$this->items() ); - /** - * The filter can be used to control when the items and totals breakdown are removed from PayPal order info. - */ - $ditch = apply_filters( 'ppcp_ditch_items_breakdown', $ditch, $this ); - - if ( $ditch ) { - unset( $purchase_unit['items'] ); - unset( $purchase_unit['amount']['breakdown'] ); - } - if ( $this->payee() ) { $purchase_unit['payee'] = $this->payee()->to_array(); } @@ -325,101 +316,11 @@ class PurchaseUnit { if ( $this->soft_descriptor() ) { $purchase_unit['soft_descriptor'] = $this->soft_descriptor(); } + + if ( $sanitize_output ) { + $purchase_unit = ( new PurchaseUnitSanitizer( $purchase_unit, $this->items() ) )->sanitize(); + } + return $purchase_unit; } - - /** - * All money values send to PayPal can only have 2 decimal points. WooCommerce internally does - * not have this restriction. Therefore the totals of the cart in WooCommerce and the totals - * of the rounded money values of the items, we send to PayPal, can differ. In those cases, - * we can not send the line items. - * - * @param Amount $amount The amount. - * @param Item ...$items The items. - * @return bool - */ - private function ditch_items_when_mismatch( Amount $amount, Item ...$items ): bool { - $breakdown = $amount->breakdown(); - if ( ! $breakdown ) { - return false; - } - - $item_total = $breakdown->item_total(); - if ( $item_total ) { - $remaining_item_total = array_reduce( - $items, - function ( float $total, Item $item ): float { - return $total - (float) $item->unit_amount()->value_str() * (float) $item->quantity(); - }, - (float) $item_total->value_str() - ); - - $remaining_item_total = round( $remaining_item_total, 2 ); - - if ( 0.0 !== $remaining_item_total ) { - return true; - } - } - - $tax_total = $breakdown->tax_total(); - $items_with_tax = array_filter( - $this->items, - function ( Item $item ): bool { - return null !== $item->tax(); - } - ); - if ( $tax_total && ! empty( $items_with_tax ) ) { - $remaining_tax_total = array_reduce( - $items, - function ( float $total, Item $item ): float { - $tax = $item->tax(); - if ( $tax ) { - $total -= (float) $tax->value_str() * (float) $item->quantity(); - } - return $total; - }, - (float) $tax_total->value_str() - ); - - $remaining_tax_total = round( $remaining_tax_total, 2 ); - - if ( 0.0 !== $remaining_tax_total ) { - return true; - } - } - - $shipping = $breakdown->shipping(); - $discount = $breakdown->discount(); - $shipping_discount = $breakdown->shipping_discount(); - $handling = $breakdown->handling(); - $insurance = $breakdown->insurance(); - - $amount_total = 0.0; - if ( $shipping ) { - $amount_total += (float) $shipping->value_str(); - } - if ( $item_total ) { - $amount_total += (float) $item_total->value_str(); - } - if ( $discount ) { - $amount_total -= (float) $discount->value_str(); - } - if ( $tax_total ) { - $amount_total += (float) $tax_total->value_str(); - } - if ( $shipping_discount ) { - $amount_total -= (float) $shipping_discount->value_str(); - } - if ( $handling ) { - $amount_total += (float) $handling->value_str(); - } - if ( $insurance ) { - $amount_total += (float) $insurance->value_str(); - } - - $amount_str = $amount->value_str(); - $amount_total_str = ( new Money( $amount_total, $amount->currency_code() ) )->value_str(); - $needs_to_ditch = $amount_str !== $amount_total_str; - return $needs_to_ditch; - } } diff --git a/modules/ppcp-api-client/src/Helper/MoneyFormatter.php b/modules/ppcp-api-client/src/Helper/MoneyFormatter.php index 447ba0a0e..3e19a10f5 100644 --- a/modules/ppcp-api-client/src/Helper/MoneyFormatter.php +++ b/modules/ppcp-api-client/src/Helper/MoneyFormatter.php @@ -25,12 +25,31 @@ class MoneyFormatter { * * @param float $value The value. * @param string $currency The 3-letter currency code. + * @param bool $round_to_floor If value rounding should be floor. * * @return string */ - public function format( float $value, string $currency ): string { + public function format( float $value, string $currency, bool $round_to_floor = false ): string { + if ( $round_to_floor ) { + return in_array( $currency, $this->currencies_without_decimals, true ) + ? (string) floor( $value ) + : number_format( $this->floor_with_decimals( $value, 2 ), 2, '.', '' ); + } + return in_array( $currency, $this->currencies_without_decimals, true ) ? (string) round( $value, 0 ) : number_format( $value, 2, '.', '' ); } + + /** + * Rounds to floor with decimal precision. + * + * @param float $value The value. + * @param int $decimals The number of decimals. + * @return float + */ + private function floor_with_decimals( float $value, int $decimals = 0 ): float { + $adjustment = (float) pow( 10, $decimals ); + return floor( $value * $adjustment ) / $adjustment; + } } diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php new file mode 100644 index 000000000..a6bc1a289 --- /dev/null +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -0,0 +1,270 @@ +purchase_unit = $purchase_unit; + $this->item_objects = $item_objects; + } + + /** + * The purchase_unit amount. + * + * @return array + */ + private function amount(): array { + return $this->purchase_unit['amount'] ?? array(); + } + + /** + * The purchase_unit currency code. + * + * @return string + */ + private function currency_code(): string { + return (string) ( $this->amount()['currency_code'] ?? '' ); + } + + /** + * The purchase_unit breakdown. + * + * @return array + */ + private function breakdown(): array { + return $this->amount()['breakdown'] ?? array(); + } + + /** + * The purchase_unit breakdown. + * + * @param string $key The breakdown element to get the value from. + * @return float + */ + private function breakdown_value( string $key ): float { + if ( ! isset( $this->breakdown()[ $key ] ) ) { + return 0.0; + } + return (float) ( $this->breakdown()[ $key ]['value'] ?? 0.0 ); + } + + /** + * The purchase_unit items array. + * + * @return array + */ + private function items(): array { + return $this->purchase_unit['items'] ?? array(); + } + + /** + * The sanitizes the purchase_unit array. + * + * @return array + */ + public function sanitize(): array { + $this->sanitize_item_amount_mismatch(); + $this->sanitize_item_tax_mismatch(); + $this->sanitize_breakdown_mismatch(); + return $this->purchase_unit; + } + + /** + * The sanitizes the purchase_unit items amount. + * + * @return void + */ + private function sanitize_item_amount_mismatch(): void { + $item_mismatch = $this->calculate_item_mismatch(); + + if ( $item_mismatch < 0 ) { + // Do floors on item amounts so item_mismatch is a positive value. + foreach ( $this->item_objects as $index => $item ) { + $this->purchase_unit['items'][ $index ] = $item->to_array( + $item->unit_amount()->is_rounding_up() + ); + } + } + + $item_mismatch = $this->calculate_item_mismatch(); + + if ( $item_mismatch > 0 ) { + // Add extra line item with roundings. + $roundings_money = new Money( $item_mismatch, $this->currency_code() ); + $this->purchase_unit['items'][] = ( new Item( 'Roundings', $roundings_money, 1 ) )->to_array(); + } + + $item_mismatch = $this->calculate_item_mismatch(); + + if ( $item_mismatch !== 0.0 ) { + // Ditch items. + if ( isset( $this->purchase_unit['items'] ) ) { + unset( $this->purchase_unit['items'] ); + } + } + } + + /** + * The sanitizes the purchase_unit items tax. + * + * @return void + */ + private function sanitize_item_tax_mismatch(): void { + $tax_mismatch = $this->calculate_tax_mismatch(); + + if ( $tax_mismatch !== 0.0 ) { + // Unset tax in items. + foreach ( $this->purchase_unit['items'] as $index => $item ) { + if ( isset( $this->purchase_unit['items'][ $index ]['tax'] ) ) { + unset( $this->purchase_unit['items'][ $index ]['tax'] ); + } + if ( isset( $this->purchase_unit['items'][ $index ]['tax_rate'] ) ) { + unset( $this->purchase_unit['items'][ $index ]['tax_rate'] ); + } + } + } + } + + /** + * The sanitizes the purchase_unit breakdown. + * + * @return void + */ + private function sanitize_breakdown_mismatch(): void { + $breakdown_mismatch = $this->calculate_breakdown_mismatch(); + + if ( $breakdown_mismatch !== 0.0 ) { + // Ditch breakdowns and items. + if ( isset( $this->purchase_unit['items'] ) ) { + unset( $this->purchase_unit['items'] ); + } + if ( isset( $this->purchase_unit['amount']['breakdown'] ) ) { + unset( $this->purchase_unit['amount']['breakdown'] ); + } + } + } + + /** + * The calculates amount mismatch of items sums with breakdown. + * + * @return float + */ + private function calculate_item_mismatch(): float { + $item_total = $this->breakdown_value( 'item_total' ); + if ( ! $item_total ) { + return 0; + } + + $remaining_item_total = array_reduce( + $this->items(), + function ( float $total, array $item ): float { + return $total - (float) $item['unit_amount']['value'] * (float) $item['quantity']; + }, + $item_total + ); + + return round( $remaining_item_total, 2 ); + } + + /** + * The calculates tax mismatch of items sums with breakdown. + * + * @return float + */ + private function calculate_tax_mismatch(): float { + $tax_total = $this->breakdown_value( 'tax_total' ); + $items_with_tax = array_filter( + $this->items(), + function ( array $item ): bool { + return isset( $item['tax'] ); + } + ); + + if ( ! $tax_total || empty( $items_with_tax ) ) { + return 0; + } + + $remaining_tax_total = array_reduce( + $this->items(), + function ( float $total, array $item ): float { + $tax = $item['tax'] ?? false; + if ( $tax ) { + $total -= (float) $tax['value'] * (float) $item['quantity']; + } + return $total; + }, + $tax_total + ); + + return round( $remaining_tax_total, 2 ); + } + + /** + * The calculates mismatch of breakdown sums with total amount. + * + * @return float + */ + private function calculate_breakdown_mismatch(): float { + $breakdown = $this->breakdown(); + if ( ! $breakdown ) { + return 0; + } + + $amount_total = 0.0; + $amount_total += $this->breakdown_value( 'item_total' ); + $amount_total += $this->breakdown_value( 'tax_total' ); + $amount_total += $this->breakdown_value( 'shipping' ); + $amount_total += $this->breakdown_value( 'discount' ); + $amount_total += $this->breakdown_value( 'shipping_discount' ); + $amount_total += $this->breakdown_value( 'handling' ); + $amount_total += $this->breakdown_value( 'insurance' ); + + $amount_str = $this->amount()['value'] ?? 0; + $amount_total_str = ( new Money( $amount_total, $this->currency_code() ) )->value_str(); + + return $amount_str - $amount_total_str; + } +} From a211553e4bee0a65c31ed4e3785c2ce6d9cc2a82 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 3 Aug 2023 12:28:20 +0200 Subject: [PATCH 12/60] Check if guest customer id exist in user meta when checking if payment exist on PayPal --- .../src/Endpoint/PaymentTokenEndpoint.php | 64 ++++++++++++++++++- modules/ppcp-vaulting/services.php | 1 + .../ppcp-vaulting/src/PaymentTokenChecker.php | 42 +++++++++++- 3 files changed, 104 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php index a268d6ee9..5cabf04d7 100644 --- a/modules/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php @@ -97,7 +97,7 @@ class PaymentTokenEndpoint { } /** - * Returns the payment tokens for a user. + * Returns the payment tokens for the given user id. * * @param int $id The user id. * @@ -118,7 +118,67 @@ class PaymentTokenEndpoint { $response = $this->request( $url, $args ); if ( is_wp_error( $response ) ) { $error = new RuntimeException( - __( 'Could not fetch payment token.', 'woocommerce-paypal-payments' ) + __( 'Could not fetch payment token for customer id.', 'woocommerce-paypal-payments' ) + ); + $this->logger->log( + 'warning', + $error->getMessage(), + array( + 'args' => $args, + 'response' => $response, + ) + ); + throw $error; + } + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + $error = new PayPalApiException( + $json, + $status_code + ); + $this->logger->log( + 'warning', + $error->getMessage(), + array( + 'args' => $args, + 'response' => $response, + ) + ); + throw $error; + } + + $tokens = array(); + foreach ( $json->payment_tokens as $token_value ) { + $tokens[] = $this->factory->from_paypal_response( $token_value ); + } + + return $tokens; + } + + /** + * Returns the payment tokens for the given guest customer id. + * + * @param string $customer_id The guest customer id. + * + * @return PaymentToken[] + * @throws RuntimeException If the request fails. + */ + public function for_guest( string $customer_id ): array { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v2/vault/payment-tokens/?customer_id=' . $customer_id; + $args = array( + 'method' => 'GET', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ), + ); + + $response = $this->request( $url, $args ); + if ( is_wp_error( $response ) ) { + $error = new RuntimeException( + __( 'Could not fetch payment token for guest customer id.', 'woocommerce-paypal-payments' ) ); $this->logger->log( 'warning', diff --git a/modules/ppcp-vaulting/services.php b/modules/ppcp-vaulting/services.php index 1c0b6a742..c274c4549 100644 --- a/modules/ppcp-vaulting/services.php +++ b/modules/ppcp-vaulting/services.php @@ -30,6 +30,7 @@ return array( $container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.processor.authorized-payments' ), $container->get( 'api.endpoint.payments' ), + $container->get( 'api.endpoint.payment-token' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, diff --git a/modules/ppcp-vaulting/src/PaymentTokenChecker.php b/modules/ppcp-vaulting/src/PaymentTokenChecker.php index cd57e9426..4e95e268a 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenChecker.php +++ b/modules/ppcp-vaulting/src/PaymentTokenChecker.php @@ -14,6 +14,8 @@ use Psr\Log\LoggerInterface; use RuntimeException; use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; @@ -67,6 +69,13 @@ class PaymentTokenChecker { */ protected $payments_endpoint; + /** + * The payment token endpoint. + * + * @var PaymentTokenEndpoint + */ + protected $payment_token_endpoint; + /** * The logger. * @@ -82,6 +91,7 @@ class PaymentTokenChecker { * @param Settings $settings The settings. * @param AuthorizedPaymentsProcessor $authorized_payments_processor The authorized payments processor. * @param PaymentsEndpoint $payments_endpoint The payments endpoint. + * @param PaymentTokenEndpoint $payment_token_endpoint The payment token endpoint. * @param LoggerInterface $logger The logger. */ public function __construct( @@ -90,6 +100,7 @@ class PaymentTokenChecker { Settings $settings, AuthorizedPaymentsProcessor $authorized_payments_processor, PaymentsEndpoint $payments_endpoint, + PaymentTokenEndpoint $payment_token_endpoint, LoggerInterface $logger ) { $this->payment_token_repository = $payment_token_repository; @@ -97,6 +108,7 @@ class PaymentTokenChecker { $this->settings = $settings; $this->authorized_payments_processor = $authorized_payments_processor; $this->payments_endpoint = $payments_endpoint; + $this->payment_token_endpoint = $payment_token_endpoint; $this->logger = $logger; } @@ -130,7 +142,7 @@ class PaymentTokenChecker { return; } - $tokens = $this->payment_token_repository->all_for_user_id( $customer_id ); + $tokens = $this->tokens_for_user( $customer_id ); if ( $tokens ) { try { $this->capture_authorized_payment( $wc_order ); @@ -231,4 +243,32 @@ class PaymentTokenChecker { } } } + + /** + * Returns customer tokens either from guest or customer id. + * + * @param int $customer_id The customer id. + * @return PaymentToken[] + */ + private function tokens_for_user( int $customer_id ): array { + $tokens = array(); + + $guest_customer_id = get_user_meta( $customer_id, 'ppcp_guest_customer_id', true ); + if ( $guest_customer_id ) { + $tokens = $this->payment_token_endpoint->for_guest( $guest_customer_id ); + } + + if ( ! $tokens ) { + $guest_customer_id = get_user_meta( $customer_id, 'ppcp_customer_id', true ); + if ( $guest_customer_id ) { + $tokens = $this->payment_token_endpoint->for_guest( $guest_customer_id ); + } + } + + if ( ! $tokens ) { + $tokens = $this->payment_token_repository->all_for_user_id( $customer_id ); + } + + return $tokens; + } } From ad95688277b61b96aca6e0d9fdce0ede2f055922 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 3 Aug 2023 12:47:41 +0200 Subject: [PATCH 13/60] Fix psalm --- modules/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php index 5cabf04d7..0ae611e8c 100644 --- a/modules/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php @@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository; +use WP_Error; /** * Class PaymentTokenEndpoint @@ -176,7 +177,7 @@ class PaymentTokenEndpoint { ); $response = $this->request( $url, $args ); - if ( is_wp_error( $response ) ) { + if ( $response instanceof WP_Error ) { $error = new RuntimeException( __( 'Could not fetch payment token for guest customer id.', 'woocommerce-paypal-payments' ) ); From af3e68a017bfd537194a145b194f3bb2551d16f3 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Thu, 3 Aug 2023 15:53:51 +0100 Subject: [PATCH 14/60] Add paypal refund fees to order template --- .../src/Admin/FeesRenderer.php | 67 ++++++++++++++++++- .../src/Gateway/PayPalGateway.php | 1 + .../src/IncomingWebhookEndpoint.php | 21 +++++- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php b/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php index f3ef396d8..dd6c781a9 100644 --- a/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php +++ b/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php @@ -25,6 +25,19 @@ class FeesRenderer { */ public function render( WC_Order $wc_order ) : string { $breakdown = $wc_order->get_meta( PayPalGateway::FEES_META_KEY ); + $refund_breakdown = $wc_order->get_meta( PayPalGateway::REFUND_FEES_META_KEY ) ?: array(); + + $refund_breakdown = [ + 'paypal_fee' => [ + 'value' => 0.25, + 'currency_code' => 'EUR', + ], + 'net_amount' => [ + 'value' => 6.75, + 'currency_code' => 'EUR', + ], + ]; + if ( ! is_array( $breakdown ) ) { return ''; } @@ -42,6 +55,44 @@ class FeesRenderer { ); } + $refund_total = 0; + $refund_currency = null; + + $refund_fee = $refund_breakdown['paypal_fee'] ?? null; + if ( is_array( $refund_fee ) ) { + $refund_total += $refund_fee['value']; + $refund_currency = $refund_fee['currency_code']; + + $html .= $this->render_money_row( + __( 'PayPal Refund Fee:', 'woocommerce-paypal-payments' ), + __( 'The fee PayPal collects for the refund transactions.', 'woocommerce-paypal-payments' ), + $refund_fee['value'], + $refund_fee['currency_code'], + true, + 'refunded-total' + ); + } + + $refund_amount = $refund_breakdown['net_amount'] ?? null; + if ( is_array( $refund_amount ) ) { + $refund_total += $refund_amount['value']; + + if ( null === $refund_currency ) { + $refund_currency = $refund_fee['currency_code']; + } else if ( $refund_currency !== $refund_fee['currency_code'] ) { + $refund_currency = false; + } + + $html .= $this->render_money_row( + __( 'PayPal Refunded:', 'woocommerce-paypal-payments' ), + __( 'The net amount that was refunded.', 'woocommerce-paypal-payments' ), + $refund_amount['value'], + $refund_amount['currency_code'], + true, + 'refunded-total' + ); + } + $net = $breakdown['net_amount'] ?? null; if ( is_array( $net ) ) { $html .= $this->render_money_row( @@ -50,6 +101,15 @@ class FeesRenderer { $net['value'], $net['currency_code'] ); + + if ( ( $refund_total > 0.0 && $refund_currency === $net['currency_code'] ) ) { + $html .= $this->render_money_row( + __( 'PayPal Net Total:', 'woocommerce-paypal-payments' ), + __( 'The net total that will be credited to your PayPal account minus the refunds.', 'woocommerce-paypal-payments' ), + $net['value'] - $refund_total, + $net['currency_code'] + ); + } } return $html; @@ -63,9 +123,10 @@ class FeesRenderer { * @param string|float $value The money value. * @param string $currency The currency code. * @param bool $negative Whether to add the minus sign. + * @param string $html_class Html class to add to the elements. * @return string */ - private function render_money_row( string $title, string $tooltip, $value, string $currency, bool $negative = false ): string { + private function render_money_row( string $title, string $tooltip, $value, string $currency, bool $negative = false, string $html_class = '' ): string { /** * Bad type hint in WC phpdoc. * @@ -73,10 +134,10 @@ class FeesRenderer { */ return ' - ' . wc_help_tip( $tooltip ) . ' ' . esc_html( $title ) . ' + ' . wc_help_tip( $tooltip ) . ' ' . esc_html( $title ) . ' - + ' . ( $negative ? ' - ' : '' ) . wc_price( $value, array( 'currency' => $currency ) ) . ' diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index c863d1663..17a365df9 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -48,6 +48,7 @@ class PayPalGateway extends \WC_Payment_Gateway { const ORDER_PAYMENT_MODE_META_KEY = '_ppcp_paypal_payment_mode'; const ORDER_PAYMENT_SOURCE_META_KEY = '_ppcp_paypal_payment_source'; const FEES_META_KEY = '_ppcp_paypal_fees'; + const REFUND_FEES_META_KEY = '_ppcp_paypal_refund_fees'; const REFUNDS_META_KEY = '_ppcp_refunds'; /** diff --git a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php index df556e0f0..5043dfd1b 100644 --- a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php +++ b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php @@ -208,6 +208,15 @@ class IncomingWebhookEndpoint { public function handle_request( \WP_REST_Request $request ): \WP_REST_Response { $event = $this->event_from_request( $request ); + $this->logger->debug( + sprintf( + 'Webhook %s received of type %s and by resource "%s"', + $event->id(), + $event->event_type(), + $event->resource_type() + ) + ); + $this->last_webhook_event_storage->save( $event ); if ( $this->simulation->is_simulation_event( $event ) ) { @@ -218,11 +227,19 @@ class IncomingWebhookEndpoint { foreach ( $this->handlers as $handler ) { if ( $handler->responsible_for_request( $request ) ) { + $this->logger->debug( + sprintf( + 'Webhook is going to be handled by %s on %s', + ( $handler->event_types() ) ? current( $handler->event_types() ) : '', + get_class( $handler ) + ) + ); $response = $handler->handle_request( $request ); $this->logger->info( sprintf( - 'Webhook has been handled by %s', - ( $handler->event_types() ) ? current( $handler->event_types() ) : '' + 'Webhook has been handled by %s on %s', + ( $handler->event_types() ) ? current( $handler->event_types() ) : '', + get_class( $handler ) ) ); return $response; From 65f89fd63ef79e325af5aad0676c041ba0893846 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Thu, 3 Aug 2023 17:57:51 +0100 Subject: [PATCH 15/60] Add api support for order endpoint to receive refunds --- modules/ppcp-api-client/services.php | 10 +- .../src/Endpoint/PaymentsEndpoint.php | 6 +- .../ppcp-api-client/src/Entity/Payments.php | 32 ++- modules/ppcp-api-client/src/Entity/Refund.php | 112 +++------- .../src/Entity/RefundCapture.php | 118 ++++++++++ .../src/Entity/RefundStatus.php | 59 +++++ .../src/Entity/SellerPayableBreakdown.php | 202 ++++++++++++++++++ .../src/Factory/PaymentsFactory.php | 25 ++- .../src/Factory/RefundFactory.php | 43 ++++ .../src/Admin/FeesRenderer.php | 11 - .../src/Processor/RefundProcessor.php | 4 +- 11 files changed, 517 insertions(+), 105 deletions(-) create mode 100644 modules/ppcp-api-client/src/Entity/RefundCapture.php create mode 100644 modules/ppcp-api-client/src/Entity/RefundStatus.php create mode 100644 modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php create mode 100644 modules/ppcp-api-client/src/Factory/RefundFactory.php diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 71c9a25f8..c96a16c6e 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans; use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; @@ -289,6 +290,12 @@ return array( $container->get( 'api.factory.fraud-processor-response' ) ); }, + 'api.factory.refund' => static function ( ContainerInterface $container ): RefundFactory { + $amount_factory = $container->get( 'api.factory.amount' ); + return new RefundFactory( + $amount_factory + ); + }, 'api.factory.purchase-unit' => static function ( ContainerInterface $container ): PurchaseUnitFactory { $amount_factory = $container->get( 'api.factory.amount' ); @@ -374,7 +381,8 @@ return array( 'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory { $authorizations_factory = $container->get( 'api.factory.authorization' ); $capture_factory = $container->get( 'api.factory.capture' ); - return new PaymentsFactory( $authorizations_factory, $capture_factory ); + $refund_factory = $container->get( 'api.factory.refund' ); + return new PaymentsFactory( $authorizations_factory, $capture_factory, $refund_factory ); }, 'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory { return new AuthorizationFactory(); diff --git a/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php index 2a39a4b9b..210beae16 100644 --- a/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php @@ -13,7 +13,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; -use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund; +use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; @@ -196,13 +196,13 @@ class PaymentsEndpoint { /** * Refunds a payment. * - * @param Refund $refund The refund to be processed. + * @param RefundCapture $refund The refund to be processed. * * @return string Refund ID. * @throws RuntimeException If the request fails. * @throws PayPalApiException If the request fails. */ - public function refund( Refund $refund ) : string { + public function refund( RefundCapture $refund ) : string { $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v2/payments/captures/' . $refund->for_capture()->id() . '/refund'; $args = array( diff --git a/modules/ppcp-api-client/src/Entity/Payments.php b/modules/ppcp-api-client/src/Entity/Payments.php index d5e051f88..98b13b9ec 100644 --- a/modules/ppcp-api-client/src/Entity/Payments.php +++ b/modules/ppcp-api-client/src/Entity/Payments.php @@ -28,13 +28,21 @@ class Payments { */ private $captures; + /** + * The Captures. + * + * @var Refund[] + */ + private $refunds; + /** * Payments constructor. * * @param array $authorizations The Authorizations. * @param array $captures The Captures. + * @param array $refunds The Refunds. */ - public function __construct( array $authorizations, array $captures ) { + public function __construct( array $authorizations, array $captures, array $refunds ) { foreach ( $authorizations as $key => $authorization ) { if ( is_a( $authorization, Authorization::class ) ) { continue; @@ -47,8 +55,15 @@ class Payments { } unset( $captures[ $key ] ); } + foreach ( $refunds as $key => $refund ) { + if ( is_a( $refund, Refund::class ) ) { + continue; + } + unset( $refunds[ $key ] ); + } $this->authorizations = $authorizations; $this->captures = $captures; + $this->refunds = $refunds; } /** @@ -70,6 +85,12 @@ class Payments { }, $this->captures() ), + 'refunds' => array_map( + static function ( Refund $refund ): array { + return $refund->to_array(); + }, + $this->refunds() + ), ); } @@ -90,4 +111,13 @@ class Payments { public function captures(): array { return $this->captures; } + + /** + * Returns the Refunds. + * + * @return Refund[] + **/ + public function refunds(): array { + return $this->refunds; + } } diff --git a/modules/ppcp-api-client/src/Entity/Refund.php b/modules/ppcp-api-client/src/Entity/Refund.php index 71c2a2f56..692fca2ce 100644 --- a/modules/ppcp-api-client/src/Entity/Refund.php +++ b/modules/ppcp-api-client/src/Entity/Refund.php @@ -1,6 +1,8 @@ capture = $capture; - $this->invoice_id = $invoice_id; - $this->note_to_payer = $note_to_payer; - $this->amount = $amount; - } - - /** - * Returns the capture for the refund. - * - * @return Capture - */ - public function for_capture() : Capture { - return $this->capture; - } - - /** - * Return the invoice id. - * - * @return string - */ - public function invoice_id() : string { - return $this->invoice_id; - } - - /** - * Returns the note to the payer. - * - * @return string - */ - public function note_to_payer() : string { - return $this->note_to_payer; - } - - /** - * Returns the Amount. - * - * @return Amount|null - */ - public function amount() { - return $this->amount; - } - - /** - * Returns the object as array. + * Returns the entity as array. * * @return array */ public function to_array() : array { - $data = array( - 'invoice_id' => $this->invoice_id(), + $data = array( ); - if ( $this->note_to_payer() ) { - $data['note_to_payer'] = $this->note_to_payer(); - } - if ( $this->amount() ) { - $data['amount'] = $this->amount()->to_array(); - } return $data; } } diff --git a/modules/ppcp-api-client/src/Entity/RefundCapture.php b/modules/ppcp-api-client/src/Entity/RefundCapture.php new file mode 100644 index 000000000..0554622c7 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/RefundCapture.php @@ -0,0 +1,118 @@ +capture = $capture; + $this->invoice_id = $invoice_id; + $this->note_to_payer = $note_to_payer; + $this->amount = $amount; + } + + /** + * Returns the capture for the refund. + * + * @return Capture + */ + public function for_capture() : Capture { + return $this->capture; + } + + /** + * Return the invoice id. + * + * @return string + */ + public function invoice_id() : string { + return $this->invoice_id; + } + + /** + * Returns the note to the payer. + * + * @return string + */ + public function note_to_payer() : string { + return $this->note_to_payer; + } + + /** + * Returns the Amount. + * + * @return Amount|null + */ + public function amount() { + return $this->amount; + } + + /** + * Returns the object as array. + * + * @return array + */ + public function to_array() : array { + $data = array( + 'invoice_id' => $this->invoice_id(), + ); + if ( $this->note_to_payer() ) { + $data['note_to_payer'] = $this->note_to_payer(); + } + if ( $this->amount() ) { + $data['amount'] = $this->amount()->to_array(); + } + return $data; + } +} diff --git a/modules/ppcp-api-client/src/Entity/RefundStatus.php b/modules/ppcp-api-client/src/Entity/RefundStatus.php new file mode 100644 index 000000000..00642bf60 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/RefundStatus.php @@ -0,0 +1,59 @@ +status = $status; + } + + /** + * Compares the current status with a given one. + * + * @param string $status The status to compare with. + * + * @return bool + */ + public function is( string $status ): bool { + return $this->status === $status; + } + + /** + * Returns the status. + * + * @return string + */ + public function name(): string { + return $this->status; + } +} diff --git a/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php b/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php new file mode 100644 index 000000000..8cdfd0b5c --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php @@ -0,0 +1,202 @@ +gross_amount = $gross_amount; +// $this->paypal_fee = $paypal_fee; +// $this->paypal_fee_in_receivable_currency = $paypal_fee_in_receivable_currency; +// $this->net_amount = $net_amount; +// $this->receivable_amount = $receivable_amount; +// $this->exchange_rate = $exchange_rate; +// $this->platform_fees = $platform_fees; +// } +// +// /** +// * The amount for this captured payment in the currency of the transaction. +// * +// * @return Money +// */ +// public function gross_amount(): ?Money { +// return $this->gross_amount; +// } +// +// /** +// * The applicable fee for this captured payment in the currency of the transaction. +// * +// * @return Money|null +// */ +// public function paypal_fee(): ?Money { +// return $this->paypal_fee; +// } +// +// /** +// * The applicable fee for this captured payment in the receivable currency. +// * +// * Present only in cases the fee is charged in the receivable currency. +// * +// * @return Money|null +// */ +// public function paypal_fee_in_receivable_currency(): ?Money { +// return $this->paypal_fee_in_receivable_currency; +// } +// +// /** +// * The net amount that the payee receives for this captured payment in their PayPal account. +// * +// * Computed as gross_amount minus the paypal_fee minus the platform_fees. +// * +// * @return Money|null +// */ +// public function net_amount(): ?Money { +// return $this->net_amount; +// } +// +// /** +// * The net amount that is credited to the payee's PayPal account. +// * +// * Present only when the currency of the captured payment is different from the currency +// * of the PayPal account where the payee wants to credit the funds. Computed as net_amount times exchange_rate. +// * +// * @return Money|null +// */ +// public function receivable_amount(): ?Money { +// return $this->receivable_amount; +// } +// +// /** +// * The exchange rate that determines the amount that is credited to the payee's PayPal account. +// * +// * Present when the currency of the captured payment is different from the currency of the PayPal account where the payee wants to credit the funds. +// * +// * @return ExchangeRate|null +// */ +// public function exchange_rate(): ?ExchangeRate { +// return $this->exchange_rate; +// } +// +// /** +// * An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment. +// * +// * @return PlatformFee[] +// */ +// public function platform_fees(): array { +// return $this->platform_fees; +// } +// +// /** +// * Returns the object as array. +// * +// * @return array +// */ +// public function to_array(): array { +// $data = array( +// 'gross_amount' => $this->gross_amount->to_array(), +// ); +// if ( $this->paypal_fee ) { +// $data['paypal_fee'] = $this->paypal_fee->to_array(); +// } +// if ( $this->paypal_fee_in_receivable_currency ) { +// $data['paypal_fee_in_receivable_currency'] = $this->paypal_fee_in_receivable_currency->to_array(); +// } +// if ( $this->net_amount ) { +// $data['net_amount'] = $this->net_amount->to_array(); +// } +// if ( $this->receivable_amount ) { +// $data['receivable_amount'] = $this->receivable_amount->to_array(); +// } +// if ( $this->exchange_rate ) { +// $data['exchange_rate'] = $this->exchange_rate->to_array(); +// } +// if ( $this->platform_fees ) { +// $data['platform_fees'] = array_map( +// function ( PlatformFee $fee ) { +// return $fee->to_array(); +// }, +// $this->platform_fees +// ); +// } +// +// return $data; +// } +} diff --git a/modules/ppcp-api-client/src/Factory/PaymentsFactory.php b/modules/ppcp-api-client/src/Factory/PaymentsFactory.php index 81f99e53f..e85b95827 100644 --- a/modules/ppcp-api-client/src/Factory/PaymentsFactory.php +++ b/modules/ppcp-api-client/src/Factory/PaymentsFactory.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; /** @@ -32,19 +33,29 @@ class PaymentsFactory { */ private $capture_factory; + /** + * The Refund factory. + * + * @var RefundFactory + */ + private $refund_factory; + /** * PaymentsFactory constructor. * * @param AuthorizationFactory $authorization_factory The Authorization factory. * @param CaptureFactory $capture_factory The Capture factory. + * @param RefundFactory $refund_factory The Refund factory. */ public function __construct( AuthorizationFactory $authorization_factory, - CaptureFactory $capture_factory + CaptureFactory $capture_factory, + RefundFactory $refund_factory ) { $this->authorization_factory = $authorization_factory; $this->capture_factory = $capture_factory; + $this->refund_factory = $refund_factory; } /** @@ -62,12 +73,18 @@ class PaymentsFactory { isset( $data->authorizations ) ? $data->authorizations : array() ); $captures = array_map( - function ( \stdClass $authorization ): Capture { - return $this->capture_factory->from_paypal_response( $authorization ); + function ( \stdClass $capture ): Capture { + return $this->capture_factory->from_paypal_response( $capture ); }, isset( $data->captures ) ? $data->captures : array() ); - $payments = new Payments( $authorizations, $captures ); + $refunds = array_map( + function ( \stdClass $refund ): Refund { + return $this->refund_factory->from_paypal_response( $refund ); + }, + isset( $data->refunds ) ? $data->refunds : array() + ); + $payments = new Payments( $authorizations, $captures, $refunds ); return $payments; } } diff --git a/modules/ppcp-api-client/src/Factory/RefundFactory.php b/modules/ppcp-api-client/src/Factory/RefundFactory.php new file mode 100644 index 000000000..dfc703da4 --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/RefundFactory.php @@ -0,0 +1,43 @@ +amount_factory = $amount_factory; + + } + + /** + * Returns the payment refund object based off the PayPal response. + * + * @param \stdClass $data The PayPal response. + * + * @return Refund + */ + public function from_paypal_response( \stdClass $data ) : Refund { + + return new Refund( + ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php b/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php index dd6c781a9..af978675d 100644 --- a/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php +++ b/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php @@ -27,17 +27,6 @@ class FeesRenderer { $breakdown = $wc_order->get_meta( PayPalGateway::FEES_META_KEY ); $refund_breakdown = $wc_order->get_meta( PayPalGateway::REFUND_FEES_META_KEY ) ?: array(); - $refund_breakdown = [ - 'paypal_fee' => [ - 'value' => 0.25, - 'currency_code' => 'EUR', - ], - 'net_amount' => [ - 'value' => 6.75, - 'currency_code' => 'EUR', - ], - ]; - if ( ! is_array( $breakdown ) ) { return ''; } diff --git a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php index 0cfea30ad..f67794cd4 100644 --- a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php @@ -20,7 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; -use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund; +use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -151,7 +151,7 @@ class RefundProcessor { } $capture = $captures[0]; - $refund = new Refund( + $refund = new RefundCapture( $capture, $capture->invoice_id(), $reason, From ef5fc4b3d4101e02701f3d227104bfcea68f4fcd Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 4 Aug 2023 11:46:34 +0100 Subject: [PATCH 16/60] Add support for refunds in api response structure --- modules/ppcp-api-client/services.php | 16 +- .../ppcp-api-client/src/Entity/Payments.php | 40 +-- modules/ppcp-api-client/src/Entity/Refund.php | 161 ++++++++++ .../src/Entity/RefundCapture.php | 4 +- .../src/Entity/RefundPayer.php | 79 +++++ .../src/Entity/RefundStatus.php | 30 +- .../src/Entity/RefundStatusDetails.php | 71 +++++ .../src/Entity/SellerPayableBreakdown.php | 274 +++++++++--------- .../src/Factory/PaymentsFactory.php | 2 +- .../src/Factory/RefundFactory.php | 60 +++- .../src/Factory/RefundPayerFactory.php | 39 +++ .../Factory/SellerPayableBreakdownFactory.php | 81 ++++++ .../src/Admin/FeesRenderer.php | 27 +- .../src/IncomingWebhookEndpoint.php | 6 +- 14 files changed, 696 insertions(+), 194 deletions(-) create mode 100644 modules/ppcp-api-client/src/Entity/RefundPayer.php create mode 100644 modules/ppcp-api-client/src/Entity/RefundStatusDetails.php create mode 100644 modules/ppcp-api-client/src/Factory/RefundPayerFactory.php create mode 100644 modules/ppcp-api-client/src/Factory/SellerPayableBreakdownFactory.php diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index c96a16c6e..41ba10ff2 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -12,11 +12,14 @@ namespace WooCommerce\PayPalCommerce\ApiClient; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans; +use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerPayableBreakdown; use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; @@ -293,7 +296,9 @@ return array( 'api.factory.refund' => static function ( ContainerInterface $container ): RefundFactory { $amount_factory = $container->get( 'api.factory.amount' ); return new RefundFactory( - $amount_factory + $amount_factory, + $container->get( 'api.factory.seller-payable-breakdown' ), + $container->get( 'api.factory.refund_payer' ) ); }, 'api.factory.purchase-unit' => static function ( ContainerInterface $container ): PurchaseUnitFactory { @@ -358,6 +363,9 @@ return array( $address_factory = $container->get( 'api.factory.address' ); return new PayerFactory( $address_factory ); }, + 'api.factory.refund_payer' => static function ( ContainerInterface $container ): RefundPayerFactory { + return new RefundPayerFactory(); + }, 'api.factory.address' => static function ( ContainerInterface $container ): AddressFactory { return new AddressFactory(); }, @@ -403,6 +411,12 @@ return array( $container->get( 'api.factory.platform-fee' ) ); }, + 'api.factory.seller-payable-breakdown' => static function ( ContainerInterface $container ): SellerPayableBreakdownFactory { + return new SellerPayableBreakdownFactory( + $container->get( 'api.factory.money' ), + $container->get( 'api.factory.platform-fee' ) + ); + }, 'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory { return new FraudProcessorResponseFactory(); }, diff --git a/modules/ppcp-api-client/src/Entity/Payments.php b/modules/ppcp-api-client/src/Entity/Payments.php index 98b13b9ec..9039f36d7 100644 --- a/modules/ppcp-api-client/src/Entity/Payments.php +++ b/modules/ppcp-api-client/src/Entity/Payments.php @@ -28,12 +28,12 @@ class Payments { */ private $captures; - /** - * The Captures. - * - * @var Refund[] - */ - private $refunds; + /** + * The Captures. + * + * @var Refund[] + */ + private $refunds; /** * Payments constructor. @@ -55,12 +55,12 @@ class Payments { } unset( $captures[ $key ] ); } - foreach ( $refunds as $key => $refund ) { - if ( is_a( $refund, Refund::class ) ) { - continue; - } - unset( $refunds[ $key ] ); - } + foreach ( $refunds as $key => $refund ) { + if ( is_a( $refund, Refund::class ) ) { + continue; + } + unset( $refunds[ $key ] ); + } $this->authorizations = $authorizations; $this->captures = $captures; $this->refunds = $refunds; @@ -112,12 +112,12 @@ class Payments { return $this->captures; } - /** - * Returns the Refunds. - * - * @return Refund[] - **/ - public function refunds(): array { - return $this->refunds; - } + /** + * Returns the Refunds. + * + * @return Refund[] + **/ + public function refunds(): array { + return $this->refunds; + } } diff --git a/modules/ppcp-api-client/src/Entity/Refund.php b/modules/ppcp-api-client/src/Entity/Refund.php index 692fca2ce..eb44fe00a 100644 --- a/modules/ppcp-api-client/src/Entity/Refund.php +++ b/modules/ppcp-api-client/src/Entity/Refund.php @@ -51,6 +51,150 @@ class Refund { */ private $invoice_id; + /** + * The custom id. + * + * @var string + */ + private $custom_id; + + /** + * The acquirer reference number. + * + * @var string + */ + private $acquirer_reference_number; + + /** + * The acquirer reference number. + * + * @var string + */ + private $note_to_payer; + + /** + * The payer of the refund. + * + * @var ?RefundPayer + */ + private $payer; + + /** + * Refund constructor. + * + * @param string $id The ID. + * @param RefundStatus $status The status. + * @param Amount $amount The amount. + * @param string $invoice_id The invoice id. + * @param string $custom_id The custom id. + * @param SellerPayableBreakdown|null $seller_payable_breakdown The detailed breakdown of the refund activity (fees, ...). + * @param string $acquirer_reference_number The acquirer reference number. + * @param string $note_to_payer The note to payer. + * @param RefundPayer|null $payer The payer. + */ + public function __construct( + string $id, + RefundStatus $status, + Amount $amount, + string $invoice_id, + string $custom_id, + ?SellerPayableBreakdown $seller_payable_breakdown, + string $acquirer_reference_number, + string $note_to_payer, + ?RefundPayer $payer + ) { + $this->id = $id; + $this->status = $status; + $this->amount = $amount; + $this->invoice_id = $invoice_id; + $this->custom_id = $custom_id; + $this->seller_payable_breakdown = $seller_payable_breakdown; + $this->acquirer_reference_number = $acquirer_reference_number; + $this->note_to_payer = $note_to_payer; + $this->payer = $payer; + } + + /** + * Returns the ID. + * + * @return string + */ + public function id() : string { + return $this->id; + } + + /** + * Returns the status. + * + * @return RefundStatus + */ + public function status() : RefundStatus { + return $this->status; + } + + /** + * Returns the amount. + * + * @return Amount + */ + public function amount() : Amount { + return $this->amount; + } + + /** + * Returns the invoice id. + * + * @return string + */ + public function invoice_id() : string { + return $this->invoice_id; + } + + /** + * Returns the custom id. + * + * @return string + */ + public function custom_id() : string { + return $this->custom_id; + } + + /** + * Returns the detailed breakdown of the refund activity (fees, ...). + * + * @return SellerPayableBreakdown|null + */ + public function seller_payable_breakdown() : ?SellerPayableBreakdown { + return $this->seller_payable_breakdown; + } + + /** + * The acquirer reference number. + * + * @return string + */ + public function acquirer_reference_number() : string { + return $this->acquirer_reference_number; + } + + /** + * The note to payer. + * + * @return string + */ + public function note_to_payer() : string { + return $this->note_to_payer; + } + + /** + * Returns the refund payer. + * + * @return RefundPayer|null + */ + public function payer() : ?RefundPayer { + return $this->payer; + } + /** * Returns the entity as array. * @@ -58,7 +202,24 @@ class Refund { */ public function to_array() : array { $data = array( + 'id' => $this->id(), + 'status' => $this->status()->name(), + 'amount' => $this->amount()->to_array(), + 'invoice_id' => $this->invoice_id(), + 'custom_id' => $this->custom_id(), + 'acquirer_reference_number' => $this->acquirer_reference_number(), + 'note_to_payer' => (array) $this->note_to_payer(), ); + $details = $this->status()->details(); + if ( $details ) { + $data['status_details'] = array( 'reason' => $details->reason() ); + } + if ( $this->seller_payable_breakdown ) { + $data['seller_payable_breakdown'] = $this->seller_payable_breakdown->to_array(); + } + if ( $this->payer ) { + $data['payer'] = $this->payer->to_array(); + } return $data; } } diff --git a/modules/ppcp-api-client/src/Entity/RefundCapture.php b/modules/ppcp-api-client/src/Entity/RefundCapture.php index 0554622c7..bca9b663f 100644 --- a/modules/ppcp-api-client/src/Entity/RefundCapture.php +++ b/modules/ppcp-api-client/src/Entity/RefundCapture.php @@ -110,8 +110,8 @@ class RefundCapture { if ( $this->note_to_payer() ) { $data['note_to_payer'] = $this->note_to_payer(); } - if ( $this->amount() ) { - $data['amount'] = $this->amount()->to_array(); + if ( $this->amount ) { + $data['amount'] = $this->amount->to_array(); } return $data; } diff --git a/modules/ppcp-api-client/src/Entity/RefundPayer.php b/modules/ppcp-api-client/src/Entity/RefundPayer.php new file mode 100644 index 000000000..febc1fad0 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/RefundPayer.php @@ -0,0 +1,79 @@ +email_address = $email_address; + $this->merchant_id = $merchant_id; + } + + /** + * Returns the email address. + * + * @return string + */ + public function email_address(): string { + return $this->email_address; + } + + /** + * Returns the merchant id. + * + * @return string + */ + public function merchant_id(): string { + return $this->merchant_id; + } + + /** + * Returns the object as array. + * + * @return array + */ + public function to_array() { + $payer = array( + 'email_address' => $this->email_address(), + ); + if ( $this->merchant_id ) { + $payer['merchant_id'] = $this->merchant_id(); + } + return $payer; + } +} diff --git a/modules/ppcp-api-client/src/Entity/RefundStatus.php b/modules/ppcp-api-client/src/Entity/RefundStatus.php index 00642bf60..0f3abc3b2 100644 --- a/modules/ppcp-api-client/src/Entity/RefundStatus.php +++ b/modules/ppcp-api-client/src/Entity/RefundStatus.php @@ -16,10 +16,10 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Entity; */ class RefundStatus { - const COMPLETED = 'COMPLETED'; - const CANCELLED = 'CANCELLED'; - const FAILED = 'FAILED'; - const PENDING = 'PENDING'; + const COMPLETED = 'COMPLETED'; + const CANCELLED = 'CANCELLED'; + const FAILED = 'FAILED'; + const PENDING = 'PENDING'; /** * The status. @@ -28,13 +28,22 @@ class RefundStatus { */ private $status; + /** + * The details. + * + * @var RefundStatusDetails|null + */ + private $details; + /** * RefundStatus constructor. * - * @param string $status The status. + * @param string $status The status. + * @param RefundStatusDetails|null $details The details. */ - public function __construct( string $status ) { + public function __construct( string $status, ?RefundStatusDetails $details = null ) { $this->status = $status; + $this->details = $details; } /** @@ -56,4 +65,13 @@ class RefundStatus { public function name(): string { return $this->status; } + + /** + * Returns the details. + * + * @return RefundStatusDetails|null + */ + public function details(): ?RefundStatusDetails { + return $this->details; + } } diff --git a/modules/ppcp-api-client/src/Entity/RefundStatusDetails.php b/modules/ppcp-api-client/src/Entity/RefundStatusDetails.php new file mode 100644 index 000000000..fac1c4e88 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/RefundStatusDetails.php @@ -0,0 +1,71 @@ +reason = $reason; + } + + /** + * Compares the current reason with a given one. + * + * @param string $reason The reason to compare with. + * + * @return bool + */ + public function is( string $reason ): bool { + return $this->reason === $reason; + } + + /** + * Returns the reason explaining refund status. + * One of RefundStatusDetails constants. + * + * @return string + */ + public function reason(): string { + return $this->reason; + } + + /** + * Returns the human-readable reason text explaining refund status. + * + * @return string + */ + public function text(): string { + switch ( $this->reason ) { + case self::ECHECK: + return __( 'The payer paid by an eCheck that has not yet cleared.', 'woocommerce-paypal-payments' ); + default: + return $this->reason; + } + } +} diff --git a/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php b/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php index 8cdfd0b5c..927ffad54 100644 --- a/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php +++ b/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php @@ -17,7 +17,7 @@ class SellerPayableBreakdown { /** * The amount for this refunded payment in the currency of the transaction. * - * @var Money + * @var Money|null */ private $gross_amount; @@ -47,11 +47,15 @@ class SellerPayableBreakdown { private $net_amount; /** + * The net amount for this refunded payment in the receivable currency. + * * @var Money|null */ private $net_amount_in_receivable_currency; /** + * The total amount for this refund. + * * @var Money|null */ private $total_refunded_amount; @@ -63,140 +67,136 @@ class SellerPayableBreakdown { */ private $platform_fees; -// /** -// * SellerReceivableBreakdown constructor. -// * -// * @param Money $gross_amount The amount for this captured payment in the currency of the transaction. -// * @param Money|null $paypal_fee The applicable fee for this captured payment in the currency of the transaction. -// * @param Money|null $paypal_fee_in_receivable_currency The applicable fee for this captured payment in the receivable currency. -// * @param Money|null $net_amount The net amount that the payee receives for this captured payment in their PayPal account. -// * @param Money|null $receivable_amount The net amount that is credited to the payee's PayPal account. -// * @param ExchangeRate|null $exchange_rate The exchange rate that determines the amount that is credited to the payee's PayPal account. -// * @param PlatformFee[] $platform_fees An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment. -// */ -// public function __construct( -// Money $gross_amount, -// ?Money $paypal_fee, -// ?Money $paypal_fee_in_receivable_currency, -// ?Money $net_amount, -// ?Money $receivable_amount, -// ?ExchangeRate $exchange_rate, -// array $platform_fees -// ) { -// $this->gross_amount = $gross_amount; -// $this->paypal_fee = $paypal_fee; -// $this->paypal_fee_in_receivable_currency = $paypal_fee_in_receivable_currency; -// $this->net_amount = $net_amount; -// $this->receivable_amount = $receivable_amount; -// $this->exchange_rate = $exchange_rate; -// $this->platform_fees = $platform_fees; -// } -// -// /** -// * The amount for this captured payment in the currency of the transaction. -// * -// * @return Money -// */ -// public function gross_amount(): ?Money { -// return $this->gross_amount; -// } -// -// /** -// * The applicable fee for this captured payment in the currency of the transaction. -// * -// * @return Money|null -// */ -// public function paypal_fee(): ?Money { -// return $this->paypal_fee; -// } -// -// /** -// * The applicable fee for this captured payment in the receivable currency. -// * -// * Present only in cases the fee is charged in the receivable currency. -// * -// * @return Money|null -// */ -// public function paypal_fee_in_receivable_currency(): ?Money { -// return $this->paypal_fee_in_receivable_currency; -// } -// -// /** -// * The net amount that the payee receives for this captured payment in their PayPal account. -// * -// * Computed as gross_amount minus the paypal_fee minus the platform_fees. -// * -// * @return Money|null -// */ -// public function net_amount(): ?Money { -// return $this->net_amount; -// } -// -// /** -// * The net amount that is credited to the payee's PayPal account. -// * -// * Present only when the currency of the captured payment is different from the currency -// * of the PayPal account where the payee wants to credit the funds. Computed as net_amount times exchange_rate. -// * -// * @return Money|null -// */ -// public function receivable_amount(): ?Money { -// return $this->receivable_amount; -// } -// -// /** -// * The exchange rate that determines the amount that is credited to the payee's PayPal account. -// * -// * Present when the currency of the captured payment is different from the currency of the PayPal account where the payee wants to credit the funds. -// * -// * @return ExchangeRate|null -// */ -// public function exchange_rate(): ?ExchangeRate { -// return $this->exchange_rate; -// } -// -// /** -// * An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment. -// * -// * @return PlatformFee[] -// */ -// public function platform_fees(): array { -// return $this->platform_fees; -// } -// -// /** -// * Returns the object as array. -// * -// * @return array -// */ -// public function to_array(): array { -// $data = array( -// 'gross_amount' => $this->gross_amount->to_array(), -// ); -// if ( $this->paypal_fee ) { -// $data['paypal_fee'] = $this->paypal_fee->to_array(); -// } -// if ( $this->paypal_fee_in_receivable_currency ) { -// $data['paypal_fee_in_receivable_currency'] = $this->paypal_fee_in_receivable_currency->to_array(); -// } -// if ( $this->net_amount ) { -// $data['net_amount'] = $this->net_amount->to_array(); -// } -// if ( $this->receivable_amount ) { -// $data['receivable_amount'] = $this->receivable_amount->to_array(); -// } -// if ( $this->exchange_rate ) { -// $data['exchange_rate'] = $this->exchange_rate->to_array(); -// } -// if ( $this->platform_fees ) { -// $data['platform_fees'] = array_map( -// function ( PlatformFee $fee ) { -// return $fee->to_array(); -// }, -// $this->platform_fees -// ); -// } -// -// return $data; -// } + /** + * SellerPayableBreakdown constructor. + * + * @param Money|null $gross_amount The amount for this refunded payment in the currency of the transaction. + * @param Money|null $paypal_fee The applicable fee for this refunded payment in the currency of the transaction. + * @param Money|null $paypal_fee_in_receivable_currency The applicable fee for this refunded payment in the receivable currency. + * @param Money|null $net_amount The net amount that the payee receives for this refunded payment in their PayPal account. + * @param Money|null $net_amount_in_receivable_currency The net amount for this refunded payment in the receivable currency. + * @param Money|null $total_refunded_amount The total amount for this refund. + * @param PlatformFee[] $platform_fees An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment. + */ + public function __construct( + ?Money $gross_amount, + ?Money $paypal_fee, + ?Money $paypal_fee_in_receivable_currency, + ?Money $net_amount, + ?Money $net_amount_in_receivable_currency, + ?Money $total_refunded_amount, + array $platform_fees + ) { + $this->gross_amount = $gross_amount; + $this->paypal_fee = $paypal_fee; + $this->paypal_fee_in_receivable_currency = $paypal_fee_in_receivable_currency; + $this->net_amount = $net_amount; + $this->net_amount_in_receivable_currency = $net_amount_in_receivable_currency; + $this->total_refunded_amount = $total_refunded_amount; + $this->platform_fees = $platform_fees; + } + + /** + * The amount for this refunded payment in the currency of the transaction. + * + * @return Money + */ + public function gross_amount(): ?Money { + return $this->gross_amount; + } + + /** + * The applicable fee for this refunded payment in the currency of the transaction. + * + * @return Money|null + */ + public function paypal_fee(): ?Money { + return $this->paypal_fee; + } + + /** + * The applicable fee for this refunded payment in the receivable currency. + * + * Present only in cases the fee is charged in the receivable currency. + * + * @return Money|null + */ + public function paypal_fee_in_receivable_currency(): ?Money { + return $this->paypal_fee_in_receivable_currency; + } + + /** + * The net amount that the payee receives for this refunded payment in their PayPal account. + * + * Computed as gross_amount minus the paypal_fee minus the platform_fees. + * + * @return Money|null + */ + public function net_amount(): ?Money { + return $this->net_amount; + } + + /** + * The net amount for this refunded payment in the receivable currency. + * + * @return Money|null + */ + public function net_amount_in_receivable_currency(): ?Money { + return $this->net_amount_in_receivable_currency; + } + + /** + * The total amount for this refund. + * + * @return Money|null + */ + public function total_refunded_amount(): ?Money { + return $this->total_refunded_amount; + } + + /** + * An array of platform or partner fees, commissions, or brokerage fees that associated with the refunded payment. + * + * @return PlatformFee[] + */ + public function platform_fees(): array { + return $this->platform_fees; + } + + /** + * Returns the object as array. + * + * @return array + */ + public function to_array(): array { + $data = array(); + if ( $this->gross_amount ) { + $data['gross_amount'] = $this->gross_amount->to_array(); + } + if ( $this->paypal_fee ) { + $data['paypal_fee'] = $this->paypal_fee->to_array(); + } + if ( $this->paypal_fee_in_receivable_currency ) { + $data['paypal_fee_in_receivable_currency'] = $this->paypal_fee_in_receivable_currency->to_array(); + } + if ( $this->net_amount ) { + $data['net_amount'] = $this->net_amount->to_array(); + } + if ( $this->net_amount_in_receivable_currency ) { + $data['net_amount_in_receivable_currency'] = $this->net_amount_in_receivable_currency->to_array(); + } + if ( $this->total_refunded_amount ) { + $data['total_refunded_amount'] = $this->total_refunded_amount->to_array(); + } + if ( $this->platform_fees ) { + $data['platform_fees'] = array_map( + function ( PlatformFee $fee ) { + return $fee->to_array(); + }, + $this->platform_fees + ); + } + + return $data; + } } diff --git a/modules/ppcp-api-client/src/Factory/PaymentsFactory.php b/modules/ppcp-api-client/src/Factory/PaymentsFactory.php index e85b95827..6f2731cf9 100644 --- a/modules/ppcp-api-client/src/Factory/PaymentsFactory.php +++ b/modules/ppcp-api-client/src/Factory/PaymentsFactory.php @@ -45,7 +45,7 @@ class PaymentsFactory { * * @param AuthorizationFactory $authorization_factory The Authorization factory. * @param CaptureFactory $capture_factory The Capture factory. - * @param RefundFactory $refund_factory The Refund factory. + * @param RefundFactory $refund_factory The Refund factory. */ public function __construct( AuthorizationFactory $authorization_factory, diff --git a/modules/ppcp-api-client/src/Factory/RefundFactory.php b/modules/ppcp-api-client/src/Factory/RefundFactory.php index dfc703da4..63530feaf 100644 --- a/modules/ppcp-api-client/src/Factory/RefundFactory.php +++ b/modules/ppcp-api-client/src/Factory/RefundFactory.php @@ -1,6 +1,6 @@ amount_factory = $amount_factory; - + $this->amount_factory = $amount_factory; + $this->seller_payable_breakdown_factory = $seller_payable_breakdown_factory; + $this->refund_payer_factory = $refund_payer_factory; } /** - * Returns the payment refund object based off the PayPal response. + * Returns the refund object based off the PayPal response. * * @param \stdClass $data The PayPal response. * * @return Refund */ public function from_paypal_response( \stdClass $data ) : Refund { + $reason = $data->status_details->reason ?? null; + $seller_payable_breakdown = isset( $data->seller_payable_breakdown ) ? + $this->seller_payable_breakdown_factory->from_paypal_response( $data->seller_payable_breakdown ) + : null; + + $payer = isset( $data->payer ) ? + $this->refund_payer_factory->from_paypal_response( $data->payer ) + : null; return new Refund( + (string) $data->id, + new RefundStatus( + (string) $data->status, + $reason ? new RefundStatusDetails( $reason ) : null + ), + $this->amount_factory->from_paypal_response( $data->amount ), + (string) ( $data->invoice_id ?? '' ), + (string) ( $data->custom_id ?? '' ), + $seller_payable_breakdown, + (string) ( $data->acquirer_reference_number ?? '' ), + (string) ( $data->note_to_payer ?? '' ), + $payer ); } } diff --git a/modules/ppcp-api-client/src/Factory/RefundPayerFactory.php b/modules/ppcp-api-client/src/Factory/RefundPayerFactory.php new file mode 100644 index 000000000..a24251bed --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/RefundPayerFactory.php @@ -0,0 +1,39 @@ +email_address ) ? $data->email_address : '', + isset( $data->merchant_id ) ? $data->merchant_id : '' + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/SellerPayableBreakdownFactory.php b/modules/ppcp-api-client/src/Factory/SellerPayableBreakdownFactory.php new file mode 100644 index 000000000..8c10d0427 --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/SellerPayableBreakdownFactory.php @@ -0,0 +1,81 @@ +money_factory = $money_factory; + $this->platform_fee_factory = $platform_fee_factory; + } + + /** + * Returns a SellerPayableBreakdownFactory object based off a PayPal Response. + * + * @param stdClass $data The JSON object. + * + * @return SellerPayableBreakdown + */ + public function from_paypal_response( stdClass $data ): SellerPayableBreakdown { + + $gross_amount = ( isset( $data->gross_amount ) ) ? $this->money_factory->from_paypal_response( $data->gross_amount ) : null; + $paypal_fee = ( isset( $data->paypal_fee ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee ) : null; + $paypal_fee_in_receivable_currency = ( isset( $data->paypal_fee_in_receivable_currency ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee_in_receivable_currency ) : null; + $net_amount = ( isset( $data->net_amount ) ) ? $this->money_factory->from_paypal_response( $data->net_amount ) : null; + $net_amount_in_receivable_currency = ( isset( $data->net_amount_in_receivable_currency ) ) ? $this->money_factory->from_paypal_response( $data->net_amount_in_receivable_currency ) : null; + $total_refunded_amount = ( isset( $data->total_refunded_amount ) ) ? $this->money_factory->from_paypal_response( $data->total_refunded_amount ) : null; + $platform_fees = ( isset( $data->platform_fees ) ) ? array_map( + function ( stdClass $fee_data ): PlatformFee { + return $this->platform_fee_factory->from_paypal_response( $fee_data ); + }, + $data->platform_fees + ) : array(); + + return new SellerPayableBreakdown( + $gross_amount, + $paypal_fee, + $paypal_fee_in_receivable_currency, + $net_amount, + $net_amount_in_receivable_currency, + $total_refunded_amount, + $platform_fees + ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php b/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php index af978675d..497daba01 100644 --- a/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php +++ b/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php @@ -24,13 +24,18 @@ class FeesRenderer { * @return string */ public function render( WC_Order $wc_order ) : string { - $breakdown = $wc_order->get_meta( PayPalGateway::FEES_META_KEY ); + $breakdown = $wc_order->get_meta( PayPalGateway::FEES_META_KEY ); $refund_breakdown = $wc_order->get_meta( PayPalGateway::REFUND_FEES_META_KEY ) ?: array(); if ( ! is_array( $breakdown ) ) { return ''; } + $refund_fee = $refund_breakdown['paypal_fee'] ?? array(); + $refund_amount = $refund_breakdown['net_amount'] ?? array(); + $refund_total = ( $refund_fee['value'] ?? 0 ) + ( $refund_amount['value'] ?? 0 ); + $refund_currency = ( $refund_amount['currency_code'] === $refund_fee['currency_code'] ) ? $refund_amount['currency_code'] : ''; + $html = ''; $fee = $breakdown['paypal_fee'] ?? null; @@ -44,14 +49,7 @@ class FeesRenderer { ); } - $refund_total = 0; - $refund_currency = null; - - $refund_fee = $refund_breakdown['paypal_fee'] ?? null; - if ( is_array( $refund_fee ) ) { - $refund_total += $refund_fee['value']; - $refund_currency = $refund_fee['currency_code']; - + if ( $refund_fee ) { $html .= $this->render_money_row( __( 'PayPal Refund Fee:', 'woocommerce-paypal-payments' ), __( 'The fee PayPal collects for the refund transactions.', 'woocommerce-paypal-payments' ), @@ -62,16 +60,7 @@ class FeesRenderer { ); } - $refund_amount = $refund_breakdown['net_amount'] ?? null; - if ( is_array( $refund_amount ) ) { - $refund_total += $refund_amount['value']; - - if ( null === $refund_currency ) { - $refund_currency = $refund_fee['currency_code']; - } else if ( $refund_currency !== $refund_fee['currency_code'] ) { - $refund_currency = false; - } - + if ( $refund_amount ) { $html .= $this->render_money_row( __( 'PayPal Refunded:', 'woocommerce-paypal-payments' ), __( 'The net amount that was refunded.', 'woocommerce-paypal-payments' ), diff --git a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php index 5043dfd1b..6b61d9c1a 100644 --- a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php +++ b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php @@ -227,10 +227,12 @@ class IncomingWebhookEndpoint { foreach ( $this->handlers as $handler ) { if ( $handler->responsible_for_request( $request ) ) { + $event_type = ( $handler->event_types() ? current( $handler->event_types() ) : '' ) ?: ''; + $this->logger->debug( sprintf( 'Webhook is going to be handled by %s on %s', - ( $handler->event_types() ) ? current( $handler->event_types() ) : '', + $event_type, get_class( $handler ) ) ); @@ -238,7 +240,7 @@ class IncomingWebhookEndpoint { $this->logger->info( sprintf( 'Webhook has been handled by %s on %s', - ( $handler->event_types() ) ? current( $handler->event_types() ) : '', + $event_type, get_class( $handler ) ) ); From 4027f4a77e12acf5b55685f594cf62ad16819c56 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 4 Aug 2023 14:18:27 +0100 Subject: [PATCH 17/60] * Fix tests --- .../ppcp-api-client/src/Entity/Payments.php | 2 +- .../src/Admin/FeesRenderer.php | 2 +- .../PHPUnit/ApiClient/Entity/PaymentsTest.php | 21 ++++++++++++-- .../ApiClient/Factory/PaymentsFactoryTest.php | 13 ++++++++- .../WcGateway/Admin/FeesRendererTest.php | 29 +++++++++++++++++++ 5 files changed, 61 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/Payments.php b/modules/ppcp-api-client/src/Entity/Payments.php index 9039f36d7..720489c7a 100644 --- a/modules/ppcp-api-client/src/Entity/Payments.php +++ b/modules/ppcp-api-client/src/Entity/Payments.php @@ -42,7 +42,7 @@ class Payments { * @param array $captures The Captures. * @param array $refunds The Refunds. */ - public function __construct( array $authorizations, array $captures, array $refunds ) { + public function __construct( array $authorizations, array $captures, array $refunds = array() ) { foreach ( $authorizations as $key => $authorization ) { if ( is_a( $authorization, Authorization::class ) ) { continue; diff --git a/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php b/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php index 497daba01..8c5fac27d 100644 --- a/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php +++ b/modules/ppcp-wc-gateway/src/Admin/FeesRenderer.php @@ -34,7 +34,7 @@ class FeesRenderer { $refund_fee = $refund_breakdown['paypal_fee'] ?? array(); $refund_amount = $refund_breakdown['net_amount'] ?? array(); $refund_total = ( $refund_fee['value'] ?? 0 ) + ( $refund_amount['value'] ?? 0 ); - $refund_currency = ( $refund_amount['currency_code'] === $refund_fee['currency_code'] ) ? $refund_amount['currency_code'] : ''; + $refund_currency = ( ( $refund_amount['currency_code'] ?? '' ) === ( $refund_fee['currency_code'] ?? '' ) ) ? ( $refund_amount['currency_code'] ?? '' ) : ''; $html = ''; diff --git a/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php b/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php index 240735f73..2793039aa 100644 --- a/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php +++ b/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php @@ -43,10 +43,19 @@ class PaymentsTest extends TestCase 'status' => 'CREATED', ] ); - $captures = [$capture]; - $authorizations = [$authorization]; + $refund = \Mockery::mock(Refund::class); + $refund->shouldReceive('to_array')->andReturn( + [ + 'id' => 'refund', + 'status' => 'CREATED', + ] + ); - $testee = new Payments($authorizations, $captures); + $authorizations = [$authorization]; + $captures = [$capture]; + $refunds = [$refund]; + + $testee = new Payments($authorizations, $captures, $refunds); $this->assertEquals( [ @@ -62,6 +71,12 @@ class PaymentsTest extends TestCase 'status' => 'CREATED', ], ], + 'refunds' => [ + [ + 'id' => 'refund', + 'status' => 'CREATED', + ], + ], ], $testee->to_array() ); diff --git a/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php index 28e14b379..0260be7b0 100644 --- a/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php +++ b/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php @@ -7,6 +7,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund; use WooCommerce\PayPalCommerce\TestCase; use Mockery; @@ -18,11 +19,15 @@ class PaymentsFactoryTest extends TestCase $authorization->shouldReceive('to_array')->andReturn(['id' => 'foo', 'status' => 'CREATED']); $capture = Mockery::mock(Capture::class); $capture->shouldReceive('to_array')->andReturn(['id' => 'capture', 'status' => 'CREATED']); + $refund = Mockery::mock(Refund::class); + $refund->shouldReceive('to_array')->andReturn(['id' => 'refund', 'status' => 'CREATED']); $authorizationsFactory = Mockery::mock(AuthorizationFactory::class); $authorizationsFactory->shouldReceive('from_paypal_response')->andReturn($authorization); $captureFactory = Mockery::mock(CaptureFactory::class); $captureFactory->shouldReceive('from_paypal_response')->andReturn($capture); + $refundFactory = Mockery::mock(RefundFactory::class); + $refundFactory->shouldReceive('from_paypal_response')->andReturn($refund); $response = (object)[ 'authorizations' => [ (object)['id' => 'foo', 'status' => 'CREATED'], @@ -30,9 +35,12 @@ class PaymentsFactoryTest extends TestCase 'captures' => [ (object)['id' => 'capture', 'status' => 'CREATED'], ], + 'refunds' => [ + (object)['id' => 'refund', 'status' => 'CREATED'], + ], ]; - $testee = new PaymentsFactory($authorizationsFactory, $captureFactory); + $testee = new PaymentsFactory($authorizationsFactory, $captureFactory, $refundFactory); $result = $testee->from_paypal_response($response); $this->assertInstanceOf(Payments::class, $result); @@ -44,6 +52,9 @@ class PaymentsFactoryTest extends TestCase 'captures' => [ ['id' => 'capture', 'status' => 'CREATED'], ], + 'refunds' => [ + ['id' => 'refund', 'status' => 'CREATED'], + ], ]; $this->assertEquals($expectedToArray, $result->to_array()); } diff --git a/tests/PHPUnit/WcGateway/Admin/FeesRendererTest.php b/tests/PHPUnit/WcGateway/Admin/FeesRendererTest.php index 0f7ccedfb..a20ee8b72 100644 --- a/tests/PHPUnit/WcGateway/Admin/FeesRendererTest.php +++ b/tests/PHPUnit/WcGateway/Admin/FeesRendererTest.php @@ -43,11 +43,32 @@ class FeesRendererTest extends TestCase ], ]); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::REFUND_FEES_META_KEY) + ->andReturn([ + 'gross_amount' => [ + 'currency_code' => 'USD', + 'value' => '20.52', + ], + 'paypal_fee' => [ + 'currency_code' => 'USD', + 'value' => '0.51', + ], + 'net_amount' => [ + 'currency_code' => 'USD', + 'value' => '50.01', + ], + ]); + $result = $this->renderer->render($wcOrder); $this->assertStringContainsString('Fee', $result); $this->assertStringContainsString('0.41', $result); $this->assertStringContainsString('Payout', $result); $this->assertStringContainsString('10.01', $result); + $this->assertStringContainsString('PayPal Refund Fee', $result); + $this->assertStringContainsString('0.51', $result); + $this->assertStringContainsString('PayPal Refund', $result); + $this->assertStringContainsString('50.01', $result); } public function testRenderWithoutNet() { @@ -62,6 +83,10 @@ class FeesRendererTest extends TestCase ], ]); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::REFUND_FEES_META_KEY) + ->andReturn([]); + $result = $this->renderer->render($wcOrder); $this->assertStringContainsString('Fee', $result); $this->assertStringContainsString('0.41', $result); @@ -78,6 +103,10 @@ class FeesRendererTest extends TestCase ->with(PayPalGateway::FEES_META_KEY) ->andReturn($meta); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::REFUND_FEES_META_KEY) + ->andReturn([]); + $this->assertSame('', $this->renderer->render($wcOrder)); } From d32aaab1afbe9311e493d5d97501c124263c5eaf Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 4 Aug 2023 17:51:32 +0100 Subject: [PATCH 18/60] Add updating refund fees in order --- .../src/Endpoint/BillingPlans.php | 2 - .../src/Entity/SellerPayableBreakdown.php | 2 +- modules/ppcp-wc-gateway/services.php | 16 +- .../src/Helper/RefundFeesUpdater.php | 145 ++++++++++++++++++ .../src/Processor/RefundProcessor.php | 25 ++- modules/ppcp-webhooks/services.php | 5 +- .../src/Handler/PaymentCaptureRefunded.php | 25 ++- .../src/Handler/PaymentSaleRefunded.php | 17 +- 8 files changed, 212 insertions(+), 25 deletions(-) create mode 100644 modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php diff --git a/modules/ppcp-api-client/src/Endpoint/BillingPlans.php b/modules/ppcp-api-client/src/Endpoint/BillingPlans.php index 57bb8c406..2745b2c37 100644 --- a/modules/ppcp-api-client/src/Endpoint/BillingPlans.php +++ b/modules/ppcp-api-client/src/Endpoint/BillingPlans.php @@ -54,8 +54,6 @@ class BillingPlans { private $plan_factory; /** - * The logger. - * * The logger. * * @var LoggerInterface diff --git a/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php b/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php index 927ffad54..9c6bafdde 100644 --- a/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php +++ b/modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php @@ -99,7 +99,7 @@ class SellerPayableBreakdown { /** * The amount for this refunded payment in the currency of the transaction. * - * @return Money + * @return Money|null */ public function gross_amount(): ?Money { return $this->gross_amount; diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 26d866066..aaf46f421 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -53,6 +53,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper; use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; +use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; @@ -333,10 +334,11 @@ return array( ); }, 'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor { - $order_endpoint = $container->get( 'api.endpoint.order' ); - $payments_endpoint = $container->get( 'api.endpoint.payments' ); - $logger = $container->get( 'woocommerce.logger.woocommerce' ); - return new RefundProcessor( $order_endpoint, $payments_endpoint, $logger ); + $order_endpoint = $container->get( 'api.endpoint.order' ); + $payments_endpoint = $container->get( 'api.endpoint.payments' ); + $refund_fees_updater = $container->get( 'wcgateway.helper.refund-fees-updater' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + return new RefundProcessor( $order_endpoint, $payments_endpoint, $refund_fees_updater, $logger ); }, 'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor { $order_endpoint = $container->get( 'api.endpoint.order' ); @@ -1000,6 +1002,12 @@ return array( ); }, + 'wcgateway.helper.refund-fees-updater' => static function ( ContainerInterface $container ): RefundFeesUpdater { + $order_endpoint = $container->get( 'api.endpoint.order' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + return new RefundFeesUpdater( $order_endpoint, $logger ); + }, + 'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers { return new MessagesDisclaimers( $container->get( 'api.shop.country' ) diff --git a/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php b/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php new file mode 100644 index 000000000..648f93966 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php @@ -0,0 +1,145 @@ +order_endpoint = $order_endpoint; + $this->logger = $logger; + } + + /** + * Updates the fees meta for a given order. + * + * @param WC_Order $wc_order The WooCommerce order. + * @return void + */ + public function update( WC_Order $wc_order ): void { + $paypal_order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ); + + if ( ! $paypal_order_id ) { + $this->logger->error( + sprintf( 'Update order paypal refund fees. No PayPal order_id. [wc_order: %s]', $wc_order->get_id() ) + ); + return; + } + + $this->logger->debug( + sprintf( 'Updating order paypal refund fees. [wc_order: %s, paypal_order: %s]', $wc_order->get_id(), $paypal_order_id ) + ); + + $paypal_order = $this->order_endpoint->order( $paypal_order_id ); + $purchase_units = $paypal_order->purchase_units(); + + $gross_amount_total = 0.0; + $fee_total = 0.0; + $net_amount_total = 0.0; + $currency_codes = array(); + + foreach ( $purchase_units as $purchase_unit ) { + $payments = $purchase_unit->payments(); + + if ( ! $payments ) { + continue; + } + + $refunds = $payments->refunds(); + + foreach ( $refunds as $refund ) { + $breakdown = $refund->seller_payable_breakdown(); + + if ( ! $breakdown ) { + continue; + } + + $gross_amount = $breakdown->gross_amount(); + if ( $gross_amount ) { + $gross_amount_total += $gross_amount->value(); + $currency_codes[] = $gross_amount->currency_code(); + } + + $paypal_fee = $breakdown->paypal_fee(); + if ( $paypal_fee ) { + $fee_total += $paypal_fee->value(); + $currency_codes[] = $paypal_fee->currency_code(); + } + + $net_amount = $breakdown->net_amount(); + if ( $net_amount ) { + $gross_amount_total += $net_amount->value(); + $currency_codes[] = $net_amount->currency_code(); + } + } + } + + $currency_codes = array_unique( $currency_codes ); + + if ( count( $currency_codes ) > 1 ) { + // There are multiple different currencies codes in the refunds. + + $this->logger->warning( + sprintf( + 'Updating order paypal refund fees. Multiple currencies detected. [wc_order: %s, paypal_order: %s, currencies: %s]', + $wc_order->get_id(), + $paypal_order_id, + implode( ',', $currency_codes ) + ) + ); + + $wc_order->update_meta_data( PayPalGateway::REFUND_FEES_META_KEY, array() ); + return; + } + + $currency_code = current( $currency_codes ) ?: ''; + + $meta_data = array( + 'gross_amount' => ( new Money( $gross_amount_total, $currency_code ) )->to_array(), + 'paypal_fee' => ( new Money( $fee_total, $currency_code ) )->to_array(), + 'net_amount' => ( new Money( $net_amount_total, $currency_code ) )->to_array(), + ); + + $wc_order->update_meta_data( PayPalGateway::REFUND_FEES_META_KEY, $meta_data ); + $wc_order->save(); + + $this->logger->debug( + sprintf( 'Updated order paypal refund fees. [wc_order: %s, paypal_order: %s]', $wc_order->get_id(), $paypal_order_id ) + ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php index f67794cd4..a8f6dfa31 100644 --- a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php @@ -23,6 +23,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater; /** * Class RefundProcessor @@ -55,18 +56,27 @@ class RefundProcessor { */ private $logger; + /** + * The refund fees updater. + * + * @var RefundFeesUpdater + */ + private $refund_fees_updater; + /** * RefundProcessor constructor. * - * @param OrderEndpoint $order_endpoint The order endpoint. - * @param PaymentsEndpoint $payments_endpoint The payments endpoint. - * @param LoggerInterface $logger The logger. + * @param OrderEndpoint $order_endpoint The order endpoint. + * @param PaymentsEndpoint $payments_endpoint The payments endpoint. + * @param RefundFeesUpdater $refund_fees_updater The refund fees updater. + * @param LoggerInterface $logger The logger. */ - public function __construct( OrderEndpoint $order_endpoint, PaymentsEndpoint $payments_endpoint, LoggerInterface $logger ) { + public function __construct( OrderEndpoint $order_endpoint, PaymentsEndpoint $payments_endpoint, RefundFeesUpdater $refund_fees_updater, LoggerInterface $logger ) { - $this->order_endpoint = $order_endpoint; - $this->payments_endpoint = $payments_endpoint; - $this->logger = $logger; + $this->order_endpoint = $order_endpoint; + $this->payments_endpoint = $payments_endpoint; + $this->refund_fees_updater = $refund_fees_updater; + $this->logger = $logger; } /** @@ -106,6 +116,7 @@ class RefundProcessor { $refund_id = $this->refund( $order, $wc_order, $amount, $reason ); $this->add_refund_to_meta( $wc_order, $refund_id ); + $this->refund_fees_updater->update( $wc_order ); break; case self::REFUND_MODE_VOID: diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php index cd0424a19..9223a7348 100644 --- a/modules/ppcp-webhooks/services.php +++ b/modules/ppcp-webhooks/services.php @@ -80,6 +80,7 @@ return array( $order_endpoint = $container->get( 'api.endpoint.order' ); $authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' ); $payment_token_factory = $container->get( 'vaulting.payment-token-factory' ); + $refund_fees_updater = $container->get( 'wcgateway.helper.refund-fees-updater' ); return array( new CheckoutOrderApproved( @@ -91,14 +92,14 @@ return array( ), new CheckoutOrderCompleted( $logger ), new CheckoutPaymentApprovalReversed( $logger ), - new PaymentCaptureRefunded( $logger ), + new PaymentCaptureRefunded( $logger, $refund_fees_updater ), new PaymentCaptureReversed( $logger ), new PaymentCaptureCompleted( $logger, $order_endpoint ), new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory ), new VaultPaymentTokenDeleted( $logger ), new PaymentCapturePending( $logger ), new PaymentSaleCompleted( $logger ), - new PaymentSaleRefunded( $logger ), + new PaymentSaleRefunded( $logger, $refund_fees_updater ), new BillingSubscriptionCancelled( $logger ), new BillingPlanPricingChangeActivated( $logger ), new CatalogProductUpdated( $logger ), diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php b/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php index ede513aa2..053cd619a 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Webhooks\Handler; use Psr\Log\LoggerInterface; use WC_Order; +use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; use WP_Error; @@ -31,13 +32,22 @@ class PaymentCaptureRefunded implements RequestHandler { */ private $logger; + /** + * The refund fees updater. + * + * @var RefundFeesUpdater + */ + private $refund_fees_updater; + /** * PaymentCaptureRefunded constructor. * - * @param LoggerInterface $logger The logger. + * @param LoggerInterface $logger The logger. + * @param RefundFeesUpdater $refund_fees_updater The refund fees updater. */ - public function __construct( LoggerInterface $logger ) { - $this->logger = $logger; + public function __construct( LoggerInterface $logger, RefundFeesUpdater $refund_fees_updater ) { + $this->logger = $logger; + $this->refund_fees_updater = $refund_fees_updater; } /** @@ -68,9 +78,11 @@ class PaymentCaptureRefunded implements RequestHandler { * @return WP_REST_Response */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { - $order_id = isset( $request['resource']['custom_id'] ) ? - $request['resource']['custom_id'] : 0; - $refund_id = (string) ( $request['resource']['id'] ?? '' ); + $resource = ( $request['resource'] ?? array() ) ?: array(); + + $order_id = $resource['custom_id'] ?? 0; + $refund_id = (string) ( $resource['id'] ?? '' ); + if ( ! $order_id ) { $message = sprintf( 'No order for webhook event %s was found.', @@ -122,6 +134,7 @@ class PaymentCaptureRefunded implements RequestHandler { if ( $refund_id ) { $this->update_transaction_id( $refund_id, $wc_order, $this->logger ); $this->add_refund_to_meta( $wc_order, $refund_id ); + $this->refund_fees_updater->update( $wc_order ); } return $this->success_response(); diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php index b3f2c06bb..fd05bc922 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Webhooks\Handler; use Psr\Log\LoggerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; use WP_Error; @@ -30,13 +31,22 @@ class PaymentSaleRefunded implements RequestHandler { */ private $logger; + /** + * The refund fees updater. + * + * @var RefundFeesUpdater + */ + private $refund_fees_updater; + /** * PaymentSaleRefunded constructor. * - * @param LoggerInterface $logger The logger. + * @param LoggerInterface $logger The logger. + * @param RefundFeesUpdater $refund_fees_updater The refund fees updater. */ - public function __construct( LoggerInterface $logger ) { - $this->logger = $logger; + public function __construct( LoggerInterface $logger, RefundFeesUpdater $refund_fees_updater ) { + $this->logger = $logger; + $this->refund_fees_updater = $refund_fees_updater; } /** @@ -120,6 +130,7 @@ class PaymentSaleRefunded implements RequestHandler { $this->update_transaction_id( $refund_id, $wc_order, $this->logger ); $this->add_refund_to_meta( $wc_order, $refund_id ); + $this->refund_fees_updater->update( $wc_order ); } return $this->success_response(); From d6c310b90538ba1d65ba3aae287cbd85ff6bb051 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Sun, 6 Aug 2023 17:51:31 +0100 Subject: [PATCH 19/60] Add tests for RefundFeesUpdater Add notices on refund transactions Add api function ppcp_update_order_refund_fees --- api/order-functions.php | 12 ++ .../src/Helper/RefundFeesUpdater.php | 54 +++++++- .../PHPUnit/Api/OrderRefundFeesUpdateTest.php | 130 ++++++++++++++++++ 3 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 tests/PHPUnit/Api/OrderRefundFeesUpdateTest.php diff --git a/api/order-functions.php b/api/order-functions.php index f477651fd..701b064fb 100644 --- a/api/order-functions.php +++ b/api/order-functions.php @@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\PPCP; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; @@ -107,3 +108,14 @@ function ppcp_void_order( WC_Order $wc_order ): void { $refund_processor->void( $order ); } + +/** + * Updates the PayPal refund fees totals on an order. + * + * @param WC_Order $wc_order The WC order. + */ +function ppcp_update_order_refund_fees( WC_Order $wc_order ): void { + $updater = PPCP::container()->get( 'wcgateway.helper.refund-fees-updater' ); + assert( $updater instanceof RefundFeesUpdater ); + $updater->update( $wc_order ); +} diff --git a/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php b/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php index 648f93966..0f7930ee9 100644 --- a/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php +++ b/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php @@ -14,6 +14,7 @@ use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WP_Comment; /** * CheckoutHelper class. @@ -72,6 +73,7 @@ class RefundFeesUpdater { $fee_total = 0.0; $net_amount_total = 0.0; $currency_codes = array(); + $refunds_ids = array(); foreach ( $purchase_units as $purchase_unit ) { $payments = $purchase_unit->payments(); @@ -83,7 +85,8 @@ class RefundFeesUpdater { $refunds = $payments->refunds(); foreach ( $refunds as $refund ) { - $breakdown = $refund->seller_payable_breakdown(); + $breakdown = $refund->seller_payable_breakdown(); + $refunds_ids[] = $refund->id(); if ( ! $breakdown ) { continue; @@ -103,8 +106,8 @@ class RefundFeesUpdater { $net_amount = $breakdown->net_amount(); if ( $net_amount ) { - $gross_amount_total += $net_amount->value(); - $currency_codes[] = $net_amount->currency_code(); + $net_amount_total += $net_amount->value(); + $currency_codes[] = $net_amount->currency_code(); } } } @@ -138,8 +141,53 @@ class RefundFeesUpdater { $wc_order->update_meta_data( PayPalGateway::REFUND_FEES_META_KEY, $meta_data ); $wc_order->save(); + $order_notes = $this->get_order_notes( $wc_order ); + + foreach ( $refunds_ids as $refund_id ) { + $has_note = false; + foreach ( $order_notes as $order_note ) { + if ( strpos( $order_note->comment_content, $refund_id ) !== false ) { + $has_note = true; + } + } + if ( ! $has_note ) { + $wc_order->add_order_note( sprintf( 'PayPal refund ID: %s', $refund_id ) ); + } + } + $this->logger->debug( sprintf( 'Updated order paypal refund fees. [wc_order: %s, paypal_order: %s]', $wc_order->get_id(), $paypal_order_id ) ); } + + /** + * Returns all order notes + * Based on WC_Order::get_customer_order_notes + * + * @param WC_Order $wc_order The WooCommerce order. + * @return WP_Comment[] + */ + private function get_order_notes( WC_Order $wc_order ): array { + $notes = array(); + $args = array( + 'post_id' => $wc_order->get_id(), + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); + + $comments = get_comments( $args ); + + if ( is_array( $comments ) ) { + foreach ( $comments as $comment ) { + if ( $comment instanceof WP_Comment ) { + $notes[] = $comment; + } + } + } + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); + + return $notes; + } + } diff --git a/tests/PHPUnit/Api/OrderRefundFeesUpdateTest.php b/tests/PHPUnit/Api/OrderRefundFeesUpdateTest.php new file mode 100644 index 000000000..c9ccedc82 --- /dev/null +++ b/tests/PHPUnit/Api/OrderRefundFeesUpdateTest.php @@ -0,0 +1,130 @@ +order_endpoint = $this->createMock(OrderEndpoint::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->refundFeesUpdater = new RefundFeesUpdater($this->order_endpoint, $this->logger); + } + + public function testUpdateWithoutPaypalOrderId(): void + { + $wc_order_id = 123; + + $wc_order = Mockery::mock(WC_Order::class); + $wc_order->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn(null); + + $wc_order->expects('get_id')->andReturn($wc_order_id); + + $this->logger->expects($this->once()) + ->method('error'); + + $this->refundFeesUpdater->update($wc_order); + } + + public function testUpdateWithValidData(): void + { + $wc_order_id = 123; + $paypal_order_id = 'test_order_id'; + $refund_id = 'XYZ123'; + $meta_data = [ + 'gross_amount' => ['value' => 10.0, 'currency_code' => 'USD'], + 'paypal_fee' => ['value' => 7.0, 'currency_code' => 'USD'], + 'net_amount' => ['value' => 3.0, 'currency_code' => 'USD'], + ]; + + when('get_comments')->justReturn([]); + + $wc_order = Mockery::mock(WC_Order::class); + $wc_order->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn($paypal_order_id); + + $wc_order->expects('get_id') + ->times(3) + ->andReturn($wc_order_id); + + $wc_order->expects('update_meta_data') + ->once() + ->with('_ppcp_paypal_refund_fees', $meta_data); + + $wc_order->expects('add_order_note') + ->once() + ->withArgs(function ($arg) use ($refund_id) { + return strpos($arg, $refund_id) !== false; + }); + + $wc_order->expects('save')->once(); + + $moneyGross = Mockery::mock(Money::class); + $moneyGross->expects('value')->once()->andReturn($meta_data['gross_amount']['value']); + $moneyGross->expects('currency_code')->once()->andReturn($meta_data['gross_amount']['currency_code']); + + $moneyFee = Mockery::mock(Money::class); + $moneyFee->expects('value')->once()->andReturn($meta_data['paypal_fee']['value']); + $moneyFee->expects('currency_code')->once()->andReturn($meta_data['paypal_fee']['currency_code']); + + $moneyNet = Mockery::mock(Money::class); + $moneyNet->expects('value')->once()->andReturn($meta_data['net_amount']['value']); + $moneyNet->expects('currency_code')->once()->andReturn($meta_data['net_amount']['currency_code']); + + $breakdown = $this->getMockBuilder(\stdClass::class) + ->addMethods(['gross_amount', 'paypal_fee', 'net_amount']) + ->getMock(); + $breakdown->method('gross_amount')->willReturn($moneyGross); + $breakdown->method('paypal_fee')->willReturn($moneyFee); + $breakdown->method('net_amount')->willReturn($moneyNet); + + $refund = $this->getMockBuilder(\stdClass::class) + ->addMethods(['id', 'seller_payable_breakdown']) + ->getMock(); + $refund->method('id')->willReturn($refund_id); + $refund->method('seller_payable_breakdown')->willReturn($breakdown); + + $payments = $this->getMockBuilder(\stdClass::class) + ->addMethods(['refunds']) + ->getMock(); + $payments->method('refunds')->willReturn([$refund]); + + $purchase_unit = $this->getMockBuilder(\stdClass::class) + ->addMethods(['payments']) + ->getMock(); + $purchase_unit->method('payments')->willReturn($payments); + + $paypal_order = Mockery::mock(Order::class); + $paypal_order->expects('purchase_units')->andReturn([$purchase_unit]); + + $this->order_endpoint->method('order')->with($paypal_order_id)->willReturn($paypal_order); + + $this->logger->expects($this->exactly(2)) + ->method('debug') + ->withConsecutive( + [$this->stringContains('Updating order paypal refund fees.')], + [$this->stringContains('Updated order paypal refund fees.')] + ); + + $this->refundFeesUpdater->update($wc_order); + } +} From a822b56102869981c56506fc46267448e4b97b35 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 7 Aug 2023 08:49:31 +0100 Subject: [PATCH 20/60] Fix typo --- modules/ppcp-api-client/src/Entity/Payments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Entity/Payments.php b/modules/ppcp-api-client/src/Entity/Payments.php index 720489c7a..2dbcf9582 100644 --- a/modules/ppcp-api-client/src/Entity/Payments.php +++ b/modules/ppcp-api-client/src/Entity/Payments.php @@ -29,7 +29,7 @@ class Payments { private $captures; /** - * The Captures. + * The Refunds. * * @var Refund[] */ From 837bdb0392b9a9fd10a695fa3e05ceb697419e10 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 7 Aug 2023 16:12:02 +0100 Subject: [PATCH 21/60] Add PurchaseUnitSanitizer tests Add subtotal mismatch admin options --- modules/ppcp-api-client/services.php | 13 +- .../src/Entity/PurchaseUnit.php | 21 ++- .../src/Factory/PurchaseUnitFactory.php | 49 +++++-- .../src/Helper/PurchaseUnitSanitizer.php | 109 ++++++++++---- .../Settings/Fields/connection-tab-fields.php | 37 +++++ .../ApiClient/Entity/PurchaseUnitTest.php | 135 ++++++++++++++++-- 6 files changed, 314 insertions(+), 50 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 71c9a25f8..92d251507 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; +use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; @@ -299,6 +300,7 @@ return array( $payments_factory = $container->get( 'api.factory.payments' ); $prefix = $container->get( 'api.prefix' ); $soft_descriptor = $container->get( 'wcgateway.soft-descriptor' ); + $sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' ); return new PurchaseUnitFactory( $amount_factory, @@ -308,7 +310,8 @@ return array( $shipping_factory, $payments_factory, $prefix, - $soft_descriptor + $soft_descriptor, + $sanitizer ); }, 'api.factory.patch-collection-factory' => static function ( ContainerInterface $container ): PatchCollectionFactory { @@ -814,4 +817,12 @@ return array( 'api.order-helper' => static function( ContainerInterface $container ): OrderHelper { return new OrderHelper(); }, + 'api.helper.purchase-unit-sanitizer' => static function( ContainerInterface $container ): PurchaseUnitSanitizer { + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + $behavior = $settings->has( 'subtotal_mismatch_behavior' ) ? $settings->get( 'subtotal_mismatch_behavior' ) : null; + $line_name = $settings->has( 'subtotal_mismatch_line_name' ) ? $settings->get( 'subtotal_mismatch_line_name' ) : null; + return new PurchaseUnitSanitizer( $behavior, $line_name ); + }, ); diff --git a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php index 9359b422b..cb2601a9d 100644 --- a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php +++ b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php @@ -93,6 +93,13 @@ class PurchaseUnit { */ private $contains_physical_goods = false; + /** + * The sanitizer for this purchase unit output. + * + * @var PurchaseUnitSanitizer|null + */ + private $sanitizer; + /** * PurchaseUnit constructor. * @@ -222,6 +229,16 @@ class PurchaseUnit { $this->custom_id = $custom_id; } + /** + * Sets the sanitizer for this purchase unit output. + * + * @param PurchaseUnitSanitizer|null $sanitizer The sanitizer. + * @return void + */ + public function set_sanitizer( ?PurchaseUnitSanitizer $sanitizer ) { + $this->sanitizer = $sanitizer; + } + /** * Returns the invoice id. * @@ -317,8 +334,8 @@ class PurchaseUnit { $purchase_unit['soft_descriptor'] = $this->soft_descriptor(); } - if ( $sanitize_output ) { - $purchase_unit = ( new PurchaseUnitSanitizer( $purchase_unit, $this->items() ) )->sanitize(); + if ( $sanitize_output && isset( $this->sanitizer ) ) { + $purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $this->items() ) ); } return $purchase_unit; diff --git a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php index 8b06d17f5..ecb35592b 100644 --- a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php +++ b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -13,6 +13,7 @@ use WC_Session_Handler; use WooCommerce\PayPalCommerce\ApiClient\Entity\Item; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; use WooCommerce\PayPalCommerce\Webhooks\CustomIds; @@ -77,17 +78,25 @@ class PurchaseUnitFactory { */ private $soft_descriptor; + /** + * The sanitizer for purchase unit output data. + * + * @var PurchaseUnitSanitizer|null + */ + private $sanitizer; + /** * PurchaseUnitFactory constructor. * - * @param AmountFactory $amount_factory The amount factory. - * @param PayeeRepository $payee_repository The Payee repository. - * @param PayeeFactory $payee_factory The Payee factory. - * @param ItemFactory $item_factory The item factory. - * @param ShippingFactory $shipping_factory The shipping factory. - * @param PaymentsFactory $payments_factory The payments factory. - * @param string $prefix The prefix. - * @param string $soft_descriptor The soft descriptor. + * @param AmountFactory $amount_factory The amount factory. + * @param PayeeRepository $payee_repository The Payee repository. + * @param PayeeFactory $payee_factory The Payee factory. + * @param ItemFactory $item_factory The item factory. + * @param ShippingFactory $shipping_factory The shipping factory. + * @param PaymentsFactory $payments_factory The payments factory. + * @param string $prefix The prefix. + * @param string $soft_descriptor The soft descriptor. + * @param ?PurchaseUnitSanitizer $sanitizer The purchase unit to_array sanitizer. */ public function __construct( AmountFactory $amount_factory, @@ -97,7 +106,8 @@ class PurchaseUnitFactory { ShippingFactory $shipping_factory, PaymentsFactory $payments_factory, string $prefix = 'WC-', - string $soft_descriptor = '' + string $soft_descriptor = '', + PurchaseUnitSanitizer $sanitizer = null ) { $this->amount_factory = $amount_factory; @@ -108,6 +118,7 @@ class PurchaseUnitFactory { $this->payments_factory = $payments_factory; $this->prefix = $prefix; $this->soft_descriptor = $soft_descriptor; + $this->sanitizer = $sanitizer; } /** @@ -151,6 +162,9 @@ class PurchaseUnitFactory { $invoice_id, $soft_descriptor ); + + $this->init_purchase_unit( $purchase_unit ); + /** * Returns PurchaseUnit for the WC order. */ @@ -221,6 +235,8 @@ class PurchaseUnitFactory { $soft_descriptor ); + $this->init_purchase_unit( $purchase_unit ); + return $purchase_unit; } @@ -283,6 +299,9 @@ class PurchaseUnitFactory { $soft_descriptor, $payments ); + + $this->init_purchase_unit( $purchase_unit ); + return $purchase_unit; } @@ -313,4 +332,16 @@ class PurchaseUnitFactory { $countries = array( 'AE', 'AF', 'AG', 'AI', 'AL', 'AN', 'AO', 'AW', 'BB', 'BF', 'BH', 'BI', 'BJ', 'BM', 'BO', 'BS', 'BT', 'BW', 'BZ', 'CD', 'CF', 'CG', 'CI', 'CK', 'CL', 'CM', 'CO', 'CR', 'CV', 'DJ', 'DM', 'DO', 'EC', 'EG', 'ER', 'ET', 'FJ', 'FK', 'GA', 'GD', 'GH', 'GI', 'GM', 'GN', 'GQ', 'GT', 'GW', 'GY', 'HK', 'HN', 'HT', 'IE', 'IQ', 'IR', 'JM', 'JO', 'KE', 'KH', 'KI', 'KM', 'KN', 'KP', 'KW', 'KY', 'LA', 'LB', 'LC', 'LK', 'LR', 'LS', 'LY', 'ML', 'MM', 'MO', 'MR', 'MS', 'MT', 'MU', 'MW', 'MZ', 'NA', 'NE', 'NG', 'NI', 'NP', 'NR', 'NU', 'OM', 'PA', 'PE', 'PF', 'PY', 'QA', 'RW', 'SA', 'SB', 'SC', 'SD', 'SL', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SY', 'TC', 'TD', 'TG', 'TL', 'TO', 'TT', 'TV', 'TZ', 'UG', 'UY', 'VC', 'VE', 'VG', 'VN', 'VU', 'WS', 'XA', 'XB', 'XC', 'XE', 'XL', 'XM', 'XN', 'XS', 'YE', 'ZM', 'ZW' ); return in_array( $country_code, $countries, true ); } + + /** + * Initializes a purchase unit object. + * + * @param PurchaseUnit $purchase_unit The purchase unit. + * @return void + */ + private function init_purchase_unit( PurchaseUnit $purchase_unit ): void { + if ( $this->sanitizer instanceof PurchaseUnitSanitizer ) { + $purchase_unit->set_sanitizer( $this->sanitizer ); + } + } } diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php index a6bc1a289..1f2a972e6 100644 --- a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -24,31 +24,80 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; * Class PurchaseUnitSanitizer */ class PurchaseUnitSanitizer { + const MODE_DITCH = 'ditch'; + const MODE_EXTRA_LINE = 'extra_line'; + const VALID_MODES = array( + self::MODE_DITCH, + self::MODE_EXTRA_LINE, + ); + + const EXTRA_LINE_NAME = 'Subtotal mismatch'; /** * The purchase unit data * * @var array */ - private $purchase_unit; + private $purchase_unit = array(); /** * The purchase unit Item objects * * @var array|Item[] */ - private $item_objects; + private $item_objects = array(); + + /** + * The working mode + * + * @var string + */ + private $mode; + + /** + * The name for the extra line + * + * @var string + */ + private $extra_line_name; /** * PurchaseUnitSanitizer constructor. * - * @param array $purchase_unit The purchase_unit array that should be sanitized. - * @param array|Item[] $item_objects The purchase unit Item objects used for recalculations. + * @param string|null $mode The mismatch handling mode, ditch or extra_line. + * @param string|null $extra_line_name The name of the extra line. */ - public function __construct( array $purchase_unit, array $item_objects ) { - $this->purchase_unit = $purchase_unit; - $this->item_objects = $item_objects; + public function __construct( string $mode = null, string $extra_line_name = null ) { + + if ( ! in_array( $mode, self::VALID_MODES, true ) ) { + $mode = self::MODE_DITCH; + } + + if ( ! $extra_line_name ) { + $extra_line_name = self::EXTRA_LINE_NAME; + } + + $this->mode = $mode; + $this->extra_line_name = $extra_line_name; + } + + /** + * Indicates if mode is ditch. + * + * @return bool + */ + private function is_mode_ditch(): bool { + return $this->mode === self::MODE_DITCH; + } + + /** + * Indicates if mode is adding extra line. + * + * @return bool + */ + private function is_mode_extra_line(): bool { + return $this->mode === self::MODE_EXTRA_LINE; } /** @@ -103,9 +152,14 @@ class PurchaseUnitSanitizer { /** * The sanitizes the purchase_unit array. * + * @param array $purchase_unit The purchase_unit array that should be sanitized. + * @param array|Item[] $item_objects The purchase unit Item objects used for recalculations. * @return array */ - public function sanitize(): array { + public function sanitize( array $purchase_unit, array $item_objects ): array { + $this->purchase_unit = $purchase_unit; + $this->item_objects = $item_objects; + $this->sanitize_item_amount_mismatch(); $this->sanitize_item_tax_mismatch(); $this->sanitize_breakdown_mismatch(); @@ -120,25 +174,28 @@ class PurchaseUnitSanitizer { private function sanitize_item_amount_mismatch(): void { $item_mismatch = $this->calculate_item_mismatch(); - if ( $item_mismatch < 0 ) { - // Do floors on item amounts so item_mismatch is a positive value. - foreach ( $this->item_objects as $index => $item ) { - $this->purchase_unit['items'][ $index ] = $item->to_array( - $item->unit_amount()->is_rounding_up() - ); + if ( $this->is_mode_extra_line() ) { + if ( $item_mismatch < 0 ) { + // Do floors on item amounts so item_mismatch is a positive value. + foreach ( $this->item_objects as $index => $item ) { + $this->purchase_unit['items'][ $index ] = $item->to_array( + $item->unit_amount()->is_rounding_up() + ); + } } + + $item_mismatch = $this->calculate_item_mismatch(); + + if ( $item_mismatch > 0 ) { + // Add extra line item with roundings. + $line_name = $this->extra_line_name; + $roundings_money = new Money( $item_mismatch, $this->currency_code() ); + $this->purchase_unit['items'][] = ( new Item( $line_name, $roundings_money, 1 ) )->to_array(); + } + + $item_mismatch = $this->calculate_item_mismatch(); } - $item_mismatch = $this->calculate_item_mismatch(); - - if ( $item_mismatch > 0 ) { - // Add extra line item with roundings. - $roundings_money = new Money( $item_mismatch, $this->currency_code() ); - $this->purchase_unit['items'][] = ( new Item( 'Roundings', $roundings_money, 1 ) )->to_array(); - } - - $item_mismatch = $this->calculate_item_mismatch(); - if ( $item_mismatch !== 0.0 ) { // Ditch items. if ( isset( $this->purchase_unit['items'] ) ) { @@ -257,8 +314,8 @@ class PurchaseUnitSanitizer { $amount_total += $this->breakdown_value( 'item_total' ); $amount_total += $this->breakdown_value( 'tax_total' ); $amount_total += $this->breakdown_value( 'shipping' ); - $amount_total += $this->breakdown_value( 'discount' ); - $amount_total += $this->breakdown_value( 'shipping_discount' ); + $amount_total -= $this->breakdown_value( 'discount' ); + $amount_total -= $this->breakdown_value( 'shipping_discount' ); $amount_total += $this->breakdown_value( 'handling' ); $amount_total += $this->breakdown_value( 'insurance' ); diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 9d42b1240..0b099ebae 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Settings; +use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Onboarding\Environment; @@ -496,6 +497,42 @@ return function ( ContainerInterface $container, array $fields ): array { 'requirements' => array(), 'gateway' => Settings::CONNECTION_TAB_ID, ), + 'subtotal_mismatch_behavior' => array( + 'title' => __( 'Subtotal mismatch behavior', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'vertical', + 'desc_tip' => true, + 'description' => __( + 'Differences between WooCommerce and PayPal roundings may give origin to a mismatch in order items subtotal calculations. If not handled these mismatches will cause the PayPal transaction to fail.', + 'woocommerce-paypal-payments' + ), + 'options' => array( + PurchaseUnitSanitizer::MODE_DITCH => __( 'Do not send line items to PayPal', 'woocommerce-paypal-payments' ), + PurchaseUnitSanitizer::MODE_EXTRA_LINE => __( 'Add another line item', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( + State::STATE_START, + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, + ), + 'subtotal_mismatch_line_name' => array( + 'title' => __( 'Subtotal mismatch line name', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'The name of the extra line that will be sent to PayPal to correct the subtotal mismatch.', 'woocommerce-paypal-payments' ), + 'maxlength' => 22, + 'default' => '', + 'screens' => array( + State::STATE_START, + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'placeholder' => PurchaseUnitSanitizer::EXTRA_LINE_NAME, + 'gateway' => Settings::CONNECTION_TAB_ID, + ), ); return array_merge( $fields, $connection_fields ); diff --git a/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php b/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php index dbb18c4de..5b546307c 100644 --- a/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php +++ b/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Entity; +use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer; use WooCommerce\PayPalCommerce\TestCase; use Mockery; @@ -75,24 +76,57 @@ class PurchaseUnitTest extends TestCase $this->assertEquals($expected, $testee->to_array()); } - /** - * @dataProvider dataForDitchTests - * @param array $items - * @param Amount $amount - * @param bool $doDitch - */ - public function testDitchMethod(array $items, Amount $amount, bool $doDitch, string $message) + /** + * @dataProvider dataForDitchTests + * @param array $items + * @param Amount $amount + * @param bool|array $doDitch + * @param string $message + */ + public function testDitchMethod(array $items, Amount $amount, $doDitch, string $message) { + if (is_array($doDitch)) { + $doDitchItems = $doDitch['items']; + $doDitchBreakdown = $doDitch['breakdown']; + $doDitchTax = $doDitch['tax']; + } else { + $doDitchItems = $doDitch; + $doDitchBreakdown = $doDitch; + $doDitchTax = $doDitch; + } + +// $dataSetName = $this->dataName(); +// if ($dataSetName !== 'dont_ditch_with_discount') { +// return; +// } +// +// print_r($amount->to_array()); +// foreach ($items as $item) { +// print_r($item->to_array()); +// } + $testee = new PurchaseUnit( $amount, $items ); + $testee->set_sanitizer(new PurchaseUnitSanitizer(PurchaseUnitSanitizer::MODE_DITCH)); + $array = $testee->to_array(); - $resultItems = $doDitch === ! array_key_exists('items', $array); - $resultBreakdown = $doDitch === ! array_key_exists('breakdown', $array['amount']); + $resultItems = $doDitchItems === ! array_key_exists('items', $array); +// +// echo "------ RESULT ------\n"; +// print_r($array); +// die('.'); + + $resultBreakdown = $doDitchBreakdown === ! array_key_exists('breakdown', $array['amount']); $this->assertTrue($resultItems, $message); $this->assertTrue($resultBreakdown, $message); + + foreach ($array['items'] ?? [] as $item) { + $resultTax = $doDitchTax === ! array_key_exists('tax', $item); + $this->assertTrue($resultTax, $message); + } } public function dataForDitchTests() : array @@ -406,6 +440,58 @@ class PurchaseUnitTest extends TestCase 'insurance' => null, ], ], + 'ditch_items_total_but_not_breakdown' => [ + 'message' => 'Items should be ditched because the item total does not add up. But not breakdown because it adds up.', + 'ditch' => [ + 'items' => true, + 'breakdown' => false, + 'tax' => true, + ], + 'items' => [ + [ + 'value' => 11, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26, + 'breakdown' => [ + 'item_total' => 20, + 'tax_total' => 6, + 'shipping' => null, + 'discount' => null, + 'shipping_discount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'ditch_items_tax_with_incorrect_tax_total' => [ + 'message' => 'Ditch tax from items. Items should not be ditched because the mismatch is on the tax.', + 'ditch' => [ + 'items' => false, + 'breakdown' => false, + 'tax' => true, + ], + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 4, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26, + 'breakdown' => [ + 'item_total' => 20, + 'tax_total' => 6, + 'shipping' => null, + 'discount' => null, + 'shipping_discount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], ]; $values = []; @@ -421,10 +507,16 @@ class PurchaseUnitTest extends TestCase 'tax' => $tax, 'quantity'=> $item['quantity'], 'category' => $item['category'], - 'to_array' => [], + 'to_array' => [ + 'unit_amount' => $unitAmount->to_array(), + 'tax' => $tax->to_array(), + 'quantity'=> $item['quantity'], + 'category' => $item['category'], + ], ] ); } + $breakdown = null; if ($test['breakdown']) { $breakdown = Mockery::mock(AmountBreakdown::class); @@ -438,10 +530,29 @@ class PurchaseUnitTest extends TestCase return $money; }); } + + $breakdown + ->shouldReceive('to_array') + ->andReturn( + array_map( + function ($value) { + return $value ? (new Money($value, 'EUR'))->to_array() : null; + }, + $test['breakdown'] + ) + ); } + + $amountMoney = new Money($test['amount'], 'EUR'); $amount = Mockery::mock(Amount::class); - $amount->shouldReceive('to_array')->andReturn(['value' => number_format( $test['amount'], 2, '.', '' ), 'breakdown' => []]); - $amount->shouldReceive('value_str')->andReturn(number_format( $test['amount'], 2, '.', '' )); + $amount + ->shouldReceive('to_array') + ->andReturn([ + 'value' => $amountMoney->value_str(), + 'currency_code' => $amountMoney->currency_code(), + 'breakdown' => $breakdown ? $breakdown->to_array() : [], + ]); + $amount->shouldReceive('value_str')->andReturn($amountMoney->value_str()); $amount->shouldReceive('currency_code')->andReturn('EUR'); $amount->shouldReceive('breakdown')->andReturn($breakdown); From a43b93ffff06a4169fb40843e40110feb536b96c Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 7 Aug 2023 18:09:25 +0100 Subject: [PATCH 22/60] Add test cases for PurchaseUnitSanitizer --- .../Endpoint/PayUponInvoiceOrderEndpoint.php | 2 +- .../src/Entity/PurchaseUnit.php | 5 +- .../src/Helper/PurchaseUnitSanitizer.php | 22 +- .../ApiClient/Entity/PurchaseUnitTest.php | 270 +++++++++++++++++- 4 files changed, 276 insertions(+), 23 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php index 97b40a73d..df3091311 100644 --- a/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php @@ -112,7 +112,7 @@ class PayUponInvoiceOrderEndpoint { 'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL', 'purchase_units' => array_map( static function ( PurchaseUnit $item ): array { - return $item->to_array( false ); + return $item->to_array( true, false ); }, $items ), diff --git a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php index cb2601a9d..26928cf4c 100644 --- a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php +++ b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php @@ -297,10 +297,11 @@ class PurchaseUnit { * Returns the object as array. * * @param bool $sanitize_output Whether output should be sanitized for PayPal consumption. + * @param bool $allow_ditch_items Whether to allow items to be ditched. * * @return array */ - public function to_array( bool $sanitize_output = true ): array { + public function to_array( bool $sanitize_output = true, bool $allow_ditch_items = true ): array { $purchase_unit = array( 'reference_id' => $this->reference_id(), 'amount' => $this->amount()->to_array(), @@ -335,7 +336,7 @@ class PurchaseUnit { } if ( $sanitize_output && isset( $this->sanitizer ) ) { - $purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $this->items() ) ); + $purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $this->items(), $allow_ditch_items ) ); } return $purchase_unit; diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php index 1f2a972e6..263b428e4 100644 --- a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -47,6 +47,13 @@ class PurchaseUnitSanitizer { */ private $item_objects = array(); + /** + * Whether to allow items to be ditched. + * + * @var bool + */ + private $allow_ditch_items = true; + /** * The working mode * @@ -154,11 +161,13 @@ class PurchaseUnitSanitizer { * * @param array $purchase_unit The purchase_unit array that should be sanitized. * @param array|Item[] $item_objects The purchase unit Item objects used for recalculations. + * @param bool $allow_ditch_items Whether to allow items to be ditched. * @return array */ - public function sanitize( array $purchase_unit, array $item_objects ): array { - $this->purchase_unit = $purchase_unit; - $this->item_objects = $item_objects; + public function sanitize( array $purchase_unit, array $item_objects, bool $allow_ditch_items = true ): array { + $this->purchase_unit = $purchase_unit; + $this->item_objects = $item_objects; + $this->allow_ditch_items = $allow_ditch_items; $this->sanitize_item_amount_mismatch(); $this->sanitize_item_tax_mismatch(); @@ -176,6 +185,7 @@ class PurchaseUnitSanitizer { if ( $this->is_mode_extra_line() ) { if ( $item_mismatch < 0 ) { + // Do floors on item amounts so item_mismatch is a positive value. foreach ( $this->item_objects as $index => $item ) { $this->purchase_unit['items'][ $index ] = $item->to_array( @@ -198,7 +208,7 @@ class PurchaseUnitSanitizer { if ( $item_mismatch !== 0.0 ) { // Ditch items. - if ( isset( $this->purchase_unit['items'] ) ) { + if ( $this->allow_ditch_items && isset( $this->purchase_unit['items'] ) ) { unset( $this->purchase_unit['items'] ); } } @@ -212,7 +222,7 @@ class PurchaseUnitSanitizer { private function sanitize_item_tax_mismatch(): void { $tax_mismatch = $this->calculate_tax_mismatch(); - if ( $tax_mismatch !== 0.0 ) { + if ( $this->allow_ditch_items && $tax_mismatch !== 0.0 ) { // Unset tax in items. foreach ( $this->purchase_unit['items'] as $index => $item ) { if ( isset( $this->purchase_unit['items'][ $index ]['tax'] ) ) { @@ -233,7 +243,7 @@ class PurchaseUnitSanitizer { private function sanitize_breakdown_mismatch(): void { $breakdown_mismatch = $this->calculate_breakdown_mismatch(); - if ( $breakdown_mismatch !== 0.0 ) { + if ( $this->allow_ditch_items && $breakdown_mismatch !== 0.0 ) { // Ditch breakdowns and items. if ( isset( $this->purchase_unit['items'] ) ) { unset( $this->purchase_unit['items'] ); diff --git a/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php b/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php index 5b546307c..328f5cb15 100644 --- a/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php +++ b/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php @@ -95,16 +95,6 @@ class PurchaseUnitTest extends TestCase $doDitchTax = $doDitch; } -// $dataSetName = $this->dataName(); -// if ($dataSetName !== 'dont_ditch_with_discount') { -// return; -// } -// -// print_r($amount->to_array()); -// foreach ($items as $item) { -// print_r($item->to_array()); -// } - $testee = new PurchaseUnit( $amount, $items @@ -114,10 +104,6 @@ class PurchaseUnitTest extends TestCase $array = $testee->to_array(); $resultItems = $doDitchItems === ! array_key_exists('items', $array); -// -// echo "------ RESULT ------\n"; -// print_r($array); -// die('.'); $resultBreakdown = $doDitchBreakdown === ! array_key_exists('breakdown', $array['amount']); $this->assertTrue($resultItems, $message); @@ -567,6 +553,262 @@ class PurchaseUnitTest extends TestCase return $values; } + /** + * @dataProvider dataForExtraLineTests + * @param array $items + * @param Amount $amount + * @param array $expected + * @param string $message + */ + public function testExtraLineMethod(array $items, Amount $amount, array $expected, string $message) + { + $testee = new PurchaseUnit( + $amount, + $items + ); + + $testee->set_sanitizer(new PurchaseUnitSanitizer(PurchaseUnitSanitizer::MODE_EXTRA_LINE, $expected['extra_line_name'] ?? null)); + + $countItemsBefore = count($items); + $array = $testee->to_array(); + $countItemsAfter = count($array['items']); + $extraItem = array_pop($array['items']); + + $this->assertEquals($countItemsBefore + 1, $countItemsAfter, $message); + $this->assertEquals($expected['extra_line_value'], $extraItem['unit_amount']['value'], $message); + $this->assertEquals($expected['extra_line_name'] ?? PurchaseUnitSanitizer::EXTRA_LINE_NAME, $extraItem['name'], $message); + + foreach ($array['items'] as $i => $item) { + $this->assertEquals($expected['item_value'][$i], $item['unit_amount']['value'], $message); + } + } + + public function dataForExtraLineTests() : array + { + $data = [ + 'default' => [ + 'message' => 'Extra line should be added with price 0.01 and line amount 10.', + 'expected' => [ + 'item_value' => [10], + 'extra_line_value' => 0.01, + ], + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26.01, + 'breakdown' => [ + 'item_total' => 20.01, + 'tax_total' => 6, + 'shipping' => null, + 'discount' => null, + 'shipping_discount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'with_custom_name' => [ + 'message' => 'Extra line should be added with price 0.01 and line amount 10.', + 'expected' => [ + 'item_value' => [10], + 'extra_line_value' => 0.01, + 'extra_line_name' => 'My custom line name', + ], + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26.01, + 'breakdown' => [ + 'item_total' => 20.01, + 'tax_total' => 6, + 'shipping' => null, + 'discount' => null, + 'shipping_discount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'with_rounding_down' => [ + 'message' => 'Extra line should be added with price 0.01 and line amount 10.00.', + 'expected' => [ + 'item_value' => [10.00], + 'extra_line_value' => 0.01 + ], + 'items' => [ + [ + 'value' => 10.005, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26.01, + 'breakdown' => [ + 'item_total' => 20.01, + 'tax_total' => 6, + 'shipping' => null, + 'discount' => null, + 'shipping_discount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'with_rounding_down_only_first_item' => [ + 'message' => 'Extra line should be added with price 0.01 and lines amount 10.00 and 5.00.', + 'expected' => [ + 'item_value' => [10.00, 5.00], + 'extra_line_value' => 0.01 + ], + 'items' => [ + [ + 'value' => 10.005, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + [ + 'value' => 5, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 36.01, + 'breakdown' => [ + 'item_total' => 30.01, + 'tax_total' => 6, + 'shipping' => null, + 'discount' => null, + 'shipping_discount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'with_multiple_roundings_down' => [ + 'message' => 'Extra line should be added with price 0.01 and lines amount 10.00, 5.00 and 6.66.', + 'expected' => [ + 'item_value' => [10.00, 5.00, 6.66], + 'extra_line_value' => 0.01 + ], + 'items' => [ + [ + 'value' => 10.005, + 'quantity' => 1, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + [ + 'value' => 5.001, + 'quantity' => 1, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + [ + 'value' => 6.666, + 'quantity' => 1, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 27.67, + 'breakdown' => [ + 'item_total' => 21.67, + 'tax_total' => 6, + 'shipping' => null, + 'discount' => null, + 'shipping_discount' => null, + 'handling' => null, + 'insurance' => null, + ], + ] + ]; + + $values = []; + foreach ($data as $testKey => $test) { + $items = []; + foreach ($test['items'] as $key => $item) { + $unitAmount = new Money($item['value'], 'EUR'); + $tax = new Money($item['tax'], 'EUR'); + $items[$key] = Mockery::mock( + Item::class, + [ + 'unit_amount' => $unitAmount, + 'tax' => $tax, + 'quantity'=> $item['quantity'], + 'category' => $item['category'], + ] + ); + + $items[$key]->shouldReceive('to_array')->andReturnUsing(function (bool $roundToFloor = false) use ($unitAmount, $tax, $item) { + return [ + 'unit_amount' => $unitAmount->to_array($roundToFloor), + 'tax' => $tax->to_array(), + 'quantity'=> $item['quantity'], + 'category' => $item['category'], + ]; + }); + + } + + $breakdown = null; + if ($test['breakdown']) { + $breakdown = Mockery::mock(AmountBreakdown::class); + foreach ($test['breakdown'] as $method => $value) { + $breakdown->shouldReceive($method)->andReturnUsing(function () use ($value) { + if (! is_numeric($value)) { + return null; + } + + $money = new Money($value, 'EUR'); + return $money; + }); + } + + $breakdown + ->shouldReceive('to_array') + ->andReturn( + array_map( + function ($value) { + return $value ? (new Money($value, 'EUR'))->to_array() : null; + }, + $test['breakdown'] + ) + ); + } + + $amountMoney = new Money($test['amount'], 'EUR'); + $amount = Mockery::mock(Amount::class); + $amount + ->shouldReceive('to_array') + ->andReturn([ + 'value' => $amountMoney->value_str(), + 'currency_code' => $amountMoney->currency_code(), + 'breakdown' => $breakdown ? $breakdown->to_array() : [], + ]); + $amount->shouldReceive('value_str')->andReturn($amountMoney->value_str()); + $amount->shouldReceive('currency_code')->andReturn('EUR'); + $amount->shouldReceive('breakdown')->andReturn($breakdown); + + $values[$testKey] = [ + $items, + $amount, + $test['expected'], + $test['message'], + ]; + } + + return $values; + } + public function testPayee() { $amount = Mockery::mock(Amount::class); From 1a0fa269ef99b0b495dd52c017a2ff19f95500f8 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Tue, 8 Aug 2023 15:20:40 +0100 Subject: [PATCH 23/60] Add passing data between PayPal order and WC order via transient helper. Add order notes when ditch oor extra line occurs. --- modules/ppcp-api-client/services.php | 6 + modules/ppcp-api-client/src/ApiModule.php | 27 +++ .../src/Endpoint/OrderEndpoint.php | 3 + .../Endpoint/PayUponInvoiceOrderEndpoint.php | 5 +- .../src/Helper/OrderTransient.php | 160 ++++++++++++++++++ .../src/Helper/PurchaseUnitSanitizer.php | 21 +++ modules/ppcp-button/services.php | 2 +- .../src/Endpoint/CreateOrderEndpoint.php | 3 + .../src/Helper/EarlyOrderHandler.php | 7 +- .../src/Processor/OrderMetaTrait.php | 13 +- psalm-baseline.xml | 5 + 11 files changed, 245 insertions(+), 7 deletions(-) create mode 100644 modules/ppcp-api-client/src/Helper/OrderTransient.php diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 92d251507..3d7ad7e03 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; +use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; @@ -817,6 +818,11 @@ return array( 'api.order-helper' => static function( ContainerInterface $container ): OrderHelper { return new OrderHelper(); }, + 'api.helper.order-transient' => static function( ContainerInterface $container ): OrderTransient { + $cache = new Cache( 'ppcp-paypal-bearer' ); + $purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' ); + return new OrderTransient( $cache, $purchase_unit_sanitizer ); + }, 'api.helper.purchase-unit-sanitizer' => static function( ContainerInterface $container ): PurchaseUnitSanitizer { $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); diff --git a/modules/ppcp-api-client/src/ApiModule.php b/modules/ppcp-api-client/src/ApiModule.php index 95a0efd14..7a07be123 100644 --- a/modules/ppcp-api-client/src/ApiModule.php +++ b/modules/ppcp-api-client/src/ApiModule.php @@ -9,10 +9,13 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient; +use WC_Order; +use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; /** * Class ApiModule @@ -40,6 +43,30 @@ class ApiModule implements ModuleInterface { WC()->session->set( 'ppcp_fees', $fees ); } ); + add_action( + 'woocommerce_paypal_payments_paypal_order_created', + function ( Order $order ) use ( $c ) { + $transient = $c->has( 'api.helper.order-transient' ) ? $c->get( 'api.helper.order-transient' ) : null; + + if ( $transient instanceof OrderTransient ) { + $transient->on_order_created( $order ); + } + }, + 10, + 1 + ); + add_action( + 'woocommerce_paypal_payments_woocommerce_order_created', + function ( WC_Order $wc_order, Order $order ) use ( $c ) { + $transient = $c->has( 'api.helper.order-transient' ) ? $c->get( 'api.helper.order-transient' ) : null; + + if ( $transient instanceof OrderTransient ) { + $transient->on_woocommerce_order_created( $wc_order, $order ); + } + }, + 10, + 2 + ); } /** diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index 3b0954fae..3b232c7f6 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -281,6 +281,9 @@ class OrderEndpoint { throw $error; } $order = $this->order_factory->from_paypal_response( $json ); + + do_action( 'woocommerce_paypal_payments_paypal_order_created', $order ); + return $order; } diff --git a/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php index df3091311..f91d848a1 100644 --- a/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php @@ -166,8 +166,11 @@ class PayUponInvoiceOrderEndpoint { throw new PayPalApiException( $json, $status_code ); } + $order = $this->order_factory->from_paypal_response( $json ); - return $this->order_factory->from_paypal_response( $json ); + do_action( 'woocommerce_paypal_payments_paypal_order_created', $order ); + + return $order; } /** diff --git a/modules/ppcp-api-client/src/Helper/OrderTransient.php b/modules/ppcp-api-client/src/Helper/OrderTransient.php new file mode 100644 index 000000000..d0c7d4a01 --- /dev/null +++ b/modules/ppcp-api-client/src/Helper/OrderTransient.php @@ -0,0 +1,160 @@ +cache = $cache; + $this->purchase_unit_sanitizer = $purchase_unit_sanitizer; + } + + /** + * Processes the created PayPal order. + * + * @param Order $order The PayPal order. + * @return void + */ + public function on_order_created( Order $order ): void { + $message = $this->purchase_unit_sanitizer->get_last_message(); + $this->add_order_note( $order, $message ); + } + + /** + * Processes the created WooCommerce order. + * + * @param WC_Order $wc_order The WooCommerce order. + * @param Order $order The PayPal order. + * @return void + */ + public function on_woocommerce_order_created( WC_Order $wc_order, Order $order ): void { + $cache_key = $this->cache_key( $order ); + + if ( ! $cache_key ) { + return; + } + + $this->apply_order_notes( $order, $wc_order ); + $this->cache->delete( $cache_key ); + } + + /** + * Adds an order note associated with a PayPal order. + * It can be added to a WooCommerce order associated with this PayPal order in the future. + * + * @param Order $order The PayPal order. + * @param string $message The message to be added to order notes. + * @return void + */ + private function add_order_note( Order $order, string $message ): void { + if ( ! $message ) { + return; + } + + $cache_key = $this->cache_key( $order ); + + if ( ! $cache_key ) { + return; + } + + $transient = $this->cache->get( $cache_key ); + + if ( ! is_array( $transient ) ) { + $transient = array(); + } + + if ( ! is_array( $transient['notes'] ) ) { + $transient['notes'] = array(); + } + + $transient['notes'][] = $message; + + $this->cache->set( $cache_key, $transient, self::CACHE_TIMEOUT ); + } + + /** + * Adds an order note associated with a PayPal order. + * It can be added to a WooCommerce order associated with this PayPal order in the future. + * + * @param Order $order The PayPal order. + * @param WC_Order $wc_order The WooCommerce order. + * @return void + */ + private function apply_order_notes( Order $order, WC_Order $wc_order ): void { + $cache_key = $this->cache_key( $order ); + + if ( ! $cache_key ) { + return; + } + + $transient = $this->cache->get( $cache_key ); + + if ( ! is_array( $transient ) ) { + return; + } + + if ( ! is_array( $transient['notes'] ) ) { + return; + } + + foreach ( $transient['notes'] as $note ) { + if ( ! is_string( $note ) ) { + continue; + } + $wc_order->add_order_note( $note ); + } + } + + /** + * Build cache key. + * + * @param Order $order The PayPal order. + * @return string|null + */ + private function cache_key( Order $order ): ?string { + if ( ! $order->id() ) { + return null; + } + return implode( '_', array( self::CACHE_KEY . $order->id() ) ); + } + +} diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php index 263b428e4..403ec5a68 100644 --- a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -68,6 +68,12 @@ class PurchaseUnitSanitizer { */ private $extra_line_name; + /** + * The last message. To be added to order notes. + * + * @var string + */ + private $last_message = ''; /** * PurchaseUnitSanitizer constructor. @@ -201,6 +207,8 @@ class PurchaseUnitSanitizer { $line_name = $this->extra_line_name; $roundings_money = new Money( $item_mismatch, $this->currency_code() ); $this->purchase_unit['items'][] = ( new Item( $line_name, $roundings_money, 1 ) )->to_array(); + + $this->last_message = __( 'Item amount mismatch. Extra line added.', 'woocommerce-paypal-payments' ); } $item_mismatch = $this->calculate_item_mismatch(); @@ -210,6 +218,7 @@ class PurchaseUnitSanitizer { // Ditch items. if ( $this->allow_ditch_items && isset( $this->purchase_unit['items'] ) ) { unset( $this->purchase_unit['items'] ); + $this->last_message = __( 'Item amount mismatch. Items ditched.', 'woocommerce-paypal-payments' ); } } } @@ -251,6 +260,8 @@ class PurchaseUnitSanitizer { if ( isset( $this->purchase_unit['amount']['breakdown'] ) ) { unset( $this->purchase_unit['amount']['breakdown'] ); } + + $this->last_message = __( 'Breakdown mismatch. Items and breakdown ditched.', 'woocommerce-paypal-payments' ); } } @@ -334,4 +345,14 @@ class PurchaseUnitSanitizer { return $amount_str - $amount_total_str; } + + /** + * Returns the last sanitization message. + * + * @return string + */ + public function get_last_message(): string { + return $this->last_message; + } + } diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 88ee20dd0..dca33196f 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -156,7 +156,7 @@ return array( $session_handler = $container->get( 'session.handler' ); $settings = $container->get( 'wcgateway.settings' ); $early_order_handler = $container->get( 'button.helper.early-order-handler' ); - $registration_needed = $container->get( 'button.current-user-must-register' ); + $registration_needed = $container->get( 'button.current-user-must-register' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new CreateOrderEndpoint( $request_data, diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index d4ddfb21f..4ee88c3c7 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -25,6 +25,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; +use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; use WooCommerce\PayPalCommerce\Button\Exception\ValidationException; use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator; use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; @@ -330,6 +331,8 @@ class CreateOrderEndpoint implements EndpointInterface { $wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() ); $wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() ); $wc_order->save_meta_data(); + + do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order ); } wp_send_json_success( $this->make_response( $order ) ); diff --git a/modules/ppcp-button/src/Helper/EarlyOrderHandler.php b/modules/ppcp-button/src/Helper/EarlyOrderHandler.php index 4ac8583d0..03d8fbeed 100644 --- a/modules/ppcp-button/src/Helper/EarlyOrderHandler.php +++ b/modules/ppcp-button/src/Helper/EarlyOrderHandler.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Button\Helper; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -163,6 +164,10 @@ class EarlyOrderHandler { /** * Patch Order so we have the \WC_Order id added. */ - return $this->order_processor->patch_order( $wc_order, $order ); + $order = $this->order_processor->patch_order( $wc_order, $order ); + + do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order ); + + return $order; } } diff --git a/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php b/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php index 815f8d73e..47348eca3 100644 --- a/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Processor; use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; +use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -22,14 +23,16 @@ trait OrderMetaTrait { /** * Adds common metadata to the order. * - * @param WC_Order $wc_order The WC order to which metadata will be added. - * @param Order $order The PayPal order. - * @param Environment $environment The environment. + * @param WC_Order $wc_order The WC order to which metadata will be added. + * @param Order $order The PayPal order. + * @param Environment $environment The environment. + * @param OrderTransient|null $order_transient The order transient helper. */ protected function add_paypal_meta( WC_Order $wc_order, Order $order, - Environment $environment + Environment $environment, + OrderTransient $order_transient = null ): void { $wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() ); $wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() ); @@ -43,6 +46,8 @@ trait OrderMetaTrait { } $wc_order->save(); + + do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order ); } /** diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 42ce967ca..c866d0bed 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -259,6 +259,11 @@ DAY_IN_SECONDS + + + DAY_IN_SECONDS + + realpath( __FILE__ ) From dea55e24c45c4440570559c2537b24e2a605dfd0 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Tue, 8 Aug 2023 17:01:03 +0100 Subject: [PATCH 24/60] Fix tests --- tests/e2e/PHPUnit/Order/PurchaseUnitTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/e2e/PHPUnit/Order/PurchaseUnitTest.php b/tests/e2e/PHPUnit/Order/PurchaseUnitTest.php index 23edee911..6a72c4255 100644 --- a/tests/e2e/PHPUnit/Order/PurchaseUnitTest.php +++ b/tests/e2e/PHPUnit/Order/PurchaseUnitTest.php @@ -364,6 +364,11 @@ class PurchaseUnitTest extends TestCase ], self::adaptAmountFormat([ 'value' => 10.69, + 'breakdown' => [ + 'item_total' => 10.69, + 'tax_total' => 0, + 'shipping' => 0, + ], ]), ]; } @@ -432,6 +437,11 @@ class PurchaseUnitTest extends TestCase ], self::adaptAmountFormat([ 'value' => 10.69, + 'breakdown' => [ + 'item_total' => 10.69, + 'tax_total' => 0, + 'shipping' => 0, + ], ], get_woocommerce_currency()), ]; } From bb3b8553b9766adafdc05c790e0b05b3cd96ef79 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 9 Aug 2023 11:48:49 +0100 Subject: [PATCH 25/60] Refactor PurchaseUnitSanitizer to subtract 0.01 instead of rounding down Refactor PurchaseUnitSanitizer to use singleton pattern --- lib/packages/Pattern/SingletonTrait.php | 33 +++++++++++++++++++ modules/ppcp-api-client/services.php | 5 ++- modules/ppcp-api-client/src/Entity/Item.php | 5 ++- modules/ppcp-api-client/src/Entity/Money.php | 19 +++-------- .../src/Entity/PurchaseUnit.php | 2 +- .../src/Helper/MoneyFormatter.php | 21 +----------- .../src/Helper/PurchaseUnitSanitizer.php | 32 ++++++++++-------- .../ApiClient/Entity/PurchaseUnitTest.php | 14 ++++---- 8 files changed, 70 insertions(+), 61 deletions(-) create mode 100644 lib/packages/Pattern/SingletonTrait.php diff --git a/lib/packages/Pattern/SingletonTrait.php b/lib/packages/Pattern/SingletonTrait.php new file mode 100644 index 000000000..73669e076 --- /dev/null +++ b/lib/packages/Pattern/SingletonTrait.php @@ -0,0 +1,33 @@ + static function( ContainerInterface $container ): PurchaseUnitSanitizer { + if ( $instance = PurchaseUnitSanitizer::get_instance() ) { + return $instance; + } $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); $behavior = $settings->has( 'subtotal_mismatch_behavior' ) ? $settings->get( 'subtotal_mismatch_behavior' ) : null; $line_name = $settings->has( 'subtotal_mismatch_line_name' ) ? $settings->get( 'subtotal_mismatch_line_name' ) : null; - return new PurchaseUnitSanitizer( $behavior, $line_name ); + return PurchaseUnitSanitizer::singleton( $behavior, $line_name ); }, ); diff --git a/modules/ppcp-api-client/src/Entity/Item.php b/modules/ppcp-api-client/src/Entity/Item.php index 13f4702ba..44356b580 100644 --- a/modules/ppcp-api-client/src/Entity/Item.php +++ b/modules/ppcp-api-client/src/Entity/Item.php @@ -201,13 +201,12 @@ class Item { /** * Returns the object as array. * - * @param bool $round_to_floor If value rounding should be floor. * @return array */ - public function to_array( bool $round_to_floor = false ): array { + public function to_array(): array { $item = array( 'name' => $this->name(), - 'unit_amount' => $this->unit_amount()->to_array( $round_to_floor ), + 'unit_amount' => $this->unit_amount()->to_array(), 'quantity' => $this->quantity(), 'description' => $this->description(), 'sku' => $this->sku(), diff --git a/modules/ppcp-api-client/src/Entity/Money.php b/modules/ppcp-api-client/src/Entity/Money.php index e8f638feb..5ba49dc43 100644 --- a/modules/ppcp-api-client/src/Entity/Money.php +++ b/modules/ppcp-api-client/src/Entity/Money.php @@ -62,11 +62,10 @@ class Money { /** * The value formatted as string for API requests. * - * @param bool $round_to_floor If value rounding should be floor. * @return string */ - public function value_str( bool $round_to_floor = false ): string { - return $this->money_formatter->format( $this->value, $this->currency_code, $round_to_floor ); + public function value_str(): string { + return $this->money_formatter->format( $this->value, $this->currency_code ); } /** @@ -81,22 +80,12 @@ class Money { /** * Returns the object as array. * - * @param bool $round_to_floor If value rounding should be floor. * @return array */ - public function to_array( bool $round_to_floor = false ): array { + public function to_array(): array { return array( 'currency_code' => $this->currency_code(), - 'value' => $this->value_str( $round_to_floor ), + 'value' => $this->value_str(), ); } - - /** - * Indicates if the default rounding for this value is rounding up. - * - * @return bool - */ - public function is_rounding_up(): bool { - return (float) $this->value_str() > $this->value; - } } diff --git a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php index 26928cf4c..61a5bbb6a 100644 --- a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php +++ b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php @@ -336,7 +336,7 @@ class PurchaseUnit { } if ( $sanitize_output && isset( $this->sanitizer ) ) { - $purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $this->items(), $allow_ditch_items ) ); + $purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $allow_ditch_items ) ); } return $purchase_unit; diff --git a/modules/ppcp-api-client/src/Helper/MoneyFormatter.php b/modules/ppcp-api-client/src/Helper/MoneyFormatter.php index 3e19a10f5..447ba0a0e 100644 --- a/modules/ppcp-api-client/src/Helper/MoneyFormatter.php +++ b/modules/ppcp-api-client/src/Helper/MoneyFormatter.php @@ -25,31 +25,12 @@ class MoneyFormatter { * * @param float $value The value. * @param string $currency The 3-letter currency code. - * @param bool $round_to_floor If value rounding should be floor. * * @return string */ - public function format( float $value, string $currency, bool $round_to_floor = false ): string { - if ( $round_to_floor ) { - return in_array( $currency, $this->currencies_without_decimals, true ) - ? (string) floor( $value ) - : number_format( $this->floor_with_decimals( $value, 2 ), 2, '.', '' ); - } - + public function format( float $value, string $currency ): string { return in_array( $currency, $this->currencies_without_decimals, true ) ? (string) round( $value, 0 ) : number_format( $value, 2, '.', '' ); } - - /** - * Rounds to floor with decimal precision. - * - * @param float $value The value. - * @param int $decimals The number of decimals. - * @return float - */ - private function floor_with_decimals( float $value, int $decimals = 0 ): float { - $adjustment = (float) pow( 10, $decimals ); - return floor( $value * $adjustment ) / $adjustment; - } } diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php index 403ec5a68..4da9281df 100644 --- a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -19,11 +19,14 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Helper; use WooCommerce\PayPalCommerce\ApiClient\Entity\Item; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; +use WooCommerce\PayPalCommerce\Vendor\Pattern\SingletonTrait; /** * Class PurchaseUnitSanitizer */ class PurchaseUnitSanitizer { + use SingletonTrait; + const MODE_DITCH = 'ditch'; const MODE_EXTRA_LINE = 'extra_line'; const VALID_MODES = array( @@ -40,13 +43,6 @@ class PurchaseUnitSanitizer { */ private $purchase_unit = array(); - /** - * The purchase unit Item objects - * - * @var array|Item[] - */ - private $item_objects = array(); - /** * Whether to allow items to be ditched. * @@ -95,6 +91,17 @@ class PurchaseUnitSanitizer { $this->extra_line_name = $extra_line_name; } + /** + * PurchaseUnitSanitizer singleton. + * + * @param string|null $mode The mismatch handling mode, ditch or extra_line. + * @param string|null $extra_line_name The name of the extra line. + * @return self + */ + public static function singleton( string $mode = null, string $extra_line_name = null ): self { + return self::set_instance( new self( $mode, $extra_line_name ) ); + } + /** * Indicates if mode is ditch. * @@ -166,13 +173,11 @@ class PurchaseUnitSanitizer { * The sanitizes the purchase_unit array. * * @param array $purchase_unit The purchase_unit array that should be sanitized. - * @param array|Item[] $item_objects The purchase unit Item objects used for recalculations. * @param bool $allow_ditch_items Whether to allow items to be ditched. * @return array */ - public function sanitize( array $purchase_unit, array $item_objects, bool $allow_ditch_items = true ): array { + public function sanitize( array $purchase_unit, bool $allow_ditch_items = true ): array { $this->purchase_unit = $purchase_unit; - $this->item_objects = $item_objects; $this->allow_ditch_items = $allow_ditch_items; $this->sanitize_item_amount_mismatch(); @@ -193,10 +198,9 @@ class PurchaseUnitSanitizer { if ( $item_mismatch < 0 ) { // Do floors on item amounts so item_mismatch is a positive value. - foreach ( $this->item_objects as $index => $item ) { - $this->purchase_unit['items'][ $index ] = $item->to_array( - $item->unit_amount()->is_rounding_up() - ); + foreach ( $this->purchase_unit['items'] as $index => $item ) { + // get a more intelligent adjustment mechanism + $this->purchase_unit['items'][ $index ]['unit_amount']['value'] = ( (float) $this->purchase_unit['items'][ $index ]['unit_amount']['value'] ) - 0.01; } } diff --git a/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php b/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php index 328f5cb15..c9d81f8aa 100644 --- a/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php +++ b/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php @@ -662,11 +662,11 @@ class PurchaseUnitTest extends TestCase 'insurance' => null, ], ], - 'with_rounding_down_only_first_item' => [ - 'message' => 'Extra line should be added with price 0.01 and lines amount 10.00 and 5.00.', + 'with_two_rounding_down' => [ + 'message' => 'Extra line should be added with price 0.03 and lines amount 10.00 and 4.99.', 'expected' => [ - 'item_value' => [10.00, 5.00], - 'extra_line_value' => 0.01 + 'item_value' => [10.00, 4.99], + 'extra_line_value' => 0.03 ], 'items' => [ [ @@ -693,11 +693,11 @@ class PurchaseUnitTest extends TestCase 'insurance' => null, ], ], - 'with_multiple_roundings_down' => [ + 'with_many_roundings_down' => [ 'message' => 'Extra line should be added with price 0.01 and lines amount 10.00, 5.00 and 6.66.', 'expected' => [ - 'item_value' => [10.00, 5.00, 6.66], - 'extra_line_value' => 0.01 + 'item_value' => [10.00, 4.99, 6.66], + 'extra_line_value' => 0.02 ], 'items' => [ [ From 2b497182d3e6c42639fa1aa2e329345597bf2aee Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 9 Aug 2023 12:03:09 +0100 Subject: [PATCH 26/60] Fix lint Fix currency precision --- modules/ppcp-api-client/services.php | 4 +++- .../ppcp-api-client/src/Helper/MoneyFormatter.php | 12 ++++++++++++ .../src/Helper/PurchaseUnitSanitizer.php | 9 +++++---- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 8e8dd790f..ae8d03961 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -824,9 +824,11 @@ return array( return new OrderTransient( $cache, $purchase_unit_sanitizer ); }, 'api.helper.purchase-unit-sanitizer' => static function( ContainerInterface $container ): PurchaseUnitSanitizer { - if ( $instance = PurchaseUnitSanitizer::get_instance() ) { + $instance = PurchaseUnitSanitizer::get_instance(); + if ( $instance ) { return $instance; } + $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); diff --git a/modules/ppcp-api-client/src/Helper/MoneyFormatter.php b/modules/ppcp-api-client/src/Helper/MoneyFormatter.php index 447ba0a0e..914adbe70 100644 --- a/modules/ppcp-api-client/src/Helper/MoneyFormatter.php +++ b/modules/ppcp-api-client/src/Helper/MoneyFormatter.php @@ -33,4 +33,16 @@ class MoneyFormatter { ? (string) round( $value, 0 ) : number_format( $value, 2, '.', '' ); } + + /** + * Returns the minimum amount a currency can be incremented or decremented. + * + * @param string $currency The 3-letter currency code. + * @return float + */ + public function minimum_increment( string $currency ): float { + return (float) in_array( $currency, $this->currencies_without_decimals, true ) + ? 1.00 + : 0.01; + } } diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php index 4da9281df..6da6ca478 100644 --- a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -172,8 +172,8 @@ class PurchaseUnitSanitizer { /** * The sanitizes the purchase_unit array. * - * @param array $purchase_unit The purchase_unit array that should be sanitized. - * @param bool $allow_ditch_items Whether to allow items to be ditched. + * @param array $purchase_unit The purchase_unit array that should be sanitized. + * @param bool $allow_ditch_items Whether to allow items to be ditched. * @return array */ public function sanitize( array $purchase_unit, bool $allow_ditch_items = true ): array { @@ -199,8 +199,9 @@ class PurchaseUnitSanitizer { // Do floors on item amounts so item_mismatch is a positive value. foreach ( $this->purchase_unit['items'] as $index => $item ) { - // get a more intelligent adjustment mechanism - $this->purchase_unit['items'][ $index ]['unit_amount']['value'] = ( (float) $this->purchase_unit['items'][ $index ]['unit_amount']['value'] ) - 0.01; + // Get a more intelligent adjustment mechanism. + $increment = ( new MoneyFormatter() )->minimum_increment( $item['unit_amount']['currency_code'] ); + $this->purchase_unit['items'][ $index ]['unit_amount']['value'] = ( (float) $item['unit_amount']['value'] ) - $increment; } } From 7baee261946f1eb4492d4a92cbf90aa2819168dc Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 9 Aug 2023 17:50:03 +0100 Subject: [PATCH 27/60] Add support for custom single product page fields --- .../SingleProductActionHandler.js | 16 +++++++++++---- .../js/modules/Entity/BookingProduct.js | 6 ++++-- .../resources/js/modules/Entity/Product.js | 11 +++++----- .../resources/js/modules/Helper/FormHelper.js | 20 ++++++++++++++++++- .../src/Endpoint/AbstractCartEndpoint.php | 9 +++++++++ 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js index abc07c594..d3523ac1d 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js @@ -73,7 +73,7 @@ class SingleProductActionHandler { getSubscriptionProducts() { const id = document.querySelector('[name="add-to-cart"]').value; - return [new Product(id, 1, null)]; + return [new Product(id, 1, null, this.extraFields())]; } configuration() @@ -107,7 +107,7 @@ class SingleProductActionHandler { { if ( this.isBookingProduct() ) { const id = document.querySelector('[name="add-to-cart"]').value; - return [new BookingProduct(id, 1, FormHelper.getPrefixedFields(this.formElement, "wc_bookings_field"))]; + return [new BookingProduct(id, 1, FormHelper.getPrefixedFields(this.formElement, "wc_bookings_field"), this.extraFields())]; } else if ( this.isGroupedProduct() ) { const products = []; this.formElement.querySelectorAll('input[type="number"]').forEach((element) => { @@ -120,17 +120,25 @@ class SingleProductActionHandler { } const id = parseInt(elementName[1]); const quantity = parseInt(element.value); - products.push(new Product(id, quantity, null)); + products.push(new Product(id, quantity, null, this.extraFields())); }) return products; } else { const id = document.querySelector('[name="add-to-cart"]').value; const qty = document.querySelector('[name="quantity"]').value; const variations = this.variations(); - return [new Product(id, qty, variations)]; + return [new Product(id, qty, variations, this.extraFields())]; } } + extraFields() { + return FormHelper.getFilteredFields( + this.formElement, + ['add-to-cart', 'quantity', 'product_id', 'variation_id'], + ['attribute_', 'wc_bookings_field'] + ); + } + createOrder() { this.cartHelper = null; diff --git a/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js b/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js index 71552f8e2..36e16a4cd 100644 --- a/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js +++ b/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js @@ -2,15 +2,17 @@ import Product from "./Product"; class BookingProduct extends Product { - constructor(id, quantity, booking) { + constructor(id, quantity, booking, extra) { super(id, quantity, null); this.booking = booking; + this.extra = extra; } data() { return { ...super.data(), - booking: this.booking + booking: this.booking, + extra: this.extra } } } diff --git a/modules/ppcp-button/resources/js/modules/Entity/Product.js b/modules/ppcp-button/resources/js/modules/Entity/Product.js index 9f5648490..b99f74b61 100644 --- a/modules/ppcp-button/resources/js/modules/Entity/Product.js +++ b/modules/ppcp-button/resources/js/modules/Entity/Product.js @@ -1,18 +1,19 @@ class Product { - constructor(id, quantity, variations) { + constructor(id, quantity, variations, extra) { this.id = id; this.quantity = quantity; this.variations = variations; + this.extra = extra; } - data() { return { id:this.id, - quantity:this.quantity, - variations:this.variations + quantity: this.quantity, + variations: this.variations, + extra: this.extra, } } } -export default Product; \ No newline at end of file +export default Product; diff --git a/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js b/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js index ff796e3eb..1599c9fe3 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js +++ b/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js @@ -7,11 +7,29 @@ export default class FormHelper { static getPrefixedFields(formElement, prefix) { let fields = {}; for(const element of formElement.elements) { - if( element.name.startsWith(prefix) ) { + if( ( ! prefix ) || element.name.startsWith(prefix) ) { fields[element.name] = element.value; } } return fields; } + static getFilteredFields(formElement, exactFilters, prefixFilters) { + let fields = {}; + + for(const element of formElement.elements) { + if (!element.name) { + continue; + } + if (exactFilters && (exactFilters.indexOf(element.name) !== -1)) { + continue; + } + if (prefixFilters && prefixFilters.some(prefixFilter => element.name.startsWith(prefixFilter))) { + continue; + } + fields[element.name] = element.value; + } + return fields; + } + } diff --git a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php index 5f61d33b9..cfa0fb745 100644 --- a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php @@ -112,6 +112,14 @@ abstract class AbstractCartEndpoint implements EndpointInterface { $success = true; foreach ( $products as $product ) { + + // Add extras to POST, they are usually added by custom plugins. + if ( $product['extra'] && is_array( $product['extra'] ) ) { + foreach ( $product['extra'] as $key => $value ) { + $_POST[ $key ] = $value; + } + } + if ( $product['product']->is_type( 'booking' ) ) { $success = $success && $this->add_booking_product( $product['product'], @@ -229,6 +237,7 @@ abstract class AbstractCartEndpoint implements EndpointInterface { 'quantity' => (int) $product['quantity'], 'variations' => $product['variations'] ?? null, 'booking' => $product['booking'] ?? null, + 'extra' => $product['extra'] ?? null, ); } return $products; From cb36c31a78d86ac1cc0643c6e8e7ccb85b8f788a Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Thu, 10 Aug 2023 08:22:36 +0100 Subject: [PATCH 28/60] Refactor BookingProduct --- .../resources/js/modules/Entity/BookingProduct.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js b/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js index 36e16a4cd..4245bcb6c 100644 --- a/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js +++ b/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js @@ -3,16 +3,14 @@ import Product from "./Product"; class BookingProduct extends Product { constructor(id, quantity, booking, extra) { - super(id, quantity, null); + super(id, quantity, null, extra); this.booking = booking; - this.extra = extra; } data() { return { ...super.data(), - booking: this.booking, - extra: this.extra + booking: this.booking } } } From ecb884c86caa2b718406c681252f6a35ca94ffe1 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Thu, 10 Aug 2023 09:46:15 +0100 Subject: [PATCH 29/60] Add SingletonDecorator Refactor PurchaseUnitSanitizer service registration to SingletonDecorator --- lib/packages/Pattern/SingletonDecorator.php | 61 +++++++++++++++++++ modules/ppcp-api-client/services.php | 12 ++-- .../src/Helper/PurchaseUnitSanitizer.php | 14 ----- 3 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 lib/packages/Pattern/SingletonDecorator.php diff --git a/lib/packages/Pattern/SingletonDecorator.php b/lib/packages/Pattern/SingletonDecorator.php new file mode 100644 index 000000000..3fdc9a2c9 --- /dev/null +++ b/lib/packages/Pattern/SingletonDecorator.php @@ -0,0 +1,61 @@ +callable = $callable; + } + + /** + * The make constructor. + * + * @param callable $callable + * @return self + */ + public static function make( callable $callable ): self { + return new static( $callable ); + } + + /** + * Invokes a callable once and returns the same result on subsequent invokes. + * + * @param mixed ...$args Arguments to be passed to the callable. + * @return mixed + */ + public function __invoke( ...$args ) { + if ( ! $this->executed ) { + $this->result = call_user_func_array( $this->callable, $args ); + $this->executed = true; + } + + return $this->result; + } +} diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index ae8d03961..d079d5704 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -20,6 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\Vendor\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; @@ -823,17 +824,12 @@ return array( $purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' ); return new OrderTransient( $cache, $purchase_unit_sanitizer ); }, - 'api.helper.purchase-unit-sanitizer' => static function( ContainerInterface $container ): PurchaseUnitSanitizer { - $instance = PurchaseUnitSanitizer::get_instance(); - if ( $instance ) { - return $instance; - } - + 'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make(static function( ContainerInterface $container ): PurchaseUnitSanitizer { $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); $behavior = $settings->has( 'subtotal_mismatch_behavior' ) ? $settings->get( 'subtotal_mismatch_behavior' ) : null; $line_name = $settings->has( 'subtotal_mismatch_line_name' ) ? $settings->get( 'subtotal_mismatch_line_name' ) : null; - return PurchaseUnitSanitizer::singleton( $behavior, $line_name ); - }, + return new PurchaseUnitSanitizer( $behavior, $line_name ); + }), ); diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php index 6da6ca478..468787c02 100644 --- a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -19,14 +19,11 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Helper; use WooCommerce\PayPalCommerce\ApiClient\Entity\Item; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; -use WooCommerce\PayPalCommerce\Vendor\Pattern\SingletonTrait; /** * Class PurchaseUnitSanitizer */ class PurchaseUnitSanitizer { - use SingletonTrait; - const MODE_DITCH = 'ditch'; const MODE_EXTRA_LINE = 'extra_line'; const VALID_MODES = array( @@ -91,17 +88,6 @@ class PurchaseUnitSanitizer { $this->extra_line_name = $extra_line_name; } - /** - * PurchaseUnitSanitizer singleton. - * - * @param string|null $mode The mismatch handling mode, ditch or extra_line. - * @param string|null $extra_line_name The name of the extra line. - * @return self - */ - public static function singleton( string $mode = null, string $extra_line_name = null ): self { - return self::set_instance( new self( $mode, $extra_line_name ) ); - } - /** * Indicates if mode is ditch. * From 4338b9879cc73e1e9e419e6de8acce8f026aff2b Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Thu, 10 Aug 2023 10:01:28 +0100 Subject: [PATCH 30/60] Fix lint --- modules/ppcp-api-client/services.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index d079d5704..66bda386f 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -824,12 +824,14 @@ return array( $purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' ); return new OrderTransient( $cache, $purchase_unit_sanitizer ); }, - 'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make(static function( ContainerInterface $container ): PurchaseUnitSanitizer { - $settings = $container->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings ); + 'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make( + static function( ContainerInterface $container ): PurchaseUnitSanitizer { + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); - $behavior = $settings->has( 'subtotal_mismatch_behavior' ) ? $settings->get( 'subtotal_mismatch_behavior' ) : null; - $line_name = $settings->has( 'subtotal_mismatch_line_name' ) ? $settings->get( 'subtotal_mismatch_line_name' ) : null; - return new PurchaseUnitSanitizer( $behavior, $line_name ); - }), + $behavior = $settings->has( 'subtotal_mismatch_behavior' ) ? $settings->get( 'subtotal_mismatch_behavior' ) : null; + $line_name = $settings->has( 'subtotal_mismatch_line_name' ) ? $settings->get( 'subtotal_mismatch_line_name' ) : null; + return new PurchaseUnitSanitizer( $behavior, $line_name ); + } + ), ); From 1b87257fdb8fbc2c0bdda4014d464bb47e85c4c8 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 11 Aug 2023 10:35:50 +0100 Subject: [PATCH 31/60] Refactor unsupported currency notice to show only on payment pages and under the PayPal plugin. --- .../ppcp-admin-notices/src/Entity/Message.php | 20 +++++++- .../src/Renderer/Renderer.php | 3 +- .../ppcp-wc-gateway/resources/js/common.js | 10 ++++ .../resources/js/common/wrapped-elements.js | 14 ++++++ modules/ppcp-wc-gateway/services.php | 16 +++++-- .../src/Assets/SettingsPageAssets.php | 38 ++++++++++++--- .../Notice/UnsupportedCurrencyAdminNotice.php | 46 +++++++++++++++---- .../src/Settings/HeaderRenderer.php | 2 + .../ppcp-wc-gateway/src/WCGatewayModule.php | 3 +- modules/ppcp-wc-gateway/webpack.config.js | 1 + 10 files changed, 131 insertions(+), 22 deletions(-) create mode 100644 modules/ppcp-wc-gateway/resources/js/common.js create mode 100644 modules/ppcp-wc-gateway/resources/js/common/wrapped-elements.js diff --git a/modules/ppcp-admin-notices/src/Entity/Message.php b/modules/ppcp-admin-notices/src/Entity/Message.php index 7a0466ef6..0e8bc959e 100644 --- a/modules/ppcp-admin-notices/src/Entity/Message.php +++ b/modules/ppcp-admin-notices/src/Entity/Message.php @@ -35,17 +35,26 @@ class Message { */ private $dismissable; + /** + * The wrapper selector that will contain the notice. + * + * @var string + */ + private $wrapper; + /** * Message constructor. * * @param string $message The message text. * @param string $type The message type. * @param bool $dismissable Whether the message is dismissable. + * @param string $wrapper The wrapper selector that will contain the notice. */ - public function __construct( string $message, string $type, bool $dismissable = true ) { + public function __construct( string $message, string $type, bool $dismissable = true, string $wrapper = '' ) { $this->type = $type; $this->message = $message; $this->dismissable = $dismissable; + $this->wrapper = $wrapper; } /** @@ -74,4 +83,13 @@ class Message { public function is_dismissable(): bool { return $this->dismissable; } + + /** + * Returns the wrapper selector that will contain the notice. + * + * @return string + */ + public function wrapper(): string { + return $this->wrapper; + } } diff --git a/modules/ppcp-admin-notices/src/Renderer/Renderer.php b/modules/ppcp-admin-notices/src/Renderer/Renderer.php index b67da1c30..f44c7aabc 100644 --- a/modules/ppcp-admin-notices/src/Renderer/Renderer.php +++ b/modules/ppcp-admin-notices/src/Renderer/Renderer.php @@ -41,9 +41,10 @@ class Renderer implements RendererInterface { $messages = $this->repository->current_message(); foreach ( $messages as $message ) { printf( - '

%s

', + '

%s

', $message->type(), ( $message->is_dismissable() ) ? 'is-dismissible' : '', + $message->wrapper() ? sprintf('data-ppcp-wrapper="%s"', $message->wrapper()) : '', wp_kses_post( $message->message() ) ); } diff --git a/modules/ppcp-wc-gateway/resources/js/common.js b/modules/ppcp-wc-gateway/resources/js/common.js new file mode 100644 index 000000000..e017594a7 --- /dev/null +++ b/modules/ppcp-wc-gateway/resources/js/common.js @@ -0,0 +1,10 @@ +import moveWrappedElements from "./common/wrapped-elements"; +document.addEventListener( + 'DOMContentLoaded', + () => { + // Wait for current execution context to end. + setTimeout(function () { + moveWrappedElements(); + }, 0); + } +); diff --git a/modules/ppcp-wc-gateway/resources/js/common/wrapped-elements.js b/modules/ppcp-wc-gateway/resources/js/common/wrapped-elements.js new file mode 100644 index 000000000..827e50586 --- /dev/null +++ b/modules/ppcp-wc-gateway/resources/js/common/wrapped-elements.js @@ -0,0 +1,14 @@ + +// This function is needed because WordPress moves our custom notices to the global placeholder. +function moveWrappedElements() { + (($) => { + $('*[data-ppcp-wrapper]').each(function() { + let $wrapper = $('.' + $(this).data('ppcpWrapper')); + if ($wrapper.length) { + $wrapper.append(this); + } + }); + })(jQuery) +} + +export default moveWrappedElements; diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 2e9c6211d..7a63ff728 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -211,10 +211,18 @@ return array( return new ConnectAdminNotice( $state, $settings ); }, 'wcgateway.notice.currency-unsupported' => static function ( ContainerInterface $container ): UnsupportedCurrencyAdminNotice { - $state = $container->get( 'onboarding.state' ); - $shop_currency = $container->get( 'api.shop.currency' ); - $supported_currencies = $container->get( 'api.supported-currencies' ); - return new UnsupportedCurrencyAdminNotice( $state, $shop_currency, $supported_currencies ); + $state = $container->get( 'onboarding.state' ); + $shop_currency = $container->get( 'api.shop.currency' ); + $supported_currencies = $container->get( 'api.supported-currencies' ); + $is_wc_gateways_list_page = $container->get( 'wcgateway.is-wc-gateways-list-page' ); + $is_ppcp_settings_page = $container->get( 'wcgateway.is-ppcp-settings-page' ); + return new UnsupportedCurrencyAdminNotice( + $state, + $shop_currency, + $supported_currencies, + $is_wc_gateways_list_page, + $is_ppcp_settings_page + ); }, 'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice { return new GatewayWithoutPayPalAdminNotice( diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index aa069a7e7..27ab97ce3 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -87,6 +87,13 @@ class SettingsPageAssets { */ protected $all_funding_sources; + /** + * Whether it's a settings page of this plugin. + * + * @var bool + */ + private $is_settings_page; + /** * Assets constructor. * @@ -100,6 +107,7 @@ class SettingsPageAssets { * @param bool $is_pay_later_button_enabled Whether Pay Later button is enabled either for checkout, cart or product page. * @param array $disabled_sources The list of disabled funding sources. * @param array $all_funding_sources The list of all existing funding sources. + * @param bool $is_settings_page Whether it's a settings page of this plugin. */ public function __construct( string $module_url, @@ -111,7 +119,8 @@ class SettingsPageAssets { Environment $environment, bool $is_pay_later_button_enabled, array $disabled_sources, - array $all_funding_sources + array $all_funding_sources, + bool $is_settings_page ) { $this->module_url = $module_url; $this->version = $version; @@ -123,6 +132,7 @@ class SettingsPageAssets { $this->is_pay_later_button_enabled = $is_pay_later_button_enabled; $this->disabled_sources = $disabled_sources; $this->all_funding_sources = $all_funding_sources; + $this->is_settings_page = $is_settings_page; } /** @@ -136,11 +146,13 @@ class SettingsPageAssets { return; } - if ( ! $this->is_paypal_payment_method_page() ) { - return; + if ( $this->is_settings_page ) { + $this->register_admin_assets(); } - $this->register_admin_assets(); + if ( $this->is_paypal_payment_method_page() ) { + $this->register_paypal_admin_assets(); + } } ); @@ -171,9 +183,9 @@ class SettingsPageAssets { } /** - * Register assets for admin pages. + * Register assets for PayPal admin pages. */ - private function register_admin_assets(): void { + private function register_paypal_admin_assets(): void { wp_enqueue_style( 'ppcp-gateway-settings', trailingslashit( $this->module_url ) . 'assets/css/gateway-settings.css', @@ -210,4 +222,18 @@ class SettingsPageAssets { ) ); } + + /** + * Register assets for PayPal admin pages. + */ + private function register_admin_assets(): void { + wp_enqueue_script( + 'ppcp-admin-common', + trailingslashit( $this->module_url ) . 'assets/js/common.js', + array(), + $this->version, + true + ); + } + } diff --git a/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php index 27ef14f79..540a557b8 100644 --- a/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php +++ b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php @@ -40,6 +40,20 @@ class UnsupportedCurrencyAdminNotice { */ private $shop_currency; + /** + * Indicates if we're on the WooCommerce gateways list page. + * + * @var bool + */ + private $is_wc_gateways_list_page; + + /** + * Indicates if we're on a PPCP Settings page. + * + * @var bool + */ + private $is_ppcp_settings_page; + /** * UnsupportedCurrencyAdminNotice constructor. * @@ -47,10 +61,19 @@ class UnsupportedCurrencyAdminNotice { * @param string $shop_currency The shop currency. * @param array $supported_currencies The supported currencies. */ - public function __construct( State $state, string $shop_currency, array $supported_currencies ) { - $this->state = $state; - $this->shop_currency = $shop_currency; - $this->supported_currencies = $supported_currencies; + public function __construct( + State $state, + string $shop_currency, + array $supported_currencies, + bool $is_wc_gateways_list_page, + bool $is_ppcp_settings_page + ) { + $this->state = $state; + $this->shop_currency = $shop_currency; + $this->supported_currencies = $supported_currencies; + $this->is_wc_gateways_list_page = $is_wc_gateways_list_page; + $this->is_ppcp_settings_page = $is_ppcp_settings_page; + } /** @@ -63,16 +86,19 @@ class UnsupportedCurrencyAdminNotice { return null; } + $paypal_currency_support_url = 'https://developer.paypal.com/api/rest/reference/currency-codes/'; + $message = sprintf( - /* translators: %1$s the shop currency, 2$s the gateway name. */ + /* translators: %1$s the shop currency, %2$s the PayPal currency support page link opening HTML tag, %3$s the link ending HTML tag. */ __( - 'Attention: Your current WooCommerce store currency (%1$s) is not supported by PayPal. Please update your store currency to one that is supported by PayPal to ensure smooth transactions. Visit the PayPal currency support page for more information on supported currencies.', + 'Attention: Your current WooCommerce store currency (%1$s) is not supported by PayPal. Please update your store currency to one that is supported by PayPal to ensure smooth transactions. Visit the %2$sPayPal currency support page%3$s for more information on supported currencies.', 'woocommerce-paypal-payments' ), $this->shop_currency, - 'https://developer.paypal.com/api/rest/reference/currency-codes/' + '', + '' ); - return new Message( $message, 'warning' ); + return new Message( $message, 'warning', true, 'ppcp-notice-wrapper' ); } /** @@ -81,7 +107,9 @@ class UnsupportedCurrencyAdminNotice { * @return bool */ protected function should_display(): bool { - return $this->state->current_state() === State::STATE_ONBOARDED && ! $this->currency_supported(); + return $this->state->current_state() === State::STATE_ONBOARDED + && ! $this->currency_supported() + && ($this->is_wc_gateways_list_page || $this->is_ppcp_settings_page); } /** diff --git a/modules/ppcp-wc-gateway/src/Settings/HeaderRenderer.php b/modules/ppcp-wc-gateway/src/Settings/HeaderRenderer.php index a4302bc37..5f9b79280 100644 --- a/modules/ppcp-wc-gateway/src/Settings/HeaderRenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/HeaderRenderer.php @@ -77,6 +77,8 @@ class HeaderRenderer { ' + +
'; } } diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 28c610b7e..477ce303f 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -183,7 +183,8 @@ class WCGatewayModule implements ModuleInterface { $c->get( 'onboarding.environment' ), $settings_status->is_pay_later_button_enabled(), $settings->has( 'disable_funding' ) ? $settings->get( 'disable_funding' ) : array(), - $c->get( 'wcgateway.settings.funding-sources' ) + $c->get( 'wcgateway.settings.funding-sources' ), + $c->get( 'wcgateway.is-ppcp-settings-page' ) ); $assets->register_assets(); } diff --git a/modules/ppcp-wc-gateway/webpack.config.js b/modules/ppcp-wc-gateway/webpack.config.js index bd4a22fdc..a8a78a8bc 100644 --- a/modules/ppcp-wc-gateway/webpack.config.js +++ b/modules/ppcp-wc-gateway/webpack.config.js @@ -6,6 +6,7 @@ module.exports = { mode: isProduction ? 'production' : 'development', target: 'web', entry: { + 'common': path.resolve('./resources/js/common.js'), 'gateway-settings': path.resolve('./resources/js/gateway-settings.js'), 'fraudnet': path.resolve('./resources/js/fraudnet.js'), 'oxxo': path.resolve('./resources/js/oxxo.js'), From 0a873eb2a151a8c847da45fc4d3eb58c3641b616 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 11 Aug 2023 10:57:08 +0100 Subject: [PATCH 32/60] Fix lint --- modules/ppcp-admin-notices/src/Renderer/Renderer.php | 2 +- .../src/Notice/UnsupportedCurrencyAdminNotice.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-admin-notices/src/Renderer/Renderer.php b/modules/ppcp-admin-notices/src/Renderer/Renderer.php index f44c7aabc..1202d2ab4 100644 --- a/modules/ppcp-admin-notices/src/Renderer/Renderer.php +++ b/modules/ppcp-admin-notices/src/Renderer/Renderer.php @@ -44,7 +44,7 @@ class Renderer implements RendererInterface { '

%s

', $message->type(), ( $message->is_dismissable() ) ? 'is-dismissible' : '', - $message->wrapper() ? sprintf('data-ppcp-wrapper="%s"', $message->wrapper()) : '', + ( $message->wrapper() ? sprintf( 'data-ppcp-wrapper="%s"', esc_attr( $message->wrapper() ) ) : '' ), wp_kses_post( $message->message() ) ); } diff --git a/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php index 540a557b8..88f02cffe 100644 --- a/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php +++ b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php @@ -60,6 +60,8 @@ class UnsupportedCurrencyAdminNotice { * @param State $state The state. * @param string $shop_currency The shop currency. * @param array $supported_currencies The supported currencies. + * @param bool $is_wc_gateways_list_page Indicates if we're on the WooCommerce gateways list page. + * @param bool $is_ppcp_settings_page Indicates if we're on a PPCP Settings page. */ public function __construct( State $state, @@ -109,7 +111,7 @@ class UnsupportedCurrencyAdminNotice { protected function should_display(): bool { return $this->state->current_state() === State::STATE_ONBOARDED && ! $this->currency_supported() - && ($this->is_wc_gateways_list_page || $this->is_ppcp_settings_page); + && ( $this->is_wc_gateways_list_page || $this->is_ppcp_settings_page ); } /** From 1d20566592e7f1f941bcd054280d49b3f3e50435 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 11 Aug 2023 11:36:20 +0100 Subject: [PATCH 33/60] Fix tests --- tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php b/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php index 6bd2ca14f..95aa53e0c 100644 --- a/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php +++ b/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php @@ -27,8 +27,9 @@ class SettingsPagesAssetsTest extends TestCase Mockery::mock(Environment::class), true, array(), - array() - ); + array(), + true + ); when('is_admin') ->justReturn(true); From 53bf234a4c9e50c8023c64db93792bb77480e2ea Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 11 Aug 2023 16:09:11 +0100 Subject: [PATCH 34/60] Fix single product page extra fields in array format --- .../resources/js/modules/Helper/FormHelper.js | 33 ++++++++++++++----- .../src/Endpoint/AbstractCartEndpoint.php | 6 +++- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js b/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js index 1599c9fe3..7f504e1c4 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js +++ b/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js @@ -5,31 +5,46 @@ export default class FormHelper { static getPrefixedFields(formElement, prefix) { + const formData = new FormData(formElement); let fields = {}; - for(const element of formElement.elements) { - if( ( ! prefix ) || element.name.startsWith(prefix) ) { - fields[element.name] = element.value; + + for (const [name, value] of formData.entries()) { + if (!prefix || name.startsWith(prefix)) { + fields[name] = value; } } + return fields; } static getFilteredFields(formElement, exactFilters, prefixFilters) { + const formData = new FormData(formElement); let fields = {}; + let counters = {}; - for(const element of formElement.elements) { - if (!element.name) { + for (let [name, value] of formData.entries()) { + + // Handle array format + if (name.indexOf('[]') !== -1) { + const k = name; + counters[k] = counters[k] || 0; + name = name.replace('[]', `[${counters[k]}]`); + counters[k]++; + } + + if (!name) { continue; } - if (exactFilters && (exactFilters.indexOf(element.name) !== -1)) { + if (exactFilters && (exactFilters.indexOf(name) !== -1)) { continue; } - if (prefixFilters && prefixFilters.some(prefixFilter => element.name.startsWith(prefixFilter))) { + if (prefixFilters && prefixFilters.some(prefixFilter => name.startsWith(prefixFilter))) { continue; } - fields[element.name] = element.value; + + fields[name] = value; } + return fields; } - } diff --git a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php index cfa0fb745..91f163b03 100644 --- a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php @@ -115,7 +115,11 @@ abstract class AbstractCartEndpoint implements EndpointInterface { // Add extras to POST, they are usually added by custom plugins. if ( $product['extra'] && is_array( $product['extra'] ) ) { - foreach ( $product['extra'] as $key => $value ) { + // Handle cases like field[] + $query = http_build_query( $product['extra'] ); + parse_str( $query, $extra ); + + foreach ( $extra as $key => $value ) { $_POST[ $key ] = $value; } } From bd6af7e957a387a8f4bc4b3195eb6d531f53c973 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 11 Aug 2023 16:15:10 +0100 Subject: [PATCH 35/60] Fix lint --- modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php | 2 +- modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php index 91f163b03..d74d59c55 100644 --- a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php @@ -115,7 +115,7 @@ abstract class AbstractCartEndpoint implements EndpointInterface { // Add extras to POST, they are usually added by custom plugins. if ( $product['extra'] && is_array( $product['extra'] ) ) { - // Handle cases like field[] + // Handle cases like field[]. $query = http_build_query( $product['extra'] ); parse_str( $query, $extra ); diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index aa069a7e7..57e1af650 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -127,8 +127,10 @@ class SettingsPageAssets { /** * Register assets provided by this module. + * + * @return void */ - public function register_assets() { + public function register_assets(): void { add_action( 'admin_enqueue_scripts', function() { From ad4b90946032f7b1a7961ca21400814f6f41dd3c Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 14 Aug 2023 08:53:32 +0100 Subject: [PATCH 36/60] Fix comments --- modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php b/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php index 0f7930ee9..b1fcab376 100644 --- a/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php +++ b/modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php @@ -57,7 +57,7 @@ class RefundFeesUpdater { if ( ! $paypal_order_id ) { $this->logger->error( - sprintf( 'Update order paypal refund fees. No PayPal order_id. [wc_order: %s]', $wc_order->get_id() ) + sprintf( 'Failed to update order paypal refund fees. No PayPal order_id. [wc_order: %s]', $wc_order->get_id() ) ); return; } @@ -173,6 +173,8 @@ class RefundFeesUpdater { 'post_id' => $wc_order->get_id(), ); + // By default, WooCommerce excludes comments of the comment_type order_note. + // We need to remove this filter to get the order notes. remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) ); $comments = get_comments( $args ); From d48c2f60c6b597e8e0484236eceff659f296f388 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 14 Aug 2023 12:00:40 +0100 Subject: [PATCH 37/60] Fix code review changes Add ppcp_ditch_items_breakdown support --- composer.json | 1 + .../Pattern/SingletonDecorator.php | 2 +- .../Pattern/SingletonTrait.php | 2 +- modules/ppcp-api-client/services.php | 26 +++---- .../src/Entity/PurchaseUnit.php | 24 +++++++ .../src/Helper/PurchaseUnitSanitizer.php | 70 ++++++++++++------- .../Settings/Fields/connection-tab-fields.php | 2 +- 7 files changed, 86 insertions(+), 41 deletions(-) rename lib/{packages => isolated}/Pattern/SingletonDecorator.php (94%) rename lib/{packages => isolated}/Pattern/SingletonTrait.php (90%) diff --git a/composer.json b/composer.json index 4a94eb54b..4a3a9624f 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "autoload": { "psr-4": { "WooCommerce\\PayPalCommerce\\": "src", + "WooCommerce\\PayPalCommerce\\Isolated\\": "lib/isolated/", "WooCommerce\\PayPalCommerce\\Vendor\\": "lib/packages/" }, "files": [ diff --git a/lib/packages/Pattern/SingletonDecorator.php b/lib/isolated/Pattern/SingletonDecorator.php similarity index 94% rename from lib/packages/Pattern/SingletonDecorator.php rename to lib/isolated/Pattern/SingletonDecorator.php index 3fdc9a2c9..ea9c85a04 100644 --- a/lib/packages/Pattern/SingletonDecorator.php +++ b/lib/isolated/Pattern/SingletonDecorator.php @@ -1,6 +1,6 @@ sanitizer->sanitize( $purchase_unit, $allow_ditch_items ) ); } + return $this->apply_ditch_items_mismatch_filter( + $this->sanitizer->has_ditched_items_breakdown(), + $purchase_unit + ); + } + + public function apply_ditch_items_mismatch_filter( bool $ditched_items_breakdown, array $purchase_unit ): array + { + /** + * The filter can be used to control when the items and totals breakdown are removed from PayPal order info. + */ + $ditch = apply_filters( 'ppcp_ditch_items_breakdown', $ditched_items_breakdown, $this ); + + if ( $ditch ) { + unset( $purchase_unit['items'] ); + unset( $purchase_unit['amount']['breakdown'] ); + + if ( isset( $this->sanitizer ) ) { + $this->sanitizer->set_last_message( + __( 'Ditch items breakdown filter. Items and breakdown ditched.', 'woocommerce-paypal-payments' ) + ); + } + } + return $purchase_unit; } } diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php index 468787c02..16d50a149 100644 --- a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -68,6 +68,13 @@ class PurchaseUnitSanitizer { */ private $last_message = ''; + /** + * If the items and breakdown has been ditched. + * + * @var bool + */ + private $has_ditched_items_breakdown = false; + /** * PurchaseUnitSanitizer constructor. * @@ -88,24 +95,6 @@ class PurchaseUnitSanitizer { $this->extra_line_name = $extra_line_name; } - /** - * Indicates if mode is ditch. - * - * @return bool - */ - private function is_mode_ditch(): bool { - return $this->mode === self::MODE_DITCH; - } - - /** - * Indicates if mode is adding extra line. - * - * @return bool - */ - private function is_mode_extra_line(): bool { - return $this->mode === self::MODE_EXTRA_LINE; - } - /** * The purchase_unit amount. * @@ -163,8 +152,9 @@ class PurchaseUnitSanitizer { * @return array */ public function sanitize( array $purchase_unit, bool $allow_ditch_items = true ): array { - $this->purchase_unit = $purchase_unit; - $this->allow_ditch_items = $allow_ditch_items; + $this->purchase_unit = $purchase_unit; + $this->allow_ditch_items = $allow_ditch_items; + $this->has_ditched_items_breakdown = false; $this->sanitize_item_amount_mismatch(); $this->sanitize_item_tax_mismatch(); @@ -180,14 +170,18 @@ class PurchaseUnitSanitizer { private function sanitize_item_amount_mismatch(): void { $item_mismatch = $this->calculate_item_mismatch(); - if ( $this->is_mode_extra_line() ) { + if ( $this->mode === self::MODE_EXTRA_LINE ) { if ( $item_mismatch < 0 ) { // Do floors on item amounts so item_mismatch is a positive value. foreach ( $this->purchase_unit['items'] as $index => $item ) { // Get a more intelligent adjustment mechanism. $increment = ( new MoneyFormatter() )->minimum_increment( $item['unit_amount']['currency_code'] ); - $this->purchase_unit['items'][ $index ]['unit_amount']['value'] = ( (float) $item['unit_amount']['value'] ) - $increment; + + $this->purchase_unit['items'][ $index ]['unit_amount'] = ( new Money( + ( (float) $item['unit_amount']['value'] ) - $increment, + $item['unit_amount']['currency_code'] + ) )->to_array(); } } @@ -199,7 +193,9 @@ class PurchaseUnitSanitizer { $roundings_money = new Money( $item_mismatch, $this->currency_code() ); $this->purchase_unit['items'][] = ( new Item( $line_name, $roundings_money, 1 ) )->to_array(); - $this->last_message = __( 'Item amount mismatch. Extra line added.', 'woocommerce-paypal-payments' ); + $this->set_last_message( + __( 'Item amount mismatch. Extra line added.', 'woocommerce-paypal-payments' ) + ); } $item_mismatch = $this->calculate_item_mismatch(); @@ -209,7 +205,9 @@ class PurchaseUnitSanitizer { // Ditch items. if ( $this->allow_ditch_items && isset( $this->purchase_unit['items'] ) ) { unset( $this->purchase_unit['items'] ); - $this->last_message = __( 'Item amount mismatch. Items ditched.', 'woocommerce-paypal-payments' ); + $this->set_last_message( + __( 'Item amount mismatch. Items ditched.', 'woocommerce-paypal-payments' ) + ); } } } @@ -252,7 +250,10 @@ class PurchaseUnitSanitizer { unset( $this->purchase_unit['amount']['breakdown'] ); } - $this->last_message = __( 'Breakdown mismatch. Items and breakdown ditched.', 'woocommerce-paypal-payments' ); + $this->has_ditched_items_breakdown = true; + $this->set_last_message( + __( 'Breakdown mismatch. Items and breakdown ditched.', 'woocommerce-paypal-payments' ) + ); } } @@ -337,6 +338,16 @@ class PurchaseUnitSanitizer { return $amount_str - $amount_total_str; } + /** + * Indicates if the items and breakdown were ditched. + * + * @return bool + */ + public function has_ditched_items_breakdown(): bool + { + return $this->has_ditched_items_breakdown; + } + /** * Returns the last sanitization message. * @@ -346,4 +357,13 @@ class PurchaseUnitSanitizer { return $this->last_message; } + /** + * Set the last sanitization message. + * + * @param string $message + */ + public function set_last_message( string $message ): void { + $this->last_message = $message; + } + } diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 0b099ebae..64babe773 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -504,7 +504,7 @@ return function ( ContainerInterface $container, array $fields ): array { 'default' => 'vertical', 'desc_tip' => true, 'description' => __( - 'Differences between WooCommerce and PayPal roundings may give origin to a mismatch in order items subtotal calculations. If not handled these mismatches will cause the PayPal transaction to fail.', + 'Differences between WooCommerce and PayPal roundings may cause mismatch in order items subtotal calculations. If not handled, these mismatches will cause the PayPal transaction to fail.', 'woocommerce-paypal-payments' ), 'options' => array( From 1e8469788312504792040a46d81f39002a8e8ee7 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 14 Aug 2023 13:40:50 +0100 Subject: [PATCH 38/60] Fix ditched_items_breakdown filter log message condition. --- modules/ppcp-api-client/src/Entity/PurchaseUnit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php index 389ce6d87..1c881edb8 100644 --- a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php +++ b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php @@ -356,7 +356,7 @@ class PurchaseUnit { unset( $purchase_unit['items'] ); unset( $purchase_unit['amount']['breakdown'] ); - if ( isset( $this->sanitizer ) ) { + if ( isset( $this->sanitizer ) && ( $ditch !== $ditched_items_breakdown ) ) { $this->sanitizer->set_last_message( __( 'Ditch items breakdown filter. Items and breakdown ditched.', 'woocommerce-paypal-payments' ) ); From 576805a15ac5047456eb7c7cd6823faed20ca7ee Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 14 Aug 2023 14:01:13 +0100 Subject: [PATCH 39/60] Fix lint --- .../src/Entity/PurchaseUnit.php | 18 ++++++++++++++---- .../src/Helper/PurchaseUnitSanitizer.php | 5 ++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php index 1c881edb8..820df82af 100644 --- a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php +++ b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php @@ -335,18 +335,28 @@ class PurchaseUnit { $purchase_unit['soft_descriptor'] = $this->soft_descriptor(); } + $has_ditched_items_breakdown = false; + if ( $sanitize_output && isset( $this->sanitizer ) ) { - $purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $allow_ditch_items ) ); + $purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $allow_ditch_items ) ); + $has_ditched_items_breakdown = $this->sanitizer->has_ditched_items_breakdown(); } return $this->apply_ditch_items_mismatch_filter( - $this->sanitizer->has_ditched_items_breakdown(), + $has_ditched_items_breakdown, $purchase_unit ); } - public function apply_ditch_items_mismatch_filter( bool $ditched_items_breakdown, array $purchase_unit ): array - { + /** + * Applies the ppcp_ditch_items_breakdown filter. + * If true purchase_unit items and breakdown are ditched from PayPal. + * + * @param bool $ditched_items_breakdown If the breakdown and items were already ditched. + * @param array $purchase_unit The purchase_unit array. + * @return array + */ + public function apply_ditch_items_mismatch_filter( bool $ditched_items_breakdown, array $purchase_unit ): array { /** * The filter can be used to control when the items and totals breakdown are removed from PayPal order info. */ diff --git a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php index 16d50a149..7cb0c048f 100644 --- a/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php +++ b/modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php @@ -343,8 +343,7 @@ class PurchaseUnitSanitizer { * * @return bool */ - public function has_ditched_items_breakdown(): bool - { + public function has_ditched_items_breakdown(): bool { return $this->has_ditched_items_breakdown; } @@ -360,7 +359,7 @@ class PurchaseUnitSanitizer { /** * Set the last sanitization message. * - * @param string $message + * @param string $message The message. */ public function set_last_message( string $message ): void { $this->last_message = $message; From 17a06a61a323f39919246154416fb43d8566a348 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 14 Aug 2023 15:43:56 +0200 Subject: [PATCH 40/60] Do not process renew if order was created with PayPal Subscription API --- modules/ppcp-subscription/src/RenewalHandler.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-subscription/src/RenewalHandler.php b/modules/ppcp-subscription/src/RenewalHandler.php index cd2385499..a45eb638c 100644 --- a/modules/ppcp-subscription/src/RenewalHandler.php +++ b/modules/ppcp-subscription/src/RenewalHandler.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Subscription; +use WC_Subscription; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; @@ -139,8 +140,16 @@ class RenewalHandler { * * @param \WC_Order $wc_order The WooCommerce order. */ - public function renew( \WC_Order $wc_order ) { + public function renew( \WC_Order $wc_order ): void { try { + $subscription = wcs_get_subscription( $wc_order->get_id() ); + if ( is_a( $subscription, WC_Subscription::class ) ) { + $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; + if ( $subscription_id ) { + return; + } + } + $this->process_order( $wc_order ); } catch ( \Exception $exception ) { $error = $exception->getMessage(); From 55f5e62dd80726740f8c0e1ed18c09e1e2477261 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 14 Aug 2023 17:02:27 +0200 Subject: [PATCH 41/60] Create subscription renewal order on payment sale completed webhook handler --- .../ppcp-webhooks/src/Handler/PaymentSaleCompleted.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php index b606e2bd9..6cbd804dd 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php @@ -89,10 +89,12 @@ class PaymentSaleCompleted implements RequestHandler { ); $subscriptions = wcs_get_subscriptions( $args ); foreach ( $subscriptions as $subscription ) { - $parent_order = wc_get_order( $subscription->get_parent() ); $transaction_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); - if ( $transaction_id && is_string( $transaction_id ) && is_a( $parent_order, WC_Order::class ) ) { - $this->update_transaction_id( $transaction_id, $parent_order, $this->logger ); + if ( $transaction_id && is_string( $transaction_id ) ) { + $renewal_order = wcs_create_renewal_order( $subscription ); + if ( is_a( $renewal_order, WC_Order::class ) ) { + $this->update_transaction_id( $transaction_id, $renewal_order, $this->logger ); + } } } From 0eb616c94e5a90488bddd2a71a90af1392649fe2 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Tue, 15 Aug 2023 08:27:59 +0100 Subject: [PATCH 42/60] Refactor WooCommerce\PayPalCommerce\Internale\Pattern to WooCommerce\PayPalCommerce\Common\Pattern --- composer.json | 2 +- .../Pattern/SingletonDecorator.php | 9 ++++++++- lib/{isolated => common}/Pattern/SingletonTrait.php | 10 +++++++++- modules/ppcp-api-client/services.php | 2 +- 4 files changed, 19 insertions(+), 4 deletions(-) rename lib/{isolated => common}/Pattern/SingletonDecorator.php (79%) rename lib/{isolated => common}/Pattern/SingletonTrait.php (71%) diff --git a/composer.json b/composer.json index 4a3a9624f..bb2ccbf7c 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "autoload": { "psr-4": { "WooCommerce\\PayPalCommerce\\": "src", - "WooCommerce\\PayPalCommerce\\Isolated\\": "lib/isolated/", + "WooCommerce\\PayPalCommerce\\Common\\": "lib/common/", "WooCommerce\\PayPalCommerce\\Vendor\\": "lib/packages/" }, "files": [ diff --git a/lib/isolated/Pattern/SingletonDecorator.php b/lib/common/Pattern/SingletonDecorator.php similarity index 79% rename from lib/isolated/Pattern/SingletonDecorator.php rename to lib/common/Pattern/SingletonDecorator.php index ea9c85a04..54282dc91 100644 --- a/lib/isolated/Pattern/SingletonDecorator.php +++ b/lib/common/Pattern/SingletonDecorator.php @@ -1,7 +1,14 @@ Date: Tue, 15 Aug 2023 08:29:26 +0100 Subject: [PATCH 43/60] Removed satackey/action-docker-layer-caching from e2e testing --- .github/workflows/e2e.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3731a1de4..0bf80c13b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -12,9 +12,6 @@ jobs: name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} steps: - - uses: satackey/action-docker-layer-caching@v0.0.11 - continue-on-error: true - - uses: ddev/github-action-setup-ddev@v1 with: autostart: false From af6d51b3a11bb3d8adea52b4c4763f41d07195f5 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Tue, 15 Aug 2023 08:58:38 +0100 Subject: [PATCH 44/60] Removed satackey/action-docker-layer-caching from e2e testing --- .github/workflows/e2e.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3731a1de4..0bf80c13b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -12,9 +12,6 @@ jobs: name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} steps: - - uses: satackey/action-docker-layer-caching@v0.0.11 - continue-on-error: true - - uses: ddev/github-action-setup-ddev@v1 with: autostart: false From cff4e2a6f61060eacd5212a86c8fc96c273ca0d7 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Tue, 15 Aug 2023 09:21:07 +0100 Subject: [PATCH 45/60] Removed satackey/action-docker-layer-caching from e2e testing --- .github/workflows/e2e.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3731a1de4..0bf80c13b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -12,9 +12,6 @@ jobs: name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} steps: - - uses: satackey/action-docker-layer-caching@v0.0.11 - continue-on-error: true - - uses: ddev/github-action-setup-ddev@v1 with: autostart: false From 26c973318195ea6ade05e5038f4a1933d938bf2a Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 16 Aug 2023 11:24:34 +0200 Subject: [PATCH 46/60] Ensure WooCommerce Subscriptions plugin functions exist --- modules/ppcp-subscription/src/SubscriptionModule.php | 4 ++++ modules/ppcp-vaulting/src/PaymentTokenChecker.php | 4 ++-- modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php | 2 +- .../src/Handler/BillingSubscriptionCancelled.php | 2 +- modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php | 7 +++++-- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-subscription/src/SubscriptionModule.php b/modules/ppcp-subscription/src/SubscriptionModule.php index 2b30fae9b..e9e3d1123 100644 --- a/modules/ppcp-subscription/src/SubscriptionModule.php +++ b/modules/ppcp-subscription/src/SubscriptionModule.php @@ -233,6 +233,10 @@ class SubscriptionModule implements ModuleInterface { * @psalm-suppress MissingClosureParamType */ function( string $post_type, $post_or_order_object ) use ( $c ) { + if ( ! function_exists( 'wcs_get_subscription' ) ) { + return; + } + $order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object; diff --git a/modules/ppcp-vaulting/src/PaymentTokenChecker.php b/modules/ppcp-vaulting/src/PaymentTokenChecker.php index cd57e9426..3d7095fb6 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenChecker.php +++ b/modules/ppcp-vaulting/src/PaymentTokenChecker.php @@ -215,11 +215,11 @@ class PaymentTokenChecker { $wc_order->update_status( 'failed', $error_message ); /** - * Function already exist in Subscription plugin + * Function already exist in WC Subscriptions plugin. * * @psalm-suppress UndefinedFunction */ - $subscriptions = wcs_get_subscriptions_for_order( $wc_order->get_id() ); + $subscriptions = function_exists( 'wcs_get_subscriptions_for_order' ) ? wcs_get_subscriptions_for_order( $wc_order->get_id() ) : array(); foreach ( $subscriptions as $key => $subscription ) { if ( $subscription->get_parent_id() === $wc_order->get_id() ) { try { diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index c863d1663..2e71ad7a5 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -525,7 +525,7 @@ class PayPalGateway extends \WC_Payment_Gateway { $order = $this->session_handler->order(); $this->add_paypal_meta( $wc_order, $order, $this->environment ); - $subscriptions = wcs_get_subscriptions_for_order( $order_id ); + $subscriptions = function_exists( 'wcs_get_subscriptions_for_order' ) ? wcs_get_subscriptions_for_order( $order_id ) : array(); foreach ( $subscriptions as $subscription ) { $subscription->update_meta_data( 'ppcp_subscription', $paypal_subscription_id ); $subscription->save(); diff --git a/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php b/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php index 0fc79f0d3..6b9df1250 100644 --- a/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php +++ b/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php @@ -81,7 +81,7 @@ class BillingSubscriptionCancelled implements RequestHandler { ), ), ); - $subscriptions = wcs_get_subscriptions( $args ); + $subscriptions = function_exists( 'wcs_get_subscriptions' ) ? wcs_get_subscriptions( $args ) : array(); foreach ( $subscriptions as $subscription ) { $subscription->update_status( 'cancelled' ); } diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php index b606e2bd9..442dabd97 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php @@ -71,10 +71,13 @@ class PaymentSaleCompleted implements RequestHandler { return $this->failure_response(); } + if ( ! function_exists( 'wcs_get_subscriptions' ) ) { + return $this->failure_response( 'WooCommerce Subscriptions plugin is not active.' ); + } + $billing_agreement_id = wc_clean( wp_unslash( $request['resource']['billing_agreement_id'] ?? '' ) ); if ( ! $billing_agreement_id ) { - $message = 'Could not retrieve billing agreement id for subscription.'; - return $this->failure_response( $message ); + return $this->failure_response( 'Could not retrieve billing agreement id for subscription.' ); } $args = array( From 715bbeba04e7f57e46820b4249ca8f20f5e2e3b9 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 16 Aug 2023 11:36:42 +0200 Subject: [PATCH 47/60] Remove satackey/action-docker-layer-caching from e2e testing --- .github/workflows/e2e.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3731a1de4..0bf80c13b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -12,9 +12,6 @@ jobs: name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} steps: - - uses: satackey/action-docker-layer-caching@v0.0.11 - continue-on-error: true - - uses: ddev/github-action-setup-ddev@v1 with: autostart: false From ae0f56e30bc5ce8312d8ab5ac7766e09fa231c40 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 16 Aug 2023 15:48:45 +0200 Subject: [PATCH 48/60] Update renewal order status to completed --- modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php index 7442c7a55..0596ed154 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php @@ -94,9 +94,12 @@ class PaymentSaleCompleted implements RequestHandler { foreach ( $subscriptions as $subscription ) { $transaction_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); if ( $transaction_id && is_string( $transaction_id ) ) { + $this->logger->info( 'Creating renewal order from PAYMENT.SALE.COMPLETED webhook handler' ); $renewal_order = wcs_create_renewal_order( $subscription ); if ( is_a( $renewal_order, WC_Order::class ) ) { + $renewal_order->payment_complete(); $this->update_transaction_id( $transaction_id, $renewal_order, $this->logger ); + $this->logger->info( 'Updating status completed and transaction id from PAYMENT.SALE.COMPLETED webhook handler' ); } } } From 9c94d5a5363452148909d32a103c0bfceee1cd79 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 16 Aug 2023 15:39:57 +0100 Subject: [PATCH 49/60] Updated lib/README.md for new common folder. --- lib/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/README.md b/lib/README.md index 9114c43ef..d38e3edd4 100644 --- a/lib/README.md +++ b/lib/README.md @@ -1,5 +1,10 @@ +## packages The packages that are likely to cause conflicts with other plugins (by loading multiple incompatible versions). Their namespaces are isolated by [Mozart](https://github.com/coenjacobs/mozart). Currently, the packages are simply added in the repo to avoid making the build process more complex (Mozart has different PHP requirements). We need to isolate only PSR-11 containers and Dhii modularity packages, which are not supposed to change often. + +## common +This folder contains reusable classes or components that do not fit into any specific module. +They are designed to be versatile and can be used by any module within the plugin. From f33f603a2346a7594ca49f8cc0a243abbfc2f39d Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 16 Aug 2023 16:53:15 +0200 Subject: [PATCH 50/60] Revert to updating parent order transcation id on each renewal from PayPal --- .../src/Handler/PaymentSaleCompleted.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php index 0596ed154..442dabd97 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php @@ -92,15 +92,10 @@ class PaymentSaleCompleted implements RequestHandler { ); $subscriptions = wcs_get_subscriptions( $args ); foreach ( $subscriptions as $subscription ) { + $parent_order = wc_get_order( $subscription->get_parent() ); $transaction_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); - if ( $transaction_id && is_string( $transaction_id ) ) { - $this->logger->info( 'Creating renewal order from PAYMENT.SALE.COMPLETED webhook handler' ); - $renewal_order = wcs_create_renewal_order( $subscription ); - if ( is_a( $renewal_order, WC_Order::class ) ) { - $renewal_order->payment_complete(); - $this->update_transaction_id( $transaction_id, $renewal_order, $this->logger ); - $this->logger->info( 'Updating status completed and transaction id from PAYMENT.SALE.COMPLETED webhook handler' ); - } + if ( $transaction_id && is_string( $transaction_id ) && is_a( $parent_order, WC_Order::class ) ) { + $this->update_transaction_id( $transaction_id, $parent_order, $this->logger ); } } From 4d0a4676ecf44d6eec2631715298f45c49eecf6f Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Fri, 18 Aug 2023 12:47:05 +0200 Subject: [PATCH 51/60] Do not add payment token if subscription use PayPal Subscriptions API --- modules/ppcp-subscription/src/SubscriptionModule.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/ppcp-subscription/src/SubscriptionModule.php b/modules/ppcp-subscription/src/SubscriptionModule.php index e9e3d1123..d49480467 100644 --- a/modules/ppcp-subscription/src/SubscriptionModule.php +++ b/modules/ppcp-subscription/src/SubscriptionModule.php @@ -74,6 +74,11 @@ class SubscriptionModule implements ModuleInterface { add_action( 'woocommerce_subscription_payment_complete', function ( $subscription ) use ( $c ) { + $paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; + if ( $paypal_subscription_id ) { + return; + } + $payment_token_repository = $c->get( 'vaulting.repository.payment-token' ); $logger = $c->get( 'woocommerce.logger.woocommerce' ); From 1cddb3d1327a488b76d9c292aa8dfb897720c263 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Fri, 18 Aug 2023 16:17:37 +0200 Subject: [PATCH 52/60] Update transaction id on parent order or otherwsie create renewal order --- .../src/Handler/PaymentSaleCompleted.php | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php index 442dabd97..4c6b26782 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php @@ -92,10 +92,24 @@ class PaymentSaleCompleted implements RequestHandler { ); $subscriptions = wcs_get_subscriptions( $args ); foreach ( $subscriptions as $subscription ) { - $parent_order = wc_get_order( $subscription->get_parent() ); $transaction_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); - if ( $transaction_id && is_string( $transaction_id ) && is_a( $parent_order, WC_Order::class ) ) { - $this->update_transaction_id( $transaction_id, $parent_order, $this->logger ); + if ( $transaction_id && is_string( $transaction_id ) ) { + $is_renewal = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? ''; + if ( $is_renewal ) { + $renewal_order = wcs_create_renewal_order( $subscription ); + if ( is_a( $renewal_order, WC_Order::class ) ) { + $renewal_order->payment_complete(); + $this->update_transaction_id( $transaction_id, $renewal_order, $this->logger ); + break; + } + } + + $parent_order = wc_get_order( $subscription->get_parent() ); + if ( is_a( $parent_order, WC_Order::class ) ) { + $subscription->update_meta_data( '_ppcp_is_subscription_renewal', 'true' ); + $subscription->save_meta_data(); + $this->update_transaction_id( $transaction_id, $parent_order, $this->logger ); + } } } From 3d5a44ba7d41b3ae2e1aa7b9176fe6fd3e074edb Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Fri, 18 Aug 2023 16:36:32 +0200 Subject: [PATCH 53/60] Fix phpunit --- tests/PHPUnit/Subscription/RenewalHandlerTest.php | 3 +++ tests/PHPUnit/TestCase.php | 1 + 2 files changed, 4 insertions(+) diff --git a/tests/PHPUnit/Subscription/RenewalHandlerTest.php b/tests/PHPUnit/Subscription/RenewalHandlerTest.php index d079012c3..70484956b 100644 --- a/tests/PHPUnit/Subscription/RenewalHandlerTest.php +++ b/tests/PHPUnit/Subscription/RenewalHandlerTest.php @@ -115,6 +115,9 @@ class RenewalHandlerTest extends TestCase ->shouldReceive('payment_source') ->andReturn(null); + $wcOrder + ->shouldReceive('get_meta') + ->andReturn(''); $wcOrder ->shouldReceive('get_id') ->andReturn(1); diff --git a/tests/PHPUnit/TestCase.php b/tests/PHPUnit/TestCase.php index 2289dbc20..4e6c859a1 100644 --- a/tests/PHPUnit/TestCase.php +++ b/tests/PHPUnit/TestCase.php @@ -38,6 +38,7 @@ class TestCase extends \PHPUnit\Framework\TestCase when('wc_clean')->returnArg(); when('get_transient')->returnArg(); when('delete_transient')->returnArg(); + when('wcs_get_subscription')->returnArg(); setUp(); } From a866fd355b7d5107ae217b8a0ba3e0e161f97b51 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 22 Aug 2023 10:50:58 +0300 Subject: [PATCH 54/60] Hide ACDC footer frame via CSS to avoid empty space It seems like this iframe (added when ACDC is enabled) can result in some empty space being shown because of some inherited `body` CSS about fonts/text. So hiding it via `display: none`, I think it will not cause any issues, because this iframe is not supposed to be visible anyway (`width`, `height` are set to `0`). --- modules/ppcp-button/resources/css/hosted-fields.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/ppcp-button/resources/css/hosted-fields.scss b/modules/ppcp-button/resources/css/hosted-fields.scss index 2305d313b..578a70a09 100644 --- a/modules/ppcp-button/resources/css/hosted-fields.scss +++ b/modules/ppcp-button/resources/css/hosted-fields.scss @@ -15,3 +15,7 @@ .ppcp-dcc-order-button { float: right; } + +iframe[id^="hosted-fields-tokenization-frame_"] { + display: none; +} From 3ad4969eecfefa9f8fe2c2158afb54815b9d7f8b Mon Sep 17 00:00:00 2001 From: Mayisha Date: Thu, 24 Aug 2023 22:04:00 +0600 Subject: [PATCH 55/60] woorelease: Product version bump update --- changelog.txt | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index b1a4ab20d..e5f66deb3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ *** Changelog *** -= 2.2.1 - xxxx-xx-xx = += 2.2.1 - 2023-08-24 = * Fix - One-page checkout causes mini cart not showing the PP button on certain pages #1536 * Fix - When onboarding loading the return_url too fast may cause the onboarding to fail #1565 * Fix - PayPal button doesn't work for variable products on product page after recent 2.2.0 release #1533 diff --git a/readme.txt b/readme.txt index 4837aa324..7bc01344f 100644 --- a/readme.txt +++ b/readme.txt @@ -81,7 +81,7 @@ Follow the steps below to connect the plugin to your PayPal account: == Changelog == -= 2.2.1 - xxxx-xx-xx = += 2.2.1 - 2023-08-24 = * Fix - One-page checkout causes mini cart not showing the PP button on certain pages #1536 * Fix - When onboarding loading the return_url too fast may cause the onboarding to fail #1565 * Fix - PayPal button doesn't work for variable products on product page after recent 2.2.0 release #1533 From 7fc13a2999c53b02fbf24e31a02a7b04b2698f17 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 28 Aug 2023 11:46:55 +0300 Subject: [PATCH 56/60] Bump 2.2.2, update changelog --- changelog.txt | 5 +++++ package.json | 2 +- readme.txt | 7 ++++++- woocommerce-paypal-payments.php | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index e5f66deb3..8b43422c6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,10 @@ *** Changelog *** += 2.2.2 - xxxx-xx-xx = +* Fix - High rate of auth voids on vaulted subscriptions for guest users #1529 +* Enhancement - HPOS compatibility issues #1594 +* Feature preview - PayPal Subscriptions API fixes and improvements #1600 #1607 + = 2.2.1 - 2023-08-24 = * Fix - One-page checkout causes mini cart not showing the PP button on certain pages #1536 * Fix - When onboarding loading the return_url too fast may cause the onboarding to fail #1565 diff --git a/package.json b/package.json index 97fbd5df5..a1a4ed290 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "2.2.1", + "version": "2.2.2", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index 7bc01344f..9c0b5a868 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, e-commerce, store, sales, sell, Requires at least: 5.3 Tested up to: 6.3 Requires PHP: 7.2 -Stable tag: 2.2.1 +Stable tag: 2.2.2 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -81,6 +81,11 @@ Follow the steps below to connect the plugin to your PayPal account: == Changelog == += 2.2.2 - xxxx-xx-xx = +* Fix - High rate of auth voids on vaulted subscriptions for guest users #1529 +* Enhancement - HPOS compatibility issues #1594 +* Feature preview - PayPal Subscriptions API fixes and improvements #1600 #1607 + = 2.2.1 - 2023-08-24 = * Fix - One-page checkout causes mini cart not showing the PP button on certain pages #1536 * Fix - When onboarding loading the return_url too fast may cause the onboarding to fail #1565 diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 13135b327..0f39a44ca 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 2.2.1 + * Version: 2.2.2 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 From 0c7f2e476923e787e95aa67ed740045043f09ee6 Mon Sep 17 00:00:00 2001 From: Danae Millan Date: Tue, 29 Aug 2023 13:44:01 -0300 Subject: [PATCH 57/60] woorelease: Product version bump update --- changelog.txt | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8b43422c6..500940c22 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ *** Changelog *** -= 2.2.2 - xxxx-xx-xx = += 2.2.2 - 2023-08-29 = * Fix - High rate of auth voids on vaulted subscriptions for guest users #1529 * Enhancement - HPOS compatibility issues #1594 * Feature preview - PayPal Subscriptions API fixes and improvements #1600 #1607 diff --git a/readme.txt b/readme.txt index 9c0b5a868..c36b06606 100644 --- a/readme.txt +++ b/readme.txt @@ -81,7 +81,7 @@ Follow the steps below to connect the plugin to your PayPal account: == Changelog == -= 2.2.2 - xxxx-xx-xx = += 2.2.2 - 2023-08-29 = * Fix - High rate of auth voids on vaulted subscriptions for guest users #1529 * Enhancement - HPOS compatibility issues #1594 * Feature preview - PayPal Subscriptions API fixes and improvements #1600 #1607 From d41a51fa63485840a2c84019f4192579429fd722 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 30 Aug 2023 16:57:06 +0100 Subject: [PATCH 58/60] Add handler support for settings fields Add settings SubElementsHandler --- .../js/SettingsHandler/SubElementsHandler.js | 51 +++++++++++++++++++ .../resources/js/gateway-settings.js | 12 +++++ .../Settings/Fields/connection-tab-fields.php | 33 ++++++++---- 3 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 modules/ppcp-wc-gateway/resources/js/SettingsHandler/SubElementsHandler.js diff --git a/modules/ppcp-wc-gateway/resources/js/SettingsHandler/SubElementsHandler.js b/modules/ppcp-wc-gateway/resources/js/SettingsHandler/SubElementsHandler.js new file mode 100644 index 000000000..cb0b30252 --- /dev/null +++ b/modules/ppcp-wc-gateway/resources/js/SettingsHandler/SubElementsHandler.js @@ -0,0 +1,51 @@ + +class SubElementsHandler { + constructor(element, options) { + const fieldSelector = 'input, select, textarea'; + + this.element = element; + this.values = options.values; + this.elements = options.elements; + + this.elementsSelector = this.elements.join(','); + + this.input = jQuery(this.element).is(fieldSelector) + ? this.element + : jQuery(this.element).find(fieldSelector).get(0); + + this.updateElementsVisibility(); + + jQuery(this.input).change(() => { + this.updateElementsVisibility(); + }); + } + + updateElementsVisibility() { + const $elements = jQuery(this.elementsSelector); + + let value = this.getValue(this.input); + value = (value !== null ? value.toString() : value); + + if (this.values.indexOf(value) !== -1) { + $elements.show(); + } else { + $elements.hide(); + } + } + + getValue(element) { + const $el = jQuery(element); + + if ($el.is(':checkbox') || $el.is(':radio')) { + if ($el.is(':checked')) { + return $el.val(); + } else { + return null; + } + } else { + return $el.val(); + } + } +} + +export default SubElementsHandler; diff --git a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js index c20432d48..bbb455be9 100644 --- a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js +++ b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js @@ -4,6 +4,7 @@ import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Rendere import MessageRenderer from "../../../ppcp-button/resources/js/modules/Renderer/MessageRenderer"; import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding"; import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; +import SubElementsHandler from "./SettingsHandler/SubElementsHandler"; document.addEventListener( 'DOMContentLoaded', @@ -307,5 +308,16 @@ document.addEventListener( createButtonPreview(() => getButtonDefaultSettings('#ppcpPayLaterButtonPreview')); }); } + + // Generic behaviours, can be moved to common.js once it's on trunk branch. + jQuery( '*[data-ppcp-handlers]' ).each( (index, el) => { + const handlers = jQuery(el).data('ppcpHandlers'); + for (const handlerConfig of handlers) { + new { + SubElementsHandler: SubElementsHandler + }[handlerConfig.handler](el, handlerConfig.options) + } + }); + } ); diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 64babe773..02f805101 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -498,25 +498,38 @@ return function ( ContainerInterface $container, array $fields ): array { 'gateway' => Settings::CONNECTION_TAB_ID, ), 'subtotal_mismatch_behavior' => array( - 'title' => __( 'Subtotal mismatch behavior', 'woocommerce-paypal-payments' ), - 'type' => 'select', - 'input_class' => array( 'wc-enhanced-select' ), - 'default' => 'vertical', - 'desc_tip' => true, - 'description' => __( + 'title' => __( 'Subtotal mismatch behavior', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'vertical', + 'desc_tip' => true, + 'description' => __( 'Differences between WooCommerce and PayPal roundings may cause mismatch in order items subtotal calculations. If not handled, these mismatches will cause the PayPal transaction to fail.', 'woocommerce-paypal-payments' ), - 'options' => array( + 'options' => array( PurchaseUnitSanitizer::MODE_DITCH => __( 'Do not send line items to PayPal', 'woocommerce-paypal-payments' ), PurchaseUnitSanitizer::MODE_EXTRA_LINE => __( 'Add another line item', 'woocommerce-paypal-payments' ), ), - 'screens' => array( + 'screens' => array( State::STATE_START, State::STATE_ONBOARDED, ), - 'requirements' => array(), - 'gateway' => Settings::CONNECTION_TAB_ID, + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, + 'custom_attributes' => array( + 'data-ppcp-handlers' => wp_json_encode( + array( + array( + 'handler' => 'SubElementsHandler', + 'options' => array( + 'values' => array( PurchaseUnitSanitizer::MODE_EXTRA_LINE ), + 'elements' => array( '#field-subtotal_mismatch_line_name' ), + ), + ), + ) + ), + ), ), 'subtotal_mismatch_line_name' => array( 'title' => __( 'Subtotal mismatch line name', 'woocommerce-paypal-payments' ), From 17cf2d36e37ae3b0e2e1103728f3ac589a7fa073 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 30 Aug 2023 17:13:03 +0100 Subject: [PATCH 59/60] Fix merge conflicts --- modules/ppcp-api-client/services.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 9483fe45a..ec73e16fd 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -27,9 +27,6 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\IdentityToken; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; @@ -42,7 +39,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\AddressFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ApplicationContextFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ExchangeRateFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\FraudProcessorResponseFactory; @@ -52,19 +48,15 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayeeFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentsFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentSourceFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PlatformFeeFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerReceivableBreakdownFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory; @@ -78,8 +70,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; -use WooCommerce\PayPalCommerce\Session\SessionHandler; -use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; return array( From f54f413f7b216e88f8e60a20580a1ca8913b246a Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 4 Sep 2023 16:09:33 +0300 Subject: [PATCH 60/60] Set payment method title for order edit page only if our gateway --- modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 0b01b4750..c703de904 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -290,9 +290,11 @@ class PayPalGateway extends \WC_Payment_Gateway { // in the constructor, so must do it here. global $theorder; if ( $theorder instanceof WC_Order ) { - $payment_method_title = $theorder->get_payment_method_title(); - if ( $payment_method_title ) { - $this->title = $payment_method_title; + if ( $theorder->get_payment_method() === self::ID ) { + $payment_method_title = $theorder->get_payment_method_title(); + if ( $payment_method_title ) { + $this->title = $payment_method_title; + } } } }