From c748c145db768672fd3bf4209c9379082b44d91f Mon Sep 17 00:00:00 2001 From: David Remer Date: Fri, 18 Sep 2020 11:55:25 +0300 Subject: [PATCH 01/14] check in approve order endpoint whether card is disabled and return error message accordingly. --- .../OnApproveHandler/onApproveForPayNow.js | 6 +++++- modules/ppcp-button/services.php | 7 ++++--- .../src/Assets/class-smartbutton.php | 2 +- .../Endpoint/class-approveorderendpoint.php | 19 ++++++++++++++++++- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js index 61e9bcb9a..845b25ecc 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js @@ -10,7 +10,11 @@ const onApprove = (context, errorHandler) => { return res.json(); }).then((data)=>{ if (!data.success) { - errorHandler.genericError(); + if (data.data.code === 100) { + errorHandler.message(data.data.message); + } else { + errorHandler.genericError(); + } console.error(data); if (typeof actions.restart !== 'undefined') { return actions.restart(); diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index a4d288577..4bab9ceb3 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -134,10 +134,11 @@ return array( }, 'button.endpoint.approve-order' => static function ( $container ): ApproveOrderEndpoint { $request_data = $container->get( 'button.request-data' ); - $order_endpoint = $container->get( 'api.endpoint.order' ); + $order_endpoint = $container->get( 'api.endpoint.order' ); $session_handler = $container->get( 'session.handler' ); - $three_d_secure = $container->get( 'button.helper.three-d-secure' ); - return new ApproveOrderEndpoint( $request_data, $order_endpoint, $session_handler, $three_d_secure ); + $three_d_secure = $container->get( 'button.helper.three-d-secure' ); + $settings = $container->get( 'wcgateway.settings' ); + return new ApproveOrderEndpoint( $request_data, $order_endpoint, $session_handler, $three_d_secure, $settings ); }, 'button.endpoint.data-client-id' => static function( $container ) : DataClientIdEndpoint { $request_data = $container->get( 'button.request-data' ); diff --git a/modules/ppcp-button/src/Assets/class-smartbutton.php b/modules/ppcp-button/src/Assets/class-smartbutton.php index 524b4e1d8..16a035a96 100644 --- a/modules/ppcp-button/src/Assets/class-smartbutton.php +++ b/modules/ppcp-button/src/Assets/class-smartbutton.php @@ -684,8 +684,8 @@ class SmartButton implements SmartButtonInterface { if ( 'GB' === $country ) { $disable_funding[] = 'card'; } - $params['disable-funding'] = implode( ',', array_unique( $disable_funding ) ); + $smart_button_url = add_query_arg( $params, 'https://www.paypal.com/sdk/js' ); return $smart_button_url; } diff --git a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php index 7d4eccfaa..e02f75797 100644 --- a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php +++ b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php @@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; /** * Class ApproveOrderEndpoint @@ -53,6 +54,8 @@ class ApproveOrderEndpoint implements EndpointInterface { */ private $threed_secure; + private $settings; + /** * ApproveOrderEndpoint constructor. * @@ -65,13 +68,15 @@ class ApproveOrderEndpoint implements EndpointInterface { RequestData $request_data, OrderEndpoint $order_endpoint, SessionHandler $session_handler, - ThreeDSecure $three_d_secure + ThreeDSecure $three_d_secure, + Settings $settings ) { $this->request_data = $request_data; $this->api_endpoint = $order_endpoint; $this->session_handler = $session_handler; $this->threed_secure = $three_d_secure; + $this->settings = $settings; } /** @@ -110,6 +115,18 @@ class ApproveOrderEndpoint implements EndpointInterface { } if ( $order->payment_source() && $order->payment_source()->card() ) { + if ( + $this->settings->has('disable_cards') + && in_array( strtolower($order->payment_source()->card()->brand()), (array) $this->settings->get( 'disable_cards' ), true ) + ) { + throw new RuntimeException( + __( + 'Unfortunately, we do not accept this card.', + 'paypal-payments-for-woocommerce' + ), + 100 + ); + } $proceed = $this->threed_secure->proceed_with_order( $order ); if ( ThreeDSecure::RETRY === $proceed ) { throw new RuntimeException( From 8478a7e18a9eb090ad7b6ca5d262dab452ffc678 Mon Sep 17 00:00:00 2001 From: David Remer Date: Fri, 18 Sep 2020 12:02:27 +0300 Subject: [PATCH 02/14] phpcs --- modules/ppcp-button/src/Assets/class-smartbutton.php | 2 +- .../src/Endpoint/class-approveorderendpoint.php | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-button/src/Assets/class-smartbutton.php b/modules/ppcp-button/src/Assets/class-smartbutton.php index 16a035a96..56b8c23df 100644 --- a/modules/ppcp-button/src/Assets/class-smartbutton.php +++ b/modules/ppcp-button/src/Assets/class-smartbutton.php @@ -686,7 +686,7 @@ class SmartButton implements SmartButtonInterface { } $params['disable-funding'] = implode( ',', array_unique( $disable_funding ) ); - $smart_button_url = add_query_arg( $params, 'https://www.paypal.com/sdk/js' ); + $smart_button_url = add_query_arg( $params, 'https://www.paypal.com/sdk/js' ); return $smart_button_url; } diff --git a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php index e02f75797..c0a3ad5e6 100644 --- a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php +++ b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php @@ -54,6 +54,11 @@ class ApproveOrderEndpoint implements EndpointInterface { */ private $threed_secure; + /** + * The settings. + * + * @var Settings + */ private $settings; /** @@ -63,6 +68,7 @@ class ApproveOrderEndpoint implements EndpointInterface { * @param OrderEndpoint $order_endpoint The order endpoint. * @param SessionHandler $session_handler The session handler. * @param ThreeDSecure $three_d_secure The 3d secure helper object. + * @param Settings $settings The settings. */ public function __construct( RequestData $request_data, @@ -116,8 +122,8 @@ class ApproveOrderEndpoint implements EndpointInterface { if ( $order->payment_source() && $order->payment_source()->card() ) { if ( - $this->settings->has('disable_cards') - && in_array( strtolower($order->payment_source()->card()->brand()), (array) $this->settings->get( 'disable_cards' ), true ) + $this->settings->has( 'disable_cards' ) + && in_array( strtolower( $order->payment_source()->card()->brand() ), (array) $this->settings->get( 'disable_cards' ), true ) ) { throw new RuntimeException( __( From d6abaafadbcdbac8aef87b8bb54fd9b5eb1936cc Mon Sep 17 00:00:00 2001 From: David Remer Date: Mon, 28 Sep 2020 11:47:24 +0300 Subject: [PATCH 03/14] enable refunds through gateway --- modules/ppcp-api-client/services.php | 9 +- .../src/Endpoint/class-paymentsendpoint.php | 60 ++++++ .../src/Entity/class-capture.php | 197 ++++++++++++++++++ .../src/Entity/class-payments.php | 40 +++- .../src/Entity/class-refund.php | 118 +++++++++++ .../src/Factory/class-capturefactory.php | 56 +++++ .../src/Factory/class-paymentsfactory.php | 23 +- modules/ppcp-wc-gateway/services.php | 22 +- .../src/Gateway/class-creditcardgateway.php | 41 +++- .../src/Gateway/class-paypalgateway.php | 40 +++- .../src/Processor/class-refundprocessor.php | 85 ++++++++ .../PHPUnit/ApiClient/Entity/PaymentsTest.php | 39 +++- .../ApiClient/Factory/PaymentsFactoryTest.php | 32 ++- .../WcGateway/Gateway/WcGatewayTest.php | 50 ++++- 14 files changed, 775 insertions(+), 37 deletions(-) create mode 100644 modules/ppcp-api-client/src/Entity/class-capture.php create mode 100644 modules/ppcp-api-client/src/Entity/class-refund.php create mode 100644 modules/ppcp-api-client/src/Factory/class-capturefactory.php create mode 100644 modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 199b14e78..1b3174ac8 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -23,6 +23,7 @@ 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\CaptureFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory; @@ -214,6 +215,11 @@ return array( 'api.factory.webhook' => static function ( $container ): WebhookFactory { return new WebhookFactory(); }, + 'api.factory.capture' => static function ( $container ): CaptureFactory { + + $amount_factory = $container->get( 'api.factory.amount' ); + return new CaptureFactory( $amount_factory ); + }, 'api.factory.purchase-unit' => static function ( $container ): PurchaseUnitFactory { $amount_factory = $container->get( 'api.factory.amount' ); @@ -277,7 +283,8 @@ return array( }, 'api.factory.payments' => static function ( $container ): PaymentsFactory { $authorizations_factory = $container->get( 'api.factory.authorization' ); - return new PaymentsFactory( $authorizations_factory ); + $capture_factory = $container->get( 'api.factory.capture' ); + return new PaymentsFactory( $authorizations_factory, $capture_factory ); }, 'api.factory.authorization' => static function ( $container ): AuthorizationFactory { return new AuthorizationFactory(); diff --git a/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php b/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php index 7ee7daaa3..4011668c1 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/class-paymentsendpoint.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; @@ -188,4 +189,63 @@ class PaymentsEndpoint { $authorization = $this->authorizations_factory->from_paypal_response( $json ); return $authorization; } + + /** + * Refunds a payment. + * + * @param Refund $refund The refund to be processed. + * + * @return bool + * @throws RuntimeException If the request fails. + */ + public function refund( Refund $refund ) : bool { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v2/payments/captures/' . $refund->for_capture()->id() . '/refund'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + 'body' => wp_json_encode( $refund->to_array() ), + ); + + $response = $this->request( $url, $args ); + $json = json_decode( $response['body'] ); + + if ( is_wp_error( $response ) ) { + $error = new RuntimeException( + __( 'Could not refund payment.', 'paypal-payments-for-woocommerce' ) + ); + $this->logger->log( + 'warning', + $error->getMessage(), + array( + 'args' => $args, + 'response' => $response, + ) + ); + throw $error; + } + + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 201 !== $status_code ) { + $error = new PayPalApiException( + $json, + $status_code + ); + $this->logger->log( + 'warning', + $error->getMessage(), + array( + 'args' => $args, + 'response' => $response, + ) + ); + throw $error; + } + + return true; + } } diff --git a/modules/ppcp-api-client/src/Entity/class-capture.php b/modules/ppcp-api-client/src/Entity/class-capture.php new file mode 100644 index 000000000..9aad9e029 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/class-capture.php @@ -0,0 +1,197 @@ +id = $id; + $this->status = $status; + $this->status_details = $status_details; + $this->amount = $amount; + $this->final_capture = $final_capture; + $this->seller_protection = $seller_protection; + $this->invoice_id = $invoice_id; + $this->custom_id = $custom_id; + } + + /** + * Returns the ID. + * + * @return string + */ + public function id() : string { + return $this->id; + } + + /** + * Returns the status. + * + * @return string + */ + public function status() : string { + 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. + * + * @return Amount + */ + public function amount() : Amount { + return $this->amount; + } + + /** + * Returns whether this is the final capture or not. + * + * @return bool + */ + public function final_capture() : bool { + return $this->final_capture; + } + + /** + * Returns the seller protection object. + * + * @return \stdClass + */ + public function seller_protection() : \stdClass { + return (object) array( 'status' => $this->seller_protection ); + } + + /** + * 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 entity as array. + * + * @return array + */ + public function to_array() : array { + return array( + 'id' => $this->id(), + 'status' => $this->status(), + 'status_details' => (array) $this->status_details(), + '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(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Entity/class-payments.php b/modules/ppcp-api-client/src/Entity/class-payments.php index 75febd6b6..d5e051f88 100644 --- a/modules/ppcp-api-client/src/Entity/class-payments.php +++ b/modules/ppcp-api-client/src/Entity/class-payments.php @@ -21,13 +21,34 @@ class Payments { */ private $authorizations; + /** + * The Captures. + * + * @var Capture[] + */ + private $captures; + /** * Payments constructor. * - * @param Authorization ...$authorizations The Authorizations. + * @param array $authorizations The Authorizations. + * @param array $captures The Captures. */ - public function __construct( Authorization ...$authorizations ) { + public function __construct( array $authorizations, array $captures ) { + foreach ( $authorizations as $key => $authorization ) { + if ( is_a( $authorization, Authorization::class ) ) { + continue; + } + unset( $authorizations[ $key ] ); + } + foreach ( $captures as $key => $capture ) { + if ( is_a( $capture, Capture::class ) ) { + continue; + } + unset( $captures[ $key ] ); + } $this->authorizations = $authorizations; + $this->captures = $captures; } /** @@ -43,6 +64,12 @@ class Payments { }, $this->authorizations() ), + 'captures' => array_map( + static function ( Capture $capture ): array { + return $capture->to_array(); + }, + $this->captures() + ), ); } @@ -54,4 +81,13 @@ class Payments { public function authorizations(): array { return $this->authorizations; } + + /** + * Returns the Captures. + * + * @return Capture[] + **/ + public function captures(): array { + return $this->captures; + } } diff --git a/modules/ppcp-api-client/src/Entity/class-refund.php b/modules/ppcp-api-client/src/Entity/class-refund.php new file mode 100644 index 000000000..71c2a2f56 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/class-refund.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/Factory/class-capturefactory.php b/modules/ppcp-api-client/src/Factory/class-capturefactory.php new file mode 100644 index 000000000..01e0ff29d --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/class-capturefactory.php @@ -0,0 +1,56 @@ +amount_factory = $amount_factory; + } + + /** + * Returns the capture object based off the PayPal response. + * + * @param \stdClass $data The PayPal response. + * + * @return Capture + */ + public function from_paypal_response( \stdClass $data ) : Capture { + + return new Capture( + (string) $data->id, + (string) $data->status, + (string) $data->status_details->reason, + $this->amount_factory->from_paypal_response( $data->amount ), + (bool) $data->final_capture, + (string) $data->seller_protection->status, + (string) $data->invoice_id, + (string) $data->custom_id + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/class-paymentsfactory.php b/modules/ppcp-api-client/src/Factory/class-paymentsfactory.php index 241fc57eb..353c71f44 100644 --- a/modules/ppcp-api-client/src/Factory/class-paymentsfactory.php +++ b/modules/ppcp-api-client/src/Factory/class-paymentsfactory.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Factory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; +use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; /** @@ -24,16 +25,26 @@ class PaymentsFactory { */ private $authorization_factory; + /** + * The Capture factory. + * + * @var CaptureFactory + */ + private $capture_factory; + /** * PaymentsFactory constructor. * - * @param AuthorizationFactory $authorization_factory The AuthorizationFactory. + * @param AuthorizationFactory $authorization_factory The Authorization factory. + * @param CaptureFactory $capture_factory The Capture factory. */ public function __construct( - AuthorizationFactory $authorization_factory + AuthorizationFactory $authorization_factory, + CaptureFactory $capture_factory ) { $this->authorization_factory = $authorization_factory; + $this->capture_factory = $capture_factory; } /** @@ -50,7 +61,13 @@ class PaymentsFactory { }, isset( $data->authorizations ) ? $data->authorizations : array() ); - $payments = new Payments( ...$authorizations ); + $captures = array_map( + function ( \stdClass $authorization ): Capture { + return $this->capture_factory->from_paypal_response( $authorization ); + }, + isset( $data->captures ) ? $data->captures : array() + ); + $payments = new Payments( $authorizations, $captures ); return $payments; } } diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 6928768ca..f0b3dd753 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -25,6 +25,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener; @@ -36,9 +37,11 @@ return array( $order_processor = $container->get( 'wcgateway.order-processor' ); $settings_renderer = $container->get( 'wcgateway.settings.render' ); $authorized_payments = $container->get( 'wcgateway.processor.authorized-payments' ); - $notice = $container->get( 'wcgateway.notice.authorize-order-action' ); - $settings = $container->get( 'wcgateway.settings' ); + $notice = $container->get( 'wcgateway.notice.authorize-order-action' ); + $settings = $container->get( 'wcgateway.settings' ); $session_handler = $container->get( 'session.handler' ); + $refund_processor = $container->get( 'wcgateway.processor.refunds' ); + $state = $container->get( 'onboarding.state' ); return new PayPalGateway( $settings_renderer, @@ -46,7 +49,9 @@ return array( $authorized_payments, $notice, $settings, - $session_handler + $session_handler, + $refund_processor, + $state ); }, 'wcgateway.credit-card-gateway' => static function ( $container ): CreditCardGateway { @@ -57,6 +62,8 @@ return array( $settings = $container->get( 'wcgateway.settings' ); $module_url = $container->get( 'wcgateway.url' ); $session_handler = $container->get( 'session.handler' ); + $refund_processor = $container->get( 'wcgateway.processor.refunds' ); + $state = $container->get( 'onboarding.state' ); return new CreditCardGateway( $settings_renderer, $order_processor, @@ -64,7 +71,9 @@ return array( $notice, $settings, $module_url, - $session_handler + $session_handler, + $refund_processor, + $state ); }, 'wcgateway.disabler' => static function ( $container ): DisableGateways { @@ -132,6 +141,11 @@ return array( $settings ); }, + 'wcgateway.processor.refunds' => static function ( $container ): RefundProcessor { + $order_endpoint = $container->get( 'api.endpoint.order' ); + $payments_endpoint = $container->get( 'api.endpoint.payments' ); + return new RefundProcessor( $order_endpoint, $payments_endpoint ); + }, 'wcgateway.processor.authorized-payments' => static function ( $container ): AuthorizedPaymentsProcessor { $order_endpoint = $container->get( 'api.endpoint.order' ); $payments_endpoint = $container->get( 'api.endpoint.payments' ); diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php index e2888a2e1..08ca72ce6 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php @@ -9,10 +9,12 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; +use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use Psr\Container\ContainerInterface; @@ -32,6 +34,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { */ private $module_url; + /** + * The refund processor. + * + * @var RefundProcessor + */ + private $refund_processor; + /** * CreditCardGateway constructor. * @@ -42,6 +51,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { * @param ContainerInterface $config The settings. * @param string $module_url The URL to the module. * @param SessionHandler $session_handler The Session Handler. + * @param RefundProcessor $refund_processor The refund processor. + * @param State $state The state. */ public function __construct( SettingsRenderer $settings_renderer, @@ -50,7 +61,9 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { AuthorizeOrderActionNotice $notice, ContainerInterface $config, string $module_url, - SessionHandler $session_handler + SessionHandler $session_handler, + RefundProcessor $refund_processor, + State $state ) { $this->id = self::ID; @@ -60,6 +73,11 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { $this->settings_renderer = $settings_renderer; $this->config = $config; $this->session_handler = $session_handler; + $this->refund_processor = $refund_processor; + + if ( $state->current_state() === State::STATE_ONBOARDED ) { + $this->supports = array( 'refunds' ); + } if ( defined( 'PPCP_FLAG_SUBSCRIPTION' ) && PPCP_FLAG_SUBSCRIPTION @@ -67,6 +85,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { && $this->config->get( 'vault_enabled' ) ) { $this->supports = array( + 'refunds', 'products', 'subscriptions', 'subscription_cancellation', @@ -209,4 +228,24 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { public function is_available() : bool { return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' ); } + + + /** + * Process refund. + * + * If the gateway declares 'refunds' support, this will allow it to refund. + * a passed in amount. + * + * @param int $order_id Order ID. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * @return boolean True or false based on success, or a WP_Error object. + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + $order = wc_get_order( $order_id ); + if ( ! is_a( $order, \WC_Order::class ) ) { + return false; + } + return $this->refund_processor->process( $order, (float) $amount, (string) $reason ); + } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php index 6bb72fa96..6d2bb96ee 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php @@ -10,10 +10,12 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use Psr\Container\ContainerInterface; @@ -72,6 +74,13 @@ class PayPalGateway extends \WC_Payment_Gateway { */ protected $session_handler; + /** + * The Refund Processor. + * + * @var RefundProcessor + */ + private $refund_processor; + /** * PayPalGateway constructor. * @@ -81,6 +90,8 @@ class PayPalGateway extends \WC_Payment_Gateway { * @param AuthorizeOrderActionNotice $notice The Order Action Notice object. * @param ContainerInterface $config The settings. * @param SessionHandler $session_handler The Session Handler. + * @param RefundProcessor $refund_processor The Refund Processor. + * @param State $state The state. */ public function __construct( SettingsRenderer $settings_renderer, @@ -88,7 +99,9 @@ class PayPalGateway extends \WC_Payment_Gateway { AuthorizedPaymentsProcessor $authorized_payments_processor, AuthorizeOrderActionNotice $notice, ContainerInterface $config, - SessionHandler $session_handler + SessionHandler $session_handler, + RefundProcessor $refund_processor, + State $state ) { $this->id = self::ID; @@ -98,6 +111,11 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->settings_renderer = $settings_renderer; $this->config = $config; $this->session_handler = $session_handler; + $this->refund_processor = $refund_processor; + + if ( $state->current_state() === State::STATE_ONBOARDED ) { + $this->supports = array( 'refunds' ); + } if ( defined( 'PPCP_FLAG_SUBSCRIPTION' ) && PPCP_FLAG_SUBSCRIPTION @@ -105,6 +123,7 @@ class PayPalGateway extends \WC_Payment_Gateway { && $this->config->get( 'vault_enabled' ) ) { $this->supports = array( + 'refunds', 'products', 'subscriptions', 'subscription_cancellation', @@ -300,4 +319,23 @@ class PayPalGateway extends \WC_Payment_Gateway { && self::ID === sanitize_text_field( wp_unslash( $_GET['section'] ) ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended + + /** + * Process refund. + * + * If the gateway declares 'refunds' support, this will allow it to refund. + * a passed in amount. + * + * @param int $order_id Order ID. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * @return boolean True or false based on success, or a WP_Error object. + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + $order = wc_get_order( $order_id ); + if ( ! is_a( $order, \WC_Order::class ) ) { + return false; + } + return $this->refund_processor->process( $order, (float) $amount, (string) $reason ); + } } diff --git a/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php b/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php new file mode 100644 index 000000000..e52c8eb76 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php @@ -0,0 +1,85 @@ +order_endpoint = $order_endpoint; + $this->payments_endpoint = $payments_endpoint; + } + + /** + * Processes a refund. + * + * @param \WC_Order $wc_order The WooCommerce order. + * @param float|null $amount The refund amount. + * @param string $reason The reason for the refund. + * + * @return bool + */ + public function process( \WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool { + $order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ); + if ( ! $order_id ) { + return false; + } + try { + $order = $this->order_endpoint->order( $order_id ); + if ( ! $order ) { + return false; + } + + $capture = $order->purchase_units()[0]->payments()->captures()[0]; + $refund = new Refund( + $capture, + $capture->invoice_id(), + $reason, + new Amount( + new Money( $amount, get_woocommerce_currency() ) + ) + ); + return $this->payments_endpoint->refund( $refund ); + } catch ( RuntimeException $error ) { + return false; + } + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php b/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php index a1062794d..adfa7798d 100644 --- a/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php +++ b/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php @@ -8,15 +8,24 @@ use WooCommerce\PayPalCommerce\ApiClient\TestCase; class PaymentsTest extends TestCase { - public function testAuthorizations() - { - $authorization = \Mockery::mock(Authorization::class); - $authorizations = [$authorization]; + public function testAuthorizations() + { + $authorization = \Mockery::mock(Authorization::class); + $authorizations = [$authorization]; - $testee = new Payments(...$authorizations); + $testee = new Payments($authorizations, []); - $this->assertEquals($authorizations, $testee->authorizations()); - } + $this->assertEquals($authorizations, $testee->authorizations()); + } + public function testCaptures() + { + $capture = \Mockery::mock(Capture::class); + $captures = [$capture]; + + $testee = new Payments([], $captures); + + $this->assertEquals($captures, $testee->captures()); + } public function testToArray() { @@ -27,9 +36,17 @@ class PaymentsTest extends TestCase 'status' => 'CREATED', ] ); + $capture = \Mockery::mock(Capture::class); + $capture->shouldReceive('to_array')->andReturn( + [ + 'id' => 'capture', + 'status' => 'CREATED', + ] + ); + $captures = [$capture]; $authorizations = [$authorization]; - $testee = new Payments(...$authorizations); + $testee = new Payments($authorizations, $captures); $this->assertEquals( [ @@ -39,6 +56,12 @@ class PaymentsTest extends TestCase 'status' => 'CREATED', ], ], + 'captures' => [ + [ + 'id' => 'capture', + 'status' => 'CREATED', + ], + ], ], $testee->to_array() ); diff --git a/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php index fe77baffc..639d940a4 100644 --- a/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php +++ b/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); 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\TestCase; use Mockery; @@ -13,27 +14,36 @@ class PaymentsFactoryTest extends TestCase { public function testFromPayPalResponse() { - $authorization = Mockery::mock(Authorization::class); - $authorization->shouldReceive('to_array')->andReturn(['id' => 'foo', 'status' => 'CREATED']); + $authorization = Mockery::mock(Authorization::class); + $authorization->shouldReceive('to_array')->andReturn(['id' => 'foo', 'status' => 'CREATED']); + $capture = Mockery::mock(Capture::class); + $capture->shouldReceive('to_array')->andReturn(['id' => 'capture', 'status' => 'CREATED']); $authorizationsFactory = Mockery::mock(AuthorizationFactory::class); - $authorizationsFactory->shouldReceive('from_paypal_response')->andReturn($authorization); - + $authorizationsFactory->shouldReceive('from_paypal_response')->andReturn($authorization); + $captureFactory = Mockery::mock(CaptureFactory::class); + $captureFactory->shouldReceive('from_paypal_response')->andReturn($capture); $response = (object)[ - 'authorizations' => [ - (object)['id' => 'foo', 'status' => 'CREATED'], - ], + 'authorizations' => [ + (object)['id' => 'foo', 'status' => 'CREATED'], + ], + 'captures' => [ + (object)['id' => 'capture', 'status' => 'CREATED'], + ], ]; - $testee = new PaymentsFactory($authorizationsFactory); + $testee = new PaymentsFactory($authorizationsFactory, $captureFactory); $result = $testee->from_paypal_response($response); $this->assertInstanceOf(Payments::class, $result); $expectedToArray = [ - 'authorizations' => [ - ['id' => 'foo', 'status' => 'CREATED'], - ], + 'authorizations' => [ + ['id' => 'foo', 'status' => 'CREATED'], + ], + 'captures' => [ + ['id' => 'capture', 'status' => 'CREATED'], + ], ]; $this->assertEquals($expectedToArray, $result->to_array()); } diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index 0780e6f15..102069ef7 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -5,11 +5,13 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; +use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\TestCase; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsFields; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; @@ -42,13 +44,19 @@ class WcGatewayTest extends TestCase ->shouldReceive('destroy_session_data'); $settings ->shouldReceive('has')->andReturnFalse(); + $refundProcessor = Mockery::mock(RefundProcessor::class); + $state = Mockery::mock(State::class); + $state + ->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); $testee = new PayPalGateway( $settingsRenderer, $orderProcessor, $authorizedPaymentsProcessor, $authorizedOrderActionNotice, $settings, - $sessionHandler + $sessionHandler, + $refundProcessor, + $state ); expect('wc_get_order') @@ -76,13 +84,19 @@ class WcGatewayTest extends TestCase $settings ->shouldReceive('has')->andReturnFalse(); $sessionHandler = Mockery::mock(SessionHandler::class); + $refundProcessor = Mockery::mock(RefundProcessor::class); + $state = Mockery::mock(State::class); + $state + ->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); $testee = new PayPalGateway( $settingsRenderer, $orderProcessor, $authorizedPaymentsProcessor, $authorizedOrderActionNotice, $settings, - $sessionHandler + $sessionHandler, + $refundProcessor, + $state ); expect('wc_get_order') @@ -116,13 +130,19 @@ class WcGatewayTest extends TestCase $settings ->shouldReceive('has')->andReturnFalse(); $sessionHandler = Mockery::mock(SessionHandler::class); + $refundProcessor = Mockery::mock(RefundProcessor::class); + $state = Mockery::mock(State::class); + $state + ->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); $testee = new PayPalGateway( $settingsRenderer, $orderProcessor, $authorizedPaymentsProcessor, $authorizedOrderActionNotice, $settings, - $sessionHandler + $sessionHandler, + $refundProcessor, + $state ); expect('wc_get_order') @@ -171,13 +191,19 @@ class WcGatewayTest extends TestCase $settings ->shouldReceive('has')->andReturnFalse(); $sessionHandler = Mockery::mock(SessionHandler::class); + $refundProcessor = Mockery::mock(RefundProcessor::class); + $state = Mockery::mock(State::class); + $state + ->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); $testee = new PayPalGateway( $settingsRenderer, $orderProcessor, $authorizedPaymentsProcessor, $authorizedOrderActionNotice, $settings, - $sessionHandler + $sessionHandler, + $refundProcessor, + $state ); $this->assertTrue($testee->capture_authorized_payment($wcOrder)); @@ -218,13 +244,19 @@ class WcGatewayTest extends TestCase $settings ->shouldReceive('has')->andReturnFalse(); $sessionHandler = Mockery::mock(SessionHandler::class); + $refundProcessor = Mockery::mock(RefundProcessor::class); + $state = Mockery::mock(State::class); + $state + ->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); $testee = new PayPalGateway( $settingsRenderer, $orderProcessor, $authorizedPaymentsProcessor, $authorizedOrderActionNotice, $settings, - $sessionHandler + $sessionHandler, + $refundProcessor, + $state ); $this->assertTrue($testee->capture_authorized_payment($wcOrder)); @@ -258,13 +290,19 @@ class WcGatewayTest extends TestCase $settings ->shouldReceive('has')->andReturnFalse(); $sessionHandler = Mockery::mock(SessionHandler::class); + $refundProcessor = Mockery::mock(RefundProcessor::class); + $state = Mockery::mock(State::class); + $state + ->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); $testee = new PayPalGateway( $settingsRenderer, $orderProcessor, $authorizedPaymentsProcessor, $authorizedOrderActionNotice, $settings, - $sessionHandler + $sessionHandler, + $refundProcessor, + $state ); $this->assertFalse($testee->capture_authorized_payment($wcOrder)); From fed6df25786253eb51b6d2dd81163ca69da96b2d Mon Sep 17 00:00:00 2001 From: David Remer Date: Mon, 28 Sep 2020 12:13:19 +0300 Subject: [PATCH 04/14] map master_card to mastercard --- .../Endpoint/class-approveorderendpoint.php | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php index c0a3ad5e6..dc819f1e2 100644 --- a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php +++ b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php @@ -121,17 +121,21 @@ class ApproveOrderEndpoint implements EndpointInterface { } if ( $order->payment_source() && $order->payment_source()->card() ) { - if ( - $this->settings->has( 'disable_cards' ) - && in_array( strtolower( $order->payment_source()->card()->brand() ), (array) $this->settings->get( 'disable_cards' ), true ) - ) { - throw new RuntimeException( - __( - 'Unfortunately, we do not accept this card.', - 'paypal-payments-for-woocommerce' - ), - 100 - ); + if ( $this->settings->has( 'disable_cards' ) ) { + $disabled_cards = (array) $this->settings->get( 'disable_cards' ); + if ( in_array( 'mastercard', $disabled_cards, true ) ) { + $disabled_cards[] = 'master_card'; + } + $card = strtolower( $order->payment_source()->card()->brand() ); + if ( in_array( $card, $disabled_cards, true ) ) { + throw new RuntimeException( + __( + 'Unfortunately, we do not accept this card.', + 'paypal-payments-for-woocommerce' + ), + 100 + ); + } } $proceed = $this->threed_secure->proceed_with_order( $order ); if ( ThreeDSecure::RETRY === $proceed ) { From 4322386c8586f120ad9169fc03b30b5c1e4f9a9e Mon Sep 17 00:00:00 2001 From: David Remer Date: Mon, 28 Sep 2020 14:05:02 +0300 Subject: [PATCH 05/14] activate spinner when credit card button is clicked --- modules/ppcp-button/resources/js/button.js | 7 +++++-- .../ActionHandler/CheckoutActionHandler.js | 7 +++++-- .../ContextBootstrap/CheckoutBootstap.js | 6 ++++-- .../resources/js/modules/Helper/Spinner.js | 20 +++++++++++++++++++ .../OnApproveHandler/onApproveForPayNow.js | 5 +++-- .../js/modules/Renderer/CreditCardRenderer.js | 6 +++++- 6 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 modules/ppcp-button/resources/js/modules/Helper/Spinner.js diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index f52b8127d..3c96cd5b6 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -7,10 +7,12 @@ import ErrorHandler from './modules/ErrorHandler'; import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer"; import dataClientIdAttributeHandler from "./modules/DataClientIdAttributeHandler"; import MessageRenderer from "./modules/Renderer/MessageRenderer"; +import Spinner from "./modules/Helper/Spinner"; const bootstrap = () => { const errorHandler = new ErrorHandler(PayPalCommerceGateway.labels.error.generic); - const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler); + const spinner = new Spinner(); + const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler, spinner); const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway); const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages); const context = PayPalCommerceGateway.context; @@ -46,7 +48,8 @@ const bootstrap = () => { const checkoutBootstap = new CheckoutBootstap( PayPalCommerceGateway, renderer, - messageRenderer + messageRenderer, + spinner ); checkoutBootstap.init(); diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index bd83ab1af..35cb281de 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -3,12 +3,14 @@ import {payerData} from "../Helper/PayerData"; class CheckoutActionHandler { - constructor(config, errorHandler) { + constructor(config, errorHandler, spinner) { this.config = config; this.errorHandler = errorHandler; + this.spinner = spinner; } configuration() { + const spinner = this.spinner; const createOrder = (data, actions) => { const payer = payerData(); const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ? @@ -31,6 +33,7 @@ class CheckoutActionHandler { return res.json(); }).then(function (data) { if (!data.success) { + spinner.unblock(); errorHandler.message(data.data.message, true); return; } @@ -44,7 +47,7 @@ class CheckoutActionHandler { } return { createOrder, - onApprove:onApprove(this, this.errorHandler), + onApprove:onApprove(this, this.errorHandler, this.spinner), onError: (error) => { this.errorHandler.genericError(); } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index de46e5759..85dfbcdc5 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -2,10 +2,11 @@ import ErrorHandler from '../ErrorHandler'; import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler'; class CheckoutBootstap { - constructor(gateway, renderer, messages) { + constructor(gateway, renderer, messages, spinner) { this.gateway = gateway; this.renderer = renderer; - this.messages = messages + this.messages = messages; + this.spinner = spinner; } init() { @@ -41,6 +42,7 @@ class CheckoutBootstap { const actionHandler = new CheckoutActionHandler( PayPalCommerceGateway, new ErrorHandler(this.gateway.labels.error.generic), + this.spinner ); this.renderer.render( diff --git a/modules/ppcp-button/resources/js/modules/Helper/Spinner.js b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js new file mode 100644 index 000000000..12790dcb1 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js @@ -0,0 +1,20 @@ +class Spinner { + + block() { + + jQuery( '.woocommerce-checkout-payment, .woocommerce-checkout-review-order-table' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + } + + unblock() { + + jQuery( '.woocommerce-checkout-payment, .woocommerce-checkout-review-order-table' ).unblock(); + } +} + +export default Spinner; \ No newline at end of file diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js index 61e9bcb9a..8dc13a7da 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js @@ -1,5 +1,6 @@ -const onApprove = (context, errorHandler) => { +const onApprove = (context, errorHandler, spinner) => { return (data, actions) => { + spinner.block(); return fetch(context.config.ajax.approve_order.endpoint, { method: 'POST', body: JSON.stringify({ @@ -9,9 +10,9 @@ const onApprove = (context, errorHandler) => { }).then((res)=>{ return res.json(); }).then((data)=>{ + spinner.unblock(); if (!data.success) { errorHandler.genericError(); - console.error(data); if (typeof actions.restart !== 'undefined') { return actions.restart(); } diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index 11e37418a..198b4ae5c 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -2,9 +2,10 @@ import dccInputFactory from "../Helper/DccInputFactory"; class CreditCardRenderer { - constructor(defaultConfig, errorHandler) { + constructor(defaultConfig, errorHandler, spinner) { this.defaultConfig = defaultConfig; this.errorHandler = errorHandler; + this.spinner = spinner; } render(wrapper, contextConfig) { @@ -83,6 +84,7 @@ class CreditCardRenderer { } }).then(hostedFields => { const submitEvent = (event) => { + this.spinner.block(); if (event) { event.preventDefault(); } @@ -103,9 +105,11 @@ class CreditCardRenderer { vault }).then((payload) => { payload.orderID = payload.orderId; + this.spinner.unblock(); return contextConfig.onApprove(payload); }); } else { + this.spinner.unblock(); this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid); } } From 82be8112714dfbaf02a105feb36de703a132907f Mon Sep 17 00:00:00 2001 From: David Remer Date: Tue, 29 Sep 2020 09:59:22 +0300 Subject: [PATCH 06/14] validate if credit card can be used in a country --- .../src/Helper/class-dccapplies.php | 237 +++++++++++++++++- .../js/modules/Renderer/CreditCardRenderer.js | 14 +- modules/ppcp-button/services.php | 11 +- .../src/Assets/class-smartbutton.php | 5 + .../Endpoint/class-approveorderendpoint.php | 22 +- modules/ppcp-wc-gateway/services.php | 20 ++ 6 files changed, 298 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-api-client/src/Helper/class-dccapplies.php b/modules/ppcp-api-client/src/Helper/class-dccapplies.php index ddc259646..166e6520b 100644 --- a/modules/ppcp-api-client/src/Helper/class-dccapplies.php +++ b/modules/ppcp-api-client/src/Helper/class-dccapplies.php @@ -38,6 +38,24 @@ class DccApplies { 'SGD', 'USD', ), + 'AT' => array( + 'AUD', + 'CAD', + 'CHF', + 'CZK', + 'DKK', + 'EUR', + 'GBP', + 'HKD', + 'HUF', + 'JPY', + 'NOK', + 'NZD', + 'PLN', + 'SEK', + 'SGD', + 'USD', + ), 'BE' => array( 'AUD', 'CAD', @@ -74,6 +92,24 @@ class DccApplies { 'SGD', 'USD', ), + 'CA' => array( + 'AUD', + 'CAD', + 'CHF', + 'CZK', + 'DKK', + 'EUR', + 'GBP', + 'HKD', + 'HUF', + 'JPY', + 'NOK', + 'NZD', + 'PLN', + 'SEK', + 'SGD', + 'USD', + ), 'CY' => array( 'AUD', 'CAD', @@ -517,14 +553,148 @@ class DccApplies { ); + /** + * Which countries support which credit cards. Empty credit card arrays mean no restriction on + * currency. Otherwise only the currencies in the array are supported. + * + * @var array + */ + private $country_card_matrix = array( + 'AU' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'AT' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'BE' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'CA' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'CAD' ), + 'jcb' => array( 'CAD' ), + ), + 'CY' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'CZ' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'DK' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'EE' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'FI' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'FR' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'GR' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'HU' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'IT' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'LV' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'LI' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'LT' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'LU' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'MT' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'NL' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'NO' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'PL' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'PT' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'RO' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'SK' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'SI' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'ES' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'SE' => array( + 'mastercard' => array(), + 'visa' => array(), + ), + 'US' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'USD' ), + 'discover' => array( 'USD' ), + ), + 'GB' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'GBP', 'USD' ), + ), + ); + /** * Returns whether DCC can be used in the current country and the current currency used. * * @return bool */ public function for_country_currency(): bool { - $region = wc_get_base_location(); - $country = $region['country']; + $country = $this->country(); $currency = get_woocommerce_currency(); if ( ! in_array( $country, array_keys( $this->allowed_country_currency_matrix ), true ) ) { return false; @@ -532,4 +702,67 @@ class DccApplies { $applies = in_array( $currency, $this->allowed_country_currency_matrix[ $country ], true ); return $applies; } + + /** + * Returns credit cards, which can be used. + * + * @return array + */ + public function valid_cards() : array { + $country = $this->country(); + $cards = array(); + if ( ! isset( $this->country_card_matrix[ $country ] ) ) { + return $cards; + } + + $supported_currencies = $this->country_card_matrix[ $country ]; + foreach ( $supported_currencies as $card => $currencies ) { + if ( $this->can_process_card( $card ) ) { + $cards[] = $card; + } + } + if ( in_array( 'amex', $cards, true ) ) { + $cards[] = 'american-express'; + } + if ( in_array( 'mastercard', $cards, true ) ) { + $cards[] = 'master-card'; + } + return $cards; + } + + /** + * Whether a card can be used or not. + * + * @param string $card The card. + * + * @return bool + */ + public function can_process_card( string $card ) : bool { + $country = $this->country(); + if ( ! isset( $this->country_card_matrix[ $country ] ) ) { + return false; + } + if ( ! isset( $this->country_card_matrix[ $country ][ $card ] ) ) { + return false; + } + + /** + * If the supported currencies array is empty, there are no + * restrictions, which currencies are supported by a card. + */ + $supported_currencies = $this->country_card_matrix[ $country ][ $card ]; + $currency = get_woocommerce_currency(); + return empty( $supported_currencies ) || in_array( $currency, $supported_currencies, true ); + } + + /** + * Returns the country code of the shop. + * + * @return string + */ + private function country() : string { + $region = wc_get_base_location(); + $country = $region['country']; + return $country; + } } diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index 56c7d7977..b64892ba9 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -5,6 +5,7 @@ class CreditCardRenderer { constructor(defaultConfig, errorHandler) { this.defaultConfig = defaultConfig; this.errorHandler = errorHandler; + this.cardValid = false; } render(wrapper, contextConfig) { @@ -79,7 +80,7 @@ class CreditCardRenderer { return state.fields[key].isValid; }); - if (formValid) { + if (formValid && this.cardValid) { let vault = document.querySelector(wrapper + ' .ppcp-credit-card-vault') ? document.querySelector(wrapper + ' .ppcp-credit-card-vault').checked : false; @@ -93,12 +94,21 @@ class CreditCardRenderer { return contextConfig.onApprove(payload); }); } else { - this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid); + const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid; + this.errorHandler.message(message); } } hostedFields.on('inputSubmitRequest', function () { submitEvent(null); }); + hostedFields.on('cardTypeChange', (event) => { + if ( ! event.cards.length ) { + this.cardValid = false; + return; + } + const validCards = this.defaultConfig.hosted_fields.valid_cards; + this.cardValid = validCards.indexOf(event.cards[0].type) !== -1; + }) document.querySelector(wrapper + ' button').addEventListener( 'click', submitEvent diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 4bab9ceb3..e5ae5c14a 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; -use Dhii\Data\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; @@ -138,7 +137,15 @@ return array( $session_handler = $container->get( 'session.handler' ); $three_d_secure = $container->get( 'button.helper.three-d-secure' ); $settings = $container->get( 'wcgateway.settings' ); - return new ApproveOrderEndpoint( $request_data, $order_endpoint, $session_handler, $three_d_secure, $settings ); + $dcc_applies = $container->get( 'api.helpers.dccapplies' ); + return new ApproveOrderEndpoint( + $request_data, + $order_endpoint, + $session_handler, + $three_d_secure, + $settings, + $dcc_applies + ); }, 'button.endpoint.data-client-id' => static function( $container ) : DataClientIdEndpoint { $request_data = $container->get( 'button.request-data' ); diff --git a/modules/ppcp-button/src/Assets/class-smartbutton.php b/modules/ppcp-button/src/Assets/class-smartbutton.php index 56b8c23df..db4fc7e48 100644 --- a/modules/ppcp-button/src/Assets/class-smartbutton.php +++ b/modules/ppcp-button/src/Assets/class-smartbutton.php @@ -601,7 +601,12 @@ class SmartButton implements SmartButtonInterface { 'Unfortunatly, your credit card details are not valid.', 'paypal-payments-for-woocommerce' ), + 'card_not_supported' => __( + 'Unfortunatly, we do not support your credit card.', + 'paypal-payments-for-woocommerce' + ), ), + 'valid_cards' => $this->dcc_applies->valid_cards(), ), 'messages' => $this->message_values(), 'labels' => array( diff --git a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php index dc819f1e2..783618c43 100644 --- a/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php +++ b/modules/ppcp-button/src/Endpoint/class-approveorderendpoint.php @@ -13,6 +13,7 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; use WooCommerce\PayPalCommerce\Session\SessionHandler; @@ -61,6 +62,13 @@ class ApproveOrderEndpoint implements EndpointInterface { */ private $settings; + /** + * The DCC applies object. + * + * @var DccApplies + */ + private $dcc_applies; + /** * ApproveOrderEndpoint constructor. * @@ -69,13 +77,15 @@ class ApproveOrderEndpoint implements EndpointInterface { * @param SessionHandler $session_handler The session handler. * @param ThreeDSecure $three_d_secure The 3d secure helper object. * @param Settings $settings The settings. + * @param DccApplies $dcc_applies The DCC applies object. */ public function __construct( RequestData $request_data, OrderEndpoint $order_endpoint, SessionHandler $session_handler, ThreeDSecure $three_d_secure, - Settings $settings + Settings $settings, + DccApplies $dcc_applies ) { $this->request_data = $request_data; @@ -83,6 +93,7 @@ class ApproveOrderEndpoint implements EndpointInterface { $this->session_handler = $session_handler; $this->threed_secure = $three_d_secure; $this->settings = $settings; + $this->dcc_applies = $dcc_applies; } /** @@ -123,11 +134,12 @@ class ApproveOrderEndpoint implements EndpointInterface { if ( $order->payment_source() && $order->payment_source()->card() ) { if ( $this->settings->has( 'disable_cards' ) ) { $disabled_cards = (array) $this->settings->get( 'disable_cards' ); - if ( in_array( 'mastercard', $disabled_cards, true ) ) { - $disabled_cards[] = 'master_card'; + $card = strtolower( $order->payment_source()->card()->brand() ); + if ( 'master_card' === $card ) { + $card = 'mastercard'; } - $card = strtolower( $order->payment_source()->card()->brand() ); - if ( in_array( $card, $disabled_cards, true ) ) { + + if ( ! $this->dcc_applies->can_process_card( $card ) || in_array( $card, $disabled_cards, true ) ) { throw new RuntimeException( __( 'Unfortunately, we do not accept this card.', diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 0dba777cd..92b816b41 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway; use Dhii\Data\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn; use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail; @@ -1581,6 +1582,25 @@ return array( if ( 'GB' === $country ) { unset( $fields['disable_funding']['options']['card'] ); } + + $dcc_applies = $container->get( 'api.helpers.dccapplies' ); + /** + * Depending on your store location, some credit cards can't be used. + * Here, we filter them out. + * + * The DCC Applies object. + * + * @var DccApplies $dcc_applies + */ + $card_options = $fields['disable_cards']['options']; + foreach ( $card_options as $card => $label ) { + if ( $dcc_applies->can_process_card( $card ) ) { + continue; + } + unset( $card_options[ $card ] ); + } + $fields['disable_cards']['options'] = $card_options; + $fields['card_icons']['options'] = $card_options; return $fields; }, From 7caa0d3dbfaf9580a553644e89d3f2e4e0a23920 Mon Sep 17 00:00:00 2001 From: David Remer Date: Tue, 29 Sep 2020 10:11:27 +0300 Subject: [PATCH 07/14] change target to apply block() --- modules/ppcp-button/resources/js/modules/Helper/Spinner.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/Spinner.js b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js index 12790dcb1..50e4eebd9 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/Spinner.js +++ b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js @@ -1,8 +1,11 @@ class Spinner { + constructor() { + this.target = 'form.woocommerce-checkout'; + } block() { - jQuery( '.woocommerce-checkout-payment, .woocommerce-checkout-review-order-table' ).block({ + jQuery( this.target ).block({ message: null, overlayCSS: { background: '#fff', @@ -13,7 +16,7 @@ class Spinner { unblock() { - jQuery( '.woocommerce-checkout-payment, .woocommerce-checkout-review-order-table' ).unblock(); + jQuery( this.target ).unblock(); } } From f236ad67aeecc40ee5efc13a995cd8d5260ca1af Mon Sep 17 00:00:00 2001 From: David Remer Date: Tue, 29 Sep 2020 13:21:01 +0300 Subject: [PATCH 08/14] check if refund reason exists --- modules/ppcp-api-client/src/Factory/class-capturefactory.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Factory/class-capturefactory.php b/modules/ppcp-api-client/src/Factory/class-capturefactory.php index 01e0ff29d..d96f10346 100644 --- a/modules/ppcp-api-client/src/Factory/class-capturefactory.php +++ b/modules/ppcp-api-client/src/Factory/class-capturefactory.php @@ -42,10 +42,11 @@ class CaptureFactory { */ public function from_paypal_response( \stdClass $data ) : Capture { + $reason = isset ( $data->status_details->reason ) ? (string) $data->status_details->reason : ''; return new Capture( (string) $data->id, (string) $data->status, - (string) $data->status_details->reason, + $reason, $this->amount_factory->from_paypal_response( $data->amount ), (bool) $data->final_capture, (string) $data->seller_protection->status, From 14e31e3a0da5ecf99e68a89e2411e5d63f433553 Mon Sep 17 00:00:00 2001 From: David Remer Date: Tue, 29 Sep 2020 13:26:22 +0300 Subject: [PATCH 09/14] make sure a order has captures before trying to refund them --- .../src/Processor/class-refundprocessor.php | 12 +++++++++++- 1 file changed, 11 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 e52c8eb76..1d6bbf290 100644 --- a/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php @@ -68,7 +68,17 @@ class RefundProcessor { return false; } - $capture = $order->purchase_units()[0]->payments()->captures()[0]; + $purchase_units = $order->purchase_units(); + if ( ! $purchase_units ) { + return false; + } + + $captures = $purchase_units[0]->payments()->captures(); + if ( ! $captures ) { + return false; + } + + $capture = $captures[0]; $refund = new Refund( $capture, $capture->invoice_id(), From 8d1ed9ab241fb2d7fe077999de30bb72bcb04073 Mon Sep 17 00:00:00 2001 From: David Remer Date: Tue, 29 Sep 2020 14:03:38 +0300 Subject: [PATCH 10/14] codestyle --- modules/ppcp-api-client/src/Factory/class-capturefactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Factory/class-capturefactory.php b/modules/ppcp-api-client/src/Factory/class-capturefactory.php index d96f10346..7389f67e5 100644 --- a/modules/ppcp-api-client/src/Factory/class-capturefactory.php +++ b/modules/ppcp-api-client/src/Factory/class-capturefactory.php @@ -42,7 +42,7 @@ class CaptureFactory { */ public function from_paypal_response( \stdClass $data ) : Capture { - $reason = isset ( $data->status_details->reason ) ? (string) $data->status_details->reason : ''; + $reason = isset( $data->status_details->reason ) ? (string) $data->status_details->reason : ''; return new Capture( (string) $data->id, (string) $data->status, From f7dde306fa477c932ec1fded88e8b3baa8ae2181 Mon Sep 17 00:00:00 2001 From: David Remer Date: Wed, 30 Sep 2020 07:57:13 +0300 Subject: [PATCH 11/14] check if payments are available in purchase unit --- .../ppcp-wc-gateway/src/Processor/class-refundprocessor.php | 6 +++++- 1 file changed, 5 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 1d6bbf290..a348a30f3 100644 --- a/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/class-refundprocessor.php @@ -73,7 +73,11 @@ class RefundProcessor { return false; } - $captures = $purchase_units[0]->payments()->captures(); + $payments = $purchase_units[0]->payments(); + if ( ! $payments ) { + return false; + } + $captures = $payments->captures(); if ( ! $captures ) { return false; } From 96fd3d6a2a6c15429f9f9bafb765f9431cfb6f2d Mon Sep 17 00:00:00 2001 From: David Remer Date: Wed, 30 Sep 2020 14:24:31 +0300 Subject: [PATCH 12/14] enable gateways in the account -> order -> pay-now --- modules/ppcp-api-client/services.php | 1 - .../src/Factory/class-payerfactory.php | 36 ++++ modules/ppcp-button/resources/js/button.js | 10 ++ .../ActionHandler/CheckoutActionHandler.js | 6 +- .../ContextBootstrap/PayNowBootstrap.js | 80 +++++++++ .../OnApproveHandler/onApproveForPayNow.js | 2 +- .../js/modules/Renderer/CreditCardRenderer.js | 5 +- modules/ppcp-button/services.php | 18 +- .../src/Assets/class-smartbutton.php | 33 +++- .../Endpoint/class-createorderendpoint.php | 169 +++++++++++++----- 10 files changed, 302 insertions(+), 58 deletions(-) create mode 100644 modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 199b14e78..bae6bc0fa 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -43,7 +43,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; -use WpOop\TransientCache\CachePoolFactory; return array( 'api.host' => function( $container ) : string { diff --git a/modules/ppcp-api-client/src/Factory/class-payerfactory.php b/modules/ppcp-api-client/src/Factory/class-payerfactory.php index 3f9ee2755..9b9c57a8f 100644 --- a/modules/ppcp-api-client/src/Factory/class-payerfactory.php +++ b/modules/ppcp-api-client/src/Factory/class-payerfactory.php @@ -36,6 +36,42 @@ class PayerFactory { $this->address_factory = $address_factory; } + /** + * Returns a Payer entity from a WooCommerce order. + * + * @param \WC_Order $wc_order The WooCommerce order. + * + * @return Payer + */ + public function from_wc_order( \WC_Order $wc_order ): Payer { + $payer_id = ''; + $birthdate = null; + + $phone = null; + if ( $wc_order->get_billing_phone() ) { + // make sure the phone number contains only numbers and is max 14. chars long. + $national_number = $wc_order->get_billing_phone(); + $national_number = preg_replace( '/[^0-9]/', '', $national_number ); + $national_number = substr( $national_number, 0, 14 ); + + $phone = new PhoneWithType( + 'HOME', + new Phone( $national_number ) + ); + } + return new Payer( + new PayerName( + $wc_order->get_billing_first_name(), + $wc_order->get_billing_last_name() + ), + $wc_order->get_billing_email(), + $payer_id, + $this->address_factory->from_wc_order( $wc_order, 'billing' ), + $birthdate, + $phone + ); + } + /** * Returns a Payer object based off a WooCommerce customer. * diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index f52b8127d..e8ee26edd 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -2,6 +2,7 @@ import MiniCartBootstap from './modules/ContextBootstrap/MiniCartBootstap'; import SingleProductBootstap from './modules/ContextBootstrap/SingleProductBootstap'; import CartBootstrap from './modules/ContextBootstrap/CartBootstap'; import CheckoutBootstap from './modules/ContextBootstrap/CheckoutBootstap'; +import PayNowBootstrap from "./modules/ContextBootstrap/PayNowBootstrap"; import Renderer from './modules/Renderer/Renderer'; import ErrorHandler from './modules/ErrorHandler'; import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer"; @@ -52,6 +53,15 @@ const bootstrap = () => { checkoutBootstap.init(); } + if (context === 'pay-now' ) { + const payNowBootstrap = new PayNowBootstrap( + PayPalCommerceGateway, + renderer, + messageRenderer + ); + payNowBootstrap.init(); + } + if (context !== 'checkout') { messageRenderer.render(); } diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index bd83ab1af..d0ceb882c 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -16,7 +16,8 @@ class CheckoutActionHandler { const errorHandler = this.errorHandler; - const formValues = jQuery('form.checkout').serialize(); + const formSelector = this.config.context === 'checkout' ? 'form.checkout' : 'form#order_review'; + const formValues = jQuery(formSelector).serialize(); return fetch(this.config.ajax.create_order.endpoint, { method: 'POST', @@ -25,6 +26,7 @@ class CheckoutActionHandler { payer, bn_code:bnCode, context:this.config.context, + order_id:this.config.order_id, form:formValues }) }).then(function (res) { @@ -38,7 +40,7 @@ class CheckoutActionHandler { input.setAttribute('type', 'hidden'); input.setAttribute('name', 'ppcp-resume-order'); input.setAttribute('value', data.data.purchase_units[0].custom_id); - document.querySelector('form.checkout').append(input); + document.querySelector(formSelector).append(input); return data.data.id; }); } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js new file mode 100644 index 000000000..b6b147802 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js @@ -0,0 +1,80 @@ +import ErrorHandler from '../ErrorHandler'; +import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler'; + +class PayNowBootstrap { + constructor(gateway, renderer, messages) { + this.gateway = gateway; + this.renderer = renderer; + this.messages = messages + } + + init() { + + this.render(); + + jQuery(document.body).on('updated_checkout', () => { + this.render(); + }); + + jQuery(document.body). + on('updated_checkout payment_method_selected', () => { + this.switchBetweenPayPalandOrderButton(); + }); + this.switchBetweenPayPalandOrderButton(); + } + + shouldRender() { + if (document.querySelector(this.gateway.button.cancel_wrapper)) { + return false; + } + + return document.querySelector(this.gateway.button.wrapper) !== null || document.querySelector(this.gateway.hosted_fields.wrapper) !== null; + } + + render() { + if (!this.shouldRender()) { + return; + } + if (document.querySelector(this.gateway.hosted_fields.wrapper + '>div')) { + document.querySelector(this.gateway.hosted_fields.wrapper + '>div').setAttribute('style', ''); + } + const actionHandler = new CheckoutActionHandler( + PayPalCommerceGateway, + new ErrorHandler(this.gateway.labels.error.generic), + ); + + this.renderer.render( + this.gateway.button.wrapper, + this.gateway.hosted_fields.wrapper, + actionHandler.configuration(), + ); + } + + switchBetweenPayPalandOrderButton() { + const currentPaymentMethod = jQuery( + 'input[name="payment_method"]:checked').val(); + + if (currentPaymentMethod !== 'ppcp-gateway' && currentPaymentMethod !== 'ppcp-credit-card-gateway') { + this.renderer.hideButtons(this.gateway.button.wrapper); + this.renderer.hideButtons(this.gateway.messages.wrapper); + this.renderer.hideButtons(this.gateway.hosted_fields.wrapper); + jQuery('#place_order').show(); + } + else { + jQuery('#place_order').hide(); + if (currentPaymentMethod === 'ppcp-gateway') { + this.renderer.showButtons(this.gateway.button.wrapper); + this.renderer.showButtons(this.gateway.messages.wrapper); + this.messages.render(); + this.renderer.hideButtons(this.gateway.hosted_fields.wrapper); + } + if (currentPaymentMethod === 'ppcp-credit-card-gateway') { + this.renderer.hideButtons(this.gateway.button.wrapper); + this.renderer.hideButtons(this.gateway.messages.wrapper); + this.renderer.showButtons(this.gateway.hosted_fields.wrapper); + } + } + } +} + +export default PayNowBootstrap; \ No newline at end of file diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js index 61e9bcb9a..50431d83a 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js @@ -12,7 +12,7 @@ const onApprove = (context, errorHandler) => { if (!data.success) { errorHandler.genericError(); console.error(data); - if (typeof actions.restart !== 'undefined') { + if (typeof actions !== 'undefined' && typeof actions.restart !== 'undefined') { return actions.restart(); } throw new Error(data.data.message); diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index ec38441af..a423f26a3 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -10,7 +10,10 @@ class CreditCardRenderer { render(wrapper, contextConfig) { if ( - this.defaultConfig.context !== 'checkout' + ( + this.defaultConfig.context !== 'checkout' + && this.defaultConfig.context !== 'pay-now' + ) || wrapper === null || document.querySelector(wrapper) === null ) { diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index a4d288577..6b47c04a0 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -107,16 +107,18 @@ return array( return new ChangeCartEndpoint( $cart, $shipping, $request_data, $repository, $data_store ); }, 'button.endpoint.create-order' => static function ( $container ): CreateOrderEndpoint { - $request_data = $container->get( 'button.request-data' ); - $repository = $container->get( 'api.repository.cart' ); - $order_endpoint = $container->get( 'api.endpoint.order' ); - $payer_factory = $container->get( 'api.factory.payer' ); - $session_handler = $container->get( 'session.handler' ); - $settings = $container->get( 'wcgateway.settings' ); - $early_order_handler = $container->get( 'button.helper.early-order-handler' ); + $request_data = $container->get( 'button.request-data' ); + $cart_repository = $container->get( 'api.repository.cart' ); + $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); + $order_endpoint = $container->get( 'api.endpoint.order' ); + $payer_factory = $container->get( 'api.factory.payer' ); + $session_handler = $container->get( 'session.handler' ); + $settings = $container->get( 'wcgateway.settings' ); + $early_order_handler = $container->get( 'button.helper.early-order-handler' ); return new CreateOrderEndpoint( $request_data, - $repository, + $cart_repository, + $purchase_unit_factory, $order_endpoint, $payer_factory, $session_handler, diff --git a/modules/ppcp-button/src/Assets/class-smartbutton.php b/modules/ppcp-button/src/Assets/class-smartbutton.php index 9e6ca8e99..03ebc806c 100644 --- a/modules/ppcp-button/src/Assets/class-smartbutton.php +++ b/modules/ppcp-button/src/Assets/class-smartbutton.php @@ -177,6 +177,15 @@ class SmartButton implements SmartButtonInterface { ), 11 ); + + add_action( + 'woocommerce_pay_order_after_submit', + array( + $this, + 'dcc_renderer', + ), + 11 + ); } return true; } @@ -232,6 +241,14 @@ class SmartButton implements SmartButtonInterface { ), 11 ); + add_action( + 'woocommerce_pay_order_after_submit', + array( + $this, + 'message_renderer', + ), + 11 + ); } return true; } @@ -294,6 +311,7 @@ class SmartButton implements SmartButtonInterface { } add_action( 'woocommerce_review_order_after_submit', array( $this, 'button_renderer' ), 10 ); + add_action( 'woocommerce_pay_order_after_submit', array( $this, 'button_renderer' ), 10 ); return true; } @@ -321,7 +339,7 @@ class SmartButton implements SmartButtonInterface { $load_script = true; } - if ( is_checkout() && $this->can_render_dcc() ) { + if ( in_array( $this->context(), array( 'pay-now', 'checkout' ), true ) && $this->can_render_dcc() ) { wp_enqueue_style( 'ppcp-hosted-fields', $this->module_url . '/assets/css/hosted-fields.css', @@ -474,7 +492,8 @@ class SmartButton implements SmartButtonInterface { */ private function can_render_dcc() : bool { - return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency(); + $can_render = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency(); + return $can_render; } /** @@ -502,6 +521,8 @@ class SmartButton implements SmartButtonInterface { esc_html__( 'Save your card', 'paypal-payments-for-woocommerce' ) ) : ''; + $label = 'checkout' === $this->context() ? __( 'Place order', 'paypal-payments-for-woocommerce' ) : __( 'Pay for order', 'paypal-payments-for-woocommerce' ); + printf( '