diff --git a/includes/abstracts/abstract-wc-order.php b/includes/abstracts/abstract-wc-order.php index e07153da6e7..31942a57d35 100644 --- a/includes/abstracts/abstract-wc-order.php +++ b/includes/abstracts/abstract-wc-order.php @@ -10,6 +10,8 @@ * @package WooCommerce\Classes */ +use Automattic\WooCommerce\Proxies\LegacyProxy; +use Automattic\WooCommerce\Utilities\ArrayUtil; use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; @@ -1361,14 +1363,23 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { */ public function add_product( $product, $qty = 1, $args = array() ) { if ( $product ) { + $order = ArrayUtil::get_value_or_default( $args, 'order' ); + $total = wc_get_price_excluding_tax( + $product, + array( + 'qty' => $qty, + 'order' => $order, + ) + ); + $default_args = array( 'name' => $product->get_name(), 'tax_class' => $product->get_tax_class(), 'product_id' => $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(), 'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0, 'variation' => $product->is_type( 'variation' ) ? $product->get_attributes() : array(), - 'subtotal' => wc_get_price_excluding_tax( $product, array( 'qty' => $qty ) ), - 'total' => wc_get_price_excluding_tax( $product, array( 'qty' => $qty ) ), + 'subtotal' => $total, + 'total' => $total, 'quantity' => $qty, ); } else { @@ -1392,7 +1403,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { } } - $item = new WC_Order_Item_Product(); + $item = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Order_Item_Product::class ); $item->set_props( $args ); $item->set_backorder_meta(); $item->set_order_id( $this->get_id() ); diff --git a/includes/class-wc-ajax.php b/includes/class-wc-ajax.php index 23ad973b9f2..e4b124301f6 100644 --- a/includes/class-wc-ajax.php +++ b/includes/class-wc-ajax.php @@ -955,7 +955,7 @@ class WC_AJAX { /* translators: %s: error message */ throw new Exception( sprintf( __( 'Error: %s', 'woocommerce' ), $validation_error->get_error_message() ) ); } - $item_id = $order->add_product( $product, $qty ); + $item_id = $order->add_product( $product, $qty, array( 'order' => $order ) ); $item = apply_filters( 'woocommerce_ajax_order_item', $order->get_item( $item_id ), $item_id, $order, $product ); $added_items[ $item_id ] = $item; $order_notes[ $item_id ] = $product->get_formatted_name(); diff --git a/includes/wc-product-functions.php b/includes/wc-product-functions.php index e09b503d582..902dceede73 100644 --- a/includes/wc-product-functions.php +++ b/includes/wc-product-functions.php @@ -9,6 +9,8 @@ */ use Automattic\Jetpack\Constants; +use Automattic\WooCommerce\Proxies\LegacyProxy; +use Automattic\WooCommerce\Utilities\ArrayUtil; use Automattic\WooCommerce\Utilities\NumberUtil; defined( 'ABSPATH' ) || exit; @@ -1081,7 +1083,9 @@ function wc_get_price_excluding_tax( $product, $args = array() ) { $line_price = $price * $qty; if ( $product->is_taxable() && wc_prices_include_tax() ) { - $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); + $order = ArrayUtil::get_value_or_default( $args, 'order' ); + $customer = $order ? wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Customer::class, $order->get_customer_id() ) : null; + $tax_rates = WC_Tax::get_rates( $product->get_tax_class(), $customer ); $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); $return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price. diff --git a/tests/legacy/mockable-functions.php b/tests/legacy/mockable-functions.php index 3fbdeab5672..974619882e0 100644 --- a/tests/legacy/mockable-functions.php +++ b/tests/legacy/mockable-functions.php @@ -12,6 +12,8 @@ return array( 'get_bloginfo', 'get_woocommerce_currencies', 'get_woocommerce_currency_symbol', + 'wc_get_price_excluding_tax', 'wc_get_shipping_method_count', + 'wc_prices_include_tax', 'wc_site_is_https', ); diff --git a/tests/php/includes/abstracts/class-wc-abstract-order-test.php b/tests/php/includes/abstracts/class-wc-abstract-order-test.php index a26f5cc565c..be35d13756a 100644 --- a/tests/php/includes/abstracts/class-wc-abstract-order-test.php +++ b/tests/php/includes/abstracts/class-wc-abstract-order-test.php @@ -5,6 +5,8 @@ * @package WooCommerce\Tests\Abstracts */ +use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\FunctionsMockerHack; + /** * Class WC_Abstract_Order. */ @@ -100,12 +102,12 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case { update_option( 'woocommerce_default_country', 'IN:AP' ); $tax_rate = array( - 'tax_rate_country' => 'IN', - 'tax_rate_state' => '', - 'tax_rate' => '25.0000', - 'tax_rate_name' => 'tax', - 'tax_rate_order' => '1', - 'tax_rate_class' => '', + 'tax_rate_country' => 'IN', + 'tax_rate_state' => '', + 'tax_rate' => '25.0000', + 'tax_rate_name' => 'tax', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', ); WC_Tax::_insert_tax_rate( $tax_rate ); @@ -137,4 +139,49 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case { $this->assertEquals( 2, $order->get_discount_tax() ); } + /** + * @testdox 'add_product' passes the order supplied in '$args' to 'wc_get_price_excluding_tax', and uses the obtained price as total and subtotal for the line item. + */ + public function test_add_product_passes_order_to_wc_get_price_excluding_tax() { + $product_passed_to_get_price = false; + $args_passed_to_get_price = false; + + FunctionsMockerHack::add_function_mocks( + array( + 'wc_get_price_excluding_tax' => function( $product, $args = array() ) use ( &$product_passed_to_get_price, &$args_passed_to_get_price ) { + $product_passed_to_get_price = $product; + $args_passed_to_get_price = $args; + + return 1234; + }, + ) + ); + + //phpcs:disable Squiz.Commenting + $order_item = new class() extends WC_Order_Item_Product { + public $passed_props; + + public function set_props( $args, $context = 'set' ) { + $this->passed_props = $args; + } + }; + //phpcs:enable Squiz.Commenting + + $this->register_legacy_proxy_class_mocks( + array( 'WC_Order_Item_Product' => $order_item ) + ); + + $product = WC_Helper_Product::create_simple_product(); + $product->set_regular_price( 100 ); + $product->save(); + + $order = wc_create_order(); + + $order->add_product( $product, 1, array( 'order' => $order ) ); + + $this->assertSame( $product, $product_passed_to_get_price ); + $this->assertSame( $order, $args_passed_to_get_price['order'] ); + $this->assertEquals( 1234, $order_item->passed_props['total'] ); + $this->assertEquals( 1234, $order_item->passed_props['subtotal'] ); + } } diff --git a/tests/php/includes/wc-product-functions-test.php b/tests/php/includes/wc-product-functions-test.php new file mode 100644 index 00000000000..26fedee2c6a --- /dev/null +++ b/tests/php/includes/wc-product-functions-test.php @@ -0,0 +1,97 @@ + '__return_true', + ) + ); + + StaticMockerHack::add_method_mocks( + array( + 'WC_Tax' => + array( + 'get_rates' => function( $tax_class, $customer ) use ( &$customer_passed_to_get_rates ) { + $customer_passed_to_get_rates = $customer; + }, + 'get_base_tax_rates' => function( $tax_class ) { + return 0; + }, + 'calc_tax' => function( $price, $rates, $price_includes_tax = false, $deprecated = false ) { + return array( 0 ); + }, + ), + ) + ); + + // phpcs:disable Squiz.Commenting.FunctionComment.Missing + + $product = new class() extends WC_Product { + public function get_price( $context = 'view' ) { + return 0; + } + + public function is_taxable() { + return true; + } + + public function get_tax_class( $context = 'view' ) { + return ''; + } + }; + + $customer = new stdClass(); + $this->register_legacy_proxy_class_mocks( + array( + 'WC_Customer' => function( $customer_id ) use ( &$customer_id_passed_to_wc_customer_constructor, $customer ) { + $customer_id_passed_to_wc_customer_constructor = $customer_id; + return $customer; + }, + ) + ); + + if ( $pass_order ) { + $order = new class() { + public function get_customer_id() { + return 1; + } + }; + + wc_get_price_excluding_tax( $product, array( 'order' => $order ) ); + + $this->assertEquals( $order->get_customer_id(), $customer_id_passed_to_wc_customer_constructor ); + $this->assertSame( $customer, $customer_passed_to_get_rates ); + } else { + wc_get_price_excluding_tax( $product ); + + $this->assertFalse( $customer_id_passed_to_wc_customer_constructor ); + $this->assertNull( $customer_passed_to_get_rates ); + } + + // phpcs:enable Squiz.Commenting.FunctionComment.Missing + } +}