Completed
Pull Request — master (#11613)
by Mike
08:58
created

WC_Coupon::get_individual_use()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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