Completed
Pull Request — master (#11261)
by Mike
08:14
created

WC_Coupon::validate_excluded_items()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 1
Metric Value
cc 5
eloc 9
nc 7
nop 0
dl 0
loc 16
rs 8.8571
c 1
b 1
f 1
1
<?php
2
3
if ( ! defined( 'ABSPATH' ) ) {
4
	exit; // Exit if accessed directly
5
}
6
7
/**
8
 * WooCommerce coupons
9
 *
10
 * The WooCommerce coupons class gets coupon data from storage and checks coupon validity.
11
 *
12
 * @class 		WC_Coupon
13
 * @version		2.3.0
14
 * @package		WooCommerce/Classes
15
 * @category	Class
16
 * @author		WooThemes
17
 *
18
 * @property    string $discount_type
19
 * @property    string $coupon_amount
20
 * @property    string $individual_use
21
 * @property    array $product_ids
22
 * @property    array $exclude_product_ids
23
 * @property    string $usage_limit
24
 * @property    string $usage_limit_per_user
25
 * @property    string $limit_usage_to_x_items
26
 * @property    string $usage_count
27
 * @property    string $expiry_date
28
 * @property    string $free_shipping
29
 * @property    array $product_categories
30
 * @property    array $exclude_product_categories
31
 * @property    string $exclude_sale_items
32
 * @property    string $minimum_amount
33
 * @property    string $maximum_amount
34
 * @property    array $customer_email
35
 */
36
class WC_Coupon {
37
38
	// Coupon message codes
39
	const E_WC_COUPON_INVALID_FILTERED               = 100;
40
	const E_WC_COUPON_INVALID_REMOVED                = 101;
41
	const E_WC_COUPON_NOT_YOURS_REMOVED              = 102;
42
	const E_WC_COUPON_ALREADY_APPLIED                = 103;
43
	const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104;
44
	const E_WC_COUPON_NOT_EXIST                      = 105;
45
	const E_WC_COUPON_USAGE_LIMIT_REACHED            = 106;
46
	const E_WC_COUPON_EXPIRED                        = 107;
47
	const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET        = 108;
48
	const E_WC_COUPON_NOT_APPLICABLE                 = 109;
49
	const E_WC_COUPON_NOT_VALID_SALE_ITEMS           = 110;
50
	const E_WC_COUPON_PLEASE_ENTER                   = 111;
51
	const E_WC_COUPON_MAX_SPEND_LIMIT_MET 			 = 112;
52
	const E_WC_COUPON_EXCLUDED_PRODUCTS              = 113;
53
	const E_WC_COUPON_EXCLUDED_CATEGORIES            = 114;
54
	const WC_COUPON_SUCCESS                          = 200;
55
	const WC_COUPON_REMOVED                          = 201;
56
57
	/** @public string Coupon code. */
58
	public $code   = '';
59
60
	/** @public int Coupon ID. */
61
	public $id     = 0;
62
63
	/** @public bool Coupon exists */
64
	public $exists = false;
65
66
	/**
67
	 * Coupon constructor. Loads coupon data.
68
	 *
69
	 * @access public
70
	 * @param mixed $code code of the coupon to load
71
	 */
72
	public function __construct( $code ) {
73
		$this->exists = $this->get_coupon( $code );
74
	}
75
76
	/**
77
	 * __isset function.
78
	 *
79
	 * @param mixed $key
80
	 * @return bool
81
	 */
82
	public function __isset( $key ) {
83
		if ( in_array( $key, array( 'coupon_custom_fields', 'type', 'amount' ) ) ) {
84
			return true;
85
		}
86
		return false;
87
	}
88
89
	/**
90
	 * __get function.
91
	 *
92
	 * @param mixed $key
93
	 * @return mixed
94
	 */
95
	public function __get( $key ) {
96
		// Get values or default if not set
97
		if ( 'coupon_custom_fields' === $key ) {
98
			$value = $this->id ? get_post_meta( $this->id ) : array();
99
		} elseif ( 'type' === $key ) {
100
			$value = $this->discount_type;
101
		} elseif ( 'amount' === $key ) {
102
			$value = $this->coupon_amount;
103
		} else {
104
			$value = '';
105
		}
106
		return $value;
107
	}
108
109
	/**
110
	 * Checks the coupon type.
111
	 *
112
	 * @param string $type Array or string of types
113
	 * @return bool
114
	 */
115
	public function is_type( $type ) {
116
		return ( $this->discount_type == $type || ( is_array( $type ) && in_array( $this->discount_type, $type ) ) ) ? true : false;
117
	}
118
119
	/**
120
	 * Gets an coupon from the database.
121
	 *
122
	 * @param string $code
123
	 * @return bool
124
	 */
125
	private function get_coupon( $code ) {
126
		$this->code  = apply_filters( 'woocommerce_coupon_code', $code );
127
128
		// Coupon data lets developers create coupons through code
129
		if ( $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $this->code ) ) {
130
			$this->populate( $coupon );
131
			return true;
132
		}
133
134
		// Otherwise get ID from the code
135
		$this->id    = $this->get_coupon_id_from_code( $this->code );
136
		$coupon_post = get_post( $this->id );
137
138
		if ( $coupon_post && $this->code === apply_filters( 'woocommerce_coupon_code', $coupon_post->post_title ) ) {
139
			$this->populate();
140
			return true;
141
		}
142
143
		return false;
144
	}
145
146
	/**
147
	 * Get a coupon ID from it's code.
148
	 * @since 2.5.0 woocommerce_coupon_code_query was removed in favour of woocommerce_get_coupon_id_from_code filter on the return. wp_cache was also implemented.
149
	 * @param  string $code
150
	 * @return int
151
	 */
152
	private function get_coupon_id_from_code( $code ) {
153
		global $wpdb;
154
155
		$coupon_id = wp_cache_get( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, 'coupons' );
156
157
		if ( false === $coupon_id ) {
158
			$sql = $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $this->code );
159
160
			if ( $coupon_id = apply_filters( 'woocommerce_get_coupon_id_from_code', $wpdb->get_var( $sql ), $this->code ) ) {
161
				wp_cache_set( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, $coupon_id, 'coupons' );
162
			}
163
		}
164
165
		return absint( $coupon_id );
166
	}
167
168
	/**
169
	 * Populates an order from the loaded post data.
170
	 */
171
	private function populate( $data = array() ) {
172
		$defaults = array(
173
			'discount_type'              => 'fixed_cart',
174
			'coupon_amount'              => 0,
175
			'individual_use'             => 'no',
176
			'product_ids'                => array(),
177
			'exclude_product_ids'        => array(),
178
			'usage_limit'                => '',
179
			'usage_limit_per_user'       => '',
180
			'limit_usage_to_x_items'     => '',
181
			'usage_count'                => '',
182
			'expiry_date'                => '',
183
			'free_shipping'              => 'no',
184
			'product_categories'         => array(),
185
			'exclude_product_categories' => array(),
186
			'exclude_sale_items'         => 'no',
187
			'minimum_amount'             => '',
188
			'maximum_amount'             => '',
189
			'customer_email'             => array()
190
		);
191
192
		if ( ! empty( $this->id ) ) {
193
			$postmeta = get_post_meta( $this->id );
194
		}
195
196
		foreach ( $defaults as $key => $value ) {
197
			// Try to load from meta if an ID is present
198
			if ( ! empty( $this->id ) ) {
199
				/**
200
				 * By not calling `get_post_meta()` individually, we may be breaking compatibility with.
201
				 * some plugins that filter on `get_post_metadata` and erroneously override based solely.
202
				 * on $meta_key -- but don't override when querying for all as $meta_key is empty().
203
				 */
204
				$this->$key = isset( $postmeta[ $key ] ) ? maybe_unserialize( array_shift( $postmeta[ $key ] ) ) : '';
205
			} else {
206
				$this->$key = ! empty( $data[ $key ] ) ? wc_clean( $data[ $key ] ) : '';
207
208
				// Backwards compat field names @deprecated
209
				if ( 'coupon_amount' === $key ) {
210
					$this->coupon_amount = ! empty( $data[ 'amount' ] ) ? wc_clean( $data[ 'amount' ] ) : $this->coupon_amount;
211
				} elseif ( 'discount_type' === $key ) {
212
					$this->discount_type = ! empty( $data[ 'type' ] ) ? wc_clean( $data[ 'type' ] ) : $this->discount_type;
213
				}
214
			}
215
216
			if ( empty( $this->$key ) ) {
217
				$this->$key = $value;
218
			} elseif ( in_array( $key, array( 'product_ids', 'exclude_product_ids', 'product_categories', 'exclude_product_categories', 'customer_email' ) ) ) {
219
				$this->$key = $this->format_array( $this->$key );
220
			} elseif ( in_array( $key, array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items', 'usage_count' ) ) ) {
221
				$this->$key = absint( $this->$key );
222
			} elseif( 'expiry_date' === $key ) {
223
				$this->expiry_date = $this->expiry_date && ! is_numeric( $this->expiry_date ) ? strtotime( $this->expiry_date ) : $this->expiry_date;
224
			}
225
		}
226
227
		do_action( 'woocommerce_coupon_loaded', $this );
228
	}
229
230
	/**
231
	 * Format loaded data as array.
232
	 * @param  string|array $array
233
	 * @return array
234
	 */
235
	public function format_array( $array ) {
236
		if ( ! is_array( $array ) ) {
237
			if ( is_serialized( $array ) ) {
238
				$array = maybe_unserialize( $array );
239
			} else {
240
				$array = explode( ',', $array );
241
			}
242
		}
243
		return array_filter( array_map( 'trim', array_map( 'strtolower', $array ) ) );
244
	}
245
246
	/**
247
	 * Check if coupon needs applying before tax.
248
	 *
249
	 * @return bool
250
	 */
251
	public function apply_before_tax() {
252
		return true;
253
	}
254
255
	/**
256
	 * Check if a coupon enables free shipping.
257
	 *
258
	 * @return bool
259
	 */
260
	public function enable_free_shipping() {
261
		return 'yes' === $this->free_shipping;
262
	}
263
264
	/**
265
	 * Check if a coupon excludes sale items.
266
	 *
267
	 * @return bool
268
	 */
269
	public function exclude_sale_items() {
270
		return 'yes' === $this->exclude_sale_items;
271
	}
272
273
	/**
274
	 * Increase usage count for current coupon.
275
	 *
276
	 * @param string $used_by Either user ID or billing email
277
	 */
278
	public function inc_usage_count( $used_by = '' ) {
279
		if ( $this->id ) {
280
			$this->usage_count++;
281
			update_post_meta( $this->id, 'usage_count', $this->usage_count );
282
283
			if ( $used_by ) {
284
				add_post_meta( $this->id, '_used_by', strtolower( $used_by ) );
285
			}
286
		}
287
	}
288
289
	/**
290
	 * Decrease usage count for current coupon.
291
	 *
292
	 * @param string $used_by Either user ID or billing email
293
	 */
294
	public function dcr_usage_count( $used_by = '' ) {
295
		if ( $this->id && $this->usage_count > 0 ) {
296
			global $wpdb;
297
			$this->usage_count--;
298
			update_post_meta( $this->id, 'usage_count', $this->usage_count );
299
300
			if ( $used_by ) {
301
				/**
302
				 * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
303
				 * all instances where the key and value match, and we only want to delete one.
304
				 */
305
				$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 ) );
306
				if ( $meta_id ) {
307
					delete_metadata_by_mid( 'post', $meta_id );
308
				}
309
			}
310
		}
311
	}
312
313
	/**
314
	 * Get records of all users who have used the current coupon.
315
	 *
316
	 * @access public
317
	 * @return array
318
	 */
319
	public function get_used_by() {
320
		$_used_by = (array) get_post_meta( $this->id, '_used_by' );
321
		// Strip out any null values.
322
		return array_filter( $_used_by );
323
	}
324
325
	/**
326
	 * Returns the error_message string.
327
	 *
328
	 * @access public
329
	 * @return string
330
	 */
331
	public function get_error_message() {
332
		return $this->error_message;
333
	}
334
335
	/**
336
	 * Ensure coupon exists or throw exception.
337
	 *
338
	 * @throws Exception
339
	 */
340
	private function validate_exists() {
341
		if ( ! $this->exists ) {
342
			throw new Exception( self::E_WC_COUPON_NOT_EXIST );
343
		}
344
	}
345
346
	/**
347
	 * Ensure coupon usage limit is valid or throw exception.
348
	 *
349
	 * @throws Exception
350
	 */
351
	private function validate_usage_limit() {
352
		if ( $this->usage_limit > 0 && $this->usage_count >= $this->usage_limit ) {
353
			throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
354
		}
355
	}
356
357
	/**
358
	 * Ensure coupon user usage limit is valid or throw exception.
359
	 *
360
	 * Per user usage limit - check here if user is logged in (against user IDs).
361
	 * Checked again for emails later on in WC_Cart::check_customer_coupons().
362
	 *
363
	 * @param  int  $user_id
364
	 * @throws Exception
365
	 */
366
	private function validate_user_usage_limit( $user_id = 0 ) {
367
		if ( empty( $user_id ) ) {
368
			$user_id = get_current_user_id();
369
		}
370
		if ( $this->usage_limit_per_user > 0 && is_user_logged_in() && $this->id ) {
371
			global $wpdb;
372
			$usage_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;", $this->id, $user_id ) );
373
374
			if ( $usage_count >= $this->usage_limit_per_user ) {
375
				throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
376
			}
377
		}
378
	}
379
380
	/**
381
	 * Ensure coupon date is valid or throw exception.
382
	 *
383
	 * @throws Exception
384
	 */
385
	private function validate_expiry_date() {
386
		if ( $this->expiry_date && current_time( 'timestamp' ) > $this->expiry_date ) {
387
			throw new Exception( $error_code = self::E_WC_COUPON_EXPIRED );
388
		}
389
	}
390
391
	/**
392
	 * Ensure coupon amount is valid or throw exception.
393
	 *
394
	 * @throws Exception
395
	 */
396
	private function validate_minimum_amount() {
397
		if ( $this->minimum_amount > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', wc_format_decimal( $this->minimum_amount ) > WC()->cart->get_displayed_subtotal(), $this ) ) {
398
			throw new Exception( self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET );
399
		}
400
	}
401
402
	/**
403
	 * Ensure coupon amount is valid or throw exception.
404
	 *
405
	 * @throws Exception
406
	 */
407
	private function validate_maximum_amount() {
408
		if ( $this->maximum_amount > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', wc_format_decimal( $this->maximum_amount ) < WC()->cart->get_displayed_subtotal(), $this ) ) {
409
			throw new Exception( self::E_WC_COUPON_MAX_SPEND_LIMIT_MET );
410
		}
411
	}
412
413
	/**
414
	 * Ensure coupon is valid for products in the cart is valid or throw exception.
415
	 *
416
	 * @throws Exception
417
	 */
418 View Code Duplication
	private function validate_product_ids() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
419
		if ( sizeof( $this->product_ids ) > 0 ) {
420
			$valid_for_cart = false;
421
			if ( ! WC()->cart->is_empty() ) {
422
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
423
					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 ) ) {
424
						$valid_for_cart = true;
425
					}
426
				}
427
			}
428
			if ( ! $valid_for_cart ) {
429
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
430
			}
431
		}
432
	}
433
434
	/**
435
	 * Ensure coupon is valid for product categories in the cart is valid or throw exception.
436
	 *
437
	 * @throws Exception
438
	 */
439 View Code Duplication
	private function validate_product_categories() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
440
		if ( sizeof( $this->product_categories ) > 0 ) {
441
			$valid_for_cart = false;
442
			if ( ! WC()->cart->is_empty() ) {
443
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
444
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
445
446
					// If we find an item with a cat in our allowed cat list, the coupon is valid
447
					if ( sizeof( array_intersect( $product_cats, $this->product_categories ) ) > 0 ) {
448
						$valid_for_cart = true;
449
					}
450
				}
451
			}
452
			if ( ! $valid_for_cart ) {
453
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
454
			}
455
		}
456
	}
457
458
	/**
459
	 * Ensure coupon is valid for sale items in the cart is valid or throw exception.
460
	 *
461
	 * @throws Exception
462
	 */
463
	private function validate_sale_items() {
464
		if ( 'yes' === $this->exclude_sale_items && $this->is_type( wc_get_product_coupon_types() ) ) {
0 ignored issues
show
Documentation introduced by
wc_get_product_coupon_types() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
465
			$valid_for_cart      = false;
466
			$product_ids_on_sale = wc_get_product_ids_on_sale();
467
468 View Code Duplication
			if ( ! WC()->cart->is_empty() ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
469
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
470
					if ( ! empty( $cart_item['variation_id'] ) ) {
471
						if ( ! in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
472
							$valid_for_cart = true;
473
						}
474
					} elseif ( ! in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
475
						$valid_for_cart = true;
476
					}
477
				}
478
			}
479
			if ( ! $valid_for_cart ) {
480
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
481
			}
482
		}
483
	}
484
485
	/**
486
	 * All exclusion rules must pass at the same time for a product coupon to be valid.
487
	 */
488
	private function validate_excluded_items() {
489
		if ( ! WC()->cart->is_empty() ) {
490
			$valid = false;
491
492
			foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
493
				if ( $this->is_valid_for_product( $cart_item['data'], $cart_item ) ) {
494
					$valid = true;
495
					break;
496
				}
497
			}
498
499
			if ( ! $valid ) {
500
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
501
			}
502
		}
503
	}
504
505
	/**
506
	 * Cart discounts cannot be added if non-eligble product is found in cart.
507
	 */
508
	private function validate_cart_excluded_items() {
509
		if ( ! $this->is_type( wc_get_product_coupon_types() ) ) {
0 ignored issues
show
Documentation introduced by
wc_get_product_coupon_types() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
510
			$this->validate_cart_excluded_product_ids();
511
			$this->validate_cart_excluded_product_categories();
512
			$this->validate_cart_excluded_sale_items();
513
		}
514
	}
515
516
	/**
517
	 * Exclude products from cart.
518
	 *
519
	 * @throws Exception
520
	 */
521 View Code Duplication
	private function validate_cart_excluded_product_ids() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
522
		// Exclude Products
523
		if ( sizeof( $this->exclude_product_ids ) > 0 ) {
524
			$valid_for_cart = true;
525
			if ( ! WC()->cart->is_empty() ) {
526
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
527
					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 ) ) {
528
						$valid_for_cart = false;
529
					}
530
				}
531
			}
532
			if ( ! $valid_for_cart ) {
533
				throw new Exception( self::E_WC_COUPON_EXCLUDED_PRODUCTS );
534
			}
535
		}
536
	}
537
538
	/**
539
	 * Exclude categories from cart.
540
	 *
541
	 * @throws Exception
542
	 */
543 View Code Duplication
	private function validate_cart_excluded_product_categories() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
544
		if ( sizeof( $this->exclude_product_categories ) > 0 ) {
545
			$valid_for_cart = true;
546
			if ( ! WC()->cart->is_empty() ) {
547
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
548
549
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
550
551
					if ( sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) {
552
						$valid_for_cart = false;
553
					}
554
				}
555
			}
556
			if ( ! $valid_for_cart ) {
557
				throw new Exception( self::E_WC_COUPON_EXCLUDED_CATEGORIES );
558
			}
559
		}
560
	}
561
562
	/**
563
	 * Exclude sale items from cart.
564
	 *
565
	 * @throws Exception
566
	 */
567
	private function validate_cart_excluded_sale_items() {
568
		if ( $this->exclude_sale_items == 'yes' ) {
569
			$valid_for_cart = true;
570
			$product_ids_on_sale = wc_get_product_ids_on_sale();
571 View Code Duplication
			if ( ! WC()->cart->is_empty() ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
572
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
573
					if ( ! empty( $cart_item['variation_id'] ) ) {
574
						if ( in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
575
							$valid_for_cart = false;
576
						}
577
					} elseif ( in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
578
						$valid_for_cart = false;
579
					}
580
				}
581
			}
582
			if ( ! $valid_for_cart ) {
583
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
584
			}
585
		}
586
	}
587
588
	/**
589
	 * Check if a coupon is valid.
590
	 *
591
	 * @return boolean validity
592
	 * @throws Exception
593
	 */
594
	public function is_valid() {
595
		try {
596
			$this->validate_exists();
597
			$this->validate_usage_limit();
598
			$this->validate_user_usage_limit();
599
			$this->validate_expiry_date();
600
			$this->validate_minimum_amount();
601
			$this->validate_maximum_amount();
602
			$this->validate_product_ids();
603
			$this->validate_product_categories();
604
			$this->validate_sale_items();
605
			$this->validate_excluded_items();
606
			$this->validate_cart_excluded_items();
607
608
			if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $this ) ) {
609
				throw new Exception( self::E_WC_COUPON_INVALID_FILTERED );
610
			}
611
		} catch ( Exception $e ) {
612
			$this->error_message = $this->get_coupon_error( $e->getMessage() );
613
			return false;
614
		}
615
616
		return true;
617
	}
618
619
	/**
620
	 * Check if a coupon is valid.
621
	 *
622
	 * @return bool
623
	 */
624
	public function is_valid_for_cart() {
625
		return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $this->is_type( wc_get_cart_coupon_types() ), $this );
0 ignored issues
show
Documentation introduced by
wc_get_cart_coupon_types() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
626
	}
627
628
	/**
629
	 * Check if a coupon is valid for a product.
630
	 *
631
	 * @param  WC_Product  $product
632
	 * @return boolean
633
	 */
634
	public function is_valid_for_product( $product, $values = array() ) {
635
		if ( ! $this->is_type( wc_get_product_coupon_types() ) ) {
0 ignored issues
show
Documentation introduced by
wc_get_product_coupon_types() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
636
			return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values );
637
		}
638
639
		$valid        = false;
640
		$product_cats = wc_get_product_cat_ids( $product->id );
641
		$product_ids  = array( $product->id, ( isset( $product->variation_id ) ? $product->variation_id : 0 ), $product->get_parent() );
642
643
		// Specific products get the discount
644 View Code Duplication
		if ( sizeof( $this->product_ids ) && sizeof( array_intersect( $product_ids, $this->product_ids ) ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
645
			$valid = true;
646
		}
647
648
		// Category discounts
649
		if ( sizeof( $this->product_categories ) && sizeof( array_intersect( $product_cats, $this->product_categories ) ) ) {
650
			$valid = true;
651
		}
652
653
		// No product ids - all items discounted
654
		if ( ! sizeof( $this->product_ids ) && ! sizeof( $this->product_categories ) ) {
655
			$valid = true;
656
		}
657
658
		// Specific product ID's excluded from the discount
659 View Code Duplication
		if ( sizeof( $this->exclude_product_ids ) && sizeof( array_intersect( $product_ids, $this->exclude_product_ids ) ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
660
			$valid = false;
661
		}
662
663
		// Specific categories excluded from the discount
664
		if ( sizeof( $this->exclude_product_categories ) && sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) ) {
665
			$valid = false;
666
		}
667
668
		// Sale Items excluded from discount
669
		if ( 'yes' === $this->exclude_sale_items ) {
670
			$product_ids_on_sale = wc_get_product_ids_on_sale();
671
672
			if ( isset( $product->variation_id ) ) {
673
				if ( in_array( $product->variation_id, $product_ids_on_sale, true ) ) {
674
					$valid = false;
675
				}
676
			} elseif ( in_array( $product->id, $product_ids_on_sale, true ) ) {
677
				$valid = false;
678
			}
679
		}
680
681
		return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values );
682
	}
683
684
	/**
685
	 * Get discount amount for a cart item.
686
	 *
687
	 * @param  float $discounting_amount Amount the coupon is being applied to
688
	 * @param  array|null $cart_item Cart item being discounted if applicable
689
	 * @param  boolean $single True if discounting a single qty item, false if its the line
690
	 * @return float Amount this coupon has discounted
691
	 */
692
	public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) {
693
		$discount      = 0;
694
		$cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity'];
695
696
		if ( $this->is_type( array( 'percent_product', 'percent' ) ) ) {
0 ignored issues
show
Documentation introduced by
array('percent_product', 'percent') is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
697
			$discount = $this->coupon_amount * ( $discounting_amount / 100 );
698
699
		} elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) {
700
			/**
701
			 * This is the most complex discount - we need to divide the discount between rows based on their price in.
702
			 * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows.
703
			 * with no price (free) don't get discounted.
704
			 *
705
			 * Get item discount by dividing item cost by subtotal to get a %.
706
			 *
707
			 * Uses price inc tax if prices include tax to work around https://github.com/woothemes/woocommerce/issues/7669 and https://github.com/woothemes/woocommerce/issues/8074.
708
			 */
709
			if ( wc_prices_include_tax() ) {
710
				$discount_percent = ( $cart_item['data']->get_price_including_tax() * $cart_item_qty ) / WC()->cart->subtotal;
711
			} else {
712
				$discount_percent = ( $cart_item['data']->get_price_excluding_tax() * $cart_item_qty ) / WC()->cart->subtotal_ex_tax;
713
			}
714
			$discount         = ( $this->coupon_amount * $discount_percent ) / $cart_item_qty;
715
716
		} elseif ( $this->is_type( 'fixed_product' ) ) {
717
			$discount = min( $this->coupon_amount, $discounting_amount );
718
			$discount = $single ? $discount : $discount * $cart_item_qty;
719
		}
720
721
		$discount = min( $discount, $discounting_amount );
722
723
		// Handle the limit_usage_to_x_items option
724
		if ( $this->is_type( array( 'percent_product', 'fixed_product' ) ) ) {
0 ignored issues
show
Documentation introduced by
array('percent_product', 'fixed_product') is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
725
			if ( $discounting_amount ) {
726
				if ( '' === $this->limit_usage_to_x_items ) {
727
					$limit_usage_qty = $cart_item_qty;
728
				} else {
729
					$limit_usage_qty              = min( $this->limit_usage_to_x_items, $cart_item_qty );
730
					$this->limit_usage_to_x_items = max( 0, $this->limit_usage_to_x_items - $limit_usage_qty );
731
				}
732
				if ( $single ) {
733
					$discount = ( $discount * $limit_usage_qty ) / $cart_item_qty;
734
				} else {
735
					$discount = ( $discount / $cart_item_qty ) * $limit_usage_qty;
736
				}
737
			}
738
		}
739
740
		$discount = wc_cart_round_discount( $discount, WC_ROUNDING_PRECISION );
741
742
		return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this );
743
	}
744
745
	/**
746
	 * Converts one of the WC_Coupon message/error codes to a message string and.
747
	 * displays the message/error.
748
	 *
749
	 * @param int $msg_code Message/error code.
750
	 */
751
	public function add_coupon_message( $msg_code ) {
752
753
		$msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code );
754
755
		if ( ! $msg ) {
756
			return;
757
		}
758
759
		if ( $msg_code < 200 ) {
760
			wc_add_notice( $msg, 'error' );
761
		} else {
762
			wc_add_notice( $msg );
763
		}
764
	}
765
766
	/**
767
	 * Map one of the WC_Coupon message codes to a message string.
768
	 *
769
	 * @param integer $msg_code
770
	 * @return string| Message/error string
771
	 */
772 View Code Duplication
	public function get_coupon_message( $msg_code ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
773
		switch ( $msg_code ) {
774
			case self::WC_COUPON_SUCCESS :
775
				$msg = __( 'Coupon code applied successfully.', 'woocommerce' );
776
			break;
777
			case self::WC_COUPON_REMOVED :
778
				$msg = __( 'Coupon code removed successfully.', 'woocommerce' );
779
			break;
780
			default:
781
				$msg = '';
782
			break;
783
		}
784
		return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this );
785
	}
786
787
	/**
788
	 * Map one of the WC_Coupon error codes to a message string.
789
	 *
790
	 * @param int $err_code Message/error code.
791
	 * @return string| Message/error string
792
	 */
793
	public function get_coupon_error( $err_code ) {
794
		switch ( $err_code ) {
795
			case self::E_WC_COUPON_INVALID_FILTERED:
796
				$err = __( 'Coupon is not valid.', 'woocommerce' );
797
			break;
798
			case self::E_WC_COUPON_NOT_EXIST:
799
				$err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), $this->code );
800
			break;
801
			case self::E_WC_COUPON_INVALID_REMOVED:
802
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), $this->code );
803
			break;
804
			case self::E_WC_COUPON_NOT_YOURS_REMOVED:
805
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), $this->code );
806
			break;
807
			case self::E_WC_COUPON_ALREADY_APPLIED:
808
				$err = __( 'Coupon code already applied!', 'woocommerce' );
809
			break;
810
			case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY:
811
				$err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), $this->code );
812
			break;
813
			case self::E_WC_COUPON_USAGE_LIMIT_REACHED:
814
				$err = __( 'Coupon usage limit has been reached.', 'woocommerce' );
815
			break;
816
			case self::E_WC_COUPON_EXPIRED:
817
				$err = __( 'This coupon has expired.', 'woocommerce' );
818
			break;
819
			case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET:
820
				$err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->minimum_amount ) );
821
			break;
822
			case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET:
823
				$err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->maximum_amount ) );
824
			break;
825
			case self::E_WC_COUPON_NOT_APPLICABLE:
826
				$err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' );
827
			break;
828
			case self::E_WC_COUPON_EXCLUDED_PRODUCTS:
829
				// Store excluded products that are in cart in $products
830
				$products = array();
831
				if ( ! WC()->cart->is_empty() ) {
832
					foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
833
						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 ) ) {
834
							$products[] = $cart_item['data']->get_title();
835
						}
836
					}
837
				}
838
839
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) );
840
				break;
841
			case self::E_WC_COUPON_EXCLUDED_CATEGORIES:
842
				// Store excluded categories that are in cart in $categories
843
				$categories = array();
844
				if ( ! WC()->cart->is_empty() ) {
845
					foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
846
						$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
847
848
						if ( sizeof( $intersect = array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) {
849
850
							foreach( $intersect as $cat_id) {
851
								$cat = get_term( $cat_id, 'product_cat' );
852
								$categories[] = $cat->name;
853
							}
854
						}
855
					}
856
				}
857
858
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) );
859
				break;
860
			case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS:
861
				$err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' );
862
			break;
863
			default:
864
				$err = '';
865
			break;
866
		}
867
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this );
868
	}
869
870
	/**
871
	 * Map one of the WC_Coupon error codes to an error string.
872
	 * No coupon instance will be available where a coupon does not exist,
873
	 * so this static method exists.
874
	 *
875
	 * @param int $err_code Error code
876
	 * @return string| Error string
877
	 */
878 View Code Duplication
	public static function get_generic_coupon_error( $err_code ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
879
		switch ( $err_code ) {
880
			case self::E_WC_COUPON_NOT_EXIST:
881
				$err = __( 'Coupon does not exist!', 'woocommerce' );
882
			break;
883
			case self::E_WC_COUPON_PLEASE_ENTER:
884
				$err = __( 'Please enter a coupon code.', 'woocommerce' );
885
			break;
886
			default:
887
				$err = '';
888
			break;
889
		}
890
		// When using this static method, there is no $this to pass to filter
891
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null );
892
	}
893
}
894