WC_Coupon::is_valid_for_product()   F
last analyzed

Complexity

Conditions 22
Paths 811

Size

Total Lines 56
Code Lines 27

Duplication

Lines 20
Ratio 35.71 %

Importance

Changes 0
Metric Value
cc 22
dl 20
loc 56
rs 3.5714
c 0
b 0
f 0
eloc 27
nc 811
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 View Code Duplication
		if ( $this->minimum_amount > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', wc_format_decimal( $this->minimum_amount ) > WC()->cart->get_displayed_subtotal(), $this ) ) {
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...
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 View Code Duplication
		if ( $this->maximum_amount > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', wc_format_decimal( $this->maximum_amount ) <= WC()->cart->get_displayed_subtotal(), $this ) ) {
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...
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 product categories in the cart is valid or throw exception.
460
	 *
461
	 * @throws Exception
462
	 */
463 View Code Duplication
	private function validate_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...
464
		if ( sizeof( $this->exclude_product_categories ) > 0 ) {
465
			$valid_for_cart = false;
466
			if ( ! WC()->cart->is_empty() ) {
467
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
468
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
469
470
					// If we find an item with a cat NOT in our disallowed cat list, the coupon is valid
471
					if ( empty( $product_cats ) || sizeof( array_diff( $product_cats, $this->exclude_product_categories ) ) > 0 ) {
472
						$valid_for_cart = true;
473
					}
474
				}
475
			}
476
			if ( ! $valid_for_cart ) {
477
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
478
			}
479
		}
480
	}
481
482
	/**
483
	 * Ensure coupon is valid for sale items in the cart is valid or throw exception.
484
	 *
485
	 * @throws Exception
486
	 */
487
	private function validate_sale_items() {
488
		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...
489
			$valid_for_cart      = false;
490
			$product_ids_on_sale = wc_get_product_ids_on_sale();
491
492
			if ( ! WC()->cart->is_empty() ) {
493
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
494
					if ( ! empty( $cart_item['variation_id'] ) ) {
495
						if ( ! in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
496
							$valid_for_cart = true;
497
						}
498
					} elseif ( ! in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
499
						$valid_for_cart = true;
500
					}
501
				}
502
			}
503
			if ( ! $valid_for_cart ) {
504
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
505
			}
506
		}
507
	}
508
509
	/**
510
	 * Cart discounts cannot be added if non-eligble product is found in cart.
511
	 */
512
	private function validate_cart_excluded_items() {
513
		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...
514
			$this->validate_cart_excluded_product_ids();
515
			$this->validate_cart_excluded_product_categories();
516
			$this->validate_cart_excluded_sale_items();
517
		}
518
	}
519
520
	/**
521
	 * Exclude products from cart.
522
	 *
523
	 * @throws Exception
524
	 */
525 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...
526
		// Exclude Products
527
		if ( sizeof( $this->exclude_product_ids ) > 0 ) {
528
			$valid_for_cart = true;
529
			if ( ! WC()->cart->is_empty() ) {
530
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
531
					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 ) ) {
532
						$valid_for_cart = false;
533
					}
534
				}
535
			}
536
			if ( ! $valid_for_cart ) {
537
				throw new Exception( self::E_WC_COUPON_EXCLUDED_PRODUCTS );
538
			}
539
		}
540
	}
541
542
	/**
543
	 * Exclude categories from cart.
544
	 *
545
	 * @throws Exception
546
	 */
547 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...
548
		if ( sizeof( $this->exclude_product_categories ) > 0 ) {
549
			$valid_for_cart = true;
550
			if ( ! WC()->cart->is_empty() ) {
551
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
552
553
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
554
555
					if ( sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) {
556
						$valid_for_cart = false;
557
					}
558
				}
559
			}
560
			if ( ! $valid_for_cart ) {
561
				throw new Exception( self::E_WC_COUPON_EXCLUDED_CATEGORIES );
562
			}
563
		}
564
	}
565
566
	/**
567
	 * Exclude sale items from cart.
568
	 *
569
	 * @throws Exception
570
	 */
571
	private function validate_cart_excluded_sale_items() {
572
		if ( $this->exclude_sale_items == 'yes' ) {
573
			$valid_for_cart = true;
574
			$product_ids_on_sale = wc_get_product_ids_on_sale();
575
			if ( ! WC()->cart->is_empty() ) {
576
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
577
					if ( ! empty( $cart_item['variation_id'] ) ) {
578
						if ( in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
579
							$valid_for_cart = false;
580
						}
581
					} elseif ( in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
582
						$valid_for_cart = false;
583
					}
584
				}
585
			}
586
			if ( ! $valid_for_cart ) {
587
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
588
			}
589
		}
590
	}
591
592
	/**
593
	 * Check if a coupon is valid.
594
	 *
595
	 * @return boolean validity
596
	 * @throws Exception
597
	 */
598
	public function is_valid() {
599
		try {
600
			$this->validate_exists();
601
			$this->validate_usage_limit();
602
			$this->validate_user_usage_limit();
603
			$this->validate_expiry_date();
604
			$this->validate_minimum_amount();
605
			$this->validate_maximum_amount();
606
			$this->validate_product_ids();
607
			$this->validate_product_categories();
608
			$this->validate_excluded_product_categories();
609
			$this->validate_sale_items();
610
			$this->validate_cart_excluded_items();
611
612
			if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $this ) ) {
613
				throw new Exception( self::E_WC_COUPON_INVALID_FILTERED );
614
			}
615
		} catch ( Exception $e ) {
616
			$this->error_message = $this->get_coupon_error( $e->getMessage() );
617
			return false;
618
		}
619
620
		return true;
621
	}
622
623
	/**
624
	 * Check if a coupon is valid.
625
	 *
626
	 * @return bool
627
	 */
628
	public function is_valid_for_cart() {
629
		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...
630
	}
631
632
	/**
633
	 * Check if a coupon is valid for a product.
634
	 *
635
	 * @param  WC_Product  $product
636
	 * @return boolean
637
	 */
638
	public function is_valid_for_product( $product, $values = array() ) {
639
		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...
640
			return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values );
641
		}
642
643
		$valid        = false;
644
		$product_cats = wc_get_product_cat_ids( $product->id );
645
646
		// Specific products get the discount
647 View Code Duplication
		if ( sizeof( $this->product_ids ) > 0 ) {
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...
648
			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 ) ) {
649
				$valid = true;
650
			}
651
		}
652
653
		// Category discounts
654 View Code Duplication
		if ( sizeof( $this->product_categories ) > 0 ) {
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...
655
			if ( sizeof( array_intersect( $product_cats, $this->product_categories ) ) > 0 ) {
656
				$valid = true;
657
			}
658
		}
659
660
		if ( ! sizeof( $this->product_ids ) && ! sizeof( $this->product_categories ) ) {
661
			// No product ids - all items discounted
662
			$valid = true;
663
		}
664
665
		// Specific product ID's excluded from the discount
666 View Code Duplication
		if ( sizeof( $this->exclude_product_ids ) > 0 ) {
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...
667
			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 ) ) {
668
				$valid = false;
669
			}
670
		}
671
672
		// Specific categories excluded from the discount
673 View Code Duplication
		if ( sizeof( $this->exclude_product_categories ) > 0 ) {
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...
674
			if ( sizeof( array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) {
675
				$valid = false;
676
			}
677
		}
678
679
		// Sale Items excluded from discount
680
		if ( $this->exclude_sale_items == 'yes' ) {
681
			$product_ids_on_sale = wc_get_product_ids_on_sale();
682
683
			if ( isset( $product->variation_id ) ) {
684
				if ( in_array( $product->variation_id, $product_ids_on_sale, true ) ) {
685
					$valid = false;
686
				}
687
			} elseif ( in_array( $product->id, $product_ids_on_sale, true ) ) {
688
				$valid = false;
689
			}
690
		}
691
692
		return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values );
693
	}
694
695
	/**
696
	 * Get discount amount for a cart item.
697
	 *
698
	 * @param  float $discounting_amount Amount the coupon is being applied to
699
	 * @param  array|null $cart_item Cart item being discounted if applicable
700
	 * @param  boolean $single True if discounting a single qty item, false if its the line
701
	 * @return float Amount this coupon has discounted
702
	 */
703
	public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) {
704
		$discount      = 0;
705
		$cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity'];
706
707
		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...
708
			$discount = $this->coupon_amount * ( $discounting_amount / 100 );
709
710
		} elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) {
711
			/**
712
			 * This is the most complex discount - we need to divide the discount between rows based on their price in.
713
			 * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows.
714
			 * with no price (free) don't get discounted.
715
			 *
716
			 * Get item discount by dividing item cost by subtotal to get a %.
717
			 *
718
			 * 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.
719
			 */
720
			if ( wc_prices_include_tax() ) {
721
				$discount_percent = ( $cart_item['data']->get_price_including_tax() * $cart_item_qty ) / WC()->cart->subtotal;
722
			} else {
723
				$discount_percent = ( $cart_item['data']->get_price_excluding_tax() * $cart_item_qty ) / WC()->cart->subtotal_ex_tax;
724
			}
725
			$discount         = ( $this->coupon_amount * $discount_percent ) / $cart_item_qty;
726
727
		} elseif ( $this->is_type( 'fixed_product' ) ) {
728
			$discount = min( $this->coupon_amount, $discounting_amount );
729
			$discount = $single ? $discount : $discount * $cart_item_qty;
730
		}
731
732
		$discount = min( $discount, $discounting_amount );
733
734
		// Handle the limit_usage_to_x_items option
735
		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...
736
			if ( $discounting_amount ) {
737
				if ( '' === $this->limit_usage_to_x_items ) {
738
					$limit_usage_qty = $cart_item_qty;
739
				} else {
740
					$limit_usage_qty              = min( $this->limit_usage_to_x_items, $cart_item_qty );
741
					$this->limit_usage_to_x_items = max( 0, $this->limit_usage_to_x_items - $limit_usage_qty );
742
				}
743
				if ( $single ) {
744
					$discount = ( $discount * $limit_usage_qty ) / $cart_item_qty;
745
				} else {
746
					$discount = ( $discount / $cart_item_qty ) * $limit_usage_qty;
747
				}
748
			}
749
		}
750
751
		$discount = wc_cart_round_discount( $discount, WC_ROUNDING_PRECISION );
752
753
		return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this );
754
	}
755
756
	/**
757
	 * Converts one of the WC_Coupon message/error codes to a message string and.
758
	 * displays the message/error.
759
	 *
760
	 * @param int $msg_code Message/error code.
761
	 */
762
	public function add_coupon_message( $msg_code ) {
763
764
		$msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code );
765
766
		if ( ! $msg ) {
767
			return;
768
		}
769
770
		if ( $msg_code < 200 ) {
771
			wc_add_notice( $msg, 'error' );
772
		} else {
773
			wc_add_notice( $msg );
774
		}
775
	}
776
777
	/**
778
	 * Map one of the WC_Coupon message codes to a message string.
779
	 *
780
	 * @param integer $msg_code
781
	 * @return string| Message/error string
782
	 */
783 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...
784
		switch ( $msg_code ) {
785
			case self::WC_COUPON_SUCCESS :
786
				$msg = __( 'Coupon code applied successfully.', 'woocommerce' );
787
			break;
788
			case self::WC_COUPON_REMOVED :
789
				$msg = __( 'Coupon code removed successfully.', 'woocommerce' );
790
			break;
791
			default:
792
				$msg = '';
793
			break;
794
		}
795
		return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this );
796
	}
797
798
	/**
799
	 * Map one of the WC_Coupon error codes to a message string.
800
	 *
801
	 * @param int $err_code Message/error code.
802
	 * @return string| Message/error string
803
	 */
804
	public function get_coupon_error( $err_code ) {
805
		switch ( $err_code ) {
806
			case self::E_WC_COUPON_INVALID_FILTERED:
807
				$err = __( 'Coupon is not valid.', 'woocommerce' );
808
			break;
809
			case self::E_WC_COUPON_NOT_EXIST:
810
				$err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), $this->code );
811
			break;
812
			case self::E_WC_COUPON_INVALID_REMOVED:
813
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), $this->code );
814
			break;
815
			case self::E_WC_COUPON_NOT_YOURS_REMOVED:
816
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), $this->code );
817
			break;
818
			case self::E_WC_COUPON_ALREADY_APPLIED:
819
				$err = __( 'Coupon code already applied!', 'woocommerce' );
820
			break;
821
			case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY:
822
				$err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), $this->code );
823
			break;
824
			case self::E_WC_COUPON_USAGE_LIMIT_REACHED:
825
				$err = __( 'Coupon usage limit has been reached.', 'woocommerce' );
826
			break;
827
			case self::E_WC_COUPON_EXPIRED:
828
				$err = __( 'This coupon has expired.', 'woocommerce' );
829
			break;
830
			case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET:
831
				$err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->minimum_amount ) );
832
			break;
833
			case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET:
834
				$err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->maximum_amount ) );
835
			break;
836
			case self::E_WC_COUPON_NOT_APPLICABLE:
837
				$err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' );
838
			break;
839
			case self::E_WC_COUPON_EXCLUDED_PRODUCTS:
840
				// Store excluded products that are in cart in $products
841
				$products = array();
842
				if ( ! WC()->cart->is_empty() ) {
843
					foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
844
						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 ) ) {
845
							$products[] = $cart_item['data']->get_title();
846
						}
847
					}
848
				}
849
850
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) );
851
				break;
852
			case self::E_WC_COUPON_EXCLUDED_CATEGORIES:
853
				// Store excluded categories that are in cart in $categories
854
				$categories = array();
855
				if ( ! WC()->cart->is_empty() ) {
856
					foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
857
						$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
858
859
						if ( sizeof( $intersect = array_intersect( $product_cats, $this->exclude_product_categories ) ) > 0 ) {
860
861
							foreach( $intersect as $cat_id) {
862
								$cat = get_term( $cat_id, 'product_cat' );
863
								$categories[] = $cat->name;
864
							}
865
						}
866
					}
867
				}
868
869
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) );
870
				break;
871
			case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS:
872
				$err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' );
873
			break;
874
			default:
875
				$err = '';
876
			break;
877
		}
878
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this );
879
	}
880
881
	/**
882
	 * Map one of the WC_Coupon error codes to an error string.
883
	 * No coupon instance will be available where a coupon does not exist,
884
	 * so this static method exists.
885
	 *
886
	 * @param int $err_code Error code
887
	 * @return string| Error string
888
	 */
889 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...
890
		switch ( $err_code ) {
891
			case self::E_WC_COUPON_NOT_EXIST:
892
				$err = __( 'Coupon does not exist!', 'woocommerce' );
893
			break;
894
			case self::E_WC_COUPON_PLEASE_ENTER:
895
				$err = __( 'Please enter a coupon code.', 'woocommerce' );
896
			break;
897
			default:
898
				$err = '';
899
			break;
900
		}
901
		// When using this static method, there is no $this to pass to filter
902
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null );
903
	}
904
}
905