diff --git a/includes/class-wc-cart.php b/includes/class-wc-cart.php index d416fd2634c..0933d4d3ae8 100644 --- a/includes/class-wc-cart.php +++ b/includes/class-wc-cart.php @@ -916,7 +916,7 @@ class WC_Cart { $cart_item_key = $cart_id; // Add item after merging with $cart_item_data - hook to allow plugins to modify cart item - $this->cart_contents[$cart_item_key] = apply_filters( 'woocommerce_add_cart_item', array_merge( $cart_item_data, array( + $this->cart_contents[ $cart_item_key ] = apply_filters( 'woocommerce_add_cart_item', array_merge( $cart_item_data, array( 'product_id' => $product_id, 'variation_id' => $variation_id, 'variation' => $variation, @@ -978,6 +978,7 @@ class WC_Cart { $this->$key = $default; unset( WC()->session->$key ); } + do_action( 'woocommerce_cart_reset', $this ); } /** @@ -986,6 +987,7 @@ class WC_Cart { public function calculate_totals() { $this->reset(); + $this->coupons = $this->get_coupons(); do_action( 'woocommerce_before_calculate_totals', $this ); @@ -1663,8 +1665,9 @@ class WC_Cart { foreach ( $this->applied_coupons as $code ) { $coupon = new WC_Coupon( $code ); - if ( $coupon->apply_before_tax() ) + if ( $coupon->apply_before_tax() ) { $coupons[ $code ] = $coupon; + } } } } @@ -1674,8 +1677,9 @@ class WC_Cart { foreach ( $this->applied_coupons as $code ) { $coupon = new WC_Coupon( $code ); - if ( ! $coupon->apply_before_tax() ) + if ( ! $coupon->apply_before_tax() ) { $coupons[ $code ] = $coupon; + } } } } @@ -1765,13 +1769,12 @@ class WC_Cart { * @return float price */ public function get_discounted_price( $values, $price, $add_totals = false ) { - if ( ! $price ) + if ( ! $price ) { return $price; + } - if ( ! empty( $this->applied_coupons ) ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); - + if ( ! empty( $this->coupons ) ) { + foreach ( $this->coupons as $code => $coupon ) { if ( $coupon->apply_before_tax() && $coupon->is_valid() ) { if ( $coupon->is_valid_for_product( $values['data'], $values ) || $coupon->is_valid_for_cart() ) { @@ -1799,10 +1802,8 @@ class WC_Cart { public function apply_cart_discounts_after_tax() { $pre_discount_total = round( $this->cart_contents_total + $this->tax_total + $this->shipping_tax_total + $this->shipping_total + $this->fee_total, $this->dp ); - if ( $this->applied_coupons ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); - + if ( $this->coupons ) { + foreach ( $this->coupons as $code => $coupon ) { do_action( 'woocommerce_cart_discount_after_tax_' . $coupon->type, $coupon ); if ( $coupon->is_valid() && ! $coupon->apply_before_tax() && $coupon->is_valid_for_cart() ) { @@ -1824,10 +1825,8 @@ class WC_Cart { * @param double $price */ public function apply_product_discounts_after_tax( $values, $price ) { - if ( ! empty( $this->applied_coupons ) ) { - foreach ( $this->applied_coupons as $code ) { - $coupon = new WC_Coupon( $code ); - + if ( ! empty( $this->coupons ) ) { + foreach ( $this->coupons as $code => $coupon ) { do_action( 'woocommerce_product_discount_after_tax_' . $coupon->type, $coupon, $values, $price ); if ( $coupon->is_valid() && ! $coupon->apply_before_tax() && $coupon->is_valid_for_product( $values['data'] ) ) { @@ -1848,8 +1847,9 @@ class WC_Cart { * @param double $amount */ private function increase_coupon_discount_amount( $code, $amount ) { - if ( empty( $this->coupon_discount_amounts[ $code ] ) ) + if ( empty( $this->coupon_discount_amounts[ $code ] ) ) { $this->coupon_discount_amounts[ $code ] = 0; + } $this->coupon_discount_amounts[ $code ] += $amount; } @@ -1862,8 +1862,9 @@ class WC_Cart { * @param integer $count */ private function increase_coupon_applied_count( $code, $count = 1 ) { - if ( empty( $this->coupon_applied_count[ $code ] ) ) + if ( empty( $this->coupon_applied_count[ $code ] ) ) { $this->coupon_applied_count[ $code ] = 0; + } $this->coupon_applied_count[ $code ] += $count; } diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php index 3a17ad1441a..231d8b06110 100644 --- a/includes/class-wc-coupon.php +++ b/includes/class-wc-coupon.php @@ -5,6 +5,7 @@ * The WooCommerce coupons class gets coupon data from storage and checks coupon validity * * @class WC_Coupon + * @version 2.3.0 * @package WooCommerce/Classes * @category Class * @author WooThemes @@ -31,73 +32,13 @@ class WC_Coupon { const WC_COUPON_REMOVED = 201; /** @public string Coupon code. */ - public $code; + public $code = ''; /** @public int Coupon ID. */ - public $id; + public $id = 0; - /** @public string Type of discount. */ - public $type; - - /** @public string Type of discount (alias). */ - public $discount_type; - - /** @public string Coupon amount. */ - public $amount; - - /** @public string "Yes" if for individual use. */ - public $individual_use; - - /** @public array Array of product IDs. */ - public $product_ids; - - /** @public int Coupon usage limit. */ - public $usage_limit; - - /** @public int Coupon usage limit per user. */ - public $usage_limit_per_user; - - /** @public int Coupon usage limit per item. */ - public $limit_usage_to_x_items; - - /** @public int Coupon usage count. */ - public $usage_count; - - /** @public string Expiry date. */ - public $expiry_date; - - /** @public string "yes" if applied before tax. */ - public $apply_before_tax; - - /** @public string "yes" if coupon grants free shipping. */ - public $free_shipping; - - /** @public array Array of category ids. */ - public $product_categories; - - /** @public array Array of category ids. */ - public $exclude_product_categories; - - /** @public string "yes" if coupon does NOT apply to items on sale. */ - public $exclude_sale_items; - - /** @public string Minimum cart amount. */ - public $minimum_amount; - - /** @public string Maximum cart amount. */ - public $maximum_amount; - - /** @public string Coupon owner's email. */ - public $customer_email; - - /** @public array Post meta. */ - public $coupon_custom_fields; - - /** @public string How much the coupon is worth. */ - public $coupon_amount; - - /** @public string Error message. */ - public $error_message; + /** @public bool Coupon exists */ + public $exists = false; /** * Coupon constructor. Loads coupon data. @@ -106,127 +47,181 @@ class WC_Coupon { * @param mixed $code code of the coupon to load */ public function __construct( $code ) { - global $wpdb; + $this->exists = $this->get_coupon( $code ); + } + /** + * __isset function. + * + * @param mixed $key + * @return bool + */ + public function __isset( $key ) { + if ( in_array( $key, array( 'coupon_custom_fields', 'type', 'amount' ) ) ) { + return true; + } + return false; + } + + /** + * __get function. + * + * @param mixed $key + * @return mixed + */ + public function __get( $key ) { + // Get values or default if not set + if ( 'coupon_custom_fields' === $key ) { + $value = $this->id ? get_post_meta( $this->id ) : array(); + } elseif ( 'type' === $key ) { + $value = $this->discount_type; + } elseif ( 'amount' === $key ) { + $value = $this->coupon_amount; + } else { + $value = ''; + } + return $value; + } + + /** + * Checks the coupon type. + * + * @param string $type Array or string of types + * @return bool + */ + public function is_type( $type ) { + return ( $this->discount_type == $type || ( is_array( $type ) && in_array( $this->discount_type, $type ) ) ) ? true : false; + } + + /** + * Gets an coupon from the database. + * + * @param string $code + * @return bool + */ + private function get_coupon( $code ) { $this->code = apply_filters( 'woocommerce_coupon_code', $code ); // Coupon data lets developers create coupons through code - $coupon_data = apply_filters( 'woocommerce_get_shop_coupon_data', false, $code ); + if ( $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $code ) ) { + $this->populate( $coupon ); + return true; + } elseif ( ( $this->id = $this->get_coupon_id_from_code( $code ) ) && $this->code === get_the_title( $this->id ) ) { + $this->populate(); + return true; + } - if ( $coupon_data ) { + return false; + } - $this->id = absint( $coupon_data['id'] ); - $this->type = esc_html( $coupon_data['type'] ); - $this->amount = esc_html( $coupon_data['amount'] ? $coupon_data['amount'] : $coupon_data['coupon_amount'] ) ; - $this->coupon_amount = $this->amount; - $this->individual_use = esc_html( $coupon_data['individual_use'] ); - $this->product_ids = is_array( $coupon_data['product_ids'] ) ? $coupon_data['product_ids'] : array(); - $this->exclude_product_ids = is_array( $coupon_data['exclude_product_ids'] ) ? $coupon_data['exclude_product_ids'] : array(); - $this->usage_limit = absint( $coupon_data['usage_limit'] ); - $this->usage_limit_per_user = isset( $coupon_data['usage_limit_per_user'] ) ? absint( $coupon_data['usage_limit_per_user'] ) : 0; - $this->limit_usage_to_x_items = isset( $coupon_data['limit_usage_to_x_items'] ) ? absint( $coupon_data['limit_usage_to_x_items'] ) : ''; - $this->usage_count = absint( $coupon_data['usage_count'] ); - $this->expiry_date = esc_html( $coupon_data['expiry_date'] ); - $this->apply_before_tax = esc_html( $coupon_data['apply_before_tax'] ); - $this->free_shipping = esc_html( $coupon_data['free_shipping'] ); - $this->product_categories = is_array( $coupon_data['product_categories'] ) ? $coupon_data['product_categories'] : array(); - $this->exclude_product_categories = is_array( $coupon_data['exclude_product_categories'] ) ? $coupon_data['exclude_product_categories'] : array(); - $this->exclude_sale_items = esc_html( $coupon_data['exclude_sale_items'] ); - $this->minimum_amount = esc_html( $coupon_data['minimum_amount'] ); - $this->maximum_amount = esc_html( $coupon_data['maximum_amount'] ); - $this->customer_email = esc_html( $coupon_data['customer_email'] ); + /** + * Get a coupon ID from it's code + * @param string $code + * @return int + */ + private function get_coupon_id_from_code( $code ) { + global $wpdb; - } else { + return absint( $wpdb->get_var( $wpdb->prepare( apply_filters( 'woocommerce_coupon_code_query', "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish'" ), $this->code ) ) ); + } - $coupon_id = $wpdb->get_var( $wpdb->prepare( apply_filters( 'woocommerce_coupon_code_query', "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish'" ), $this->code ) ); + /** + * Populates an order from the loaded post data. + */ + private function populate( $data = array() ) { + $defaults = array( + 'discount_type' => 'fixed_cart', + 'coupon_amount' => 0, + 'individual_use' => 'no', + 'product_ids' => array(), + 'exclude_product_ids' => array(), + 'usage_limit' => '', + 'usage_limit_per_user' => '', + 'limit_usage_to_x_items' => '', + 'usage_count' => '', + 'expiry_date' => '', + 'apply_before_tax' => 'yes', + 'free_shipping' => 'no', + 'product_categories' => array(), + 'exclude_product_categories' => array(), + 'exclude_sale_items' => 'no', + 'minimum_amount' => '', + 'maximum_amount' => '', + 'customer_email' => array() + ); - if ( ! $coupon_id ) - return; + foreach ( $defaults as $key => $value ) { + // Try to load from meta if an ID is present + if ( $this->id ) { + $this->$key = get_post_meta( $this->id, $key, true ); + } else { + $this->$key = ! empty( $data[ $key ] ) ? wc_clean( $data[ $key ] ) : ''; - $coupon = get_post( $coupon_id ); - $this->post_title = apply_filters( 'woocommerce_coupon_code', $coupon->post_title ); + // Backwards compat field names @deprecated + if ( 'coupon_amount' === $key ) { + $this->coupon_amount = ! empty( $data[ 'amount' ] ) ? wc_clean( $data[ 'amount' ] ) : $this->coupon_amount; + } elseif ( 'discount_type' === $key ) { + $this->discount_type = ! empty( $data[ 'type' ] ) ? wc_clean( $data[ 'type' ] ) : $this->discount_type; + } + } - if ( empty( $coupon ) || $this->code !== $this->post_title ) - return; - - $this->id = $coupon->ID; - $this->coupon_custom_fields = get_post_meta( $this->id ); - - $load_data = array( - 'discount_type' => 'fixed_cart', - 'coupon_amount' => 0, - 'individual_use' => 'no', - 'product_ids' => '', - 'exclude_product_ids' => '', - 'usage_limit' => '', - 'usage_limit_per_user' => '', - 'limit_usage_to_x_items' => '', - 'usage_count' => '', - 'expiry_date' => '', - 'apply_before_tax' => 'yes', - 'free_shipping' => 'no', - 'product_categories' => array(), - 'exclude_product_categories' => array(), - 'exclude_sale_items' => 'no', - 'minimum_amount' => '', - 'maximum_amount' => '', - 'customer_email' => array() - ); - - foreach ( $load_data as $key => $default ) - $this->$key = isset( $this->coupon_custom_fields[ $key ][0] ) && $this->coupon_custom_fields[ $key ][0] !== '' ? $this->coupon_custom_fields[ $key ][0] : $default; - - // Alias - $this->type = $this->discount_type; - $this->amount = $this->coupon_amount; - - // Formatting - $this->product_ids = array_filter( array_map( 'trim', explode( ',', $this->product_ids ) ) ); - $this->exclude_product_ids = array_filter( array_map( 'trim', explode( ',', $this->exclude_product_ids ) ) ); - $this->expiry_date = $this->expiry_date ? strtotime( $this->expiry_date ) : ''; - $this->product_categories = array_filter( array_map( 'trim', (array) maybe_unserialize( $this->product_categories ) ) ); - $this->exclude_product_categories = array_filter( array_map( 'trim', (array) maybe_unserialize( $this->exclude_product_categories ) ) ); - $this->customer_email = array_filter( array_map( 'trim', array_map( 'strtolower', (array) maybe_unserialize( $this->customer_email ) ) ) ); + if ( empty( $this->$key ) ) { + $this->$key = $value; + } elseif ( in_array( $key, array( 'product_ids', 'exclude_product_ids', 'product_categories', 'exclude_product_categories', 'customer_email' ) ) ) { + $this->$key = $this->format_array( $this->$key ); + } elseif ( in_array( $key, array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items', 'usage_count' ) ) ) { + $this->$key = absint( $this->$key ); + } elseif( 'expiry_date' === $key ) { + $this->expiry_date = $this->expiry_date && ! is_numeric( $this->expiry_date ) ? strtotime( $this->expiry_date ) : $this->expiry_date; + } } do_action( 'woocommerce_coupon_loaded', $this ); } + /** + * Format loaded data as array + * @param string|array $array + * @return array + */ + public function format_array( $array ) { + if ( ! is_array( $array ) ) { + if ( is_serialized( $array ) ) { + $array = maybe_unserialize( $array ); + } else { + $array = explode( ',', $array ); + } + } + return array_filter( array_map( 'trim', array_map( 'strtolower', $array ) ) ); + } /** * Check if coupon needs applying before tax. * - * @access public * @return bool */ public function apply_before_tax() { - return $this->apply_before_tax == 'yes' ? true : false; + return 'yes' === $this->apply_before_tax; } - /** * Check if a coupon enables free shipping. * - * @access public * @return bool */ public function enable_free_shipping() { - return $this->free_shipping == 'yes' ? true : false; + return 'yes' === $this->free_shipping; } - /** * Check if a coupon excludes sale items. * - * @access public * @return bool */ public function exclude_sale_items() { - return $this->exclude_sale_items == 'yes' ? true : false; + return 'yes' === $this->exclude_sale_items; } - - /** * Increase usage count for current coupon. * @@ -235,15 +230,15 @@ class WC_Coupon { * @return void */ public function inc_usage_count( $used_by = '' ) { - $this->usage_count++; - update_post_meta( $this->id, 'usage_count', $this->usage_count ); + if ( $this->id ) { + update_post_meta( $this->id, 'usage_count', ( $this->usage_count ++ ) ); - if ( $used_by ) { - add_post_meta( $this->id, '_used_by', strtolower( $used_by ) ); + if ( $used_by ) { + add_post_meta( $this->id, '_used_by', strtolower( $used_by ) ); + } } } - /** * Decrease usage count for current coupon. * @@ -252,15 +247,16 @@ class WC_Coupon { * @return void */ public function dcr_usage_count( $used_by = '' ) { - global $wpdb; + if ( $this->id ) { + global $wpdb; - $this->usage_count--; - update_post_meta( $this->id, 'usage_count', $this->usage_count ); + update_post_meta( $this->id, 'usage_count', ( $this->usage_count -- ) ); - // Delete 1 used by meta - $meta_id = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", $used_by, $this->id ) ); - if ( $meta_id ) { - delete_metadata_by_mid( 'post', $meta_id ); + // Delete 1 used by meta + $meta_id = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", $used_by, $this->id ) ); + if ( $meta_id ) { + delete_metadata_by_mid( 'post', $meta_id ); + } } } @@ -275,190 +271,229 @@ class WC_Coupon { } /** - * is_valid function. + * Ensure coupon exists or throw exception + */ + private function validate_exists() { + if ( ! $this->exists ) { + throw new Exception( self::E_WC_COUPON_NOT_EXIST ); + } + } + + /** + * Ensure coupon usage limit is valid or throw exception + */ + private function validate_usage_limit() { + if ( $this->usage_limit > 0 && $this->usage_count >= $this->usage_limit ) { + throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); + } + } + + /** + * Ensure coupon user usage limit is valid or throw exception * - * Check if a coupon is valid. Return a reason code if invalid. Reason codes: + * 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() + */ + private function validate_user_usage_limit() { + if ( $this->usage_limit_per_user > 0 && is_user_logged_in() && $this->id ) { + $used_by = (array) get_post_meta( $this->id, '_used_by' ); + $usage_count = sizeof( array_keys( $used_by, get_current_user_id() ) ); + + if ( $usage_count >= $this->usage_limit_per_user ) { + throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); + } + } + } + + /** + * Ensure coupon date is valid or throw exception + */ + private function validate_expiry_date() { + if ( $this->expiry_date && current_time( 'timestamp' ) > $this->expiry_date ) { + throw new Exception( $error_code = self::E_WC_COUPON_EXPIRED ); + } + } + + /** + * Ensure coupon amount is valid or throw exception + */ + private function validate_minimum_amount() { + if ( $this->minimum_amount > 0 && $this->minimum_amount > WC()->cart->subtotal ) { + throw new Exception( self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET ); + } + } + + /** + * Ensure coupon amount is valid or throw exception + */ + private function validate_maximum_amount() { + if ( $this->maximum_amount > 0 && $this->maximum_amount < WC()->cart->subtotal ) { + throw new Exception( self::E_WC_COUPON_MAX_SPEND_LIMIT_MET ); + } + } + + /** + * Ensure coupon is valid for products in the cart is valid or throw exception + */ + private function validate_product_ids() { + if ( sizeof( $this->product_ids ) > 0 ) { + $valid_for_cart = false; + if ( sizeof( WC()->cart->get_cart() ) > 0 ) { + foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( in_array( $cart_item['product_id'], $this->product_ids ) || in_array( $cart_item['variation_id'], $this->product_ids ) || in_array( $cart_item['data']->get_parent(), $this->product_ids ) ) { + $valid_for_cart = true; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE ); + } + } + } + + /** + * Ensure coupon is valid for product categories in the cart is valid or throw exception + */ + private function validate_product_categories() { + if ( sizeof( $this->product_categories ) > 0 ) { + $valid_for_cart = false; + if ( sizeof( WC()->cart->get_cart() ) > 0 ) { + foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + + $product_cats = wp_get_post_terms( $cart_item['product_id'], 'product_cat', array( "fields" => "ids" ) ); + + if ( sizeof( array_intersect( $product_cats, $this->product_categories ) ) > 0 ) { + $valid_for_cart = true; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE ); + } + } + } + + /** + * Ensure coupon is valid for sale items in the cart is valid or throw exception + */ + private function validate_sale_items() { + if ( 'yes' === $this->exclude_sale_items && $this->is_type( array( 'fixed_product', 'percent_product' ) ) ) { + $valid_for_cart = false; + $product_ids_on_sale = wc_get_product_ids_on_sale(); + if ( sizeof( WC()->cart->get_cart() ) > 0 ) { + foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( sizeof( array_intersect( array( absint( $cart_item['product_id'] ), absint( $cart_item['variation_id'] ), $cart_item['data']->get_parent() ), $product_ids_on_sale ) ) === 0 ) { + // not on sale + $valid_for_cart = true; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS ); + } + } + } + + /** + * Cart discounts cannot be added if non-eligble product is found in cart + */ + private function validate_cart_excluded_items() { + if ( ! $this->is_type( array( 'fixed_product', 'percent_product' ) ) ) { + $this->validate_cart_excluded_product_ids(); + $this->validate_cart_excluded_product_categories(); + $this->validate_cart_excluded_sale_items(); + } + } + + /** + * Exclude products from cart + */ + private function validate_cart_excluded_product_ids() { + // Exclude Products + if ( sizeof( $this->exclude_product_ids ) > 0 ) { + $valid_for_cart = true; + if ( sizeof( WC()->cart->get_cart() ) > 0 ) { + foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( in_array( $cart_item['product_id'], $this->exclude_product_ids ) || in_array( $cart_item['variation_id'], $this->exclude_product_ids ) || in_array( $cart_item['data']->get_parent(), $this->exclude_product_ids ) ) { + $valid_for_cart = false; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_EXCLUDED_PRODUCTS ); + } + } + } + + /** + * Exclude categories from cart + */ + private function validate_cart_excluded_product_categories() { + if ( sizeof( $this->exclude_product_categories ) > 0 ) { + $valid_for_cart = true; + if ( sizeof( WC()->cart->get_cart() ) > 0 ) { + foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + + $product_cats = wp_get_post_terms( $cart_item['product_id'], 'product_cat', array( "fields" => "ids" ) ); + + if ( sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) { + $valid_for_cart = false; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_EXCLUDED_CATEGORIES ); + } + } + } + + /** + * Exclude sale items from cart + */ + private function validate_cart_excluded_sale_items() { + if ( $this->exclude_sale_items == 'yes' ) { + $valid_for_cart = true; + $product_ids_on_sale = wc_get_product_ids_on_sale(); + if ( sizeof( WC()->cart->get_cart() ) > 0 ) { + foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( in_array( $cart_item['product_id'], $product_ids_on_sale, true ) || in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) || in_array( $cart_item['data']->get_parent(), $product_ids_on_sale, true ) ) { + $valid_for_cart = false; + } + } + } + if ( ! $valid_for_cart ) { + throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS ); + } + } + } + + /** + * Check if a coupon is valid. * - * @access public - * @return boolean validity or a WP_Error if not valid + * @return boolean validity */ public function is_valid() { + try { + $this->validate_exists(); + $this->validate_usage_limit(); + $this->validate_user_usage_limit(); + $this->validate_expiry_date(); + $this->validate_minimum_amount(); + $this->validate_maximum_amount(); + $this->validate_product_ids(); + $this->validate_product_categories(); + $this->validate_sale_items(); + $this->validate_cart_excluded_items(); - $error_code = null; - $valid = true; - $error = false; - - if ( $this->id ) { - - // Usage Limit - if ( $this->usage_limit > 0 ) { - if ( $this->usage_count >= $this->usage_limit ) { - $valid = false; - $error_code = self::E_WC_COUPON_USAGE_LIMIT_REACHED; - } + if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $this ) ) { + throw new Exception( self::E_WC_COUPON_INVALID_FILTERED ); } - - // 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() - if ( $this->usage_limit_per_user > 0 && is_user_logged_in() ) { - $used_by = (array) get_post_meta( $this->id, '_used_by' ); - $usage_count = sizeof( array_keys( $used_by, get_current_user_id() ) ); - - if ( $usage_count >= $this->usage_limit_per_user ) { - $valid = false; - $error_code = self::E_WC_COUPON_USAGE_LIMIT_REACHED; - } - } - - // Expired - if ( $this->expiry_date ) { - if ( current_time( 'timestamp' ) > $this->expiry_date ) { - $valid = false; - $error_code = self::E_WC_COUPON_EXPIRED; - } - } - - // Minimum spend - if ( $this->minimum_amount > 0 ) { - if ( $this->minimum_amount > WC()->cart->subtotal ) { - $valid = false; - $error_code = self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET; - } - } - - // Maximum spend - if ( $this->maximum_amount > 0 ) { - if ( $this->maximum_amount < WC()->cart->subtotal ) { - $valid = false; - $error_code = self::E_WC_COUPON_MAX_SPEND_LIMIT_MET; - } - } - - // Product ids - If a product included is found in the cart then its valid - if ( sizeof( $this->product_ids ) > 0 ) { - $valid_for_cart = false; - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - - if ( in_array( $cart_item['product_id'], $this->product_ids ) || in_array( $cart_item['variation_id'], $this->product_ids ) || in_array( $cart_item['data']->get_parent(), $this->product_ids ) ) - $valid_for_cart = true; - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_APPLICABLE; - } - } - - // Category ids - If a product included is found in the cart then its valid - if ( sizeof( $this->product_categories ) > 0 ) { - $valid_for_cart = false; - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - - $product_cats = wp_get_post_terms($cart_item['product_id'], 'product_cat', array("fields" => "ids")); - - if ( sizeof( array_intersect( $product_cats, $this->product_categories ) ) > 0 ) - $valid_for_cart = true; - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_APPLICABLE; - } - } - - // Exclude Sale Items check for product coupons - valid if a non-sale item is present - if ( 'yes' === $this->exclude_sale_items && in_array( $this->type, array( 'fixed_product', 'percent_product' ) ) ) { - $valid_for_cart = false; - $product_ids_on_sale = wc_get_product_ids_on_sale(); - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - if ( sizeof( array_intersect( array( absint( $cart_item['product_id'] ), absint( $cart_item['variation_id'] ), $cart_item['data']->get_parent() ), $product_ids_on_sale ) ) === 0 ) { - // not on sale - $valid_for_cart = true; - } - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_VALID_SALE_ITEMS; - } - } - - // Cart discounts cannot be added if non-eligble product is found in cart - if ( $this->type != 'fixed_product' && $this->type != 'percent_product' ) { - - // Exclude Products - if ( sizeof( $this->exclude_product_ids ) > 0 ) { - $valid_for_cart = true; - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - if ( in_array( $cart_item['product_id'], $this->exclude_product_ids ) || in_array( $cart_item['variation_id'], $this->exclude_product_ids ) || in_array( $cart_item['data']->get_parent(), $this->exclude_product_ids ) ) { - $valid_for_cart = false; - } - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_EXCLUDED_PRODUCTS; - } - } - - // Exclude Sale Items - if ( $this->exclude_sale_items == 'yes' ) { - $valid_for_cart = true; - $product_ids_on_sale = wc_get_product_ids_on_sale(); - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - if ( in_array( $cart_item['product_id'], $product_ids_on_sale, true ) || in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) || in_array( $cart_item['data']->get_parent(), $product_ids_on_sale, true ) ) { - $valid_for_cart = false; - } - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_NOT_VALID_SALE_ITEMS; - } - } - - // Exclude Categories - if ( sizeof( $this->exclude_product_categories ) > 0 ) { - $valid_for_cart = true; - if ( sizeof( WC()->cart->get_cart() ) > 0 ) { - foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - - $product_cats = wp_get_post_terms( $cart_item['product_id'], 'product_cat', array( "fields" => "ids" ) ); - - if ( sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) { - $valid_for_cart = false; - } - } - } - if ( ! $valid_for_cart ) { - $valid = false; - $error_code = self::E_WC_COUPON_EXCLUDED_CATEGORIES; - } - } - } - - $valid = apply_filters( 'woocommerce_coupon_is_valid', $valid, $this ); - - if ( $valid ) { - return true; - } else { - if ( is_null( $error_code ) ) - $error_code = self::E_WC_COUPON_INVALID_FILTERED; - } - - } else { - $error_code = self::E_WC_COUPON_NOT_EXIST; + } catch ( Exception $e ) { + $this->error_message = $this->get_coupon_error( $e->getMessage() ); + return false; } - if ( $error_code ) - $this->error_message = $this->get_coupon_error( $error_code ); - - return false; + return true; } /** @@ -467,8 +502,7 @@ class WC_Coupon { * @return bool */ public function is_valid_for_cart() { - $valid = $this->type != 'fixed_cart' && $this->type != 'percent' ? false : true; - return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $valid, $this ); + return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $this->is_type( array( 'fixed_cart', 'percent' ) ), $this ); } /** @@ -478,8 +512,9 @@ class WC_Coupon { * @return boolean */ public function is_valid_for_product( $product, $values = array() ) { - if ( $this->type != 'fixed_product' && $this->type != 'percent_product' ) + if ( ! $this->is_type( array( 'fixed_product', 'percent_product' ) ) ) { return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values ); + } $valid = false; $product_cats = wp_get_post_terms( $product->id, 'product_cat', array( "fields" => "ids" ) ); @@ -487,14 +522,16 @@ class WC_Coupon { // Specific products get the discount if ( sizeof( $this->product_ids ) > 0 ) { - if ( in_array( $product->id, $this->product_ids ) || ( isset( $product->variation_id ) && in_array( $product->variation_id, $this->product_ids ) ) || in_array( $product->get_parent(), $this->product_ids ) ) + if ( in_array( $product->id, $this->product_ids ) || ( isset( $product->variation_id ) && in_array( $product->variation_id, $this->product_ids ) ) || in_array( $product->get_parent(), $this->product_ids ) ) { $valid = true; + } // Category discounts } elseif ( sizeof( $this->product_categories ) > 0 ) { - if ( sizeof( array_intersect( $product_cats, $this->product_categories ) ) > 0 ) + if ( sizeof( array_intersect( $product_cats, $this->product_categories ) ) > 0 ) { $valid = true; + } } else { // No product ids - all items discounted @@ -502,21 +539,26 @@ class WC_Coupon { } // Specific product ID's excluded from the discount - if ( sizeof( $this->exclude_product_ids ) > 0 ) - if ( in_array( $product->id, $this->exclude_product_ids ) || ( isset( $product->variation_id ) && in_array( $product->variation_id, $this->exclude_product_ids ) ) || in_array( $product->get_parent(), $this->exclude_product_ids ) ) + if ( sizeof( $this->exclude_product_ids ) > 0 ) { + if ( in_array( $product->id, $this->exclude_product_ids ) || ( isset( $product->variation_id ) && in_array( $product->variation_id, $this->exclude_product_ids ) ) || in_array( $product->get_parent(), $this->exclude_product_ids ) ) { $valid = false; + } + } // Specific categories excluded from the discount - if ( sizeof( $this->exclude_product_categories ) > 0 ) - if ( sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) + if ( sizeof( $this->exclude_product_categories ) > 0 ) { + if ( sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) { $valid = false; + } + } // Sale Items excluded from discount if ( $this->exclude_sale_items == 'yes' ) { $product_ids_on_sale = wc_get_product_ids_on_sale(); - if ( in_array( $product->id, $product_ids_on_sale, true ) || ( isset( $product->variation_id ) && in_array( $product->variation_id, $product_ids_on_sale, true ) ) || in_array( $product->get_parent(), $product_ids_on_sale, true ) ) + if ( in_array( $product->id, $product_ids_on_sale, true ) || ( isset( $product->variation_id ) && in_array( $product->variation_id, $product_ids_on_sale, true ) ) || in_array( $product->get_parent(), $product_ids_on_sale, true ) ) { $valid = false; + } } return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values ); @@ -533,9 +575,9 @@ class WC_Coupon { public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) { $discount = 0; - if ( $this->type == 'fixed_product') { + if ( $this->is_type( 'fixed_product' ) ) { - $discount = $discounting_amount < $this->amount ? $discounting_amount : $this->amount; + $discount = $discounting_amount < $this->coupon_amount ? $discounting_amount : $this->coupon_amount; // If dealing with a line and not a single item, we need to multiple fixed discount by cart item qty. if ( ! $single && ! is_null( $cart_item ) ) { @@ -543,11 +585,11 @@ class WC_Coupon { $discount = $discount * $cart_item['quantity']; } - } elseif ( $this->type == 'percent_product' || $this->type == 'percent' ) { + } elseif ( $this->is_type( array( 'percent_product', 'percent' ) ) ) { - $discount = round( ( $discounting_amount / 100 ) * $this->amount, WC()->cart->dp ); + $discount = round( ( $discounting_amount / 100 ) * $this->coupon_amount, WC()->cart->dp ); - } elseif ( $this->type == 'fixed_cart' ) { + } elseif ( $this->is_type( 'fixed_cart' ) ) { if ( ! is_null( $cart_item ) ) { /** * This is the most complex discount - we need to divide the discount between rows based on their price in @@ -562,15 +604,18 @@ class WC_Coupon { $discount_percent = ( $cart_item['data']->get_price_excluding_tax() * $cart_item['quantity'] ) / WC()->cart->subtotal_ex_tax; } - $discount = min( ( $this->amount * $discount_percent ) / $cart_item['quantity'], $discounting_amount ); + $discount = min( ( $this->coupon_amount * $discount_percent ) / $cart_item['quantity'], $discounting_amount ); } else { - $discount = min( $this->amount, $discounting_amount ); + $discount = min( $this->coupon_amount, $discounting_amount ); } } // Handle the limit_usage_to_x_items option - if ( in_array( $this->type, array( 'percent_product', 'fixed_product' ) ) && ! is_null( $cart_item ) ) { - $qty = empty( $this->limit_usage_to_x_items ) ? $cart_item['quantity'] : min( $this->limit_usage_to_x_items, $cart_item['quantity'] ); + if ( $this->is_type( array( 'percent_product', 'fixed_product' ) ) && ! is_null( $cart_item ) ) { + $qty = '' === $this->limit_usage_to_x_items ? $cart_item['quantity'] : min( $this->limit_usage_to_x_items, $cart_item['quantity'] ); + + // Reduce limits + $this->limit_usage_to_x_items = max( 0, $this->limit_usage_to_x_items - $qty ); if ( $single ) { $discount = ( $discount * $qty ) / $cart_item['quantity']; @@ -586,27 +631,24 @@ class WC_Coupon { * Converts one of the WC_Coupon message/error codes to a message string and * displays the message/error. * - * @access public * @param int $msg_code Message/error code. * @return void */ public function add_coupon_message( $msg_code ) { - - if ( $msg_code < 200 ) + if ( $msg_code < 200 ) { wc_add_notice( $this->get_coupon_error( $msg_code ), 'error' ); - else + } else { wc_add_notice( $this->get_coupon_message( $msg_code ) ); + } } /** * Map one of the WC_Coupon message codes to a message string * - * @access public * @param integer $msg_code * @return string| Message/error string */ public function get_coupon_message( $msg_code ) { - switch ( $msg_code ) { case self::WC_COUPON_SUCCESS : $msg = __( 'Coupon code applied successfully.', 'woocommerce' ); @@ -618,19 +660,16 @@ class WC_Coupon { $msg = ''; break; } - return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this ); } /** * Map one of the WC_Coupon error codes to a message string * - * @access public * @param int $err_code Message/error code. * @return string| Message/error string */ public function get_coupon_error( $err_code ) { - switch ( $err_code ) { case self::E_WC_COUPON_INVALID_FILTERED: $err = __( 'Coupon is not valid.', 'woocommerce' ); @@ -666,7 +705,6 @@ class WC_Coupon { $err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' ); break; case self::E_WC_COUPON_EXCLUDED_PRODUCTS: - // Store excluded products that are in cart in $products $products = array(); if ( sizeof( WC()->cart->get_cart() ) > 0 ) { @@ -680,7 +718,6 @@ class WC_Coupon { $err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ); break; case self::E_WC_COUPON_EXCLUDED_CATEGORIES: - // Store excluded categories that are in cart in $categories $categories = array(); if ( sizeof( WC()->cart->get_cart() ) > 0 ) { @@ -709,7 +746,6 @@ class WC_Coupon { $err = ''; break; } - return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this ); } @@ -718,12 +754,10 @@ class WC_Coupon { * No coupon instance will be available where a coupon does not exist, * so this static method exists. * - * @access public * @param int $err_code Error code * @return string| Error string */ public static function get_generic_coupon_error( $err_code ) { - switch ( $err_code ) { case self::E_WC_COUPON_NOT_EXIST: $err = __( 'Coupon does not exist!', 'woocommerce' ); @@ -735,9 +769,7 @@ class WC_Coupon { $err = ''; break; } - // When using this static method, there is no $this to pass to filter return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null ); } - } diff --git a/readme.txt b/readme.txt index 6afc3fe2f62..7d4610ce8ad 100644 --- a/readme.txt +++ b/readme.txt @@ -133,6 +133,7 @@ Yes you can! Join in on our [GitHub repository](http://github.com/woothemes/wooc = 2.3.0 = * Feature - Made tax importer expand postcode ranges. * Feature - Print styles for reports. +* Feature - Remove products from the cart in the widget. * Refactor - Removed deprecated methods from WC_Frontend_Scripts and rewrote script registration and localization to run once. * Refactor - Routing all email functionality through one send() method. * Refactor - Replaced existing email css inliner with Emogrifier.