From bac08954830ca4fdf58031d85f5c8ff94463534a Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 6 Oct 2021 12:55:38 +0300 Subject: [PATCH 01/13] Add details to authorization status --- .../src/Entity/class-authorizationstatus.php | 24 ++++++- .../class-authorizationstatusdetails.php | 66 +++++++++++++++++++ .../Factory/class-authorizationfactory.php | 8 ++- 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php diff --git a/modules/ppcp-api-client/src/Entity/class-authorizationstatus.php b/modules/ppcp-api-client/src/Entity/class-authorizationstatus.php index 3f9903f1d..1275cd878 100644 --- a/modules/ppcp-api-client/src/Entity/class-authorizationstatus.php +++ b/modules/ppcp-api-client/src/Entity/class-authorizationstatus.php @@ -44,13 +44,21 @@ class AuthorizationStatus { */ private $status; + /** + * The details. + * + * @var AuthorizationStatusDetails|null + */ + private $details; + /** * AuthorizationStatus constructor. * - * @param string $status The status. + * @param string $status The status. + * @param AuthorizationStatusDetails|null $details The details. * @throws RuntimeException When the status is not valid. */ - public function __construct( string $status ) { + public function __construct( string $status, ?AuthorizationStatusDetails $details = null ) { if ( ! in_array( $status, self::VALID_STATUS, true ) ) { throw new RuntimeException( sprintf( @@ -60,7 +68,8 @@ class AuthorizationStatus { ) ); } - $this->status = $status; + $this->status = $status; + $this->details = $details; } /** @@ -91,4 +100,13 @@ class AuthorizationStatus { public function name(): string { return $this->status; } + + /** + * Returns the details. + * + * @return AuthorizationStatusDetails|null + */ + public function details(): ?AuthorizationStatusDetails { + return $this->details; + } } diff --git a/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php b/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php new file mode 100644 index 000000000..302bd3296 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php @@ -0,0 +1,66 @@ +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 authorization status. + * + * @return string + */ + public function name(): string { + return $this->reason; + } +} diff --git a/modules/ppcp-api-client/src/Factory/class-authorizationfactory.php b/modules/ppcp-api-client/src/Factory/class-authorizationfactory.php index 6cc999d10..c65e764af 100644 --- a/modules/ppcp-api-client/src/Factory/class-authorizationfactory.php +++ b/modules/ppcp-api-client/src/Factory/class-authorizationfactory.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; +use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatusDetails; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; /** @@ -39,9 +40,14 @@ class AuthorizationFactory { ); } + $reason = $data->status_details->reason ?? null; + return new Authorization( $data->id, - new AuthorizationStatus( $data->status ) + new AuthorizationStatus( + $data->status, + $reason ? new AuthorizationStatusDetails( $reason ) : null + ) ); } } From 7d0949eece5ce4ca01a9d2fc1b9a03a0e80f8493 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 6 Oct 2021 15:43:27 +0300 Subject: [PATCH 02/13] Throw if denied authorization like in capture() --- .../ppcp-api-client/src/Endpoint/class-orderendpoint.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/ppcp-api-client/src/Endpoint/class-orderendpoint.php b/modules/ppcp-api-client/src/Endpoint/class-orderendpoint.php index b70ddaa75..33c2aed4d 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-orderendpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/class-orderendpoint.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; +use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; @@ -412,6 +413,12 @@ class OrderEndpoint { throw $error; } $order = $this->order_factory->from_paypal_response( $json ); + + $authorization_status = $order->purchase_units()[0]->payments()->authorizations()[0]->status() ?? null; + if ( $authorization_status && $authorization_status->is( AuthorizationStatus::DENIED ) ) { + throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) ); + } + return $order; } From df68c948b9cfa0d34402a9af93ffba914689289a Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 6 Oct 2021 16:03:38 +0300 Subject: [PATCH 03/13] Refactor payment error handling --- .../src/Gateway/class-processpaymenttrait.php | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php index 4ce4c29d1..16a3c1cbe 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php @@ -9,6 +9,7 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; +use Exception; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; @@ -103,11 +104,8 @@ trait ProcessPaymentTrait { } $this->logger->warning( "Could neither capture nor authorize order {$order->id()} using a saved credit card:" . 'Status: ' . $order->status()->name() . ' Intent: ' . $order->intent() ); - } catch ( RuntimeException $error ) { - $this->logger->error( $error->getMessage() ); - $this->session_handler->destroy_session_data(); - wc_add_notice( $error->getMessage(), 'error' ); + $this->handle_failure( $wc_order, $error ); return null; } } @@ -186,12 +184,7 @@ trait ProcessPaymentTrait { $this->session_handler->destroy_session_data(); } catch ( RuntimeException $error ) { - $wc_order->update_status( - 'failed', - __( 'Could not process order.', 'woocommerce-paypal-payments' ) - ); - $this->session_handler->destroy_session_data(); - wc_add_notice( $error->getMessage(), 'error' ); + $this->handle_failure( $wc_order, $error ); return $failure_data; } @@ -234,4 +227,23 @@ trait ProcessPaymentTrait { } return false; } + + /** + * Handles the payment failure. + * + * @param \WC_Order $wc_order The order. + * @param Exception $error The error causing the failure. + */ + protected function handle_failure( \WC_Order $wc_order, Exception $error ): void { + $this->logger->error( 'Payment failed: ' . $error->getMessage() ); + + $wc_order->update_status( + 'failed', + __( 'Could not process order.', 'woocommerce-paypal-payments' ) + ); + + $this->session_handler->destroy_session_data(); + + wc_add_notice( $error->getMessage(), 'error' ); + } } From 45249966e973411866a65dc884c2f4b39bc647e0 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 6 Oct 2021 17:44:41 +0300 Subject: [PATCH 04/13] Add missing order meta --- modules/ppcp-wc-gateway/services.php | 12 ++++-- .../src/Gateway/class-creditcardgateway.php | 22 +++++++++- .../src/Gateway/class-paypalgateway.php | 22 +++++++++- .../src/Gateway/class-processpaymenttrait.php | 15 ++++++- .../src/Processor/class-ordermetatrait.php | 41 +++++++++++++++++++ .../src/Processor/class-orderprocessor.php | 24 +++++------ .../WcGateway/Gateway/WcGatewayTest.php | 31 ++++++++++---- .../Processor/OrderProcessorTest.php | 17 ++++++-- 8 files changed, 153 insertions(+), 31 deletions(-) create mode 100644 modules/ppcp-wc-gateway/src/Processor/class-ordermetatrait.php diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index fe1d32aa9..35be6f9fc 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -53,6 +53,7 @@ return array( $transaction_url_provider = $container->get( 'wcgateway.transaction-url-provider' ); $subscription_helper = $container->get( 'subscription.helper' ); $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); + $environment = $container->get( 'onboarding.environment' ); return new PayPalGateway( $settings_renderer, $order_processor, @@ -64,7 +65,8 @@ return array( $state, $transaction_url_provider, $subscription_helper, - $page_id + $page_id, + $environment ); }, 'wcgateway.credit-card-gateway' => static function ( $container ): CreditCardGateway { @@ -83,7 +85,8 @@ return array( $payer_factory = $container->get( 'api.factory.payer' ); $order_endpoint = $container->get( 'api.endpoint.order' ); $subscription_helper = $container->get( 'subscription.helper' ); - $logger = $container->get( 'woocommerce.logger.woocommerce' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + $environment = $container->get( 'onboarding.environment' ); return new CreditCardGateway( $settings_renderer, $order_processor, @@ -100,7 +103,8 @@ return array( $payer_factory, $order_endpoint, $subscription_helper, - $logger + $logger, + $environment ); }, 'wcgateway.disabler' => static function ( $container ): DisableGateways { @@ -209,7 +213,7 @@ return array( $authorized_payments_processor, $settings, $logger, - $environment->current_environment_is( Environment::SANDBOX ) + $environment ); }, 'wcgateway.processor.refunds' => static function ( $container ): RefundProcessor { diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php index e23c90600..452334d2e 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php @@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; +use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; @@ -96,6 +97,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { */ private $order_endpoint; + /** + * The environment. + * + * @var Environment + */ + protected $environment; + /** * CreditCardGateway constructor. * @@ -115,6 +123,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { * @param OrderEndpoint $order_endpoint The order endpoint. * @param SubscriptionHelper $subscription_helper The subscription helper. * @param LoggerInterface $logger The logger. + * @param Environment $environment The environment. */ public function __construct( SettingsRenderer $settings_renderer, @@ -132,7 +141,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { PayerFactory $payer_factory, OrderEndpoint $order_endpoint, SubscriptionHelper $subscription_helper, - LoggerInterface $logger + LoggerInterface $logger, + Environment $environment ) { $this->id = self::ID; @@ -143,6 +153,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { $this->config = $config; $this->session_handler = $session_handler; $this->refund_processor = $refund_processor; + $this->environment = $environment; if ( $state->current_state() === State::STATE_ONBOARDED ) { $this->supports = array( 'refunds' ); @@ -424,4 +435,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { private function is_enabled(): bool { return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' ); } + + /** + * Returns the environment. + * + * @return Environment + */ + protected function environment(): Environment { + return $this->environment; + } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php index fabce710a..f16396e06 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; +use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; @@ -111,6 +112,13 @@ class PayPalGateway extends \WC_Payment_Gateway { */ protected $page_id; + /** + * The environment. + * + * @var Environment + */ + protected $environment; + /** * PayPalGateway constructor. * @@ -125,6 +133,7 @@ class PayPalGateway extends \WC_Payment_Gateway { * @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order. * @param SubscriptionHelper $subscription_helper The subscription helper. * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. + * @param Environment $environment The environment. */ public function __construct( SettingsRenderer $settings_renderer, @@ -137,7 +146,8 @@ class PayPalGateway extends \WC_Payment_Gateway { State $state, TransactionUrlProvider $transaction_url_provider, SubscriptionHelper $subscription_helper, - string $page_id + string $page_id, + Environment $environment ) { $this->id = self::ID; @@ -150,6 +160,7 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->refund_processor = $refund_processor; $this->transaction_url_provider = $transaction_url_provider; $this->page_id = $page_id; + $this->environment = $environment; $this->onboarded = $state->current_state() === State::STATE_ONBOARDED; if ( $this->onboarded ) { @@ -430,4 +441,13 @@ class PayPalGateway extends \WC_Payment_Gateway { return $ret; } + + /** + * Returns the environment. + * + * @return Environment + */ + protected function environment(): Environment { + return $this->environment; + } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php index 16a3c1cbe..0f4130afa 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php @@ -13,11 +13,16 @@ use Exception; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\Onboarding\Environment; +use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; /** * Trait ProcessPaymentTrait */ trait ProcessPaymentTrait { + + use OrderMetaTrait; + /** * Process a payment for an WooCommerce order. * @@ -74,6 +79,8 @@ trait ProcessPaymentTrait { $selected_token ); + $this->add_paypal_meta( $wc_order, $order, $this->environment() ); + if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'CAPTURE' ) { $wc_order->update_status( 'processing', @@ -90,7 +97,6 @@ trait ProcessPaymentTrait { if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'AUTHORIZE' ) { $this->order_endpoint->authorize( $order ); $wc_order->update_meta_data( PayPalGateway::CAPTURED_META_KEY, 'false' ); - $wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() ); $wc_order->update_status( 'on-hold', __( 'Awaiting payment.', 'woocommerce-paypal-payments' ) @@ -246,4 +252,11 @@ trait ProcessPaymentTrait { wc_add_notice( $error->getMessage(), 'error' ); } + + /** + * Returns the environment. + * + * @return Environment + */ + abstract protected function environment(): Environment; } diff --git a/modules/ppcp-wc-gateway/src/Processor/class-ordermetatrait.php b/modules/ppcp-wc-gateway/src/Processor/class-ordermetatrait.php new file mode 100644 index 000000000..8aba3d7fa --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Processor/class-ordermetatrait.php @@ -0,0 +1,41 @@ +update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() ); + $wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() ); + $wc_order->update_meta_data( + PayPalGateway::ORDER_PAYMENT_MODE_META_KEY, + $environment->current_environment_is( Environment::SANDBOX ) ? 'sandbox' : 'live' + ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Processor/class-orderprocessor.php b/modules/ppcp-wc-gateway/src/Processor/class-orderprocessor.php index 022604f7e..26dac5dee 100644 --- a/modules/ppcp-wc-gateway/src/Processor/class-orderprocessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/class-orderprocessor.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; +use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -25,12 +26,14 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; */ class OrderProcessor { + use OrderMetaTrait; + /** - * Whether current payment mode is sandbox. + * The environment. * - * @var bool + * @var Environment */ - protected $sandbox_mode; + protected $environment; /** * The payment token repository. @@ -105,7 +108,7 @@ class OrderProcessor { * @param AuthorizedPaymentsProcessor $authorized_payments_processor The Authorized Payments Processor. * @param Settings $settings The Settings. * @param LoggerInterface $logger A logger service. - * @param bool $sandbox_mode Whether sandbox mode enabled. + * @param Environment $environment The environment. */ public function __construct( SessionHandler $session_handler, @@ -115,7 +118,7 @@ class OrderProcessor { AuthorizedPaymentsProcessor $authorized_payments_processor, Settings $settings, LoggerInterface $logger, - bool $sandbox_mode + Environment $environment ) { $this->session_handler = $session_handler; @@ -124,7 +127,7 @@ class OrderProcessor { $this->threed_secure = $three_d_secure; $this->authorized_payments_processor = $authorized_payments_processor; $this->settings = $settings; - $this->sandbox_mode = $sandbox_mode; + $this->environment = $environment; $this->logger = $logger; } @@ -140,12 +143,8 @@ class OrderProcessor { if ( ! $order ) { return false; } - $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->update_meta_data( - PayPalGateway::ORDER_PAYMENT_MODE_META_KEY, - $this->sandbox_mode ? 'sandbox' : 'live' - ); + + $this->add_paypal_meta( $wc_order, $order, $this->environment ); $error_message = null; if ( ! $this->order_is_approved( $order ) ) { @@ -164,6 +163,7 @@ class OrderProcessor { } $order = $this->patch_order( $wc_order, $order ); + if ( $order->intent() === 'CAPTURE' ) { $order = $this->order_endpoint->capture( $order ); } diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index 3070a2966..9c3be9b76 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -5,6 +5,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; @@ -21,8 +22,15 @@ use function Brain\Monkey\Functions\when; class WcGatewayTest extends TestCase { + private $environment; - public function testProcessPaymentSuccess() { + public function setUp(): void { + parent::setUp(); + + $this->environment = Mockery::mock(Environment::class); + } + + public function testProcessPaymentSuccess() { expect('is_admin')->andReturn(false); $orderId = 1; @@ -69,7 +77,8 @@ class WcGatewayTest extends TestCase $state, $transactionUrlProvider, $subscriptionHelper, - PayPalGateway::ID + PayPalGateway::ID, + $this->environment ); expect('wc_get_order') @@ -118,7 +127,8 @@ class WcGatewayTest extends TestCase $state, $transactionUrlProvider, $subscriptionHelper, - PayPalGateway::ID + PayPalGateway::ID, + $this->environment ); expect('wc_get_order') @@ -184,7 +194,8 @@ class WcGatewayTest extends TestCase $state, $transactionUrlProvider, $subscriptionHelper, - PayPalGateway::ID + PayPalGateway::ID, + $this->environment ); expect('wc_get_order') @@ -255,7 +266,8 @@ class WcGatewayTest extends TestCase $state, $transactionUrlProvider, $subscriptionHelper, - PayPalGateway::ID + PayPalGateway::ID, + $this->environment ); $this->assertTrue($testee->capture_authorized_payment($wcOrder)); @@ -310,7 +322,8 @@ class WcGatewayTest extends TestCase $state, $transactionUrlProvider, $subscriptionHelper, - PayPalGateway::ID + PayPalGateway::ID, + $this->environment ); $this->assertTrue($testee->capture_authorized_payment($wcOrder)); @@ -359,7 +372,8 @@ class WcGatewayTest extends TestCase $state, $transactionUrlProvider, $subscriptionHelper, - PayPalGateway::ID + PayPalGateway::ID, + $this->environment ); $this->assertFalse($testee->capture_authorized_payment($wcOrder)); @@ -399,7 +413,8 @@ class WcGatewayTest extends TestCase $onboardingState, $transactionUrlProvider, $subscriptionHelper, - PayPalGateway::ID + PayPalGateway::ID, + $this->environment ); $this->assertSame($needSetup, $testee->needs_setup()); diff --git a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php index 11e9db2d7..5289167c3 100644 --- a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php +++ b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Processor; +use Dhii\Container\Dictionary; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; @@ -13,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; +use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\TestCase; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -22,6 +24,13 @@ use function Brain\Monkey\Functions\when; class OrderProcessorTest extends TestCase { + private $environment; + + public function setUp(): void { + parent::setUp(); + + $this->environment = new Environment(new Dictionary([])); + } public function testAuthorize() { $transactionId = 'ABC123'; @@ -112,7 +121,7 @@ class OrderProcessorTest extends TestCase $authorizedPaymentProcessor, $settings, $logger, - false + $this->environment ); $cart = Mockery::mock(\WC_Cart::class); @@ -240,7 +249,7 @@ class OrderProcessorTest extends TestCase $authorizedPaymentProcessor, $settings, $logger, - false + $this->environment ); $cart = Mockery::mock(\WC_Cart::class); @@ -340,7 +349,7 @@ class OrderProcessorTest extends TestCase $authorizedPaymentProcessor, $settings, $logger, - false + $this->environment ); $wcOrder @@ -355,7 +364,7 @@ class OrderProcessorTest extends TestCase PayPalGateway::INTENT_META_KEY, $orderIntent ); - + $this->assertFalse($testee->process($wcOrder)); $this->assertNotEmpty($testee->last_error()); } From b7fcee1179e515a1f62dc6e4c7a3477f14b266bb Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 6 Oct 2021 18:53:49 +0300 Subject: [PATCH 05/13] Stop if failed to handle saved card payment --- .../ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php index 0f4130afa..764513d3a 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php @@ -110,6 +110,8 @@ trait ProcessPaymentTrait { } $this->logger->warning( "Could neither capture nor authorize order {$order->id()} using a saved credit card:" . 'Status: ' . $order->status()->name() . ' Intent: ' . $order->intent() ); + + return null; } catch ( RuntimeException $error ) { $this->handle_failure( $wc_order, $error ); return null; From 64c685487393e363c7151c54448b94a4690efdb9 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 6 Oct 2021 19:07:07 +0300 Subject: [PATCH 06/13] Use payment_complete() Not sure which way is better, but we should choose one --- .../src/Gateway/class-processpaymenttrait.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php index 764513d3a..6e35227ab 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php @@ -82,10 +82,7 @@ trait ProcessPaymentTrait { $this->add_paypal_meta( $wc_order, $order, $this->environment() ); if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'CAPTURE' ) { - $wc_order->update_status( - 'processing', - __( 'Payment received.', 'woocommerce-paypal-payments' ) - ); + $wc_order->payment_complete(); $this->session_handler->destroy_session_data(); return array( From e8fa6cd611648557c5406889477c93f16d2dcd3f Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 6 Oct 2021 21:38:37 +0300 Subject: [PATCH 07/13] Fix name --- .../src/Entity/class-authorizationstatusdetails.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php b/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php index 302bd3296..95624bba0 100644 --- a/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php +++ b/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php @@ -60,7 +60,7 @@ class AuthorizationStatusDetails { * * @return string */ - public function name(): string { + public function reason(): string { return $this->reason; } } From bfec11b1743f11c09afa8603f62e3cb6147736e2 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 6 Oct 2021 21:43:58 +0300 Subject: [PATCH 08/13] Remove wrong authorization statuses --- .../src/Entity/class-authorizationstatusdetails.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php b/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php index 95624bba0..1966b0e11 100644 --- a/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php +++ b/modules/ppcp-api-client/src/Entity/class-authorizationstatusdetails.php @@ -16,17 +16,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Entity; */ class AuthorizationStatusDetails { - const BUYER_COMPLAINT = 'BUYER_COMPLAINT'; - const CHARGEBACK = 'CHARGEBACK'; - const ECHECK = 'ECHECK'; - const INTERNATIONAL_WITHDRAWAL = 'INTERNATIONAL_WITHDRAWAL'; - const OTHER = 'OTHER'; - const PENDING_REVIEW = 'PENDING_REVIEW'; - const RECEIVING_PREFERENCE_MANDATES_MANUAL_ACTION = 'RECEIVING_PREFERENCE_MANDATES_MANUAL_ACTION'; - const REFUNDED = 'REFUNDED'; - const TRANSACTION_APPROVED_AWAITING_FUNDING = 'TRANSACTION_APPROVED_AWAITING_FUNDING'; - const UNILATERAL = 'REFUNDED'; - const VERIFICATION_REQUIRED = 'VERIFICATION_REQUIRED'; + const PENDING_REVIEW = 'PENDING_REVIEW'; /** * The reason. From 1a7eae93c2f00e7282ee1429cf6105803b93b8ba Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 6 Oct 2021 22:12:06 +0300 Subject: [PATCH 09/13] Refactor capture status, make like authorization status --- .../src/Endpoint/class-orderendpoint.php | 5 +- .../src/Entity/class-capture.php | 50 ++++-------- .../src/Entity/class-capturestatus.php | 79 +++++++++++++++++++ .../src/Entity/class-capturestatusdetails.php | 66 ++++++++++++++++ .../src/Factory/class-capturefactory.php | 11 ++- .../ApiClient/Endpoint/OrderEndpointTest.php | 3 +- 6 files changed, 175 insertions(+), 39 deletions(-) create mode 100644 modules/ppcp-api-client/src/Entity/class-capturestatus.php create mode 100644 modules/ppcp-api-client/src/Entity/class-capturestatusdetails.php diff --git a/modules/ppcp-api-client/src/Endpoint/class-orderendpoint.php b/modules/ppcp-api-client/src/Endpoint/class-orderendpoint.php index 33c2aed4d..cf9a65f4c 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-orderendpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/class-orderendpoint.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; +use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; @@ -339,8 +340,8 @@ class OrderEndpoint { $order = $this->order_factory->from_paypal_response( $json ); - $purchase_units_payments_captures_status = $order->purchase_units()[0]->payments()->captures()[0]->status() ?? ''; - if ( $purchase_units_payments_captures_status && 'DECLINED' === $purchase_units_payments_captures_status ) { + $capture_status = $order->purchase_units()[0]->payments()->captures()[0]->status() ?? null; + if ( $capture_status && $capture_status->is( CaptureStatus::DECLINED ) ) { throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) ); } diff --git a/modules/ppcp-api-client/src/Entity/class-capture.php b/modules/ppcp-api-client/src/Entity/class-capture.php index 9aad9e029..4b4853add 100644 --- a/modules/ppcp-api-client/src/Entity/class-capture.php +++ b/modules/ppcp-api-client/src/Entity/class-capture.php @@ -26,17 +26,10 @@ class Capture { /** * The status. * - * @var string + * @var CaptureStatus */ private $status; - /** - * The status details. - * - * @var string - */ - private $status_details; - /** * The amount. * @@ -75,19 +68,17 @@ class Capture { /** * Capture constructor. * - * @param string $id The ID. - * @param string $status The status. - * @param string $status_details The status details. - * @param Amount $amount The amount. - * @param bool $final_capture The final capture. - * @param string $seller_protection The seller protection. - * @param string $invoice_id The invoice id. - * @param string $custom_id The custom id. + * @param string $id The ID. + * @param CaptureStatus $status The status. + * @param Amount $amount The amount. + * @param bool $final_capture The final capture. + * @param string $seller_protection The seller protection. + * @param string $invoice_id The invoice id. + * @param string $custom_id The custom id. */ public function __construct( string $id, - string $status, - string $status_details, + CaptureStatus $status, Amount $amount, bool $final_capture, string $seller_protection, @@ -97,7 +88,6 @@ class Capture { $this->id = $id; $this->status = $status; - $this->status_details = $status_details; $this->amount = $amount; $this->final_capture = $final_capture; $this->seller_protection = $seller_protection; @@ -117,21 +107,12 @@ class Capture { /** * Returns the status. * - * @return string + * @return CaptureStatus */ - public function status() : string { + public function status() : CaptureStatus { return $this->status; } - /** - * Returns the status details object. - * - * @return \stdClass - */ - public function status_details() : \stdClass { - return (object) array( 'reason' => $this->status_details ); - } - /** * Returns the amount. * @@ -183,15 +164,18 @@ class Capture { * @return array */ public function to_array() : array { - return array( + $data = array( 'id' => $this->id(), - 'status' => $this->status(), - 'status_details' => (array) $this->status_details(), + 'status' => $this->status()->name(), 'amount' => $this->amount()->to_array(), 'final_capture' => $this->final_capture(), 'seller_protection' => (array) $this->seller_protection(), 'invoice_id' => $this->invoice_id(), 'custom_id' => $this->custom_id(), ); + if ( $this->status()->details() ) { + $data['status_details'] = array( 'reason' => $this->status()->details()->reason() ); + } + return $data; } } diff --git a/modules/ppcp-api-client/src/Entity/class-capturestatus.php b/modules/ppcp-api-client/src/Entity/class-capturestatus.php new file mode 100644 index 000000000..aae0c920d --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/class-capturestatus.php @@ -0,0 +1,79 @@ +status = $status; + $this->details = $details; + } + + /** + * 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; + } + + /** + * Returns the details. + * + * @return CaptureStatusDetails|null + */ + public function details(): ?CaptureStatusDetails { + return $this->details; + } +} diff --git a/modules/ppcp-api-client/src/Entity/class-capturestatusdetails.php b/modules/ppcp-api-client/src/Entity/class-capturestatusdetails.php new file mode 100644 index 000000000..326875da9 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/class-capturestatusdetails.php @@ -0,0 +1,66 @@ +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 capture status. + * + * @return string + */ + public function reason(): string { + return $this->reason; + } +} diff --git a/modules/ppcp-api-client/src/Factory/class-capturefactory.php b/modules/ppcp-api-client/src/Factory/class-capturefactory.php index 7389f67e5..ec0309014 100644 --- a/modules/ppcp-api-client/src/Factory/class-capturefactory.php +++ b/modules/ppcp-api-client/src/Factory/class-capturefactory.php @@ -10,6 +10,8 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\ApiClient\Factory; use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; +use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; +use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatusDetails; /** * Class CaptureFactory @@ -42,11 +44,14 @@ class CaptureFactory { */ public function from_paypal_response( \stdClass $data ) : Capture { - $reason = isset( $data->status_details->reason ) ? (string) $data->status_details->reason : ''; + $reason = $data->status_details->reason ?? null; + return new Capture( (string) $data->id, - (string) $data->status, - $reason, + new CaptureStatus( + (string) $data->status, + $reason ? new CaptureStatusDetails( $reason ) : null + ), $this->amount_factory->from_paypal_response( $data->amount ), (bool) $data->final_capture, (string) $data->seller_protection->status, diff --git a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php index d44471319..16bd945f7 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php @@ -8,6 +8,7 @@ use Requests_Utility_CaseInsensitiveDictionary; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; +use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection; @@ -278,7 +279,7 @@ class OrderEndpointTest extends TestCase $expectedOrder->shouldReceive('purchase_units')->once()->andReturn(['0'=>$purchaseUnit]); $purchaseUnit->shouldReceive('payments')->once()->andReturn($payment); $payment->shouldReceive('captures')->once()->andReturn(['0'=>$capture]); - $capture->shouldReceive('status')->once()->andReturn(''); + $capture->shouldReceive('status')->once()->andReturn(new CaptureStatus(CaptureStatus::COMPLETED)); $result = $testee->capture($orderToCapture); $this->assertEquals($expectedOrder, $result); From 98bd5bd245d29a2547c813cb5ed6cb9e4fd69cf0 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Oct 2021 09:21:12 +0300 Subject: [PATCH 10/13] Extract/refactor order status changes, fix denied payment handling --- .../src/Gateway/class-processpaymenttrait.php | 46 +++--- .../src/Processor/class-orderprocessor.php | 12 +- .../class-paymentstatushandlingtrait.php | 141 ++++++++++++++++++ .../Processor/OrderProcessorTest.php | 40 ++--- 4 files changed, 189 insertions(+), 50 deletions(-) create mode 100644 modules/ppcp-wc-gateway/src/Processor/class-paymentstatushandlingtrait.php diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php index 6e35227ab..86c3b57e8 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-processpaymenttrait.php @@ -15,13 +15,14 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; +use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; /** * Trait ProcessPaymentTrait */ trait ProcessPaymentTrait { - use OrderMetaTrait; + use OrderMetaTrait, PaymentsStatusHandlingTrait; /** * Process a payment for an WooCommerce order. @@ -81,34 +82,33 @@ trait ProcessPaymentTrait { $this->add_paypal_meta( $wc_order, $order, $this->environment() ); - if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'CAPTURE' ) { - $wc_order->payment_complete(); - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); + if ( ! $order->status()->is( OrderStatus::COMPLETED ) ) { + $this->logger->warning( "Unexpected status for order {$order->id()} using a saved credit card: " . $order->status()->name() ); + return null; } - if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'AUTHORIZE' ) { - $this->order_endpoint->authorize( $order ); + if ( ! in_array( + $order->intent(), + array( 'CAPTURE', 'AUTHORIZE' ), + true + ) ) { + $this->logger->warning( "Could neither capture nor authorize order {$order->id()} using a saved credit card:" . 'Status: ' . $order->status()->name() . ' Intent: ' . $order->intent() ); + return null; + } + + if ( $order->intent() === 'AUTHORIZE' ) { + $order = $this->order_endpoint->authorize( $order ); + $wc_order->update_meta_data( PayPalGateway::CAPTURED_META_KEY, 'false' ); - $wc_order->update_status( - 'on-hold', - __( 'Awaiting payment.', 'woocommerce-paypal-payments' ) - ); - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); } - $this->logger->warning( "Could neither capture nor authorize order {$order->id()} using a saved credit card:" . 'Status: ' . $order->status()->name() . ' Intent: ' . $order->intent() ); + $this->handle_new_order_status( $order, $wc_order ); - return null; + $this->session_handler->destroy_session_data(); + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $wc_order ), + ); } catch ( RuntimeException $error ) { $this->handle_failure( $wc_order, $error ); return null; diff --git a/modules/ppcp-wc-gateway/src/Processor/class-orderprocessor.php b/modules/ppcp-wc-gateway/src/Processor/class-orderprocessor.php index 26dac5dee..fdcf95076 100644 --- a/modules/ppcp-wc-gateway/src/Processor/class-orderprocessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/class-orderprocessor.php @@ -26,7 +26,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; */ class OrderProcessor { - use OrderMetaTrait; + use OrderMetaTrait, PaymentsStatusHandlingTrait; /** * The environment. @@ -170,6 +170,7 @@ class OrderProcessor { if ( $order->intent() === 'AUTHORIZE' ) { $order = $this->order_endpoint->authorize( $order ); + $wc_order->update_meta_data( PayPalGateway::CAPTURED_META_KEY, 'false' ); } @@ -179,14 +180,7 @@ class OrderProcessor { $this->set_order_transaction_id( $transaction_id, $wc_order ); } - $wc_order->update_status( - 'on-hold', - __( 'Awaiting payment.', 'woocommerce-paypal-payments' ) - ); - if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'CAPTURE' ) { - - $wc_order->payment_complete(); - } + $this->handle_new_order_status( $order, $wc_order ); if ( $this->capture_authorized_downloads( $order ) && AuthorizedPaymentsProcessor::SUCCESSFUL === $this->authorized_payments_processor->process( $wc_order ) ) { $wc_order->add_order_note( diff --git a/modules/ppcp-wc-gateway/src/Processor/class-paymentstatushandlingtrait.php b/modules/ppcp-wc-gateway/src/Processor/class-paymentstatushandlingtrait.php new file mode 100644 index 000000000..20d56be72 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Processor/class-paymentstatushandlingtrait.php @@ -0,0 +1,141 @@ +intent() === 'CAPTURE' ) { + $this->handle_capture_status( $order->purchase_units()[0]->payments()->captures()[0], $wc_order ); + } elseif ( $order->intent() === 'AUTHORIZE' ) { + $this->handle_authorization_status( $order->purchase_units()[0]->payments()->authorizations()[0], $wc_order ); + } + } + + /** + * Changes the order status, based on the capture. + * + * @param Capture $capture The capture. + * @param WC_Order $wc_order The WC order. + * + * @throws RuntimeException If payment denied. + */ + protected function handle_capture_status( + Capture $capture, + WC_Order $wc_order + ): void { + $status = $capture->status(); + + if ( $status->details() ) { + $this->add_status_details_note( $wc_order, $status->name(), $status->details()->reason() ); + } + + switch ( $status->name() ) { + case CaptureStatus::COMPLETED: + $wc_order->payment_complete(); + break; + // It is checked in the capture endpoint already, but there are other ways to capture, + // such as when paid via saved card. + case CaptureStatus::DECLINED: + $wc_order->update_status( + 'failed', + __( 'Could not capture the payment.', 'woocommerce-paypal-payments' ) + ); + throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) ); + case CaptureStatus::PENDING: + case CaptureStatus::FAILED: + $wc_order->update_status( + 'on-hold', + __( 'Awaiting payment.', 'woocommerce-paypal-payments' ) + ); + break; + } + } + + /** + * Changes the order status, based on the authorization. + * + * @param Authorization $authorization The authorization. + * @param WC_Order $wc_order The WC order. + * + * @throws RuntimeException If payment denied. + */ + protected function handle_authorization_status( + Authorization $authorization, + WC_Order $wc_order + ): void { + $status = $authorization->status(); + + if ( $status->details() ) { + $this->add_status_details_note( $wc_order, $status->name(), $status->details()->reason() ); + } + + switch ( $status->name() ) { + case AuthorizationStatus::CREATED: + case AuthorizationStatus::PENDING: + $wc_order->update_status( + 'on-hold', + __( 'Awaiting payment.', 'woocommerce-paypal-payments' ) + ); + break; + case AuthorizationStatus::DENIED: + $wc_order->update_status( + 'failed', + __( 'Could not get the payment authorization.', 'woocommerce-paypal-payments' ) + ); + throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) ); + } + } + + /** + * Adds the order note with status details. + * + * @param WC_Order $wc_order The WC order to which the note will be added. + * @param string $status The status name. + * @param string $reason The status reason. + */ + protected function add_status_details_note( + WC_Order $wc_order, + string $status, + string $reason + ): void { + $wc_order->add_order_note( + sprintf( + /* translators: %1$s - PENDING, DENIED, ... %2$s - PENDING_REVIEW, ... */ + __( 'PayPal order payment is set to %1$s status, details: %2$s.', 'woocommerce-paypal-payments' ), + $status, + $reason + ) + ); + $wc_order->save(); + } +} diff --git a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php index 5289167c3..13019b4c1 100644 --- a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php +++ b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php @@ -7,7 +7,10 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Processor; use Dhii\Container\Dictionary; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; +use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; +use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; @@ -35,31 +38,33 @@ class OrderProcessorTest extends TestCase public function testAuthorize() { $transactionId = 'ABC123'; - $capture = Mockery::mock(Capture::class); - $capture->expects('id') + $authorization = Mockery::mock(Authorization::class); + $authorization->shouldReceive('id') ->andReturn($transactionId); + $authorization->shouldReceive('status') + ->andReturn(new AuthorizationStatus(AuthorizationStatus::CREATED)); $payments = Mockery::mock(Payments::class); - $payments->expects('captures') - ->andReturn([$capture]); + $payments->shouldReceive('authorizations') + ->andReturn([$authorization]); + $payments->shouldReceive('captures') + ->andReturn([]); $purchaseUnit = Mockery::mock(PurchaseUnit::class); - $purchaseUnit->expects('payments') + $purchaseUnit->shouldReceive('payments') ->andReturn($payments); $wcOrder = Mockery::mock(\WC_Order::class); $wcOrder->expects('update_meta_data') ->with(PayPalGateway::ORDER_PAYMENT_MODE_META_KEY, 'live'); - $wcOrder->expects('set_transaction_id') - ->with($transactionId); $orderStatus = Mockery::mock(OrderStatus::class); $orderStatus - ->expects('is') + ->shouldReceive('is') ->with(OrderStatus::APPROVED) ->andReturn(true); $orderStatus - ->expects('is') + ->shouldReceive('is') ->with(OrderStatus::COMPLETED) ->andReturn(true); @@ -76,7 +81,7 @@ class OrderProcessorTest extends TestCase $currentOrder ->shouldReceive('status') ->andReturn($orderStatus); - $currentOrder->expects('purchase_units') + $currentOrder->shouldReceive('purchase_units') ->andReturn([$purchaseUnit]); $sessionHandler = Mockery::mock(SessionHandler::class); @@ -165,23 +170,25 @@ class OrderProcessorTest extends TestCase $capture = Mockery::mock(Capture::class); $capture->expects('id') ->andReturn($transactionId); + $capture->expects('status') + ->andReturn(new CaptureStatus(CaptureStatus::COMPLETED)); $payments = Mockery::mock(Payments::class); - $payments->expects('captures') + $payments->shouldReceive('captures') ->andReturn([$capture]); $purchaseUnit = Mockery::mock(PurchaseUnit::class); - $purchaseUnit->expects('payments') + $purchaseUnit->shouldReceive('payments') ->andReturn($payments); $wcOrder = Mockery::mock(\WC_Order::class); $orderStatus = Mockery::mock(OrderStatus::class); $orderStatus - ->expects('is') + ->shouldReceive('is') ->with(OrderStatus::APPROVED) ->andReturn(true); $orderStatus - ->expects('is') + ->shouldReceive('is') ->with(OrderStatus::COMPLETED) ->andReturn(true); $orderId = 'abc'; @@ -197,7 +204,7 @@ class OrderProcessorTest extends TestCase ->shouldReceive('status') ->andReturn($orderStatus); $currentOrder - ->expects('purchase_units') + ->shouldReceive('purchase_units') ->andReturn([$purchaseUnit]); $sessionHandler = Mockery::mock(SessionHandler::class); $sessionHandler @@ -273,9 +280,6 @@ class OrderProcessorTest extends TestCase PayPalGateway::INTENT_META_KEY, $orderIntent ); - $wcOrder - ->expects('update_status') - ->with('on-hold', 'Awaiting payment.'); $wcOrder->expects('update_meta_data') ->with(PayPalGateway::ORDER_PAYMENT_MODE_META_KEY, 'live'); $wcOrder->expects('set_transaction_id') From 06ee070c875fb1886ef7f7bf08cda3807c572f35 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Oct 2021 09:30:26 +0300 Subject: [PATCH 11/13] Allow voiding pending authorizations --- .../ppcp-wc-gateway/src/Processor/class-refundprocessor.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php b/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php index c5ca98057..d114554ca 100644 --- a/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php @@ -183,6 +183,7 @@ class RefundProcessor { * @return bool */ private function is_voidable_authorization( Authorization $authorization ): bool { - return $authorization->status()->is( AuthorizationStatus::CREATED ); + return $authorization->status()->is( AuthorizationStatus::CREATED ) || + $authorization->status()->is( AuthorizationStatus::PENDING ); } } From 03e9ac9aad29371b87b6dc710e48720a9c00b74b Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Oct 2021 10:15:29 +0300 Subject: [PATCH 12/13] Fix capture endpoint returned object --- modules/ppcp-api-client/services.php | 4 +- .../src/Endpoint/class-paymentsendpoint.php | 18 +++- .../Endpoint/PaymentsEndpointTest.php | 86 +++++++++---------- .../AuthorizedPaymentsProcessorTest.php | 14 ++- 4 files changed, 71 insertions(+), 51 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 982d4265f..f5d140c90 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -141,12 +141,14 @@ return array( }, 'api.endpoint.payments' => static function ( $container ): PaymentsEndpoint { $authorizations_factory = $container->get( 'api.factory.authorization' ); - $logger = $container->get( 'woocommerce.logger.woocommerce' ); + $capture_factory = $container->get( 'api.factory.capture' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new PaymentsEndpoint( $container->get( 'api.host' ), $container->get( 'api.bearer' ), $authorizations_factory, + $capture_factory, $logger ); }, diff --git a/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php b/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php index dc162dead..dc5f21743 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php @@ -11,11 +11,13 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; +use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; use Psr\Log\LoggerInterface; +use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory; /** * Class PaymentsEndpoint @@ -45,6 +47,13 @@ class PaymentsEndpoint { */ private $authorizations_factory; + /** + * The capture factory. + * + * @var CaptureFactory + */ + private $capture_factory; + /** * The logger. * @@ -58,18 +67,21 @@ class PaymentsEndpoint { * @param string $host The host. * @param Bearer $bearer The bearer. * @param AuthorizationFactory $authorization_factory The authorization factory. + * @param CaptureFactory $capture_factory The capture factory. * @param LoggerInterface $logger The logger. */ public function __construct( string $host, Bearer $bearer, AuthorizationFactory $authorization_factory, + CaptureFactory $capture_factory, LoggerInterface $logger ) { $this->host = $host; $this->bearer = $bearer; $this->authorizations_factory = $authorization_factory; + $this->capture_factory = $capture_factory; $this->logger = $logger; } @@ -136,11 +148,11 @@ class PaymentsEndpoint { * * @param string $authorization_id The id. * - * @return Authorization + * @return Capture * @throws RuntimeException If the request fails. * @throws PayPalApiException If the request fails. */ - public function capture( string $authorization_id ): Authorization { + public function capture( string $authorization_id ): Capture { $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v2/payments/authorizations/' . $authorization_id . '/capture'; $args = array( @@ -167,7 +179,7 @@ class PaymentsEndpoint { ); } - return $this->authorizations_factory->from_paypal_response( $json ); + return $this->capture_factory->from_paypal_response( $json ); } /** diff --git a/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php index 65112a61d..337d58ebf 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php @@ -8,10 +8,12 @@ use Psr\Log\NullLogger; use Requests_Utility_CaseInsensitiveDictionary; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; +use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\ErrorResponseCollection; use WooCommerce\PayPalCommerce\ApiClient\Entity\Token; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ErrorResponseCollectionFactory; use WooCommerce\PayPalCommerce\ApiClient\TestCase; use Mockery; @@ -21,7 +23,22 @@ use function Brain\Monkey\Functions\expect; class PaymentsEndpointTest extends TestCase { - public function testAuthorizationDefault() + private $authorizationFactory; + private $captureFactory; + + private $logger; + + public function setUp(): void + { + parent::setUp(); + + $this->authorizationFactory = Mockery::mock(AuthorizationFactory::class); + $this->captureFactory = Mockery::mock(CaptureFactory::class); + + $this->logger = new NullLogger(); + } + + public function testAuthorizationDefault() { expect('wp_json_encode')->andReturnUsing('json_encode'); $host = 'https://example.com/'; @@ -35,15 +52,10 @@ class PaymentsEndpointTest extends TestCase ->expects('bearer')->andReturn($token); $authorization = Mockery::mock(Authorization::class); - $authorizationFactory = Mockery::mock(AuthorizationFactory::class); - $authorizationFactory + $this->authorizationFactory ->expects('from_paypal_response') ->andReturn($authorization); - $logger = Mockery::mock(LoggerInterface::class); - $logger->shouldNotReceive('log'); - $logger->shouldReceive('debug'); - $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); $headers->shouldReceive('getAll'); $rawResponse = [ @@ -54,8 +66,9 @@ class PaymentsEndpointTest extends TestCase $testee = new PaymentsEndpoint( $host, $bearer, - $authorizationFactory, - $logger + $this->authorizationFactory, + $this->captureFactory, + $this->logger ); expect('wp_remote_get')->andReturnUsing( @@ -92,12 +105,6 @@ class PaymentsEndpointTest extends TestCase $bearer = Mockery::mock(Bearer::class); $bearer->expects('bearer')->andReturn($token); - $authorizationFactory = Mockery::mock(AuthorizationFactory::class); - - $logger = Mockery::mock(LoggerInterface::class); - $logger->shouldReceive('log'); - $logger->shouldReceive('debug'); - $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); $headers->shouldReceive('getAll'); $rawResponse = [ @@ -108,8 +115,9 @@ class PaymentsEndpointTest extends TestCase $testee = new PaymentsEndpoint( $host, $bearer, - $authorizationFactory, - $logger + $this->authorizationFactory, + $this->captureFactory, + $this->logger ); expect('wp_remote_get')->andReturn($rawResponse); @@ -131,8 +139,6 @@ class PaymentsEndpointTest extends TestCase $bearer = Mockery::mock(Bearer::class); $bearer->expects('bearer')->andReturn($token); - $authorizationFactory = Mockery::mock(AuthorizationFactory::class); - $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); $headers->shouldReceive('getAll'); $rawResponse = [ @@ -140,15 +146,12 @@ class PaymentsEndpointTest extends TestCase 'headers' => $headers, ]; - $logger = Mockery::mock(LoggerInterface::class); - $logger->shouldReceive('log'); - $logger->shouldReceive('debug'); - $testee = new PaymentsEndpoint( $host, $bearer, - $authorizationFactory, - $logger + $this->authorizationFactory, + $this->captureFactory, + $this->logger ); expect('wp_remote_get')->andReturn($rawResponse); @@ -172,16 +175,10 @@ class PaymentsEndpointTest extends TestCase $bearer ->expects('bearer')->andReturn($token); - $authorization = Mockery::mock(Authorization::class); - $authorizationFactory = Mockery::mock(AuthorizationFactory::class); - $authorizationFactory + $capture = Mockery::mock(Capture::class); + $this->captureFactory ->expects('from_paypal_response') - ->andReturn($authorization); - - - $logger = Mockery::mock(LoggerInterface::class); - $logger->shouldNotReceive('log'); - $logger->shouldReceive('debug'); + ->andReturn($capture); $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); $headers->shouldReceive('getAll'); @@ -193,8 +190,9 @@ class PaymentsEndpointTest extends TestCase $testee = new PaymentsEndpoint( $host, $bearer, - $authorizationFactory, - $logger + $this->authorizationFactory, + $this->captureFactory, + $this->logger ); expect('wp_remote_get')->andReturnUsing( @@ -219,7 +217,7 @@ class PaymentsEndpointTest extends TestCase expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(201); $result = $testee->capture($authorizationId); - $this->assertEquals($authorization, $result); + $this->assertEquals($capture, $result); } public function testCaptureIsWpError() @@ -234,8 +232,6 @@ class PaymentsEndpointTest extends TestCase $bearer = Mockery::mock(Bearer::class); $bearer->expects('bearer')->andReturn($token); - $authorizationFactory = Mockery::mock(AuthorizationFactory::class); - $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); $headers->shouldReceive('getAll'); $rawResponse = [ @@ -246,8 +242,9 @@ class PaymentsEndpointTest extends TestCase $testee = new PaymentsEndpoint( $host, $bearer, - $authorizationFactory, - new NullLogger() + $this->authorizationFactory, + $this->captureFactory, + $this->logger ); expect('wp_remote_get')->andReturn($rawResponse); @@ -269,8 +266,6 @@ class PaymentsEndpointTest extends TestCase $bearer = Mockery::mock(Bearer::class); $bearer->expects('bearer')->andReturn($token); - $authorizationFactory = Mockery::mock(AuthorizationFactory::class); - $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); $headers->shouldReceive('getAll'); $rawResponse = [ @@ -281,8 +276,9 @@ class PaymentsEndpointTest extends TestCase $testee = new PaymentsEndpoint( $host, $bearer, - $authorizationFactory, - new NullLogger() + $this->authorizationFactory, + $this->captureFactory, + $this->logger ); expect('wp_remote_get')->andReturn($rawResponse); diff --git a/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php b/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php index 2672d2f13..1f7bed337 100644 --- a/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php +++ b/tests/PHPUnit/WcGateway/Processor/AuthorizedPaymentsProcessorTest.php @@ -10,6 +10,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; +use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; +use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; @@ -57,7 +59,7 @@ class AuthorizedPaymentsProcessorTest extends TestCase $this->paymentsEndpoint ->expects('capture') ->with($this->authorizationId) - ->andReturn($this->createAuthorization($this->authorizationId, AuthorizationStatus::CAPTURED)); + ->andReturn($this->createCapture(CaptureStatus::COMPLETED)); $this->assertEquals(AuthorizedPaymentsProcessor::SUCCESSFUL, $this->testee->process($this->wcOrder)); } @@ -78,7 +80,7 @@ class AuthorizedPaymentsProcessorTest extends TestCase $this->paymentsEndpoint ->expects('capture') ->with($authorization->id()) - ->andReturn($this->createAuthorization($authorization->id(), AuthorizationStatus::CAPTURED)); + ->andReturn($this->createCapture(CaptureStatus::COMPLETED)); } $this->assertEquals(AuthorizedPaymentsProcessor::SUCCESSFUL, $this->testee->process($this->wcOrder)); @@ -143,6 +145,14 @@ class AuthorizedPaymentsProcessorTest extends TestCase return $authorization; } + private function createCapture(string $status): Capture { + $capture = Mockery::mock(Capture::class); + $capture + ->shouldReceive('status') + ->andReturn(new CaptureStatus($status)); + return $capture; + } + private function createPaypalOrder(array $authorizations): Order { $payments = Mockery::mock(Payments::class); $payments From fb3c4f01012011457bfcefcdd6b5cf29ca5fda5a Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 7 Oct 2021 10:59:13 +0300 Subject: [PATCH 13/13] Quick fix handling of capture statuses after capturing via order action --- .../src/Gateway/class-paypalgateway.php | 27 ++++++++++++------- .../class-authorizedpaymentsprocessor.php | 23 +++++++++++++++- .../WcGateway/Gateway/WcGatewayTest.php | 12 +++++++++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php index f16396e06..935d1441b 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php @@ -253,16 +253,6 @@ class PayPalGateway extends \WC_Payment_Gateway { $result_status = $this->authorized_payments->process( $wc_order ); $this->render_authorization_message_for_status( $result_status ); - if ( AuthorizedPaymentsProcessor::SUCCESSFUL === $result_status ) { - $wc_order->add_order_note( - __( 'Payment successfully captured.', 'woocommerce-paypal-payments' ) - ); - $wc_order->update_meta_data( self::CAPTURED_META_KEY, 'true' ); - $wc_order->save(); - $wc_order->payment_complete(); - return true; - } - if ( AuthorizedPaymentsProcessor::ALREADY_CAPTURED === $result_status ) { if ( $wc_order->get_status() === 'on-hold' ) { $wc_order->add_order_note( @@ -275,6 +265,23 @@ class PayPalGateway extends \WC_Payment_Gateway { $wc_order->payment_complete(); return true; } + + $captures = $this->authorized_payments->captures(); + if ( empty( $captures ) ) { + return false; + } + + $this->handle_capture_status( end( $captures ), $wc_order ); + + if ( AuthorizedPaymentsProcessor::SUCCESSFUL === $result_status ) { + $wc_order->add_order_note( + __( 'Payment successfully captured.', 'woocommerce-paypal-payments' ) + ); + $wc_order->update_meta_data( self::CAPTURED_META_KEY, 'true' ); + $wc_order->save(); + return true; + } + return false; } diff --git a/modules/ppcp-wc-gateway/src/Processor/class-authorizedpaymentsprocessor.php b/modules/ppcp-wc-gateway/src/Processor/class-authorizedpaymentsprocessor.php index 153e8ebbc..7e19cd85f 100644 --- a/modules/ppcp-wc-gateway/src/Processor/class-authorizedpaymentsprocessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/class-authorizedpaymentsprocessor.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; +use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -23,6 +24,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; */ class AuthorizedPaymentsProcessor { + use PaymentsStatusHandlingTrait; + const SUCCESSFUL = 'SUCCESSFUL'; const ALREADY_CAPTURED = 'ALREADY_CAPTURED'; const FAILED = 'FAILED'; @@ -51,6 +54,13 @@ class AuthorizedPaymentsProcessor { */ private $logger; + /** + * The capture results. + * + * @var Capture[] + */ + private $captures; + /** * AuthorizedPaymentsProcessor constructor. * @@ -77,6 +87,8 @@ class AuthorizedPaymentsProcessor { * @return string One of the AuthorizedPaymentsProcessor status constants. */ public function process( \WC_Order $wc_order ): string { + $this->captures = array(); + try { $order = $this->paypal_order_from_wc_order( $wc_order ); } catch ( Exception $exception ) { @@ -106,6 +118,15 @@ class AuthorizedPaymentsProcessor { return self::SUCCESSFUL; } + /** + * Returns the capture results. + * + * @return Capture[] + */ + public function captures(): array { + return $this->captures; + } + /** * Returns the PayPal order from a given WooCommerce order. * @@ -144,7 +165,7 @@ class AuthorizedPaymentsProcessor { private function capture_authorizations( Authorization ...$authorizations ) { $uncaptured_authorizations = $this->authorizations_to_capture( ...$authorizations ); foreach ( $uncaptured_authorizations as $authorization ) { - $this->payments_endpoint->capture( $authorization->id() ); + $this->captures[] = $this->payments_endpoint->capture( $authorization->id() ); } } diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index 9c3be9b76..8d3ad40e5 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -5,6 +5,8 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Psr\Container\ContainerInterface; +use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; +use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; @@ -234,11 +236,18 @@ class WcGatewayTest extends TestCase ->expects('save'); $settingsRenderer = Mockery::mock(SettingsRenderer::class); $orderProcessor = Mockery::mock(OrderProcessor::class); + $capture = Mockery::mock(Capture::class); + $capture + ->shouldReceive('status') + ->andReturn(new CaptureStatus(CaptureStatus::COMPLETED)); $authorizedPaymentsProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); $authorizedPaymentsProcessor ->expects('process') ->with($wcOrder) ->andReturn(AuthorizedPaymentsProcessor::SUCCESSFUL); + $authorizedPaymentsProcessor + ->expects('captures') + ->andReturn([$capture]); $authorizedOrderActionNotice = Mockery::mock(AuthorizeOrderActionNotice::class); $authorizedOrderActionNotice ->expects('display_message') @@ -346,6 +355,9 @@ class WcGatewayTest extends TestCase ->expects('process') ->with($wcOrder) ->andReturn($lastStatus); + $authorizedPaymentsProcessor + ->expects('captures') + ->andReturn([]); $authorizedOrderActionNotice = Mockery::mock(AuthorizeOrderActionNotice::class); $authorizedOrderActionNotice ->expects('display_message')