2017-07-18 04:43:31 +00:00
< ? php
/**
* Discount calculation
*
* @ author Automattic
* @ package WooCommerce / Classes
* @ version 3.2 . 0
* @ since 3.2 . 0
*/
2017-07-26 10:07:17 +00:00
if ( ! defined ( 'ABSPATH' ) ) {
exit ;
}
2017-07-18 04:43:31 +00:00
/**
* Discounts class .
*/
class WC_Discounts {
/**
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
*
* @ var array
*/
2017-07-18 19:42:47 +00:00
protected $discounts = array ();
2017-07-18 17:07:46 +00:00
2017-07-19 14:55:56 +00:00
/**
* An array of applied coupons codes and total discount .
*
* @ var array
*/
protected $applied_coupons = array ();
2017-07-19 11:26:01 +00:00
/**
* Constructor .
2017-07-25 14:24:00 +00:00
*
* @ param array $items Items to discount .
2017-07-19 11:26:01 +00:00
*/
2017-07-25 14:57:58 +00:00
public function __construct ( $items = array () ) {
2017-07-24 16:21:08 +00:00
$this -> set_items ( $items );
2017-07-19 11:26:01 +00:00
}
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
}
2017-07-19 11:26:01 +00:00
/**
* Get discount by key without precision .
*
* @ since 3.2 . 0
2017-07-25 14:24:00 +00:00
* @ param string $key name of discount row to return .
2017-07-19 11:26:01 +00:00
* @ return array
*/
public function get_discount ( $key ) {
2017-07-27 12:48:58 +00:00
return isset ( $this -> discounts [ $key ] ) ? wc_remove_number_precision_deep ( $this -> discounts [ $key ] ) : 0 ;
2017-07-19 11:26:01 +00:00
}
/**
2017-07-24 16:21:08 +00:00
* Get all discount totals with precision .
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-07-27 12:48:58 +00:00
return $in_cents ? $this -> discounts : wc_remove_number_precision_deep ( $this -> discounts );
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-07-19 11:26:01 +00:00
* @ return float
*/
public function get_discounted_price_in_cents ( $item ) {
return $item -> price - $this -> discounts [ $item -> key ];
}
2017-07-19 14:55:56 +00:00
/**
* Returns a list of applied coupons with name value pairs - name being
* the coupon code , and value being the total amount disounted .
*
* @ since 3.2 . 0
* @ return array
*/
public function get_applied_coupons () {
2017-07-27 12:48:58 +00:00
return wc_remove_number_precision_deep ( $this -> applied_coupons );
2017-07-19 14:55:56 +00:00
}
2017-07-18 04:43:31 +00:00
/**
2017-07-18 13:04:56 +00:00
* Set cart / order items which will be discounted .
2017-07-18 04:43:31 +00:00
*
2017-07-18 13:04:56 +00:00
* @ since 3.2 . 0
2017-07-26 15:50:34 +00:00
* @ param array $items List items .
2017-07-18 04:43:31 +00:00
*/
2017-07-24 16:21:08 +00:00
public function set_items ( $items ) {
2017-07-19 15:17:58 +00:00
$this -> items = array ();
$this -> discounts = array ();
$this -> applied_coupons = array ();
2017-07-18 17:47:05 +00:00
2017-07-24 16:21:08 +00:00
if ( ! empty ( $items ) && is_array ( $items ) ) {
2017-07-26 15:50:34 +00:00
foreach ( $items as $key => $item ) {
$this -> items [ $key ] = $item ;
$this -> items [ $key ] -> key = $key ;
$this -> items [ $key ] -> price = $item -> subtotal ;
2017-07-18 13:04:56 +00:00
}
2017-07-24 16:21:08 +00:00
$this -> discounts = array_fill_keys ( array_keys ( $items ), 0 );
2017-07-18 13:04:56 +00:00
}
2017-07-24 16:21:08 +00:00
uasort ( $this -> items , array ( $this , 'sort_by_price' ) );
2017-07-18 04:43:31 +00:00
}
2017-07-20 13:33:38 +00:00
/**
* Allows a discount to be applied to the items programmatically without a coupon .
2017-07-20 16:34:30 +00:00
*
* @ param string $raw_discount Discount amount either fixed or percentage .
* @ return int discounted amount in cents .
2017-07-20 13:33:38 +00:00
*/
2017-07-20 16:34:30 +00:00
public function apply_discount ( $raw_discount ) {
2017-07-20 19:33:27 +00:00
// Get total item cost after any extra discounts.
$total_to_discount = 0 ;
2017-07-20 16:34:30 +00:00
2017-07-20 19:33:27 +00:00
foreach ( $this -> items as $item ) {
$total_to_discount += $this -> get_discounted_price_in_cents ( $item );
}
2017-07-20 16:34:30 +00:00
2017-07-20 19:33:27 +00:00
foreach ( $this -> discounts as $key => $value ) {
if ( strstr ( $key , 'discount-' ) ) {
$total_to_discount = $total_to_discount - $value ;
2017-07-20 16:34:30 +00:00
}
2017-07-20 19:33:27 +00:00
}
2017-07-20 16:34:30 +00:00
2017-07-27 18:08:22 +00:00
if ( is_a ( $raw_discount , 'WC_Discount' ) ) {
$discount = $raw_discount ;
2017-07-20 19:33:27 +00:00
} else {
2017-07-27 18:08:22 +00:00
$discount = new WC_Discount ;
$discount -> set_amount ( $raw_discount );
2017-07-20 19:33:27 +00:00
}
2017-07-20 16:34:30 +00:00
2017-07-27 18:08:22 +00:00
$discount_total = $discount -> get_discount_amount ( $total_to_discount );
2017-07-20 19:33:27 +00:00
$discount_id = '' ;
$index = 1 ;
2017-07-20 16:34:30 +00:00
2017-07-20 19:33:27 +00:00
while ( ! $discount_id ) {
$discount_id = 'discount-' . $raw_discount ;
2017-07-20 16:34:30 +00:00
2017-07-20 19:33:27 +00:00
if ( 1 < $index ) {
$discount_id .= '-' . $index ;
2017-07-20 16:34:30 +00:00
}
2017-07-20 13:33:38 +00:00
2017-07-20 19:33:27 +00:00
if ( isset ( $this -> discounts [ $discount_id ] ) ) {
$index ++ ;
$discount_id = '' ;
}
2017-07-20 13:33:38 +00:00
}
2017-07-20 19:33:27 +00:00
return $this -> discounts [ $discount_id ] = $discount_total ;
2017-07-20 13:33:38 +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-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
*/
public function apply_coupon ( $coupon ) {
if ( ! is_a ( $coupon , 'WC_Coupon' ) ) {
return false ;
}
2017-07-27 14:31:10 +00:00
$is_coupon_valid = $this -> is_coupon_valid ( $coupon );
if ( is_wp_error ( $is_coupon_valid ) ) {
return $is_coupon_valid ;
}
2017-07-19 14:55:56 +00:00
if ( ! isset ( $this -> applied_coupons [ $coupon -> get_code () ] ) ) {
2017-07-27 12:48:58 +00:00
$this -> applied_coupons [ $coupon -> get_code () ] = array (
'discount' => 0 ,
'discount_tax' => 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
$coupon_type = $coupon -> get_discount_type ();
2017-07-19 14:55:56 +00:00
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 () ) {
case 'percent' :
2017-07-27 14:31:10 +00:00
$this -> apply_percentage_discount ( $items_to_apply , $coupon -> get_amount (), $coupon );
2017-07-19 14:55:56 +00:00
break ;
case 'fixed_product' :
2017-07-27 14:31:10 +00:00
$this -> apply_fixed_product_discount ( $items_to_apply , wc_add_number_precision ( $coupon -> get_amount () ), $coupon );
2017-07-19 14:55:56 +00:00
break ;
case 'fixed_cart' :
2017-07-27 14:31:10 +00:00
$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 );
// Store totals.
$this -> discounts [ $item -> key ] += $discount ;
2017-07-27 14:46:02 +00:00
if ( $coupon ) {
$this -> applied_coupons [ $coupon -> get_code () ][ 'discount' ] += $discount ;
$this -> applied_coupons [ $coupon -> get_code () ][ 'discount_tax' ] += $discount_tax ;
}
2017-07-27 14:31:10 +00:00
}
}
2017-07-19 14:55:56 +00:00
break ;
}
}
/**
* 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 ) {
$price_1 = $a -> price * $a -> quantity ;
2017-07-26 01:36:41 +00:00
$price_2 = $b -> price * $b -> quantity ;
2017-07-19 14:55:56 +00:00
if ( $price_1 === $price_2 ) {
return 0 ;
}
return ( $price_1 < $price_2 ) ? 1 : - 1 ;
}
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 ) {
$items_to_apply = array ();
$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 ();
}
foreach ( $this -> items as $item ) {
if ( 0 === $this -> get_discounted_price_in_cents ( $item ) ) {
continue ;
}
2017-07-26 13:32:43 +00:00
if ( ! $coupon -> is_valid_for_product ( $item -> product , $item -> object ) && ! $coupon -> is_valid_for_cart () ) {
2017-07-19 12:49:22 +00:00
continue ;
}
if ( $limit_usage_qty && $applied_count > $limit_usage_qty ) {
break ;
}
if ( $limit_usage_qty && $item -> quantity > ( $limit_usage_qty - $applied_count ) ) {
$limit_to_qty = absint ( $limit_usage_qty - $applied_count );
$item -> price = ( $item -> price / $item -> quantity ) * $limit_to_qty ;
$item -> quantity = $limit_to_qty ; // Lower the qty so the discount is applied less.
}
if ( 0 >= $item -> quantity ) {
continue ;
}
$items_to_apply [] = $item ;
$applied_count += $item -> quantity ;
}
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-27 14:31:10 +00:00
* @ 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 .
2017-07-18 17:47:05 +00:00
*/
2017-07-27 14:31:10 +00:00
protected function apply_percentage_discount ( $items_to_apply , $amount , $coupon = null ) {
$total_discount = 0 ;
2017-07-19 14:55:56 +00:00
2017-07-19 12:49:22 +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.
$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 = $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 ;
2017-07-27 14:46:02 +00:00
if ( $coupon ) {
$this -> applied_coupons [ $coupon -> get_code () ][ 'discount' ] += $discount ;
$this -> applied_coupons [ $coupon -> get_code () ][ 'discount_tax' ] += $discount_tax ;
}
2017-07-18 17:47:05 +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-19 14:55:56 +00:00
* @ param array $items_to_apply Array of items to apply the coupon to .
2017-07-27 14:31:10 +00:00
* @ param int $amount Amount of discount .
* @ param WC_Coupon $coupon Coupon object if appliable . Passed through filters .
* @ return int Total discounted .
2017-07-18 17:07:46 +00:00
*/
2017-07-27 14:31:10 +00:00
protected function apply_fixed_product_discount ( $items_to_apply , $amount , $coupon = null ) {
$total_discount = 0 ;
2017-07-19 14:55:56 +00:00
2017-07-19 12:49:22 +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.
$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 = $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 ;
2017-07-27 14:46:02 +00:00
if ( $coupon ) {
$this -> applied_coupons [ $coupon -> get_code () ][ 'discount' ] += $discount ;
$this -> applied_coupons [ $coupon -> get_code () ][ 'discount_tax' ] += $discount_tax ;
}
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
* @ param array $items_to_apply Array of items to apply the coupon to .
2017-07-25 14:24:00 +00:00
* @ param int $cart_discount Fixed discount amount to apply .
2017-07-27 14:31:10 +00:00
* @ param WC_Coupon $coupon Coupon object if appliable . Passed through filters .
* @ return int Total discounted .
2017-07-18 17:52:50 +00:00
*/
2017-07-27 14:31:10 +00:00
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' ) );
2017-07-19 11:26:01 +00:00
2017-07-19 12:49:22 +00:00
if ( ! $item_count = array_sum ( wp_list_pluck ( $items_to_apply , 'quantity' ) ) ) {
2017-07-27 14:31:10 +00:00
return $total_discount ;
2017-07-19 11:26:01 +00:00
}
2017-07-27 14:31:10 +00:00
$per_item_discount = floor ( $cart_discount / $item_count ); // round it down to the nearest cent.
2017-07-19 11:26:01 +00:00
if ( $per_item_discount > 0 ) {
2017-07-27 14:31:10 +00:00
$total_discounted = $this -> apply_fixed_product_discount ( $items_to_apply , $per_item_discount , $coupon );
2017-07-19 11:26:01 +00:00
/**
* If there is still discount remaining , repeat the process .
*/
2017-07-27 14:31:10 +00:00
if ( $total_discounted > 0 && $total_discounted < $cart_discount ) {
$total_discounted = $total_discounted + $this -> apply_fixed_cart_discount ( $items_to_apply , $cart_discount - $total_discounted );
2017-07-19 11:26:01 +00:00
}
2017-07-18 19:42:47 +00:00
2017-07-26 01:36:41 +00:00
} elseif ( $cart_discount > 0 ) {
2017-07-27 14:46:02 +00:00
$total_discounted = $this -> apply_fixed_cart_discount_remainder ( $items_to_apply , $cart_discount , $coupon );
2017-07-27 14:31:10 +00:00
}
return $total_discount ;
}
/**
* 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 array $items_to_apply Array of items to apply the coupon to .
* @ param int $cart_discount Fixed discount amount to apply .
* @ param WC_Coupon $coupon Coupon object if appliable . Passed through filters .
2017-07-27 14:31:10 +00:00
* @ return int Total discounted .
*/
2017-07-27 14:46:02 +00:00
protected function apply_fixed_cart_discount_remainder ( $items_to_apply , $remaining_discount , $coupon = null ) {
2017-07-27 14:31:10 +00:00
$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 ;
2017-07-27 14:46:02 +00:00
if ( $coupon ) {
$this -> applied_coupons [ $coupon -> get_code () ][ 'discount' ] += $discount ;
$this -> applied_coupons [ $coupon -> get_code () ][ 'discount_tax' ] += $discount_tax ;
}
2017-07-27 14:31:10 +00:00
2017-07-27 14:46:02 +00:00
if ( $total_discount >= $remaining_discount ) {
2017-07-27 14:31:10 +00:00
break 2 ;
2017-07-19 11:26:01 +00:00
}
}
2017-07-27 14:46:02 +00:00
if ( $total_discount >= $remaining_discount ) {
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
}
/**
* Return discounted tax amount for an item .
*
* @ param object $item
* @ param int $discount_amount
* @ return int
*/
protected function get_item_discount_tax ( $item , $discount_amount ) {
if ( $item -> product -> is_taxable () ) {
$taxes = WC_Tax :: calc_tax ( $discount_amount , $item -> tax_rates , false );
return array_sum ( $taxes );
}
return 0 ;
2017-07-18 17:52:50 +00:00
}
2017-07-26 01:36:41 +00:00
/*
|--------------------------------------------------------------------------
| Validation & Error Handling
|--------------------------------------------------------------------------
*/
/**
* 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 ) {
if ( ! $coupon -> get_id () ) {
/* 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 ) ) {
$user_id = get_current_user_id ();
}
if ( $coupon -> get_usage_limit_per_user () > 0 && is_user_logged_in () && $coupon -> get_id () && $coupon -> get_data_store () ) {
$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 ) {
if ( $coupon -> get_date_expires () && current_time ( 'timestamp' , true ) > $coupon -> get_date_expires () -> getTimestamp () ) {
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 .
* @ param float $subtotal Items subtotal .
* @ return bool
*/
protected function validate_coupon_minimum_amount ( $coupon , $subtotal = 0 ) {
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 .
* @ param float $subtotal Items subtotal .
* @ return bool
*/
protected function validate_coupon_maximum_amount ( $coupon , $subtotal = 0 ) {
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 ;
foreach ( $this -> items as $item ) {
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 ;
foreach ( $this -> items as $item ) {
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 () );
// 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 ;
foreach ( $this -> items as $item ) {
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 ) {
if ( ! $this -> items && $coupon -> is_type ( wc_get_product_coupon_types () ) ) {
$valid = false ;
foreach ( $this -> 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 ();
foreach ( $this -> items as $item ) {
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 ();
foreach ( $this -> items as $item ) {
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 () );
$cat_id_list = array_intersect ( $product_cats , $coupon -> get_excluded_product_categories () );
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 ;
}
/**
* 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 );
if ( ! apply_filters ( 'woocommerce_discount_is_coupon_valid' , true , $coupon , $this ) ) {
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-07-26 10:07:17 +00:00
$message = apply_filters ( 'woocommerce_coupon_error' , $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
}