Completed
Push — master ( f54ada...499e19 )
by Mike
22:45
created

WC_Coupon::validate_maximum_amount()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 3
Ratio 60 %

Importance

Changes 0
Metric Value
cc 3
dl 3
loc 5
rs 9.4285
c 0
b 0
f 0
eloc 3
nc 2
nop 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( wc_get_coupon_id_by_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 discount amount for a cart item.
338
	 *
339
	 * @param  float $discounting_amount Amount the coupon is being applied to
340
	 * @param  array|null $cart_item Cart item being discounted if applicable
341
	 * @param  boolean $single True if discounting a single qty item, false if its the line
342
	 * @return float Amount this coupon has discounted
343
	 */
344
	public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) {
345
		$discount      = 0;
346
		$cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity'];
347
348
		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...
349
			$discount = $this->get_amount() * ( $discounting_amount / 100 );
350
		} elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) {
351
			/**
352
			 * This is the most complex discount - we need to divide the discount between rows based on their price in.
353
			 * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows.
354
			 * with no price (free) don't get discounted.
355
			 *
356
			 * Get item discount by dividing item cost by subtotal to get a %.
357
			 *
358
			 * 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.
359
			 */
360
			if ( wc_prices_include_tax() ) {
361
				$discount_percent = ( $cart_item['data']->get_price_including_tax() * $cart_item_qty ) / WC()->cart->subtotal;
362
			} else {
363
				$discount_percent = ( $cart_item['data']->get_price_excluding_tax() * $cart_item_qty ) / WC()->cart->subtotal_ex_tax;
364
			}
365
			$discount = ( $this->get_amount() * $discount_percent ) / $cart_item_qty;
366
367
		} elseif ( $this->is_type( 'fixed_product' ) ) {
368
			$discount = min( $this->get_amount(), $discounting_amount );
369
			$discount = $single ? $discount : $discount * $cart_item_qty;
370
		}
371
372
		$discount = min( $discount, $discounting_amount );
373
374
		// Handle the limit_usage_to_x_items option
375
		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...
376
			if ( $discounting_amount ) {
377
				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...
378
					$limit_usage_qty = $cart_item_qty;
379
				} else {
380
					$limit_usage_qty = min( $this->get_limit_usage_to_x_items(), $cart_item_qty );
381
					$this->set_limit_usage_to_x_items( max( 0, $this->get_limit_usage_to_x_items() - $limit_usage_qty ) );
382
				}
383
				if ( $single ) {
384
					$discount = ( $discount * $limit_usage_qty ) / $cart_item_qty;
385
				} else {
386
					$discount = ( $discount / $cart_item_qty ) * $limit_usage_qty;
387
				}
388
			}
389
		}
390
391
		$discount = round( $discount, wc_get_rounding_precision() );
392
393
		return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this );
394
	}
395
396
	/*
397
	|--------------------------------------------------------------------------
398
	| Setters
399
	|--------------------------------------------------------------------------
400
	|
401
	| Functions for setting coupon data. These should not update anything in the
402
	| database itself and should only change what is stored in the class
403
	| object.
404
	|
405
	*/
406
407
	/**
408
	 * Set ID
409
	 * @param int $value
410
	 * @throws WC_Data_Exception
411
	 */
412
	public function set_id( $value ) {
413
		$this->_data['id'] = absint( $value );
414
	}
415
416
	/**
417
	 * Set coupon code.
418
	 * @since  2.7.0
419
	 * @param  string $code
420
	 * @throws WC_Data_Exception
421
	 */
422
	public function set_code( $code ) {
423
		$this->_data['code'] = apply_filters( 'woocommerce_coupon_code', $code );
424
	}
425
426
	/**
427
	 * Set coupon description.
428
	 * @since  2.7.0
429
	 * @param  string $description
430
	 * @throws WC_Data_Exception
431
	 */
432
	public function set_description( $description ) {
433
		$this->_data['description'] = $description;
434
	}
435
436
	/**
437
	 * Set discount type.
438
	 * @since  2.7.0
439
	 * @param  string $discount_type
440
	 * @throws WC_Data_Exception
441
	 */
442
	public function set_discount_type( $discount_type ) {
443
		if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ) ) ) {
444
			$this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) );
445
		}
446
		$this->_data['discount_type'] = $discount_type;
447
	}
448
449
	/**
450
	 * Set amount.
451
	 * @since  2.7.0
452
	 * @param  float $amount
453
	 * @throws WC_Data_Exception
454
	 */
455
	public function set_amount( $amount ) {
456
		$this->_data['amount'] = wc_format_decimal( $amount );
457
	}
458
459
	/**
460
	 * Set expiration date.
461
	 * @since  2.7.0
462
	 * @param string $timestamp Timestamp
463
	 * @throws WC_Data_Exception
464
	 */
465
	public function set_date_expires( $timestamp ) {
466
		$this->_data['date_expires'] = is_numeric( $timestamp ) ? $timestamp : strtotime( $timestamp );
467
	}
468
469
	/**
470
	 * Set date_created
471
	 * @since  2.7.0
472
	 * @param string $timestamp Timestamp
473
	 * @throws WC_Data_Exception
474
	 */
475
	public function set_date_created( $timestamp ) {
476
		$this->_data['date_created'] = is_numeric( $timestamp ) ? $timestamp : strtotime( $timestamp );
477
	}
478
479
	/**
480
	 * Set date_modified
481
	 * @since  2.7.0
482
	 * @param string $timestamp
483
	 * @throws WC_Data_Exception
484
	 */
485
	public function set_date_modified( $timestamp ) {
486
		$this->_data['date_modified'] = is_numeric( $timestamp ) ? $timestamp : strtotime( $timestamp );
487
	}
488
489
	/**
490
	 * Set how many times this coupon has been used.
491
	 * @since  2.7.0
492
	 * @param  int $usage_count
493
	 * @throws WC_Data_Exception
494
	 */
495
	public function set_usage_count( $usage_count ) {
496
		$this->_data['usage_count'] = absint( $usage_count );
497
	}
498
499
	/**
500
	 * Set if this coupon can only be used once.
501
	 * @since  2.7.0
502
	 * @param  bool $is_individual_use
503
	 * @throws WC_Data_Exception
504
	 */
505
	public function set_individual_use( $is_individual_use ) {
506
		$this->_data['individual_use'] = (bool) $is_individual_use;
507
	}
508
509
	/**
510
	 * Set the product IDs this coupon can be used with.
511
	 * @since  2.7.0
512
	 * @param  array $product_ids
513
	 * @throws WC_Data_Exception
514
	 */
515
	public function set_product_ids( $product_ids ) {
516
		$this->_data['product_ids'] = (array) $product_ids;
517
	}
518
519
	/**
520
	 * Set the product IDs this coupon cannot be used with.
521
	 * @since  2.7.0
522
	 * @param  array $excluded_product_ids
523
	 * @throws WC_Data_Exception
524
	 */
525
	public function set_excluded_product_ids( $excluded_product_ids ) {
526
		$this->_data['excluded_product_ids'] = (array) $excluded_product_ids;
527
	}
528
529
	/**
530
	 * Set the amount of times this coupon can be used.
531
	 * @since  2.7.0
532
	 * @param  int $usage_limit
533
	 * @throws WC_Data_Exception
534
	 */
535
	public function set_usage_limit( $usage_limit ) {
536
		$this->_data['usage_limit'] = absint( $usage_limit );
537
	}
538
539
	/**
540
	 * Set the amount of times this coupon can be used per user.
541
	 * @since  2.7.0
542
	 * @param  int $usage_limit
543
	 * @throws WC_Data_Exception
544
	 */
545
	public function set_usage_limit_per_user( $usage_limit ) {
546
		$this->_data['usage_limit_per_user'] = absint( $usage_limit );
547
	}
548
549
	/**
550
	 * Set usage limit to x number of items.
551
	 * @since  2.7.0
552
	 * @param  int $limit_usage_to_x_items
553
	 * @throws WC_Data_Exception
554
	 */
555
	public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) {
556
		$this->_data['limit_usage_to_x_items'] = $limit_usage_to_x_items;
557
	}
558
559
	/**
560
	 * Set if this coupon enables free shipping or not.
561
	 * @since  2.7.0
562
	 * @param  bool $free_shipping
563
	 * @throws WC_Data_Exception
564
	 */
565
	public function set_free_shipping( $free_shipping ) {
566
		$this->_data['free_shipping'] = (bool) $free_shipping;
567
	}
568
569
	/**
570
	 * Set the product category IDs this coupon can be used with.
571
	 * @since  2.7.0
572
	 * @param  array $product_categories
573
	 * @throws WC_Data_Exception
574
	 */
575
	public function set_product_categories( $product_categories ) {
576
		$this->_data['product_categories'] = (array) $product_categories;
577
	}
578
579
	/**
580
	 * Set the product category IDs this coupon cannot be used with.
581
	 * @since  2.7.0
582
	 * @param  array $excluded_product_categories
583
	 * @throws WC_Data_Exception
584
	 */
585
	public function set_excluded_product_categories( $excluded_product_categories ) {
586
		$this->_data['excluded_product_categories'] = (array) $excluded_product_categories;
587
	}
588
589
	/**
590
	 * Set if this coupon should excluded sale items or not.
591
	 * @since  2.7.0
592
	 * @param  bool $exclude_sale_items
593
	 * @throws WC_Data_Exception
594
	 */
595
	public function set_exclude_sale_items( $exclude_sale_items ) {
596
		$this->_data['exclude_sale_items'] = (bool) $exclude_sale_items;
597
	}
598
599
	/**
600
	 * Set the minimum spend amount.
601
	 * @since  2.7.0
602
	 * @param  float $amount
603
	 * @throws WC_Data_Exception
604
	 */
605
	public function set_minimum_amount( $amount ) {
606
		$this->_data['minimum_amount'] = wc_format_decimal( $amount );
607
	}
608
609
	/**
610
	 * Set the maximum spend amount.
611
	 * @since  2.7.0
612
	 * @param  float $amount
613
	 * @throws WC_Data_Exception
614
	 */
615
	public function set_maximum_amount( $amount ) {
616
		$this->_data['maximum_amount'] = wc_format_decimal( $amount );
617
	}
618
619
	/**
620
	 * Set email restrictions.
621
	 * @since  2.7.0
622
	 * @param  array $emails
623
	 * @throws WC_Data_Exception
624
	 */
625
	public function set_email_restrictions( $emails = array() ) {
626
		$emails = array_filter( array_map( 'sanitize_email', (array) $emails ) );
627
		foreach ( $emails as $email ) {
628
			if ( ! is_email( $email ) ) {
629
				$this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) );
630
			}
631
		}
632
		$this->_data['email_restrictions'] = $emails;
633
	}
634
635
	/**
636
	 * Set which users have used this coupon.
637
	 * @since 2.7.0
638
	 * @param array $used_by
639
	 * @throws WC_Data_Exception
640
	 */
641
	public function set_used_by( $used_by ) {
642
		$this->_data['used_by'] = array_filter( $used_by );
643
	}
644
645
	/*
646
	|--------------------------------------------------------------------------
647
	| CRUD methods
648
	|--------------------------------------------------------------------------
649
	|
650
	| Methods which create, read, update and delete coupons from the database.
651
	|
652
	| A save method is included for convenience (chooses update or create based
653
	| on if the order exists yet).
654
	|
655
	*/
656
657
	/**
658
	 * Reads an coupon from the database and sets its data to the class.
659
	 * @since 2.7.0
660
	 * @param  int $coupon_id
661
	 */
662
	public function read( $coupon_id ) {
663
		$this->set_defaults();
664
665
		if ( ! $coupon_id ) {
666
			return;
667
		}
668
669
		$post_object = get_post( $coupon_id );
670
671
		if ( ! $post_object ) {
672
			return;
673
		}
674
675
		$this->set_props( array(
676
			'id'                          => $coupon_id,
677
			'code'                        => $post_object->post_title,
678
			'description'                 => $post_object->post_excerpt,
679
			'date_created'                => $post_object->post_date,
680
			'date_modified'               => $post_object->post_modified,
681
			'date_expires'                => get_post_meta( $coupon_id, 'expiry_date', true ),
682
			'discount_type'               => get_post_meta( $coupon_id, 'discount_type', true ),
683
			'amount'                      => get_post_meta( $coupon_id, 'coupon_amount', true ),
684
			'usage_count'                 => get_post_meta( $coupon_id, 'usage_count', true ),
685
			'individual_use'              => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ),
686
			'product_ids'                 => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ),
687
			'excluded_product_ids'        => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ),
688
			'usage_limit'                 => get_post_meta( $coupon_id, 'usage_limit', true ),
689
			'usage_limit_per_user'        => get_post_meta( $coupon_id, 'usage_limit_per_user', true ),
690
			'limit_usage_to_x_items'      => get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ),
691
			'free_shipping'               => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ),
692
			'product_categories'          => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ),
693
			'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ),
694
			'exclude_sale_items'          => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ),
695
			'minimum_amount'              => get_post_meta( $coupon_id, 'minimum_amount', true ),
696
			'maximum_amount'              => get_post_meta( $coupon_id, 'maximum_amount', true ),
697
			'email_restrictions'          => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
698
			'used_by'                     => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
699
		) );
700
		$this->read_meta_data();
701
702
		do_action( 'woocommerce_coupon_loaded', $this );
703
	}
704
705
	/**
706
	 * Create a new coupon.
707
	 * @since 2.7.0
708
	 */
709
	public function create() {
710
		$this->set_date_created( current_time( 'timestamp' ) );
711
712
		$coupon_id = wp_insert_post( apply_filters( 'woocommerce_new_coupon_data', array(
713
			'post_type'     => 'shop_coupon',
714
			'post_status'   => 'publish',
715
			'post_author'   => get_current_user_id(),
716
			'post_title'    => $this->get_code(),
717
			'post_content'  => '',
718
			'post_excerpt'  => $this->get_description(),
719
			'post_date'     => date( 'Y-m-d H:i:s', $this->get_date_created() ),
720
			'post_date_gmt' => get_gmt_from_date( date( 'Y-m-d H:i:s', $this->get_date_created() ) ),
721
		) ), true );
722
723
		if ( $coupon_id ) {
724
			$this->_data['id'] = $coupon_id;
725
			$this->update_post_meta( $coupon_id );
726
			$this->save_meta_data();
727
			do_action( 'woocommerce_new_coupon', $coupon_id );
728
		}
729
	}
730
731
	/**
732
	 * Updates an existing coupon.
733
	 * @since 2.7.0
734
	 */
735
	public function update() {
736
		$coupon_id = $this->get_id();
737
738
		$post_data = array(
739
			'ID'           => $coupon_id,
740
			'post_title'   => $this->get_code(),
741
			'post_excerpt' => $this->get_description(),
742
		);
743
744
		wp_update_post( $post_data );
745
		$this->update_post_meta( $coupon_id );
746
		$this->save_meta_data();
747
		do_action( 'woocommerce_update_coupon', $coupon_id );
748
	}
749
750
	/**
751
	 * Save data (either create or update depending on if we are working on an existing coupon)
752
	 * @since 2.7.0
753
	 */
754
	public function save() {
755
		if ( $this->get_id() ) {
756
			$this->update();
757
		} else {
758
			$this->create();
759
		}
760
	}
761
762
	/**
763
	 * Delete coupon from the database.
764
	 * @since 2.7.0
765
	 */
766
	public function delete() {
767
		wp_delete_post( $this->get_id() );
768
		do_action( 'woocommerce_delete_coupon', $this->get_id() );
769
	}
770
771
	/**
772
	* Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class.
773
	* @since 2.7.0
774
	* @param int $coupon_id
775
	*/
776
	private function update_post_meta( $coupon_id ) {
777
		update_post_meta( $coupon_id, 'discount_type', $this->get_discount_type() );
778
		update_post_meta( $coupon_id, 'coupon_amount', $this->get_amount() );
779
		update_post_meta( $coupon_id, 'individual_use', ( true === $this->get_individual_use() ) ? 'yes' : 'no' );
780
		update_post_meta( $coupon_id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $this->get_product_ids() ) ) ) );
781
		update_post_meta( $coupon_id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $this->get_excluded_product_ids() ) ) ) );
782
		update_post_meta( $coupon_id, 'usage_limit', $this->get_usage_limit() );
783
		update_post_meta( $coupon_id, 'usage_limit_per_user', $this->get_usage_limit_per_user() );
784
		update_post_meta( $coupon_id, 'limit_usage_to_x_items', $this->get_limit_usage_to_x_items() );
785
		update_post_meta( $coupon_id, 'usage_count', $this->get_usage_count() );
786
		update_post_meta( $coupon_id, 'expiry_date', $this->get_date_expires() );
787
		update_post_meta( $coupon_id, 'free_shipping', ( true === $this->get_free_shipping() ) ? 'yes' : 'no' );
788
		update_post_meta( $coupon_id, 'product_categories', array_filter( array_map( 'intval', $this->get_product_categories() ) ) );
789
		update_post_meta( $coupon_id, 'exclude_product_categories', array_filter( array_map( 'intval', $this->get_excluded_product_categories() ) ) );
790
		update_post_meta( $coupon_id, 'exclude_sale_items', ( true === $this->get_exclude_sale_items() ) ? 'yes' : 'no' );
791
		update_post_meta( $coupon_id, 'minimum_amount', $this->get_minimum_amount() );
792
		update_post_meta( $coupon_id, 'maximum_amount', $this->get_maximum_amount() );
793
		update_post_meta( $coupon_id, 'customer_email', array_filter( array_map( 'sanitize_email', $this->get_email_restrictions() ) ) );
794
	}
795
796
	/**
797
	 * Developers can programically return coupons. This function will read those values into our WC_Coupon class.
798
	 * @since  2.7.0
799
	 * @param  string $code  Coupon code
800
	 * @param  array $coupon Array of coupon properties
801
	 */
802
	public function read_manual_coupon( $code, $coupon ) {
803
		// 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.
804
		$convert_fields_to_array = array( 'product_ids', 'exclude_product_ids' );
805
		foreach ( $convert_fields_to_array as $field ) {
806
			if ( ! is_array( $coupon[ $field ] ) ) {
807
				_doing_it_wrong( $field, $field . ' should be an array instead of a string.', '2.7' );
808
				$coupon[ $field ] = array_filter( explode( ',', $coupon[ $field ] ) );
809
			}
810
		}
811
812
		// flip yes|no to true|false
813
		$yes_no_fields = array( 'individual_use', 'free_shipping', 'exclude_sale_items' );
814
		foreach ( $yes_no_fields as $field ) {
815
			if ( 'yes' === $coupon[ $field ] || 'no' === $coupon[ $field ] ) {
816
				_doing_it_wrong( $field, $field . ' should be true or false instead of yes or no.', '2.7' );
817
				$coupon[ $field ] = 'yes' === $coupon[ $field ];
818
			}
819
		}
820
821
		// BW compat
822
		$coupon[ 'date_expires' ]                = isset( $coupon[ 'date_expires' ] ) ? $coupon[ 'date_expires' ]                             : '';
823
		$coupon[ 'date_expires' ]                = isset( $coupon[ 'expiry_date' ] ) ? $coupon[ 'expiry_date' ]                               : $coupon[ 'date_expires' ];
824
		$coupon[ 'excluded_product_ids' ]        = isset( $coupon[ 'excluded_product_ids'] ) ? $coupon[ 'excluded_product_ids']               : '';
825
		$coupon[ 'excluded_product_ids' ]        = isset( $coupon[ 'exclude_product_ids'] ) ? $coupon[ 'exclude_product_ids']                 : $coupon[ 'excluded_product_ids'];
826
		$coupon[ 'excluded_product_categories' ] = isset( $coupon[ 'excluded_product_categories'] ) ? $coupon[ 'excluded_product_categories'] : '';
827
		$coupon[ 'excluded_product_categories' ] = isset( $coupon[ 'exclude_product_categories'] ) ? $coupon[ 'exclude_product_categories']   : $coupon[ 'excluded_product_categories'];
828
829
		$this->set_code( $code );
830
		$this->set_props( $coupon );
831
	}
832
833
	/*
834
    |--------------------------------------------------------------------------
835
    | Other Actions
836
    |--------------------------------------------------------------------------
837
    */
838
839
	/**
840
	 * Increase usage count for current coupon.
841
	 *
842
	 * @param string $used_by Either user ID or billing email
843
	 */
844
	public function inc_usage_count( $used_by = '' ) {
845
		if ( $this->get_id() ) {
846
			$this->_data['usage_count']++;
847
			update_post_meta( $this->get_id(), 'usage_count', $this->get_usage_count() );
848
			if ( $used_by ) {
849
				add_post_meta( $this->get_id(), '_used_by', strtolower( $used_by ) );
850
				$this->set_used_by( (array) get_post_meta( $this->get_id(), '_used_by' ) );
851
			}
852
		}
853
	}
854
855
	/**
856
	 * Decrease usage count for current coupon.
857
	 *
858
	 * @param string $used_by Either user ID or billing email
859
	 */
860
	public function dcr_usage_count( $used_by = '' ) {
861
		if ( $this->get_id() && $this->get_usage_count() > 0 ) {
862
			global $wpdb;
863
			$this->_data['usage_count']--;
864
			update_post_meta( $this->get_id(), 'usage_count', $this->get_usage_count() );
865
			if ( $used_by ) {
866
				/**
867
				 * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
868
				 * all instances where the key and value match, and we only want to delete one.
869
				 */
870
				$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() ) );
871
				if ( $meta_id ) {
872
					delete_metadata_by_mid( 'post', $meta_id );
873
					$this->set_used_by( (array) get_post_meta( $this->get_id(), '_used_by' ) );
874
				}
875
			}
876
		}
877
	}
878
879
	/*
880
    |--------------------------------------------------------------------------
881
    | Validation & Error Handling
882
    |--------------------------------------------------------------------------
883
    */
884
885
	/**
886
	 * Returns the error_message string.
887
	 *
888
	 * @access public
889
	 * @return string
890
	 */
891
	public function get_error_message() {
892
		return $this->error_message;
893
	}
894
895
	/**
896
	 * Ensure coupon exists or throw exception.
897
	 *
898
	 * @throws Exception
899
	 */
900
	private function validate_exists() {
901
		if ( ! $this->get_id() ) {
902
			throw new Exception( self::E_WC_COUPON_NOT_EXIST );
903
		}
904
	}
905
906
	/**
907
	 * Ensure coupon usage limit is valid or throw exception.
908
	 *
909
	 * @throws Exception
910
	 */
911
	private function validate_usage_limit() {
912
		if ( $this->get_usage_limit() > 0 && $this->get_usage_count() >= $this->get_usage_limit() ) {
913
			throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
914
		}
915
	}
916
917
	/**
918
	 * Ensure coupon user usage limit is valid or throw exception.
919
	 *
920
	 * Per user usage limit - check here if user is logged in (against user IDs).
921
	 * Checked again for emails later on in WC_Cart::check_customer_coupons().
922
	 *
923
	 * @param  int  $user_id
924
	 * @throws Exception
925
	 */
926
	private function validate_user_usage_limit( $user_id = 0 ) {
927
		if ( empty( $user_id ) ) {
928
			$user_id = get_current_user_id();
929
		}
930
		if ( $this->get_usage_limit_per_user() > 0 && is_user_logged_in() && $this->get_id() ) {
931
			global $wpdb;
932
			$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 ) );
933
934
			if ( $usage_count >= $this->get_usage_limit_per_user() ) {
935
				throw new Exception( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
936
			}
937
		}
938
	}
939
940
	/**
941
	 * Ensure coupon date is valid or throw exception.
942
	 *
943
	 * @throws Exception
944
	 */
945
	private function validate_expiry_date() {
946
		if ( $this->get_date_expires() && current_time( 'timestamp' ) > $this->get_date_expires() ) {
947
			throw new Exception( $error_code = self::E_WC_COUPON_EXPIRED );
948
		}
949
	}
950
951
	/**
952
	 * Ensure coupon amount is valid or throw exception.
953
	 *
954
	 * @throws Exception
955
	 */
956
	private function validate_minimum_amount() {
957 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...
958
			throw new Exception( self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET );
959
		}
960
	}
961
962
	/**
963
	 * Ensure coupon amount is valid or throw exception.
964
	 *
965
	 * @throws Exception
966
	 */
967
	private function validate_maximum_amount() {
968 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...
969
			throw new Exception( self::E_WC_COUPON_MAX_SPEND_LIMIT_MET );
970
		}
971
	}
972
973
	/**
974
	 * Ensure coupon is valid for products in the cart is valid or throw exception.
975
	 *
976
	 * @throws Exception
977
	 */
978 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...
979
		if ( sizeof( $this->get_product_ids() ) > 0 ) {
980
			$valid_for_cart = false;
981
			if ( ! WC()->cart->is_empty() ) {
982
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
983
					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() ) ) {
984
						$valid_for_cart = true;
985
					}
986
				}
987
			}
988
			if ( ! $valid_for_cart ) {
989
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
990
			}
991
		}
992
	}
993
994
	/**
995
	 * Ensure coupon is valid for product categories in the cart is valid or throw exception.
996
	 *
997
	 * @throws Exception
998
	 */
999 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...
1000
		if ( sizeof( $this->get_product_categories() ) > 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
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
1005
1006
					// If we find an item with a cat in our allowed cat list, the coupon is valid
1007
					if ( sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) > 0 ) {
1008
						$valid_for_cart = true;
1009
					}
1010
				}
1011
			}
1012
			if ( ! $valid_for_cart ) {
1013
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
1014
			}
1015
		}
1016
	}
1017
1018
	/**
1019
	 * Ensure coupon is valid for sale items in the cart is valid or throw exception.
1020
	 *
1021
	 * @throws Exception
1022
	 */
1023 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...
1024
		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...
1025
			$valid_for_cart      = false;
1026
			$product_ids_on_sale = wc_get_product_ids_on_sale();
1027
1028
			if ( ! WC()->cart->is_empty() ) {
1029
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1030
					if ( ! empty( $cart_item['variation_id'] ) ) {
1031
						if ( ! in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
1032
							$valid_for_cart = true;
1033
						}
1034
					} elseif ( ! in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
1035
						$valid_for_cart = true;
1036
					}
1037
				}
1038
			}
1039
			if ( ! $valid_for_cart ) {
1040
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
1041
			}
1042
		}
1043
	}
1044
1045
	/**
1046
	 * All exclusion rules must pass at the same time for a product coupon to be valid.
1047
	 */
1048
	private function validate_excluded_items() {
1049
		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...
1050
			$valid = false;
1051
1052
			foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1053
				if ( $this->is_valid_for_product( $cart_item['data'], $cart_item ) ) {
1054
					$valid = true;
1055
					break;
1056
				}
1057
			}
1058
1059
			if ( ! $valid ) {
1060
				throw new Exception( self::E_WC_COUPON_NOT_APPLICABLE );
1061
			}
1062
		}
1063
	}
1064
1065
	/**
1066
	 * Cart discounts cannot be added if non-eligble product is found in cart.
1067
	 */
1068
	private function validate_cart_excluded_items() {
1069
		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...
1070
			$this->validate_cart_excluded_product_ids();
1071
			$this->validate_cart_excluded_product_categories();
1072
			$this->validate_cart_excluded_sale_items();
1073
		}
1074
	}
1075
1076
	/**
1077
	 * Exclude products from cart.
1078
	 *
1079
	 * @throws Exception
1080
	 */
1081 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...
1082
		// Exclude Products
1083
		if ( sizeof( $this->get_excluded_product_ids() ) > 0 ) {
1084
			$valid_for_cart = true;
1085
			if ( ! WC()->cart->is_empty() ) {
1086
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1087
					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() ) ) {
1088
						$valid_for_cart = false;
1089
					}
1090
				}
1091
			}
1092
			if ( ! $valid_for_cart ) {
1093
				throw new Exception( self::E_WC_COUPON_EXCLUDED_PRODUCTS );
1094
			}
1095
		}
1096
	}
1097
1098
	/**
1099
	 * Exclude categories from cart.
1100
	 *
1101
	 * @throws Exception
1102
	 */
1103 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...
1104
		if ( sizeof( $this->get_excluded_product_categories() ) > 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
					$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
1109
					if ( sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) {
1110
						$valid_for_cart = false;
1111
					}
1112
				}
1113
			}
1114
			if ( ! $valid_for_cart ) {
1115
				throw new Exception( self::E_WC_COUPON_EXCLUDED_CATEGORIES );
1116
			}
1117
		}
1118
	}
1119
1120
	/**
1121
	 * Exclude sale items from cart.
1122
	 *
1123
	 * @throws Exception
1124
	 */
1125 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...
1126
		if ( $this->get_exclude_sale_items() ) {
1127
			$valid_for_cart = true;
1128
			$product_ids_on_sale = wc_get_product_ids_on_sale();
1129
			if ( ! WC()->cart->is_empty() ) {
1130
				foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1131
					if ( ! empty( $cart_item['variation_id'] ) ) {
1132
						if ( in_array( $cart_item['variation_id'], $product_ids_on_sale, true ) ) {
1133
							$valid_for_cart = false;
1134
						}
1135
					} elseif ( in_array( $cart_item['product_id'], $product_ids_on_sale, true ) ) {
1136
						$valid_for_cart = false;
1137
					}
1138
				}
1139
			}
1140
			if ( ! $valid_for_cart ) {
1141
				throw new Exception( self::E_WC_COUPON_NOT_VALID_SALE_ITEMS );
1142
			}
1143
		}
1144
	}
1145
1146
	/**
1147
	 * Check if a coupon is valid.
1148
	 *
1149
	 * @return boolean validity
1150
	 * @throws Exception
1151
	 */
1152
	public function is_valid() {
1153
		try {
1154
			$this->validate_exists();
1155
			$this->validate_usage_limit();
1156
			$this->validate_user_usage_limit();
1157
			$this->validate_expiry_date();
1158
			$this->validate_minimum_amount();
1159
			$this->validate_maximum_amount();
1160
			$this->validate_product_ids();
1161
			$this->validate_product_categories();
1162
			$this->validate_sale_items();
1163
			$this->validate_excluded_items();
1164
			$this->validate_cart_excluded_items();
1165
1166
			if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $this ) ) {
1167
				throw new Exception( self::E_WC_COUPON_INVALID_FILTERED );
1168
			}
1169
		} catch ( Exception $e ) {
1170
			$this->error_message = $this->get_coupon_error( $e->getMessage() );
1171
			return false;
1172
		}
1173
1174
		return true;
1175
	}
1176
1177
	/**
1178
	 * Check if a coupon is valid.
1179
	 *
1180
	 * @return bool
1181
	 */
1182
	public function is_valid_for_cart() {
1183
		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...
1184
	}
1185
1186
	/**
1187
	 * Check if a coupon is valid for a product.
1188
	 *
1189
	 * @param  WC_Product  $product
1190
	 * @return boolean
1191
	 */
1192
	public function is_valid_for_product( $product, $values = array() ) {
1193
		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...
1194
			return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values );
1195
		}
1196
1197
		$valid        = false;
1198
		$product_cats = wc_get_product_cat_ids( $product->id );
1199
		$product_ids  = array( $product->id, ( isset( $product->variation_id ) ? $product->variation_id : 0 ), $product->get_parent() );
1200
1201
		// Specific products get the discount
1202
		if ( sizeof( $this->get_product_ids() ) && sizeof( array_intersect( $product_ids, $this->get_product_ids() ) ) ) {
1203
			$valid = true;
1204
		}
1205
1206
		// Category discounts
1207
		if ( sizeof( $this->get_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_product_categories() ) ) ) {
1208
			$valid = true;
1209
		}
1210
1211
		// No product ids - all items discounted
1212
		if ( ! sizeof( $this->get_product_ids() ) && ! sizeof( $this->get_product_categories() ) ) {
1213
			$valid = true;
1214
		}
1215
1216
		// Specific product ID's excluded from the discount
1217
		if ( sizeof( $this->get_excluded_product_ids()) && sizeof( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) {
1218
			$valid = false;
1219
		}
1220
1221
		// Specific categories excluded from the discount
1222
		if ( sizeof( $this->get_excluded_product_categories() ) && sizeof( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) {
1223
			$valid = false;
1224
		}
1225
1226
		// Sale Items excluded from discount
1227
		if ( $this->get_exclude_sale_items() ) {
1228
			$product_ids_on_sale = wc_get_product_ids_on_sale();
1229
1230
			if ( isset( $product->variation_id ) ) {
1231
				if ( in_array( $product->variation_id, $product_ids_on_sale, true ) ) {
1232
					$valid = false;
1233
				}
1234
			} elseif ( in_array( $product->id, $product_ids_on_sale, true ) ) {
1235
				$valid = false;
1236
			}
1237
		}
1238
1239
		return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values );
1240
	}
1241
1242
	/**
1243
	 * Converts one of the WC_Coupon message/error codes to a message string and.
1244
	 * displays the message/error.
1245
	 *
1246
	 * @param int $msg_code Message/error code.
1247
	 */
1248
	public function add_coupon_message( $msg_code ) {
1249
		$msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code );
1250
1251
		if ( ! $msg ) {
1252
			return;
1253
		}
1254
1255
		if ( $msg_code < 200 ) {
1256
			wc_add_notice( $msg, 'error' );
1257
		} else {
1258
			wc_add_notice( $msg );
1259
		}
1260
	}
1261
1262
	/**
1263
	 * Map one of the WC_Coupon message codes to a message string.
1264
	 *
1265
	 * @param integer $msg_code
1266
	 * @return string| Message/error string
1267
	 */
1268 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...
1269
		switch ( $msg_code ) {
1270
			case self::WC_COUPON_SUCCESS :
1271
				$msg = __( 'Coupon code applied successfully.', 'woocommerce' );
1272
			break;
1273
			case self::WC_COUPON_REMOVED :
1274
				$msg = __( 'Coupon code removed successfully.', 'woocommerce' );
1275
			break;
1276
			default:
1277
				$msg = '';
1278
			break;
1279
		}
1280
		return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this );
1281
	}
1282
1283
	/**
1284
	 * Map one of the WC_Coupon error codes to a message string.
1285
	 *
1286
	 * @param int $err_code Message/error code.
1287
	 * @return string| Message/error string
1288
	 */
1289
	public function get_coupon_error( $err_code ) {
1290
		switch ( $err_code ) {
1291
			case self::E_WC_COUPON_INVALID_FILTERED:
1292
				$err = __( 'Coupon is not valid.', 'woocommerce' );
1293
			break;
1294
			case self::E_WC_COUPON_NOT_EXIST:
1295
				$err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), $this->get_code() );
1296
			break;
1297
			case self::E_WC_COUPON_INVALID_REMOVED:
1298
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), $this->get_code() );
1299
			break;
1300
			case self::E_WC_COUPON_NOT_YOURS_REMOVED:
1301
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), $this->get_code() );
1302
			break;
1303
			case self::E_WC_COUPON_ALREADY_APPLIED:
1304
				$err = __( 'Coupon code already applied!', 'woocommerce' );
1305
			break;
1306
			case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY:
1307
				$err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), $this->get_code() );
1308
			break;
1309
			case self::E_WC_COUPON_USAGE_LIMIT_REACHED:
1310
				$err = __( 'Coupon usage limit has been reached.', 'woocommerce' );
1311
			break;
1312
			case self::E_WC_COUPON_EXPIRED:
1313
				$err = __( 'This coupon has expired.', 'woocommerce' );
1314
			break;
1315
			case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET:
1316
				$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...
1317
			break;
1318
			case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET:
1319
				$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...
1320
			break;
1321
			case self::E_WC_COUPON_NOT_APPLICABLE:
1322
				$err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' );
1323
			break;
1324
			case self::E_WC_COUPON_EXCLUDED_PRODUCTS:
1325
				// Store excluded products that are in cart in $products
1326
				$products = array();
1327
				if ( ! WC()->cart->is_empty() ) {
1328
					foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1329
						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() ) ) {
1330
							$products[] = $cart_item['data']->get_title();
1331
						}
1332
					}
1333
				}
1334
1335
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) );
1336
				break;
1337
			case self::E_WC_COUPON_EXCLUDED_CATEGORIES:
1338
				// Store excluded categories that are in cart in $categories
1339
				$categories = array();
1340
				if ( ! WC()->cart->is_empty() ) {
1341
					foreach( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1342
						$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
1343
1344
						if ( sizeof( $intersect = array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) > 0 ) {
1345
1346
							foreach( $intersect as $cat_id) {
1347
								$cat = get_term( $cat_id, 'product_cat' );
1348
								$categories[] = $cat->name;
1349
							}
1350
						}
1351
					}
1352
				}
1353
1354
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) );
1355
				break;
1356
			case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS:
1357
				$err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' );
1358
			break;
1359
			default:
1360
				$err = '';
1361
			break;
1362
		}
1363
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this );
1364
	}
1365
1366
	/**
1367
	 * Map one of the WC_Coupon error codes to an error string.
1368
	 * No coupon instance will be available where a coupon does not exist,
1369
	 * so this static method exists.
1370
	 *
1371
	 * @param int $err_code Error code
1372
	 * @return string| Error string
1373
	 */
1374 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...
1375
		switch ( $err_code ) {
1376
			case self::E_WC_COUPON_NOT_EXIST:
1377
				$err = __( 'Coupon does not exist!', 'woocommerce' );
1378
			break;
1379
			case self::E_WC_COUPON_PLEASE_ENTER:
1380
				$err = __( 'Please enter a coupon code.', 'woocommerce' );
1381
			break;
1382
			default:
1383
				$err = '';
1384
			break;
1385
		}
1386
		// When using this static method, there is no $this to pass to filter
1387
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null );
1388
	}
1389
}
1390