Completed
Pull Request — master (#11785)
by Mike
09:49
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
		'description'                 => '',
30
		'discount_type'               => 'fixed_cart',
31
		'amount'                      => 0,
32
		'date_expires'                => '',
33
		'date_created'                => '',
34
		'date_modified'               => '',
35
		'usage_count'                 => 0,
36
		'used_by'                     => '',
37
		'individual_use'              => false,
38
		'product_ids'                 => array(),
39
		'excluded_product_ids'        => array(),
40
		'usage_limit'                 => 0,
41
		'usage_limit_per_user'        => 0,
42
		'limit_usage_to_x_items'      => 0,
43
		'free_shipping'               => false,
44
		'product_categories'          => array(),
45
		'excluded_product_categories' => array(),
46
		'exclude_sale_items'          => false,
47
		'minimum_amount'              => '',
48
		'maximum_amount'              => '',
49
		'email_restrictions'          => array(),
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) explode( ',', 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', implode( ',', 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( ',', (array) $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
		// expiry_date to date_expires
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
846
		$this->set_code( $code );
847
		$this->set_props( $coupon );
848
	}
849
850
	/*
851
    |--------------------------------------------------------------------------
852
    | Other Actions
853
    |--------------------------------------------------------------------------
854
    */
855
856
	/**
857
	 * Increase usage count for current coupon.
858
	 *
859
	 * @param string $used_by Either user ID or billing email
860
	 */
861
	public function inc_usage_count( $used_by = '' ) {
862
		if ( $this->get_id() ) {
863
			$this->_data['usage_count']++;
864
			update_post_meta( $this->get_id(), 'usage_count', $this->get_usage_count() );
865
			if ( $used_by ) {
866
				add_post_meta( $this->get_id(), '_used_by', strtolower( $used_by ) );
867
				$this->set_used_by( (array) get_post_meta( $this->get_id(), '_used_by' ) );
868
			}
869
		}
870
	}
871
872
	/**
873
	 * Decrease usage count for current coupon.
874
	 *
875
	 * @param string $used_by Either user ID or billing email
876
	 */
877
	public function dcr_usage_count( $used_by = '' ) {
878
		if ( $this->get_id() && $this->get_usage_count() > 0 ) {
879
			global $wpdb;
880
			$this->_data['usage_count']--;
881
			update_post_meta( $this->get_id(), 'usage_count', $this->get_usage_count() );
882
			if ( $used_by ) {
883
				/**
884
				 * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
885
				 * all instances where the key and value match, and we only want to delete one.
886
				 */
887
				$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() ) );
888
				if ( $meta_id ) {
889
					delete_metadata_by_mid( 'post', $meta_id );
890
					$this->set_used_by( (array) get_post_meta( $this->get_id(), '_used_by' ) );
891
				}
892
			}
893
		}
894
	}
895
896
	/*
897
    |--------------------------------------------------------------------------
898
    | Validation & Error Handling
899
    |--------------------------------------------------------------------------
900
    */
901
902
	/**
903
	 * Returns the error_message string.
904
	 *
905
	 * @access public
906
	 * @return string
907
	 */
908
	public function get_error_message() {
909
		return $this->error_message;
910
	}
911
912
	/**
913
	 * Ensure coupon exists or throw exception.
914
	 *
915
	 * @throws Exception
916
	 */
917
	private function validate_exists() {
918
		if ( ! $this->get_id() ) {
919
			throw new Exception( self::E_WC_COUPON_NOT_EXIST );
920
		}
921
	}
922
923
	/**
924
	 * Ensure coupon usage limit is valid or throw exception.
925
	 *
926
	 * @throws Exception
927
	 */
928
	private function validate_usage_limit() {
929
		if ( $this->get_usage_limit() > 0 && $this->get_usage_count() >= $this->get_usage_limit() ) {
930
			throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
931
		}
932
	}
933
934
	/**
935
	 * Ensure coupon user usage limit is valid or throw exception.
936
	 *
937
	 * Per user usage limit - check here if user is logged in (against user IDs).
938
	 * Checked again for emails later on in WC_Cart::check_customer_coupons().
939
	 *
940
	 * @param  int  $user_id
941
	 * @throws Exception
942
	 */
943
	private function validate_user_usage_limit( $user_id = 0 ) {
944
		if ( empty( $user_id ) ) {
945
			$user_id = get_current_user_id();
946
		}
947
		if ( $this->get_usage_limit_per_user() > 0 && is_user_logged_in() && $this->get_id() ) {
948
			global $wpdb;
949
			$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 ) );
950
951
			if ( $usage_count >= $this->get_usage_limit_per_user() ) {
952
				throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
953
			}
954
		}
955
	}
956
957
	/**
958
	 * Ensure coupon date is valid or throw exception.
959
	 *
960
	 * @throws Exception
961
	 */
962
	private function validate_expiry_date() {
963
		if ( $this->get_date_expires() && current_time( 'timestamp' ) > $this->get_date_expires() ) {
964
			throw new Exception( $error_code = self::E_WC_COUPON_EXPIRED );
965
		}
966
	}
967
968
	/**
969
	 * Ensure coupon amount is valid or throw exception.
970
	 *
971
	 * @throws Exception
972
	 */
973
	private function validate_minimum_amount() {
974 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...
975
			throw new Exception( self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET );
976
		}
977
	}
978
979
	/**
980
	 * Ensure coupon amount is valid or throw exception.
981
	 *
982
	 * @throws Exception
983
	 */
984
	private function validate_maximum_amount() {
985 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...
986
			throw new Exception( self::E_WC_COUPON_MAX_SPEND_LIMIT_MET );
987
		}
988
	}
989
990
	/**
991
	 * Ensure coupon is valid for products in the cart is valid or throw exception.
992
	 *
993
	 * @throws Exception
994
	 */
995 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...
996
		if ( sizeof( $this->get_product_ids() ) > 0 ) {
997
			$valid_for_cart = false;
998
			if ( ! WC()->cart->is_empty() ) {
999
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1000
					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() ) ) {
1001
						$valid_for_cart = true;
1002
					}
1003
				}
1004
			}
1005
			if ( ! $valid_for_cart ) {
1006
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
1007
			}
1008
		}
1009
	}
1010
1011
	/**
1012
	 * Ensure coupon is valid for product categories in the cart is valid or throw exception.
1013
	 *
1014
	 * @throws Exception
1015
	 */
1016 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...
1017
		if ( sizeof( $this->get_product_categories() ) > 0 ) {
1018
			$valid_for_cart = false;
1019
			if ( ! WC()->cart->is_empty() ) {
1020
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1021
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
1022
1023
					// If we find an item with a cat in our allowed cat list, the coupon is valid
1024
					if ( sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) > 0 ) {
1025
						$valid_for_cart = true;
1026
					}
1027
				}
1028
			}
1029
			if ( ! $valid_for_cart ) {
1030
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
1031
			}
1032
		}
1033
	}
1034
1035
	/**
1036
	 * Ensure coupon is valid for sale items in the cart is valid or throw exception.
1037
	 *
1038
	 * @throws Exception
1039
	 */
1040 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...
1041
		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...
1042
			$valid_for_cart      = false;
1043
			$product_ids_on_sale = wc_get_product_ids_on_sale();
1044
1045
			if ( ! WC()->cart->is_empty() ) {
1046
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1047
					if ( ! empty( $cart_item['variation_id'] ) ) {
1048
						if ( ! in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
1049
							$valid_for_cart = true;
1050
						}
1051
					} elseif ( ! in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
1052
						$valid_for_cart = true;
1053
					}
1054
				}
1055
			}
1056
			if ( ! $valid_for_cart ) {
1057
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
1058
			}
1059
		}
1060
	}
1061
1062
	/**
1063
	 * All exclusion rules must pass at the same time for a product coupon to be valid.
1064
	 */
1065
	private function validate_excluded_items() {
1066
		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...
1067
			$valid = false;
1068
1069
			foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1070
				if ( $this->is_valid_for_product( $cart_item['data'], $cart_item ) ) {
1071
					$valid = true;
1072
					break;
1073
				}
1074
			}
1075
1076
			if ( ! $valid ) {
1077
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
1078
			}
1079
		}
1080
	}
1081
1082
	/**
1083
	 * Cart discounts cannot be added if non-eligble product is found in cart.
1084
	 */
1085
	private function validate_cart_excluded_items() {
1086
		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...
1087
			$this->validate_cart_excluded_product_ids();
1088
			$this->validate_cart_excluded_product_categories();
1089
			$this->validate_cart_excluded_sale_items();
1090
		}
1091
	}
1092
1093
	/**
1094
	 * Exclude products from cart.
1095
	 *
1096
	 * @throws Exception
1097
	 */
1098 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...
1099
		// Exclude Products
1100
		if ( sizeof( $this->get_excluded_product_ids() ) > 0 ) {
1101
			$valid_for_cart = true;
1102
			if ( ! WC()->cart->is_empty() ) {
1103
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1104
					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() ) ) {
1105
						$valid_for_cart = false;
1106
					}
1107
				}
1108
			}
1109
			if ( ! $valid_for_cart ) {
1110
				throw new Exception( self::E_WC_COUPON_EXCLUDED_PRODUCTS );
1111
			}
1112
		}
1113
	}
1114
1115
	/**
1116
	 * Exclude categories from cart.
1117
	 *
1118
	 * @throws Exception
1119
	 */
1120 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...
1121
		if ( sizeof( $this->get_excluded_product_categories() ) > 0 ) {
1122
			$valid_for_cart = true;
1123
			if ( ! WC()->cart->is_empty() ) {
1124
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1125
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
1126
					if ( sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) {
1127
						$valid_for_cart = false;
1128
					}
1129
				}
1130
			}
1131
			if ( ! $valid_for_cart ) {
1132
				throw new Exception( self::E_WC_COUPON_EXCLUDED_CATEGORIES );
1133
			}
1134
		}
1135
	}
1136
1137
	/**
1138
	 * Exclude sale items from cart.
1139
	 *
1140
	 * @throws Exception
1141
	 */
1142 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...
1143
		if ( $this->get_exclude_sale_items() ) {
1144
			$valid_for_cart = true;
1145
			$product_ids_on_sale = wc_get_product_ids_on_sale();
1146
			if ( ! WC()->cart->is_empty() ) {
1147
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1148
					if ( ! empty( $cart_item['variation_id'] ) ) {
1149
						if ( in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
1150
							$valid_for_cart = false;
1151
						}
1152
					} elseif ( in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
1153
						$valid_for_cart = false;
1154
					}
1155
				}
1156
			}
1157
			if ( ! $valid_for_cart ) {
1158
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
1159
			}
1160
		}
1161
	}
1162
1163
	/**
1164
	 * Check if a coupon is valid.
1165
	 *
1166
	 * @return boolean validity
1167
	 * @throws Exception
1168
	 */
1169
	public function is_valid() {
1170
		try {
1171
			$this->validate_exists();
1172
			$this->validate_usage_limit();
1173
			$this->validate_user_usage_limit();
1174
			$this->validate_expiry_date();
1175
			$this->validate_minimum_amount();
1176
			$this->validate_maximum_amount();
1177
			$this->validate_product_ids();
1178
			$this->validate_product_categories();
1179
			$this->validate_sale_items();
1180
			$this->validate_excluded_items();
1181
			$this->validate_cart_excluded_items();
1182
1183
			if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $this ) ) {
1184
				throw new Exception( self::E_WC_COUPON_INVALID_FILTERED );
1185
			}
1186
		} catch ( Exception $e ) {
1187
			$this->error_message = $this->get_coupon_error( $e->getMessage() );
1188
			return false;
1189
		}
1190
1191
		return true;
1192
	}
1193
1194
	/**
1195
	 * Check if a coupon is valid.
1196
	 *
1197
	 * @return bool
1198
	 */
1199
	public function is_valid_for_cart() {
1200
		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...
1201
	}
1202
1203
	/**
1204
	 * Check if a coupon is valid for a product.
1205
	 *
1206
	 * @param  WC_Product  $product
1207
	 * @return boolean
1208
	 */
1209
	public function is_valid_for_product( $product, $values = array() ) {
1210
		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...
1211
			return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values );
1212
		}
1213
1214
		$valid        = false;
1215
		$product_cats = wc_get_product_cat_ids( $product->id );
1216
		$product_ids  = array( $product->id, ( isset( $product->variation_id ) ? $product->variation_id : 0 ), $product->get_parent() );
1217
1218
		// Specific products get the discount
1219
		if ( sizeof( $this->get_product_ids() ) && sizeof( array_intersect( $product_ids, $this->get_product_ids() ) ) ) {
1220
			$valid = true;
1221
		}
1222
1223
		// Category discounts
1224
		if ( sizeof( $this->get_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) ) {
1225
			$valid = true;
1226
		}
1227
1228
		// No product ids - all items discounted
1229
		if ( ! sizeof( $this->get_product_ids() ) && ! sizeof( $this->get_product_categories() ) ) {
1230
			$valid = true;
1231
		}
1232
1233
		// Specific product ID's excluded from the discount
1234
		if ( sizeof( $this->get_excluded_product_ids()) && sizeof( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) {
1235
			$valid = false;
1236
		}
1237
1238
		// Specific categories excluded from the discount
1239
		if ( sizeof( $this->get_excluded_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) {
1240
			$valid = false;
1241
		}
1242
1243
		// Sale Items excluded from discount
1244
		if ( $this->get_exclude_sale_items() ) {
1245
			$product_ids_on_sale = wc_get_product_ids_on_sale();
1246
1247
			if ( isset( $product->variation_id ) ) {
1248
				if ( in_array( $product->variation_id, $product_ids_on_sale, true ) ) {
1249
					$valid = false;
1250
				}
1251
			} elseif ( in_array( $product->id, $product_ids_on_sale, true ) ) {
1252
				$valid = false;
1253
			}
1254
		}
1255
1256
		return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values );
1257
	}
1258
1259
	/**
1260
	 * Converts one of the WC_Coupon message/error codes to a message string and.
1261
	 * displays the message/error.
1262
	 *
1263
	 * @param int $msg_code Message/error code.
1264
	 */
1265
	public function add_coupon_message( $msg_code ) {
1266
		$msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code );
1267
1268
		if ( ! $msg ) {
1269
			return;
1270
		}
1271
1272
		if ( $msg_code < 200 ) {
1273
			wc_add_notice( $msg, 'error' );
1274
		} else {
1275
			wc_add_notice( $msg );
1276
		}
1277
	}
1278
1279
	/**
1280
	 * Map one of the WC_Coupon message codes to a message string.
1281
	 *
1282
	 * @param integer $msg_code
1283
	 * @return string| Message/error string
1284
	 */
1285 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...
1286
		switch ( $msg_code ) {
1287
			case self::WC_COUPON_SUCCESS :
1288
				$msg = __( 'Coupon code applied successfully.', 'woocommerce' );
1289
			break;
1290
			case self::WC_COUPON_REMOVED :
1291
				$msg = __( 'Coupon code removed successfully.', 'woocommerce' );
1292
			break;
1293
			default:
1294
				$msg = '';
1295
			break;
1296
		}
1297
		return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this );
1298
	}
1299
1300
	/**
1301
	 * Map one of the WC_Coupon error codes to a message string.
1302
	 *
1303
	 * @param int $err_code Message/error code.
1304
	 * @return string| Message/error string
1305
	 */
1306
	public function get_coupon_error( $err_code ) {
1307
		switch ( $err_code ) {
1308
			case self::E_WC_COUPON_INVALID_FILTERED:
1309
				$err = __( 'Coupon is not valid.', 'woocommerce' );
1310
			break;
1311
			case self::E_WC_COUPON_NOT_EXIST:
1312
				$err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), $this->get_code() );
1313
			break;
1314
			case self::E_WC_COUPON_INVALID_REMOVED:
1315
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), $this->get_code() );
1316
			break;
1317
			case self::E_WC_COUPON_NOT_YOURS_REMOVED:
1318
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), $this->get_code() );
1319
			break;
1320
			case self::E_WC_COUPON_ALREADY_APPLIED:
1321
				$err = __( 'Coupon code already applied!', 'woocommerce' );
1322
			break;
1323
			case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY:
1324
				$err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), $this->get_code() );
1325
			break;
1326
			case self::E_WC_COUPON_USAGE_LIMIT_REACHED:
1327
				$err = __( 'Coupon usage limit has been reached.', 'woocommerce' );
1328
			break;
1329
			case self::E_WC_COUPON_EXPIRED:
1330
				$err = __( 'This coupon has expired.', 'woocommerce' );
1331
			break;
1332
			case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET:
1333
				$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...
1334
			break;
1335
			case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET:
1336
				$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...
1337
			break;
1338
			case self::E_WC_COUPON_NOT_APPLICABLE:
1339
				$err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' );
1340
			break;
1341
			case self::E_WC_COUPON_EXCLUDED_PRODUCTS:
1342
				// Store excluded products that are in cart in $products
1343
				$products = array();
1344
				if ( ! WC()->cart->is_empty() ) {
1345
					foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1346
						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() ) ) {
1347
							$products[] = $cart_item['data']->get_title();
1348
						}
1349
					}
1350
				}
1351
1352
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) );
1353
				break;
1354
			case self::E_WC_COUPON_EXCLUDED_CATEGORIES:
1355
				// Store excluded categories that are in cart in $categories
1356
				$categories = array();
1357
				if ( ! WC()->cart->is_empty() ) {
1358
					foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1359
						$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
1360
1361
						if ( sizeof( $intersect = array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) {
1362
1363
							foreach( $intersect as $cat_id) {
1364
								$cat = get_term( $cat_id, 'product_cat' );
1365
								$categories[] = $cat->name;
1366
							}
1367
						}
1368
					}
1369
				}
1370
1371
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) );
1372
				break;
1373
			case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS:
1374
				$err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' );
1375
			break;
1376
			default:
1377
				$err = '';
1378
			break;
1379
		}
1380
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this );
1381
	}
1382
1383
	/**
1384
	 * Map one of the WC_Coupon error codes to an error string.
1385
	 * No coupon instance will be available where a coupon does not exist,
1386
	 * so this static method exists.
1387
	 *
1388
	 * @param int $err_code Error code
1389
	 * @return string| Error string
1390
	 */
1391 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...
1392
		switch ( $err_code ) {
1393
			case self::E_WC_COUPON_NOT_EXIST:
1394
				$err = __( 'Coupon does not exist!', 'woocommerce' );
1395
			break;
1396
			case self::E_WC_COUPON_PLEASE_ENTER:
1397
				$err = __( 'Please enter a coupon code.', 'woocommerce' );
1398
			break;
1399
			default:
1400
				$err = '';
1401
			break;
1402
		}
1403
		// When using this static method, there is no $this to pass to filter
1404
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null );
1405
	}
1406
}
1407