Completed
Push — master ( 9b8056...335a5e )
by Mike
15:35
created

WC_Coupon::read_manual_coupon()   C

Complexity

Conditions 15
Paths 19

Size

Total Lines 39
Code Lines 32

Duplication

Lines 12
Ratio 30.77 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 15
eloc 32
c 4
b 1
f 0
nc 19
nop 2
dl 12
loc 39
rs 5.0504

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
include_once( 'legacy/class-wc-legacy-coupon.php' );
3
4
if ( ! defined( 'ABSPATH' ) ) {
5
	exit;
6
}
7
8
/**
9
 * WooCommerce coupons.
10
 *
11
 * The WooCommerce coupons class gets coupon data from storage and checks coupon validity.
12
 *
13
 * @class 		WC_Coupon
14
 * @version		2.7.0
15
 * @package		WooCommerce/Classes
16
 * @category	Class
17
 * @author		WooThemes
18
 */
19
class WC_Coupon extends WC_Legacy_Coupon {
20
21
	/**
22
	 * Data array, with defaults.
23
	 * @since 2.7.0
24
	 * @var array
25
	 */
26
	protected $_data = array(
27
		'id'                          => 0,
28
		'code'                        => '',
29
		'amount'                      => 0,
30
		'date_created'                => '',
31
		'date_modified'               => '',
32
		'discount_type'               => 'fixed_cart',
33
		'description'                 => '',
34
		'date_expires'                => '',
35
		'usage_count'                 => 0,
36
		'individual_use'              => false,
37
		'product_ids'                 => array(),
38
		'excluded_product_ids'        => array(),
39
		'usage_limit'                 => 0,
40
		'usage_limit_per_user'        => 0,
41
		'limit_usage_to_x_items'      => 0,
42
		'free_shipping'               => false,
43
		'product_categories'          => array(),
44
		'excluded_product_categories' => array(),
45
		'exclude_sale_items'          => false,
46
		'minimum_amount'              => '',
47
		'maximum_amount'              => '',
48
		'email_restrictions'          => array(),
49
		'used_by'                     => '',
50
	);
51
52
	// Coupon message codes
53
	const E_WC_COUPON_INVALID_FILTERED               = 100;
54
	const E_WC_COUPON_INVALID_REMOVED                = 101;
55
	const E_WC_COUPON_NOT_YOURS_REMOVED              = 102;
56
	const E_WC_COUPON_ALREADY_APPLIED                = 103;
57
	const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104;
58
	const E_WC_COUPON_NOT_EXIST                      = 105;
59
	const E_WC_COUPON_USAGE_LIMIT_REACHED            = 106;
60
	const E_WC_COUPON_EXPIRED                        = 107;
61
	const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET        = 108;
62
	const E_WC_COUPON_NOT_APPLICABLE                 = 109;
63
	const E_WC_COUPON_NOT_VALID_SALE_ITEMS           = 110;
64
	const E_WC_COUPON_PLEASE_ENTER                   = 111;
65
	const E_WC_COUPON_MAX_SPEND_LIMIT_MET 			 = 112;
66
	const E_WC_COUPON_EXCLUDED_PRODUCTS              = 113;
67
	const E_WC_COUPON_EXCLUDED_CATEGORIES            = 114;
68
	const WC_COUPON_SUCCESS                          = 200;
69
	const WC_COUPON_REMOVED                          = 201;
70
71
	/**
72
	 * Internal meta type used to store coupon data.
73
	 * @since 2.7.0
74
	 * @var string
75
	 */
76
	protected $_meta_type = 'post';
77
78
	/**
79
	 * Data stored in meta keys, but not considered "meta" for a coupon.
80
	 * @since 2.7.0
81
	 * @var array
82
	 */
83
	protected $_internal_meta_keys = array(
84
		'discount_type', 'coupon_amount', 'expiry_date', 'usage_count',
85
		'individual_use', 'product_ids', 'exclude_product_ids', 'usage_limit',
86
		'usage_limit_per_user', 'limit_usage_to_x_items', 'free_shipping',
87
		'product_categories', 'exclude_product_categories', 'exclude_sale_items',
88
		'minimum_amount', 'maximum_amount', 'customer_email', '_used_by',
89
		'_edit_lock', '_edit_last',
90
	);
91
92
	/**
93
	 * Coupon constructor. Loads coupon data.
94
	 * @param mixed $data Coupon data, object, ID or code.
95
	 */
96
	public function __construct( $data = '' ) {
97
		parent::__construct( $data );
98
99
		if ( $data instanceof WC_Coupon ) {
100
			$this->read( absint( $data->get_id() ) );
101
		} elseif ( $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data ) ) {
102
			_doing_it_wrong( 'woocommerce_get_shop_coupon_data', 'Reading a manual coupon via woocommerce_get_shop_coupon_data has been deprecated. Please sent an instance of WC_Coupon instead.', '2.7' );
103
			$this->read_manual_coupon( $data, $coupon );
104
		} elseif ( is_numeric( $data ) && 'shop_coupon' === get_post_type( $data ) ) {
105
			$this->read( $data );
106
		} elseif ( ! empty( $data ) ) {
107
			$this->set_code( $data );
108
			$this->read( wc_get_coupon_id_by_code( $data ) );
109
		}
110
	}
111
112
	/**
113
	 * Checks the coupon type.
114
	 * @param  string $type Array or string of types
115
	 * @return bool
116
	 */
117
	public function is_type( $type ) {
118
		return ( $this->get_discount_type() == $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type ) ) );
119
	}
120
121
	/*
122
    |--------------------------------------------------------------------------
123
    | Getters
124
    |--------------------------------------------------------------------------
125
    |
126
    | Methods for getting data from the coupon object.
127
    |
128
    */
129
130
   /**
131
    * Get coupon ID.
132
    * @since  2.7.0
133
    * @return integer
134
    */
135
	public function get_id() {
136
		return $this->_data['id'];
137
	}
138
139
	/**
140
	 * Get coupon code.
141
	 * @since  2.7.0
142
	 * @return string
143
	 */
144
	public function get_code() {
145
		return $this->_data['code'];
146
	}
147
148
	/**
149
	 * Get coupon description.
150
	 * @since  2.7.0
151
	 * @return string
152
	 */
153
	public function get_description() {
154
		return $this->_data['description'];
155
	}
156
157
	/**
158
	 * Get discount type.
159
	 * @since  2.7.0
160
	 * @return string
161
	 */
162
	public function get_discount_type() {
163
		return $this->_data['discount_type'];
164
	}
165
166
	/**
167
	 * Get coupon code.
168
	 * @since  2.7.0
169
	 * @return float
170
	 */
171
	public function get_amount() {
172
		return wc_format_decimal( $this->_data['amount'] );
173
	}
174
175
	/**
176
	 * Get coupon expiration date.
177
	 * @since  2.7.0
178
	 * @return int
179
	 */
180
	public function get_date_expires() {
181
		return $this->_data['date_expires'];
182
	}
183
184
	/**
185
	 * Get date_created
186
	 * @since 2.7.0
187
	 * @return int
188
	 */
189
	public function get_date_created() {
190
		return $this->_data['date_created'];
191
	}
192
193
	/**
194
	 * Get date_modified
195
	 * @since 2.7.0
196
	 * @return int
197
	 */
198
	public function get_date_modified() {
199
		return $this->_data['date_modified'];
200
	}
201
202
	/**
203
	 * Get coupon usage count.
204
	 * @since  2.7.0
205
	 * @return integer
206
	 */
207
	public function get_usage_count() {
208
		return $this->_data['usage_count'];
209
	}
210
211
	/**
212
	 * Get the "indvidual use" checkbox status.
213
	 * @since  2.7.0
214
	 * @return bool
215
	 */
216
	public function get_individual_use() {
217
		return $this->_data['individual_use'];
218
	}
219
220
	/**
221
	 * Get product IDs this coupon can apply to.
222
	 * @since  2.7.0
223
	 * @return array
224
	 */
225
	public function get_product_ids() {
226
		return $this->_data['product_ids'];
227
	}
228
229
	/**
230
	 * Get product IDs that this coupon should not apply to.
231
	 * @since  2.7.0
232
	 * @return array
233
	 */
234
	public function get_excluded_product_ids() {
235
		return $this->_data['excluded_product_ids'];
236
	}
237
238
	/**
239
	 * Get coupon usage limit.
240
	 * @since  2.7.0
241
	 * @return integer
242
	 */
243
	public function get_usage_limit() {
244
		return $this->_data['usage_limit'];
245
	}
246
247
	/**
248
	 * Get coupon usage limit per customer (for a single customer)
249
	 * @since  2.7.0
250
	 * @return integer
251
	 */
252
	public function get_usage_limit_per_user() {
253
		return $this->_data['usage_limit_per_user'];
254
	}
255
256
	/**
257
	 * Usage limited to certain amount of items
258
	 * @since  2.7.0
259
	 * @return integer
260
	 */
261
	public function get_limit_usage_to_x_items() {
262
		return $this->_data['limit_usage_to_x_items'];
263
	}
264
265
	/**
266
	 * If this coupon grants free shipping or not.
267
	 * @since  2.7.0
268
	 * @return bool
269
	 */
270
	public function get_free_shipping() {
271
		return $this->_data['free_shipping'];
272
	}
273
274
	/**
275
	 * Get product categories this coupon can apply to.
276
	 * @since  2.7.0
277
	 * @return array
278
	 */
279
	public function get_product_categories() {
280
		return $this->_data['product_categories'];
281
	}
282
283
	/**
284
	 * Get product categories this coupon cannot not apply to.
285
	 * @since  2.7.0
286
	 * @return array
287
	 */
288
	public function get_excluded_product_categories() {
289
		return $this->_data['excluded_product_categories'];
290
	}
291
292
	/**
293
	 * If this coupon should exclude items on sale.
294
	 * @since  2.7.0
295
	 * @return bool
296
	 */
297
	public function get_exclude_sale_items() {
298
		return $this->_data['exclude_sale_items'];
299
	}
300
301
	/**
302
	 * Get minium spend amount.
303
	 * @since  2.7.0
304
	 * @return float
305
	 */
306
	public function get_minimum_amount() {
307
		return wc_format_decimal( $this->_data['minimum_amount'] );
308
	}
309
	/**
310
	 * Get maximum spend amount.
311
	 * @since  2.7.0
312
	 * @return float
313
	 */
314
	public function get_maximum_amount() {
315
		return wc_format_decimal( $this->_data['maximum_amount'] );
316
	}
317
318
	/**
319
	 * Get emails to check customer usage restrictions.
320
	 * @since  2.7.0
321
	 * @return array
322
	 */
323
	public function get_email_restrictions() {
324
		return $this->_data['email_restrictions'];
325
	}
326
327
	/**
328
	 * Get records of all users who have used the current coupon.
329
	 *
330
	 * @return array
331
	 */
332
	public function get_used_by() {
333
		return $this->_data['used_by'];
334
	}
335
336
	/**
337
	 * Get discount amount for a cart item.
338
	 *
339
	 * @param  float $discounting_amount Amount the coupon is being applied to
340
	 * @param  array|null $cart_item Cart item being discounted if applicable
341
	 * @param  boolean $single True if discounting a single qty item, false if its the line
342
	 * @return float Amount this coupon has discounted
343
	 */
344
	public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) {
345
		$discount      = 0;
346
		$cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity'];
347
348
		if ( $this->is_type( array( 'percent_product', 'percent' ) ) ) {
0 ignored issues
show
Documentation introduced by
array('percent_product', 'percent') is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
349
			$discount = $this->get_amount() * ( $discounting_amount / 100 );
350
		} elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) {
351
			/**
352
			 * This is the most complex discount - we need to divide the discount between rows based on their price in.
353
			 * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows.
354
			 * with no price (free) don't get discounted.
355
			 *
356
			 * Get item discount by dividing item cost by subtotal to get a %.
357
			 *
358
			 * Uses price inc tax if prices include tax to work around https://github.com/woothemes/woocommerce/issues/7669 and https://github.com/woothemes/woocommerce/issues/8074.
359
			 */
360
			if ( wc_prices_include_tax() ) {
361
				$discount_percent = ( $cart_item['data']->get_price_including_tax() * $cart_item_qty ) / WC()->cart->subtotal;
362
			} else {
363
				$discount_percent = ( $cart_item['data']->get_price_excluding_tax() * $cart_item_qty ) / WC()->cart->subtotal_ex_tax;
364
			}
365
			$discount = ( $this->get_amount() * $discount_percent ) / $cart_item_qty;
366
367
		} elseif ( $this->is_type( 'fixed_product' ) ) {
368
			$discount = min( $this->get_amount(), $discounting_amount );
369
			$discount = $single ? $discount : $discount * $cart_item_qty;
370
		}
371
372
		$discount = min( $discount, $discounting_amount );
373
374
		// Handle the limit_usage_to_x_items option
375
		if ( $this->is_type( array( 'percent_product', 'fixed_product' ) ) ) {
0 ignored issues
show
Documentation introduced by
array('percent_product', 'fixed_product') is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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