2017-07-18 04:43:31 +00:00
< ? php
/**
* Discount calculation
*
* @ package WooCommerce / Classes
* @ since 3.2 . 0
*/
2018-02-27 18:48:05 +00:00
defined ( 'ABSPATH' ) || exit ;
2017-07-26 10:07:17 +00:00
2017-07-18 04:43:31 +00:00
/**
* Discounts class .
*/
class WC_Discounts {
2017-08-24 16:07:31 +00:00
/**
* Reference to cart or order object .
*
* @ since 3.2 . 0
2018-06-05 18:57:49 +00:00
* @ var WC_Cart | WC_Order
2017-08-24 16:07:31 +00:00
*/
protected $object ;
2017-07-18 04:43:31 +00:00
/**
2017-07-18 13:04:56 +00:00
* An array of items to discount .
2017-07-18 04:43:31 +00:00
*
2017-07-18 13:04:56 +00:00
* @ var array
2017-07-18 04:43:31 +00:00
*/
2017-07-18 13:04:56 +00:00
protected $items = array ();
2017-07-18 04:43:31 +00:00
2017-07-18 17:07:46 +00:00
/**
2017-07-19 14:55:56 +00:00
* An array of discounts which have been applied to items .
2017-07-18 17:07:46 +00:00
*
2017-07-28 14:35:41 +00:00
* @ var array [] Code => Item Key => Value
2017-07-18 17:07:46 +00:00
*/
2017-07-18 19:42:47 +00:00
protected $discounts = array ();
2017-07-18 17:07:46 +00:00
2017-07-19 11:26:01 +00:00
/**
2018-06-05 19:08:35 +00:00
* WC_Discounts Constructor .
2017-07-25 14:24:00 +00:00
*
2018-06-05 19:08:35 +00:00
* @ param WC_Cart | WC_Order $object Cart or order object .
2017-07-19 11:26:01 +00:00
*/
2018-06-05 19:08:35 +00:00
public function __construct ( $object = null ) {
2017-10-24 16:53:51 +00:00
if ( is_a ( $object , 'WC_Cart' ) ) {
$this -> set_items_from_cart ( $object );
} elseif ( is_a ( $object , 'WC_Order' ) ) {
$this -> set_items_from_order ( $object );
2017-07-28 12:02:39 +00:00
}
}
2017-11-02 16:18:17 +00:00
/**
* Set items directly . Used by WC_Cart_Totals .
*
* @ since 3.2 . 3
* @ param array $items Items to set .
*/
public function set_items ( $items ) {
$this -> items = $items ;
$this -> discounts = array ();
uasort ( $this -> items , array ( $this , 'sort_by_price' ) );
}
2017-07-28 12:02:39 +00:00
/**
2017-08-09 15:16:36 +00:00
* Normalise cart items which will be discounted .
2017-07-28 12:02:39 +00:00
*
* @ since 3.2 . 0
2017-09-27 16:12:45 +00:00
* @ param WC_Cart $cart Cart object .
2017-07-28 12:02:39 +00:00
*/
public function set_items_from_cart ( $cart ) {
2018-02-27 18:48:05 +00:00
$this -> items = array ();
$this -> discounts = array ();
2017-07-28 14:35:41 +00:00
if ( ! is_a ( $cart , 'WC_Cart' ) ) {
return ;
}
2017-07-28 12:02:39 +00:00
2017-10-24 16:53:51 +00:00
$this -> object = $cart ;
2017-07-28 12:02:39 +00:00
foreach ( $cart -> get_cart () as $key => $cart_item ) {
$item = new stdClass ();
$item -> key = $key ;
$item -> object = $cart_item ;
$item -> product = $cart_item [ 'data' ];
2017-10-22 14:13:46 +00:00
$item -> quantity = $cart_item [ 'quantity' ];
2017-12-04 20:38:29 +00:00
$item -> price = wc_add_number_precision_deep ( $item -> product -> get_price () * $item -> quantity );
2017-10-22 14:13:46 +00:00
$this -> items [ $key ] = $item ;
2017-10-22 14:13:31 +00:00
}
2017-10-22 14:13:46 +00:00
uasort ( $this -> items , array ( $this , 'sort_by_price' ) );
2017-07-19 11:26:01 +00:00
}
2017-08-09 15:16:36 +00:00
/**
* Normalise order items which will be discounted .
*
* @ since 3.2 . 0
2018-06-05 19:01:39 +00:00
* @ param WC_Order $order Order object .
2017-08-09 15:16:36 +00:00
*/
public function set_items_from_order ( $order ) {
2018-02-27 18:48:05 +00:00
$this -> items = array ();
$this -> discounts = array ();
2017-08-09 15:16:36 +00:00
if ( ! is_a ( $order , 'WC_Order' ) ) {
return ;
}
2017-10-24 16:53:51 +00:00
$this -> object = $order ;
2017-08-09 15:16:36 +00:00
foreach ( $order -> get_items () as $order_item ) {
2018-02-27 18:48:05 +00:00
$item = new stdClass ();
$item -> key = $order_item -> get_id ();
$item -> object = $order_item ;
$item -> product = $order_item -> get_product ();
$item -> quantity = $order_item -> get_quantity ();
$item -> price = wc_add_number_precision_deep ( $order_item -> get_subtotal () );
2017-08-25 17:21:27 +00:00
if ( $order -> get_prices_include_tax () ) {
2018-02-27 18:48:05 +00:00
$item -> price += wc_add_number_precision_deep ( $order_item -> get_subtotal_tax () );
2017-08-25 17:21:27 +00:00
}
2017-08-09 15:16:36 +00:00
$this -> items [ $order_item -> get_id () ] = $item ;
}
uasort ( $this -> items , array ( $this , 'sort_by_price' ) );
}
2018-02-16 21:52:04 +00:00
/**
* Get the object concerned .
*
* @ since 3.3 . 2
* @ return object
*/
public function get_object () {
return $this -> object ;
}
2017-07-18 04:43:31 +00:00
/**
2017-07-18 13:04:56 +00:00
* Get items .
2017-07-18 04:43:31 +00:00
*
2017-07-18 13:04:56 +00:00
* @ since 3.2 . 0
2017-07-18 14:42:46 +00:00
* @ return object []
2017-07-18 04:43:31 +00:00
*/
2017-07-18 13:04:56 +00:00
public function get_items () {
return $this -> items ;
2017-07-18 04:43:31 +00:00
}
2018-02-16 21:51:02 +00:00
/**
* Get items to validate .
*
* @ since 3.3 . 2
* @ return object []
*/
public function get_items_to_validate () {
return apply_filters ( 'woocommerce_coupon_get_items_to_validate' , $this -> get_items (), $this );
}
2017-07-19 11:26:01 +00:00
/**
2017-07-28 14:35:41 +00:00
* Get discount by key with or without precision .
2017-07-19 11:26:01 +00:00
*
* @ since 3.2 . 0
2017-07-25 14:24:00 +00:00
* @ param string $key name of discount row to return .
2017-07-28 14:35:41 +00:00
* @ param bool $in_cents Should the totals be returned in cents , or without precision .
2017-07-19 11:26:01 +00:00
* @ return array
*/
2017-07-28 14:35:41 +00:00
public function get_discount ( $key , $in_cents = false ) {
$item_discount_totals = $this -> get_discounts_by_item ( $in_cents );
2017-09-08 01:34:58 +00:00
return isset ( $item_discount_totals [ $key ] ) ? $item_discount_totals [ $key ] : 0 ;
2017-07-19 11:26:01 +00:00
}
/**
2017-10-22 14:13:46 +00:00
* Get all discount totals .
2017-07-19 11:26:01 +00:00
*
* @ since 3.2 . 0
2017-07-25 16:25:06 +00:00
* @ param bool $in_cents Should the totals be returned in cents , or without precision .
2017-07-19 11:26:01 +00:00
* @ return array
*/
2017-07-25 16:25:06 +00:00
public function get_discounts ( $in_cents = false ) {
2017-10-22 14:13:46 +00:00
$discounts = $this -> discounts ;
2017-07-28 14:35:41 +00:00
return $in_cents ? $discounts : wc_remove_number_precision_deep ( $discounts );
}
/**
* Get all discount totals per item .
*
* @ since 3.2 . 0
* @ param bool $in_cents Should the totals be returned in cents , or without precision .
* @ return array
*/
public function get_discounts_by_item ( $in_cents = false ) {
2017-10-22 14:13:46 +00:00
$discounts = $this -> discounts ;
$item_discount_totals = ( array ) array_shift ( $discounts );
2017-07-27 21:08:38 +00:00
2017-10-22 14:13:46 +00:00
foreach ( $discounts as $item_discounts ) {
foreach ( $item_discounts as $item_key => $item_discount ) {
$item_discount_totals [ $item_key ] += $item_discount ;
2017-07-28 14:35:41 +00:00
}
2017-07-27 21:08:38 +00:00
}
2017-10-22 14:13:46 +00:00
return $in_cents ? $item_discount_totals : wc_remove_number_precision_deep ( $item_discount_totals );
2017-07-27 21:08:38 +00:00
}
2017-07-28 14:35:41 +00:00
/**
* Get all discount totals per coupon .
*
* @ since 3.2 . 0
* @ param bool $in_cents Should the totals be returned in cents , or without precision .
* @ return array
*/
public function get_discounts_by_coupon ( $in_cents = false ) {
$coupon_discount_totals = array_map ( 'array_sum' , $this -> discounts );
2017-07-28 15:17:57 +00:00
return $in_cents ? $coupon_discount_totals : wc_remove_number_precision_deep ( $coupon_discount_totals );
2017-07-28 14:35:41 +00:00
}
2017-07-19 11:26:01 +00:00
/**
* Get discounted price of an item without precision .
*
* @ since 3.2 . 0
2017-07-25 14:24:00 +00:00
* @ param object $item Get data for this item .
2017-07-19 11:26:01 +00:00
* @ return float
*/
public function get_discounted_price ( $item ) {
2017-07-27 12:48:58 +00:00
return wc_remove_number_precision_deep ( $this -> get_discounted_price_in_cents ( $item ) );
2017-07-19 11:26:01 +00:00
}
/**
* Get discounted price of an item to precision ( in cents ) .
*
* @ since 3.2 . 0
2017-07-25 14:24:00 +00:00
* @ param object $item Get data for this item .
2017-08-08 08:24:26 +00:00
* @ return int
2017-07-19 11:26:01 +00:00
*/
public function get_discounted_price_in_cents ( $item ) {
2018-01-11 15:01:07 +00:00
return absint ( round ( $item -> price - $this -> get_discount ( $item -> key , true ) ) );
2017-07-19 11:26:01 +00:00
}
2017-07-19 14:55:56 +00:00
/**
* Apply a discount to all items using a coupon .
*
* @ since 3.2 . 0
2017-07-25 14:11:32 +00:00
* @ param WC_Coupon $coupon Coupon object being applied to the items .
2017-09-06 10:26:34 +00:00
* @ param bool $validate Set to false to skip coupon validation .
2018-06-05 19:47:16 +00:00
* @ throws Exception Error message when coupon isn ' t valid .
2017-07-26 01:36:41 +00:00
* @ return bool | WP_Error True if applied or WP_Error instance in failure .
2017-07-19 14:55:56 +00:00
*/
2017-09-06 10:26:34 +00:00
public function apply_coupon ( $coupon , $validate = true ) {
2017-08-11 15:16:50 +00:00
if ( ! is_a ( $coupon , 'WC_Coupon' ) ) {
return new WP_Error ( 'invalid_coupon' , __ ( 'Invalid coupon' , 'woocommerce' ) );
}
2017-09-06 10:26:34 +00:00
$is_coupon_valid = $validate ? $this -> is_coupon_valid ( $coupon ) : true ;
2017-07-27 14:31:10 +00:00
if ( is_wp_error ( $is_coupon_valid ) ) {
return $is_coupon_valid ;
}
2017-08-10 11:06:03 +00:00
if ( ! isset ( $this -> discounts [ $coupon -> get_code () ] ) ) {
$this -> discounts [ $coupon -> get_code () ] = array_fill_keys ( array_keys ( $this -> items ), 0 );
2017-07-19 14:55:56 +00:00
}
$items_to_apply = $this -> get_items_to_apply_coupon ( $coupon );
2017-07-27 14:31:10 +00:00
// Core discounts are handled here as of 3.2.
2017-07-19 14:55:56 +00:00
switch ( $coupon -> get_discount_type () ) {
2018-02-27 18:48:05 +00:00
case 'percent' :
2017-08-10 11:06:03 +00:00
$this -> apply_coupon_percent ( $coupon , $items_to_apply );
2017-07-19 14:55:56 +00:00
break ;
2018-02-27 18:48:05 +00:00
case 'fixed_product' :
2017-08-10 11:06:03 +00:00
$this -> apply_coupon_fixed_product ( $coupon , $items_to_apply );
2017-07-19 14:55:56 +00:00
break ;
2018-02-27 18:48:05 +00:00
case 'fixed_cart' :
2017-08-10 11:06:03 +00:00
$this -> apply_coupon_fixed_cart ( $coupon , $items_to_apply );
2017-07-27 14:31:10 +00:00
break ;
2018-02-27 18:48:05 +00:00
default :
2018-02-27 11:28:27 +00:00
$this -> apply_coupon_custom ( $coupon , $items_to_apply );
2017-07-19 14:55:56 +00:00
break ;
}
2017-07-27 21:08:38 +00:00
return true ;
2017-07-19 14:55:56 +00:00
}
/**
* Sort by price .
*
2017-07-26 10:07:17 +00:00
* @ since 3.2 . 0
2017-07-25 14:24:00 +00:00
* @ param array $a First element .
* @ param array $b Second element .
2017-07-19 14:55:56 +00:00
* @ return int
*/
protected function sort_by_price ( $a , $b ) {
2017-10-22 14:13:46 +00:00
$price_1 = $a -> price * $a -> quantity ;
$price_2 = $b -> price * $b -> quantity ;
2017-07-19 14:55:56 +00:00
if ( $price_1 === $price_2 ) {
return 0 ;
}
2017-10-22 14:13:46 +00:00
return ( $price_1 < $price_2 ) ? 1 : - 1 ;
2017-07-19 14:55:56 +00:00
}
2017-07-19 12:49:22 +00:00
/**
* Filter out all products which have been fully discounted to 0.
* Used as array_filter callback .
*
2017-07-26 10:07:17 +00:00
* @ since 3.2 . 0
2017-07-25 14:24:00 +00:00
* @ param object $item Get data for this item .
2017-07-19 12:49:22 +00:00
* @ return bool
*/
protected function filter_products_with_price ( $item ) {
return $this -> get_discounted_price_in_cents ( $item ) > 0 ;
}
/**
* Get items which the coupon should be applied to .
*
2017-07-26 10:07:17 +00:00
* @ since 3.2 . 0
2017-07-25 14:24:00 +00:00
* @ param object $coupon Coupon object .
2017-07-19 12:49:22 +00:00
* @ return array
*/
protected function get_items_to_apply_coupon ( $coupon ) {
2018-02-27 18:48:05 +00:00
$items_to_apply = array ();
2017-07-19 12:49:22 +00:00
2018-02-16 21:51:02 +00:00
foreach ( $this -> get_items_to_validate () as $item ) {
2017-09-20 17:42:10 +00:00
$item_to_apply = clone $item ; // Clone the item so changes to this item do not affect the originals.
2017-10-22 16:13:12 +00:00
if ( 0 === $this -> get_discounted_price_in_cents ( $item_to_apply ) || 0 >= $item_to_apply -> quantity ) {
2017-07-19 12:49:22 +00:00
continue ;
}
2017-10-22 16:13:12 +00:00
2017-09-20 17:42:10 +00:00
if ( ! $coupon -> is_valid_for_product ( $item_to_apply -> product , $item_to_apply -> object ) && ! $coupon -> is_valid_for_cart () ) {
2017-07-19 12:49:22 +00:00
continue ;
}
2017-09-20 17:42:10 +00:00
2017-10-22 14:13:46 +00:00
$items_to_apply [] = $item_to_apply ;
}
2017-07-19 12:49:22 +00:00
return $items_to_apply ;
}
2017-07-18 17:07:46 +00:00
/**
2017-07-27 14:31:10 +00:00
* Apply percent discount to items and return an array of discounts granted .
2017-07-18 19:42:47 +00:00
*
* @ since 3.2 . 0
2017-07-28 14:35:41 +00:00
* @ param WC_Coupon $coupon Coupon object . Passed through filters .
2017-07-28 16:49:39 +00:00
* @ param array $items_to_apply Array of items to apply the coupon to .
2017-07-27 14:31:10 +00:00
* @ return int Total discounted .
2017-07-18 17:47:05 +00:00
*/
2017-08-10 11:06:03 +00:00
protected function apply_coupon_percent ( $coupon , $items_to_apply ) {
2017-10-24 17:07:23 +00:00
$total_discount = 0 ;
$cart_total = 0 ;
$limit_usage_qty = 0 ;
$applied_count = 0 ;
$adjust_final_discount = true ;
2017-10-22 16:13:12 +00:00
if ( null !== $coupon -> get_limit_usage_to_x_items () ) {
$limit_usage_qty = $coupon -> get_limit_usage_to_x_items ();
}
2017-07-19 14:55:56 +00:00
2018-02-22 19:39:37 +00:00
$coupon_amount = $coupon -> get_amount ();
2017-09-13 01:33:43 +00:00
2017-10-22 14:13:46 +00:00
foreach ( $items_to_apply as $item ) {
2017-07-27 14:31:10 +00:00
// Find out how much price is available to discount for the item.
2018-02-27 18:48:05 +00:00
$discounted_price = $this -> get_discounted_price_in_cents ( $item );
2017-07-27 14:31:10 +00:00
// Get the price we actually want to discount, based on settings.
2017-09-08 01:43:27 +00:00
$price_to_discount = ( 'yes' === get_option ( 'woocommerce_calc_discounts_sequentially' , 'no' ) ) ? $discounted_price : $item -> price ;
2017-07-27 14:31:10 +00:00
2017-10-22 16:13:12 +00:00
// See how many and what price to apply to.
2017-11-15 10:31:31 +00:00
$apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item -> quantity ? $limit_usage_qty - $applied_count : $item -> quantity ;
$apply_quantity = max ( 0 , apply_filters ( 'woocommerce_coupon_get_apply_quantity' , $apply_quantity , $item , $coupon , $this ) );
2017-10-22 16:13:12 +00:00
$price_to_discount = ( $price_to_discount / $item -> quantity ) * $apply_quantity ;
2017-08-08 08:24:26 +00:00
2017-07-27 14:31:10 +00:00
// Run coupon calculations.
2017-09-13 01:33:43 +00:00
$discount = floor ( $price_to_discount * ( $coupon_amount / 100 ) );
2017-08-15 14:29:22 +00:00
2017-08-24 16:07:31 +00:00
if ( is_a ( $this -> object , 'WC_Cart' ) && has_filter ( 'woocommerce_coupon_get_discount_amount' ) ) {
2017-08-15 14:29:22 +00:00
// Send through the legacy filter, but not as cents.
2017-10-24 17:07:23 +00:00
$filtered_discount = wc_add_number_precision ( apply_filters ( 'woocommerce_coupon_get_discount_amount' , wc_remove_number_precision ( $discount ), wc_remove_number_precision ( $price_to_discount ), $item -> object , false , $coupon ) );
if ( $filtered_discount !== $discount ) {
$discount = $filtered_discount ;
$adjust_final_discount = false ;
}
2017-08-15 14:29:22 +00:00
}
2018-03-07 13:36:43 +00:00
$discount = wc_round_discount ( min ( $discounted_price , $discount ), 0 );
2017-10-22 16:13:12 +00:00
$cart_total = $cart_total + $price_to_discount ;
$total_discount = $total_discount + $discount ;
$applied_count = $applied_count + $apply_quantity ;
2017-07-28 14:35:41 +00:00
// Store code and discount amount per item.
2017-10-22 14:13:46 +00:00
$this -> discounts [ $coupon -> get_code () ][ $item -> key ] += $discount ;
2017-07-18 17:47:05 +00:00
}
2017-08-08 08:24:26 +00:00
2017-09-13 01:33:43 +00:00
// Work out how much discount would have been given to the cart as a whole and compare to what was discounted on all line items.
2018-03-07 13:36:43 +00:00
$cart_total_discount = wc_round_discount ( $cart_total * ( $coupon_amount / 100 ), 0 );
2017-08-08 08:24:26 +00:00
2017-10-24 17:07:23 +00:00
if ( $total_discount < $cart_total_discount && $adjust_final_discount ) {
2017-08-10 11:06:03 +00:00
$total_discount += $this -> apply_coupon_remainder ( $coupon , $items_to_apply , $cart_total_discount - $total_discount );
2017-08-08 08:24:26 +00:00
}
2017-07-27 14:31:10 +00:00
return $total_discount ;
2017-07-18 17:47:05 +00:00
}
/**
2017-07-18 19:42:47 +00:00
* Apply fixed product discount to items .
2017-07-18 17:07:46 +00:00
*
2017-07-18 19:42:47 +00:00
* @ since 3.2 . 0
2017-07-28 14:35:41 +00:00
* @ param WC_Coupon $coupon Coupon object . Passed through filters .
* @ param array $items_to_apply Array of items to apply the coupon to .
* @ param int $amount Fixed discount amount to apply in cents . Leave blank to pull from coupon .
2017-07-27 14:31:10 +00:00
* @ return int Total discounted .
2017-07-18 17:07:46 +00:00
*/
2017-08-10 11:06:03 +00:00
protected function apply_coupon_fixed_product ( $coupon , $items_to_apply , $amount = null ) {
2017-10-22 16:13:12 +00:00
$total_discount = 0 ;
2018-02-22 19:39:37 +00:00
$amount = $amount ? $amount : wc_add_number_precision ( $coupon -> get_amount () );
2017-10-22 16:13:12 +00:00
$limit_usage_qty = 0 ;
$applied_count = 0 ;
if ( null !== $coupon -> get_limit_usage_to_x_items () ) {
$limit_usage_qty = $coupon -> get_limit_usage_to_x_items ();
}
2017-07-19 14:55:56 +00:00
2017-10-22 14:13:46 +00:00
foreach ( $items_to_apply as $item ) {
2017-07-27 14:31:10 +00:00
// Find out how much price is available to discount for the item.
2018-02-27 18:48:05 +00:00
$discounted_price = $this -> get_discounted_price_in_cents ( $item );
2017-07-27 14:31:10 +00:00
// Get the price we actually want to discount, based on settings.
2017-09-08 01:43:27 +00:00
$price_to_discount = ( 'yes' === get_option ( 'woocommerce_calc_discounts_sequentially' , 'no' ) ) ? $discounted_price : $item -> price ;
2017-07-27 14:31:10 +00:00
// Run coupon calculations.
2017-10-22 16:13:12 +00:00
if ( $limit_usage_qty ) {
2017-11-15 10:31:31 +00:00
$apply_quantity = $limit_usage_qty - $applied_count < $item -> quantity ? $limit_usage_qty - $applied_count : $item -> quantity ;
$apply_quantity = max ( 0 , apply_filters ( 'woocommerce_coupon_get_apply_quantity' , $apply_quantity , $item , $coupon , $this ) );
2017-10-22 16:13:12 +00:00
$discount = min ( $amount , $item -> price / $item -> quantity ) * $apply_quantity ;
} else {
2017-11-15 10:31:31 +00:00
$apply_quantity = apply_filters ( 'woocommerce_coupon_get_apply_quantity' , $item -> quantity , $item , $coupon , $this );
2017-10-22 16:13:12 +00:00
$discount = $amount * $apply_quantity ;
}
2017-08-15 14:29:22 +00:00
2017-08-24 16:07:31 +00:00
if ( is_a ( $this -> object , 'WC_Cart' ) && has_filter ( 'woocommerce_coupon_get_discount_amount' ) ) {
2017-08-15 14:29:22 +00:00
// Send through the legacy filter, but not as cents.
$discount = wc_add_number_precision ( apply_filters ( 'woocommerce_coupon_get_discount_amount' , wc_remove_number_precision ( $discount ), wc_remove_number_precision ( $price_to_discount ), $item -> object , false , $coupon ) );
}
2017-10-22 16:13:12 +00:00
$discount = min ( $discounted_price , $discount );
$total_discount = $total_discount + $discount ;
$applied_count = $applied_count + $apply_quantity ;
2017-07-28 14:35:41 +00:00
// Store code and discount amount per item.
2017-10-22 14:13:46 +00:00
$this -> discounts [ $coupon -> get_code () ][ $item -> key ] += $discount ;
2017-07-18 17:07:46 +00:00
}
2017-07-27 14:31:10 +00:00
return $total_discount ;
2017-07-18 04:43:31 +00:00
}
2017-07-18 17:52:50 +00:00
/**
2017-07-19 11:26:01 +00:00
* Apply fixed cart discount to items .
2017-07-18 17:52:50 +00:00
*
2017-07-19 14:55:56 +00:00
* @ since 3.2 . 0
2017-07-28 14:35:41 +00:00
* @ param WC_Coupon $coupon Coupon object . Passed through filters .
* @ param array $items_to_apply Array of items to apply the coupon to .
* @ param int $amount Fixed discount amount to apply in cents . Leave blank to pull from coupon .
2017-07-27 14:31:10 +00:00
* @ return int Total discounted .
2017-07-18 17:52:50 +00:00
*/
2017-08-10 11:06:03 +00:00
protected function apply_coupon_fixed_cart ( $coupon , $items_to_apply , $amount = null ) {
2017-07-27 14:31:10 +00:00
$total_discount = 0 ;
2017-08-08 08:24:26 +00:00
$amount = $amount ? $amount : wc_add_number_precision ( $coupon -> get_amount () );
2017-07-27 14:31:10 +00:00
$items_to_apply = array_filter ( $items_to_apply , array ( $this , 'filter_products_with_price' ) );
2018-02-27 18:48:05 +00:00
$item_count = array_sum ( wp_list_pluck ( $items_to_apply , 'quantity' ) );
2017-07-19 11:26:01 +00:00
2018-02-27 18:48:05 +00:00
if ( ! $item_count ) {
2017-07-27 14:31:10 +00:00
return $total_discount ;
2017-07-19 11:26:01 +00:00
}
2017-08-15 14:29:22 +00:00
if ( ! $amount ) {
// If there is no amount we still send it through so filters are fired.
$total_discount = $this -> apply_coupon_fixed_product ( $coupon , $items_to_apply , 0 );
} else {
$per_item_discount = absint ( $amount / $item_count ); // round it down to the nearest cent.
2017-07-19 11:26:01 +00:00
2017-08-15 14:29:22 +00:00
if ( $per_item_discount > 0 ) {
$total_discount = $this -> apply_coupon_fixed_product ( $coupon , $items_to_apply , $per_item_discount );
2017-07-19 11:26:01 +00:00
2017-08-15 14:29:22 +00:00
/**
* If there is still discount remaining , repeat the process .
*/
if ( $total_discount > 0 && $total_discount < $amount ) {
$total_discount += $this -> apply_coupon_fixed_cart ( $coupon , $items_to_apply , $amount - $total_discount );
}
} elseif ( $amount > 0 ) {
$total_discount += $this -> apply_coupon_remainder ( $coupon , $items_to_apply , $amount );
2017-07-19 11:26:01 +00:00
}
2017-07-27 14:31:10 +00:00
}
return $total_discount ;
}
2018-02-27 11:28:27 +00:00
/**
* Apply custom coupon discount to items .
*
* @ since 3.3
* @ param WC_Coupon $coupon Coupon object . Passed through filters .
* @ param array $items_to_apply Array of items to apply the coupon to .
* @ return int Total discounted .
*/
protected function apply_coupon_custom ( $coupon , $items_to_apply ) {
foreach ( $items_to_apply as $item ) {
$discounted_price = $this -> get_discounted_price_in_cents ( $item );
$price_to_discount = wc_remove_number_precision ( ( 'yes' === get_option ( 'woocommerce_calc_discounts_sequentially' , 'no' ) ) ? $discounted_price : $item -> price );
$discount = wc_add_number_precision ( $coupon -> get_discount_amount ( $price_to_discount / $item -> quantity , $item -> object , true ) ) * $item -> quantity ;
$discount = min ( $discounted_price , $discount );
// Store code and discount amount per item.
$this -> discounts [ $coupon -> get_code () ][ $item -> key ] += $discount ;
}
2018-03-21 03:00:54 +00:00
// Allow post-processing for custom coupon types (e.g. calculating discrepancy, etc).
2018-02-27 11:28:27 +00:00
$this -> discounts [ $coupon -> get_code () ] = apply_filters ( 'woocommerce_coupon_custom_discounts_array' , $this -> discounts [ $coupon -> get_code () ], $coupon );
return array_sum ( $this -> discounts [ $coupon -> get_code () ] );
}
2017-07-27 14:31:10 +00:00
/**
* 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
2017-07-27 14:46:02 +00:00
* @ param WC_Coupon $coupon Coupon object if appliable . Passed through filters .
2017-07-28 14:35:41 +00:00
* @ param array $items_to_apply Array of items to apply the coupon to .
* @ param int $amount Fixed discount amount to apply .
2017-07-27 14:31:10 +00:00
* @ return int Total discounted .
*/
2017-08-10 11:06:03 +00:00
protected function apply_coupon_remainder ( $coupon , $items_to_apply , $amount ) {
2017-07-27 14:31:10 +00:00
$total_discount = 0 ;
2017-10-22 14:13:46 +00:00
foreach ( $items_to_apply as $item ) {
2017-07-27 14:31:10 +00:00
for ( $i = 0 ; $i < $item -> quantity ; $i ++ ) {
// Find out how much price is available to discount for the item.
2018-02-27 18:48:05 +00:00
$discounted_price = $this -> get_discounted_price_in_cents ( $item );
2017-07-27 14:31:10 +00:00
// Get the price we actually want to discount, based on settings.
2017-09-08 01:43:27 +00:00
$price_to_discount = ( 'yes' === get_option ( 'woocommerce_calc_discounts_sequentially' , 'no' ) ) ? $discounted_price : $item -> price ;
2017-07-27 14:31:10 +00:00
// Run coupon calculations.
2017-09-11 19:56:49 +00:00
$discount = min ( $price_to_discount , 1 );
2017-07-27 14:31:10 +00:00
// Store totals.
2017-07-28 14:35:41 +00:00
$total_discount += $discount ;
// Store code and discount amount per item.
2017-10-22 14:13:46 +00:00
$this -> discounts [ $coupon -> get_code () ][ $item -> key ] += $discount ;
2017-07-27 14:31:10 +00:00
2017-07-28 14:35:41 +00:00
if ( $total_discount >= $amount ) {
2017-07-27 14:31:10 +00:00
break 2 ;
2017-07-19 11:26:01 +00:00
}
}
2017-07-28 14:35:41 +00:00
if ( $total_discount >= $amount ) {
2017-07-27 14:31:10 +00:00
break ;
}
2017-07-19 11:26:01 +00:00
}
2017-07-27 14:31:10 +00:00
return $total_discount ;
2017-07-27 12:48:58 +00:00
}
2017-07-26 01:36:41 +00:00
/**
* Ensure coupon exists or throw exception .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_exists ( $coupon ) {
2017-08-25 15:07:07 +00:00
if ( ! $coupon -> get_id () && ! $coupon -> get_virtual () ) {
2017-07-26 01:36:41 +00:00
/* translators: %s: coupon code */
throw new Exception ( sprintf ( __ ( 'Coupon "%s" does not exist!' , 'woocommerce' ), $coupon -> get_code () ), 105 );
}
return true ;
}
/**
* Ensure coupon usage limit is valid or throw exception .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_usage_limit ( $coupon ) {
if ( $coupon -> get_usage_limit () > 0 && $coupon -> get_usage_count () >= $coupon -> get_usage_limit () ) {
throw new Exception ( __ ( 'Coupon usage limit has been reached.' , 'woocommerce' ), 106 );
}
return true ;
}
/**
* Ensure coupon user usage limit is valid or throw exception .
*
* Per user usage limit - check here if user is logged in ( against user IDs ) .
* Checked again for emails later on in WC_Cart :: check_customer_coupons () .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ param int $user_id User ID .
* @ return bool
*/
protected function validate_coupon_user_usage_limit ( $coupon , $user_id = 0 ) {
if ( empty ( $user_id ) ) {
2018-01-26 10:37:23 +00:00
if ( $this -> object instanceof WC_Order ) {
$user_id = $this -> object -> get_customer_id ();
} else {
$user_id = get_current_user_id ();
}
2017-07-26 01:36:41 +00:00
}
2017-08-14 11:28:03 +00:00
if ( $coupon && $user_id && $coupon -> get_usage_limit_per_user () > 0 && $coupon -> get_id () && $coupon -> get_data_store () ) {
2017-07-26 01:36:41 +00:00
$date_store = $coupon -> get_data_store ();
$usage_count = $date_store -> get_usage_by_user_id ( $coupon , $user_id );
if ( $usage_count >= $coupon -> get_usage_limit_per_user () ) {
throw new Exception ( __ ( 'Coupon usage limit has been reached.' , 'woocommerce' ), 106 );
}
}
return true ;
}
/**
* Ensure coupon date is valid or throw exception .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_expiry_date ( $coupon ) {
2018-03-03 11:46:14 +00:00
if ( $coupon -> get_date_expires () && apply_filters ( 'woocommerce_coupon_validate_expiry_date' , current_time ( 'timestamp' , true ) > $coupon -> get_date_expires () -> getTimestamp (), $coupon , $this ) ) {
2017-07-26 01:36:41 +00:00
throw new Exception ( __ ( 'This coupon has expired.' , 'woocommerce' ), 107 );
}
return true ;
}
/**
* Ensure coupon amount is valid or throw exception .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
2017-09-20 12:51:41 +00:00
protected function validate_coupon_minimum_amount ( $coupon ) {
2017-11-16 10:43:50 +00:00
$subtotal = wc_remove_number_precision ( $this -> get_object_subtotal () );
2017-07-26 01:36:41 +00:00
if ( $coupon -> get_minimum_amount () > 0 && apply_filters ( 'woocommerce_coupon_validate_minimum_amount' , $coupon -> get_minimum_amount () > $subtotal , $coupon , $subtotal ) ) {
/* translators: %s: coupon minimum amount */
throw new Exception ( sprintf ( __ ( 'The minimum spend for this coupon is %s.' , 'woocommerce' ), wc_price ( $coupon -> get_minimum_amount () ) ), 108 );
}
return true ;
}
/**
* Ensure coupon amount is valid or throw exception .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
2017-09-20 12:51:41 +00:00
protected function validate_coupon_maximum_amount ( $coupon ) {
2017-11-16 10:43:50 +00:00
$subtotal = wc_remove_number_precision ( $this -> get_object_subtotal () );
2017-07-26 01:36:41 +00:00
if ( $coupon -> get_maximum_amount () > 0 && apply_filters ( 'woocommerce_coupon_validate_maximum_amount' , $coupon -> get_maximum_amount () < $subtotal , $coupon ) ) {
/* translators: %s: coupon maximum amount */
throw new Exception ( sprintf ( __ ( 'The maximum spend for this coupon is %s.' , 'woocommerce' ), wc_price ( $coupon -> get_maximum_amount () ) ), 112 );
}
return true ;
}
/**
* Ensure coupon is valid for products in the list is valid or throw exception .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_product_ids ( $coupon ) {
if ( count ( $coupon -> get_product_ids () ) > 0 ) {
$valid = false ;
2018-02-16 21:51:02 +00:00
foreach ( $this -> get_items_to_validate () as $item ) {
2017-07-26 01:36:41 +00:00
if ( $item -> product && in_array ( $item -> product -> get_id (), $coupon -> get_product_ids (), true ) || in_array ( $item -> product -> get_parent_id (), $coupon -> get_product_ids (), true ) ) {
$valid = true ;
break ;
}
}
if ( ! $valid ) {
throw new Exception ( __ ( 'Sorry, this coupon is not applicable to selected products.' , 'woocommerce' ), 109 );
}
}
return true ;
}
/**
* Ensure coupon is valid for product categories in the list is valid or throw exception .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_product_categories ( $coupon ) {
if ( count ( $coupon -> get_product_categories () ) > 0 ) {
$valid = false ;
2018-02-16 21:51:02 +00:00
foreach ( $this -> get_items_to_validate () as $item ) {
2017-07-26 01:36:41 +00:00
if ( $coupon -> get_exclude_sale_items () && $item -> product && $item -> product -> is_on_sale () ) {
continue ;
}
$product_cats = wc_get_product_cat_ids ( $item -> product -> get_id () );
2017-10-12 14:40:58 +00:00
if ( $item -> product -> get_parent_id () ) {
$product_cats = array_merge ( $product_cats , wc_get_product_cat_ids ( $item -> product -> get_parent_id () ) );
}
2017-07-26 01:36:41 +00:00
// If we find an item with a cat in our allowed cat list, the coupon is valid.
if ( count ( array_intersect ( $product_cats , $coupon -> get_product_categories () ) ) > 0 ) {
$valid = true ;
break ;
}
}
if ( ! $valid ) {
throw new Exception ( __ ( 'Sorry, this coupon is not applicable to selected products.' , 'woocommerce' ), 109 );
}
}
return true ;
}
/**
* Ensure coupon is valid for sale items in the list is valid or throw exception .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_sale_items ( $coupon ) {
if ( $coupon -> get_exclude_sale_items () ) {
$valid = false ;
2018-02-16 21:51:02 +00:00
foreach ( $this -> get_items_to_validate () as $item ) {
2017-07-26 01:36:41 +00:00
if ( $item -> product && ! $item -> product -> is_on_sale () ) {
$valid = true ;
break ;
}
}
if ( ! $valid ) {
throw new Exception ( __ ( 'Sorry, this coupon is not valid for sale items.' , 'woocommerce' ), 110 );
}
}
return true ;
}
/**
* All exclusion rules must pass at the same time for a product coupon to be valid .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_excluded_items ( $coupon ) {
2018-02-27 19:55:40 +00:00
$items = $this -> get_items_to_validate ();
if ( ! empty ( $items ) && $coupon -> is_type ( wc_get_product_coupon_types () ) ) {
2017-07-26 01:36:41 +00:00
$valid = false ;
2018-02-27 19:55:40 +00:00
foreach ( $items as $item ) {
2017-07-26 13:32:43 +00:00
if ( $item -> product && $coupon -> is_valid_for_product ( $item -> product , $item -> object ) ) {
2017-07-26 01:36:41 +00:00
$valid = true ;
2017-07-19 11:26:01 +00:00
break ;
}
}
2017-07-26 01:36:41 +00:00
if ( ! $valid ) {
throw new Exception ( __ ( 'Sorry, this coupon is not applicable to selected products.' , 'woocommerce' ), 109 );
}
}
return true ;
}
/**
* Cart discounts cannot be added if non - eligible product is found .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_eligible_items ( $coupon ) {
if ( ! $coupon -> is_type ( wc_get_product_coupon_types () ) ) {
$this -> validate_coupon_excluded_product_ids ( $coupon );
$this -> validate_coupon_excluded_product_categories ( $coupon );
}
return true ;
}
/**
* Exclude products .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_excluded_product_ids ( $coupon ) {
// Exclude Products.
if ( count ( $coupon -> get_excluded_product_ids () ) > 0 ) {
$products = array ();
2018-02-16 21:51:02 +00:00
foreach ( $this -> get_items_to_validate () as $item ) {
2017-07-26 01:36:41 +00:00
if ( $item -> product && in_array ( $item -> product -> get_id (), $coupon -> get_excluded_product_ids (), true ) || in_array ( $item -> product -> get_parent_id (), $coupon -> get_excluded_product_ids (), true ) ) {
$products [] = $item -> product -> get_name ();
}
}
if ( ! empty ( $products ) ) {
/* translators: %s: products list */
throw new Exception ( sprintf ( __ ( 'Sorry, this coupon is not applicable to the products: %s.' , 'woocommerce' ), implode ( ', ' , $products ) ), 113 );
}
2017-07-19 11:26:01 +00:00
}
2017-07-19 14:55:56 +00:00
2017-07-26 01:36:41 +00:00
return true ;
}
/**
* Exclude categories from product list .
*
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool
*/
protected function validate_coupon_excluded_product_categories ( $coupon ) {
if ( count ( $coupon -> get_excluded_product_categories () ) > 0 ) {
$categories = array ();
2018-02-16 21:51:02 +00:00
foreach ( $this -> get_items_to_validate () as $item ) {
2018-01-02 11:57:29 +00:00
if ( ! $item -> product ) {
continue ;
}
2017-07-26 01:36:41 +00:00
$product_cats = wc_get_product_cat_ids ( $item -> product -> get_id () );
2017-10-12 14:40:58 +00:00
if ( $item -> product -> get_parent_id () ) {
$product_cats = array_merge ( $product_cats , wc_get_product_cat_ids ( $item -> product -> get_parent_id () ) );
}
2018-02-27 18:48:05 +00:00
$cat_id_list = array_intersect ( $product_cats , $coupon -> get_excluded_product_categories () );
2017-07-26 01:36:41 +00:00
if ( count ( $cat_id_list ) > 0 ) {
foreach ( $cat_id_list as $cat_id ) {
$cat = get_term ( $cat_id , 'product_cat' );
$categories [] = $cat -> name ;
}
}
}
if ( ! empty ( $categories ) ) {
/* translators: %s: categories list */
throw new Exception ( sprintf ( __ ( 'Sorry, this coupon is not applicable to the categories: %s.' , 'woocommerce' ), implode ( ', ' , array_unique ( $categories ) ) ), 114 );
}
}
return true ;
}
2017-11-16 10:43:50 +00:00
/**
* Get the object subtotal
*
* @ return int
*/
protected function get_object_subtotal () {
if ( is_a ( $this -> object , 'WC_Cart' ) ) {
return wc_add_number_precision ( $this -> object -> get_displayed_subtotal () );
2017-11-16 12:03:02 +00:00
} elseif ( is_a ( $this -> object , 'WC_Order' ) ) {
2017-11-16 10:43:50 +00:00
return wc_add_number_precision ( $this -> object -> get_subtotal () );
} else {
return array_sum ( wp_list_pluck ( $this -> items , 'price' ) );
}
}
2017-07-26 01:36:41 +00:00
/**
* Check if a coupon is valid .
*
2017-07-26 10:07:17 +00:00
* Error Codes :
* - 100 : Invalid filtered .
* - 101 : Invalid removed .
* - 102 : Not yours removed .
* - 103 : Already applied .
* - 104 : Individual use only .
* - 105 : Not exists .
* - 106 : Usage limit reached .
* - 107 : Expired .
* - 108 : Minimum spend limit not met .
* - 109 : Not applicable .
* - 110 : Not valid for sale items .
* - 111 : Missing coupon code .
* - 112 : Maximum spend limit met .
* - 113 : Excluded products .
* - 114 : Excluded categories .
*
2017-07-26 01:36:41 +00:00
* @ since 3.2 . 0
* @ throws Exception Error message .
* @ param WC_Coupon $coupon Coupon data .
* @ return bool | WP_Error
*/
public function is_coupon_valid ( $coupon ) {
try {
$this -> validate_coupon_exists ( $coupon );
$this -> validate_coupon_usage_limit ( $coupon );
$this -> validate_coupon_user_usage_limit ( $coupon );
$this -> validate_coupon_expiry_date ( $coupon );
$this -> validate_coupon_minimum_amount ( $coupon );
$this -> validate_coupon_maximum_amount ( $coupon );
$this -> validate_coupon_product_ids ( $coupon );
$this -> validate_coupon_product_categories ( $coupon );
$this -> validate_coupon_sale_items ( $coupon );
$this -> validate_coupon_excluded_items ( $coupon );
$this -> validate_coupon_eligible_items ( $coupon );
2017-07-28 10:51:57 +00:00
if ( ! apply_filters ( 'woocommerce_coupon_is_valid' , true , $coupon , $this ) ) {
2017-07-26 01:36:41 +00:00
throw new Exception ( __ ( 'Coupon is not valid.' , 'woocommerce' ), 100 );
}
} catch ( Exception $e ) {
/**
2017-07-26 10:07:17 +00:00
* Filter the coupon error message .
2017-07-26 01:36:41 +00:00
*
* @ param string $error_message Error message .
* @ param int $error_code Error code .
* @ param WC_Coupon $coupon Coupon data .
*/
2017-09-07 10:14:05 +00:00
$message = apply_filters ( 'woocommerce_coupon_error' , is_numeric ( $e -> getMessage () ) ? $coupon -> get_coupon_error ( $e -> getMessage () ) : $e -> getMessage (), $e -> getCode (), $coupon );
2017-07-26 01:36:41 +00:00
return new WP_Error ( 'invalid_coupon' , $message , array (
'status' => 400 ,
) );
2017-07-26 10:07:17 +00:00
}
2017-07-26 01:36:41 +00:00
return true ;
2017-07-18 17:52:50 +00:00
}
2017-07-18 04:43:31 +00:00
}