Completed
Pull Request — master (#11785)
by Mike
09:46
created

WC_Coupon::get_date_expires()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
include_once( 'legacy/class-wc-legacy-coupon.php' );
3
4
if ( ! defined( 'ABSPATH' ) ) {
5
	exit;
6
}
7
8
/**
9
 * WooCommerce coupons.
10
 *
11
 * The WooCommerce coupons class gets coupon data from storage and checks coupon validity.
12
 *
13
 * @class 		WC_Coupon
14
 * @version		2.7.0
15
 * @package		WooCommerce/Classes
16
 * @category	Class
17
 * @author		WooThemes
18
 */
19
class WC_Coupon extends WC_Legacy_Coupon {
20
21
	/**
22
	 * Data array, with defaults.
23
	 * @since 2.7.0
24
	 * @var array
25
	 */
26
	protected $_data = array(
27
		'id'                          => 0,
28
		'code'                        => '',
29
		'amount'                      => 0,
30
		'date_created'                => '',
31
		'date_modified'               => '',
32
		'discount_type'               => 'fixed_cart',
33
		'description'                 => '',
34
		'date_expires'                => '',
35
		'usage_count'                 => 0,
36
		'individual_use'              => false,
37
		'product_ids'                 => array(),
38
		'excluded_product_ids'        => array(),
39
		'usage_limit'                 => 0,
40
		'usage_limit_per_user'        => 0,
41
		'limit_usage_to_x_items'      => 0,
42
		'free_shipping'               => false,
43
		'product_categories'          => array(),
44
		'excluded_product_categories' => array(),
45
		'exclude_sale_items'          => false,
46
		'minimum_amount'              => '',
47
		'maximum_amount'              => '',
48
		'email_restrictions'          => array(),
49
		'used_by'                     => '',
50
	);
51
52
	// Coupon message codes
53
	const E_WC_COUPON_INVALID_FILTERED               = 100;
54
	const E_WC_COUPON_INVALID_REMOVED                = 101;
55
	const E_WC_COUPON_NOT_YOURS_REMOVED              = 102;
56
	const E_WC_COUPON_ALREADY_APPLIED                = 103;
57
	const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104;
58
	const E_WC_COUPON_NOT_EXIST                      = 105;
59
	const E_WC_COUPON_USAGE_LIMIT_REACHED            = 106;
60
	const E_WC_COUPON_EXPIRED                        = 107;
61
	const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET        = 108;
62
	const E_WC_COUPON_NOT_APPLICABLE                 = 109;
63
	const E_WC_COUPON_NOT_VALID_SALE_ITEMS           = 110;
64
	const E_WC_COUPON_PLEASE_ENTER                   = 111;
65
	const E_WC_COUPON_MAX_SPEND_LIMIT_MET 			 = 112;
66
	const E_WC_COUPON_EXCLUDED_PRODUCTS              = 113;
67
	const E_WC_COUPON_EXCLUDED_CATEGORIES            = 114;
68
	const WC_COUPON_SUCCESS                          = 200;
69
	const WC_COUPON_REMOVED                          = 201;
70
71
	/**
72
	 * Internal meta type used to store coupon data.
73
	 * @since 2.7.0
74
	 * @var string
75
	 */
76
	protected $_meta_type = 'post';
77
78
	/**
79
	 * Data stored in meta keys, but not considered "meta" for a coupon.
80
	 * @since 2.7.0
81
	 * @var array
82
	 */
83
	protected $_internal_meta_keys = array(
84
		'discount_type', 'coupon_amount', 'expiry_date', 'usage_count',
85
		'individual_use', 'product_ids', 'exclude_product_ids', 'usage_limit',
86
		'usage_limit_per_user', 'limit_usage_to_x_items', 'free_shipping',
87
		'product_categories', 'exclude_product_categories', 'exclude_sale_items',
88
		'minimum_amount', 'maximum_amount', 'customer_email', '_used_by',
89
		'_edit_lock', '_edit_last',
90
	);
91
92
	/**
93
	 * Coupon constructor. Loads coupon data.
94
	 * @param mixed $data Coupon data, object, ID or code.
95
	 */
96
	public function __construct( $data = '' ) {
97
		parent::__construct( $data );
98
99
		if ( $data instanceof WC_Coupon ) {
100
			$this->read( absint( $data->get_id() ) );
101
		} elseif ( $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data ) ) {
102
			_doing_it_wrong( 'woocommerce_get_shop_coupon_data', 'Reading a manual coupon via woocommerce_get_shop_coupon_data has been deprecated. Please sent an instance of WC_Coupon instead.', '2.7' );
103
			$this->read_manual_coupon( $data, $coupon );
104
		} elseif ( is_numeric( $data ) && 'shop_coupon' === get_post_type( $data ) ) {
105
			$this->read( $data );
106
		} elseif ( ! empty( $data ) ) {
107
			$this->set_code( $data );
108
			$this->read( absint( self::get_coupon_id_from_code( $data ) ) );
109
		}
110
	}
111
112
	/**
113
	 * Checks the coupon type.
114
	 * @param  string $type Array or string of types
115
	 * @return bool
116
	 */
117
	public function is_type( $type ) {
118
		return ( $this->get_discount_type() == $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type ) ) );
119
	}
120
121
	/*
122
    |--------------------------------------------------------------------------
123
    | Getters
124
    |--------------------------------------------------------------------------
125
    |
126
    | Methods for getting data from the coupon object.
127
    |
128
    */
129
130
   /**
131
    * Get coupon ID.
132
    * @since  2.7.0
133
    * @return integer
134
    */
135
	public function get_id() {
136
		return $this->_data['id'];
137
	}
138
139
	/**
140
	 * Get coupon code.
141
	 * @since  2.7.0
142
	 * @return string
143
	 */
144
	public function get_code() {
145
		return $this->_data['code'];
146
	}
147
148
	/**
149
	 * Get coupon description.
150
	 * @since  2.7.0
151
	 * @return string
152
	 */
153
	public function get_description() {
154
		return $this->_data['description'];
155
	}
156
157
	/**
158
	 * Get discount type.
159
	 * @since  2.7.0
160
	 * @return string
161
	 */
162
	public function get_discount_type() {
163
		return $this->_data['discount_type'];
164
	}
165
166
	/**
167
	 * Get coupon code.
168
	 * @since  2.7.0
169
	 * @return float
170
	 */
171
	public function get_amount() {
172
		return wc_format_decimal( $this->_data['amount'] );
173
	}
174
175
	/**
176
	 * Get coupon expiration date.
177
	 * @since  2.7.0
178
	 * @return int
179
	 */
180
	public function get_date_expires() {
181
		return $this->_data['date_expires'];
182
	}
183
184
	/**
185
	 * Get date_created
186
	 * @since 2.7.0
187
	 * @return int
188
	 */
189
	public function get_date_created() {
190
		return $this->_data['date_created'];
191
	}
192
193
	/**
194
	 * Get date_modified
195
	 * @since 2.7.0
196
	 * @return int
197
	 */
198
	public function get_date_modified() {
199
		return $this->_data['date_modified'];
200
	}
201
202
	/**
203
	 * Get coupon usage count.
204
	 * @since  2.7.0
205
	 * @return integer
206
	 */
207
	public function get_usage_count() {
208
		return $this->_data['usage_count'];
209
	}
210
211
	/**
212
	 * Get the "indvidual use" checkbox status.
213
	 * @since  2.7.0
214
	 * @return bool
215
	 */
216
	public function get_individual_use() {
217
		return $this->_data['individual_use'];
218
	}
219
220
	/**
221
	 * Get product IDs this coupon can apply to.
222
	 * @since  2.7.0
223
	 * @return array
224
	 */
225
	public function get_product_ids() {
226
		return $this->_data['product_ids'];
227
	}
228
229
	/**
230
	 * Get product IDs that this coupon should not apply to.
231
	 * @since  2.7.0
232
	 * @return array
233
	 */
234
	public function get_excluded_product_ids() {
235
		return $this->_data['excluded_product_ids'];
236
	}
237
238
	/**
239
	 * Get coupon usage limit.
240
	 * @since  2.7.0
241
	 * @return integer
242
	 */
243
	public function get_usage_limit() {
244
		return $this->_data['usage_limit'];
245
	}
246
247
	/**
248
	 * Get coupon usage limit per customer (for a single customer)
249
	 * @since  2.7.0
250
	 * @return integer
251
	 */
252
	public function get_usage_limit_per_user() {
253
		return $this->_data['usage_limit_per_user'];
254
	}
255
256
	/**
257
	 * Usage limited to certain amount of items
258
	 * @since  2.7.0
259
	 * @return integer
260
	 */
261
	public function get_limit_usage_to_x_items() {
262
		return $this->_data['limit_usage_to_x_items'];
263
	}
264
265
	/**
266
	 * If this coupon grants free shipping or not.
267
	 * @since  2.7.0
268
	 * @return bool
269
	 */
270
	public function get_free_shipping() {
271
		return $this->_data['free_shipping'];
272
	}
273
274
	/**
275
	 * Get product categories this coupon can apply to.
276
	 * @since  2.7.0
277
	 * @return array
278
	 */
279
	public function get_product_categories() {
280
		return $this->_data['product_categories'];
281
	}
282
283
	/**
284
	 * Get product categories this coupon cannot not apply to.
285
	 * @since  2.7.0
286
	 * @return array
287
	 */
288
	public function get_excluded_product_categories() {
289
		return $this->_data['excluded_product_categories'];
290
	}
291
292
	/**
293
	 * If this coupon should exclude items on sale.
294
	 * @since  2.7.0
295
	 * @return bool
296
	 */
297
	public function get_exclude_sale_items() {
298
		return $this->_data['exclude_sale_items'];
299
	}
300
301
	/**
302
	 * Get minium spend amount.
303
	 * @since  2.7.0
304
	 * @return float
305
	 */
306
	public function get_minimum_amount() {
307
		return wc_format_decimal( $this->_data['minimum_amount'] );
308
	}
309
	/**
310
	 * Get maximum spend amount.
311
	 * @since  2.7.0
312
	 * @return float
313
	 */
314
	public function get_maximum_amount() {
315
		return wc_format_decimal( $this->_data['maximum_amount'] );
316
	}
317
318
	/**
319
	 * Get emails to check customer usage restrictions.
320
	 * @since  2.7.0
321
	 * @return array
322
	 */
323
	public function get_email_restrictions() {
324
		return $this->_data['email_restrictions'];
325
	}
326
327
	/**
328
	 * Get records of all users who have used the current coupon.
329
	 *
330
	 * @return array
331
	 */
332
	public function get_used_by() {
333
		return $this->_data['used_by'];
334
	}
335
336
	/**
337
	 * Get a coupon ID from it's code.
338
	 * @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.
339
	 * @param  string $code
340
	 * @return int
341
	 */
342
	private function get_coupon_id_from_code( $code ) {
343
		global $wpdb;
344
345
		$coupon_id = wp_cache_get( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, 'coupons' );
346
347
		if ( false === $coupon_id ) {
348
			$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->get_code() );
349
350
			if ( $coupon_id = apply_filters( 'woocommerce_get_coupon_id_from_code', $wpdb->get_var( $sql ), $this->get_code() ) ) {
351
				wp_cache_set( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, $coupon_id, 'coupons' );
352
			}
353
		}
354
		return absint( $coupon_id );
355
	}
356
357
	/**
358
	 * Get discount amount for a cart item.
359
	 *
360
	 * @param  float $discounting_amount Amount the coupon is being applied to
361
	 * @param  array|null $cart_item Cart item being discounted if applicable
362
	 * @param  boolean $single True if discounting a single qty item, false if its the line
363
	 * @return float Amount this coupon has discounted
364
	 */
365
	public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) {
366
		$discount      = 0;
367
		$cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity'];
368
369
		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...
370
			$discount = $this->get_amount() * ( $discounting_amount / 100 );
371
		} elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) {
372
			/**
373
			 * This is the most complex discount - we need to divide the discount between rows based on their price in.
374
			 * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows.
375
			 * with no price (free) don't get discounted.
376
			 *
377
			 * Get item discount by dividing item cost by subtotal to get a %.
378
			 *
379
			 * 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.
380
			 */
381
			if ( wc_prices_include_tax() ) {
382
				$discount_percent = ( $cart_item['data']->get_price_including_tax() * $cart_item_qty ) / WC()->cart->subtotal;
383
			} else {
384
				$discount_percent = ( $cart_item['data']->get_price_excluding_tax() * $cart_item_qty ) / WC()->cart->subtotal_ex_tax;
385
			}
386
			$discount = ( $this->get_amount() * $discount_percent ) / $cart_item_qty;
387
388
		} elseif ( $this->is_type( 'fixed_product' ) ) {
389
			$discount = min( $this->get_amount(), $discounting_amount );
390
			$discount = $single ? $discount : $discount * $cart_item_qty;
391
		}
392
393
		$discount = min( $discount, $discounting_amount );
394
395
		// Handle the limit_usage_to_x_items option
396
		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...
397
			if ( $discounting_amount ) {
398
				if ( '' === $this->get_limit_usage_to_x_items() ) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of '' (string) and $this->get_limit_usage_to_x_items() (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
399
					$limit_usage_qty = $cart_item_qty;
400
				} else {
401
					$limit_usage_qty = min( $this->get_limit_usage_to_x_items(), $cart_item_qty );
402
					$this->set_limit_usage_to_x_items( max( 0, $this->get_limit_usage_to_x_items() - $limit_usage_qty ) );
403
				}
404
				if ( $single ) {
405
					$discount = ( $discount * $limit_usage_qty ) / $cart_item_qty;
406
				} else {
407
					$discount = ( $discount / $cart_item_qty ) * $limit_usage_qty;
408
				}
409
			}
410
		}
411
412
		$discount = round( $discount, wc_get_rounding_precision() );
413
414
		return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this );
415
	}
416
417
	/*
418
	|--------------------------------------------------------------------------
419
	| Setters
420
	|--------------------------------------------------------------------------
421
	|
422
	| Functions for setting coupon data. These should not update anything in the
423
	| database itself and should only change what is stored in the class
424
	| object.
425
	|
426
	*/
427
428
	/**
429
	 * Set ID
430
	 * @param int $value
431
	 * @throws WC_Data_Exception
432
	 */
433
	public function set_id( $value ) {
434
		$this->_data['id'] = absint( $value );
435
	}
436
437
	/**
438
	 * Set coupon code.
439
	 * @since  2.7.0
440
	 * @param  string $code
441
	 * @throws WC_Data_Exception
442
	 */
443
	public function set_code( $code ) {
444
		$this->_data['code'] = apply_filters( 'woocommerce_coupon_code', $code );
445
	}
446
447
	/**
448
	 * Set coupon description.
449
	 * @since  2.7.0
450
	 * @param  string $description
451
	 * @throws WC_Data_Exception
452
	 */
453
	public function set_description( $description ) {
454
		$this->_data['description'] = $description;
455
	}
456
457
	/**
458
	 * Set discount type.
459
	 * @since  2.7.0
460
	 * @param  string $discount_type
461
	 * @throws WC_Data_Exception
462
	 */
463
	public function set_discount_type( $discount_type ) {
464
		if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ) ) ) {
465
			$this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) );
466
		}
467
		$this->_data['discount_type'] = $discount_type;
468
	}
469
470
	/**
471
	 * Set amount.
472
	 * @since  2.7.0
473
	 * @param  float $amount
474
	 * @throws WC_Data_Exception
475
	 */
476
	public function set_amount( $amount ) {
477
		$this->_data['amount'] = wc_format_decimal( $amount );
478
	}
479
480
	/**
481
	 * Set expiration date.
482
	 * @since  2.7.0
483
	 * @param string $timestamp Timestamp
484
	 * @throws WC_Data_Exception
485
	 */
486
	public function set_date_expires( $timestamp ) {
487
		$this->_data['date_expires'] = is_numeric( $timestamp ) ? $timestamp : strtotime( $timestamp );
488
	}
489
490
	/**
491
	 * Set date_created
492
	 * @since  2.7.0
493
	 * @param string $timestamp Timestamp
494
	 * @throws WC_Data_Exception
495
	 */
496
	public function set_date_created( $timestamp ) {
497
		$this->_data['date_created'] = is_numeric( $timestamp ) ? $timestamp : strtotime( $timestamp );
498
	}
499
500
	/**
501
	 * Set date_modified
502
	 * @since  2.7.0
503
	 * @param string $timestamp
504
	 * @throws WC_Data_Exception
505
	 */
506
	public function set_date_modified( $timestamp ) {
507
		$this->_data['date_modified'] = is_numeric( $timestamp ) ? $timestamp : strtotime( $timestamp );
508
	}
509
510
	/**
511
	 * Set how many times this coupon has been used.
512
	 * @since  2.7.0
513
	 * @param  int $usage_count
514
	 * @throws WC_Data_Exception
515
	 */
516
	public function set_usage_count( $usage_count ) {
517
		$this->_data['usage_count'] = absint( $usage_count );
518
	}
519
520
	/**
521
	 * Set if this coupon can only be used once.
522
	 * @since  2.7.0
523
	 * @param  bool $is_individual_use
524
	 * @throws WC_Data_Exception
525
	 */
526
	public function set_individual_use( $is_individual_use ) {
527
		$this->_data['individual_use'] = (bool) $is_individual_use;
528
	}
529
530
	/**
531
	 * Set the product IDs this coupon can be used with.
532
	 * @since  2.7.0
533
	 * @param  array $product_ids
534
	 * @throws WC_Data_Exception
535
	 */
536
	public function set_product_ids( $product_ids ) {
537
		$this->_data['product_ids'] = (array) $product_ids;
538
	}
539
540
	/**
541
	 * Set the product IDs this coupon cannot be used with.
542
	 * @since  2.7.0
543
	 * @param  array $excluded_product_ids
544
	 * @throws WC_Data_Exception
545
	 */
546
	public function set_excluded_product_ids( $excluded_product_ids ) {
547
		$this->_data['excluded_product_ids'] = (array) $excluded_product_ids;
548
	}
549
550
	/**
551
	 * Set the amount of times this coupon can be used.
552
	 * @since  2.7.0
553
	 * @param  int $usage_limit
554
	 * @throws WC_Data_Exception
555
	 */
556
	public function set_usage_limit( $usage_limit ) {
557
		$this->_data['usage_limit'] = absint( $usage_limit );
558
	}
559
560
	/**
561
	 * Set the amount of times this coupon can be used per user.
562
	 * @since  2.7.0
563
	 * @param  int $usage_limit
564
	 * @throws WC_Data_Exception
565
	 */
566
	public function set_usage_limit_per_user( $usage_limit ) {
567
		$this->_data['usage_limit_per_user'] = absint( $usage_limit );
568
	}
569
570
	/**
571
	 * Set usage limit to x number of items.
572
	 * @since  2.7.0
573
	 * @param  int $limit_usage_to_x_items
574
	 * @throws WC_Data_Exception
575
	 */
576
	public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) {
577
		$this->_data['limit_usage_to_x_items'] = $limit_usage_to_x_items;
578
	}
579
580
	/**
581
	 * Set if this coupon enables free shipping or not.
582
	 * @since  2.7.0
583
	 * @param  bool $free_shipping
584
	 * @throws WC_Data_Exception
585
	 */
586
	public function set_free_shipping( $free_shipping ) {
587
		$this->_data['free_shipping'] = (bool) $free_shipping;
588
	}
589
590
	/**
591
	 * Set the product category IDs this coupon can be used with.
592
	 * @since  2.7.0
593
	 * @param  array $product_categories
594
	 * @throws WC_Data_Exception
595
	 */
596
	public function set_product_categories( $product_categories ) {
597
		$this->_data['product_categories'] = (array) $product_categories;
598
	}
599
600
	/**
601
	 * Set the product category IDs this coupon cannot be used with.
602
	 * @since  2.7.0
603
	 * @param  array $excluded_product_categories
604
	 * @throws WC_Data_Exception
605
	 */
606
	public function set_excluded_product_categories( $excluded_product_categories ) {
607
		$this->_data['excluded_product_categories'] = (array) $excluded_product_categories;
608
	}
609
610
	/**
611
	 * Set if this coupon should excluded sale items or not.
612
	 * @since  2.7.0
613
	 * @param  bool $exclude_sale_items
614
	 * @throws WC_Data_Exception
615
	 */
616
	public function set_exclude_sale_items( $exclude_sale_items ) {
617
		$this->_data['exclude_sale_items'] = (bool) $exclude_sale_items;
618
	}
619
620
	/**
621
	 * Set the minimum spend amount.
622
	 * @since  2.7.0
623
	 * @param  float $amount
624
	 * @throws WC_Data_Exception
625
	 */
626
	public function set_minimum_amount( $amount ) {
627
		$this->_data['minimum_amount'] = wc_format_decimal( $amount );
628
	}
629
630
	/**
631
	 * Set the maximum spend amount.
632
	 * @since  2.7.0
633
	 * @param  float $amount
634
	 * @throws WC_Data_Exception
635
	 */
636
	public function set_maximum_amount( $amount ) {
637
		$this->_data['maximum_amount'] = wc_format_decimal( $amount );
638
	}
639
640
	/**
641
	 * Set email restrictions.
642
	 * @since  2.7.0
643
	 * @param  array $emails
644
	 * @throws WC_Data_Exception
645
	 */
646
	public function set_email_restrictions( $emails = array() ) {
647
		$emails = array_filter( array_map( 'sanitize_email', (array) $emails ) );
648
		foreach ( $emails as $email ) {
649
			if ( ! is_email( $email ) ) {
650
				$this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) );
651
			}
652
		}
653
		$this->_data['email_restrictions'] = $emails;
654
	}
655
656
	/**
657
	 * Set which users have used this coupon.
658
	 * @since 2.7.0
659
	 * @param array $used_by
660
	 * @throws WC_Data_Exception
661
	 */
662
	public function set_used_by( $used_by ) {
663
		$this->_data['used_by'] = array_filter( $used_by );
664
	}
665
666
	/*
667
	|--------------------------------------------------------------------------
668
	| CRUD methods
669
	|--------------------------------------------------------------------------
670
	|
671
	| Methods which create, read, update and delete coupons from the database.
672
	|
673
	| A save method is included for convenience (chooses update or create based
674
	| on if the order exists yet).
675
	|
676
	*/
677
678
	/**
679
	 * Reads an coupon from the database and sets its data to the class.
680
	 * @since 2.7.0
681
	 * @param  int $coupon_id
682
	 */
683
	public function read( $coupon_id ) {
684
		$this->set_defaults();
685
686
		if ( ! $coupon_id ) {
687
			return;
688
		}
689
690
		$post_object = get_post( $coupon_id );
691
692
		if ( ! $post_object ) {
693
			return;
694
		}
695
696
		$this->set_props( array(
697
			'id'                          => $coupon_id,
698
			'code'                        => $post_object->post_title,
699
			'description'                 => $post_object->post_excerpt,
700
			'date_created'                => $post_object->post_date,
701
			'date_modified'               => $post_object->post_modified,
702
			'date_expires'                => get_post_meta( $coupon_id, 'expiry_date', true ),
703
			'discount_type'               => get_post_meta( $coupon_id, 'discount_type', true ),
704
			'amount'                      => get_post_meta( $coupon_id, 'coupon_amount', true ),
705
			'usage_count'                 => get_post_meta( $coupon_id, 'usage_count', true ),
706
			'individual_use'              => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ),
707
			'product_ids'                 => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ),
708
			'excluded_product_ids'        => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ),
709
			'usage_limit'                 => get_post_meta( $coupon_id, 'usage_limit', true ),
710
			'usage_limit_per_user'        => get_post_meta( $coupon_id, 'usage_limit_per_user', true ),
711
			'limit_usage_to_x_items'      => get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ),
712
			'free_shipping'               => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ),
713
			'product_categories'          => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ),
714
			'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ),
715
			'exclude_sale_items'          => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ),
716
			'minimum_amount'              => get_post_meta( $coupon_id, 'minimum_amount', true ),
717
			'maximum_amount'              => get_post_meta( $coupon_id, 'maximum_amount', true ),
718
			'email_restrictions'          => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
719
			'used_by'                     => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
720
		) );
721
		$this->read_meta_data();
722
723
		do_action( 'woocommerce_coupon_loaded', $this );
724
	}
725
726
	/**
727
	 * Create a new coupon.
728
	 * @since 2.7.0
729
	 */
730
	public function create() {
731
		$this->set_date_created( current_time( 'timestamp' ) );
732
733
		$coupon_id = wp_insert_post( apply_filters( 'woocommerce_new_coupon_data', array(
734
			'post_type'     => 'shop_coupon',
735
			'post_status'   => 'publish',
736
			'post_author'   => get_current_user_id(),
737
			'post_title'    => $this->get_code(),
738
			'post_content'  => '',
739
			'post_excerpt'  => $this->get_description(),
740
			'post_date'     => date( 'Y-m-d H:i:s', $this->get_date_created() ),
741
			'post_date_gmt' => get_gmt_from_date( date( 'Y-m-d H:i:s', $this->get_date_created() ) ),
742
		) ), true );
743
744
		if ( $coupon_id ) {
745
			$this->_data['id'] = $coupon_id;
746
			$this->update_post_meta( $coupon_id );
747
			$this->save_meta_data();
748
			do_action( 'woocommerce_new_coupon', $coupon_id );
749
		}
750
	}
751
752
	/**
753
	 * Updates an existing coupon.
754
	 * @since 2.7.0
755
	 */
756
	public function update() {
757
		$coupon_id = $this->get_id();
758
759
		$post_data = array(
760
			'ID'           => $coupon_id,
761
			'post_title'   => $this->get_code(),
762
			'post_excerpt' => $this->get_description(),
763
		);
764
765
		wp_update_post( $post_data );
766
		$this->update_post_meta( $coupon_id );
767
		$this->save_meta_data();
768
		do_action( 'woocommerce_update_coupon', $coupon_id );
769
	}
770
771
	/**
772
	 * Save data (either create or update depending on if we are working on an existing coupon)
773
	 * @since 2.7.0
774
	 */
775
	public function save() {
776
		if ( $this->get_id() ) {
777
			$this->update();
778
		} else {
779
			$this->create();
780
		}
781
	}
782
783
	/**
784
	 * Delete coupon from the database.
785
	 * @since 2.7.0
786
	 */
787
	public function delete() {
788
		wp_delete_post( $this->get_id() );
789
		do_action( 'woocommerce_delete_coupon', $this->get_id() );
790
	}
791
792
	/**
793
	* Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class.
794
	* @since 2.7.0
795
	* @param int $coupon_id
796
	*/
797
	private function update_post_meta( $coupon_id ) {
798
		update_post_meta( $coupon_id, 'discount_type', $this->get_discount_type() );
799
		update_post_meta( $coupon_id, 'coupon_amount', $this->get_amount() );
800
		update_post_meta( $coupon_id, 'individual_use', ( true === $this->get_individual_use() ) ? 'yes' : 'no' );
801
		update_post_meta( $coupon_id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $this->get_product_ids() ) ) ) );
802
		update_post_meta( $coupon_id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $this->get_excluded_product_ids() ) ) ) );
803
		update_post_meta( $coupon_id, 'usage_limit', $this->get_usage_limit() );
804
		update_post_meta( $coupon_id, 'usage_limit_per_user', $this->get_usage_limit_per_user() );
805
		update_post_meta( $coupon_id, 'limit_usage_to_x_items', $this->get_limit_usage_to_x_items() );
806
		update_post_meta( $coupon_id, 'usage_count', $this->get_usage_count() );
807
		update_post_meta( $coupon_id, 'expiry_date', $this->get_date_expires() );
808
		update_post_meta( $coupon_id, 'free_shipping', ( true === $this->get_free_shipping() ) ? 'yes' : 'no' );
809
		update_post_meta( $coupon_id, 'product_categories', array_filter( array_map( 'intval', $this->get_product_categories() ) ) );
810
		update_post_meta( $coupon_id, 'exclude_product_categories', array_filter( array_map( 'intval', $this->get_excluded_product_categories() ) ) );
811
		update_post_meta( $coupon_id, 'exclude_sale_items', ( true === $this->get_exclude_sale_items() ) ? 'yes' : 'no' );
812
		update_post_meta( $coupon_id, 'minimum_amount', $this->get_minimum_amount() );
813
		update_post_meta( $coupon_id, 'maximum_amount', $this->get_maximum_amount() );
814
		update_post_meta( $coupon_id, 'customer_email', array_filter( array_map( 'sanitize_email', $this->get_email_restrictions() ) ) );
815
	}
816
817
	/**
818
	 * Developers can programically return coupons. This function will read those values into our WC_Coupon class.
819
	 * @since  2.7.0
820
	 * @param  string $code  Coupon code
821
	 * @param  array $coupon Array of coupon properties
822
	 */
823
	public function read_manual_coupon( $code, $coupon ) {
824
		// product_ids and exclude_product_ids could be passed in as an empty string '', or comma separated values, when it should be an empty array for the new format.
825
		$convert_fields_to_array = array( 'product_ids', 'exclude_product_ids' );
826
		foreach ( $convert_fields_to_array as $field ) {
827
			if ( ! is_array( $coupon[ $field ] ) ) {
828
				_doing_it_wrong( $field, $field . ' should be an array instead of a string.', '2.7' );
829
				$coupon[ $field ] = array_filter( explode( ',', $coupon[ $field ] ) );
830
			}
831
		}
832
833
		// flip yes|no to true|false
834
		$yes_no_fields = array( 'individual_use', 'free_shipping', 'exclude_sale_items' );
835
		foreach ( $yes_no_fields as $field ) {
836
			if ( 'yes' === $coupon[ $field ] || 'no' === $coupon[ $field ] ) {
837
				_doing_it_wrong( $field, $field . ' should be true or false instead of yes or no.', '2.7' );
838
				$coupon[ $field ] = 'yes' === $coupon[ $field ];
839
			}
840
		}
841
842
		// BW compat
843
		$coupon[ 'date_expires' ]                = isset( $coupon[ 'date_expires' ] ) ? $coupon[ 'date_expires' ]                             : '';
844
		$coupon[ 'date_expires' ]                = isset( $coupon[ 'expiry_date' ] ) ? $coupon[ 'expiry_date' ]                               : $coupon[ 'date_expires' ];
845
		$coupon[ 'excluded_product_ids' ]        = isset( $coupon[ 'excluded_product_ids'] ) ? $coupon[ 'excluded_product_ids']               : '';
846
		$coupon[ 'excluded_product_ids' ]        = isset( $coupon[ 'exclude_product_ids'] ) ? $coupon[ 'exclude_product_ids']                 : $coupon[ 'excluded_product_ids'];
847
		$coupon[ 'excluded_product_categories' ] = isset( $coupon[ 'excluded_product_categories'] ) ? $coupon[ 'excluded_product_categories'] : '';
848
		$coupon[ 'excluded_product_categories' ] = isset( $coupon[ 'exclude_product_categories'] ) ? $coupon[ 'exclude_product_categories']   : $coupon[ 'excluded_product_categories'];
849
850
		$this->set_code( $code );
851
		$this->set_props( $coupon );
852
	}
853
854
	/*
855
    |--------------------------------------------------------------------------
856
    | Other Actions
857
    |--------------------------------------------------------------------------
858
    */
859
860
	/**
861
	 * Increase usage count for current coupon.
862
	 *
863
	 * @param string $used_by Either user ID or billing email
864
	 */
865
	public function inc_usage_count( $used_by = '' ) {
866
		if ( $this->get_id() ) {
867
			$this->_data['usage_count']++;
868
			update_post_meta( $this->get_id(), 'usage_count', $this->get_usage_count() );
869
			if ( $used_by ) {
870
				add_post_meta( $this->get_id(), '_used_by', strtolower( $used_by ) );
871
				$this->set_used_by( (array) get_post_meta( $this->get_id(), '_used_by' ) );
872
			}
873
		}
874
	}
875
876
	/**
877
	 * Decrease usage count for current coupon.
878
	 *
879
	 * @param string $used_by Either user ID or billing email
880
	 */
881
	public function dcr_usage_count( $used_by = '' ) {
882
		if ( $this->get_id() && $this->get_usage_count() > 0 ) {
883
			global $wpdb;
884
			$this->_data['usage_count']--;
885
			update_post_meta( $this->get_id(), 'usage_count', $this->get_usage_count() );
886
			if ( $used_by ) {
887
				/**
888
				 * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
889
				 * all instances where the key and value match, and we only want to delete one.
890
				 */
891
				$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->get_id() ) );
892
				if ( $meta_id ) {
893
					delete_metadata_by_mid( 'post', $meta_id );
894
					$this->set_used_by( (array) get_post_meta( $this->get_id(), '_used_by' ) );
895
				}
896
			}
897
		}
898
	}
899
900
	/*
901
    |--------------------------------------------------------------------------
902
    | Validation & Error Handling
903
    |--------------------------------------------------------------------------
904
    */
905
906
	/**
907
	 * Returns the error_message string.
908
	 *
909
	 * @access public
910
	 * @return string
911
	 */
912
	public function get_error_message() {
913
		return $this->error_message;
914
	}
915
916
	/**
917
	 * Ensure coupon exists or throw exception.
918
	 *
919
	 * @throws Exception
920
	 */
921
	private function validate_exists() {
922
		if ( ! $this->get_id() ) {
923
			throw new Exception( self::E_WC_COUPON_NOT_EXIST );
924
		}
925
	}
926
927
	/**
928
	 * Ensure coupon usage limit is valid or throw exception.
929
	 *
930
	 * @throws Exception
931
	 */
932
	private function validate_usage_limit() {
933
		if ( $this->get_usage_limit() > 0 && $this->get_usage_count() >= $this->get_usage_limit() ) {
934
			throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
935
		}
936
	}
937
938
	/**
939
	 * Ensure coupon user usage limit is valid or throw exception.
940
	 *
941
	 * Per user usage limit - check here if user is logged in (against user IDs).
942
	 * Checked again for emails later on in WC_Cart::check_customer_coupons().
943
	 *
944
	 * @param  int  $user_id
945
	 * @throws Exception
946
	 */
947
	private function validate_user_usage_limit( $user_id = 0 ) {
948
		if ( empty( $user_id ) ) {
949
			$user_id = get_current_user_id();
950
		}
951
		if ( $this->get_usage_limit_per_user() > 0 && is_user_logged_in() && $this->get_id() ) {
952
			global $wpdb;
953
			$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->get_id(), $user_id ) );
954
955
			if ( $usage_count >= $this->get_usage_limit_per_user() ) {
956
				throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
957
			}
958
		}
959
	}
960
961
	/**
962
	 * Ensure coupon date is valid or throw exception.
963
	 *
964
	 * @throws Exception
965
	 */
966
	private function validate_expiry_date() {
967
		if ( $this->get_date_expires() && current_time( 'timestamp' ) > $this->get_date_expires() ) {
968
			throw new Exception( $error_code = self::E_WC_COUPON_EXPIRED );
969
		}
970
	}
971
972
	/**
973
	 * Ensure coupon amount is valid or throw exception.
974
	 *
975
	 * @throws Exception
976
	 */
977
	private function validate_minimum_amount() {
978 View Code Duplication
		if ( $this->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $this->get_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...
979
			throw new Exception( self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET );
980
		}
981
	}
982
983
	/**
984
	 * Ensure coupon amount is valid or throw exception.
985
	 *
986
	 * @throws Exception
987
	 */
988
	private function validate_maximum_amount() {
989 View Code Duplication
		if ( $this->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $this->get_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...
990
			throw new Exception( self::E_WC_COUPON_MAX_SPEND_LIMIT_MET );
991
		}
992
	}
993
994
	/**
995
	 * Ensure coupon is valid for products in the cart is valid or throw exception.
996
	 *
997
	 * @throws Exception
998
	 */
999 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...
1000
		if ( sizeof( $this->get_product_ids() ) > 0 ) {
1001
			$valid_for_cart = false;
1002
			if ( ! WC()->cart->is_empty() ) {
1003
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1004
					if ( in_array( $cart_item['product_id'], $this->get_product_ids() ) || in_array( $cart_item['variation_id'], $this->get_product_ids() ) || in_array( $cart_item['data']->get_parent(), $this->get_product_ids() ) ) {
1005
						$valid_for_cart = true;
1006
					}
1007
				}
1008
			}
1009
			if ( ! $valid_for_cart ) {
1010
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
1011
			}
1012
		}
1013
	}
1014
1015
	/**
1016
	 * Ensure coupon is valid for product categories in the cart is valid or throw exception.
1017
	 *
1018
	 * @throws Exception
1019
	 */
1020 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...
1021
		if ( sizeof( $this->get_product_categories() ) > 0 ) {
1022
			$valid_for_cart = false;
1023
			if ( ! WC()->cart->is_empty() ) {
1024
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1025
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
1026
1027
					// If we find an item with a cat in our allowed cat list, the coupon is valid
1028
					if ( sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) > 0 ) {
1029
						$valid_for_cart = true;
1030
					}
1031
				}
1032
			}
1033
			if ( ! $valid_for_cart ) {
1034
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
1035
			}
1036
		}
1037
	}
1038
1039
	/**
1040
	 * Ensure coupon is valid for sale items in the cart is valid or throw exception.
1041
	 *
1042
	 * @throws Exception
1043
	 */
1044 View Code Duplication
	private function validate_sale_items() {
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...
1045
		if ( $this->get_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...
1046
			$valid_for_cart      = false;
1047
			$product_ids_on_sale = wc_get_product_ids_on_sale();
1048
1049
			if ( ! WC()->cart->is_empty() ) {
1050
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1051
					if ( ! empty( $cart_item['variation_id'] ) ) {
1052
						if ( ! in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
1053
							$valid_for_cart = true;
1054
						}
1055
					} elseif ( ! in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
1056
						$valid_for_cart = true;
1057
					}
1058
				}
1059
			}
1060
			if ( ! $valid_for_cart ) {
1061
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
1062
			}
1063
		}
1064
	}
1065
1066
	/**
1067
	 * All exclusion rules must pass at the same time for a product coupon to be valid.
1068
	 */
1069
	private function validate_excluded_items() {
1070
		if ( ! WC()->cart->is_empty() && $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...
1071
			$valid = false;
1072
1073
			foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1074
				if ( $this->is_valid_for_product( $cart_item['data'], $cart_item ) ) {
1075
					$valid = true;
1076
					break;
1077
				}
1078
			}
1079
1080
			if ( ! $valid ) {
1081
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
1082
			}
1083
		}
1084
	}
1085
1086
	/**
1087
	 * Cart discounts cannot be added if non-eligble product is found in cart.
1088
	 */
1089
	private function validate_cart_excluded_items() {
1090
		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...
1091
			$this->validate_cart_excluded_product_ids();
1092
			$this->validate_cart_excluded_product_categories();
1093
			$this->validate_cart_excluded_sale_items();
1094
		}
1095
	}
1096
1097
	/**
1098
	 * Exclude products from cart.
1099
	 *
1100
	 * @throws Exception
1101
	 */
1102 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...
1103
		// Exclude Products
1104
		if ( sizeof( $this->get_excluded_product_ids() ) > 0 ) {
1105
			$valid_for_cart = true;
1106
			if ( ! WC()->cart->is_empty() ) {
1107
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1108
					if ( in_array( $cart_item['product_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['variation_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['data']->get_parent(), $this->get_excluded_product_ids() ) ) {
1109
						$valid_for_cart = false;
1110
					}
1111
				}
1112
			}
1113
			if ( ! $valid_for_cart ) {
1114
				throw new Exception( self::E_WC_COUPON_EXCLUDED_PRODUCTS );
1115
			}
1116
		}
1117
	}
1118
1119
	/**
1120
	 * Exclude categories from cart.
1121
	 *
1122
	 * @throws Exception
1123
	 */
1124 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...
1125
		if ( sizeof( $this->get_excluded_product_categories() ) > 0 ) {
1126
			$valid_for_cart = true;
1127
			if ( ! WC()->cart->is_empty() ) {
1128
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1129
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
1130
					if ( sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) {
1131
						$valid_for_cart = false;
1132
					}
1133
				}
1134
			}
1135
			if ( ! $valid_for_cart ) {
1136
				throw new Exception( self::E_WC_COUPON_EXCLUDED_CATEGORIES );
1137
			}
1138
		}
1139
	}
1140
1141
	/**
1142
	 * Exclude sale items from cart.
1143
	 *
1144
	 * @throws Exception
1145
	 */
1146 View Code Duplication
	private function validate_cart_excluded_sale_items() {
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...
1147
		if ( $this->get_exclude_sale_items() ) {
1148
			$valid_for_cart = true;
1149
			$product_ids_on_sale = wc_get_product_ids_on_sale();
1150
			if ( ! WC()->cart->is_empty() ) {
1151
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1152
					if ( ! empty( $cart_item['variation_id'] ) ) {
1153
						if ( in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
1154
							$valid_for_cart = false;
1155
						}
1156
					} elseif ( in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
1157
						$valid_for_cart = false;
1158
					}
1159
				}
1160
			}
1161
			if ( ! $valid_for_cart ) {
1162
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
1163
			}
1164
		}
1165
	}
1166
1167
	/**
1168
	 * Check if a coupon is valid.
1169
	 *
1170
	 * @return boolean validity
1171
	 * @throws Exception
1172
	 */
1173
	public function is_valid() {
1174
		try {
1175
			$this->validate_exists();
1176
			$this->validate_usage_limit();
1177
			$this->validate_user_usage_limit();
1178
			$this->validate_expiry_date();
1179
			$this->validate_minimum_amount();
1180
			$this->validate_maximum_amount();
1181
			$this->validate_product_ids();
1182
			$this->validate_product_categories();
1183
			$this->validate_sale_items();
1184
			$this->validate_excluded_items();
1185
			$this->validate_cart_excluded_items();
1186
1187
			if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $this ) ) {
1188
				throw new Exception( self::E_WC_COUPON_INVALID_FILTERED );
1189
			}
1190
		} catch ( Exception $e ) {
1191
			$this->error_message = $this->get_coupon_error( $e->getMessage() );
1192
			return false;
1193
		}
1194
1195
		return true;
1196
	}
1197
1198
	/**
1199
	 * Check if a coupon is valid.
1200
	 *
1201
	 * @return bool
1202
	 */
1203
	public function is_valid_for_cart() {
1204
		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...
1205
	}
1206
1207
	/**
1208
	 * Check if a coupon is valid for a product.
1209
	 *
1210
	 * @param  WC_Product  $product
1211
	 * @return boolean
1212
	 */
1213
	public function is_valid_for_product( $product, $values = array() ) {
1214
		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...
1215
			return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values );
1216
		}
1217
1218
		$valid        = false;
1219
		$product_cats = wc_get_product_cat_ids( $product->id );
1220
		$product_ids  = array( $product->id, ( isset( $product->variation_id ) ? $product->variation_id : 0 ), $product->get_parent() );
1221
1222
		// Specific products get the discount
1223
		if ( sizeof( $this->get_product_ids() ) && sizeof( array_intersect( $product_ids, $this->get_product_ids() ) ) ) {
1224
			$valid = true;
1225
		}
1226
1227
		// Category discounts
1228
		if ( sizeof( $this->get_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) ) {
1229
			$valid = true;
1230
		}
1231
1232
		// No product ids - all items discounted
1233
		if ( ! sizeof( $this->get_product_ids() ) && ! sizeof( $this->get_product_categories() ) ) {
1234
			$valid = true;
1235
		}
1236
1237
		// Specific product ID's excluded from the discount
1238
		if ( sizeof( $this->get_excluded_product_ids()) && sizeof( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) {
1239
			$valid = false;
1240
		}
1241
1242
		// Specific categories excluded from the discount
1243
		if ( sizeof( $this->get_excluded_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) {
1244
			$valid = false;
1245
		}
1246
1247
		// Sale Items excluded from discount
1248
		if ( $this->get_exclude_sale_items() ) {
1249
			$product_ids_on_sale = wc_get_product_ids_on_sale();
1250
1251
			if ( isset( $product->variation_id ) ) {
1252
				if ( in_array( $product->variation_id, $product_ids_on_sale, true ) ) {
1253
					$valid = false;
1254
				}
1255
			} elseif ( in_array( $product->id, $product_ids_on_sale, true ) ) {
1256
				$valid = false;
1257
			}
1258
		}
1259
1260
		return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values );
1261
	}
1262
1263
	/**
1264
	 * Converts one of the WC_Coupon message/error codes to a message string and.
1265
	 * displays the message/error.
1266
	 *
1267
	 * @param int $msg_code Message/error code.
1268
	 */
1269
	public function add_coupon_message( $msg_code ) {
1270
		$msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code );
1271
1272
		if ( ! $msg ) {
1273
			return;
1274
		}
1275
1276
		if ( $msg_code < 200 ) {
1277
			wc_add_notice( $msg, 'error' );
1278
		} else {
1279
			wc_add_notice( $msg );
1280
		}
1281
	}
1282
1283
	/**
1284
	 * Map one of the WC_Coupon message codes to a message string.
1285
	 *
1286
	 * @param integer $msg_code
1287
	 * @return string| Message/error string
1288
	 */
1289 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...
1290
		switch ( $msg_code ) {
1291
			case self::WC_COUPON_SUCCESS :
1292
				$msg = __( 'Coupon code applied successfully.', 'woocommerce' );
1293
			break;
1294
			case self::WC_COUPON_REMOVED :
1295
				$msg = __( 'Coupon code removed successfully.', 'woocommerce' );
1296
			break;
1297
			default:
1298
				$msg = '';
1299
			break;
1300
		}
1301
		return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this );
1302
	}
1303
1304
	/**
1305
	 * Map one of the WC_Coupon error codes to a message string.
1306
	 *
1307
	 * @param int $err_code Message/error code.
1308
	 * @return string| Message/error string
1309
	 */
1310
	public function get_coupon_error( $err_code ) {
1311
		switch ( $err_code ) {
1312
			case self::E_WC_COUPON_INVALID_FILTERED:
1313
				$err = __( 'Coupon is not valid.', 'woocommerce' );
1314
			break;
1315
			case self::E_WC_COUPON_NOT_EXIST:
1316
				$err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), $this->get_code() );
1317
			break;
1318
			case self::E_WC_COUPON_INVALID_REMOVED:
1319
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), $this->get_code() );
1320
			break;
1321
			case self::E_WC_COUPON_NOT_YOURS_REMOVED:
1322
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), $this->get_code() );
1323
			break;
1324
			case self::E_WC_COUPON_ALREADY_APPLIED:
1325
				$err = __( 'Coupon code already applied!', 'woocommerce' );
1326
			break;
1327
			case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY:
1328
				$err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), $this->get_code() );
1329
			break;
1330
			case self::E_WC_COUPON_USAGE_LIMIT_REACHED:
1331
				$err = __( 'Coupon usage limit has been reached.', 'woocommerce' );
1332
			break;
1333
			case self::E_WC_COUPON_EXPIRED:
1334
				$err = __( 'This coupon has expired.', 'woocommerce' );
1335
			break;
1336
			case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET:
1337
				$err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_minimum_amount() ) );
0 ignored issues
show
Documentation introduced by
$this->get_minimum_amount() is of type string|array, but the function expects a double.

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...
1338
			break;
1339
			case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET:
1340
				$err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_maximum_amount() ) );
0 ignored issues
show
Documentation introduced by
$this->get_maximum_amount() is of type string|array, but the function expects a double.

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...
1341
			break;
1342
			case self::E_WC_COUPON_NOT_APPLICABLE:
1343
				$err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' );
1344
			break;
1345
			case self::E_WC_COUPON_EXCLUDED_PRODUCTS:
1346
				// Store excluded products that are in cart in $products
1347
				$products = array();
1348
				if ( ! WC()->cart->is_empty() ) {
1349
					foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1350
						if ( in_array( $cart_item['product_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['variation_id'], $this->get_excluded_product_ids() ) || in_array( $cart_item['data']->get_parent(), $this->get_excluded_product_ids() ) ) {
1351
							$products[] = $cart_item['data']->get_title();
1352
						}
1353
					}
1354
				}
1355
1356
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) );
1357
				break;
1358
			case self::E_WC_COUPON_EXCLUDED_CATEGORIES:
1359
				// Store excluded categories that are in cart in $categories
1360
				$categories = array();
1361
				if ( ! WC()->cart->is_empty() ) {
1362
					foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1363
						$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
1364
1365
						if ( sizeof( $intersect = array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) {
1366
1367
							foreach( $intersect as $cat_id) {
1368
								$cat = get_term( $cat_id, 'product_cat' );
1369
								$categories[] = $cat->name;
1370
							}
1371
						}
1372
					}
1373
				}
1374
1375
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) );
1376
				break;
1377
			case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS:
1378
				$err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' );
1379
			break;
1380
			default:
1381
				$err = '';
1382
			break;
1383
		}
1384
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this );
1385
	}
1386
1387
	/**
1388
	 * Map one of the WC_Coupon error codes to an error string.
1389
	 * No coupon instance will be available where a coupon does not exist,
1390
	 * so this static method exists.
1391
	 *
1392
	 * @param int $err_code Error code
1393
	 * @return string| Error string
1394
	 */
1395 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...
1396
		switch ( $err_code ) {
1397
			case self::E_WC_COUPON_NOT_EXIST:
1398
				$err = __( 'Coupon does not exist!', 'woocommerce' );
1399
			break;
1400
			case self::E_WC_COUPON_PLEASE_ENTER:
1401
				$err = __( 'Please enter a coupon code.', 'woocommerce' );
1402
			break;
1403
			default:
1404
				$err = '';
1405
			break;
1406
		}
1407
		// When using this static method, there is no $this to pass to filter
1408
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null );
1409
	}
1410
}
1411