From ded2be8d28e04ed76e3399151f212add7b273b8e Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Thu, 27 Jul 2017 15:31:10 +0100 Subject: [PATCH] Legacy filters --- includes/class-wc-cart-totals.php | 13 ++ includes/class-wc-coupon.php | 24 +-- includes/class-wc-discounts.php | 226 +++++++++++++---------- tests/unit-tests/discounts/discounts.php | 23 ++- 4 files changed, 154 insertions(+), 132 deletions(-) diff --git a/includes/class-wc-cart-totals.php b/includes/class-wc-cart-totals.php index 5f6a3dec62a..8e8415c4e93 100644 --- a/includes/class-wc-cart-totals.php +++ b/includes/class-wc-cart-totals.php @@ -409,6 +409,19 @@ final class WC_Cart_Totals { $item->total = $this->get_discounted_price_in_cents( $item_key ); $item->total_tax = 0; + if ( has_filter( 'woocommerce_get_discounted_price' ) ) { + /** + * Allow plugins to filter this price like in the legacy cart class. + * + * This is legacy and should probably be deprecated in the future. + * $item->object is the cart item object. + * $this->object is the cart object. + */ + $item->total = wc_add_number_precision( + apply_filters( 'woocommerce_get_discounted_price', wc_remove_number_precision( $item->total ), $item->object, $this->object ) + ); + } + if ( $this->calculate_tax && $item->product->is_taxable() ) { $item->taxes = WC_Tax::calc_tax( $item->total, $item->tax_rates, $item->price_includes_tax ); $item->total_tax = array_sum( $item->taxes ); diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php index 16cbb28ec0a..c1192336e75 100644 --- a/includes/class-wc-coupon.php +++ b/includes/class-wc-coupon.php @@ -390,29 +390,7 @@ class WC_Coupon extends WC_Legacy_Coupon { $discount = $single ? $discount : $discount * $cart_item_qty; } - $discount = (float) min( $discount, $discounting_amount ); - - // Handle the limit_usage_to_x_items option - if ( ! $this->is_type( array( 'fixed_cart' ) ) ) { - if ( $discounting_amount ) { - if ( null === $this->get_limit_usage_to_x_items() ) { - $limit_usage_qty = $cart_item_qty; - } else { - $limit_usage_qty = min( $this->get_limit_usage_to_x_items(), $cart_item_qty ); - - $this->set_limit_usage_to_x_items( max( 0, ( $this->get_limit_usage_to_x_items() - $limit_usage_qty ) ) ); - } - if ( $single ) { - $discount = ( $discount * $limit_usage_qty ) / $cart_item_qty; - } else { - $discount = ( $discount / $cart_item_qty ) * $limit_usage_qty; - } - } - } - - $discount = round( $discount, wc_get_rounding_precision() ); - - return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this ); + return apply_filters( 'woocommerce_coupon_get_discount_amount', round( min( $discount, $discounting_amount ), wc_get_rounding_precision() ), $discounting_amount, $cart_item, $single, $this ); } /* diff --git a/includes/class-wc-discounts.php b/includes/class-wc-discounts.php index 567b77bdf62..8832bb8d5f4 100644 --- a/includes/class-wc-discounts.php +++ b/includes/class-wc-discounts.php @@ -14,8 +14,6 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Discounts class. - * - * @todo Manual discounts. */ class WC_Discounts { @@ -40,20 +38,12 @@ class WC_Discounts { */ protected $applied_coupons = array(); - /** - * Precision so we can work in cents. - * - * @var int - */ - protected $precision = 1; - /** * Constructor. * * @param array $items Items to discount. */ public function __construct( $items = array() ) { - $this->precision = pow( 10, wc_get_price_decimals() ); $this->set_items( $items ); } @@ -157,6 +147,12 @@ class WC_Discounts { return false; } + $is_coupon_valid = $this->is_coupon_valid( $coupon ); + + if ( is_wp_error( $is_coupon_valid ) ) { + return $is_coupon_valid; + } + if ( ! isset( $this->applied_coupons[ $coupon->get_code() ] ) ) { $this->applied_coupons[ $coupon->get_code() ] = array( 'discount' => 0, @@ -164,30 +160,39 @@ class WC_Discounts { ); } - $is_coupon_valid = $this->is_coupon_valid( $coupon ); - if ( is_wp_error( $is_coupon_valid ) ) { - return $is_coupon_valid; - } - - // @todo how can we support the old woocommerce_coupon_get_discount_amount filter? $items_to_apply = $this->get_items_to_apply_coupon( $coupon ); - $result = false; + $coupon_type = $coupon->get_discount_type(); + // Core discounts are handled here as of 3.2. switch ( $coupon->get_discount_type() ) { case 'percent' : - $result = $this->apply_percentage_discount( $items_to_apply, $coupon->get_amount() ); + $this->apply_percentage_discount( $items_to_apply, $coupon->get_amount(), $coupon ); break; case 'fixed_product' : - $result = $this->apply_fixed_product_discount( $items_to_apply, $coupon->get_amount() * $this->precision ); + $this->apply_fixed_product_discount( $items_to_apply, wc_add_number_precision( $coupon->get_amount() ), $coupon ); break; case 'fixed_cart' : - $result = $this->apply_fixed_cart_discount( $items_to_apply, $coupon->get_amount() * $this->precision ); + $this->apply_fixed_cart_discount( $items_to_apply, wc_add_number_precision( $coupon->get_amount() ), $coupon ); break; - } + default : + if ( has_action( 'woocommerce_discounts_apply_coupon_' . $coupon_type ) ) { + // Allow custom coupon types to control this in their class per item, unless the new action is used. + do_action( 'woocommerce_discounts_apply_coupon_' . $coupon_type, $coupon, $items_to_apply, $this ); + } else { + // Fallback to old coupon-logic. + foreach ( $items_to_apply as $item ) { + $discounted_price = $this->get_discounted_price_in_cents( $item ); + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price : $discounted_price; + $discount = min( $discounted_price, $coupon->get_discount_amount( $price_to_discount, $item->object ) ); + $discount_tax = $this->get_item_discount_tax( $item, $discount ); - if ( $result ) { - $this->applied_coupons[ $coupon->get_code() ]['discount'] += $result['discount']; - $this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $result['discount_tax']; + // Store totals. + $this->discounts[ $item->key ] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax; + } + } + break; } } @@ -261,41 +266,38 @@ class WC_Discounts { } /** - * Apply a discount amount to an item and ensure it does not go negative. + * Apply percent discount to items and return an array of discounts granted. * * @since 3.2.0 - * @param object $item Get data for this item. - * @param int $discount Amount of discount. - * @return int Amount discounted. + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $amount Amount of discount. + * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. + * @return int Total discounted. */ - protected function add_item_discount( &$item, $discount ) { - $discounted_price = $this->get_discounted_price_in_cents( $item ); - $discount = $discount > $discounted_price ? $discounted_price : $discount; - $this->discounts[ $item->key ] = $this->discounts[ $item->key ] + $discount; - return $discount; - } + protected function apply_percentage_discount( $items_to_apply, $amount, $coupon = null ) { + $total_discount = 0; - /** - * Apply percent discount to items. - * - * @since 3.2.0 - * @param array $items_to_apply Array of items to apply the coupon to. - * @param int $amount Amount of discount. - * @return array totals discounted in cents - */ - protected function apply_percentage_discount( $items_to_apply, $amount ) { - $return = array( - 'discount' => 0, - 'discount_tax' => 0, - ); + var_dump($items_to_apply); foreach ( $items_to_apply as $item ) { - $discounted_amount = $this->add_item_discount( $item, $amount * ( $this->get_discounted_price_in_cents( $item ) / 100 ) ); - $return['discount'] += $discounted_amount; - $return['discount_tax'] += $this->get_item_discount_tax( $item, $discounted_amount ); - } + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); - return $return; + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price: $discounted_price; + + // Run coupon calculations. + $discount = $amount * ( $price_to_discount / 100 ); + $discount = min( $discounted_price, apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $price_to_discount, $item->object, false, $coupon ) ); + $discount_tax = $this->get_item_discount_tax( $item, $discount ); + + // Store totals. + $total_discount += $discount; + $this->discounts[ $item->key ] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax; + } + return $total_discount; } /** @@ -303,22 +305,32 @@ class WC_Discounts { * * @since 3.2.0 * @param array $items_to_apply Array of items to apply the coupon to. - * @param int $discount Amount of discout. - * @return array totals discounted in cents + * @param int $amount Amount of discount. + * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. + * @return int Total discounted. */ - protected function apply_fixed_product_discount( $items_to_apply, $discount ) { - $return = array( - 'discount' => 0, - 'discount_tax' => 0, - ); + protected function apply_fixed_product_discount( $items_to_apply, $amount, $coupon = null ) { + $total_discount = 0; foreach ( $items_to_apply as $item ) { - $discounted_amount = $this->add_item_discount( $item, $discount * $item->quantity ); - $return['discount'] += $discounted_amount; - $return['discount_tax'] += $this->get_item_discount_tax( $item, $discounted_amount ); - } + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); - return $return; + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price: $discounted_price; + + // Run coupon calculations. + $discount = $amount * $item->quantity; + $discount = min( $discounted_price, apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $price_to_discount, $item->object, false, $coupon ) ); + $discount_tax = $this->get_item_discount_tax( $item, $discount ); + + // Store totals. + $total_discount += $discount; + $this->discounts[ $item->key ] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax; + } + return $total_discount; } /** @@ -327,60 +339,74 @@ class WC_Discounts { * @since 3.2.0 * @param array $items_to_apply Array of items to apply the coupon to. * @param int $cart_discount Fixed discount amount to apply. - * @return array totals discounted in cents + * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. + * @return int Total discounted. */ - protected function apply_fixed_cart_discount( $items_to_apply, $cart_discount ) { - $return = array( - 'discount' => 0, - 'discount_tax' => 0, - ); - $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); + protected function apply_fixed_cart_discount( $items_to_apply, $cart_discount, $coupon = null ) { + $total_discount = 0; + $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); if ( ! $item_count = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) ) ) { - return $return; + return $total_discount; } - $per_item_discount = floor( $cart_discount / $item_count ); // round it down to the nearest cent number. + $per_item_discount = floor( $cart_discount / $item_count ); // round it down to the nearest cent. if ( $per_item_discount > 0 ) { - - foreach ( $items_to_apply as $item ) { - $discounted_amount = $this->add_item_discount( $item, $per_item_discount * $item->quantity ); - $return['discount'] += $discounted_amount; - $return['discount_tax'] += $this->get_item_discount_tax( $item, $discounted_amount ); - } + $total_discounted = $this->apply_fixed_product_discount( $items_to_apply, $per_item_discount, $coupon ); /** * If there is still discount remaining, repeat the process. */ - if ( $return['discount'] > 0 && $return['discount'] < $cart_discount ) { - $discounted_amounts = $this->apply_fixed_cart_discount( $items_to_apply, $cart_discount - $return['discount'] ); - $return['discount'] += array_sum( wp_list_pluck( $discounted_amounts, 'discount' ) ); - $return['discount_tax'] += array_sum( wp_list_pluck( $discounted_amounts, 'discount_tax' ) ); + if ( $total_discounted > 0 && $total_discounted < $cart_discount ) { + $total_discounted = $total_discounted + $this->apply_fixed_cart_discount( $items_to_apply, $cart_discount - $total_discounted ); } } elseif ( $cart_discount > 0 ) { - /** - * Deal with remaining fractional discounts by splitting it over items - * until the amount is expired, discounting 1 cent at a time. - */ - foreach ( $items_to_apply as $item ) { - for ( $i = 0; $i < $item->quantity; $i ++ ) { - $discounted_amount = $this->add_item_discount( $item, 1 ); - $return['discount'] += $discounted_amount; - $return['discount_tax'] += $this->get_item_discount_tax( $item, $discounted_amount ); + $total_discounted = $this->apply_fixed_cart_discount_remainder( $items_to_apply, $cart_discount ); + } + return $total_discount; + } - if ( $return['discount'] >= $cart_discount ) { - break 2; - } - } - if ( $return['discount'] >= $cart_discount ) { - break; + /** + * Deal with remaining fractional discounts by splitting it over items + * until the amount is expired, discounting 1 cent at a time. + * + * @since 3.2.0 + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $cart_discount Fixed discount amount to apply. + * @return int Total discounted. + */ + protected function apply_fixed_cart_discount_remainder( $items_to_apply, $remaining_discount ) { + $total_discount = 0; + + foreach ( $items_to_apply as $item ) { + for ( $i = 0; $i < $item->quantity; $i ++ ) { + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); + + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price: $discounted_price; + + // Run coupon calculations. + $discount = min( $discounted_price, 1 ); + $discount_tax = $this->get_item_discount_tax( $item, $discount ); + + // Store totals. + $total_discount += $discount; + $this->discounts[ $item->key ] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax; + + if ( $total_discount >= $cart_discount ) { + break 2; } } + if ( $total_discount >= $cart_discount ) { + break; + } } - - return $return; + return $total_discount; } /** diff --git a/tests/unit-tests/discounts/discounts.php b/tests/unit-tests/discounts/discounts.php index 2e019c4cd53..6e819acc392 100644 --- a/tests/unit-tests/discounts/discounts.php +++ b/tests/unit-tests/discounts/discounts.php @@ -24,10 +24,11 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { 'taxes' => array(), 'discounted_price' => 0, ); - $item->cart_item = $cart_item; - $item->quantity = $cart_item['quantity']; - $item->price = $cart_item['data']->get_price() * $precision * $cart_item['quantity']; - $item->product = $cart_item['data']; + $item->cart_item = $cart_item; + $item->quantity = $cart_item['quantity']; + $item->price = $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; @@ -88,34 +89,38 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { $coupon = WC_Helper_Coupon::create_coupon( 'test' ); $coupon->set_amount( 50 ); $coupon->set_discount_type( 'percent' ); + $coupon->save(); $discounts->apply_coupon( $coupon ); - $this->assertEquals( array( 'test' => 5 ), $discounts->get_applied_coupons() ); + $this->assertEquals( array( 'test' => array( 'discount' => 5, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); $coupon2 = WC_Helper_Coupon::create_coupon( 'test2' ); $coupon2->set_code( 'test2' ); $coupon2->set_amount( 50 ); $coupon2->set_discount_type( 'percent' ); + $coupon->save(); $discounts->apply_coupon( $coupon2 ); - $this->assertEquals( array( 'test' => 5, 'test2' => 2.50 ), $discounts->get_applied_coupons() ); + $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 ); - $this->assertEquals( array( 'test' => 6.25, 'test2' => 2.50 ), $discounts->get_applied_coupons() ); + $this->assertEquals( array( 'test' => array( 'discount' => 6.25, 'discount_tax' => 0 ), 'test2' => array( 'discount' => 5, 'discount_tax' => 2.50 ) ), $discounts->get_applied_coupons() ); // Test different coupon types. WC()->cart->empty_cart(); WC()->cart->add_to_cart( $product->get_id(), 2 ); $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 ); - $this->assertEquals( array( 'test' => 4 ), $discounts->get_applied_coupons() ); + $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 ); - $this->assertEquals( array( 'test' => 2 ), $discounts->get_applied_coupons() ); + $this->assertEquals( array( 'test' => array( 'discount' => 2, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); // Cleanup. WC()->cart->empty_cart();