From 97df78819943d7bab641280b71b2be755e7d3e3f Mon Sep 17 00:00:00 2001 From: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> Date: Thu, 22 Feb 2024 02:35:27 +1100 Subject: [PATCH] Add support for WooCommerce Deposits when using Apple Pay and Google Pay (#7910) Co-authored-by: Guilherme Pressutto --- changelog/7907-support-deposit-express-pay | 4 + .../use-express-checkout-product-handler.js | 5 +- client/payment-request/index.js | 26 +++++ ...ayments-payment-request-button-handler.php | 41 ++++++-- ...ayments-express-checkout-button-helper.php | 2 +- ...lass-wc-helper-deposit-product-manager.php | 95 ++++++++++++++++++- .../test-class-woocommerce-deposits.php | 4 +- ...ayments-payment-request-button-handler.php | 87 +++++++++++++++++ 8 files changed, 246 insertions(+), 18 deletions(-) create mode 100644 changelog/7907-support-deposit-express-pay diff --git a/changelog/7907-support-deposit-express-pay b/changelog/7907-support-deposit-express-pay new file mode 100644 index 00000000000..6851d72dd79 --- /dev/null +++ b/changelog/7907-support-deposit-express-pay @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Added support for WooCommerce Deposits when using Apple Pay and Google Pay diff --git a/client/checkout/woopay/express-button/use-express-checkout-product-handler.js b/client/checkout/woopay/express-button/use-express-checkout-product-handler.js index c588fa8b728..2b8bdb607ea 100644 --- a/client/checkout/woopay/express-button/use-express-checkout-product-handler.js +++ b/client/checkout/woopay/express-button/use-express-checkout-product-handler.js @@ -125,10 +125,7 @@ const useExpressCheckoutProductHandler = ( api ) => { const formData = new FormData( addOnForm ); formData.forEach( ( value, name ) => { - if ( - /^addon-/.test( name ) || - /^wc_gc_giftcard_/.test( name ) - ) { + if ( /^(addon-|wc_)/.test( name ) ) { if ( /\[\]$/.test( name ) ) { const fieldName = name.substring( 0, name.length - 2 ); diff --git a/client/payment-request/index.js b/client/payment-request/index.js index f16aeb5afe1..fbbb303715e 100644 --- a/client/payment-request/index.js +++ b/client/payment-request/index.js @@ -310,6 +310,19 @@ jQuery( ( $ ) => { 0 ); + // WC Deposits Support. + const depositObject = {}; + if ( $( 'input[name=wc_deposit_option]' ).length ) { + depositObject.wc_deposit_option = $( + 'input[name=wc_deposit_option]:checked' + ).val(); + } + if ( $( 'input[name=wc_deposit_payment_plan]' ).length ) { + depositObject.wc_deposit_payment_plan = $( + 'input[name=wc_deposit_payment_plan]:checked' + ).val(); + } + const data = { product_id: productId, qty: $( '.quantity .qty' ).val(), @@ -317,6 +330,7 @@ jQuery( ( $ ) => { ? wcpayPaymentRequest.getAttributes().data : [], addon_value: addonValue, + ...depositObject, }; return api.paymentRequestGetSelectedProductData( data ); @@ -437,6 +451,18 @@ jQuery( ( $ ) => { wcpayPaymentRequest.addToCart(); } ); + // WooCommerce Deposits support. + // Trigger the "woocommerce_variation_has_changed" event when the deposit option is changed. + $( + 'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]' + ).on( 'change', () => { + $( 'form' ) + .has( + 'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]' + ) + .trigger( 'woocommerce_variation_has_changed' ); + } ); + $( document.body ).on( 'woocommerce_variation_has_changed', () => { wcpayPaymentRequest.blockPaymentRequestButton(); diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index d2835eaeb04..81675a3fb44 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -204,11 +204,13 @@ public function get_button_settings() { * Gets the product total price. * * @param object $product WC_Product_* object. + * @param bool $is_deposit Whether customer is paying a deposit. + * @param int $deposit_plan_id The ID of the deposit plan. * @return mixed Total price. * * @throws Invalid_Price_Exception Whenever a product has no price. */ - public function get_product_price( $product ) { + public function get_product_price( $product, ?bool $is_deposit = null, int $deposit_plan_id = 0 ) { // If prices should include tax, using tax inclusive price. if ( $this->express_checkout_helper->cart_prices_include_tax() ) { $base_price = wc_get_price_including_tax( $product ); @@ -216,6 +218,27 @@ public function get_product_price( $product ) { $base_price = wc_get_price_excluding_tax( $product ); } + // If WooCommerce Deposits is active, we need to get the correct price for the product. + if ( class_exists( 'WC_Deposits_Product_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) { + // If is_deposit is null, we use the default deposit type for the product. + if ( is_null( $is_deposit ) ) { + $is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() ); + } + if ( $is_deposit ) { + $deposit_type = WC_Deposits_Product_Manager::get_deposit_type( $product->get_id() ); + $available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() ); + // Default to first (default) plan if no plan is specified. + if ( 'plan' === $deposit_type && 0 === $deposit_plan_id && ! empty( $available_plan_ids ) ) { + $deposit_plan_id = $available_plan_ids[0]; + } + + // Ensure the selected plan is available for the product. + if ( 0 === $deposit_plan_id || in_array( $deposit_plan_id, $available_plan_ids, true ) ) { + $base_price = WC_Deposits_Product_Manager::get_deposit_amount( $product, $deposit_plan_id, 'display', $base_price ); + } + } + } + // Add subscription sign-up fees to product price. $sign_up_fee = 0; $subscription_types = [ @@ -979,12 +1002,14 @@ public function ajax_get_selected_product_data() { check_ajax_referer( 'wcpay-get-selected-product-data', 'security' ); try { - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; - $qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id ); - $addon_value = isset( $_POST['addon_value'] ) ? max( (float) $_POST['addon_value'], 0 ) : 0; - $product = wc_get_product( $product_id ); - $variation_id = null; - $currency = get_woocommerce_currency(); + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; + $qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id ); + $addon_value = isset( $_POST['addon_value'] ) ? max( (float) $_POST['addon_value'], 0 ) : 0; + $product = wc_get_product( $product_id ); + $variation_id = null; + $currency = get_woocommerce_currency(); + $is_deposit = isset( $_POST['wc_deposit_option'] ) ? 'yes' === sanitize_text_field( wp_unslash( $_POST['wc_deposit_option'] ) ) : null; + $deposit_plan_id = isset( $_POST['wc_deposit_payment_plan'] ) ? absint( $_POST['wc_deposit_payment_plan'] ) : 0; if ( ! is_a( $product, 'WC_Product' ) ) { /* translators: product ID */ @@ -1012,7 +1037,7 @@ public function ajax_get_selected_product_data() { throw new Exception( sprintf( __( 'You cannot add that amount of "%1$s"; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce-payments' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) ); } - $price = $this->get_product_price( $product ); + $price = $this->get_product_price( $product, $is_deposit, $deposit_plan_id ); $total = $qty * $price + $addon_value; $quantity_label = 1 < $qty ? ' (x' . $qty . ')' : ''; 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 c5194b5a888..2aff7815095 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 @@ -170,7 +170,7 @@ public function build_display_items( $itemized_display_items = false ) { $currency = get_woocommerce_currency(); // Default show only subtotal instead of itemization. - if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', true ) || $itemized_display_items ) { + if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', ! $itemized_display_items ) ) { foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { $amount = $cart_item['line_subtotal']; $subtotal += $cart_item['line_subtotal']; diff --git a/tests/unit/helpers/class-wc-helper-deposit-product-manager.php b/tests/unit/helpers/class-wc-helper-deposit-product-manager.php index e542442cfe3..eefde552fb7 100644 --- a/tests/unit/helpers/class-wc-helper-deposit-product-manager.php +++ b/tests/unit/helpers/class-wc-helper-deposit-product-manager.php @@ -5,16 +5,105 @@ * @package WooCommerce\Payments\Tests */ +// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound + /** * Class WC_Deposits_Product_Manager. * * This helper class should ONLY be used for unit tests!. */ class WC_Deposits_Product_Manager { - public static function get_deposit_type( WC_Product_Simple $product ) { + /** + * @param WC_Product_Simple|int $product + * @return mixed + */ + public static function get_deposit_type( $product ) { + $product = self::get_product( $product ); return $product->get_meta( '_wc_deposit_type' ); } - public static function deposits_enabled( WC_Product_Simple $product ) { - return true === $product->get_meta( '_wc_deposits_enabled' ); + + /** + * @param WC_Product_Simple|int $product + * @return mixed + */ + public static function get_deposit_selected_type( $product ) { + $product = self::get_product( $product ); + return $product->get_meta( '_wc_deposit_selected_type' ); + } + + /** + * @param WC_Product_Simple|int $product + * @return bool + */ + public static function deposits_enabled( $product ) { + $product = self::get_product( $product ); + $setting = $product->get_meta( '_wc_deposit_enabled' ); + return 'optional' === $setting || 'forced' === $setting; + } + + /** + * @param WC_Product_Simple|int $product + * @return mixed + */ + public static function get_deposit_amount( $product, $plan_id = 0, $context = 'display', $product_price = null ) { + $product = self::get_product( $product ); + $type = self::get_deposit_type( $product ); + $percentage = 'percent' === $type; + $amount = $product->get_meta( '_wc_deposit_amount' ); + + if ( ! $amount ) { + $amount = get_option( 'wc_deposits_default_amount' ); + } + + if ( ! $amount ) { + return false; + } + + if ( $percentage ) { + $product_price = is_null( $product_price ) ? $product->get_price() : $product_price; + $amount = ( $product_price / 100 ) * $amount; + } + + $price = $amount; + return wc_format_decimal( $price ); + } + + /** + * @param $product + * @return mixed|null + */ + public static function get_product( $product ) { + if ( ! is_object( $product ) ) { + $product = apply_filters( 'test_deposit_get_product', wc_get_product( $product ) ); + } + + return $product; + } +} + +/** + * Class WC_Deposits_Plans_Manager. + */ +class WC_Deposits_Plans_Manager { + /** + * Get plan ids assigned to a product. + * + * @param int $product_id Product ID. + * @return int[] + */ + public static function get_plan_ids_for_product( $product_id ) { + $product = WC_Deposits_Product_Manager::get_product( $product_id ); + $map = array_map( 'absint', array_filter( (array) $product->get_meta( '_wc_deposit_payment_plans' ) ) ); + if ( count( $map ) <= 0 ) { + $map = self::get_default_plan_ids(); + } + return $map; + } + + /** + * Get the default plan IDs. + */ + public static function get_default_plan_ids() { + return array_map( 'absint', array_filter( (array) get_option( 'wc_deposits_default_plans', [] ) ) ); } } diff --git a/tests/unit/multi-currency/compatibility/test-class-woocommerce-deposits.php b/tests/unit/multi-currency/compatibility/test-class-woocommerce-deposits.php index e9e3c8a082e..136ede0b717 100644 --- a/tests/unit/multi-currency/compatibility/test-class-woocommerce-deposits.php +++ b/tests/unit/multi-currency/compatibility/test-class-woocommerce-deposits.php @@ -93,7 +93,7 @@ function( $input ) { $amount = 10.00; $product = WC_Helper_Product::create_simple_product(); - $product->add_meta_data( '_wc_deposits_enabled', true ); + $product->add_meta_data( '_wc_deposit_enabled', 'optional' ); $product->add_meta_data( '_wc_deposit_type', 'plan' ); $product->save(); @@ -112,7 +112,7 @@ public function test_maybe_convert_product_prices_for_deposits() { ->willReturn( true ); $product = WC_Helper_Product::create_simple_product(); - $product->add_meta_data( '_wc_deposits_enabled', true ); + $product->add_meta_data( '_wc_deposit_enabled', 'optional' ); $product->add_meta_data( '_wc_deposit_type', 'plan' ); $product->save(); diff --git a/tests/unit/test-class-wc-payments-payment-request-button-handler.php b/tests/unit/test-class-wc-payments-payment-request-button-handler.php index 10c83c22260..e84472950ae 100644 --- a/tests/unit/test-class-wc-payments-payment-request-button-handler.php +++ b/tests/unit/test-class-wc-payments-payment-request-button-handler.php @@ -339,6 +339,75 @@ public function test_get_product_price_returns_simple_price() { ); } + public function test_get_product_price_returns_deposit_amount() { + $product_price = 10; + $this->simple_product->set_price( $product_price ); + + $this->assertEquals( + $product_price, + $this->pr->get_product_price( $this->simple_product, false ), + 'When deposit is disabled, the regular price should be returned.' + ); + $this->assertEquals( + $product_price, + $this->pr->get_product_price( $this->simple_product, true ), + 'When deposit is enabled, but the product has no setting for deposit, the regular price should be returned.' + ); + + $this->simple_product->update_meta_data( '_wc_deposit_enabled', 'optional' ); + $this->simple_product->update_meta_data( '_wc_deposit_type', 'percent' ); + $this->simple_product->update_meta_data( '_wc_deposit_amount', 50 ); + $this->simple_product->save_meta_data(); + + $this->assertEquals( + $product_price, + $this->pr->get_product_price( $this->simple_product, false ), + 'When deposit is disabled, the regular price should be returned.' + ); + $this->assertEquals( + $product_price * 0.5, + $this->pr->get_product_price( $this->simple_product, true ), + 'When deposit is enabled, the deposit price should be returned.' + ); + + $this->simple_product->delete_meta_data( '_wc_deposit_amount' ); + $this->simple_product->delete_meta_data( '_wc_deposit_type' ); + $this->simple_product->delete_meta_data( '_wc_deposit_enabled' ); + $this->simple_product->save_meta_data(); + } + + public function test_get_product_price_returns_deposit_amount_default_values() { + $product_price = 10; + $this->simple_product->set_price( $product_price ); + + $this->assertEquals( + $product_price, + $this->pr->get_product_price( $this->simple_product ), + 'When deposit is disabled by default, the regular price should be returned.' + ); + + $this->simple_product->update_meta_data( '_wc_deposit_enabled', 'optional' ); + $this->simple_product->update_meta_data( '_wc_deposit_type', 'percent' ); + $this->simple_product->update_meta_data( '_wc_deposit_amount', 50 ); + $this->simple_product->update_meta_data( '_wc_deposit_selected_type', 'full' ); + $this->simple_product->save_meta_data(); + + $this->assertEquals( + $product_price, + $this->pr->get_product_price( $this->simple_product ), + 'When deposit is optional and disabled by default, the regular price should be returned.' + ); + + $this->simple_product->update_meta_data( '_wc_deposit_selected_type', 'deposit' ); + $this->simple_product->save_meta_data(); + + $this->assertEquals( + $product_price * 0.5, + $this->pr->get_product_price( $this->simple_product ), + 'When deposit is optional and selected by default, the deposit price should be returned.' + ); + } + /** * @dataProvider provide_get_product_tax_tests */ @@ -440,6 +509,12 @@ public function provide_get_product_tax_tests() { public function test_get_product_price_includes_subscription_sign_up_fee() { $mock_product = $this->create_mock_subscription( 'subscription' ); + add_filter( + 'test_deposit_get_product', + function() use ( $mock_product ) { + return $mock_product; + } + ); // We have a helper because we are not loading subscriptions. WC_Subscriptions_Product::set_sign_up_fee( 10 ); @@ -452,6 +527,12 @@ public function test_get_product_price_includes_subscription_sign_up_fee() { public function test_get_product_price_includes_variable_subscription_sign_up_fee() { $mock_product = $this->create_mock_subscription( 'subscription_variation' ); + add_filter( + 'test_deposit_get_product', + function() use ( $mock_product ) { + return $mock_product; + } + ); // We have a helper because we are not loading subscriptions. WC_Subscriptions_Product::set_sign_up_fee( 10 ); @@ -477,6 +558,12 @@ public function test_get_product_price_throws_exception_for_products_without_pri public function test_get_product_price_throws_exception_for_a_non_numeric_signup_fee() { $mock_product = $this->create_mock_subscription( 'subscription' ); + add_filter( + 'test_deposit_get_product', + function() use ( $mock_product ) { + return $mock_product; + } + ); WC_Subscriptions_Product::set_sign_up_fee( 'a' ); $this->expectException( WCPay\Exceptions\Invalid_Price_Exception::class );