diff --git a/includes/class-wc-cart-totals.php b/includes/class-wc-cart-totals.php index a12fa3ffd45..f0933c69471 100644 --- a/includes/class-wc-cart-totals.php +++ b/includes/class-wc-cart-totals.php @@ -110,11 +110,10 @@ final class WC_Cart_Totals { * * @since 3.2.0 * @param object $cart Cart object to calculate totals for. - * @param object $customer Customer who owns this cart. */ - public function __construct( &$cart = null, &$customer = null ) { + public function __construct( &$cart = null ) { $this->object = $cart; - $this->calculate_tax = wc_tax_enabled() && ! $customer->get_is_vat_exempt(); + $this->calculate_tax = wc_tax_enabled() && ! $cart->get_customer()->get_is_vat_exempt(); if ( is_a( $cart, 'WC_Cart' ) ) { $this->calculate(); @@ -233,7 +232,7 @@ final class WC_Cart_Totals { $fee->total = wc_add_number_precision_deep( $fee->object->amount ); if ( $this->calculate_tax && $fee->object->taxable ) { - $fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->object->tax_class, $this->customer ), false ); + $fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->object->tax_class, $this->object->get_customer() ), false ); $fee->total_tax = array_sum( $fee->taxes ); if ( ! $this->round_at_subtotal() ) { @@ -321,7 +320,7 @@ final class WC_Cart_Totals { */ protected function get_item_tax_rates( $item ) { $tax_class = $item->product->get_tax_class(); - return isset( $this->item_tax_rates[ $tax_class ] ) ? $this->item_tax_rates[ $tax_class ] : $this->item_tax_rates[ $tax_class ] = WC_Tax::get_rates( $item->product->get_tax_class(), $this->customer ); + return isset( $this->item_tax_rates[ $tax_class ] ) ? $this->item_tax_rates[ $tax_class ] : $this->item_tax_rates[ $tax_class ] = WC_Tax::get_rates( $item->product->get_tax_class(), $this->object->get_customer() ); } /** @@ -492,10 +491,10 @@ final class WC_Cart_Totals { protected function calculate_discounts() { $this->set_coupons(); - $discounts = new WC_Discounts( $this->items ); + $discounts = new WC_Discounts( $this->object ); foreach ( $this->coupons as $coupon ) { - $discounts->apply_coupon( $coupon ); + $discounts->apply_discount( $coupon ); } $this->discount_totals = $discounts->get_discounts( true ); @@ -514,6 +513,18 @@ final class WC_Cart_Totals { $applied_coupons = $discounts->get_applied_coupons(); $this->object->coupon_discount_amounts = wp_list_pluck( $applied_coupons, 'discount' ); + + + + + + + + + + + + $this->object->coupon_discount_tax_amounts = wp_list_pluck( $applied_coupons, 'discount_tax' ); } diff --git a/includes/class-wc-cart.php b/includes/class-wc-cart.php index 3db24df3ed9..31bdcd1dd3a 100644 --- a/includes/class-wc-cart.php +++ b/includes/class-wc-cart.php @@ -1098,6 +1098,16 @@ class WC_Cart { return ( $first_item_subtotal < $second_item_subtotal ) ? 1 : -1; } + /** + * Get cart's owner. + * + * @since 3.2.0 + * @return WC_Customer + */ + public function get_customer() { + return WC()->customer; + } + /** * Calculate totals for the items in the cart. */ @@ -1396,7 +1406,7 @@ class WC_Cart { } // VAT exemption done at this point - so all totals are correct before exemption - if ( WC()->customer->get_is_vat_exempt() ) { + if ( $this->get_customer()->get_is_vat_exempt() ) { $this->remove_taxes(); } @@ -1412,7 +1422,7 @@ class WC_Cart { $this->tax_total = WC_Tax::get_tax_total( $this->taxes ); // VAT exemption done at this point - so all totals are correct before exemption - if ( WC()->customer->get_is_vat_exempt() ) { + if ( $this->get_customer()->get_is_vat_exempt() ) { $this->remove_taxes(); } } @@ -1533,12 +1543,12 @@ class WC_Cart { 'ID' => get_current_user_id(), ), 'destination' => array( - 'country' => WC()->customer->get_shipping_country(), - 'state' => WC()->customer->get_shipping_state(), - 'postcode' => WC()->customer->get_shipping_postcode(), - 'city' => WC()->customer->get_shipping_city(), - 'address' => WC()->customer->get_shipping_address(), - 'address_2' => WC()->customer->get_shipping_address_2(), + 'country' => $this->get_customer()->get_shipping_country(), + 'state' => $this->get_customer()->get_shipping_state(), + 'postcode' => $this->get_customer()->get_shipping_postcode(), + 'city' => $this->get_customer()->get_shipping_city(), + 'address' => $this->get_customer()->get_shipping_address(), + 'address_2' => $this->get_customer()->get_shipping_address_2(), ), 'cart_subtotal' => $this->get_displayed_subtotal(), ), @@ -1596,8 +1606,8 @@ class WC_Cart { } if ( 'yes' === get_option( 'woocommerce_shipping_cost_requires_address' ) ) { - if ( ! WC()->customer->has_calculated_shipping() ) { - if ( ! WC()->customer->get_shipping_country() || ( ! WC()->customer->get_shipping_state() && ! WC()->customer->get_shipping_postcode() ) ) { + if ( ! $this->get_customer()->has_calculated_shipping() ) { + if ( ! $this->get_customer()->get_shipping_country() || ( ! $this->get_customer()->get_shipping_state() && ! $this->get_customer()->get_shipping_postcode() ) ) { return false; } } diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php index f1746d1d48b..0fda8c44e2e 100644 --- a/includes/class-wc-coupon.php +++ b/includes/class-wc-coupon.php @@ -753,6 +753,8 @@ class WC_Coupon extends WC_Legacy_Coupon { $this->error_message = $valid->get_error_message(); return false; } + + return $valid; } /** diff --git a/includes/class-wc-discount.php b/includes/class-wc-discount.php index af4b1c6e42e..3db04cd7712 100644 --- a/includes/class-wc-discount.php +++ b/includes/class-wc-discount.php @@ -4,11 +4,9 @@ if ( ! defined( 'ABSPATH' ) ) { } /** - * A single discount. + * A discount. * - * Represents a fixed or percent based discount at cart level. Extended by cart - * and order discounts since they share the same logic for things like tax - * calculation. + * Represents a fixed, percent or coupon based discount calculated by WC_Discounts class. * * @author Automattic * @package WooCommerce/Classes @@ -23,9 +21,11 @@ class WC_Discount extends WC_Data { * @var array */ protected $data = array( + 'coupon_code' => '', + 'discounts' => array(), // Array of discounts. Keys for item ids, values for the discount amount. 'amount' => 0, 'discount' => 0, - 'discount_type' => 'fixed_cart', + 'type' => 'fixed', ); /** @@ -37,6 +37,15 @@ class WC_Discount extends WC_Data { return ( $this->get_discount_type() === $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type ) ) ); } + /** + * Valid discount types. + * + * @return array + */ + protected function get_valid_discount_types() { + return array( 'fixed', 'percent', 'coupon' ); + } + /** * Prefix for action and filter hooks on data. * @@ -95,8 +104,8 @@ class WC_Discount extends WC_Data { * @param string $context * @return string */ - public function get_discount_type( $context = 'view' ) { - return $this->get_prop( 'discount_type', $context ); + public function get_type( $context = 'view' ) { + return $this->get_prop( 'type', $context ); } /** @@ -105,11 +114,11 @@ class WC_Discount extends WC_Data { * @param string $discount_type * @throws WC_Data_Exception */ - public function set_discount_type( $discount_type ) { - if ( ! in_array( $discount_type, array( 'percent', 'fixed_cart' ) ) ) { - $this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) ); + public function set_type( $discount_type ) { + if ( ! in_array( $discount_type, $this->get_valid_discount_types() ) ) { + $this->error( 'invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) ); } - $this->set_prop( 'discount_type', $discount_type ); + $this->set_prop( 'type', $discount_type ); } /** @@ -130,11 +139,6 @@ class WC_Discount extends WC_Data { return $this->get_prop( 'discount', $context ); } - /** - * Array of negative taxes. @todo should this be here? - */ - public function set_taxes() {} - /** * Calculates the amount of negative tax to apply for this discount, since * discounts are applied before tax. diff --git a/includes/class-wc-discounts.php b/includes/class-wc-discounts.php index eccf714061a..ee3fba3ca8e 100644 --- a/includes/class-wc-discounts.php +++ b/includes/class-wc-discounts.php @@ -27,7 +27,7 @@ class WC_Discounts { /** * An array of discounts which have been applied to items. * - * @var array + * @var WC_Discount[] */ protected $discounts = array(); @@ -48,10 +48,40 @@ class WC_Discounts { /** * Constructor. * - * @param array $items Items to discount. + * @param array $object Cart or order object. */ - public function __construct( $items = array() ) { - $this->set_items( $items ); + public function __construct( $object = array() ) { + if ( is_a( $object, 'WC_Cart' ) ) { + $this->set_items_from_cart( $object ); + } else { + // @todo accept order objects. + } + } + + /** + * Normalise cart/order items which will be discounted. + * + * @since 3.2.0 + * @param array $cart Cart object. + */ + public function set_items_from_cart( $cart ) { + $this->items = array(); + + foreach ( $cart->get_cart() as $key => $cart_item ) { + $item = new stdClass(); + $item->key = $key; + $item->object = $cart_item; + $item->product = $cart_item['data']; + $item->quantity = $cart_item['quantity']; + $item->price = wc_add_number_precision_deep( $item->product->get_price() ) * $item->quantity; + $item->tax_class = $item->product->get_tax_class(); + $item->tax_rates = WC_Tax::get_rates( $item->tax_class, $cart->get_customer() ); + $this->items[ $key ] = $item; + } + + uasort( $this->items, array( $this, 'sort_by_price' ) ); + + $this->discounts = array_merge( array_fill_keys( array_keys( $this->items ), 0 ), $this->discounts ); } /** @@ -84,7 +114,7 @@ class WC_Discounts { */ public function get_discounts( $in_cents = false ) { - $discounts = $in_cents ? $this->discounts : wc_remove_number_precision_deep ( $this->discounts ); + $discounts = $in_cents ? $this->discounts : wc_remove_number_precision_deep( $this->discounts ); $manual = array(); foreach ( $this->manual_discounts as $manual_discount ) { @@ -132,35 +162,45 @@ class WC_Discounts { } /** - * Set cart/order items which will be discounted. + * Apply a discount to all items. * - * @since 3.2.0 - * @param array $items List items. - */ - public function set_items( $items ) { - $this->items = array(); - $this->discounts = array(); - $this->applied_coupons = array(); - - if ( ! empty( $items ) && is_array( $items ) ) { - foreach ( $items as $key => $item ) { - $this->items[ $key ] = $item; - $this->items[ $key ]->key = $key; - $this->items[ $key ]->price = $item->subtotal; - } - $this->discounts = array_fill_keys( array_keys( $items ), 0 ); - } - - uasort( $this->items, array( $this, 'sort_by_price' ) ); - } - - /** - * Allows a discount to be applied to the items programmatically without a coupon. - * - * @param string $raw_discount Discount amount either fixed or percentage. - * @return int discounted amount in cents. + * @param string|object $raw_discount Accepts a string (fixed or percent discounts), WC_Discount object, or WC_Coupon object. + * @return bool|WP_Error True if applied or WP_Error instance in failure. */ public function apply_discount( $raw_discount ) { + if ( is_a( $raw_discount, 'WC_Coupon' ) ) { + return $this->apply_coupon( $raw_discount ); + } + + $discount = false; + + if ( is_a( $raw_discount, 'WC_Discount' ) ) { + $discount = $raw_discount; + } elseif ( strstr( $raw_discount, '%' ) ) { + $discount = new WC_Discount; + $discount->set_type( 'percent' ); + $discount->set_amount( trim( $raw_discount, '%' ) ); + } elseif ( 0 < absint( $raw_discount ) ) { + $discount = new WC_Discount; + $discount->set_type( 'fixed' ); + $discount->set_amount( absint( $raw_discount ) ); + } + + if ( ! $discount ) { + return new WP_Error( 'invalid_coupon', __( 'Invalid discount', 'woocommerce' ) ); + } + + + + + + + + + + + + // Get total item cost after any extra discounts. $total_to_discount = 0; @@ -214,11 +254,7 @@ class WC_Discounts { * @param WC_Coupon $coupon Coupon object being applied to the items. * @return bool|WP_Error True if applied or WP_Error instance in failure. */ - public function apply_coupon( $coupon ) { - if ( ! is_a( $coupon, 'WC_Coupon' ) ) { - return false; - } - + protected function apply_coupon( $coupon ) { $is_coupon_valid = $this->is_coupon_valid( $coupon ); if ( is_wp_error( $is_coupon_valid ) ) { diff --git a/tests/unit-tests/discounts/discounts.php b/tests/unit-tests/discounts/discounts.php index 95ffee1cdac..e8e4b2a7389 100644 --- a/tests/unit-tests/discounts/discounts.php +++ b/tests/unit-tests/discounts/discounts.php @@ -6,38 +6,10 @@ */ class WC_Tests_Discounts extends WC_Unit_Test_Case { - protected function get_items_for_discounts_class() { - $items = array(); - $precision = pow( 10, wc_get_price_decimals() ); - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - $item = (object) array( - 'key' => '', - 'quantity' => 0, - 'price' => 0, - 'product' => false, - 'price_includes_tax' => wc_prices_include_tax(), - 'subtotal' => 0, - 'subtotal_tax' => 0, - 'subtotal_taxes' => array(), - 'total' => 0, - 'total_tax' => 0, - 'taxes' => array(), - 'discounted_price' => 0, - ); - $item->object = $cart_item; - $item->quantity = $cart_item['quantity']; - $item->subtotal = $cart_item['data']->get_price() * $precision * $cart_item['quantity']; - $item->product = $cart_item['data']; - $item->tax_rates = WC_Tax::get_rates( $item->product->get_tax_class() ); - $items[ $cart_item_key ] = $item; - } - return $items; - } - /** * Test get and set items. */ - public function test_get_set_items() { + public function test_get_set_items_from_cart() { // Create dummy product - price will be 10 $product = WC_Helper_Product::create_simple_product(); @@ -52,22 +24,22 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { // Test setting items to the cart. $discounts = new WC_Discounts(); - $discounts->set_items( $this->get_items_for_discounts_class() ); + $discounts->set_items_from_cart( WC()->cart ); $this->assertEquals( 1, count( $discounts->get_items() ) ); // Test setting items to an order. $discounts = new WC_Discounts(); - $discounts->set_items( $this->get_items_for_discounts_class() ); + $discounts->set_items_from_cart( WC()->cart ); $this->assertEquals( 1, count( $discounts->get_items() ) ); // Empty array of items. $discounts = new WC_Discounts(); - $discounts->set_items( array() ); + $discounts->set_items_from_cart( array() ); $this->assertEquals( array(), $discounts->get_items() ); // Invalid items. $discounts = new WC_Discounts(); - $discounts->set_items( false ); + $discounts->set_items_from_cart( false ); $this->assertEquals( array(), $discounts->get_items() ); // Cleanup. @@ -83,14 +55,14 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { $discounts = new WC_Discounts(); $product = WC_Helper_Product::create_simple_product(); WC()->cart->add_to_cart( $product->get_id(), 1 ); - $discounts->set_items( $this->get_items_for_discounts_class() ); + $discounts->set_items_from_cart( WC()->cart ); // Test applying multiple coupons and getting totals. $coupon = WC_Helper_Coupon::create_coupon( 'test' ); $coupon->set_amount( 50 ); $coupon->set_discount_type( 'percent' ); $coupon->save(); - $discounts->apply_coupon( $coupon ); + $discounts->apply_discount( $coupon ); $this->assertEquals( array( 'test' => array( 'discount' => 5, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); @@ -99,11 +71,11 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { $coupon2->set_amount( 50 ); $coupon2->set_discount_type( 'percent' ); $coupon->save(); - $discounts->apply_coupon( $coupon2 ); + $discounts->apply_discount( $coupon2 ); $this->assertEquals( array( 'test' => array( 'discount' => 5, 'discount_tax' => 0 ), 'test2' => array( 'discount' => 2.50, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); - $discounts->apply_coupon( $coupon ); + $discounts->apply_discount( $coupon ); $this->assertEquals( array( 'test' => array( 'discount' => 6.25, 'discount_tax' => 0 ), 'test2' => array( 'discount' => 2.50, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); // Test different coupon types. @@ -112,14 +84,14 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { $coupon->set_discount_type( 'fixed_product' ); $coupon->set_amount( 2 ); $coupon->save(); - $discounts->set_items( $this->get_items_for_discounts_class() ); - $discounts->apply_coupon( $coupon ); + $discounts->set_items_from_cart( WC()->cart ); + $discounts->apply_discount( $coupon ); $this->assertEquals( array( 'test' => array( 'discount' => 4, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); $coupon->set_discount_type( 'fixed_cart' ); $coupon->save(); - $discounts->set_items( $this->get_items_for_discounts_class() ); - $discounts->apply_coupon( $coupon ); + $discounts->set_items_from_cart( WC()->cart ); + $discounts->apply_discount( $coupon ); $this->assertEquals( array( 'test' => array( 'discount' => 2, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); // Cleanup. @@ -146,20 +118,20 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { // Apply a percent discount. $coupon->set_discount_type( 'percent' ); - $discounts->set_items( $this->get_items_for_discounts_class() ); - $discounts->apply_coupon( $coupon ); + $discounts->set_items_from_cart( WC()->cart ); + $discounts->apply_discount( $coupon ); $this->assertEquals( 9, $discounts->get_discounted_price( current( $discounts->get_items() ) ) ); // Apply a fixed cart coupon. $coupon->set_discount_type( 'fixed_cart' ); - $discounts->set_items( $this->get_items_for_discounts_class() ); - $discounts->apply_coupon( $coupon ); + $discounts->set_items_from_cart( WC()->cart ); + $discounts->apply_discount( $coupon ); $this->assertEquals( 0, $discounts->get_discounted_price( current( $discounts->get_items() ) ) ); // Apply a fixed product coupon. $coupon->set_discount_type( 'fixed_product' ); - $discounts->set_items( $this->get_items_for_discounts_class() ); - $discounts->apply_coupon( $coupon ); + $discounts->set_items_from_cart( WC()->cart ); + $discounts->apply_discount( $coupon ); $this->assertEquals( 0, $discounts->get_discounted_price( current( $discounts->get_items() ) ) ); // Cleanup. @@ -431,11 +403,11 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { $products[] = $product; } - $discounts->set_items( $this->get_items_for_discounts_class() ); + $discounts->set_items_from_cart( WC()->cart ); foreach ( $test['coupons'] as $coupon_props ) { $coupon->set_props( $coupon_props ); - $discounts->apply_coupon( $coupon ); + $discounts->apply_discount( $coupon ); } $this->assertEquals( $test['expected_total_discount'], array_sum( $discounts->get_discounts() ), 'Test case ' . $test_index . ' failed (' . print_r( $test, true ) . ' - ' . print_r( $discounts->get_discounts(), true ) . ')' ); @@ -497,7 +469,7 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { WC()->cart->add_to_cart( $product2->get_id(), 1 ); $discounts = new WC_Discounts(); - $discounts->set_items( $this->get_items_for_discounts_class() ); + $discounts->set_items_from_cart( WC()->cart ); $discounts->apply_discount( '50%' ); $all_discounts = $discounts->get_discounts(); @@ -510,7 +482,7 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { // Test fixed discounts. $discounts = new WC_Discounts(); - $discounts->set_items( $this->get_items_for_discounts_class() ); + $discounts->set_items_from_cart( WC()->cart ); $discounts->apply_discount( '5' ); $all_discounts = $discounts->get_discounts();