From 93eaed140bbb5af01a90137287c32a8c04381fce Mon Sep 17 00:00:00 2001 From: Daniel Guerra <15204776+danielmx-dev@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:19:22 +0300 Subject: [PATCH 1/8] Use the customer ID saved in the subscription to process renewal payments (#9065) Co-authored-by: Brett Shumaker --- ...x-8892-use-correct-customer-id-in-renewals | 4 ++ includes/class-payment-information.php | 21 +++++- includes/class-wc-payment-gateway-wcpay.php | 10 ++- ...wc-payment-gateway-wcpay-subscriptions.php | 4 +- tests/unit/test-class-payment-information.php | 17 +++++ ...wc-payment-gateway-wcpay-subscriptions.php | 70 +++++++++++++++++++ 6 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 changelog/fix-8892-use-correct-customer-id-in-renewals diff --git a/changelog/fix-8892-use-correct-customer-id-in-renewals b/changelog/fix-8892-use-correct-customer-id-in-renewals new file mode 100644 index 00000000000..2e367a199dd --- /dev/null +++ b/changelog/fix-8892-use-correct-customer-id-in-renewals @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Use the customer id saved in the subscription to process renewal payments. diff --git a/includes/class-payment-information.php b/includes/class-payment-information.php index 38191bf7ded..2fd39f78739 100644 --- a/includes/class-payment-information.php +++ b/includes/class-payment-information.php @@ -105,6 +105,13 @@ class Payment_Information { */ private $payment_method_stripe_id; + /** + * The WCPay Customer ID that owns the payment token. + * + * @var string + */ + private $customer_id; + /** * Payment information constructor. * @@ -117,6 +124,7 @@ class Payment_Information { * @param string $cvc_confirmation The CVC confirmation for this payment method. * @param string $fingerprint The attached fingerprint. * @param string $payment_method_stripe_id The Stripe ID of the payment method used for this payment. + * @param string $customer_id The WCPay Customer ID that owns the payment token. * * @throws Invalid_Payment_Method_Exception When no payment method is found in the provided request. */ @@ -129,7 +137,8 @@ public function __construct( Payment_Capture_Type $manual_capture = null, string $cvc_confirmation = null, string $fingerprint = '', - string $payment_method_stripe_id = null + string $payment_method_stripe_id = null, + string $customer_id = null ) { if ( empty( $payment_method ) && empty( $token ) && ! \WC_Payments::is_network_saved_cards_enabled() ) { // If network-wide cards are enabled, a payment method or token may not be specified and the platform default one will be used. @@ -147,6 +156,7 @@ public function __construct( $this->cvc_confirmation = $cvc_confirmation; $this->fingerprint = $fingerprint; $this->payment_method_stripe_id = $payment_method_stripe_id; + $this->customer_id = $customer_id; } /** @@ -436,4 +446,13 @@ public function get_fingerprint() { public function get_payment_method_stripe_id() { return $this->payment_method_stripe_id; } + + /** + * Returns the WCPay Customer ID that owns the payment token. + * + * @return string The WCPay Customer ID. + */ + public function get_customer_id() { + return $this->customer_id; + } } diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index c5126c398e0..e1e6a3543c9 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -1475,10 +1475,16 @@ public function process_payment_for_order( $cart, $payment_information, $schedul $amount = $order->get_total(); $metadata = $this->get_metadata_from_order( $order, $payment_information->get_payment_type() ); - $customer_details_options = [ + $customer_details_options = [ 'is_woopay' => filter_var( $metadata['paid_on_woopay'] ?? false, FILTER_VALIDATE_BOOLEAN ), ]; - list( $user, $customer_id ) = $this->manage_customer_details_for_order( $order, $customer_details_options ); + + if ( $payment_information->get_customer_id() ) { + $user = $order->get_user(); + $customer_id = $payment_information->get_customer_id(); + } else { + list( $user, $customer_id ) = $this->manage_customer_details_for_order( $order, $customer_details_options ); + } $intent_failed = false; $payment_needed = $amount > 0; diff --git a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php index da7a1379c14..31ec70bedf8 100644 --- a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php +++ b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php @@ -331,8 +331,10 @@ public function scheduled_subscription_payment( $amount, $renewal_order ) { return; } + $customer_id = $this->order_service->get_customer_id_for_order( $renewal_order ); + try { - $payment_information = new Payment_Information( '', $renewal_order, Payment_Type::RECURRING(), $token, Payment_Initiated_By::MERCHANT(), null, null, '', $this->get_payment_method_to_use_for_intent() ); + $payment_information = new Payment_Information( '', $renewal_order, Payment_Type::RECURRING(), $token, Payment_Initiated_By::MERCHANT(), null, null, '', $this->get_payment_method_to_use_for_intent(), $customer_id ); $this->process_payment_for_order( null, $payment_information, true ); } catch ( API_Exception $e ) { Logger::error( 'Error processing subscription renewal: ' . $e->getMessage() ); diff --git a/tests/unit/test-class-payment-information.php b/tests/unit/test-class-payment-information.php index b6cfd8c3960..3067e1c13b2 100644 --- a/tests/unit/test-class-payment-information.php +++ b/tests/unit/test-class-payment-information.php @@ -98,6 +98,23 @@ public function test_set_token_updates_token() { $this->assertTrue( $payment_information->is_using_saved_payment_method() ); } + public function test_get_customer_id() { + $expected_customer_id = 'old_customer_id'; + $payment_information = new Payment_Information( + self::PAYMENT_METHOD, + null, + Payment_Type::SINGLE(), + $this->card_token, + null, + null, + null, + '', + null, + $expected_customer_id + ); + $this->assertEquals( $expected_customer_id, $payment_information->get_customer_id() ); + } + public function test_get_payment_method_from_request() { $payment_method = Payment_Information::get_payment_method_from_request( [ self::PAYMENT_METHOD_REQUEST_KEY => self::PAYMENT_METHOD ] diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php index 151b3b919fe..8d011f7f508 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php @@ -354,6 +354,76 @@ public function test_scheduled_subscription_payment() { $this->assertEquals( 'processing', $renewal_order->get_status() ); } + public function test_scheduled_subscription_payment_with_saved_customer_id() { + $saved_customer_id = self::CUSTOMER_ID . '_old'; + + $renewal_order = WC_Helper_Order::create_order( self::USER_ID ); + + $token = WC_Helper_Token::create_token( self::PAYMENT_METHOD_ID, self::USER_ID ); + $renewal_order->add_payment_token( $token ); + + $this->order_service->set_customer_id_for_order( $renewal_order, $saved_customer_id ); + + $mock_subscription = new WC_Subscription(); + + $this->mock_wcs_get_subscriptions_for_renewal_order( [ '1' => $mock_subscription ] ); + + $this->mock_customer_service + ->expects( $this->never() ) + ->method( 'get_customer_id_by_user_id' ); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); + + $request->expects( $this->once() ) + ->method( 'set_customer' ) + ->with( $saved_customer_id ); + + $request->expects( $this->once() ) + ->method( 'set_payment_method' ) + ->with( self::PAYMENT_METHOD_ID ); + + $request->expects( $this->once() ) + ->method( 'set_cvc_confirmation' ) + ->with( null ); + + $request->expects( $this->once() ) + ->method( 'set_amount' ) + ->with( 5000 ) + ->willReturn( $request ); + + $request->expects( $this->once() ) + ->method( 'set_currency_code' ) + ->with( 'usd' ) + ->willReturn( $request ); + + $request->expects( $this->never() ) + ->method( 'setup_future_usage' ); + + $request->expects( $this->once() ) + ->method( 'set_capture_method' ) + ->with( false ); + + $request->expects( $this->once() ) + ->method( 'set_off_session' ) + ->with( true ); + + $request->expects( $this->once() ) + ->method( 'set_capture_method' ) + ->with( false ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( WC_Helper_Intention::create_intention() ); + + $this->mock_customer_service + ->expects( $this->never() ) + ->method( 'update_customer_for_user' ); + + $this->wcpay_gateway->scheduled_subscription_payment( $renewal_order->get_total(), $renewal_order ); + + $this->assertEquals( 'processing', $renewal_order->get_status() ); + } + public function test_scheduled_subscription_payment_fails_when_token_is_missing() { $renewal_order = WC_Helper_Order::create_order( self::USER_ID ); From 76d2cd5199a6cfa61b4a36f42ade13af0bb2a78a Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Fri, 12 Jul 2024 14:56:31 -0300 Subject: [PATCH 2/8] Fix payment method title for Express Checkout Element orders (#9090) --- changelog/2024-07-11-21-06-01-653423 | 4 ++ ...payments-express-checkout-ajax-handler.php | 33 +----------- ...yments-express-checkout-button-handler.php | 29 ++++++++++ ...ayments-express-checkout-button-helper.php | 53 +++++++++++++++++++ 4 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 changelog/2024-07-11-21-06-01-653423 diff --git a/changelog/2024-07-11-21-06-01-653423 b/changelog/2024-07-11-21-06-01-653423 new file mode 100644 index 00000000000..81ed2da10c2 --- /dev/null +++ b/changelog/2024-07-11-21-06-01-653423 @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix payment method title for Express Checkout Element orders. diff --git a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php index c4ba8cff02b..e912ca8d262 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php @@ -112,7 +112,7 @@ public function ajax_pay_for_order() { throw new Exception( __( 'This order does not require payment!', 'woocommerce-payments' ) ); } - $this->add_order_meta( $order_id ); + $this->express_checkout_button_helper->add_order_payment_method_title( $order_id ); // Load the gateway. $all_gateways = WC()->payment_gateways->get_available_payment_gateways(); @@ -431,35 +431,4 @@ public function ajax_empty_cart() { wp_send_json( [ 'result' => 'success' ] ); } - - /** - * Add needed order meta - * - * @param integer $order_id The order ID. - * - * @return void - */ - public function add_order_meta( $order_id ) { - if ( empty( $_POST['express_payment_type'] ) || ! isset( $_POST['payment_method'] ) || 'woocommerce_payments' !== $_POST['payment_method'] ) { // phpcs:ignore WordPress.Security.NonceVerification - return; - } - - $order = wc_get_order( $order_id ); - - $express_payment_type = wc_clean( wp_unslash( $_POST['express_payment_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification - - $express_payment_titles = [ - 'apple_pay' => 'Apple Pay', - 'google_pay' => 'Google Pay', - ]; - - $suffix = apply_filters( 'wcpay_payment_request_payment_method_title_suffix', 'WooPayments' ); - if ( ! empty( $suffix ) ) { - $suffix = " ($suffix)"; - } - - $payment_method_title = isset( $express_payment_titles[ $express_payment_type ] ) ? $express_payment_titles[ $express_payment_type ] : 'Express Payment'; - $order->set_payment_method_title( $payment_method_title . $suffix ); - $order->save(); - } } diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php index 6a8d66d1bd8..9c62be93c45 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php @@ -94,6 +94,8 @@ public function init() { add_filter( 'woocommerce_registration_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] ); add_action( 'before_woocommerce_pay_form', [ $this, 'display_pay_for_order_page_html' ], 1 ); + add_filter( 'woocommerce_gateway_title', [ $this, 'filter_gateway_title' ], 10, 2 ); + add_action( 'woocommerce_checkout_order_processed', [ $this->express_checkout_helper, 'add_order_payment_method_title' ], 10, 2 ); $this->express_checkout_ajax_handler->init(); } @@ -402,4 +404,31 @@ public function get_login_redirect_url( $redirect ) { return $url; } + + /** + * Filters the gateway title to reflect the button type used. + * + * @param string $title Gateway title. + * @param string $id Gateway ID. + */ + public function filter_gateway_title( $title, $id ) { + if ( 'woocommerce_payments' !== $id || ! is_admin() ) { + return $title; + } + + $order = $this->express_checkout_helper->get_current_order(); + $method_title = is_object( $order ) ? $order->get_payment_method_title() : ''; + + if ( ! empty( $method_title ) ) { + if ( + strpos( $method_title, 'Apple Pay' ) === 0 + || strpos( $method_title, 'Google Pay' ) === 0 + || strpos( $method_title, 'Payment Request' ) === 0 // Legacy PRB title. + ) { + return $method_title; + } + } + + return $title; + } } diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php index 8f9d0d7ac9c..5d52fa94691 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php @@ -329,6 +329,26 @@ public function get_product() { return null; } + /** + * Used to get the order in admin edit page. + * + * @return WC_Order|WC_Order_Refund|bool + */ + public function get_current_order() { + global $theorder; + global $post; + + if ( is_object( $theorder ) ) { + return $theorder; + } + + if ( is_object( $post ) ) { + return wc_get_order( $post->ID ); + } + + return false; + } + /** * Returns true if the provided WC_Product is a subscription, false otherwise. * @@ -1033,6 +1053,39 @@ public function update_shipping_method( $shipping_methods ) { WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); } + /** + * Add express checkout payment method title to the order. + * + * @param integer $order_id The order ID. + * + * @return void + */ + public function add_order_payment_method_title( $order_id ) { + if ( empty( $_POST['express_payment_type'] ) || ! isset( $_POST['payment_method'] ) || 'woocommerce_payments' !== $_POST['payment_method'] ) { // phpcs:ignore WordPress.Security.NonceVerification + return; + } + + $express_payment_type = wc_clean( wp_unslash( $_POST['express_payment_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification + $express_payment_titles = [ + 'apple_pay' => 'Apple Pay', + 'google_pay' => 'Google Pay', + ]; + $payment_method_title = $express_payment_titles[ $express_payment_type ] ?? false; + + if ( ! $payment_method_title ) { + return; + } + + $suffix = apply_filters( 'wcpay_payment_request_payment_method_title_suffix', 'WooPayments' ); + if ( ! empty( $suffix ) ) { + $suffix = " ($suffix)"; + } + + $order = wc_get_order( $order_id ); + $order->set_payment_method_title( $payment_method_title . $suffix ); + $order->save(); + } + /** * Calculate and set shipping method. * From 6f434a4f706f2c9ff30c52e300d8b8f50b50d87e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 12 Jul 2024 19:18:24 -0500 Subject: [PATCH 3/8] Disable ECE for non shipping products if Tax is calculated on billing address (#9089) --- ...-non-shipping-products-tax-billing-address | 4 +++ client/express-checkout/utils/normalize.js | 1 + .../express-checkout/utils/test/normalize.js | 2 ++ ...ayments-express-checkout-button-helper.php | 32 +++++++++++++++++++ .../class-fraud-prevention-service.php | 13 ++++++-- 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 changelog/as-disable-ece-non-shipping-products-tax-billing-address diff --git a/changelog/as-disable-ece-non-shipping-products-tax-billing-address b/changelog/as-disable-ece-non-shipping-products-tax-billing-address new file mode 100644 index 00000000000..846d41bc0df --- /dev/null +++ b/changelog/as-disable-ece-non-shipping-products-tax-billing-address @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Disable ECE for non shipping products if Tax is calculated on billing address. diff --git a/client/express-checkout/utils/normalize.js b/client/express-checkout/utils/normalize.js index e07cc89e450..a5ab114fffc 100644 --- a/client/express-checkout/utils/normalize.js +++ b/client/express-checkout/utils/normalize.js @@ -87,6 +87,7 @@ export const normalizePayForOrderData = ( event, paymentMethodId ) => { payment_method: 'woocommerce_payments', 'wcpay-payment-method': paymentMethodId, express_payment_type: event?.expressPaymentType, + 'wcpay-fraud-prevention-token': window.wcpayFraudPreventionToken ?? '', }; }; diff --git a/client/express-checkout/utils/test/normalize.js b/client/express-checkout/utils/test/normalize.js index 96dca4c5b49..aaa15dee135 100644 --- a/client/express-checkout/utils/test/normalize.js +++ b/client/express-checkout/utils/test/normalize.js @@ -302,6 +302,7 @@ describe( 'Express checkout normalization', () => { expect( normalizePayForOrderData( event, 'pm_123456' ) ).toEqual( { payment_method: 'woocommerce_payments', 'wcpay-payment-method': 'pm_123456', + 'wcpay-fraud-prevention-token': 'token123', express_payment_type: 'express', } ); } ); @@ -315,6 +316,7 @@ describe( 'Express checkout normalization', () => { ).toEqual( { payment_method: 'woocommerce_payments', 'wcpay-payment-method': '', + 'wcpay-fraud-prevention-token': 'token123', express_payment_type: undefined, } ); } ); diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php index 5d52fa94691..eddb8bf24ef 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php @@ -417,6 +417,23 @@ public function should_show_express_checkout_button() { return true; } + // Non-shipping product and billing is calculated based on shopper billing addres. Excludes Pay for Order page. + if ( + // If the product doesn't needs shipping. + ( + // on the product page. + ( $this->is_product() && ! $this->product_needs_shipping( $this->get_product() ) ) || + + // on the cart or checkout page. + ( ( $this->is_cart() || $this->is_checkout() ) && ! WC()->cart->needs_shipping() ) + ) + + // ...and billing is calculated based on billing address. + && 'billing' === get_option( 'woocommerce_tax_based_on' ) + ) { + return false; + } + // Cart total is 0 or is on product page and product price is 0. // Exclude pay-for-order pages from this check. if ( @@ -431,6 +448,21 @@ public function should_show_express_checkout_button() { return true; } + /** + * Check if the passed product needs to be shipped. + * + * @param WC_Product $product The product to check. + * + * @return bool Returns true if the product requires shipping; otherwise, returns false. + */ + public function product_needs_shipping( WC_Product $product ) { + if ( ! $product ) { + return false; + } + + return wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping(); + } + /** * Checks to make sure product type is supported. * diff --git a/includes/fraud-prevention/class-fraud-prevention-service.php b/includes/fraud-prevention/class-fraud-prevention-service.php index 9bef49875c5..db783718a44 100644 --- a/includes/fraud-prevention/class-fraud-prevention-service.php +++ b/includes/fraud-prevention/class-fraud-prevention-service.php @@ -87,9 +87,9 @@ public static function maybe_append_fraud_prevention_token() { return; } - // Don't add the token if the user isn't on the cart, checkout or product page. + // Don't add the token if the user isn't on the cart, checkout, product or pay for order page. // Checking the product and cart page too because the user can pay quickly via the payment buttons on that page. - if ( ! is_checkout() && ! is_cart() && ! is_product() ) { + if ( ! is_checkout() && ! is_cart() && ! is_product() && ! $instance->is_pay_for_order_page() ) { return; } @@ -103,6 +103,15 @@ public static function maybe_append_fraud_prevention_token() { ); } + /** + * Checks if this is the Pay for Order page. + * + * @return bool + */ + public function is_pay_for_order_page() { + return is_checkout() && isset( $_GET['pay_for_order'] ); // phpcs:ignore WordPress.Security.NonceVerification + } + /** * Sets a instance to be used in request cycle. * Introduced primarily for supporting unit tests. From ed21d55b64bf49fba158ca646da76b0884cdac84 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Mon, 15 Jul 2024 05:22:47 -0300 Subject: [PATCH 4/8] Fix Express Checkout Element button width (#9094) --- changelog/fix-9005-fix-ece-button-width | 4 ++++ client/checkout/express-checkout-buttons.scss | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 changelog/fix-9005-fix-ece-button-width diff --git a/changelog/fix-9005-fix-ece-button-width b/changelog/fix-9005-fix-ece-button-width new file mode 100644 index 00000000000..d6474e6cef0 --- /dev/null +++ b/changelog/fix-9005-fix-ece-button-width @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix Express Checkout Element button width. diff --git a/client/checkout/express-checkout-buttons.scss b/client/checkout/express-checkout-buttons.scss index 9cf1134ed5b..fb2c698c315 100644 --- a/client/checkout/express-checkout-buttons.scss +++ b/client/checkout/express-checkout-buttons.scss @@ -8,6 +8,14 @@ } } +// This fixes width calculation issues inside the iframe for blocks and shortcode pages. +.wcpay-payment-request-wrapper, +.wc-block-components-express-payment__event-buttons { + .StripeElement iframe { + max-width: unset; + } +} + .woocommerce-checkout .wcpay-payment-request-wrapper { margin-bottom: 1.5em; } From 75686b3d36ac5ed90a0d17d903aa2932d5e5d95e Mon Sep 17 00:00:00 2001 From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:27:03 +0300 Subject: [PATCH 5/8] Bump WC tested up to version to 9.1.2 (#9097) Co-authored-by: oaratovskyi --- changelog/dev-bump-wc-version-9-1-2 | 4 ++++ woocommerce-payments.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/dev-bump-wc-version-9-1-2 diff --git a/changelog/dev-bump-wc-version-9-1-2 b/changelog/dev-bump-wc-version-9-1-2 new file mode 100644 index 00000000000..90d68bcb789 --- /dev/null +++ b/changelog/dev-bump-wc-version-9-1-2 @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Bump WC tested up to version to 9.1.2 diff --git a/woocommerce-payments.php b/woocommerce-payments.php index e82179c5e8d..5f0413a259b 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -8,7 +8,7 @@ * Text Domain: woocommerce-payments * Domain Path: /languages * WC requires at least: 7.6 - * WC tested up to: 8.9.3 + * WC tested up to: 9.1.2 * Requires at least: 6.0 * Requires PHP: 7.3 * Version: 7.9.1 From 5036be760ed5e40cb9f90cf4288abd2b1d80cca3 Mon Sep 17 00:00:00 2001 From: Alefe Souza Date: Mon, 15 Jul 2024 15:50:17 -0300 Subject: [PATCH 6/8] Send optional fields data to WooPay (#9077) --- .../fix-send-optional-fields-data-to-woopay | 4 ++ includes/woopay/class-woopay-session.php | 66 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 changelog/fix-send-optional-fields-data-to-woopay diff --git a/changelog/fix-send-optional-fields-data-to-woopay b/changelog/fix-send-optional-fields-data-to-woopay new file mode 100644 index 00000000000..a4acf9957f3 --- /dev/null +++ b/changelog/fix-send-optional-fields-data-to-woopay @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Send optional fields data to WooPay. diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index 06fc2a93a35..02aa58a38ec 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -487,6 +487,7 @@ public static function get_init_session_request( $order_id = null, $key = null, 'return_url' => ! $is_pay_for_order ? wc_get_cart_url() : $order->get_checkout_payment_url(), 'blocks_data' => $blocks_data_extractor->get_data(), 'checkout_schema_namespaces' => $blocks_data_extractor->get_checkout_schema_namespaces(), + 'optional_fields_status' => self::get_option_fields_status(), ], 'user_session' => null, 'preloaded_requests' => ! $is_pay_for_order ? [ @@ -768,4 +769,69 @@ private static function get_formatted_custom_message() { return str_replace( array_keys( $replacement_map ), array_values( $replacement_map ), $custom_message ); } + + /** + * Returns the status of checkout optional/required address fields. + * + * @return array The status of the checkout fields. + */ + private static function get_option_fields_status() { + // Shortcode checkout options. + $company = get_option( 'woocommerce_checkout_company_field', 'optional' ); + $address_2 = get_option( 'woocommerce_checkout_address_2_field', 'optional' ); + $phone = get_option( 'woocommerce_checkout_phone_field', 'required' ); + + // Blocks checkout options. To get the blocks checkout options, we need + // to parse the checkout page content because the options are stored + // in the blocks HTML as a JSON. + $checkout_page_id = get_option( 'woocommerce_checkout_page_id' ); + $checkout_page = get_post( $checkout_page_id ); + + if ( empty( $checkout_page ) ) { + return [ + 'company' => $company, + 'address_2' => $address_2, + 'phone' => $phone, + ]; + } + + $checkout_page_blocks = parse_blocks( $checkout_page->post_content ); + $checkout_block_index = array_search( 'woocommerce/checkout', array_column( $checkout_page_blocks, 'blockName' ), true ); + + // If we can find the index, it means the merchant checkout page is using blocks checkout. + if ( false !== $checkout_block_index && ! empty( $checkout_page_blocks[ $checkout_block_index ]['attrs'] ) ) { + $checkout_block_attrs = $checkout_page_blocks[ $checkout_block_index ]['attrs']; + + $company = 'optional'; + $address_2 = 'optional'; + $phone = 'optional'; + + if ( ! empty( $checkout_block_attrs['requireCompanyField'] ) ) { + $company = 'required'; + } + + if ( ! empty( $checkout_block_attrs['requirePhoneField'] ) ) { + $phone = 'required'; + } + + // showCompanyField is undefined by default. + if ( empty( $checkout_block_attrs['showCompanyField'] ) ) { + $company = 'hidden'; + } + + if ( isset( $checkout_block_attrs['showApartmentField'] ) && false === $checkout_block_attrs['showApartmentField'] ) { + $address_2 = 'hidden'; + } + + if ( isset( $checkout_block_attrs['showPhoneField'] ) && false === $checkout_block_attrs['showPhoneField'] ) { + $phone = 'hidden'; + } + } + + return [ + 'company' => $company, + 'address_2' => $address_2, + 'phone' => $phone, + ]; + } } From d6c8dc4277f1bcd478a2075dd93e25c69c46fcd3 Mon Sep 17 00:00:00 2001 From: Wesley Rosa Date: Tue, 16 Jul 2024 12:22:43 -0300 Subject: [PATCH 7/8] Fix fatal errors when subscription classes are not available (#8823) Co-authored-by: Wesley Rosa Co-authored-by: Diego Curbelo Co-authored-by: Francesco --- changelog/fix-fatal-errors-when-subscriptions-are-unavailable | 4 ++++ .../subscriptions/class-wc-payments-subscription-service.php | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 changelog/fix-fatal-errors-when-subscriptions-are-unavailable diff --git a/changelog/fix-fatal-errors-when-subscriptions-are-unavailable b/changelog/fix-fatal-errors-when-subscriptions-are-unavailable new file mode 100644 index 00000000000..aa457b9b6f0 --- /dev/null +++ b/changelog/fix-fatal-errors-when-subscriptions-are-unavailable @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixing fatal errors when subscription classes are not available diff --git a/includes/subscriptions/class-wc-payments-subscription-service.php b/includes/subscriptions/class-wc-payments-subscription-service.php index 229bef20ae7..b68bd99cbbe 100644 --- a/includes/subscriptions/class-wc-payments-subscription-service.php +++ b/includes/subscriptions/class-wc-payments-subscription-service.php @@ -1080,6 +1080,10 @@ private function validate_subscription_data( $subscription_data ) { * @return bool True if store has active WCPay subscriptions, otherwise false. */ public static function store_has_active_wcpay_subscriptions() { + if ( ! function_exists( 'wcs_get_subscriptions' ) ) { + return false; + } + $active_wcpay_subscriptions = wcs_get_subscriptions( [ 'subscriptions_per_page' => 1, From 71203da3c6294b76d5f8e65263f9ac9fce5c13f0 Mon Sep 17 00:00:00 2001 From: Daniel Guerra <15204776+danielmx-dev@users.noreply.github.com> Date: Tue, 16 Jul 2024 20:21:25 +0300 Subject: [PATCH 8/8] Allow Afterpay to process payments when the state field is optional (UK and NZ customers) (#9079) Co-authored-by: Samir Merchant --- ...empty-state-when-optional-afterpay-gateway | 4 + includes/class-wc-payment-gateway-wcpay.php | 28 +++- tests/WCPAY_UnitTestCase.php | 5 +- .../test-class-wc-payment-gateway-wcpay.php | 126 ++++++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-9046-allow-empty-state-when-optional-afterpay-gateway diff --git a/changelog/fix-9046-allow-empty-state-when-optional-afterpay-gateway b/changelog/fix-9046-allow-empty-state-when-optional-afterpay-gateway new file mode 100644 index 00000000000..34b8d13c9d4 --- /dev/null +++ b/changelog/fix-9046-allow-empty-state-when-optional-afterpay-gateway @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Allow Afterpay gateway to process payments when the state/county is optional for GB and NZ addresses. diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index e1e6a3543c9..2ee0a00d1a1 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -4501,7 +4501,33 @@ private function upe_needs_redirection( $payment_methods ) { * @return void */ private function handle_afterpay_shipping_requirement( WC_Order $order, Create_And_Confirm_Intention $request ): void { - $check_if_usable = function ( array $address ): bool { + $wc_locale_data = WC()->countries->get_country_locale(); + + $check_if_usable = function ( array $address ) use ( $wc_locale_data ): bool { + if ( $address['country'] && isset( $wc_locale_data[ $address['country'] ] ) ) { + $country_locale_data = $wc_locale_data[ $address['country'] ]; + $fields_to_check = [ + 'state' => 'state', + 'city' => 'city', + 'postcode' => 'postal_code', + 'address_1' => 'line1', + ]; + + foreach ( $fields_to_check as $locale_field => $address_field ) { + $is_field_required = ( + ! isset( $country_locale_data[ $locale_field ] ) || + ! isset( $country_locale_data[ $locale_field ]['required'] ) || + $country_locale_data[ $locale_field ]['required'] + ); + + if ( $is_field_required && ! $address[ $address_field ] ) { + return false; + } + } + + return true; + } + return $address['country'] && $address['state'] && $address['city'] && $address['postal_code'] && $address['line1']; }; diff --git a/tests/WCPAY_UnitTestCase.php b/tests/WCPAY_UnitTestCase.php index 4470710b519..d7f38e3b5d0 100644 --- a/tests/WCPAY_UnitTestCase.php +++ b/tests/WCPAY_UnitTestCase.php @@ -114,14 +114,15 @@ public function createMock( string $original_class_name ): MockObject { // phpcs * @param mixed $response The expected response. * @param WC_Payments_API_Client $api_client_mock Specific API client mock if necessary. * @param WC_Payments_Http $http_mock Specific HTTP mock if necessary. + * @param bool $force_request_mock When true, a request will be mocked even if $total_api_calls is 0. * * @return Request|MockObject The mocked request. */ - protected function mock_wcpay_request( string $request_class, int $total_api_calls = 1, $request_class_constructor_id = null, $response = null, $api_client_mock = null, $http_mock = null ) { + protected function mock_wcpay_request( string $request_class, int $total_api_calls = 1, $request_class_constructor_id = null, $response = null, $api_client_mock = null, $http_mock = null, $force_request_mock = false ) { $http_mock = $http_mock ? $http_mock : $this->createMock( WC_Payments_Http::class ); $api_client_mock = $api_client_mock ? $api_client_mock : $this->createMock( WC_Payments_API_Client::class ); - if ( 1 > $total_api_calls ) { + if ( 1 > $total_api_calls && ! $force_request_mock ) { $api_client_mock->expects( $this->never() )->method( 'send_request' ); // No expectation for calls, return here. diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index 6879500a7b2..dbc0e81876b 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -13,6 +13,7 @@ use WCPay\Core\Server\Request\Get_Charge; use WCPay\Core\Server\Request\Get_Intention; use WCPay\Core\Server\Request\Get_Setup_Intention; +use WCPay\Constants\Country_Code; use WCPay\Constants\Order_Status; use WCPay\Constants\Intent_Status; use WCPay\Constants\Payment_Method; @@ -21,6 +22,7 @@ use WCPay\Exceptions\Amount_Too_Small_Exception; use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Fraud_Prevention_Enabled_Exception; +use WCPay\Exceptions\Invalid_Address_Exception; use WCPay\Exceptions\Process_Payment_Exception; use WCPay\Exceptions\Order_ID_Mismatch_Exception; use WCPay\Fraud_Prevention\Fraud_Prevention_Service; @@ -188,6 +190,13 @@ class WC_Payment_Gateway_WCPay_Test extends WCPAY_UnitTestCase { */ private $mock_duplicates_detection_service; + /** + * Backup of WC locale data + * + * @var array + */ + private $locale_backup; + /** * Pre-test setup */ @@ -269,6 +278,8 @@ public function set_up() { ->method( 'get_payment_metadata' ) ->willReturn( [] ); wcpay_get_test_container()->replace( OrderService::class, $mock_order_service ); + + $this->locale_backup = WC()->countries->get_country_locale(); } /** @@ -305,6 +316,7 @@ public function tear_down() { wcpay_get_test_container()->reset_all_replacements(); WC()->session->set( 'wc_notices', [] ); + WC()->countries->locale = $this->locale_backup; } public function test_process_redirect_payment_intent_processing() { @@ -2825,6 +2837,120 @@ public function test_process_payment_for_order_upe_payment_method() { $this->card_gateway->process_payment_for_order( WC()->cart, $pi ); } + /** + * @dataProvider process_payment_for_order_afterpay_clearpay_provider + */ + public function test_process_payment_for_order_afterpay_clearpay( array $address, array $locale_data, ?string $expected_exception ) { + $payment_method = 'woocommerce_payments_afterpay_clearpay'; + $expected_upe_payment_method_for_pi_creation = 'afterpay_clearpay'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 100 ); + $order->set_billing_city( $address['city'] ); + $order->set_billing_state( $address['state'] ); + $order->set_billing_postcode( $address['postcode'] ); + $order->set_billing_country( $address['country'] ); + $order->save(); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'afterpay_clearpay' ); + + if ( $expected_exception ) { + $this->mock_wcpay_request( Create_And_Confirm_Intention::class, 0, null, null, null, null, true ); + $this->expectException( $expected_exception ); + } else { + $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); + $request->expects( $this->once() ) + ->method( 'set_payment_methods' ) + ->with( [ $expected_upe_payment_method_for_pi_creation ] ); + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( WC_Helper_Intention::create_intention( [ 'status' => 'success' ] ) ); + } + + WC()->countries->locale = $locale_data; + + $afterpay_gateway = current( + array_filter( + $this->gateways, + function ( $gateway ) { + return $gateway->get_payment_method()->get_id() === 'afterpay_clearpay'; + } + ) + ); + + $afterpay_gateway->process_payment_for_order( WC()->cart, $pi ); + } + + public function process_payment_for_order_afterpay_clearpay_provider() { + return [ + 'with valid full address' => [ + 'address' => [ + 'city' => 'WooCity', + 'state' => 'NY', + 'postcode' => '12345', + 'country' => Country_Code::UNITED_STATES, + ], + // An empty locale data means all fields should be required. + 'locale_data' => [], + 'expected_exception' => null, + ], + 'with incomplete address' => [ + 'address' => [ + 'city' => 'WooCity', + 'state' => '', + 'postcode' => '12345', + 'country' => Country_Code::UNITED_STATES, + ], + 'locale_data' => [ + // A missing `required` attribute means that the field will be required. + Country_Code::UNITED_STATES => [ 'state' => [ 'label' => 'State' ] ], + ], + 'expected_exception' => Invalid_Address_Exception::class, + ], + 'optional state' => [ + 'address' => [ + 'city' => 'London', + 'state' => '', + 'postcode' => 'HA9 9LY', + 'country' => Country_Code::UNITED_KINGDOM, + ], + 'locale_data' => [ + Country_Code::UNITED_KINGDOM => [ 'state' => [ 'required' => false ] ], + ], + 'expected_exception' => null, + ], + 'optional state and postcode' => [ + 'address' => [ + 'city' => 'London', + 'state' => '', + 'postcode' => '', + 'country' => Country_Code::UNITED_KINGDOM, + ], + 'locale_data' => [ + Country_Code::UNITED_KINGDOM => [ + 'state' => [ 'required' => false ], + 'postcode' => [ 'required' => false ], + ], + ], + 'expected_exception' => null, + ], + 'optional state, invalid address' => [ + 'address' => [ + 'city' => '', + 'state' => 'London', + 'postcode' => 'HA9 9LY', + 'country' => Country_Code::UNITED_KINGDOM, + ], + 'locale_data' => [ + Country_Code::UNITED_KINGDOM => [ 'state' => [ 'required' => false ] ], + ], + 'expected_exception' => Invalid_Address_Exception::class, + ], + ]; + } + public function test_process_payment_caches_mimimum_amount_and_displays_error_upon_exception() { delete_transient( 'wcpay_minimum_amount_usd' );