Completed
Push — master ( fb1e2f...9a3784 )
by Justin
25:57
created

WC_Coupon::set_id()   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 1
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
		'code'                        => '',
28
		'amount'                      => 0,
29
		'date_created'                => '',
30
		'date_modified'               => '',
31
		'discount_type'               => 'fixed_cart',
32
		'description'                 => '',
33
		'date_expires'                => '',
34
		'usage_count'                 => 0,
35
		'individual_use'              => false,
36
		'product_ids'                 => array(),
37
		'excluded_product_ids'        => array(),
38
		'usage_limit'                 => 0,
39
		'usage_limit_per_user'        => 0,
40
		'limit_usage_to_x_items'      => 0,
41
		'free_shipping'               => false,
42
		'product_categories'          => array(),
43
		'excluded_product_categories' => array(),
44
		'exclude_sale_items'          => false,
45
		'minimum_amount'              => '',
46
		'maximum_amount'              => '',
47
		'email_restrictions'          => array(),
48
		'used_by'                     => '',
49
	);
50
51
	// Coupon message codes
52
	const E_WC_COUPON_INVALID_FILTERED               = 100;
53
	const E_WC_COUPON_INVALID_REMOVED                = 101;
54
	const E_WC_COUPON_NOT_YOURS_REMOVED              = 102;
55
	const E_WC_COUPON_ALREADY_APPLIED                = 103;
56
	const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104;
57
	const E_WC_COUPON_NOT_EXIST                      = 105;
58
	const E_WC_COUPON_USAGE_LIMIT_REACHED            = 106;
59
	const E_WC_COUPON_EXPIRED                        = 107;
60
	const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET        = 108;
61
	const E_WC_COUPON_NOT_APPLICABLE                 = 109;
62
	const E_WC_COUPON_NOT_VALID_SALE_ITEMS           = 110;
63
	const E_WC_COUPON_PLEASE_ENTER                   = 111;
64
	const E_WC_COUPON_MAX_SPEND_LIMIT_MET 			 = 112;
65
	const E_WC_COUPON_EXCLUDED_PRODUCTS              = 113;
66
	const E_WC_COUPON_EXCLUDED_CATEGORIES            = 114;
67
	const WC_COUPON_SUCCESS                          = 200;
68
	const WC_COUPON_REMOVED                          = 201;
69
70
	/**
71
	 * Internal meta type used to store coupon data.
72
	 * @since 2.7.0
73
	 * @var string
74
	 */
75
	protected $meta_type = 'post';
76
77
	/**
78
	 * Data stored in meta keys, but not considered "meta" for a coupon.
79
	 * @since 2.7.0
80
	 * @var array
81
	 */
82
	protected $internal_meta_keys = array(
83
		'discount_type',
84
		'coupon_amount',
85
		'expiry_date',
86
		'usage_count',
87
		'individual_use',
88
		'product_ids',
89
		'exclude_product_ids',
90
		'usage_limit',
91
		'usage_limit_per_user',
92
		'limit_usage_to_x_items',
93
		'free_shipping',
94
		'product_categories',
95
		'exclude_product_categories',
96
		'exclude_sale_items',
97
		'minimum_amount',
98
		'maximum_amount',
99
		'customer_email',
100
		'_used_by',
101
		'_edit_lock',
102
		'_edit_last',
103
	);
104
105
	/**
106
	 * Coupon constructor. Loads coupon data.
107
	 * @param mixed $data Coupon data, object, ID or code.
108
	 */
109
	public function __construct( $data = '' ) {
110
		parent::__construct( $data );
111
112
		if ( $data instanceof WC_Coupon ) {
113
			$this->read( absint( $data->get_id() ) );
114
		} elseif ( $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data ) ) {
115
			_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' );
116
			$this->read_manual_coupon( $data, $coupon );
117
		} elseif ( is_numeric( $data ) && 'shop_coupon' === get_post_type( $data ) ) {
118
			$this->read( $data );
119
		} elseif ( ! empty( $data ) ) {
120
			$this->read( wc_get_coupon_id_by_code( $data ) );
121
			$this->set_code( $data );
122
		}
123
	}
124
125
	/**
126
	 * Checks the coupon type.
127
	 * @param  string $type Array or string of types
128
	 * @return bool
129
	 */
130
	public function is_type( $type ) {
131
		return ( $this->get_discount_type() == $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type ) ) );
132
	}
133
134
	/*
135
	|--------------------------------------------------------------------------
136
	| Getters
137
	|--------------------------------------------------------------------------
138
	|
139
	| Methods for getting data from the coupon object.
140
	|
141
	*/
142
143
	/**
144
	 * Get coupon code.
145
	 * @since  2.7.0
146
	 * @return string
147
	 */
148
	public function get_code() {
149
		return $this->data['code'];
150
	}
151
152
	/**
153
	 * Get coupon description.
154
	 * @since  2.7.0
155
	 * @return string
156
	 */
157
	public function get_description() {
158
		return $this->data['description'];
159
	}
160
161
	/**
162
	 * Get discount type.
163
	 * @since  2.7.0
164
	 * @return string
165
	 */
166
	public function get_discount_type() {
167
		return $this->data['discount_type'];
168
	}
169
170
	/**
171
	 * Get coupon code.
172
	 * @since  2.7.0
173
	 * @return float
174
	 */
175
	public function get_amount() {
176
		return wc_format_decimal( $this->data['amount'] );
177
	}
178
179
	/**
180
	 * Get coupon expiration date.
181
	 * @since  2.7.0
182
	 * @return int
183
	 */
184
	public function get_date_expires() {
185
		return $this->data['date_expires'];
186
	}
187
188
	/**
189
	 * Get date_created
190
	 * @since 2.7.0
191
	 * @return int
192
	 */
193
	public function get_date_created() {
194
		return $this->data['date_created'];
195
	}
196
197
	/**
198
	 * Get date_modified
199
	 * @since 2.7.0
200
	 * @return int
201
	 */
202
	public function get_date_modified() {
203
		return $this->data['date_modified'];
204
	}
205
206
	/**
207
	 * Get coupon usage count.
208
	 * @since  2.7.0
209
	 * @return integer
210
	 */
211
	public function get_usage_count() {
212
		return $this->data['usage_count'];
213
	}
214
215
	/**
216
	 * Get the "indvidual use" checkbox status.
217
	 * @since  2.7.0
218
	 * @return bool
219
	 */
220
	public function get_individual_use() {
221
		return $this->data['individual_use'];
222
	}
223
224
	/**
225
	 * Get product IDs this coupon can apply to.
226
	 * @since  2.7.0
227
	 * @return array
228
	 */
229
	public function get_product_ids() {
230
		return $this->data['product_ids'];
231
	}
232
233
	/**
234
	 * Get product IDs that this coupon should not apply to.
235
	 * @since  2.7.0
236
	 * @return array
237
	 */
238
	public function get_excluded_product_ids() {
239
		return $this->data['excluded_product_ids'];
240
	}
241
242
	/**
243
	 * Get coupon usage limit.
244
	 * @since  2.7.0
245
	 * @return integer
246
	 */
247
	public function get_usage_limit() {
248
		return $this->data['usage_limit'];
249
	}
250
251
	/**
252
	 * Get coupon usage limit per customer (for a single customer)
253
	 * @since  2.7.0
254
	 * @return integer
255
	 */
256
	public function get_usage_limit_per_user() {
257
		return $this->data['usage_limit_per_user'];
258
	}
259
260
	/**
261
	 * Usage limited to certain amount of items
262
	 * @since  2.7.0
263
	 * @return integer
264
	 */
265
	public function get_limit_usage_to_x_items() {
266
		return $this->data['limit_usage_to_x_items'];
267
	}
268
269
	/**
270
	 * If this coupon grants free shipping or not.
271
	 * @since  2.7.0
272
	 * @return bool
273
	 */
274
	public function get_free_shipping() {
275
		return $this->data['free_shipping'];
276
	}
277
278
	/**
279
	 * Get product categories this coupon can apply to.
280
	 * @since  2.7.0
281
	 * @return array
282
	 */
283
	public function get_product_categories() {
284
		return $this->data['product_categories'];
285
	}
286
287
	/**
288
	 * Get product categories this coupon cannot not apply to.
289
	 * @since  2.7.0
290
	 * @return array
291
	 */
292
	public function get_excluded_product_categories() {
293
		return $this->data['excluded_product_categories'];
294
	}
295
296
	/**
297
	 * If this coupon should exclude items on sale.
298
	 * @since  2.7.0
299
	 * @return bool
300
	 */
301
	public function get_exclude_sale_items() {
302
		return $this->data['exclude_sale_items'];
303
	}
304
305
	/**
306
	 * Get minium spend amount.
307
	 * @since  2.7.0
308
	 * @return float
309
	 */
310
	public function get_minimum_amount() {
311
		return wc_format_decimal( $this->data['minimum_amount'] );
312
	}
313
	/**
314
	 * Get maximum spend amount.
315
	 * @since  2.7.0
316
	 * @return float
317
	 */
318
	public function get_maximum_amount() {
319
		return wc_format_decimal( $this->data['maximum_amount'] );
320
	}
321
322
	/**
323
	 * Get emails to check customer usage restrictions.
324
	 * @since  2.7.0
325
	 * @return array
326
	 */
327
	public function get_email_restrictions() {
328
		return $this->data['email_restrictions'];
329
	}
330
331
	/**
332
	 * Get records of all users who have used the current coupon.
333
	 *
334
	 * @return array
335
	 */
336
	public function get_used_by() {
337
		return $this->data['used_by'];
338
	}
339
340
	/**
341
	 * Get discount amount for a cart item.
342
	 *
343
	 * @param  float $discounting_amount Amount the coupon is being applied to
344
	 * @param  array|null $cart_item Cart item being discounted if applicable
345
	 * @param  boolean $single True if discounting a single qty item, false if its the line
346
	 * @return float Amount this coupon has discounted
347
	 */
348
	public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) {
349
		$discount      = 0;
350
		$cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity'];
351
352
		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...
353
			$discount = $this->get_amount() * ( $discounting_amount / 100 );
354
		} elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) {
355
			/**
356
			 * This is the most complex discount - we need to divide the discount between rows based on their price in.
357
			 * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows.
358
			 * with no price (free) don't get discounted.
359
			 *
360
			 * Get item discount by dividing item cost by subtotal to get a %.
361
			 *
362
			 * 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.
363
			 */
364
			if ( wc_prices_include_tax() ) {
365
				$discount_percent = ( $cart_item['data']->get_price_including_tax() * $cart_item_qty ) / WC()->cart->subtotal;
366
			} else {
367
				$discount_percent = ( $cart_item['data']->get_price_excluding_tax() * $cart_item_qty ) / WC()->cart->subtotal_ex_tax;
368
			}
369
			$discount = ( $this->get_amount() * $discount_percent ) / $cart_item_qty;
370
371
		} elseif ( $this->is_type( 'fixed_product' ) ) {
372
			$discount = min( $this->get_amount(), $discounting_amount );
373
			$discount = $single ? $discount : $discount * $cart_item_qty;
374
		}
375
376
		$discount = min( $discount, $discounting_amount );
377
378
		// Handle the limit_usage_to_x_items option
379
		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...
380
			if ( $discounting_amount ) {
381
				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...
382
					$limit_usage_qty = $cart_item_qty;
383
				} else {
384
					$limit_usage_qty = min( $this->get_limit_usage_to_x_items(), $cart_item_qty );
385
					$this->set_limit_usage_to_x_items( max( 0, $this->get_limit_usage_to_x_items() - $limit_usage_qty ) );
386
				}
387
				if ( $single ) {
388
					$discount = ( $discount * $limit_usage_qty ) / $cart_item_qty;
389
				} else {
390
					$discount = ( $discount / $cart_item_qty ) * $limit_usage_qty;
391
				}
392
			}
393
		}
394
395
		$discount = round( $discount, wc_get_rounding_precision() );
396
397
		return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this );
398
	}
399
400
	/*
401
	|--------------------------------------------------------------------------
402
	| Setters
403
	|--------------------------------------------------------------------------
404
	|
405
	| Functions for setting coupon data. These should not update anything in the
406
	| database itself and should only change what is stored in the class
407
	| object.
408
	|
409
	*/
410
411
	/**
412
	 * Set coupon code.
413
	 * @since  2.7.0
414
	 * @param  string $code
415
	 * @throws WC_Data_Exception
416
	 */
417
	public function set_code( $code ) {
418
		$this->data['code'] = apply_filters( 'woocommerce_coupon_code', $code );
419
	}
420
421
	/**
422
	 * Set coupon description.
423
	 * @since  2.7.0
424
	 * @param  string $description
425
	 * @throws WC_Data_Exception
426
	 */
427
	public function set_description( $description ) {
428
		$this->data['description'] = $description;
429
	}
430
431
	/**
432
	 * Set discount type.
433
	 * @since  2.7.0
434
	 * @param  string $discount_type
435
	 * @throws WC_Data_Exception
436
	 */
437
	public function set_discount_type( $discount_type ) {
438
		if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ) ) ) {
439
			$this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) );
440
		}
441
		$this->data['discount_type'] = $discount_type;
442
	}
443
444
	/**
445
	 * Set amount.
446
	 * @since  2.7.0
447
	 * @param  float $amount
448
	 * @throws WC_Data_Exception
449
	 */
450
	public function set_amount( $amount ) {
451
		$this->data['amount'] = wc_format_decimal( $amount );
452
	}
453
454
	/**
455
	 * Set expiration date.
456
	 * @since  2.7.0
457
	 * @param string $timestamp Timestamp
458
	 * @throws WC_Data_Exception
459
	 */
460
	public function set_date_expires( $timestamp ) {
461
		$this->data['date_expires'] = is_numeric( $timestamp ) ? $timestamp : strtotime( $timestamp );
462
	}
463
464
	/**
465
	 * Set date_created
466
	 * @since  2.7.0
467
	 * @param string $timestamp Timestamp
468
	 * @throws WC_Data_Exception
469
	 */
470
	public function set_date_created( $timestamp ) {
471
		$this->data['date_created'] = is_numeric( $timestamp ) ? $timestamp : strtotime( $timestamp );
472
	}
473
474
	/**
475
	 * Set date_modified
476
	 * @since  2.7.0
477
	 * @param string $timestamp
478
	 * @throws WC_Data_Exception
479
	 */
480
	public function set_date_modified( $timestamp ) {
481
		$this->data['date_modified'] = is_numeric( $timestamp ) ? $timestamp : strtotime( $timestamp );
482
	}
483
484
	/**
485
	 * Set how many times this coupon has been used.
486
	 * @since  2.7.0
487
	 * @param  int $usage_count
488
	 * @throws WC_Data_Exception
489
	 */
490
	public function set_usage_count( $usage_count ) {
491
		$this->data['usage_count'] = absint( $usage_count );
492
	}
493
494
	/**
495
	 * Set if this coupon can only be used once.
496
	 * @since  2.7.0
497
	 * @param  bool $is_individual_use
498
	 * @throws WC_Data_Exception
499
	 */
500
	public function set_individual_use( $is_individual_use ) {
501
		$this->data['individual_use'] = (bool) $is_individual_use;
502
	}
503
504
	/**
505
	 * Set the product IDs this coupon can be used with.
506
	 * @since  2.7.0
507
	 * @param  array $product_ids
508
	 * @throws WC_Data_Exception
509
	 */
510
	public function set_product_ids( $product_ids ) {
511
		$this->data['product_ids'] = (array) $product_ids;
512
	}
513
514
	/**
515
	 * Set the product IDs this coupon cannot be used with.
516
	 * @since  2.7.0
517
	 * @param  array $excluded_product_ids
518
	 * @throws WC_Data_Exception
519
	 */
520
	public function set_excluded_product_ids( $excluded_product_ids ) {
521
		$this->data['excluded_product_ids'] = (array) $excluded_product_ids;
522
	}
523
524
	/**
525
	 * Set the amount of times this coupon can be used.
526
	 * @since  2.7.0
527
	 * @param  int $usage_limit
528
	 * @throws WC_Data_Exception
529
	 */
530
	public function set_usage_limit( $usage_limit ) {
531
		$this->data['usage_limit'] = absint( $usage_limit );
532
	}
533
534
	/**
535
	 * Set the amount of times this coupon can be used per user.
536
	 * @since  2.7.0
537
	 * @param  int $usage_limit
538
	 * @throws WC_Data_Exception
539
	 */
540
	public function set_usage_limit_per_user( $usage_limit ) {
541
		$this->data['usage_limit_per_user'] = absint( $usage_limit );
542
	}
543
544
	/**
545
	 * Set usage limit to x number of items.
546
	 * @since  2.7.0
547
	 * @param  int $limit_usage_to_x_items
548
	 * @throws WC_Data_Exception
549
	 */
550
	public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) {
551
		$this->data['limit_usage_to_x_items'] = $limit_usage_to_x_items;
552
	}
553
554
	/**
555
	 * Set if this coupon enables free shipping or not.
556
	 * @since  2.7.0
557
	 * @param  bool $free_shipping
558
	 * @throws WC_Data_Exception
559
	 */
560
	public function set_free_shipping( $free_shipping ) {
561
		$this->data['free_shipping'] = (bool) $free_shipping;
562
	}
563
564
	/**
565
	 * Set the product category IDs this coupon can be used with.
566
	 * @since  2.7.0
567
	 * @param  array $product_categories
568
	 * @throws WC_Data_Exception
569
	 */
570
	public function set_product_categories( $product_categories ) {
571
		$this->data['product_categories'] = (array) $product_categories;
572
	}
573
574
	/**
575
	 * Set the product category IDs this coupon cannot be used with.
576
	 * @since  2.7.0
577
	 * @param  array $excluded_product_categories
578
	 * @throws WC_Data_Exception
579
	 */
580
	public function set_excluded_product_categories( $excluded_product_categories ) {
581
		$this->data['excluded_product_categories'] = (array) $excluded_product_categories;
582
	}
583
584
	/**
585
	 * Set if this coupon should excluded sale items or not.
586
	 * @since  2.7.0
587
	 * @param  bool $exclude_sale_items
588
	 * @throws WC_Data_Exception
589
	 */
590
	public function set_exclude_sale_items( $exclude_sale_items ) {
591
		$this->data['exclude_sale_items'] = (bool) $exclude_sale_items;
592
	}
593
594
	/**
595
	 * Set the minimum spend amount.
596
	 * @since  2.7.0
597
	 * @param  float $amount
598
	 * @throws WC_Data_Exception
599
	 */
600
	public function set_minimum_amount( $amount ) {
601
		$this->data['minimum_amount'] = wc_format_decimal( $amount );
602
	}
603
604
	/**
605
	 * Set the maximum spend amount.
606
	 * @since  2.7.0
607
	 * @param  float $amount
608
	 * @throws WC_Data_Exception
609
	 */
610
	public function set_maximum_amount( $amount ) {
611
		$this->data['maximum_amount'] = wc_format_decimal( $amount );
612
	}
613
614
	/**
615
	 * Set email restrictions.
616
	 * @since  2.7.0
617
	 * @param  array $emails
618
	 * @throws WC_Data_Exception
619
	 */
620
	public function set_email_restrictions( $emails = array() ) {
621
		$emails = array_filter( array_map( 'sanitize_email', (array) $emails ) );
622
		foreach ( $emails as $email ) {
623
			if ( ! is_email( $email ) ) {
624
				$this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) );
625
			}
626
		}
627
		$this->data['email_restrictions'] = $emails;
628
	}
629
630
	/**
631
	 * Set which users have used this coupon.
632
	 * @since 2.7.0
633
	 * @param array $used_by
634
	 * @throws WC_Data_Exception
635
	 */
636
	public function set_used_by( $used_by ) {
637
		$this->data['used_by'] = array_filter( $used_by );
638
	}
639
640
	/*
641
	|--------------------------------------------------------------------------
642
	| CRUD methods
643
	|--------------------------------------------------------------------------
644
	|
645
	| Methods which create, read, update and delete coupons from the database.
646
	|
647
	| A save method is included for convenience (chooses update or create based
648
	| on if the order exists yet).
649
	|
650
	*/
651
652
	/**
653
	 * Reads an coupon from the database and sets its data to the class.
654
	 * @since 2.7.0
655
	 * @param  int $coupon_id
656
	 */
657
	public function read( $coupon_id ) {
658
		$this->set_defaults();
659
660
		if ( ! $coupon_id || ! ( $post_object = get_post( $coupon_id ) ) ) {
661
			return;
662
		}
663
664
		$this->set_id( $coupon_id );
665
		$this->set_props( array(
666
			'code'                        => $post_object->post_title,
667
			'description'                 => $post_object->post_excerpt,
668
			'date_created'                => $post_object->post_date,
669
			'date_modified'               => $post_object->post_modified,
670
			'date_expires'                => get_post_meta( $coupon_id, 'expiry_date', true ),
671
			'discount_type'               => get_post_meta( $coupon_id, 'discount_type', true ),
672
			'amount'                      => get_post_meta( $coupon_id, 'coupon_amount', true ),
673
			'usage_count'                 => get_post_meta( $coupon_id, 'usage_count', true ),
674
			'individual_use'              => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ),
675
			'product_ids'                 => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ),
676
			'excluded_product_ids'        => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ),
677
			'usage_limit'                 => get_post_meta( $coupon_id, 'usage_limit', true ),
678
			'usage_limit_per_user'        => get_post_meta( $coupon_id, 'usage_limit_per_user', true ),
679
			'limit_usage_to_x_items'      => get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ),
680
			'free_shipping'               => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ),
681
			'product_categories'          => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ),
682
			'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ),
683
			'exclude_sale_items'          => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ),
684
			'minimum_amount'              => get_post_meta( $coupon_id, 'minimum_amount', true ),
685
			'maximum_amount'              => get_post_meta( $coupon_id, 'maximum_amount', true ),
686
			'email_restrictions'          => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
687
			'used_by'                     => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
688
		) );
689
		$this->read_meta_data();
690
691
		do_action( 'woocommerce_coupon_loaded', $this );
692
	}
693
694
	/**
695
	 * Create a new coupon.
696
	 * @since 2.7.0
697
	 */
698
	public function create() {
699
		$this->set_date_created( current_time( 'timestamp' ) );
700
701
		$coupon_id = wp_insert_post( apply_filters( 'woocommerce_new_coupon_data', array(
702
			'post_type'     => 'shop_coupon',
703
			'post_status'   => 'publish',
704
			'post_author'   => get_current_user_id(),
705
			'post_title'    => $this->get_code(),
706
			'post_content'  => '',
707
			'post_excerpt'  => $this->get_description(),
708
			'post_date'     => date( 'Y-m-d H:i:s', $this->get_date_created() ),
709
			'post_date_gmt' => get_gmt_from_date( date( 'Y-m-d H:i:s', $this->get_date_created() ) ),
710
		) ), true );
711
712
		if ( $coupon_id ) {
713
			$this->set_id( $coupon_id );
714
			$this->update_post_meta( $coupon_id );
715
			$this->save_meta_data();
716
			do_action( 'woocommerce_new_coupon', $coupon_id );
717
		}
718
	}
719
720
	/**
721
	 * Updates an existing coupon.
722
	 * @since 2.7.0
723
	 */
724
	public function update() {
725
		$coupon_id = $this->get_id();
726
727
		$post_data = array(
728
			'ID'           => $coupon_id,
729
			'post_title'   => $this->get_code(),
730
			'post_excerpt' => $this->get_description(),
731
		);
732
733
		wp_update_post( $post_data );
734
		$this->update_post_meta( $coupon_id );
735
		$this->save_meta_data();
736
		do_action( 'woocommerce_update_coupon', $coupon_id );
737
	}
738
739
	/**
740
	 * Save data (either create or update depending on if we are working on an existing coupon)
741
	 * @since 2.7.0
742
	 */
743
	public function save() {
744
		if ( $this->get_id() ) {
745
			$this->update();
746
		} else {
747
			$this->create();
748
		}
749
	}
750
751
	/**
752
	 * Delete coupon from the database.
753
	 * @since 2.7.0
754
	 */
755
	public function delete() {
756
		wp_delete_post( $this->get_id() );
757
		do_action( 'woocommerce_delete_coupon', $this->get_id() );
758
		$this->set_id( 0 );
759
	}
760
761
	/**
762
	 * Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class.
763
	 * @since 2.7.0
764
	 * @param int $coupon_id
765
	 */
766
	private function update_post_meta( $coupon_id ) {
767
		update_post_meta( $coupon_id, 'discount_type', $this->get_discount_type() );
768
		update_post_meta( $coupon_id, 'coupon_amount', $this->get_amount() );
769
		update_post_meta( $coupon_id, 'individual_use', ( true === $this->get_individual_use() ) ? 'yes' : 'no' );
770
		update_post_meta( $coupon_id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $this->get_product_ids() ) ) ) );
771
		update_post_meta( $coupon_id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $this->get_excluded_product_ids() ) ) ) );
772
		update_post_meta( $coupon_id, 'usage_limit', $this->get_usage_limit() );
773
		update_post_meta( $coupon_id, 'usage_limit_per_user', $this->get_usage_limit_per_user() );
774
		update_post_meta( $coupon_id, 'limit_usage_to_x_items', $this->get_limit_usage_to_x_items() );
775
		update_post_meta( $coupon_id, 'usage_count', $this->get_usage_count() );
776
		update_post_meta( $coupon_id, 'expiry_date', $this->get_date_expires() );
777
		update_post_meta( $coupon_id, 'free_shipping', ( true === $this->get_free_shipping() ) ? 'yes' : 'no' );
778
		update_post_meta( $coupon_id, 'product_categories', array_filter( array_map( 'intval', $this->get_product_categories() ) ) );
779
		update_post_meta( $coupon_id, 'exclude_product_categories', array_filter( array_map( 'intval', $this->get_excluded_product_categories() ) ) );
780
		update_post_meta( $coupon_id, 'exclude_sale_items', ( true === $this->get_exclude_sale_items() ) ? 'yes' : 'no' );
781
		update_post_meta( $coupon_id, 'minimum_amount', $this->get_minimum_amount() );
782
		update_post_meta( $coupon_id, 'maximum_amount', $this->get_maximum_amount() );
783
		update_post_meta( $coupon_id, 'customer_email', array_filter( array_map( 'sanitize_email', $this->get_email_restrictions() ) ) );
784
	}
785
786
	/**
787
	 * Developers can programically return coupons. This function will read those values into our WC_Coupon class.
788
	 * @since  2.7.0
789
	 * @param  string $code  Coupon code
790
	 * @param  array $coupon Array of coupon properties
791
	 */
792
	public function read_manual_coupon( $code, $coupon ) {
793
		foreach ( $coupon as $key => $value ) {
794
			switch ( $key ) {
795
				case 'excluded_product_ids' :
796
				case 'exclude_product_ids' :
797
					if ( ! is_array( $coupon[ $key ] ) ) {
798
						_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '2.7' );
799
						$coupon['excluded_product_ids'] = wc_string_to_array( $value );
800
					}
801
					break;
802
				case 'exclude_product_categories' :
803 View Code Duplication
				case 'excluded_product_categories' :
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...
804
					if ( ! is_array( $coupon[ $key ] ) ) {
805
						_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '2.7' );
806
						$coupon['excluded_product_categories'] = wc_string_to_array( $value );
807
					}
808
					break;
809 View Code Duplication
				case 'product_ids' :
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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