diff --git a/.editorconfig b/.editorconfig index 922986d27..9920ff350 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,6 @@ trim_trailing_whitespace = false [*.{js,json,yml}] indent_style = space + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 000000000..6d4d11f21 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,41 @@ +name: Build package + +on: + workflow_dispatch: + inputs: + packageVersion: + description: 'Package Version' + required: false + type: string + +jobs: + package: + runs-on: ubuntu-latest + + name: Build package + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.1 + tools: composer:v1 + + - name: Set plugin version header + env: + PACKAGE_VERSION: ${{ github.event.inputs.packageVersion }} + run: 'sed -Ei "s/Version: .*/Version: ${PACKAGE_VERSION}/g" woocommerce-paypal-payments.php' + if: github.event.inputs.packageVersion + + - name: Build + run: yarn build + + - name: Unzip # GH currently always zips, so if we upload a zip we get a zip inside a zip + run: unzip woocommerce-paypal-payments.zip -d dist + + - name: Upload + uses: actions/upload-artifact@v3 + with: + name: woocommerce-paypal-payments + path: dist/ diff --git a/changelog.txt b/changelog.txt index 6879bff12..423d52ab8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,14 +1,27 @@ *** Changelog *** -= 1.9.1 - TBD = += 1.9.2 - TBD = +* Fix - Do not allow birth date older than 100 years for PUI. #743 +* Fix - Store the customer id for vaulted payment method in usermeta to not lose vaulted methods after the invoice prefix change. #698 +* Fix - Capture Virtual-Only Orders setting did not auto-capture subscription renewal payments. #626 +* Fix - Voiding authorization at PayPal did not update the status/order notes. #712 +* Fix - PayPal scripts were loading on pages without smart buttons or Pay Later messaging. #750 +* Fix - Do not show links for unavailable gateways settings pages. #753 +* Fix - The smart buttons were not loaded on single product page if a subscription product exists in the cart. #703 +* Fix - DCC was causing other gateways to disappear after checkout validation error. #757 +* Fix - Buttons not loading on single product page with default settings when product is in cart. #777 +* Enhancement - Improve Checkout Field Validation Message. #739 +* Enhancement - Handle PAYER_ACTION_REQUIRED error. #759 + += 1.9.1 - 2022-07-25 = * Fix - ITEM_TOTAL_MISMATCH error when checking out with multiple products #721 * Fix - Unable to purchase a product with Credit card button in pay for order page #718 * Fix - Pay Later messaging only displayed when smart button is active on the same page #283 * Fix - Pay Later messaging displayed for out of stock variable products or with no variation selected #667 * Fix - Placeholders and card type detection not working for PayPal Card Processing (260) #685 -* Fix - PUI gateway is displayed with unsupported store currency #711 +* Fix - PUI gateway is displayed with unsupported store currency #711 * Fix - Wrong PUI locale sent causing error PAYMENT_SOURCE_CANNOT_BE_USED #741 -* Enhancement - Missing PayPal fee in WC order details for PUI purchase #714 +* Enhancement - Missing PayPal fee in WC order details for PUI purchase #714 * Enhancement - Skip loading of PUI js file on all pages where PUI gateway is not displayed #723 * Enhancement - PUI feature capitalization not consistent #724 diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index f34ac93da..ef1b40e3d 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -400,6 +400,60 @@ return array( ); }, + + 'api.shop.is-latin-america' => static function ( ContainerInterface $container ): bool { + return in_array( + $container->get( 'api.shop.country' ), + array( + 'AI', + 'AG', + 'AR', + 'AW', + 'BS', + 'BB', + 'BZ', + 'BM', + 'BO', + 'BR', + 'VG', + 'KY', + 'CL', + 'CO', + 'CR', + 'DM', + 'DO', + 'EC', + 'SV', + 'FK', + 'GF', + 'GD', + 'GP', + 'GT', + 'GY', + 'HN', + 'JM', + 'MQ', + 'MX', + 'MS', + 'AN', + 'NI', + 'PA', + 'PY', + 'PE', + 'KN', + 'LC', + 'PM', + 'VC', + 'SR', + 'TT', + 'TC', + 'UY', + 'VE', + ), + true + ); + }, + /** * Currencies supported by PayPal. * diff --git a/modules/ppcp-api-client/src/Endpoint/IdentityToken.php b/modules/ppcp-api-client/src/Endpoint/IdentityToken.php index a01e746ac..0c27e3fe4 100644 --- a/modules/ppcp-api-client/src/Endpoint/IdentityToken.php +++ b/modules/ppcp-api-client/src/Endpoint/IdentityToken.php @@ -106,7 +106,7 @@ class IdentityToken { && defined( 'PPCP_FLAG_SUBSCRIPTION' ) && PPCP_FLAG_SUBSCRIPTION ) { $customer_id = $this->customer_repository->customer_id_for_user( ( $user_id ) ); - + update_user_meta( $user_id, 'ppcp_customer_id', $customer_id ); $args['body'] = wp_json_encode( array( 'customer_id' => $customer_id, diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index 41448a542..57b8b73d6 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; +use stdClass; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; @@ -28,6 +29,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; +use WP_Error; /** * Class OrderEndpoint @@ -192,7 +194,7 @@ class OrderEndpoint { 'application_context' => $this->application_context_repository ->current_context( $shipping_preference )->to_array(), ); - if ( $payer && ! empty( $payer->email_address() ) && ! empty( $payer->name() ) ) { + if ( $payer && ! empty( $payer->email_address() ) ) { $data['payer'] = $payer->to_array(); } if ( $payment_token ) { @@ -564,4 +566,49 @@ class OrderEndpoint { $new_order = $this->order( $order_to_update->id() ); return $new_order; } + + /** + * Confirms payment source. + * + * @param string $id The PayPal order ID. + * @param array $payment_source The payment source. + * @return stdClass + * @throws PayPalApiException If the request fails. + * @throws RuntimeException If something unexpected happens. + */ + public function confirm_payment_source( string $id, array $payment_source ): stdClass { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $id . '/confirm-payment-source'; + + $data = array( + 'payment_source' => $payment_source, + 'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL', + 'application_context' => array( + 'locale' => 'es-MX', + ), + ); + + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + 'body' => wp_json_encode( $data ), + ); + + $response = $this->request( $url, $args ); + if ( $response instanceof WP_Error ) { + throw new RuntimeException( $response->get_error_message() ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + throw new PayPalApiException( $json, $status_code ); + } + + return $json; + } } diff --git a/modules/ppcp-api-client/src/Entity/Payer.php b/modules/ppcp-api-client/src/Entity/Payer.php index d20fd215a..45164c1f5 100644 --- a/modules/ppcp-api-client/src/Entity/Payer.php +++ b/modules/ppcp-api-client/src/Entity/Payer.php @@ -18,7 +18,7 @@ class Payer { /** * The name. * - * @var PayerName + * @var PayerName|null */ private $name; @@ -46,7 +46,7 @@ class Payer { /** * The address. * - * @var Address + * @var Address|null */ private $address; @@ -67,7 +67,7 @@ class Payer { /** * Payer constructor. * - * @param PayerName $name The name. + * @param PayerName|null $name The name. * @param string $email_address The email. * @param string $payer_id The payer id. * @param Address|null $address The address. @@ -76,7 +76,7 @@ class Payer { * @param PayerTaxInfo|null $tax_info The tax info. */ public function __construct( - PayerName $name, + ?PayerName $name, string $email_address, string $payer_id, Address $address = null, @@ -97,12 +97,21 @@ class Payer { /** * Returns the name. * - * @return PayerName + * @return PayerName|null */ - public function name(): PayerName { + public function name(): ?PayerName { return $this->name; } + /** + * Sets the name. + * + * @param PayerName|null $name The value. + */ + public function set_name( ?PayerName $name ): void { + $this->name = $name; + } + /** * Returns the email address. * @@ -139,6 +148,15 @@ class Payer { return $this->address; } + /** + * Sets the address. + * + * @param Address|null $address The value. + */ + public function set_address( ?Address $address ): void { + $this->address = $address; + } + /** * Returns the phone. * @@ -164,27 +182,26 @@ class Payer { */ public function to_array() { $payer = array( - 'name' => $this->name()->to_array(), 'email_address' => $this->email_address(), ); - if ( $this->address() ) { - $payer['address'] = $this->address->to_array(); - if ( 2 !== strlen( $this->address()->country_code() ) ) { - unset( $payer['address'] ); - } + if ( $this->name ) { + $payer['name'] = $this->name->to_array(); } - if ( $this->payer_id() ) { - $payer['payer_id'] = $this->payer_id(); + if ( $this->address && 2 === strlen( $this->address->country_code() ) ) { + $payer['address'] = $this->address->to_array(); + } + if ( $this->payer_id ) { + $payer['payer_id'] = $this->payer_id; } - if ( $this->phone() ) { - $payer['phone'] = $this->phone()->to_array(); + if ( $this->phone ) { + $payer['phone'] = $this->phone->to_array(); } - if ( $this->tax_info() ) { - $payer['tax_info'] = $this->tax_info()->to_array(); + if ( $this->tax_info ) { + $payer['tax_info'] = $this->tax_info->to_array(); } - if ( $this->birthdate() ) { - $payer['birth_date'] = $this->birthdate()->format( 'Y-m-d' ); + if ( $this->birthdate ) { + $payer['birth_date'] = $this->birthdate->format( 'Y-m-d' ); } return $payer; } diff --git a/modules/ppcp-api-client/src/Exception/PayPalApiException.php b/modules/ppcp-api-client/src/Exception/PayPalApiException.php index d5720532b..d2752cb72 100644 --- a/modules/ppcp-api-client/src/Exception/PayPalApiException.php +++ b/modules/ppcp-api-client/src/Exception/PayPalApiException.php @@ -111,15 +111,6 @@ class PayPalApiException extends RuntimeException { return false; } - /** - * Returns response issues. - * - * @return array - */ - public function issues(): array { - return $this->response->issues ?? array(); - } - /** * The HTTP status code. * diff --git a/modules/ppcp-api-client/src/Factory/AmountFactory.php b/modules/ppcp-api-client/src/Factory/AmountFactory.php index 71f1c1a9d..51a6d8c11 100644 --- a/modules/ppcp-api-client/src/Factory/AmountFactory.php +++ b/modules/ppcp-api-client/src/Factory/AmountFactory.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Item; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -132,7 +133,7 @@ class AmountFactory { $total_value = (float) $order->get_total(); if ( ( - CreditCardGateway::ID === $order->get_payment_method() + in_array( $order->get_payment_method(), array( CreditCardGateway::ID, CardButtonGateway::ID ), true ) || ( PayPalGateway::ID === $order->get_payment_method() && 'card' === $order->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY ) ) ) && $this->is_free_trial_order( $order ) diff --git a/modules/ppcp-api-client/src/Repository/CustomerRepository.php b/modules/ppcp-api-client/src/Repository/CustomerRepository.php index 99063b7e3..c4f172dc8 100644 --- a/modules/ppcp-api-client/src/Repository/CustomerRepository.php +++ b/modules/ppcp-api-client/src/Repository/CustomerRepository.php @@ -57,6 +57,6 @@ class CustomerRepository { return $guest_customer_id; } - return $this->prefix . (string) $user_id; + return get_user_meta( $user_id, 'ppcp_customer_id', true ) ?: $this->prefix . (string) $user_id; } } diff --git a/modules/ppcp-button/package.json b/modules/ppcp-button/package.json index ada30892e..03403b794 100644 --- a/modules/ppcp-button/package.json +++ b/modules/ppcp-button/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "license": "GPL-3.0-or-later", "main": "resources/js/button.js", + "dependencies": { + "deepmerge": "^4.2.2" + }, "devDependencies": { "@babel/core": "^7.9.0", "@babel/preset-env": "^7.9.5", diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index ed8c400cf..03a38a3f7 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -18,7 +18,9 @@ import {hide, setVisible} from "./modules/Helper/Hiding"; import {isChangePaymentPage} from "./modules/Helper/Subscriptions"; import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler"; -const buttonsSpinner = new Spinner('.ppc-button-wrapper'); +// TODO: could be a good idea to have a separate spinner for each gateway, +// but I think we care mainly about the script loading, so one spinner should be enough. +const buttonsSpinner = new Spinner(document.querySelector('.ppc-button-wrapper')); const cardsSpinner = new Spinner('#ppcp-hosted-fields'); const bootstrap = () => { @@ -38,9 +40,36 @@ const bootstrap = () => { requiredFields.each((i, input) => { jQuery(input).trigger('validate'); }); - if (jQuery('form.woocommerce-checkout .validate-required.woocommerce-invalid:visible').length) { + const invalidFields = Array.from(jQuery('form.woocommerce-checkout .validate-required.woocommerce-invalid:visible')); + if (invalidFields.length) { + const billingFieldsContainer = document.querySelector('.woocommerce-billing-fields'); + const shippingFieldsContainer = document.querySelector('.woocommerce-shipping-fields'); + + const nameMessageMap = PayPalCommerceGateway.labels.error.required.elements; + const messages = invalidFields.map(el => { + const name = el.querySelector('[name]')?.getAttribute('name'); + if (name && name in nameMessageMap) { + return nameMessageMap[name]; + } + let label = el.querySelector('label').textContent + .replaceAll('*', '') + .trim(); + if (billingFieldsContainer?.contains(el)) { + label = PayPalCommerceGateway.labels.billing_field.replace('%s', label); + } + if (shippingFieldsContainer?.contains(el)) { + label = PayPalCommerceGateway.labels.shipping_field.replace('%s', label); + } + return PayPalCommerceGateway.labels.error.required.field + .replace('%s', `${label}`) + }).filter(s => s.length > 2); + errorHandler.clear(); - errorHandler.message(PayPalCommerceGateway.labels.error.js_validation); + if (messages.length) { + messages.forEach(s => errorHandler.message(s)); + } else { + errorHandler.message(PayPalCommerceGateway.labels.error.required.generic); + } return actions.reject(); } @@ -138,6 +167,11 @@ document.addEventListener( return; } + const paypalButtonGatewayIds = [ + PaymentMethods.PAYPAL, + ...Object.entries(PayPalCommerceGateway.separate_buttons).map(([k, data]) => data.id), + ] + // Sometimes PayPal script takes long time to load, // so we additionally hide the standard order button here to avoid failed orders. // Normally it is hidden later after the script load. @@ -153,12 +187,12 @@ document.addEventListener( } const currentPaymentMethod = getCurrentPaymentMethod(); - const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL; + const isPaypalButton = paypalButtonGatewayIds.includes(currentPaymentMethod); const isCards = currentPaymentMethod === PaymentMethods.CARDS; - setVisible(ORDER_BUTTON_SELECTOR, !isPaypal && !isCards, true); + setVisible(ORDER_BUTTON_SELECTOR, !isPaypalButton && !isCards, true); - if (isPaypal) { + if (isPaypalButton) { // stopped after the first rendering of the buttons, in onInit buttonsSpinner.block(); } else { diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index 733b5e339..8b4dd7501 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -26,6 +26,9 @@ class CheckoutActionHandler { const createaccount = jQuery('#createaccount').is(":checked") ? true : false; + const paymentMethod = getCurrentPaymentMethod(); + const fundingSource = window.ppcpFundingSource; + return fetch(this.config.ajax.create_order.endpoint, { method: 'POST', body: JSON.stringify({ @@ -34,8 +37,8 @@ class CheckoutActionHandler { bn_code:bnCode, context:this.config.context, order_id:this.config.order_id, - payment_method: getCurrentPaymentMethod(), - funding_source: window.ppcpFundingSource, + payment_method: paymentMethod, + funding_source: fundingSource, form: formJsonObj, createaccount: createaccount }) diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js index 29e6a4819..d32ca440f 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js @@ -32,9 +32,7 @@ class CartBootstrap { ); this.renderer.render( - this.gateway.button.wrapper, - this.gateway.hosted_fields.wrapper, - actionHandler.configuration(), + actionHandler.configuration() ); } } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index bc83c736e..9d008d4b6 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -69,9 +69,7 @@ class CheckoutBootstap { ); this.renderer.render( - this.gateway.button.wrapper, - this.gateway.hosted_fields.wrapper, - actionHandler.configuration(), + actionHandler.configuration() ); this.buttonChangeObserver.observe( @@ -84,16 +82,27 @@ class CheckoutBootstap { const currentPaymentMethod = getCurrentPaymentMethod(); const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL; const isCard = currentPaymentMethod === PaymentMethods.CARDS; + const isSeparateButtonGateway = [PaymentMethods.CARD_BUTTON].includes(currentPaymentMethod); const isSavedCard = isCard && isSavedCardSelected(); - const isNotOurGateway = !isPaypal && !isCard; + const isNotOurGateway = !isPaypal && !isCard && !isSeparateButtonGateway; const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; const hasVaultedPaypal = PayPalCommerceGateway.vaulted_paypal_email !== ''; + const paypalButtonWrappers = { + ...Object.entries(PayPalCommerceGateway.separate_buttons) + .reduce((result, [k, data]) => { + return {...result, [data.id]: data.wrapper} + }, {}), + }; + setVisible(this.standardOrderButtonSelector, (isPaypal && isFreeTrial && hasVaultedPaypal) || isNotOurGateway || isSavedCard, true); setVisible('.ppcp-vaulted-paypal-details', isPaypal); setVisible(this.gateway.button.wrapper, isPaypal && !(isFreeTrial && hasVaultedPaypal)); setVisible(this.gateway.messages.wrapper, isPaypal && !isFreeTrial); setVisible(this.gateway.hosted_fields.wrapper, isCard && !isSavedCard); + for (const [gatewayId, wrapper] of Object.entries(paypalButtonWrappers)) { + setVisible(wrapper, gatewayId === currentPaymentMethod); + } if (isPaypal && !isFreeTrial) { this.messages.render(); diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js index 1a45e6814..35465c12e 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/MiniCartBootstap.js @@ -32,9 +32,13 @@ class MiniCartBootstap { } this.renderer.render( - this.gateway.button.mini_cart_wrapper, - this.gateway.hosted_fields.mini_cart_wrapper, - this.actionHandler.configuration() + this.actionHandler.configuration(), + { + button: { + wrapper: this.gateway.button.mini_cart_wrapper, + style: this.gateway.button.mini_cart_style, + }, + } ); } } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index 6da17f4e9..ecb44eabe 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -85,9 +85,7 @@ class SingleProductBootstap { ); this.renderer.render( - this.gateway.button.wrapper, - this.gateway.hosted_fields.wrapper, - actionHandler.configuration(), + actionHandler.configuration() ); } } diff --git a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js index 196ad596b..7d5292251 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js +++ b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js @@ -1,6 +1,8 @@ export const PaymentMethods = { PAYPAL: 'ppcp-gateway', CARDS: 'ppcp-credit-card-gateway', + OXXO: 'ppcp-oxxo-gateway', + CARD_BUTTON: 'ppcp-card-button-gateway', }; export const ORDER_BUTTON_SELECTOR = '#place_order'; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index cc811bb58..65e96c0c8 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -1,33 +1,96 @@ +import merge from "deepmerge"; + class Renderer { - constructor(creditCardRenderer, defaultConfig, onSmartButtonClick, onSmartButtonsInit) { - this.defaultConfig = defaultConfig; + constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) { + this.defaultSettings = defaultSettings; this.creditCardRenderer = creditCardRenderer; this.onSmartButtonClick = onSmartButtonClick; this.onSmartButtonsInit = onSmartButtonsInit; + + this.renderedSources = new Set(); } - render(wrapper, hostedFieldsWrapper, contextConfig) { + render(contextConfig, settingsOverride = {}) { + const settings = merge(this.defaultSettings, settingsOverride); - this.renderButtons(wrapper, contextConfig); - this.creditCardRenderer.render(hostedFieldsWrapper, contextConfig); + const enabledSeparateGateways = Object.fromEntries(Object.entries( + settings.separate_buttons).filter(([s, data]) => document.querySelector(data.wrapper) + )); + const hasEnabledSeparateGateways = Object.keys(enabledSeparateGateways).length !== 0; + + if (!hasEnabledSeparateGateways) { + this.renderButtons( + settings.button.wrapper, + settings.button.style, + contextConfig, + hasEnabledSeparateGateways + ); + } else { + // render each button separately + for (const fundingSource of paypal.getFundingSources().filter(s => !(s in enabledSeparateGateways))) { + let style = settings.button.style; + if (fundingSource !== 'paypal') { + style = { + shape: style.shape, + }; + } + + this.renderButtons( + settings.button.wrapper, + style, + contextConfig, + hasEnabledSeparateGateways, + fundingSource + ); + } + } + + this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfig); + + for (const [fundingSource, data] of Object.entries(enabledSeparateGateways)) { + this.renderButtons( + data.wrapper, + data.style, + contextConfig, + hasEnabledSeparateGateways, + fundingSource + ); + } } - renderButtons(wrapper, contextConfig) { - if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper) || 'undefined' === typeof paypal.Buttons ) { + renderButtons(wrapper, style, contextConfig, hasEnabledSeparateGateways, fundingSource = null) { + if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) || 'undefined' === typeof paypal.Buttons ) { return; } - const style = wrapper === this.defaultConfig.button.wrapper ? this.defaultConfig.button.style : this.defaultConfig.button.mini_cart_style; - paypal.Buttons({ + if (fundingSource) { + contextConfig.fundingSource = fundingSource; + } + + const btn = paypal.Buttons({ style, ...contextConfig, onClick: this.onSmartButtonClick, onInit: this.onSmartButtonsInit, - }).render(wrapper); + }); + if (!btn.isEligible()) { + return; + } + + btn.render(wrapper); + + this.renderedSources.add(wrapper + fundingSource ?? ''); } - isAlreadyRendered(wrapper) { - return document.querySelector(wrapper).hasChildNodes(); + isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) { + // Simply check that has child nodes when we do not need to render buttons separately, + // this will reduce the risk of breaking with different themes/plugins + // and on the cart page (where we also do not need to render separately), which may fully reload this part of the page. + // Ideally we should also find a way to detect such full reloads and remove the corresponding keys from the set. + if (!hasEnabledSeparateGateways) { + return document.querySelector(wrapper).hasChildNodes(); + } + return this.renderedSources.has(wrapper + fundingSource ?? ''); } hideButtons(element) { diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 3b4b710fd..cd30058cf 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -133,6 +133,7 @@ return array( $settings, $early_order_handler, $registration_needed, + $container->get( 'wcgateway.settings.card_billing_data_mode' ), $logger ); }, diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 33a2eb1a4..b69417a03 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -28,7 +28,9 @@ use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -421,16 +423,23 @@ class SmartButton implements SmartButtonInterface { ) { add_action( $this->single_product_renderer_hook(), - array( - $this, - 'button_renderer', - ), + function () { + $product = wc_get_product(); + + if ( + is_a( $product, WC_Product::class ) + && ! $this->product_supports_payment( $product ) + ) { + + return; + } + + $this->button_renderer( PayPalGateway::ID ); + }, 31 ); } - add_action( $this->pay_order_renderer_hook(), array( $this, 'button_renderer' ), 10 ); - $not_enabled_on_minicart = $this->settings->has( 'button_mini_cart_enabled' ) && ! $this->settings->get( 'button_mini_cart_enabled' ); if ( @@ -457,21 +466,38 @@ class SmartButton implements SmartButtonInterface { ); } - add_action( $this->checkout_button_renderer_hook(), array( $this, 'button_renderer' ), 10 ); + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - $not_enabled_on_cart = $this->settings->has( 'button_cart_enabled' ) && - ! $this->settings->get( 'button_cart_enabled' ); - add_action( - $this->proceed_to_checkout_button_renderer_hook(), - function() use ( $not_enabled_on_cart ) { - if ( ! is_cart() || $not_enabled_on_cart || $this->is_free_trial_cart() || $this->is_cart_price_total_zero() ) { - return; + if ( isset( $available_gateways['ppcp-gateway'] ) ) { + add_action( + $this->pay_order_renderer_hook(), + function (): void { + $this->button_renderer( PayPalGateway::ID ); + $this->button_renderer( CardButtonGateway::ID ); } + ); + add_action( + $this->checkout_button_renderer_hook(), + function (): void { + $this->button_renderer( PayPalGateway::ID ); + $this->button_renderer( CardButtonGateway::ID ); + } + ); - $this->button_renderer(); - }, - 20 - ); + $not_enabled_on_cart = $this->settings->has( 'button_cart_enabled' ) && + ! $this->settings->get( 'button_cart_enabled' ); + add_action( + $this->proceed_to_checkout_button_renderer_hook(), + function() use ( $not_enabled_on_cart ) { + if ( ! is_cart() || $not_enabled_on_cart || $this->is_free_trial_cart() || $this->is_cart_price_total_zero() ) { + return; + } + + $this->button_renderer( PayPalGateway::ID ); + }, + 20 + ); + } return true; } @@ -524,32 +550,24 @@ class SmartButton implements SmartButtonInterface { /** * Renders the HTML for the buttons. + * + * @param string $gateway_id The gateway ID, like 'ppcp-gateway'. */ - public function button_renderer() { + public function button_renderer( string $gateway_id ) { if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) { return; } - $product = wc_get_product(); - - if ( - ! is_checkout() && is_a( $product, WC_Product::class ) - && ! $this->product_supports_payment( $product ) - ) { - - return; - } - $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - if ( ! isset( $available_gateways['ppcp-gateway'] ) ) { + if ( ! isset( $available_gateways[ $gateway_id ] ) ) { return; } // The wrapper is needed for the loading spinner, // otherwise jQuery block() prevents buttons rendering. - echo '
'; + echo '
'; } /** @@ -810,7 +828,7 @@ class SmartButton implements SmartButtonInterface { 'bn_codes' => $this->bn_codes(), 'payer' => $this->payerData(), 'button' => array( - 'wrapper' => '#ppc-button', + 'wrapper' => '#ppc-button-' . PayPalGateway::ID, 'mini_cart_wrapper' => '#ppc-button-minicart', 'cancel_wrapper' => '#ppcp-cancel', 'url' => $this->url(), @@ -830,10 +848,19 @@ class SmartButton implements SmartButtonInterface { 'tagline' => $this->style_for_context( 'tagline', $this->context() ), ), ), + 'separate_buttons' => array( + 'card' => array( + 'id' => CardButtonGateway::ID, + 'wrapper' => '#ppc-button-' . CardButtonGateway::ID, + 'style' => array( + 'shape' => $this->style_for_context( 'shape', $this->context() ), + // TODO: color black, white from the gateway settings. + ), + ), + ), 'hosted_fields' => array( - 'wrapper' => '#ppcp-hosted-fields', - 'mini_cart_wrapper' => '#ppcp-hosted-fields-mini-cart', - 'labels' => array( + 'wrapper' => '#ppcp-hosted-fields', + 'labels' => array( 'credit_card_number' => '', 'cvv' => '', 'mm_yy' => __( 'MM/YY', 'woocommerce-paypal-payments' ), @@ -847,21 +874,36 @@ class SmartButton implements SmartButtonInterface { ), 'cardholder_name_required' => __( 'Cardholder\'s first and last name are required, please fill the checkout form required fields.', 'woocommerce-paypal-payments' ), ), - 'valid_cards' => $this->dcc_applies->valid_cards(), - 'contingency' => $this->get_3ds_contingency(), + 'valid_cards' => $this->dcc_applies->valid_cards(), + 'contingency' => $this->get_3ds_contingency(), ), 'messages' => $this->message_values(), 'labels' => array( - 'error' => array( - 'generic' => __( + 'error' => array( + 'generic' => __( 'Something went wrong. Please try again or choose another payment source.', 'woocommerce-paypal-payments' ), - 'js_validation' => __( - 'Required form fields are not filled or invalid.', - 'woocommerce-paypal-payments' + 'required' => array( + 'generic' => __( + 'Required form fields are not filled.', + 'woocommerce-paypal-payments' + ), + // phpcs:ignore WordPress.WP.I18n + 'field' => __( '%s is a required field.', 'woocommerce' ), + 'elements' => array( // Map
=> text for error messages. + 'terms' => __( + 'Please read and accept the terms and conditions to proceed with your order.', + // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch + 'woocommerce' + ), + ), ), ), + // phpcs:ignore WordPress.WP.I18n + 'billing_field' => _x( 'Billing %s', 'checkout-validation', 'woocommerce' ), + // phpcs:ignore WordPress.WP.I18n + 'shipping_field' => _x( 'Shipping %s', 'checkout-validation', 'woocommerce' ), ), 'order_id' => 'pay-now' === $this->context() ? absint( $wp->query_vars['order-pay'] ) : 0, 'single_product_buttons_enabled' => $this->settings->has( 'button_product_enabled' ) && $this->settings->get( 'button_product_enabled' ), @@ -870,10 +912,10 @@ class SmartButton implements SmartButtonInterface { ); if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) { - unset( $localize['button']['mini_cart_style']['tagline'] ); + $localize['button']['mini_cart_style']['tagline'] = false; } if ( $this->style_for_context( 'layout', $this->context() ) !== 'horizontal' ) { - unset( $localize['button']['style']['tagline'] ); + $localize['button']['style']['tagline'] = false; } $this->request_data->dequeue_nonce_fix(); @@ -901,7 +943,9 @@ class SmartButton implements SmartButtonInterface { * @throws \WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException If a setting was not found. */ private function url(): string { - $intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture'; + $intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture'; + $product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent; + $other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent; $params = array( 'client-id' => $this->client_id, @@ -910,9 +954,7 @@ class SmartButton implements SmartButtonInterface { 'components' => implode( ',', $this->components() ), 'vault' => $this->can_save_vault_token() ? 'true' : 'false', 'commit' => is_checkout() ? 'true' : 'false', - 'intent' => ( $this->subscription_helper->cart_contains_subscription() || $this->subscription_helper->current_product_is_subscription() ) - ? 'authorize' - : $intent, + 'intent' => $this->context() === 'product' ? $product_intent : $other_context_intent, ); if ( $this->environment->current_environment_is( Environment::SANDBOX ) @@ -933,7 +975,10 @@ class SmartButton implements SmartButtonInterface { $is_dcc_enabled = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ); - if ( is_checkout() && $is_dcc_enabled ) { + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $is_separate_card_enabled = isset( $available_gateways[ CardButtonGateway::ID ] ); + + if ( is_checkout() && ( $is_dcc_enabled || $is_separate_card_enabled ) ) { $key = array_search( 'card', $disable_funding, true ); if ( false !== $key ) { unset( $disable_funding[ $key ] ); @@ -942,7 +987,7 @@ class SmartButton implements SmartButtonInterface { if ( $this->is_free_trial_cart() ) { $all_sources = array_keys( $this->all_funding_sources ); - if ( $is_dcc_enabled ) { + if ( $is_dcc_enabled || $is_separate_card_enabled ) { $all_sources = array_diff( $all_sources, array( 'card' ) ); } $disable_funding = $all_sources; @@ -1018,6 +1063,7 @@ class SmartButton implements SmartButtonInterface { if ( $this->load_button_component() ) { $components[] = 'buttons'; + $components[] = 'funding-eligibility'; } if ( $this->messages_apply->for_country() @@ -1049,9 +1095,10 @@ class SmartButton implements SmartButtonInterface { } if ( $this->context() === 'product' - && $this->settings->has( 'button_product_enabled' ) - && $this->settings->get( 'button_product_enabled' ) - || $this->settings->has( 'message_product_enabled' ) + && ( + ( $this->settings->has( 'button_product_enabled' ) && $this->settings->get( 'button_product_enabled' ) ) || + ( $this->settings->has( 'message_product_enabled' ) && $this->settings->get( 'message_product_enabled' ) ) + ) ) { $load_buttons = true; } @@ -1061,14 +1108,17 @@ class SmartButton implements SmartButtonInterface { ) { $load_buttons = true; } + if ( $this->context() === 'cart' - && $this->settings->has( 'button_cart_enabled' ) - && $this->settings->get( 'button_cart_enabled' ) - || $this->settings->has( 'message_product_enabled' ) + && ( + ( $this->settings->has( 'button_cart_enabled' ) && $this->settings->get( 'button_cart_enabled' ) ) || + ( $this->settings->has( 'message_cart_enabled' ) && $this->settings->get( 'message_cart_enabled' ) ) + ) ) { $load_buttons = true; } + if ( $this->context() === 'pay-now' ) { $load_buttons = true; } @@ -1112,6 +1162,9 @@ class SmartButton implements SmartButtonInterface { if ( $source && $source->card() ) { return false; // Ignore for DCC. } + if ( 'card' === $this->session_handler->funding_source() ) { + return false; // Ignore for card buttons. + } return true; } diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index c0045c8c4..b9a59c490 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface; use stdClass; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount; +use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; @@ -27,7 +28,9 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; +use WooCommerce\PayPalCommerce\WcGateway\CardBillingMode; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -118,6 +121,13 @@ class CreateOrderEndpoint implements EndpointInterface { */ private $registration_needed; + /** + * The value of card_billing_data_mode from the settings. + * + * @var string + */ + protected $card_billing_data_mode; + /** * The logger. * @@ -137,6 +147,7 @@ class CreateOrderEndpoint implements EndpointInterface { * @param Settings $settings The Settings object. * @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object. * @param bool $registration_needed Whether a new user must be registered during checkout. + * @param string $card_billing_data_mode The value of card_billing_data_mode from the settings. * @param LoggerInterface $logger The logger. */ public function __construct( @@ -149,6 +160,7 @@ class CreateOrderEndpoint implements EndpointInterface { Settings $settings, EarlyOrderHandler $early_order_handler, bool $registration_needed, + string $card_billing_data_mode, LoggerInterface $logger ) { @@ -161,6 +173,7 @@ class CreateOrderEndpoint implements EndpointInterface { $this->settings = $settings; $this->early_order_handler = $early_order_handler; $this->registration_needed = $registration_needed; + $this->card_billing_data_mode = $card_billing_data_mode; $this->logger = $logger; } @@ -204,7 +217,7 @@ class CreateOrderEndpoint implements EndpointInterface { // The cart does not have any info about payment method, so we must handle free trial here. if ( ( - CreditCardGateway::ID === $payment_method + in_array( $payment_method, array( CreditCardGateway::ID, CardButtonGateway::ID ), true ) || ( PayPalGateway::ID === $payment_method && 'card' === $funding_source ) ) && $this->is_free_trial_cart() @@ -331,18 +344,40 @@ class CreateOrderEndpoint implements EndpointInterface { private function create_paypal_order( \WC_Order $wc_order = null ): Order { assert( $this->purchase_unit instanceof PurchaseUnit ); + $funding_source = $this->parsed_request_data['funding_source'] ?? ''; + $payer = $this->payer( $this->parsed_request_data, $wc_order ); + $shipping_preference = $this->shipping_preference_factory->from_state( $this->purchase_unit, $this->parsed_request_data['context'], WC()->cart, - $this->parsed_request_data['funding_source'] ?? '' + $funding_source ); + if ( 'card' === $funding_source ) { + if ( CardBillingMode::MINIMAL_INPUT === $this->card_billing_data_mode ) { + if ( ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS === $shipping_preference ) { + if ( $payer ) { + $payer->set_address( null ); + } + } + if ( ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING === $shipping_preference ) { + if ( $payer ) { + $payer->set_name( null ); + } + } + } + + if ( CardBillingMode::NO_WC === $this->card_billing_data_mode ) { + $payer = null; + } + } + try { return $this->api_endpoint->create( array( $this->purchase_unit ), $shipping_preference, - $this->payer( $this->parsed_request_data, $wc_order ), + $payer, null, $this->payment_method() ); @@ -364,7 +399,7 @@ class CreateOrderEndpoint implements EndpointInterface { return $this->api_endpoint->create( array( $this->purchase_unit ), $shipping_preference, - $this->payer( $this->parsed_request_data, $wc_order ), + $payer, null, $this->payment_method() ); diff --git a/modules/ppcp-button/yarn.lock b/modules/ppcp-button/yarn.lock index e34a77cfb..7ffe05930 100644 --- a/modules/ppcp-button/yarn.lock +++ b/modules/ppcp-button/yarn.lock @@ -1353,6 +1353,11 @@ debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" diff --git a/modules/ppcp-session/src/Cancellation/CancelController.php b/modules/ppcp-session/src/Cancellation/CancelController.php index 51d48008c..3c23983f5 100644 --- a/modules/ppcp-session/src/Cancellation/CancelController.php +++ b/modules/ppcp-session/src/Cancellation/CancelController.php @@ -70,6 +70,10 @@ class CancelController { return; // Ignore for DCC. } + if ( 'card' === $this->session_handler->funding_source() ) { + return; // Ignore for card buttons. + } + $url = add_query_arg( array( $param_name => wp_create_nonce( $nonce ) ), wc_get_checkout_url() ); add_action( 'woocommerce_review_order_after_submit', diff --git a/modules/ppcp-subscription/services.php b/modules/ppcp-subscription/services.php index 026d26965..bf112050c 100644 --- a/modules/ppcp-subscription/services.php +++ b/modules/ppcp-subscription/services.php @@ -24,6 +24,8 @@ return array( $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); $payer_factory = $container->get( 'api.factory.payer' ); $environment = $container->get( 'onboarding.environment' ); + $settings = $container->get( 'wcgateway.settings' ); + $authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' ); return new RenewalHandler( $logger, $repository, @@ -31,7 +33,9 @@ return array( $purchase_unit_factory, $container->get( 'api.factory.shipping-preference' ), $payer_factory, - $environment + $environment, + $settings, + $authorized_payments_processor ); }, 'subscription.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository { diff --git a/modules/ppcp-subscription/src/RenewalHandler.php b/modules/ppcp-subscription/src/RenewalHandler.php index eebd76533..771a57c9d 100644 --- a/modules/ppcp-subscription/src/RenewalHandler.php +++ b/modules/ppcp-subscription/src/RenewalHandler.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Subscription; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; @@ -17,10 +18,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use Psr\Log\LoggerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; /** * Class RenewalHandler @@ -80,16 +83,32 @@ class RenewalHandler { */ protected $environment; + /** + * The settings + * + * @var Settings + */ + protected $settings; + + /** + * The processor for authorized payments. + * + * @var AuthorizedPaymentsProcessor + */ + protected $authorized_payments_processor; + /** * RenewalHandler constructor. * - * @param LoggerInterface $logger The logger. - * @param PaymentTokenRepository $repository The payment token repository. - * @param OrderEndpoint $order_endpoint The order endpoint. - * @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory. - * @param ShippingPreferenceFactory $shipping_preference_factory The shipping_preference factory. - * @param PayerFactory $payer_factory The payer factory. - * @param Environment $environment The environment. + * @param LoggerInterface $logger The logger. + * @param PaymentTokenRepository $repository The payment token repository. + * @param OrderEndpoint $order_endpoint The order endpoint. + * @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory. + * @param ShippingPreferenceFactory $shipping_preference_factory The shipping_preference factory. + * @param PayerFactory $payer_factory The payer factory. + * @param Environment $environment The environment. + * @param Settings $settings The Settings. + * @param AuthorizedPaymentsProcessor $authorized_payments_processor The Authorized Payments Processor. */ public function __construct( LoggerInterface $logger, @@ -98,16 +117,20 @@ class RenewalHandler { PurchaseUnitFactory $purchase_unit_factory, ShippingPreferenceFactory $shipping_preference_factory, PayerFactory $payer_factory, - Environment $environment + Environment $environment, + Settings $settings, + AuthorizedPaymentsProcessor $authorized_payments_processor ) { - $this->logger = $logger; - $this->repository = $repository; - $this->order_endpoint = $order_endpoint; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->payer_factory = $payer_factory; - $this->environment = $environment; + $this->logger = $logger; + $this->repository = $repository; + $this->order_endpoint = $order_endpoint; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->payer_factory = $payer_factory; + $this->environment = $environment; + $this->settings = $settings; + $this->authorized_payments_processor = $authorized_payments_processor; } /** @@ -179,6 +202,14 @@ class RenewalHandler { } $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( + __( 'Payment successfully captured.', 'woocommerce-paypal-payments' ) + ); + $wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'true' ); + $wc_order->update_status( 'completed' ); + } } /** @@ -229,4 +260,39 @@ class RenewalHandler { return current( $tokens ); } + + /** + * Returns if an order should be captured immediately. + * + * @param Order $order The PayPal order. + * + * @return bool + * @throws NotFoundException When a setting was not found. + */ + protected function capture_authorized_downloads( Order $order ): bool { + if ( + ! $this->settings->has( 'capture_for_virtual_only' ) + || ! $this->settings->get( 'capture_for_virtual_only' ) + ) { + return false; + } + + if ( $order->intent() === 'CAPTURE' ) { + return false; + } + + /** + * We fetch the order again as the authorize endpoint (from which the Order derives) + * drops the item's category, making it impossible to check, if purchase units contain + * physical goods. + */ + $order = $this->order_endpoint->order( $order->id() ); + + foreach ( $order->purchase_units() as $unit ) { + if ( $unit->contains_physical_goods() ) { + return false; + } + } + return true; + } } diff --git a/modules/ppcp-vaulting/src/PaymentTokenChecker.php b/modules/ppcp-vaulting/src/PaymentTokenChecker.php index 604c7cc37..849c7a08f 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenChecker.php +++ b/modules/ppcp-vaulting/src/PaymentTokenChecker.php @@ -16,6 +16,7 @@ use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; @@ -118,7 +119,7 @@ class PaymentTokenChecker { if ( $tokens ) { try { if ( $this->is_free_trial_order( $wc_order ) ) { - if ( CreditCardGateway::ID === $wc_order->get_payment_method() + if ( in_array( $wc_order->get_payment_method(), array( CreditCardGateway::ID, CardButtonGateway::ID ), true ) || ( PayPalGateway::ID === $wc_order->get_payment_method() && 'card' === $wc_order->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY ) ) ) { $order = $this->order_repository->for_wc_order( $wc_order ); diff --git a/modules/ppcp-wc-gateway/assets/images/oxxo.svg b/modules/ppcp-wc-gateway/assets/images/oxxo.svg new file mode 100644 index 000000000..c67d3c53d --- /dev/null +++ b/modules/ppcp-wc-gateway/assets/images/oxxo.svg @@ -0,0 +1,18 @@ + + + + logo OXXO + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/ppcp-wc-gateway/resources/js/oxxo.js b/modules/ppcp-wc-gateway/resources/js/oxxo.js new file mode 100644 index 000000000..a6d976a9c --- /dev/null +++ b/modules/ppcp-wc-gateway/resources/js/oxxo.js @@ -0,0 +1,18 @@ +document.addEventListener( + 'DOMContentLoaded', + function() { + jQuery('form.checkout').on('checkout_place_order_success', function(type, data) { + if(data.payer_action && data.payer_action !== '') { + const width = screen.width / 2; + const height = screen.height / 2; + const left = width - (width / 2); + const top = height - (height / 2); + window.open( + data.payer_action, + '_blank', + 'popup, width=' + width + ', height=' + height + ', top=' + top + ', left=' + left + ); + } + }); + } +); diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index a9e8f44a0..63948c96f 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -29,7 +29,11 @@ use WooCommerce\PayPalCommerce\WcGateway\Checkout\CheckoutPayPalAddressPreset; use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\FraudNet; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\FraudNetSessionId; @@ -38,13 +42,14 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PaymentSourceFac use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoice; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider; +use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper; use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; -use WooCommerce\PayPalCommerce\WcGateway\Notice\DccWithoutPayPalAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\GatewayWithoutPayPalAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; @@ -55,11 +60,10 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; return array( - 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { + 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { $order_processor = $container->get( 'wcgateway.order-processor' ); $settings_renderer = $container->get( 'wcgateway.settings.render' ); $funding_source_renderer = $container->get( 'wcgateway.funding-source.renderer' ); - $authorized_payments = $container->get( 'wcgateway.processor.authorized-payments' ); $settings = $container->get( 'wcgateway.settings' ); $session_handler = $container->get( 'session.handler' ); $refund_processor = $container->get( 'wcgateway.processor.refunds' ); @@ -68,8 +72,6 @@ return array( $subscription_helper = $container->get( 'subscription.helper' ); $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); $payment_token_repository = $container->get( 'vaulting.repository.payment-token' ); - $payments_endpoint = $container->get( 'api.endpoint.payments' ); - $order_endpoint = $container->get( 'api.endpoint.order' ); $environment = $container->get( 'onboarding.environment' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); $api_shop_country = $container->get( 'api.shop.country' ); @@ -77,7 +79,6 @@ return array( $settings_renderer, $funding_source_renderer, $order_processor, - $authorized_payments, $settings, $session_handler, $refund_processor, @@ -87,14 +88,11 @@ return array( $page_id, $environment, $payment_token_repository, - $container->get( 'api.factory.shipping-preference' ), $logger, - $payments_endpoint, - $order_endpoint, $api_shop_country ); }, - 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { + 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { $order_processor = $container->get( 'wcgateway.order-processor' ); $settings_renderer = $container->get( 'wcgateway.settings.render' ); $authorized_payments = $container->get( 'wcgateway.processor.authorized-payments' ); @@ -133,27 +131,43 @@ return array( $payments_endpoint ); }, - 'wcgateway.disabler' => static function ( ContainerInterface $container ): DisableGateways { + 'wcgateway.card-button-gateway' => static function ( ContainerInterface $container ): CardButtonGateway { + return new CardButtonGateway( + $container->get( 'wcgateway.settings.render' ), + $container->get( 'wcgateway.order-processor' ), + $container->get( 'wcgateway.settings' ), + $container->get( 'session.handler' ), + $container->get( 'wcgateway.processor.refunds' ), + $container->get( 'onboarding.state' ), + $container->get( 'wcgateway.transaction-url-provider' ), + $container->get( 'subscription.helper' ), + $container->get( 'wcgateway.settings.allow_card_button_gateway.default' ), + $container->get( 'onboarding.environment' ), + $container->get( 'vaulting.repository.payment-token' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'wcgateway.disabler' => static function ( ContainerInterface $container ): DisableGateways { $session_handler = $container->get( 'session.handler' ); $settings = $container->get( 'wcgateway.settings' ); return new DisableGateways( $session_handler, $settings ); }, - 'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool { + 'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool { $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; return 'wc-settings' === $page && 'checkout' === $tab; }, - 'wcgateway.is-ppcp-settings-page' => static function ( ContainerInterface $container ): bool { + 'wcgateway.is-ppcp-settings-page' => static function ( ContainerInterface $container ): bool { if ( ! $container->get( 'wcgateway.is-wc-payments-page' ) ) { return false; } $section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : ''; - return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID, PayUponInvoiceGateway::ID ), true ); + return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID, PayUponInvoiceGateway::ID, CardButtonGateway::ID, OXXOGateway::ID ), true ); }, - 'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string { + 'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string { if ( ! $container->get( 'wcgateway.is-ppcp-settings-page' ) ) { return ''; } @@ -164,36 +178,70 @@ return array( return $ppcp_tab ? $ppcp_tab : $section; }, - 'wcgateway.settings' => static function ( ContainerInterface $container ): Settings { + 'wcgateway.settings' => static function ( ContainerInterface $container ): Settings { return new Settings(); }, - 'wcgateway.notice.connect' => static function ( ContainerInterface $container ): ConnectAdminNotice { + 'wcgateway.notice.connect' => static function ( ContainerInterface $container ): ConnectAdminNotice { $state = $container->get( 'onboarding.state' ); $settings = $container->get( 'wcgateway.settings' ); return new ConnectAdminNotice( $state, $settings ); }, - 'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): DccWithoutPayPalAdminNotice { - $state = $container->get( 'onboarding.state' ); - $settings = $container->get( 'wcgateway.settings' ); - $is_payments_page = $container->get( 'wcgateway.is-wc-payments-page' ); - $is_ppcp_settings_page = $container->get( 'wcgateway.is-ppcp-settings-page' ); - return new DccWithoutPayPalAdminNotice( $state, $settings, $is_payments_page, $is_ppcp_settings_page ); + 'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice { + return new GatewayWithoutPayPalAdminNotice( + CreditCardGateway::ID, + $container->get( 'onboarding.state' ), + $container->get( 'wcgateway.settings' ), + $container->get( 'wcgateway.is-wc-payments-page' ), + $container->get( 'wcgateway.is-ppcp-settings-page' ) + ); }, - 'wcgateway.notice.authorize-order-action' => + 'wcgateway.notice.card-button-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice { + return new GatewayWithoutPayPalAdminNotice( + CardButtonGateway::ID, + $container->get( 'onboarding.state' ), + $container->get( 'wcgateway.settings' ), + $container->get( 'wcgateway.is-wc-payments-page' ), + $container->get( 'wcgateway.is-ppcp-settings-page' ) + ); + }, + 'wcgateway.notice.authorize-order-action' => static function ( ContainerInterface $container ): AuthorizeOrderActionNotice { return new AuthorizeOrderActionNotice(); }, - 'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer { + 'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer { return new SectionsRenderer( $container->get( 'wcgateway.current-ppcp-settings-page-id' ), - $container->get( 'api.shop.country' ) + $container->get( 'wcgateway.settings.sections' ) ); }, - 'wcgateway.settings.status' => static function ( ContainerInterface $container ): SettingsStatus { + 'wcgateway.settings.sections' => static function ( ContainerInterface $container ): array { + $sections = array( + PayPalGateway::ID => __( 'PayPal Checkout', 'woocommerce-paypal-payments' ), + CreditCardGateway::ID => __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ), + CardButtonGateway::ID => __( 'PayPal Card Button', 'woocommerce-paypal-payments' ), + OXXOGateway::ID => __( 'OXXO', 'woocommerce-paypal-payments' ), + PayUponInvoiceGateway::ID => __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), + WebhooksStatusPage::ID => __( 'Webhooks Status', 'woocommerce-paypal-payments' ), + ); + + // Remove for all not registered in WC gateways that cannot render anything in this case. + $gateways = WC()->payment_gateways->payment_gateways(); + foreach ( array_diff( + array_keys( $sections ), + array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID ) + ) as $id ) { + if ( ! isset( $gateways[ $id ] ) ) { + unset( $sections[ $id ] ); + } + } + + return $sections; + }, + 'wcgateway.settings.status' => static function ( ContainerInterface $container ): SettingsStatus { $settings = $container->get( 'wcgateway.settings' ); return new SettingsStatus( $settings ); }, - 'wcgateway.settings.render' => static function ( ContainerInterface $container ): SettingsRenderer { + 'wcgateway.settings.render' => static function ( ContainerInterface $container ): SettingsRenderer { $settings = $container->get( 'wcgateway.settings' ); $state = $container->get( 'onboarding.state' ); $fields = $container->get( 'wcgateway.settings.fields' ); @@ -213,7 +261,7 @@ return array( $page_id ); }, - 'wcgateway.settings.listener' => static function ( ContainerInterface $container ): SettingsListener { + 'wcgateway.settings.listener' => static function ( ContainerInterface $container ): SettingsListener { $settings = $container->get( 'wcgateway.settings' ); $fields = $container->get( 'wcgateway.settings.fields' ); $webhook_registrar = $container->get( 'webhook.registrar' ); @@ -235,7 +283,7 @@ return array( $signup_link_ids ); }, - 'wcgateway.order-processor' => static function ( ContainerInterface $container ): OrderProcessor { + 'wcgateway.order-processor' => static function ( ContainerInterface $container ): OrderProcessor { $session_handler = $container->get( 'session.handler' ); $order_endpoint = $container->get( 'api.endpoint.order' ); @@ -260,13 +308,13 @@ return array( $order_helper ); }, - 'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor { + 'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor { $order_endpoint = $container->get( 'api.endpoint.order' ); $payments_endpoint = $container->get( 'api.endpoint.payments' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new RefundProcessor( $order_endpoint, $payments_endpoint, $logger ); }, - 'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor { + 'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor { $order_endpoint = $container->get( 'api.endpoint.order' ); $payments_endpoint = $container->get( 'api.endpoint.payments' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); @@ -282,23 +330,23 @@ return array( $subscription_helper ); }, - 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { + 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); return new RenderAuthorizeAction( $column ); }, - 'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail { + 'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); return new PaymentStatusOrderDetail( $column ); }, - 'wcgateway.admin.orders-payment-status-column' => static function ( ContainerInterface $container ): OrderTablePaymentStatusColumn { + 'wcgateway.admin.orders-payment-status-column' => static function ( ContainerInterface $container ): OrderTablePaymentStatusColumn { $settings = $container->get( 'wcgateway.settings' ); return new OrderTablePaymentStatusColumn( $settings ); }, - 'wcgateway.admin.fees-renderer' => static function ( ContainerInterface $container ): FeesRenderer { + 'wcgateway.admin.fees-renderer' => static function ( ContainerInterface $container ): FeesRenderer { return new FeesRenderer(); }, - 'wcgateway.settings.fields' => static function ( ContainerInterface $container ): array { + 'wcgateway.settings.fields' => static function ( ContainerInterface $container ): array { $state = $container->get( 'onboarding.state' ); assert( $state instanceof State ); @@ -861,6 +909,40 @@ return array( 'requirements' => array(), 'gateway' => 'paypal', ), + 'card_billing_data_mode' => array( + 'title' => __( 'Card billing data handling', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'desc_tip' => true, + 'description' => __( 'Using the WC form data increases convenience for the customers, but can cause issues if card details do not match the billing data in the checkout form.', 'woocommerce-paypal-payments' ), + 'default' => $container->get( 'wcgateway.settings.card_billing_data_mode.default' ), + 'options' => array( + CardBillingMode::USE_WC => __( 'Use WC checkout form data (do not show any address fields)', 'woocommerce-paypal-payments' ), + CardBillingMode::MINIMAL_INPUT => __( 'Request only name and postal code', 'woocommerce-paypal-payments' ), + CardBillingMode::NO_WC => __( 'Do not use WC checkout form data (request all address fields)', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( + State::STATE_START, + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => array( 'paypal', CardButtonGateway::ID ), + ), + 'allow_card_button_gateway' => array( + 'title' => __( 'Separate Card Button from PayPal gateway', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'desc_tip' => true, + 'label' => __( 'Enable a separate payment gateway for the branded PayPal Debit or Credit Card button.', 'woocommerce-paypal-payments' ), + 'description' => __( 'By default, the Debit or Credit Card button is displayed in the PayPal Checkout payment gateway. This setting creates a second gateway for the Card button.', 'woocommerce-paypal-payments' ), + 'default' => $container->get( 'wcgateway.settings.allow_card_button_gateway.default' ), + 'screens' => array( + State::STATE_START, + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + ), // General button styles. 'button_style_heading' => array( @@ -2071,10 +2153,14 @@ return array( $fields['disable_cards']['options'] = $card_options; $fields['card_icons']['options'] = array_merge( $dark_versions, $card_options ); + if ( defined( 'PPCP_FLAG_SEPARATE_APM_BUTTONS' ) && PPCP_FLAG_SEPARATE_APM_BUTTONS === false ) { + unset( $fields['allow_card_button_gateway'] ); + } + return $fields; }, - 'wcgateway.all-funding-sources' => static function( ContainerInterface $container ): array { + 'wcgateway.all-funding-sources' => static function( ContainerInterface $container ): array { return array( 'card' => _x( 'Credit or debit cards', 'Name of payment method', 'woocommerce-paypal-payments' ), 'credit' => _x( 'Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ), @@ -2092,28 +2178,28 @@ return array( ); }, - 'wcgateway.checkout.address-preset' => static function( ContainerInterface $container ): CheckoutPayPalAddressPreset { + 'wcgateway.checkout.address-preset' => static function( ContainerInterface $container ): CheckoutPayPalAddressPreset { return new CheckoutPayPalAddressPreset( $container->get( 'session.handler' ) ); }, - 'wcgateway.url' => static function ( ContainerInterface $container ): string { + 'wcgateway.url' => static function ( ContainerInterface $container ): string { return plugins_url( $container->get( 'wcgateway.relative-path' ), dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, - 'wcgateway.relative-path' => static function( ContainerInterface $container ): string { + 'wcgateway.relative-path' => static function( ContainerInterface $container ): string { return 'modules/ppcp-wc-gateway/'; }, - 'wcgateway.absolute-path' => static function( ContainerInterface $container ): string { + 'wcgateway.absolute-path' => static function( ContainerInterface $container ): string { return plugin_dir_path( dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ) . $container->get( 'wcgateway.relative-path' ); }, - 'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint { + 'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint { $gateway = $container->get( 'wcgateway.paypal-gateway' ); $endpoint = $container->get( 'api.endpoint.order' ); $prefix = $container->get( 'api.prefix' ); @@ -2124,40 +2210,43 @@ return array( ); }, - 'wcgateway.transaction-url-sandbox' => static function ( ContainerInterface $container ): string { + 'wcgateway.transaction-url-sandbox' => static function ( ContainerInterface $container ): string { return 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; }, - 'wcgateway.transaction-url-live' => static function ( ContainerInterface $container ): string { + 'wcgateway.transaction-url-live' => static function ( ContainerInterface $container ): string { return 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; }, - 'wcgateway.transaction-url-provider' => static function ( ContainerInterface $container ): TransactionUrlProvider { + 'wcgateway.transaction-url-provider' => static function ( ContainerInterface $container ): TransactionUrlProvider { $sandbox_url_base = $container->get( 'wcgateway.transaction-url-sandbox' ); $live_url_base = $container->get( 'wcgateway.transaction-url-live' ); return new TransactionUrlProvider( $sandbox_url_base, $live_url_base ); }, - 'wcgateway.helper.dcc-product-status' => static function ( ContainerInterface $container ) : DCCProductStatus { + 'wcgateway.helper.dcc-product-status' => static function ( ContainerInterface $container ) : DCCProductStatus { $settings = $container->get( 'wcgateway.settings' ); $partner_endpoint = $container->get( 'api.endpoint.partners' ); return new DCCProductStatus( $settings, $partner_endpoint ); }, - 'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers { + 'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers { return new MessagesDisclaimers( $container->get( 'api.shop.country' ) ); }, - 'wcgateway.funding-source.renderer' => function ( ContainerInterface $container ) : FundingSourceRenderer { + 'wcgateway.funding-source.renderer' => function ( ContainerInterface $container ) : FundingSourceRenderer { return new FundingSourceRenderer( $container->get( 'wcgateway.settings' ) ); }, - 'wcgateway.pay-upon-invoice-order-endpoint' => static function ( ContainerInterface $container ): PayUponInvoiceOrderEndpoint { + 'wcgateway.checkout-helper' => static function ( ContainerInterface $container ): CheckoutHelper { + return new CheckoutHelper(); + }, + 'wcgateway.pay-upon-invoice-order-endpoint' => static function ( ContainerInterface $container ): PayUponInvoiceOrderEndpoint { return new PayUponInvoiceOrderEndpoint( $container->get( 'api.host' ), $container->get( 'api.bearer' ), @@ -2166,10 +2255,10 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'wcgateway.pay-upon-invoice-payment-source-factory' => static function ( ContainerInterface $container ): PaymentSourceFactory { + 'wcgateway.pay-upon-invoice-payment-source-factory' => static function ( ContainerInterface $container ): PaymentSourceFactory { return new PaymentSourceFactory(); }, - 'wcgateway.pay-upon-invoice-gateway' => static function ( ContainerInterface $container ): PayUponInvoiceGateway { + 'wcgateway.pay-upon-invoice-gateway' => static function ( ContainerInterface $container ): PayUponInvoiceGateway { return new PayUponInvoiceGateway( $container->get( 'wcgateway.pay-upon-invoice-order-endpoint' ), $container->get( 'api.factory.purchase-unit' ), @@ -2177,16 +2266,17 @@ return array( $container->get( 'onboarding.environment' ), $container->get( 'wcgateway.transaction-url-provider' ), $container->get( 'woocommerce.logger.woocommerce' ), - $container->get( 'wcgateway.pay-upon-invoice-helper' ) + $container->get( 'wcgateway.pay-upon-invoice-helper' ), + $container->get( 'wcgateway.checkout-helper' ) ); }, - 'wcgateway.pay-upon-invoice-fraudnet-session-id' => static function ( ContainerInterface $container ): FraudNetSessionId { + 'wcgateway.pay-upon-invoice-fraudnet-session-id' => static function ( ContainerInterface $container ): FraudNetSessionId { return new FraudNetSessionId(); }, 'wcgateway.pay-upon-invoice-fraudnet-source-website-id' => static function ( ContainerInterface $container ): FraudNetSourceWebsiteId { return new FraudNetSourceWebsiteId( $container->get( 'api.merchant_id' ) ); }, - 'wcgateway.pay-upon-invoice-fraudnet' => static function ( ContainerInterface $container ): FraudNet { + 'wcgateway.pay-upon-invoice-fraudnet' => static function ( ContainerInterface $container ): FraudNet { $session_id = $container->get( 'wcgateway.pay-upon-invoice-fraudnet-session-id' ); $source_website_id = $container->get( 'wcgateway.pay-upon-invoice-fraudnet-source-website-id' ); return new FraudNet( @@ -2194,16 +2284,18 @@ return array( (string) $source_website_id() ); }, - 'wcgateway.pay-upon-invoice-helper' => static function( ContainerInterface $container ): PayUponInvoiceHelper { - return new PayUponInvoiceHelper(); + 'wcgateway.pay-upon-invoice-helper' => static function( ContainerInterface $container ): PayUponInvoiceHelper { + return new PayUponInvoiceHelper( + $container->get( 'wcgateway.checkout-helper' ) + ); }, - 'wcgateway.pay-upon-invoice-product-status' => static function( ContainerInterface $container ): PayUponInvoiceProductStatus { + 'wcgateway.pay-upon-invoice-product-status' => static function( ContainerInterface $container ): PayUponInvoiceProductStatus { return new PayUponInvoiceProductStatus( $container->get( 'wcgateway.settings' ), $container->get( 'api.endpoint.partners' ) ); }, - 'wcgateway.pay-upon-invoice' => static function ( ContainerInterface $container ): PayUponInvoice { + 'wcgateway.pay-upon-invoice' => static function ( ContainerInterface $container ): PayUponInvoice { return new PayUponInvoice( $container->get( 'wcgateway.url' ), $container->get( 'wcgateway.pay-upon-invoice-fraudnet' ), @@ -2217,10 +2309,36 @@ return array( $container->get( 'wcgateway.current-ppcp-settings-page-id' ), $container->get( 'wcgateway.pay-upon-invoice-product-status' ), $container->get( 'wcgateway.pay-upon-invoice-helper' ), + $container->get( 'wcgateway.checkout-helper' ), $container->get( 'api.factory.capture' ) ); }, - 'wcgateway.logging.is-enabled' => function ( ContainerInterface $container ) : bool { + 'wcgateway.oxxo' => static function( ContainerInterface $container ): OXXO { + return new OXXO( + $container->get( 'wcgateway.checkout-helper' ), + $container->get( 'wcgateway.url' ), + $container->get( 'ppcp.asset-version' ) + ); + }, + 'wcgateway.oxxo-gateway' => static function( ContainerInterface $container ): OXXOGateway { + return new OXXOGateway( + $container->get( 'api.endpoint.order' ), + $container->get( 'api.factory.purchase-unit' ), + $container->get( 'api.factory.shipping-preference' ), + $container->get( 'wcgateway.url' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'wcgateway.endpoint.oxxo' => static function ( ContainerInterface $container ): OXXOEndpoint { + return new OXXOEndpoint( + $container->get( 'button.request-data' ), + $container->get( 'api.endpoint.order' ), + $container->get( 'api.factory.purchase-unit' ), + $container->get( 'api.factory.shipping-preference' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'wcgateway.logging.is-enabled' => function ( ContainerInterface $container ) : bool { $settings = $container->get( 'wcgateway.settings' ); /** @@ -2232,7 +2350,7 @@ return array( ); }, - 'wcgateway.helper.vaulting-scope' => static function ( ContainerInterface $container ): bool { + 'wcgateway.helper.vaulting-scope' => static function ( ContainerInterface $container ): bool { try { $token = $container->get( 'api.bearer' )->bearer(); return $token->vaulting_available(); @@ -2241,7 +2359,7 @@ return array( } }, - 'button.helper.vaulting-label' => static function ( ContainerInterface $container ): string { + 'button.helper.vaulting-label' => static function ( ContainerInterface $container ): string { $vaulting_label = __( 'Enable saved cards and subscription features on your store.', 'woocommerce-paypal-payments' ); if ( ! $container->get( 'wcgateway.helper.vaulting-scope' ) ) { @@ -2263,7 +2381,7 @@ return array( return $vaulting_label; }, - 'wcgateway.settings.fields.pay-later-label' => static function ( ContainerInterface $container ): string { + 'wcgateway.settings.fields.pay-later-label' => static function ( ContainerInterface $container ): string { $pay_later_label = '%s'; $pay_later_label .= ''; $pay_later_label .= __( "You have PayPal vaulting enabled, that's why Pay Later Messaging options are unavailable now. You cannot use both features at the same time.", 'woocommerce-paypal-payments' ); @@ -2271,4 +2389,32 @@ return array( return $pay_later_label; }, + + 'wcgateway.settings.card_billing_data_mode.default' => static function ( ContainerInterface $container ): string { + return $container->get( 'api.shop.is-latin-america' ) ? CardBillingMode::MINIMAL_INPUT : CardBillingMode::USE_WC; + }, + 'wcgateway.settings.card_billing_data_mode' => static function ( ContainerInterface $container ): string { + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof ContainerInterface ); + + return $settings->has( 'card_billing_data_mode' ) ? + (string) $settings->get( 'card_billing_data_mode' ) : + $container->get( 'wcgateway.settings.card_billing_data_mode.default' ); + }, + + 'wcgateway.settings.allow_card_button_gateway.default' => static function ( ContainerInterface $container ): bool { + return $container->get( 'api.shop.is-latin-america' ); + }, + 'wcgateway.settings.allow_card_button_gateway' => static function ( ContainerInterface $container ): bool { + if ( defined( 'PPCP_FLAG_SEPARATE_APM_BUTTONS' ) && PPCP_FLAG_SEPARATE_APM_BUTTONS === false ) { + return false; + } + + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof ContainerInterface ); + + return $settings->has( 'allow_card_button_gateway' ) ? + (bool) $settings->get( 'allow_card_button_gateway' ) : + $container->get( 'wcgateway.settings.allow_card_button_gateway.default' ); + }, ); diff --git a/modules/ppcp-wc-gateway/src/CardBillingMode.php b/modules/ppcp-wc-gateway/src/CardBillingMode.php new file mode 100644 index 000000000..300b186ca --- /dev/null +++ b/modules/ppcp-wc-gateway/src/CardBillingMode.php @@ -0,0 +1,19 @@ +disable_both_gateways() ) { + if ( $this->disable_all_gateways() ) { unset( $methods[ PayPalGateway::ID ] ); unset( $methods[ CreditCardGateway::ID ] ); + unset( $methods[ CardButtonGateway::ID ] ); return $methods; } @@ -77,21 +79,15 @@ class DisableGateways { return $methods; } - if ( $this->is_credit_card() ) { - return array( - CreditCardGateway::ID => $methods[ CreditCardGateway::ID ], - PayPalGateway::ID => $methods[ PayPalGateway::ID ], - ); - } return array( PayPalGateway::ID => $methods[ PayPalGateway::ID ] ); } /** - * Whether both gateways should be disabled or not. + * Whether all gateways should be disabled or not. * * @return bool */ - private function disable_both_gateways() : bool { + private function disable_all_gateways() : bool { if ( ! $this->settings->has( 'enabled' ) || ! $this->settings->get( 'enabled' ) ) { return true; } @@ -110,22 +106,20 @@ class DisableGateways { * @return bool */ private function needs_to_disable_gateways(): bool { - return $this->session_handler->order() !== null; - } - - /** - * Whether the current PayPal session is done via DCC payment. - * - * @return bool - */ - private function is_credit_card(): bool { $order = $this->session_handler->order(); if ( ! $order ) { return false; } - if ( ! $order->payment_source() || ! $order->payment_source()->card() ) { - return false; + + $source = $order->payment_source(); + if ( $source && $source->card() ) { + return false; // DCC. } + + if ( 'card' === $this->session_handler->funding_source() ) { + return false; // Card buttons. + } + return true; } } diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index 6239e39e2..57c29c2a4 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\Webhooks\Handler\PrefixTrait; @@ -51,7 +52,7 @@ class ReturnUrlEndpoint { /** * Handles the incoming request. */ - public function handle_request() { + public function handle_request(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['token'] ) ) { @@ -68,7 +69,12 @@ class ReturnUrlEndpoint { } $wc_order = wc_get_order( $wc_order_id ); - if ( ! $wc_order ) { + if ( ! is_a( $wc_order, \WC_Order::class ) ) { + exit(); + } + + if ( $wc_order->get_payment_method() === OXXOGateway::ID ) { + wp_safe_redirect( wc_get_checkout_url() ); exit(); } diff --git a/modules/ppcp-wc-gateway/src/Exception/GatewayGenericException.php b/modules/ppcp-wc-gateway/src/Exception/GatewayGenericException.php new file mode 100644 index 000000000..cbf157b03 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Exception/GatewayGenericException.php @@ -0,0 +1,32 @@ +getCode() : 0, + $inner + ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php new file mode 100644 index 000000000..45aff4ce6 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php @@ -0,0 +1,364 @@ +id = self::ID; + $this->settings_renderer = $settings_renderer; + $this->order_processor = $order_processor; + $this->config = $config; + $this->session_handler = $session_handler; + $this->refund_processor = $refund_processor; + $this->state = $state; + $this->transaction_url_provider = $transaction_url_provider; + $this->subscription_helper = $subscription_helper; + $this->default_enabled = $default_enabled; + $this->environment = $environment; + $this->onboarded = $state->current_state() === State::STATE_ONBOARDED; + $this->payment_token_repository = $payment_token_repository; + $this->logger = $logger; + + if ( $this->onboarded ) { + $this->supports = array( 'refunds' ); + } + if ( + defined( 'PPCP_FLAG_SUBSCRIPTION' ) + && PPCP_FLAG_SUBSCRIPTION + && $this->gateways_enabled() + && $this->vault_setting_enabled() + ) { + $this->supports = array( + 'refunds', + 'products', + 'subscriptions', + 'subscription_cancellation', + 'subscription_suspension', + 'subscription_reactivation', + 'subscription_amount_changes', + 'subscription_date_changes', + 'subscription_payment_method_change', + 'subscription_payment_method_change_customer', + 'subscription_payment_method_change_admin', + 'multiple_subscriptions', + ); + } + + $this->method_title = __( 'PayPal Card Button', 'woocommerce-paypal-payments' ); + $this->method_description = __( 'The separate payment gateway with the card button. If disabled, the button is included in the PayPal gateway.', 'woocommerce-paypal-payments' ); + $this->title = $this->get_option( 'title', __( 'Debit & Credit Cards', 'woocommerce-paypal-payments' ) ); + $this->description = $this->get_option( 'description', '' ); + + $this->init_form_fields(); + $this->init_settings(); + + add_action( + 'woocommerce_update_options_payment_gateways_' . $this->id, + array( + $this, + 'process_admin_options', + ) + ); + } + + /** + * Whether the Gateway needs to be setup. + * + * @return bool + */ + public function needs_setup(): bool { + return ! $this->onboarded; + } + + /** + * Initializes the form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'label' => __( 'Enable PayPal Card Button', 'woocommerce-paypal-payments' ), + 'default' => $this->default_enabled ? 'yes' : 'no', + 'desc_tip' => true, + 'description' => __( 'Enable/Disable the separate payment gateway with the card button.', 'woocommerce-paypal-payments' ), + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->title, + 'desc_tip' => true, + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->description, + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'ppcp' => array( + 'type' => 'ppcp', + ), + ); + } + + /** + * Process payment for a WooCommerce order. + * + * @param int $order_id The WooCommerce order id. + * + * @return array + */ + public function process_payment( $order_id ) { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + ); + } + + /** + * If customer has chosen change Subscription payment. + */ + if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) { + $saved_paypal_payment = filter_input( INPUT_POST, 'saved_paypal_payment', FILTER_SANITIZE_STRING ); + if ( $saved_paypal_payment ) { + update_post_meta( $order_id, 'payment_token_id', $saved_paypal_payment ); + + return $this->handle_payment_success( $wc_order ); + } + } + + /** + * If the WC_Order is paid through the approved webhook. + */ + //phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) { + return $this->handle_payment_success( $wc_order ); + } + //phpcs:enable WordPress.Security.NonceVerification.Recommended + + try { + if ( ! $this->order_processor->process( $wc_order ) ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + $this->order_processor->last_error() + ) + ); + } + + if ( $this->subscription_helper->has_subscription( $order_id ) ) { + $this->schedule_saved_payment_check( $order_id, $wc_order->get_customer_id() ); + } + + return $this->handle_payment_success( $wc_order ); + } catch ( PayPalApiException $error ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + Messages::generic_payment_error_message() . ' ' . $error->getMessage(), + $error->getCode(), + $error + ) + ); + } catch ( RuntimeException $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } + + /** + * 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 ); + } + + /** + * Return transaction url for this gateway and given order. + * + * @param \WC_Order $order WC order to get transaction url by. + * + * @return string + */ + public function get_transaction_url( $order ): string { + $this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order ); + + return parent::get_transaction_url( $order ); + } + + /** + * Returns the settings renderer. + * + * @return SettingsRenderer + */ + protected function settings_renderer(): SettingsRenderer { + return $this->settings_renderer; + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php index b11116878..4b97120bc 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php @@ -9,20 +9,30 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; +use Exception; use Psr\Log\LoggerInterface; +use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; +use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; +use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use Psr\Container\ContainerInterface; @@ -31,7 +41,8 @@ use Psr\Container\ContainerInterface; */ class CreditCardGateway extends \WC_Payment_Gateway_CC { - use ProcessPaymentTrait; + use ProcessPaymentTrait, OrderMetaTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait, FreeTrialHandlerTrait, + GatewaySettingsRendererTrait; const ID = 'ppcp-credit-card-gateway'; @@ -203,15 +214,25 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { Environment $environment, PaymentsEndpoint $payments_endpoint ) { - $this->id = self::ID; + $this->settings_renderer = $settings_renderer; $this->order_processor = $order_processor; $this->authorized_payments_processor = $authorized_payments_processor; - $this->settings_renderer = $settings_renderer; $this->config = $config; + $this->module_url = $module_url; $this->session_handler = $session_handler; $this->refund_processor = $refund_processor; + $this->state = $state; + $this->transaction_url_provider = $transaction_url_provider; + $this->payment_token_repository = $payment_token_repository; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->payer_factory = $payer_factory; + $this->order_endpoint = $order_endpoint; + $this->subscription_helper = $subscription_helper; + $this->logger = $logger; $this->environment = $environment; + $this->payments_endpoint = $payments_endpoint; if ( $state->current_state() === State::STATE_ONBOARDED ) { $this->supports = array( 'refunds' ); @@ -261,18 +282,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { 'process_admin_options', ) ); - - $this->module_url = $module_url; - $this->payment_token_repository = $payment_token_repository; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->payer_factory = $payer_factory; - $this->order_endpoint = $order_endpoint; - $this->transaction_url_provider = $transaction_url_provider; - $this->subscription_helper = $subscription_helper; - $this->logger = $logger; - $this->payments_endpoint = $payments_endpoint; - $this->state = $state; } /** @@ -295,20 +304,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { remove_action( 'gettext', 'replace_credit_card_cvv_label' ); } - /** - * Renders the settings. - * - * @return string - */ - public function generate_ppcp_html(): string { - - ob_start(); - $this->settings_renderer->render(); - $content = ob_get_contents(); - ob_end_clean(); - return $content; - } - /** * Replace WooCommerce credit card field label. * @@ -409,6 +404,158 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { return $this->is_enabled(); } + /** + * Process payment for a WooCommerce order. + * + * @param int $order_id The WooCommerce order id. + * + * @return array + */ + public function process_payment( $order_id ) { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + ); + } + + /** + * If customer has chosen a saved credit card payment. + */ + $saved_credit_card = filter_input( INPUT_POST, 'saved_credit_card', FILTER_SANITIZE_STRING ); + $change_payment = filter_input( INPUT_POST, 'woocommerce_change_payment', FILTER_SANITIZE_STRING ); + if ( $saved_credit_card && ! isset( $change_payment ) ) { + + $user_id = (int) $wc_order->get_customer_id(); + $customer = new \WC_Customer( $user_id ); + $tokens = $this->payment_token_repository->all_for_user_id( (int) $customer->get_id() ); + + $selected_token = null; + foreach ( $tokens as $token ) { + if ( $token->id() === $saved_credit_card ) { + $selected_token = $token; + break; + } + } + + if ( ! $selected_token ) { + return $this->handle_payment_failure( + $wc_order, + new GatewayGenericException( new Exception( 'Saved card token not found.' ) ) + ); + } + + $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); + $payer = $this->payer_factory->from_customer( $customer ); + + $shipping_preference = $this->shipping_preference_factory->from_state( + $purchase_unit, + '' + ); + + try { + $order = $this->order_endpoint->create( + array( $purchase_unit ), + $shipping_preference, + $payer, + $selected_token + ); + + $this->add_paypal_meta( $wc_order, $order, $this->environment ); + + if ( ! $order->status()->is( OrderStatus::COMPLETED ) ) { + return $this->handle_payment_failure( + $wc_order, + new GatewayGenericException( new Exception( "Unexpected status for order {$order->id()} using a saved card: {$order->status()->name()}." ) ) + ); + } + + if ( ! in_array( + $order->intent(), + array( 'CAPTURE', 'AUTHORIZE' ), + true + ) ) { + return $this->handle_payment_failure( + $wc_order, + new GatewayGenericException( new Exception( "Could neither capture nor authorize order {$order->id()} using a saved card. Status: {$order->status()->name()}. Intent: {$order->intent()}." ) ) + ); + } + + if ( $order->intent() === 'AUTHORIZE' ) { + $order = $this->order_endpoint->authorize( $order ); + + $wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' ); + } + + $transaction_id = $this->get_paypal_order_transaction_id( $order ); + if ( $transaction_id ) { + $this->update_transaction_id( $transaction_id, $wc_order ); + } + + $this->handle_new_order_status( $order, $wc_order ); + + if ( $this->is_free_trial_order( $wc_order ) ) { + $this->authorized_payments_processor->void_authorizations( $order ); + $wc_order->payment_complete(); + } elseif ( $this->config->has( 'intent' ) && strtoupper( (string) $this->config->get( 'intent' ) ) === 'CAPTURE' ) { + $this->authorized_payments_processor->capture_authorized_payment( $wc_order ); + } + + return $this->handle_payment_success( $wc_order ); + } catch ( RuntimeException $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } + + /** + * If customer has chosen change Subscription payment. + */ + if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) { + if ( $saved_credit_card ) { + update_post_meta( $order_id, 'payment_token_id', $saved_credit_card ); + + return $this->handle_payment_success( $wc_order ); + } + } + + /** + * If the WC_Order is paid through the approved webhook. + */ + //phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) { + return $this->handle_payment_success( $wc_order ); + } + //phpcs:enable WordPress.Security.NonceVerification.Recommended + + try { + if ( ! $this->order_processor->process( $wc_order ) ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + $this->order_processor->last_error() + ) + ); + } + + if ( $this->subscription_helper->has_subscription( $order_id ) ) { + $this->schedule_saved_payment_check( $order_id, $wc_order->get_customer_id() ); + } + + return $this->handle_payment_success( $wc_order ); + } catch ( PayPalApiException $error ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + Messages::generic_payment_error_message() . ' ' . $error->getMessage(), + $error->getCode(), + $error + ) + ); + } catch ( RuntimeException $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } /** * Process refund. @@ -500,11 +647,11 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { } /** - * Returns the environment. + * Returns the settings renderer. * - * @return Environment + * @return SettingsRenderer */ - protected function environment(): Environment { - return $this->environment; + protected function settings_renderer(): SettingsRenderer { + return $this->settings_renderer; } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/GatewaySettingsRendererTrait.php b/modules/ppcp-wc-gateway/src/Gateway/GatewaySettingsRendererTrait.php new file mode 100644 index 000000000..804ea5fdd --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/GatewaySettingsRendererTrait.php @@ -0,0 +1,37 @@ +settings_renderer()->render(); + $content = ob_get_contents(); + ob_end_clean(); + return $content; + } + + /** + * Returns the settings renderer. + * + * @return SettingsRenderer + */ + abstract protected function settings_renderer(): SettingsRenderer; +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/Messages.php b/modules/ppcp-wc-gateway/src/Gateway/Messages.php new file mode 100644 index 000000000..ea5124578 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/Messages.php @@ -0,0 +1,27 @@ +checkout_helper = $checkout_helper; + $this->module_url = $module_url; + $this->asset_version = $asset_version; + } + + /** + * Initializes OXXO integration. + */ + public function init(): void { + + add_filter( + 'woocommerce_available_payment_gateways', + function ( array $methods ): array { + + if ( ! $this->checkout_allowed_for_oxxo() ) { + unset( $methods[ OXXOGateway::ID ] ); + } + + return $methods; + } + ); + + add_action( + 'wp_enqueue_scripts', + array( $this, 'register_assets' ) + ); + + add_filter( + 'woocommerce_thankyou_order_received_text', + function( string $message, WC_Order $order ) { + $payer_action = $order->get_meta( 'ppcp_oxxo_payer_action' ) ?? ''; + + $button = ''; + if ( $payer_action ) { + $button = '

' . esc_html__( 'See OXXO voucher', 'woocommerce-paypal-payments' ) . '

'; + } + + return $message . ' ' . $button; + }, + 10, + 2 + ); + + add_action( + 'woocommerce_email_before_order_table', + function ( WC_Order $order, bool $sent_to_admin ) { + if ( + ! $sent_to_admin + && $order->get_payment_method() === OXXOGateway::ID + && $order->has_status( 'on-hold' ) + ) { + $payer_action = $order->get_meta( 'ppcp_oxxo_payer_action' ) ?? ''; + if ( $payer_action ) { + echo '

' . esc_html__( 'See OXXO voucher', 'woocommerce-paypal-payments' ) . '

'; + } + } + }, + 10, + 2 + ); + + add_filter( + 'ppcp_payment_capture_reversed_webhook_update_status_note', + function( string $note, WC_Order $wc_order, string $event_type ): string { + if ( $wc_order->get_payment_method() === OXXOGateway::ID && $event_type === 'PAYMENT.CAPTURE.DENIED' ) { + $note = __( 'OXXO voucher has expired or the buyer didn\'t complete the payment successfully.', 'woocommerce-paypal-payments' ); + } + + return $note; + }, + 10, + 3 + ); + + add_action( + 'add_meta_boxes', + function( string $post_type ) { + if ( $post_type === 'shop_order' ) { + $post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_STRING ); + $order = wc_get_order( $post_id ); + if ( is_a( $order, WC_Order::class ) && $order->get_payment_method() === OXXOGateway::ID ) { + $payer_action = $order->get_meta( 'ppcp_oxxo_payer_action' ); + if ( $payer_action ) { + add_meta_box( + 'ppcp_oxxo_payer_action', + __( 'OXXO Voucher/Ticket', 'woocommerce-paypal-payments' ), + function() use ( $payer_action ) { + echo '

' . esc_html__( 'See OXXO voucher', 'woocommerce-paypal-payments' ) . '

'; + }, + $post_type, + 'side', + 'high' + ); + } + } + } + } + ); + + add_action( + 'woocommerce_order_details_before_order_table_items', + function( WC_Order $order ) { + if ( $order->get_payment_method() === OXXOGateway::ID ) { + $payer_action = $order->get_meta( 'ppcp_oxxo_payer_action' ); + if ( $payer_action ) { + echo '

' . esc_html__( 'See OXXO voucher', 'woocommerce-paypal-payments' ) . '

'; + } + } + } + ); + } + + /** + * Checks if checkout is allowed for OXXO. + * + * @return bool + */ + private function checkout_allowed_for_oxxo(): bool { + if ( 'MXN' !== get_woocommerce_currency() ) { + return false; + } + + $billing_country = filter_input( INPUT_POST, 'country', FILTER_SANITIZE_STRING ) ?? null; + if ( $billing_country && 'MX' !== $billing_country ) { + return false; + } + + if ( ! $this->checkout_helper->is_checkout_amount_allowed( 0, 10000 ) ) { + return false; + } + + return true; + } + + /** + * Register OXXO assets. + */ + public function register_assets(): void { + $gateway_settings = get_option( 'woocommerce_ppcp-oxxo-gateway_settings' ); + $gateway_enabled = $gateway_settings['enabled'] ?? ''; + if ( $gateway_enabled === 'yes' && is_checkout() ) { + wp_enqueue_script( + 'ppcp-oxxo', + trailingslashit( $this->module_url ) . 'assets/js/oxxo.js', + array(), + $this->asset_version, + true + ); + } + + wp_localize_script( + 'ppcp-oxxo', + 'OXXOConfig', + array( + 'oxxo_endpoint' => \WC_AJAX::get_endpoint( 'ppc-oxxo' ), + 'oxxo_nonce' => wp_create_nonce( 'ppc-oxxo' ), + 'error' => array( + 'generic' => __( + 'Something went wrong. Please try again or choose another payment source.', + 'woocommerce-paypal-payments' + ), + 'js_validation' => __( + 'Required form fields are not filled or invalid.', + 'woocommerce-paypal-payments' + ), + ), + ) + ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXOEndpoint.php b/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXOEndpoint.php new file mode 100644 index 000000000..64d8e713f --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXOEndpoint.php @@ -0,0 +1,156 @@ +request_data = $request_data; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->order_endpoint = $order_endpoint; + $this->logger = $logger; + } + + /** + * The nonce + * + * @return string + */ + public static function nonce(): string { + return 'ppc-oxxo'; + } + + /** + * Handles the request. + * + * @return bool + */ + public function handle_request(): bool { + $purchase_unit = $this->purchase_unit_factory->from_wc_cart(); + $payer_action = ''; + + try { + $shipping_preference = $this->shipping_preference_factory->from_state( + $purchase_unit, + 'checkout' + ); + + $order = $this->order_endpoint->create( array( $purchase_unit ), $shipping_preference ); + + $payment_source = array( + 'oxxo' => array( + 'name' => 'John Doe', + 'email' => 'foo@bar.com', + 'country_code' => 'MX', + ), + ); + + $payment_method = $this->order_endpoint->confirm_payment_source( $order->id(), $payment_source ); + + foreach ( $payment_method->links as $link ) { + if ( $link->rel === 'payer-action' ) { + $payer_action = $link->href; + } + } + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + + if ( is_a( $exception, PayPalApiException::class ) && is_array( $exception->details() ) ) { + $details = ''; + foreach ( $exception->details() as $detail ) { + $issue = $detail->issue ?? ''; + $field = $detail->field ?? ''; + $description = $detail->description ?? ''; + $details .= $issue . ' ' . $field . ' ' . $description . '
'; + } + + $error = $details; + } + + $this->logger->error( $error ); + wc_add_notice( $error, 'error' ); + + wp_send_json_error( 'Could not get OXXO payer action.' ); + return false; + } + + WC()->session->set( 'ppcp_payer_action', $payer_action ); + + wp_send_json_success( + array( 'payer_action' => $payer_action ) + ); + + return true; + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXOGateway.php b/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXOGateway.php new file mode 100644 index 000000000..1711b8664 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXOGateway.php @@ -0,0 +1,210 @@ +id = self::ID; + + $this->method_title = __( 'OXXO', 'woocommerce-paypal-payments' ); + $this->method_description = __( 'OXXO is a Mexican chain of convenience stores.', 'woocommerce-paypal-payments' ); + + $this->title = $this->get_option( 'title', $this->method_title ); + $this->description = $this->get_option( 'description', __( 'OXXO allows you to pay bills and online purchases in-store with cash.', 'woocommerce-paypal-payments' ) ); + + $this->init_form_fields(); + $this->init_settings(); + + add_action( + 'woocommerce_update_options_payment_gateways_' . $this->id, + array( + $this, + 'process_admin_options', + ) + ); + + $this->order_endpoint = $order_endpoint; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->module_url = $module_url; + $this->logger = $logger; + + $this->icon = esc_url( $this->module_url ) . 'assets/images/oxxo.svg'; + } + + /** + * Initialize the form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'label' => __( 'OXXO', 'woocommerce-paypal-payments' ), + 'default' => 'no', + 'desc_tip' => true, + 'description' => __( 'Enable/Disable OXXO payment gateway.', 'woocommerce-paypal-payments' ), + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->title, + 'desc_tip' => true, + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->description, + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + ); + } + + /** + * Processes the order. + * + * @param int $order_id The WC order ID. + * @return array + */ + public function process_payment( $order_id ) { + $wc_order = wc_get_order( $order_id ); + $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); + $payer_action = ''; + + try { + $shipping_preference = $this->shipping_preference_factory->from_state( + $purchase_unit, + 'checkout' + ); + + $order = $this->order_endpoint->create( array( $purchase_unit ), $shipping_preference ); + $payment_source = array( + 'oxxo' => array( + 'name' => $wc_order->get_billing_first_name() . ' ' . $wc_order->get_billing_last_name(), + 'email' => $wc_order->get_billing_email(), + 'country_code' => $wc_order->get_billing_country(), + ), + ); + $payment_method = $this->order_endpoint->confirm_payment_source( $order->id(), $payment_source ); + foreach ( $payment_method->links as $link ) { + if ( $link->rel === 'payer-action' ) { + $payer_action = $link->href; + $wc_order->add_meta_data( 'ppcp_oxxo_payer_action', $payer_action ); + $wc_order->save_meta_data(); + } + } + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + + if ( is_a( $exception, PayPalApiException::class ) && is_array( $exception->details() ) ) { + $details = ''; + foreach ( $exception->details() as $detail ) { + $issue = $detail->issue ?? ''; + $field = $detail->field ?? ''; + $description = $detail->description ?? ''; + $details .= $issue . ' ' . $field . ' ' . $description . '
'; + } + + $error = $details; + } + + $this->logger->error( $error ); + wc_add_notice( $error, 'error' ); + + $wc_order->update_status( + 'failed', + $error + ); + + return array( + 'result' => 'failure', + 'redirect' => wc_get_checkout_url(), + ); + } + + WC()->cart->empty_cart(); + + $result = array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $wc_order ), + ); + + if ( $payer_action ) { + $result['payer_action'] = $payer_action; + } + + return $result; + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 1aabbfc5a..1adbbbaf6 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -9,18 +9,21 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; +use Exception; use Psr\Log\LoggerInterface; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; +use WC_Order; +use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; +use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; +use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; -use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; @@ -32,7 +35,7 @@ use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; */ class PayPalGateway extends \WC_Payment_Gateway { - use ProcessPaymentTrait; + use ProcessPaymentTrait, FreeTrialHandlerTrait, GatewaySettingsRendererTrait; const ID = 'ppcp-gateway'; const INTENT_META_KEY = '_ppcp_paypal_intent'; @@ -63,13 +66,6 @@ class PayPalGateway extends \WC_Payment_Gateway { */ protected $order_processor; - /** - * The processor for authorized payments. - * - * @var AuthorizedPaymentsProcessor - */ - protected $authorized_payments_processor; - /** * The settings. * @@ -119,27 +115,6 @@ class PayPalGateway extends \WC_Payment_Gateway { */ protected $payment_token_repository; - /** - * The shipping_preference factory. - * - * @var ShippingPreferenceFactory - */ - private $shipping_preference_factory; - - /** - * The payments endpoint - * - * @var PaymentsEndpoint - */ - protected $payments_endpoint; - - /** - * The order endpoint. - * - * @var OrderEndpoint - */ - protected $order_endpoint; - /** * Whether the plugin is in onboarded state. * @@ -178,30 +153,25 @@ class PayPalGateway extends \WC_Payment_Gateway { /** * PayPalGateway constructor. * - * @param SettingsRenderer $settings_renderer The Settings Renderer. - * @param FundingSourceRenderer $funding_source_renderer The funding source renderer. - * @param OrderProcessor $order_processor The Order Processor. - * @param AuthorizedPaymentsProcessor $authorized_payments_processor The Authorized Payments Processor. - * @param ContainerInterface $config The settings. - * @param SessionHandler $session_handler The Session Handler. - * @param RefundProcessor $refund_processor The Refund Processor. - * @param State $state The state. - * @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. - * @param PaymentTokenRepository $payment_token_repository The payment token repository. - * @param ShippingPreferenceFactory $shipping_preference_factory The shipping_preference factory. - * @param LoggerInterface $logger The logger. - * @param PaymentsEndpoint $payments_endpoint The payments endpoint. - * @param OrderEndpoint $order_endpoint The order endpoint. - * @param string $api_shop_country The api shop country. + * @param SettingsRenderer $settings_renderer The Settings Renderer. + * @param FundingSourceRenderer $funding_source_renderer The funding source renderer. + * @param OrderProcessor $order_processor The Order Processor. + * @param ContainerInterface $config The settings. + * @param SessionHandler $session_handler The Session Handler. + * @param RefundProcessor $refund_processor The Refund Processor. + * @param State $state The state. + * @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. + * @param PaymentTokenRepository $payment_token_repository The payment token repository. + * @param LoggerInterface $logger The logger. + * @param string $api_shop_country The api shop country. */ public function __construct( SettingsRenderer $settings_renderer, FundingSourceRenderer $funding_source_renderer, OrderProcessor $order_processor, - AuthorizedPaymentsProcessor $authorized_payments_processor, ContainerInterface $config, SessionHandler $session_handler, RefundProcessor $refund_processor, @@ -211,37 +181,25 @@ class PayPalGateway extends \WC_Payment_Gateway { string $page_id, Environment $environment, PaymentTokenRepository $payment_token_repository, - ShippingPreferenceFactory $shipping_preference_factory, LoggerInterface $logger, - PaymentsEndpoint $payments_endpoint, - OrderEndpoint $order_endpoint, string $api_shop_country ) { - - $this->id = self::ID; - $this->order_processor = $order_processor; - $this->authorized_payments_processor = $authorized_payments_processor; - $this->settings_renderer = $settings_renderer; - $this->funding_source_renderer = $funding_source_renderer; - $this->config = $config; - $this->session_handler = $session_handler; - $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; - $this->id = self::ID; - $this->order_processor = $order_processor; - $this->authorized_payments = $authorized_payments_processor; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->settings_renderer = $settings_renderer; - $this->config = $config; - $this->session_handler = $session_handler; - $this->refund_processor = $refund_processor; - $this->transaction_url_provider = $transaction_url_provider; - $this->page_id = $page_id; - $this->environment = $environment; - $this->logger = $logger; + $this->id = self::ID; + $this->settings_renderer = $settings_renderer; + $this->funding_source_renderer = $funding_source_renderer; + $this->order_processor = $order_processor; + $this->config = $config; + $this->session_handler = $session_handler; + $this->refund_processor = $refund_processor; + $this->state = $state; + $this->transaction_url_provider = $transaction_url_provider; + $this->subscription_helper = $subscription_helper; + $this->page_id = $page_id; + $this->environment = $environment; + $this->onboarded = $state->current_state() === State::STATE_ONBOARDED; + $this->payment_token_repository = $payment_token_repository; + $this->logger = $logger; + $this->api_shop_country = $api_shop_country; if ( $this->onboarded ) { $this->supports = array( 'refunds' ); @@ -291,13 +249,6 @@ class PayPalGateway extends \WC_Payment_Gateway { 'process_admin_options', ) ); - $this->subscription_helper = $subscription_helper; - $this->payment_token_repository = $payment_token_repository; - $this->logger = $logger; - $this->payments_endpoint = $payments_endpoint; - $this->order_endpoint = $order_endpoint; - $this->state = $state; - $this->api_shop_country = $api_shop_country; } /** @@ -306,7 +257,6 @@ class PayPalGateway extends \WC_Payment_Gateway { * @return bool */ public function needs_setup(): bool { - return ! $this->onboarded; } @@ -334,20 +284,6 @@ class PayPalGateway extends \WC_Payment_Gateway { } } - /** - * Renders the settings. - * - * @return string - */ - public function generate_ppcp_html(): string { - - ob_start(); - $this->settings_renderer->render( false ); - $content = ob_get_contents(); - ob_end_clean(); - return $content; - } - /** * Defines the method title. If we are on the credit card tab in the settings, we want to change this. * @@ -450,6 +386,130 @@ class PayPalGateway extends \WC_Payment_Gateway { } // phpcs:enable WordPress.Security.NonceVerification.Recommended + /** + * Process payment for a WooCommerce order. + * + * @param int $order_id The WooCommerce order id. + * + * @return array + */ + public function process_payment( $order_id ) { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + ); + } + + $funding_source = filter_input( INPUT_POST, 'ppcp-funding-source', FILTER_SANITIZE_STRING ); + + if ( 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) ) { + $user_id = (int) $wc_order->get_customer_id(); + $tokens = $this->payment_token_repository->all_for_user_id( $user_id ); + if ( ! array_filter( + $tokens, + function ( PaymentToken $token ): bool { + return isset( $token->source()->paypal ); + } + ) ) { + return $this->handle_payment_failure( $wc_order, new Exception( 'No saved PayPal account.' ) ); + } + + $wc_order->payment_complete(); + + return $this->handle_payment_success( $wc_order ); + } + + /** + * If customer has chosen change Subscription payment. + */ + if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) { + $saved_paypal_payment = filter_input( INPUT_POST, 'saved_paypal_payment', FILTER_SANITIZE_STRING ); + if ( $saved_paypal_payment ) { + update_post_meta( $order_id, 'payment_token_id', $saved_paypal_payment ); + + return $this->handle_payment_success( $wc_order ); + } + } + + /** + * If the WC_Order is paid through the approved webhook. + */ + //phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) { + return $this->handle_payment_success( $wc_order ); + } + //phpcs:enable WordPress.Security.NonceVerification.Recommended + + try { + if ( ! $this->order_processor->process( $wc_order ) ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + $this->order_processor->last_error() + ) + ); + } + + if ( $this->subscription_helper->has_subscription( $order_id ) ) { + $this->schedule_saved_payment_check( $order_id, $wc_order->get_customer_id() ); + } + + return $this->handle_payment_success( $wc_order ); + } catch ( PayPalApiException $error ) { + $retry_keys_messages = array( + 'INSTRUMENT_DECLINED' => __( 'Instrument declined.', 'woocommerce-paypal-payments' ), + 'PAYER_ACTION_REQUIRED' => __( 'Payer action required, possibly overcharge.', 'woocommerce-paypal-payments' ), + ); + $retry_errors = array_filter( + array_keys( $retry_keys_messages ), + function ( string $key ) use ( $error ): bool { + return $error->has_detail( $key ); + } + ); + if ( $retry_errors ) { + $retry_error_key = $retry_errors[0]; + + $wc_order->update_status( + 'failed', + $retry_keys_messages[ $retry_error_key ] . ' ' . $error->details()[0]->description ?? '' + ); + + $this->session_handler->increment_insufficient_funding_tries(); + if ( $this->session_handler->insufficient_funding_tries() >= 3 ) { + return $this->handle_payment_failure( + null, + new Exception( + __( 'Please use a different payment method.', 'woocommerce-paypal-payments' ), + $error->getCode(), + $error + ) + ); + } + + $host = $this->config->has( 'sandbox_on' ) && $this->config->get( 'sandbox_on' ) ? + 'https://www.sandbox.paypal.com/' : 'https://www.paypal.com/'; + $url = $host . 'checkoutnow?token=' . $this->session_handler->order()->id(); + return array( + 'result' => 'success', + 'redirect' => $url, + ); + } + + return $this->handle_payment_failure( + $wc_order, + new Exception( + Messages::generic_payment_error_message() . ' ' . $error->getMessage(), + $error->getCode(), + $error + ) + ); + } catch ( RuntimeException $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } + /** * Process refund. * @@ -503,11 +563,11 @@ class PayPalGateway extends \WC_Payment_Gateway { } /** - * Returns the environment. + * Returns the settings renderer. * - * @return Environment + * @return SettingsRenderer */ - protected function environment(): Environment { - return $this->environment; + protected function settings_renderer(): SettingsRenderer { + return $this->settings_renderer; } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php index 87f89997a..b9502bbe8 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php @@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory; use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; @@ -115,6 +116,13 @@ class PayUponInvoice { */ protected $pui_product_status; + /** + * The checkout helper. + * + * @var CheckoutHelper + */ + protected $checkout_helper; + /** * The capture factory. * @@ -137,6 +145,7 @@ class PayUponInvoice { * @param string $current_ppcp_settings_page_id Current PayPal settings page id. * @param PayUponInvoiceProductStatus $pui_product_status The PUI product status. * @param PayUponInvoiceHelper $pui_helper The PUI helper. + * @param CheckoutHelper $checkout_helper The checkout helper. * @param CaptureFactory $capture_factory The capture factory. */ public function __construct( @@ -152,6 +161,7 @@ class PayUponInvoice { string $current_ppcp_settings_page_id, PayUponInvoiceProductStatus $pui_product_status, PayUponInvoiceHelper $pui_helper, + CheckoutHelper $checkout_helper, CaptureFactory $capture_factory ) { $this->module_url = $module_url; @@ -166,6 +176,7 @@ class PayUponInvoice { $this->current_ppcp_settings_page_id = $current_ppcp_settings_page_id; $this->pui_product_status = $pui_product_status; $this->pui_helper = $pui_helper; + $this->checkout_helper = $checkout_helper; $this->capture_factory = $capture_factory; } @@ -298,7 +309,7 @@ class PayUponInvoice { } }, 10, - 3 + 2 ); add_filter( @@ -360,7 +371,7 @@ class PayUponInvoice { } $birth_date = filter_input( INPUT_POST, 'billing_birth_date', FILTER_SANITIZE_STRING ); - if ( ( $birth_date && ! $this->pui_helper->validate_birth_date( $birth_date ) ) || $birth_date === '' ) { + if ( ( $birth_date && ! $this->checkout_helper->validate_birth_date( $birth_date ) ) || $birth_date === '' ) { $errors->add( 'validation', __( 'Invalid birth date.', 'woocommerce-paypal-payments' ) ); } @@ -477,7 +488,7 @@ class PayUponInvoice { if ( $post_type === 'shop_order' ) { $post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_STRING ); $order = wc_get_order( $post_id ); - if ( is_a( $order, WC_Order::class ) && $order->get_payment_method() === 'ppcp-pay-upon-invoice-gateway' ) { + if ( is_a( $order, WC_Order::class ) && $order->get_payment_method() === PayUponInvoiceGateway::ID ) { $instructions = $order->get_meta( 'ppcp_ratepay_payment_instructions_payment_reference' ); if ( $instructions ) { add_meta_box( diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php index fe9d53d38..4596bd321 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php @@ -12,14 +12,13 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice; use Psr\Log\LoggerInterface; use RuntimeException; use WC_Order; -use WC_Order_Item_Product; use WC_Payment_Gateway; -use WC_Product; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PayUponInvoiceOrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider; +use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; @@ -81,6 +80,13 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway { */ protected $pui_helper; + /** + * The checkout helper. + * + * @var CheckoutHelper + */ + protected $checkout_helper; + /** * PayUponInvoiceGateway constructor. * @@ -91,6 +97,7 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway { * @param TransactionUrlProvider $transaction_url_provider The transaction URL provider. * @param LoggerInterface $logger The logger. * @param PayUponInvoiceHelper $pui_helper The PUI helper. + * @param CheckoutHelper $checkout_helper The checkout helper. */ public function __construct( PayUponInvoiceOrderEndpoint $order_endpoint, @@ -99,7 +106,8 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway { Environment $environment, TransactionUrlProvider $transaction_url_provider, LoggerInterface $logger, - PayUponInvoiceHelper $pui_helper + PayUponInvoiceHelper $pui_helper, + CheckoutHelper $checkout_helper ) { $this->id = self::ID; @@ -128,6 +136,7 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway { $this->environment = $environment; $this->transaction_url_provider = $transaction_url_provider; $this->pui_helper = $pui_helper; + $this->checkout_helper = $checkout_helper; } /** @@ -198,7 +207,7 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway { $pay_for_order = filter_input( INPUT_GET, 'pay_for_order', FILTER_SANITIZE_STRING ); if ( 'true' === $pay_for_order ) { - if ( ! $this->pui_helper->validate_birth_date( $birth_date ) ) { + if ( ! $this->checkout_helper->validate_birth_date( $birth_date ) ) { wc_add_notice( 'Invalid birth date.', 'error' ); return array( 'result' => 'failure', diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index 10e0e1313..f78871d1a 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -10,279 +10,14 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Exception; -use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; -use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; -use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; -use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\Onboarding\Environment; -use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; -use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; -use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; -use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; -use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; +use Throwable; +use WC_Order; +use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; /** * Trait ProcessPaymentTrait */ trait ProcessPaymentTrait { - - use OrderMetaTrait, PaymentsStatusHandlingTrait, TransactionIdHandlingTrait, FreeTrialHandlerTrait; - - /** - * Process a payment for an WooCommerce order. - * - * @param int $order_id The WooCommerce order id. - * - * @return array - * - * @throws RuntimeException When processing payment fails. - */ - public function process_payment( $order_id ) { - - $failure_data = array( - 'result' => 'failure', - 'redirect' => wc_get_checkout_url(), - ); - - $wc_order = wc_get_order( $order_id ); - if ( ! is_a( $wc_order, \WC_Order::class ) ) { - wc_add_notice( - __( 'Couldn\'t find order to process', 'woocommerce-paypal-payments' ), - 'error' - ); - - return $failure_data; - } - - $payment_method = filter_input( INPUT_POST, 'payment_method', FILTER_SANITIZE_STRING ); - $funding_source = filter_input( INPUT_POST, 'ppcp-funding-source', FILTER_SANITIZE_STRING ); - - /** - * If customer has chosen a saved credit card payment. - */ - $saved_credit_card = filter_input( INPUT_POST, 'saved_credit_card', FILTER_SANITIZE_STRING ); - $change_payment = filter_input( INPUT_POST, 'woocommerce_change_payment', FILTER_SANITIZE_STRING ); - if ( CreditCardGateway::ID === $payment_method && $saved_credit_card && ! isset( $change_payment ) ) { - - $user_id = (int) $wc_order->get_customer_id(); - $customer = new \WC_Customer( $user_id ); - $tokens = $this->payment_token_repository->all_for_user_id( (int) $customer->get_id() ); - - $selected_token = null; - foreach ( $tokens as $token ) { - if ( $token->id() === $saved_credit_card ) { - $selected_token = $token; - break; - } - } - - if ( ! $selected_token ) { - return null; - } - - $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); - $payer = $this->payer_factory->from_customer( $customer ); - - $shipping_preference = $this->shipping_preference_factory->from_state( - $purchase_unit, - '' - ); - - try { - $order = $this->order_endpoint->create( - array( $purchase_unit ), - $shipping_preference, - $payer, - $selected_token - ); - - $this->add_paypal_meta( $wc_order, $order, $this->environment() ); - - 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 ( ! 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( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' ); - } - - $transaction_id = $this->get_paypal_order_transaction_id( $order ); - if ( $transaction_id ) { - $this->update_transaction_id( $transaction_id, $wc_order ); - } - - $this->handle_new_order_status( $order, $wc_order ); - - if ( $this->is_free_trial_order( $wc_order ) ) { - $this->authorized_payments_processor->void_authorizations( $order ); - $wc_order->payment_complete(); - } elseif ( $this->config->has( 'intent' ) && strtoupper( (string) $this->config->get( 'intent' ) ) === 'CAPTURE' ) { - $this->authorized_payments_processor->capture_authorized_payment( $wc_order ); - } - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } catch ( RuntimeException $error ) { - $this->handle_failure( $wc_order, $error ); - return null; - } - } - - if ( PayPalGateway::ID === $payment_method && 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) ) { - $user_id = (int) $wc_order->get_customer_id(); - $tokens = $this->payment_token_repository->all_for_user_id( $user_id ); - if ( ! array_filter( - $tokens, - function ( PaymentToken $token ): bool { - return isset( $token->source()->paypal ); - } - ) ) { - $this->handle_failure( $wc_order, new Exception( 'No saved PayPal account.' ) ); - return null; - } - - $wc_order->payment_complete(); - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - - /** - * If customer has chosen change Subscription payment. - */ - if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) { - if ( 'ppcp-credit-card-gateway' === $this->id && $saved_credit_card ) { - update_post_meta( $order_id, 'payment_token_id', $saved_credit_card ); - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - - $saved_paypal_payment = filter_input( INPUT_POST, 'saved_paypal_payment', FILTER_SANITIZE_STRING ); - if ( 'ppcp-gateway' === $this->id && $saved_paypal_payment ) { - update_post_meta( $order_id, 'payment_token_id', $saved_paypal_payment ); - - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - } - - /** - * If the WC_Order is payed through the approved webhook. - */ - //phpcs:disable WordPress.Security.NonceVerification.Recommended - if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) { - $this->session_handler->destroy_session_data(); - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - //phpcs:enable WordPress.Security.NonceVerification.Recommended - - try { - if ( $this->order_processor->process( $wc_order ) ) { - if ( $this->subscription_helper->has_subscription( $order_id ) ) { - as_schedule_single_action( - time() + ( 1 * MINUTE_IN_SECONDS ), - 'woocommerce_paypal_payments_check_saved_payment', - array( - 'order_id' => $order_id, - 'customer_id' => $wc_order->get_customer_id(), - 'intent' => $this->config->has( 'intent' ) ? $this->config->get( 'intent' ) : '', - ) - ); - } - - WC()->cart->empty_cart(); - $this->session_handler->destroy_session_data(); - - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - } - } catch ( PayPalApiException $error ) { - if ( $error->has_detail( 'INSTRUMENT_DECLINED' ) ) { - $wc_order->update_status( - 'failed', - __( 'Instrument declined. ', 'woocommerce-paypal-payments' ) . $error->details()[0]->description ?? '' - ); - - $this->session_handler->increment_insufficient_funding_tries(); - $host = $this->config->has( 'sandbox_on' ) && $this->config->get( 'sandbox_on' ) ? - 'https://www.sandbox.paypal.com/' : 'https://www.paypal.com/'; - $url = $host . 'checkoutnow?token=' . $this->session_handler->order()->id(); - if ( $this->session_handler->insufficient_funding_tries() >= 3 ) { - $this->session_handler->destroy_session_data(); - wc_add_notice( - __( 'Please use a different payment method.', 'woocommerce-paypal-payments' ), - 'error' - ); - return $failure_data; - } - return array( - 'result' => 'success', - 'redirect' => $url, - ); - } - - $error_message = $error->getMessage(); - if ( $error->issues() ) { - $error_message = implode( - array_map( - function( $issue ) { - return $issue->issue . ' ' . $issue->description . '
'; - }, - $error->issues() - ) - ); - } - wc_add_notice( $error_message, 'error' ); - - $this->session_handler->destroy_session_data(); - } catch ( RuntimeException $error ) { - $this->handle_failure( $wc_order, $error ); - return $failure_data; - } - - wc_add_notice( - $this->order_processor->last_error(), - 'error' - ); - - $wc_order->update_status( - 'failed', - __( 'Could not process order. ', 'woocommerce-paypal-payments' ) . $this->order_processor->last_error() - ); - - return $failure_data; - } - /** * Checks if PayPal or Credit Card gateways are enabled. * @@ -311,29 +46,86 @@ trait ProcessPaymentTrait { return false; } + /** + * Scheduled the vaulted payment check. + * + * @param int $wc_order_id The WC order ID. + * @param int $customer_id The customer ID. + */ + protected function schedule_saved_payment_check( int $wc_order_id, int $customer_id ): void { + as_schedule_single_action( + time() + ( 1 * MINUTE_IN_SECONDS ), + 'woocommerce_paypal_payments_check_saved_payment', + array( + 'order_id' => $wc_order_id, + 'customer_id' => $customer_id, + 'intent' => $this->config->has( 'intent' ) ? $this->config->get( 'intent' ) : '', + ) + ); + } + /** * Handles the payment failure. * - * @param \WC_Order $wc_order The order. - * @param Exception $error The error causing the failure. + * @param WC_Order|null $wc_order The order. + * @param Exception $error The error causing the failure. + * @return array The data that can be returned by the gateway process_payment method. */ - protected function handle_failure( \WC_Order $wc_order, Exception $error ): void { - $this->logger->error( 'Payment failed: ' . $error->getMessage() ); + protected function handle_payment_failure( ?WC_Order $wc_order, Exception $error ): array { + $this->logger->error( 'Payment failed: ' . $this->format_exception( $error ) ); - $wc_order->update_status( - 'failed', - __( 'Could not process order. ', 'woocommerce-paypal-payments' ) . $error->getMessage() - ); + if ( $wc_order ) { + $wc_order->update_status( + 'failed', + $this->format_exception( $error ) + ); + } $this->session_handler->destroy_session_data(); wc_add_notice( $error->getMessage(), 'error' ); + + return array( + 'result' => 'failure', + 'redirect' => wc_get_checkout_url(), + ); } /** - * Returns the environment. + * Handles the payment completion. * - * @return Environment + * @param WC_Order|null $wc_order The order. + * @param string|null $url The redirect URL. + * @return array The data that can be returned by the gateway process_payment method. */ - abstract protected function environment(): Environment; + protected function handle_payment_success( ?WC_Order $wc_order, string $url = null ): array { + if ( ! $url ) { + $url = $this->get_return_url( $wc_order ); + } + + $this->session_handler->destroy_session_data(); + + return array( + 'result' => 'success', + 'redirect' => $url, + ); + } + + /** + * Outputs the exception, including the inner exception. + * + * @param Throwable $exception The exception to format. + * @return string + */ + protected function format_exception( Throwable $exception ): string { + $output = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine(); + $prev = $exception->getPrevious(); + if ( ! $prev ) { + return $output; + } + if ( $exception instanceof GatewayGenericException ) { + $output = ''; + } + return $output . ' ' . $this->format_exception( $prev ); + } } diff --git a/modules/ppcp-wc-gateway/src/Helper/CheckoutHelper.php b/modules/ppcp-wc-gateway/src/Helper/CheckoutHelper.php new file mode 100644 index 000000000..977144024 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Helper/CheckoutHelper.php @@ -0,0 +1,131 @@ +cart ?? null; + if ( $cart && ! is_checkout_pay_page() ) { + $cart_total = (float) $cart->get_total( 'numeric' ); + if ( $cart_total < $minimum || $cart_total > $maximum ) { + return false; + } + + $items = $cart->get_cart_contents(); + foreach ( $items as $item ) { + $product = wc_get_product( $item['product_id'] ); + if ( is_a( $product, WC_Product::class ) && ! $this->is_physical_product( $product ) ) { + return false; + } + } + } + + if ( is_wc_endpoint_url( 'order-pay' ) ) { + /** + * Needed for WordPress `query_vars`. + * + * @psalm-suppress InvalidGlobal + */ + global $wp; + + if ( isset( $wp->query_vars['order-pay'] ) && absint( $wp->query_vars['order-pay'] ) > 0 ) { + $order_id = absint( $wp->query_vars['order-pay'] ); + $order = wc_get_order( $order_id ); + if ( is_a( $order, WC_Order::class ) ) { + $order_total = (float) $order->get_total(); + if ( $order_total < $minimum || $order_total > $maximum ) { + return false; + } + + foreach ( $order->get_items() as $item_id => $item ) { + if ( is_a( $item, WC_Order_Item_Product::class ) ) { + $product = wc_get_product( $item->get_product_id() ); + if ( is_a( $product, WC_Product::class ) && ! $this->is_physical_product( $product ) ) { + return false; + } + } + } + } + } + } + + return true; + } + + /** + * Ensures date is valid and at least 18 years back. + * + * @param string $date The date. + * @param string $format The date format. + * @return bool + */ + public function validate_birth_date( string $date, string $format = 'Y-m-d' ): bool { + $d = DateTime::createFromFormat( $format, $date ); + if ( false === $d ) { + return false; + } + + if ( $date !== $d->format( $format ) ) { + return false; + } + + $date_time = strtotime( $date ); + if ( $date_time && time() < strtotime( '+18 years', $date_time ) ) { + return false; + } + if ( $date_time < strtotime( '-100 years', time() ) ) { + return false; + } + + return true; + } + + /** + * Ensures product is neither downloadable nor virtual. + * + * @param WC_Product $product WC product. + * @return bool + */ + public function is_physical_product( WC_Product $product ):bool { + if ( $product->is_downloadable() || $product->is_virtual() ) { + return false; + } + + if ( is_a( $product, WC_Product_Variable::class ) ) { + foreach ( $product->get_available_variations( 'object' ) as $variation ) { + if ( is_a( $variation, WC_Product_Variation::class ) ) { + if ( true === $variation->is_downloadable() || true === $variation->is_virtual() ) { + return false; + } + } + } + } + + return true; + } +} diff --git a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php index 8c1125348..1ac5ecb73 100644 --- a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php +++ b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php @@ -9,65 +9,25 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Helper; -use DateTime; -use WC_Order; -use WC_Order_Item_Product; -use WC_Product; -use WC_Product_Variable; -use WC_Product_Variation; - /** * Class PayUponInvoiceHelper */ class PayUponInvoiceHelper { /** - * Ensures date is valid and at least 18 years back. + * The checkout helper. * - * @param string $date The date. - * @param string $format The date format. - * @return bool + * @var CheckoutHelper */ - public function validate_birth_date( string $date, string $format = 'Y-m-d' ): bool { - $d = DateTime::createFromFormat( $format, $date ); - if ( false === $d ) { - return false; - } - - if ( $date !== $d->format( $format ) ) { - return false; - } - - $date_time = strtotime( $date ); - if ( $date_time && time() < strtotime( '+18 years', $date_time ) ) { - return false; - } - - return true; - } + protected $checkout_helper; /** - * Ensures product is ready for PUI. + * PayUponInvoiceHelper constructor. * - * @param WC_Product $product WC product. - * @return bool + * @param CheckoutHelper $checkout_helper The checkout helper. */ - public function product_ready_for_pui( WC_Product $product ):bool { - if ( $product->is_downloadable() || $product->is_virtual() ) { - return false; - } - - if ( is_a( $product, WC_Product_Variable::class ) ) { - foreach ( $product->get_available_variations( 'object' ) as $variation ) { - if ( is_a( $variation, WC_Product_Variation::class ) ) { - if ( true === $variation->is_downloadable() || true === $variation->is_virtual() ) { - return false; - } - } - } - } - - return true; + public function __construct( CheckoutHelper $checkout_helper ) { + $this->checkout_helper = $checkout_helper; } /** @@ -90,49 +50,8 @@ class PayUponInvoiceHelper { return false; } - $cart = WC()->cart ?? null; - if ( $cart && ! is_checkout_pay_page() ) { - $cart_total = (float) $cart->get_total( 'numeric' ); - if ( $cart_total < 5 || $cart_total > 2500 ) { - return false; - } - - $items = $cart->get_cart_contents(); - foreach ( $items as $item ) { - $product = wc_get_product( $item['product_id'] ); - if ( is_a( $product, WC_Product::class ) && ! $this->product_ready_for_pui( $product ) ) { - return false; - } - } - } - - if ( is_wc_endpoint_url( 'order-pay' ) ) { - /** - * Needed for WordPress `query_vars`. - * - * @psalm-suppress InvalidGlobal - */ - global $wp; - - if ( isset( $wp->query_vars['order-pay'] ) && absint( $wp->query_vars['order-pay'] ) > 0 ) { - $order_id = absint( $wp->query_vars['order-pay'] ); - $order = wc_get_order( $order_id ); - if ( is_a( $order, WC_Order::class ) ) { - $order_total = (float) $order->get_total(); - if ( $order_total < 5 || $order_total > 2500 ) { - return false; - } - - foreach ( $order->get_items() as $item_id => $item ) { - if ( is_a( $item, WC_Order_Item_Product::class ) ) { - $product = wc_get_product( $item->get_product_id() ); - if ( is_a( $product, WC_Product::class ) && ! $this->product_ready_for_pui( $product ) ) { - return false; - } - } - } - } - } + if ( ! $this->checkout_helper->is_checkout_amount_allowed( 5, 2500 ) ) { + return false; } return true; diff --git a/modules/ppcp-wc-gateway/src/Notice/DccWithoutPayPalAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php similarity index 57% rename from modules/ppcp-wc-gateway/src/Notice/DccWithoutPayPalAdminNotice.php rename to modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php index 2343c9e00..171f50280 100644 --- a/modules/ppcp-wc-gateway/src/Notice/DccWithoutPayPalAdminNotice.php +++ b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php @@ -1,6 +1,6 @@ id = $id; $this->state = $state; $this->settings = $settings; $this->is_payments_page = $is_payments_page; @@ -76,12 +86,20 @@ class DccWithoutPayPalAdminNotice { return null; } + $gateway = $this->get_gateway(); + if ( ! $gateway ) { + return null; + } + + $name = $gateway->get_method_title(); + $message = sprintf( - /* translators: %1$s the gateway name. */ + /* translators: %1$s the gateway name, %2$s URL. */ __( - 'PayPal Card Processing cannot be used without the PayPal gateway. Enable the PayPal Gateway.', + '%1$s cannot be used without the PayPal gateway. Enable the PayPal gateway.', 'woocommerce-paypal-payments' ), + $name, admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) ); return new Message( $message, 'warning' ); @@ -93,9 +111,29 @@ class DccWithoutPayPalAdminNotice { * @return bool */ protected function should_display(): bool { - return State::STATE_ONBOARDED === $this->state->current_state() - && ( $this->is_payments_page || $this->is_ppcp_settings_page ) - && ( $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) - && ( ! $this->settings->has( 'enabled' ) || ! $this->settings->get( 'enabled' ) ); + if ( State::STATE_ONBOARDED !== $this->state->current_state() || + ( ! $this->is_payments_page && ! $this->is_ppcp_settings_page ) ) { + return false; + } + if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) { + return false; + } + + $gateway = $this->get_gateway(); + + return $gateway && wc_string_to_bool( $gateway->get_option( 'enabled' ) ); + } + + /** + * Returns the gateway object or null. + * + * @return WC_Payment_Gateway|null + */ + protected function get_gateway(): ?WC_Payment_Gateway { + $gateways = WC()->payment_gateways->payment_gateways(); + if ( ! isset( $gateways[ $this->id ] ) ) { + return null; + } + return $gateways[ $this->id ]; } } diff --git a/modules/ppcp-wc-gateway/src/Settings/PageMatcherTrait.php b/modules/ppcp-wc-gateway/src/Settings/PageMatcherTrait.php index f3b41fdfb..ea301362b 100644 --- a/modules/ppcp-wc-gateway/src/Settings/PageMatcherTrait.php +++ b/modules/ppcp-wc-gateway/src/Settings/PageMatcherTrait.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Settings; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; @@ -34,6 +35,7 @@ trait PageMatcherTrait { $gateway_page_id_map = array( PayPalGateway::ID => 'paypal', CreditCardGateway::ID => 'dcc', // TODO: consider using just the gateway ID for PayPal and DCC too. + CardButtonGateway::ID => CardButtonGateway::ID, WebhooksStatusPage::ID => WebhooksStatusPage::ID, ); return array_key_exists( $current_page_id, $gateway_page_id_map ) diff --git a/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php index 24faa5201..275574e97 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php @@ -10,8 +10,6 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Settings; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; /** @@ -29,21 +27,21 @@ class SectionsRenderer { protected $page_id; /** - * The api shop country. + * Key - page/gateway ID, value - displayed text. * - * @var string + * @var array */ - protected $api_shop_country; + protected $sections; /** * SectionsRenderer constructor. * - * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. - * @param string $api_shop_country The api shop country. + * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. + * @param array $sections Key - page/gateway ID, value - displayed text. */ - public function __construct( string $page_id, string $api_shop_country ) { - $this->page_id = $page_id; - $this->api_shop_country = $api_shop_country; + public function __construct( string $page_id, array $sections ) { + $this->page_id = $page_id; + $this->sections = $sections; } /** @@ -58,30 +56,22 @@ class SectionsRenderer { /** * Renders the Sections tab. */ - public function render() { + public function render(): void { if ( ! $this->should_render() ) { return; } - $sections = array( - PayPalGateway::ID => __( 'PayPal Checkout', 'woocommerce-paypal-payments' ), - CreditCardGateway::ID => __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ), - PayUponInvoiceGateway::ID => __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), - WebhooksStatusPage::ID => __( 'Webhooks Status', 'woocommerce-paypal-payments' ), - ); - - if ( 'DE' !== $this->api_shop_country ) { - unset( $sections[ PayUponInvoiceGateway::ID ] ); - } - echo '
    '; - $array_keys = array_keys( $sections ); + $array_keys = array_keys( $this->sections ); - foreach ( $sections as $id => $label ) { - $url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&' . self::KEY . '=' . $id ); - if ( PayUponInvoiceGateway::ID === $id ) { - $url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-pay-upon-invoice-gateway' ); + foreach ( $this->sections as $id => $label ) { + $url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=' . $id ); + if ( in_array( $id, array( CreditCardGateway::ID, WebhooksStatusPage::ID ), true ) ) { + // We need section=ppcp-gateway for the webhooks page because it is not a gateway, + // and for DCC because otherwise it will not render the page if gateway is not available (country/currency). + // Other gateways render fields differently, and their pages are not expected to work when gateway is not available. + $url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&' . self::KEY . '=' . $id ); } echo '
  • ' . esc_html( $label ) . ' ' . ( end( $array_keys ) === $id ? '' : '|' ) . '
  • '; } diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 8bb469a9d..d01287f67 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -29,7 +29,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; -use WooCommerce\PayPalCommerce\WcGateway\Notice\DccWithoutPayPalAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\GatewayWithoutPayPalAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -164,11 +164,15 @@ class WCGatewayModule implements ModuleInterface { $notices[] = $connect_message; } - $dcc_without_paypal_notice = $c->get( 'wcgateway.notice.dcc-without-paypal' ); - assert( $dcc_without_paypal_notice instanceof DccWithoutPayPalAdminNotice ); - $dcc_without_paypal_message = $dcc_without_paypal_notice->message(); - if ( $dcc_without_paypal_message ) { - $notices[] = $dcc_without_paypal_message; + foreach ( array( + $c->get( 'wcgateway.notice.dcc-without-paypal' ), + $c->get( 'wcgateway.notice.card-button-without-paypal' ), + ) as $gateway_without_paypal_notice ) { + assert( $gateway_without_paypal_notice instanceof GatewayWithoutPayPalAdminNotice ); + $message = $gateway_without_paypal_notice->message(); + if ( $message ) { + $notices[] = $message; + } } $authorize_order_action = $c->get( 'wcgateway.notice.authorize-order-action' ); @@ -231,6 +235,10 @@ class WCGatewayModule implements ModuleInterface { if ( 'DE' === $c->get( 'api.shop.country' ) && 'EUR' === $c->get( 'api.shop.currency' ) ) { ( $c->get( 'wcgateway.pay-upon-invoice' ) )->init(); } + + if ( defined( 'PPCP_FLAG_OXXO' ) && PPCP_FLAG_OXXO === true ) { + ( $c->get( 'wcgateway.oxxo' ) )->init(); + } } ); @@ -260,6 +268,18 @@ class WCGatewayModule implements ModuleInterface { 10, 2 ); + + add_action( + 'wc_ajax_ppc-oxxo', + static function () use ( $c ) { + if ( defined( 'PPCP_FLAG_OXXO' ) && PPCP_FLAG_OXXO === false ) { + return; + } + + $endpoint = $c->get( 'wcgateway.endpoint.oxxo' ); + $endpoint->handle_request(); + } + ); } /** @@ -284,10 +304,18 @@ class WCGatewayModule implements ModuleInterface { $methods[] = $container->get( 'wcgateway.credit-card-gateway' ); } + if ( $container->get( 'wcgateway.settings.allow_card_button_gateway' ) ) { + $methods[] = $container->get( 'wcgateway.card-button-gateway' ); + } + if ( 'DE' === $container->get( 'api.shop.country' ) && 'EUR' === $container->get( 'api.shop.currency' ) ) { $methods[] = $container->get( 'wcgateway.pay-upon-invoice-gateway' ); } + if ( defined( 'PPCP_FLAG_OXXO' ) && PPCP_FLAG_OXXO === true ) { + $methods[] = $container->get( 'wcgateway.oxxo-gateway' ); + } + return (array) $methods; } ); diff --git a/modules/ppcp-wc-gateway/webpack.config.js b/modules/ppcp-wc-gateway/webpack.config.js index 5abb1f94a..d66aad695 100644 --- a/modules/ppcp-wc-gateway/webpack.config.js +++ b/modules/ppcp-wc-gateway/webpack.config.js @@ -8,6 +8,7 @@ module.exports = { entry: { 'gateway-settings': path.resolve('./resources/js/gateway-settings.js'), 'pay-upon-invoice': path.resolve('./resources/js/pay-upon-invoice.js'), + 'oxxo': path.resolve('./resources/js/oxxo.js'), }, output: { path: path.resolve(__dirname, 'assets/'), diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php index 0b29eb718..5e7ca8674 100644 --- a/modules/ppcp-webhooks/services.php +++ b/modules/ppcp-webhooks/services.php @@ -20,6 +20,8 @@ use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint; use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderApproved; use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderCompleted; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureCompleted; +use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureDenied; +use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCapturePending; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureRefunded; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureReversed; use Psr\Container\ContainerInterface; @@ -78,6 +80,7 @@ return array( new PaymentCaptureCompleted( $logger, $prefix, $order_endpoint ), new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor ), new VaultCreditCardCreated( $logger, $prefix ), + new PaymentCapturePending( $logger ), ); }, diff --git a/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php b/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php index fdbdf8abc..350d0e168 100644 --- a/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php +++ b/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Webhooks\Handler; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use Psr\Log\LoggerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; /** @@ -189,7 +190,7 @@ class CheckoutOrderApproved implements RequestHandler { } foreach ( $wc_orders as $wc_order ) { - if ( PayUponInvoiceGateway::ID === $wc_order->get_payment_method() ) { + if ( PayUponInvoiceGateway::ID === $wc_order->get_payment_method() || OXXOGateway::ID === $wc_order->get_payment_method() ) { continue; } diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCapturePending.php b/modules/ppcp-webhooks/src/Handler/PaymentCapturePending.php new file mode 100644 index 000000000..225e70121 --- /dev/null +++ b/modules/ppcp-webhooks/src/Handler/PaymentCapturePending.php @@ -0,0 +1,122 @@ +logger = $logger; + } + + /** + * The event types a handler handles. + * + * @return string[] + */ + public function event_types(): array { + return array( 'PAYMENT.CAPTURE.PENDING' ); + } + + /** + * Whether a handler is responsible for a given request or not. + * + * @param \WP_REST_Request $request The request. + * + * @return bool + */ + public function responsible_for_request( \WP_REST_Request $request ): bool { + return in_array( $request['event_type'], $this->event_types(), true ); + } + + /** + * Responsible for handling the request. + * + * @param WP_REST_Request $request The request. + * + * @return WP_REST_Response + */ + public function handle_request( WP_REST_Request $request ): WP_REST_Response { + $response = array( 'success' => false ); + $order_id = $request['resource'] !== null && isset( $request['resource']['custom_id'] ) + ? $this->sanitize_custom_id( $request['resource']['custom_id'] ) + : 0; + if ( ! $order_id ) { + $message = sprintf( + // translators: %s is the PayPal webhook Id. + __( + 'No order for webhook event %s was found.', + 'woocommerce-paypal-payments' + ), + $request['id'] !== null && isset( $request['id'] ) ? $request['id'] : '' + ); + $this->logger->log( + 'warning', + $message, + array( + 'request' => $request, + ) + ); + $response['message'] = $message; + return new WP_REST_Response( $response ); + } + + $resource = $request['resource']; + if ( ! is_array( $resource ) ) { + $message = 'Resource data not found in webhook request.'; + $this->logger->warning( $message, array( 'request' => $request ) ); + $response['message'] = $message; + return new WP_REST_Response( $response ); + } + + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, \WC_Order::class ) ) { + $message = sprintf( + 'WC order for PayPal ID %s not found.', + $request['resource'] !== null && isset( $request['resource']['id'] ) ? $request['resource']['id'] : '' + ); + + $this->logger->warning( $message ); + + $response['message'] = $message; + return new WP_REST_Response( $response ); + } + + if ( $wc_order->get_status() === 'pending' ) { + $wc_order->update_status( 'on-hold', __( 'Payment initiation was successful, and is waiting for the buyer to complete the payment.', 'woocommerce-paypal-payments' ) ); + + } + + $response['success'] = true; + return new WP_REST_Response( $response ); + } +} diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php b/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php index 7a4db8bf1..d879a2345 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentCaptureRefunded.php @@ -46,7 +46,7 @@ class PaymentCaptureRefunded implements RequestHandler { * @return string[] */ public function event_types(): array { - return array( 'PAYMENT.CAPTURE.REFUNDED' ); + return array( 'PAYMENT.CAPTURE.REFUNDED', 'PAYMENT.AUTHORIZATION.VOIDED' ); } /** diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCaptureReversed.php b/modules/ppcp-webhooks/src/Handler/PaymentCaptureReversed.php index bef765503..5354e555c 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentCaptureReversed.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentCaptureReversed.php @@ -112,12 +112,17 @@ class PaymentCaptureReversed implements RequestHandler { return rest_ensure_response( $response ); } + /** + * Allows adding an update status note. + */ + $note = apply_filters( 'ppcp_payment_capture_reversed_webhook_update_status_note', '', $wc_order, $request['event_type'] ); + /** * The WooCommerce order. * * @var \WC_Order $wc_order */ - $response['success'] = (bool) $wc_order->update_status( 'cancelled' ); + $response['success'] = (bool) $wc_order->update_status( 'cancelled', $note ); $message = $response['success'] ? sprintf( // translators: %1$s is the order id. diff --git a/package.json b/package.json index f9b601973..edabc03b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "1.9.1", + "version": "1.9.2", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index dec5134eb..e3bf1233a 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, e-commerce, store, sales, sell, Requires at least: 5.3 Tested up to: 6.0 Requires PHP: 7.1 -Stable tag: 1.9.1 +Stable tag: 1.9.2 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -81,6 +81,19 @@ Follow the steps below to connect the plugin to your PayPal account: == Changelog == += 1.9.2 = +* Fix - Do not allow birth date older than 100 years for PUI. #743 +* Fix - Store the customer id for vaulted payment method in usermeta to not lose vaulted methods after the invoice prefix change. #698 +* Fix - Capture Virtual-Only Orders setting did not auto-capture subscription renewal payments. #626 +* Fix - Voiding authorization at PayPal did not update the status/order notes. #712 +* Fix - PayPal scripts were loading on pages without smart buttons or Pay Later messaging. #750 +* Fix - Do not show links for unavailable gateways settings pages. #753 +* Fix - The smart buttons were not loaded on single product page if a subscription product exists in the cart. #703 +* Fix - DCC was causing other gateways to disappear after checkout validation error. #757 +* Fix - Buttons not loading on single product page with default settings when product is in cart. #777 +* Enhancement - Improve Checkout Field Validation Message. #739 +* Enhancement - Handle PAYER_ACTION_REQUIRED error. #759 + = 1.9.1 = * Fix - ITEM_TOTAL_MISMATCH error when checking out with multiple products #721 * Fix - Unable to purchase a product with Credit card button in pay for order page #718 diff --git a/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php b/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php index 1f6a30e80..6b0b2ef14 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php @@ -46,6 +46,7 @@ class IdentityTokenTest extends TestCase public function testGenerateForCustomerReturnsToken() { + $id = 1; define( 'PPCP_FLAG_SUBSCRIPTION', true ); $token = Mockery::mock(Token::class); $token @@ -60,6 +61,7 @@ class IdentityTokenTest extends TestCase $this->settings->shouldReceive('has')->andReturn(true); $this->settings->shouldReceive('get')->andReturn(true); $this->customer_repository->shouldReceive('customer_id_for_user')->andReturn('prefix1'); + expect('update_user_meta')->with($id, 'ppcp_customer_id', 'prefix1'); $rawResponse = [ 'body' => '{"client_token":"abc123", "expires_in":3600}', @@ -97,6 +99,7 @@ class IdentityTokenTest extends TestCase public function testGenerateForCustomerFailsBecauseWpError() { + $id = 1; $token = Mockery::mock(Token::class); $token ->expects('token')->andReturn('bearer'); @@ -111,7 +114,8 @@ class IdentityTokenTest extends TestCase $this->logger->shouldReceive('debug'); $this->settings->shouldReceive('has')->andReturn(true); $this->settings->shouldReceive('get')->andReturn(true); - $this->customer_repository->shouldReceive('customer_id_for_user'); + $this->customer_repository->shouldReceive('customer_id_for_user')->andReturn('prefix1'); + expect('update_user_meta')->with($id, 'ppcp_customer_id', 'prefix1'); $this->expectException(RuntimeException::class); $this->sut->generate_for_user(1); @@ -119,6 +123,7 @@ class IdentityTokenTest extends TestCase public function testGenerateForCustomerFailsBecauseResponseCodeIsNot200() { + $id = 1; $token = Mockery::mock(Token::class); $token ->expects('token')->andReturn('bearer'); @@ -137,7 +142,8 @@ class IdentityTokenTest extends TestCase $this->logger->shouldReceive('debug'); $this->settings->shouldReceive('has')->andReturn(true); $this->settings->shouldReceive('get')->andReturn(true); - $this->customer_repository->shouldReceive('customer_id_for_user'); + $this->customer_repository->shouldReceive('customer_id_for_user')->andReturn('prefix1'); + expect('update_user_meta')->with($id, 'ppcp_customer_id', 'prefix1'); $this->expectException(PayPalApiException::class); $this->sut->generate_for_user(1); diff --git a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php index 642d9c4b6..4052febd2 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php @@ -1046,8 +1046,6 @@ class OrderEndpointTest extends TestCase $payer = Mockery::mock(Payer::class); $payer->expects('email_address')->andReturn('email@email.com'); - $payerName = Mockery::mock(PayerName::class); - $payer->expects('name')->andReturn($payerName); $payer->expects('to_array')->andReturn(['payer']); $result = $testee->create([$purchaseUnit], ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE, $payer); $this->assertEquals($expectedOrder, $result); @@ -1138,8 +1136,6 @@ class OrderEndpointTest extends TestCase $payer = Mockery::mock(Payer::class); $payer->expects('email_address')->andReturn('email@email.com'); - $payerName = Mockery::mock(PayerName::class); - $payer->expects('name')->andReturn($payerName); $payer->expects('to_array')->andReturn(['payer']); $testee->create([$purchaseUnit], ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING, $payer); } @@ -1229,8 +1225,6 @@ class OrderEndpointTest extends TestCase $this->expectException(RuntimeException::class); $payer = Mockery::mock(Payer::class); $payer->expects('email_address')->andReturn('email@email.com'); - $payerName = Mockery::mock(PayerName::class); - $payer->expects('name')->andReturn($payerName); $payer->expects('to_array')->andReturn(['payer']); $testee->create([$purchaseUnit], ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE, $payer); } diff --git a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php index 00d863a9e..d185f37ea 100644 --- a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php +++ b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php @@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\TestCase; +use WooCommerce\PayPalCommerce\WcGateway\CardBillingMode; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; @@ -152,6 +153,7 @@ class CreateOrderEndpointTest extends TestCase $session_handler = Mockery::mock(SessionHandler::class); $settings = Mockery::mock(Settings::class); $early_order_handler = Mockery::mock(EarlyOrderHandler::class); + $settings->shouldReceive('has')->andReturnFalse(); $testee = new CreateOrderEndpoint( $request_data, @@ -163,6 +165,7 @@ class CreateOrderEndpointTest extends TestCase $settings, $early_order_handler, false, + CardBillingMode::MINIMAL_INPUT, new NullLogger() ); return array($payer_factory, $testee); diff --git a/tests/PHPUnit/Subscription/RenewalHandlerTest.php b/tests/PHPUnit/Subscription/RenewalHandlerTest.php index de3249fd6..86b725f49 100644 --- a/tests/PHPUnit/Subscription/RenewalHandlerTest.php +++ b/tests/PHPUnit/Subscription/RenewalHandlerTest.php @@ -23,6 +23,8 @@ use Mockery; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; class RenewalHandlerTest extends TestCase { @@ -48,6 +50,11 @@ class RenewalHandlerTest extends TestCase $this->shippingPreferenceFactory = Mockery::mock(ShippingPreferenceFactory::class); $this->payerFactory = Mockery::mock(PayerFactory::class); $this->environment = new Environment(new Dictionary([])); + $authorizedPaymentProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); + $settings = Mockery::mock(Settings::class); + $settings + ->shouldReceive('has') + ->andReturnFalse(); $this->logger->shouldReceive('error')->andReturnUsing(function ($msg) { throw new Exception($msg); @@ -61,7 +68,9 @@ class RenewalHandlerTest extends TestCase $this->purchaseUnitFactory, $this->shippingPreferenceFactory, $this->payerFactory, - $this->environment + $this->environment, + $settings, + $authorizedPaymentProcessor ); } diff --git a/tests/PHPUnit/WcGateway/Gateway/OXXO/OXXOGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/OXXO/OXXOGatewayTest.php new file mode 100644 index 000000000..5f55ff8e8 --- /dev/null +++ b/tests/PHPUnit/WcGateway/Gateway/OXXO/OXXOGatewayTest.php @@ -0,0 +1,141 @@ +orderEndpoint = Mockery::mock(OrderEndpoint::class); + $this->purchaseUnitFactory = Mockery::mock(PurchaseUnitFactory::class); + $this->shippingPreferenceFactory = Mockery::mock(ShippingPreferenceFactory::class); + $this->logger = Mockery::mock(LoggerInterface::class); + + $this->wcOrder = Mockery::mock(WC_Order::class); + when('wc_get_order')->justReturn($this->wcOrder); + when('get_option')->justReturn([ + 'title' => 'foo', + 'description' => 'bar', + ]); + + $this->testee = new OXXOGateway( + $this->orderEndpoint, + $this->purchaseUnitFactory, + $this->shippingPreferenceFactory, + 'oxxo.svg', + $this->logger + ); + } + + public function testProcessPaymentSuccess() + { + $this->wcOrder->shouldReceive('get_billing_first_name')->andReturn('John'); + $this->wcOrder->shouldReceive('get_billing_last_name')->andReturn('Doe'); + $this->wcOrder->shouldReceive('get_billing_email')->andReturn('foo@bar.com'); + $this->wcOrder->shouldReceive('get_billing_country')->andReturn('MX'); + + list($purchaseUnit, $shippingPreference) = $this->setStubs(); + + $linkHref = 'https://sandbox.paypal.com/payment/oxxo?token=ABC123'; + $this->orderEndpoint + ->shouldReceive('confirm_payment_source') + ->with('1', [ + 'oxxo' => [ + 'name' => 'John Doe', + 'email' => 'foo@bar.com', + 'country_code' => 'MX', + ] + ] + )->andReturn((object)[ + 'links' => [ + (object)[ + 'rel' => 'payer-action', + 'href' => $linkHref, + ], + ] + ]); + + $order = Mockery::mock(Order::class); + $order->shouldReceive('id')->andReturn('1'); + + $this->orderEndpoint + ->shouldReceive('create') + ->with([$purchaseUnit], $shippingPreference) + ->andReturn($order); + + $this->wcOrder + ->shouldReceive('add_meta_data') + ->with('ppcp_oxxo_payer_action', $linkHref) + ->andReturn(true); + $this->wcOrder->shouldReceive('save_meta_data'); + + $woocommerce = Mockery::mock(\WooCommerce::class); + $cart = Mockery::mock(\WC_Cart::class); + when('WC')->justReturn($woocommerce); + $woocommerce->cart = $cart; + $cart->shouldReceive('empty_cart'); + + $result = $this->testee->process_payment(1); + $this->assertEquals('success', $result['result']); + } + + public function testProcessPaymentFailure() + { + list($purchaseUnit, $shippingPreference) = $this->setStubs(); + + $this->orderEndpoint + ->shouldReceive('create') + ->with([$purchaseUnit], $shippingPreference) + ->andThrows(RuntimeException::class); + + $this->logger->shouldReceive('error'); + when('wc_add_notice')->justReturn(); + when('wc_get_checkout_url')->justReturn(); + $this->wcOrder->shouldReceive('update_status'); + + $result = $this->testee->process_payment(1); + $this->assertEquals('failure', $result['result']); + + } + + /** + * @return array + */ + private function setStubs(): array + { + $purchaseUnit = Mockery::mock(PurchaseUnit::class); + $this->purchaseUnitFactory + ->shouldReceive('from_wc_order') + ->with($this->wcOrder) + ->andReturn($purchaseUnit); + + $shippingPreference = 'SOME_SHIPPING_PREFERENCE'; + $this->shippingPreferenceFactory + ->shouldReceive('from_state') + ->with($purchaseUnit, 'checkout') + ->andReturn($shippingPreference); + return array($purchaseUnit, $shippingPreference); + } +} diff --git a/tests/PHPUnit/WcGateway/Gateway/PayUponInvoice/PayUponInvoiceGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/PayUponInvoice/PayUponInvoiceGatewayTest.php index ca9fe38c5..02268e917 100644 --- a/tests/PHPUnit/WcGateway/Gateway/PayUponInvoice/PayUponInvoiceGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/PayUponInvoice/PayUponInvoiceGatewayTest.php @@ -13,6 +13,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\TestCase; use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider; +use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper; use function Brain\Monkey\Functions\when; @@ -26,6 +27,7 @@ class PayUponInvoiceGatewayTest extends TestCase private $logger; private $testee; private $pui_helper; + private $checkout_helper; public function setUp(): void { @@ -38,6 +40,7 @@ class PayUponInvoiceGatewayTest extends TestCase $this->logger = Mockery::mock(LoggerInterface::class); $this->transaction_url_provider = Mockery::mock(TransactionUrlProvider::class); $this->pui_helper = Mockery::mock(PayUponInvoiceHelper::class); + $this->checkout_helper = Mockery::mock(CheckoutHelper::class); $this->setInitStubs(); @@ -48,7 +51,8 @@ class PayUponInvoiceGatewayTest extends TestCase $this->environment, $this->transaction_url_provider, $this->logger, - $this->pui_helper + $this->pui_helper, + $this->checkout_helper ); } diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index b9b2a87cb..ec901bafa 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -3,14 +3,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; -use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; -use Psr\Log\NullLogger; -use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; -use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; -use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; @@ -37,7 +30,6 @@ class WcGatewayTest extends TestCase private $settingsRenderer; private $funding_source_renderer; private $orderProcessor; - private $authorizedOrdersProcessor; private $settings; private $refundProcessor; private $onboardingState; @@ -45,10 +37,7 @@ class WcGatewayTest extends TestCase private $subscriptionHelper; private $environment; private $paymentTokenRepository; - private $shipping_preference_factory; private $logger; - private $paymentsEndpoint; - private $orderEndpoint; private $apiShopCountry; public function setUp(): void { @@ -60,7 +49,6 @@ class WcGatewayTest extends TestCase $this->settingsRenderer = Mockery::mock(SettingsRenderer::class); $this->orderProcessor = Mockery::mock(OrderProcessor::class); - $this->authorizedOrdersProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); $this->settings = Mockery::mock(Settings::class); $this->sessionHandler = Mockery::mock(SessionHandler::class); $this->refundProcessor = Mockery::mock(RefundProcessor::class); @@ -69,10 +57,7 @@ class WcGatewayTest extends TestCase $this->subscriptionHelper = Mockery::mock(SubscriptionHelper::class); $this->environment = Mockery::mock(Environment::class); $this->paymentTokenRepository = Mockery::mock(PaymentTokenRepository::class); - $this->shipping_preference_factory = Mockery::mock(ShippingPreferenceFactory::class); $this->logger = Mockery::mock(LoggerInterface::class); - $this->paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class); - $this->orderEndpoint = Mockery::mock(OrderEndpoint::class); $this->funding_source_renderer = new FundingSourceRenderer($this->settings); $this->apiShopCountry = 'DE'; @@ -87,6 +72,7 @@ class WcGatewayTest extends TestCase $this->settings->shouldReceive('has')->andReturnFalse(); $this->logger->shouldReceive('info'); + $this->logger->shouldReceive('error'); } private function createGateway() @@ -95,7 +81,6 @@ class WcGatewayTest extends TestCase $this->settingsRenderer, $this->funding_source_renderer, $this->orderProcessor, - $this->authorizedOrdersProcessor, $this->settings, $this->sessionHandler, $this->refundProcessor, @@ -105,10 +90,7 @@ class WcGatewayTest extends TestCase PayPalGateway::ID, $this->environment, $this->paymentTokenRepository, - $this->shipping_preference_factory, $this->logger, - $this->paymentsEndpoint, - $this->orderEndpoint, $this->apiShopCountry ); } @@ -173,8 +155,10 @@ class WcGatewayTest extends TestCase when('wc_get_checkout_url') ->justReturn($redirectUrl); - expect('wc_add_notice') - ->with('Couldn\'t find order to process','error'); + $this->sessionHandler + ->shouldReceive('destroy_session_data'); + + expect('wc_add_notice'); $this->assertEquals( [ @@ -195,7 +179,6 @@ class WcGatewayTest extends TestCase ->andReturnFalse(); $this->orderProcessor ->expects('last_error') - ->twice() ->andReturn($lastError); $this->subscriptionHelper->shouldReceive('has_subscription')->with($orderId)->andReturn(true); $this->subscriptionHelper->shouldReceive('is_subscription_change_payment')->andReturn(true); @@ -206,6 +189,8 @@ class WcGatewayTest extends TestCase expect('wc_get_order') ->with($orderId) ->andReturn($wcOrder); + $this->sessionHandler + ->shouldReceive('destroy_session_data'); expect('wc_add_notice') ->with($lastError, 'error'); diff --git a/tests/PHPUnit/WcGateway/Helper/PayUponInvoiceHelperTest.php b/tests/PHPUnit/WcGateway/Helper/PayUponInvoiceHelperTest.php index 3a1d9bdc0..10a99b422 100644 --- a/tests/PHPUnit/WcGateway/Helper/PayUponInvoiceHelperTest.php +++ b/tests/PHPUnit/WcGateway/Helper/PayUponInvoiceHelperTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Helper; use DateTime; +use Mockery; use WooCommerce\PayPalCommerce\TestCase; class PayUponInvoiceHelperTest extends TestCase @@ -13,7 +14,7 @@ class PayUponInvoiceHelperTest extends TestCase */ public function testValidateBirthDate($input, $output) { - $this->assertSame((new PayUponInvoiceHelper())->validate_birth_date($input), $output); + $this->assertSame((new CheckoutHelper())->validate_birth_date($input), $output); } public function datesProvider(): array{ @@ -26,6 +27,7 @@ class PayUponInvoiceHelperTest extends TestCase ['1942-02-31', false], ['01-01-1942', false], ['1942-01-01', true], + ['0001-01-01', false], ]; } diff --git a/tests/stubs/WC_Payment_Gateway.php b/tests/stubs/WC_Payment_Gateway.php index 6fbb823c8..bb87ee010 100644 --- a/tests/stubs/WC_Payment_Gateway.php +++ b/tests/stubs/WC_Payment_Gateway.php @@ -4,7 +4,7 @@ declare(strict_types=1); class WC_Payment_Gateway { - protected function get_option(string $key) : string { + public function get_option(string $key, $empty_value = null) { return $key; } @@ -19,4 +19,4 @@ class WC_Payment_Gateway public function process_admin_options() { } -} \ No newline at end of file +} diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 38817fd03..6dc5feb05 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,13 +3,13 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 1.9.1 + * Version: 1.9.2 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 * Requires PHP: 7.1 * WC requires at least: 3.9 - * WC tested up to: 6.6 + * WC tested up to: 6.7 * Text Domain: woocommerce-paypal-payments * * @package WooCommerce\PayPalCommerce @@ -24,6 +24,8 @@ define( 'PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com' ); define( 'PAYPAL_INTEGRATION_DATE', '2022-04-13' ); define( 'PPCP_FLAG_SUBSCRIPTION', true ); +define( 'PPCP_FLAG_OXXO', apply_filters( 'woocommerce_paypal_payments_enable_oxxo_feature', false ) ); +define( 'PPCP_FLAG_SEPARATE_APM_BUTTONS', apply_filters( 'woocommerce_paypal_payments_enable_separate_apm_buttons_feature', false ) ); ! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' ); ! defined( 'CONNECT_WOO_SANDBOX_CLIENT_ID' ) && define( 'CONNECT_WOO_SANDBOX_CLIENT_ID', 'AYmOHbt1VHg-OZ_oihPdzKEVbU3qg0qXonBcAztuzniQRaKE0w1Hr762cSFwd4n8wxOl-TCWohEa0XM_' );