Completed
Pull Request — master (#11372)
by Matty
08:38
created

WC_Cart::check_cart_items()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 4
nop 0
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
if ( ! defined( 'ABSPATH' ) ) {
4
	exit; // Exit if accessed directly
5
}
6
7
/**
8
 * WooCommerce cart
9
 *
10
 * The WooCommerce cart class stores cart data and active coupons as well as handling customer sessions and some cart related urls.
11
 * The cart class also has a price calculation function which calls upon other classes to calculate totals.
12
 *
13
 * @class 		WC_Cart
14
 * @version		2.1.0
15
 * @package		WooCommerce/Classes
16
 * @category	Class
17
 * @author 		WooThemes
18
 */
19
class WC_Cart {
20
21
	/** @var array Contains an array of cart items. */
22
	public $cart_contents = array();
23
24
	/** @var array Contains an array of removed cart items. */
25
	public $removed_cart_contents = array();
26
27
	/** @var array Contains an array of coupon codes applied to the cart. */
28
	public $applied_coupons = array();
29
30
	/** @var array Contains an array of coupon code discounts after they have been applied. */
31
	public $coupon_discount_amounts = array();
32
33
	/** @var array Contains an array of coupon code discount taxes. Used for tax incl pricing. */
34
	public $coupon_discount_tax_amounts = array();
35
36
	/** @var array Contains an array of coupon usage counts after they have been applied. */
37
	public $coupon_applied_count = array();
38
39
	/** @var array Array of coupons */
40
	public $coupons = array();
41
42
	/** @var float The total cost of the cart items. */
43
	public $cart_contents_total;
44
45
	/** @var float Cart grand total. */
46
	public $total;
47
48
	/** @var float Cart subtotal. */
49
	public $subtotal;
50
51
	/** @var float Cart subtotal without tax. */
52
	public $subtotal_ex_tax;
53
54
	/** @var float Total cart tax. */
55
	public $tax_total;
56
57
	/** @var array An array of taxes/tax rates for the cart. */
58
	public $taxes;
59
60
	/** @var array An array of taxes/tax rates for the shipping. */
61
	public $shipping_taxes;
62
63
	/** @var float Discount amount before tax */
64
	public $discount_cart;
65
66
	/** @var float Discounted tax amount. Used predominantly for displaying tax inclusive prices correctly */
67
	public $discount_cart_tax;
68
69
	/** @var float Total for additional fees. */
70
	public $fee_total;
71
72
	/** @var float Shipping cost. */
73
	public $shipping_total;
74
75
	/** @var float Shipping tax. */
76
	public $shipping_tax_total;
77
78
	/** @var array cart_session_data. Array of data the cart calculates and stores in the session with defaults */
79
	public $cart_session_data = array(
80
		'cart_contents_total'         => 0,
81
		'total'                       => 0,
82
		'subtotal'                    => 0,
83
		'subtotal_ex_tax'             => 0,
84
		'tax_total'                   => 0,
85
		'taxes'                       => array(),
86
		'shipping_taxes'              => array(),
87
		'discount_cart'               => 0,
88
		'discount_cart_tax'           => 0,
89
		'shipping_total'              => 0,
90
		'shipping_tax_total'          => 0,
91
		'coupon_discount_amounts'     => array(),
92
		'coupon_discount_tax_amounts' => array(),
93
		'fee_total'                   => 0,
94
		'fees'                        => array()
95
	);
96
97
	/**
98
	 * An array of fees.
99
	 *
100
	 * @var array
101
	 */
102
	public $fees = array();
103
104
	/**
105
	 * Constructor for the cart class. Loads options and hooks in the init method.
106
	 */
107
	public function __construct() {
108
		add_action( 'wp_loaded', array( $this, 'init' ) ); // Get cart after WP and plugins are loaded.
109
		add_action( 'wp', array( $this, 'maybe_set_cart_cookies' ), 99 ); // Set cookies
110
		add_action( 'shutdown', array( $this, 'maybe_set_cart_cookies' ), 0 ); // Set cookies before shutdown and ob flushing
111
		add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 );
112
		add_action( 'woocommerce_applied_coupon', array( $this, 'calculate_totals' ), 20, 0 );
113
	}
114
115
	/**
116
	 * Auto-load in-accessible properties on demand.
117
	 *
118
	 * @param  mixed $key
119
	 * @return mixed
120
	 */
121
	public function __get( $key ) {
122
		switch ( $key ) {
123
			case 'prices_include_tax' :
124
				return wc_prices_include_tax();
125
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
126
			case 'round_at_subtotal' :
127
				return 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
128
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
129
			case 'tax_display_cart' :
130
				return get_option( 'woocommerce_tax_display_cart' );
131
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
132
			case 'dp' :
133
				return wc_get_price_decimals();
134
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
135
			case 'display_totals_ex_tax' :
136
			case 'display_cart_ex_tax' :
137
				return $this->tax_display_cart === 'excl';
138
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
139
			case 'cart_contents_weight' :
140
				return $this->get_cart_contents_weight();
141
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
142
			case 'cart_contents_count' :
143
				return $this->get_cart_contents_count();
144
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
145
			case 'tax' :
146
				_deprecated_argument( 'WC_Cart->tax', '2.3', 'Use WC_Tax:: directly' );
147
				$this->tax = new WC_Tax();
148
			return $this->tax;
149
			case 'discount_total':
150
				_deprecated_argument( 'WC_Cart->discount_total', '2.3', 'After tax coupons are no longer supported. For more information see: https://woocommerce.wordpress.com/2014/12/upcoming-coupon-changes-in-woocommerce-2-3/' );
151
			return 0;
152
		}
153
	}
154
155
	/**
156
	 * Loads the cart data from the PHP session during WordPress init and hooks in other methods.
157
	 */
158
	public function init() {
159
		$this->get_cart_from_session();
160
161
		add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ), 1 );
162
		add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_coupons' ), 1 );
163
		add_action( 'woocommerce_after_checkout_validation', array( $this, 'check_customer_coupons' ), 1 );
164
	}
165
166
	/**
167
	 * Will set cart cookies if needed, once, during WP hook.
168
	 */
169
	public function maybe_set_cart_cookies() {
170
		if ( ! headers_sent() && did_action( 'wp_loaded' ) ) {
171
			if ( ! $this->is_empty() ) {
172
				$this->set_cart_cookies( true );
173
			} elseif ( isset( $_COOKIE['woocommerce_items_in_cart'] ) ) {
174
				$this->set_cart_cookies( false );
175
			}
176
		}
177
	}
178
179
	/**
180
	 * Set cart hash cookie and items in cart.
181
	 *
182
	 * @access private
183
	 * @param bool $set (default: true)
184
	 */
185
	private function set_cart_cookies( $set = true ) {
186
		if ( $set ) {
187
			wc_setcookie( 'woocommerce_items_in_cart', 1 );
188
			wc_setcookie( 'woocommerce_cart_hash', md5( json_encode( $this->get_cart_for_session() ) ) );
189
		} elseif ( isset( $_COOKIE['woocommerce_items_in_cart'] ) ) {
190
			wc_setcookie( 'woocommerce_items_in_cart', 0, time() - HOUR_IN_SECONDS );
191
			wc_setcookie( 'woocommerce_cart_hash', '', time() - HOUR_IN_SECONDS );
192
		}
193
		do_action( 'woocommerce_set_cart_cookies', $set );
194
	}
195
196
	/*-----------------------------------------------------------------------------------*/
197
	/* Cart Session Handling */
198
	/*-----------------------------------------------------------------------------------*/
199
200
		/**
201
		 * Get the cart data from the PHP session and store it in class variables.
202
		 */
203
		public function get_cart_from_session() {
204
			// Load cart session data from session
205
			foreach ( $this->cart_session_data as $key => $default ) {
206
				$this->$key = WC()->session->get( $key, $default );
207
			}
208
209
			$update_cart_session         = false;
210
			$this->removed_cart_contents = array_filter( WC()->session->get( 'removed_cart_contents', array() ) );
211
			$this->applied_coupons       = array_filter( WC()->session->get( 'applied_coupons', array() ) );
212
213
			/**
214
			 * Load the cart object. This defaults to the persistent cart if null.
215
			 */
216
			$cart = WC()->session->get( 'cart', null );
217
218
			if ( is_null( $cart ) && ( $saved_cart = get_user_meta( get_current_user_id(), '_woocommerce_persistent_cart', true ) ) ) {
219
				$cart                = $saved_cart['cart'];
220
				$update_cart_session = true;
221
			} elseif ( is_null( $cart ) ) {
222
				$cart = array();
223
			}
224
225
			if ( is_array( $cart ) ) {
226
				// Prime meta cache to reduce future queries
227
				update_meta_cache( 'post', wp_list_pluck( $cart, 'product_id' ) );
228
229
				foreach ( $cart as $key => $values ) {
230
					$_product = wc_get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] );
231
232
					if ( ! empty( $_product ) && $_product->exists() && $values['quantity'] > 0 ) {
233
234
						if ( ! $_product->is_purchasable() ) {
235
236
							// Flag to indicate the stored cart should be update
237
							$update_cart_session = true;
238
							wc_add_notice( sprintf( __( '%s has been removed from your cart because it can no longer be purchased. Please contact us if you need assistance.', 'woocommerce' ), $_product->get_title() ), 'error' );
239
							do_action( 'woocommerce_remove_cart_item_from_session', $key, $values );
240
241
						} else {
242
243
							// Put session data into array. Run through filter so other plugins can load their own session data
244
							$session_data                = array_merge( $values, array( 'data' => $_product ) );
245
							$this->cart_contents[ $key ] = apply_filters( 'woocommerce_get_cart_item_from_session', $session_data, $values, $key );
246
247
						}
248
					}
249
				}
250
			}
251
252
			// Trigger action
253
			do_action( 'woocommerce_cart_loaded_from_session', $this );
254
255
			if ( $update_cart_session ) {
256
				WC()->session->cart = $this->get_cart_for_session();
257
			}
258
259
			// Queue re-calc if subtotal is not set
260
			if ( ( ! $this->subtotal && ! $this->is_empty() ) || $update_cart_session ) {
261
				$this->calculate_totals();
262
			}
263
		}
264
265
		/**
266
		 * Sets the php session data for the cart and coupons.
267
		 */
268
		public function set_session() {
269
			// Set cart and coupon session data
270
			$cart_session = $this->get_cart_for_session();
271
272
			WC()->session->set( 'cart', $cart_session );
273
			WC()->session->set( 'applied_coupons', $this->applied_coupons );
274
			WC()->session->set( 'coupon_discount_amounts', $this->coupon_discount_amounts );
275
			WC()->session->set( 'coupon_discount_tax_amounts', $this->coupon_discount_tax_amounts );
276
			WC()->session->set( 'removed_cart_contents', $this->removed_cart_contents );
277
278
			foreach ( $this->cart_session_data as $key => $default ) {
279
				WC()->session->set( $key, $this->$key );
280
			}
281
282
			if ( get_current_user_id() ) {
283
				$this->persistent_cart_update();
284
			}
285
286
			do_action( 'woocommerce_cart_updated' );
287
		}
288
289
		/**
290
		 * Empties the cart and optionally the persistent cart too.
291
		 *
292
		 * @param bool $clear_persistent_cart (default: true)
293
		 */
294
		public function empty_cart( $clear_persistent_cart = true ) {
295
			$this->cart_contents = array();
296
			$this->reset( true );
297
298
			unset( WC()->session->order_awaiting_payment, WC()->session->applied_coupons, WC()->session->coupon_discount_amounts, WC()->session->coupon_discount_tax_amounts, WC()->session->cart );
299
300
			if ( $clear_persistent_cart && get_current_user_id() ) {
301
				$this->persistent_cart_destroy();
302
			}
303
304
			do_action( 'woocommerce_cart_emptied' );
305
		}
306
307
	/*-----------------------------------------------------------------------------------*/
308
	/* Persistent cart handling */
309
	/*-----------------------------------------------------------------------------------*/
310
311
		/**
312
		 * Save the persistent cart when the cart is updated.
313
		 */
314
		public function persistent_cart_update() {
315
			update_user_meta( get_current_user_id(), '_woocommerce_persistent_cart', array(
316
				'cart' => WC()->session->get( 'cart' )
317
			) );
318
		}
319
320
		/**
321
		 * Delete the persistent cart permanently.
322
		 */
323
		public function persistent_cart_destroy() {
324
			delete_user_meta( get_current_user_id(), '_woocommerce_persistent_cart' );
325
		}
326
327
	/*-----------------------------------------------------------------------------------*/
328
	/* Cart Data Functions */
329
	/*-----------------------------------------------------------------------------------*/
330
331
		/**
332
		 * Coupons enabled function. Filterable.
333
		 *
334
		 * @deprecated 2.5.0 in favor to wc_coupons_enabled()
335
		 *
336
		 * @return bool
337
		 */
338
		public function coupons_enabled() {
339
			return wc_coupons_enabled();
340
		}
341
342
		/**
343
		 * Get number of items in the cart.
344
		 * @return int
345
		 */
346
		public function get_cart_contents_count() {
347
			return apply_filters( 'woocommerce_cart_contents_count', array_sum( wp_list_pluck( $this->get_cart(), 'quantity' ) ) );
348
		}
349
350
		/**
351
		 * Get weight of items in the cart.
352
		 * @since 2.5.0
353
		 * @return int
354
		 */
355
		public function get_cart_contents_weight() {
356
			$weight = 0;
357
358
			foreach ( $this->get_cart() as $cart_item_key => $values ) {
359
				$weight += $values['data']->get_weight() * $values['quantity'];
360
			}
361
362
			return apply_filters( 'woocommerce_cart_contents_weight', $weight );
363
		}
364
365
		/**
366
		 * Checks if the cart is empty.
367
		 *
368
		 * @return bool
369
		 */
370
		public function is_empty() {
371
			return 0 === sizeof( $this->get_cart() );
372
		}
373
374
		/**
375
		 * Check all cart items for errors.
376
		 */
377
		public function check_cart_items() {
378
			// Result
379
			$return = true;
380
381
			// Check cart item validity
382
			$result = $this->check_cart_item_validity();
383
384
			if ( is_wp_error( $result ) ) {
385
				wc_add_notice( $result->get_error_message(), 'error' );
386
				$return = false;
387
			}
388
389
			// Check item stock
390
			$result = $this->check_cart_item_stock();
391
392
			if ( is_wp_error( $result ) ) {
393
				wc_add_notice( $result->get_error_message(), 'error' );
394
				$return = false;
395
			}
396
397
			return $return;
398
		}
399
400
		/**
401
		 * Check cart coupons for errors.
402
		 */
403
		public function check_cart_coupons() {
404
			foreach ( $this->applied_coupons as $code ) {
405
				$coupon = new WC_Coupon( $code );
406
407 View Code Duplication
				if ( ! $coupon->is_valid() ) {
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...
408
					// Error message
409
					$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_INVALID_REMOVED );
410
411
					// Remove the coupon
412
					$this->remove_coupon( $code );
413
414
					// Flag totals for refresh
415
					WC()->session->set( 'refresh_totals', true );
416
				}
417
			}
418
		}
419
420
		/**
421
		 * Get cart items quantities - merged so we can do accurate stock checks on items across multiple lines.
422
		 *
423
		 * @return array
424
		 */
425
		public function get_cart_item_quantities() {
426
			$quantities = array();
427
428
			foreach ( $this->get_cart() as $cart_item_key => $values ) {
429
				$_product = $values['data'];
430
431
				if ( $_product->is_type( 'variation' ) && true === $_product->managing_stock() ) {
432
					// Variation has stock levels defined so its handled individually
433
					$quantities[ $values['variation_id'] ] = isset( $quantities[ $values['variation_id'] ] ) ? $quantities[ $values['variation_id'] ] + $values['quantity'] : $values['quantity'];
434
				} else {
435
					$quantities[ $values['product_id'] ] = isset( $quantities[ $values['product_id'] ] ) ? $quantities[ $values['product_id'] ] + $values['quantity'] : $values['quantity'];
436
				}
437
			}
438
439
			return $quantities;
440
		}
441
442
		/**
443
		 * Looks through cart items and checks the posts are not trashed or deleted.
444
		 *
445
		 * @return bool|WP_Error
446
		 */
447
		public function check_cart_item_validity() {
448
			$return = true;
449
450
			foreach ( $this->get_cart() as $cart_item_key => $values ) {
451
				$_product = $values['data'];
452
453
				if ( ! $_product || ! $_product->exists() || 'trash' === $_product->post->post_status ) {
454
					$this->set_quantity( $cart_item_key, 0 );
455
					$return = new WP_Error( 'invalid', __( 'An item which is no longer available was removed from your cart.', 'woocommerce' ) );
456
				}
457
			}
458
459
			return $return;
460
		}
461
462
		/**
463
		 * Looks through the cart to check each item is in stock. If not, add an error.
464
		 *
465
		 * @return bool|WP_Error
466
		 */
467
		public function check_cart_item_stock() {
468
			global $wpdb;
469
470
			$error               = new WP_Error();
471
			$product_qty_in_cart = $this->get_cart_item_quantities();
472
473
			// First stock check loop
474
			foreach ( $this->get_cart() as $cart_item_key => $values ) {
475
				$_product = $values['data'];
476
477
				/**
478
				 * Check stock based on stock-status.
479
				 */
480
				if ( ! $_product->is_in_stock() ) {
481
					$error->add( 'out-of-stock', sprintf( __( 'Sorry, "%s" is not in stock. Please edit your cart and try again. We apologise for any inconvenience caused.', 'woocommerce' ), $_product->get_title() ) );
482
					return $error;
483
				}
484
485
				if ( ! $_product->managing_stock() ) {
486
					continue;
487
				}
488
489
				$check_qty = $_product->is_type( 'variation' ) && true === $_product->managing_stock() ? $product_qty_in_cart[ $values['variation_id'] ] : $product_qty_in_cart[ $values['product_id'] ];
490
491
				/**
492
				 * Check stock based on all items in the cart.
493
				 */
494
				if ( ! $_product->has_enough_stock( $check_qty ) ) {
495
					$error->add( 'out-of-stock', sprintf( __( 'Sorry, we do not have enough "%s" in stock to fulfill your order (%s in stock). Please edit your cart and try again. We apologise for any inconvenience caused.', 'woocommerce' ), $_product->get_title(), $_product->get_stock_quantity() ) );
496
					return $error;
497
				}
498
499
				/**
500
				 * Finally consider any held stock, from pending orders.
501
				 */
502
				if ( get_option( 'woocommerce_hold_stock_minutes' ) > 0 && ! $_product->backorders_allowed() ) {
503
					$order_id   = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : 0;
504
					$held_stock = $wpdb->get_var(
505
						$wpdb->prepare( "
506
							SELECT SUM( order_item_meta.meta_value ) AS held_qty
507
							FROM {$wpdb->posts} AS posts
508
							LEFT JOIN {$wpdb->prefix}woocommerce_order_items as order_items ON posts.ID = order_items.order_id
509
							LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id
510
							LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta2 ON order_items.order_item_id = order_item_meta2.order_item_id
511
							WHERE 	order_item_meta.meta_key   = '_qty'
512
							AND 	order_item_meta2.meta_key  = %s AND order_item_meta2.meta_value  = %d
513
							AND 	posts.post_type            IN ( '" . implode( "','", wc_get_order_types() ) . "' )
514
							AND 	posts.post_status          = 'wc-pending'
515
							AND		posts.ID                   != %d;",
516
							$_product->is_type( 'variation' ) && true === $_product->managing_stock() ? '_variation_id' : '_product_id',
517
							$_product->is_type( 'variation' ) && true === $_product->managing_stock() ? $values['variation_id'] : $values['product_id'],
518
							$order_id
519
						)
520
					);
521
522
					$not_enough_stock = false;
523
524
					if ( $_product->is_type( 'variation' ) && 'parent' === $_product->managing_stock() && $_product->parent->get_stock_quantity() < ( $held_stock + $check_qty ) ) {
525
						$not_enough_stock = true;
526
					} elseif ( $_product->get_stock_quantity() < ( $held_stock + $check_qty ) ) {
527
						$not_enough_stock = true;
528
					}
529
					if ( $not_enough_stock ) {
530
						$error->add( 'out-of-stock', sprintf( __( 'Sorry, we do not have enough "%s" in stock to fulfill your order right now. Please try again in %d minutes or edit your cart and try again. We apologise for any inconvenience caused.', 'woocommerce' ), $_product->get_title(), get_option( 'woocommerce_hold_stock_minutes' ) ) );
531
						return $error;
532
					}
533
				}
534
			}
535
536
			return true;
537
		}
538
539
		/**
540
		 * Gets and formats a list of cart item data + variations for display on the frontend.
541
		 *
542
		 * @param  array  $cart_item
543
		 * @param  bool   $flat      (default: false)
544
		 * @return string
545
		 */
546
		public function get_item_data( $cart_item, $flat = false ) {
547
			$item_data = array();
548
549
			// Variation data
550
			if ( ! empty( $cart_item['data']->variation_id ) && is_array( $cart_item['variation'] ) ) {
551
552
				foreach ( $cart_item['variation'] as $name => $value ) {
553
554
					if ( '' === $value ) {
555
						continue;
556
					}
557
558
					$taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) );
559
560
					// If this is a term slug, get the term's nice name
561
					if ( taxonomy_exists( $taxonomy ) ) {
562
						$term = get_term_by( 'slug', $value, $taxonomy );
563
						if ( ! is_wp_error( $term ) && $term && $term->name ) {
564
							$value = $term->name;
565
						}
566
						$label = wc_attribute_label( $taxonomy );
567
568
					// If this is a custom option slug, get the options name
569
					} else {
570
						$value              = apply_filters( 'woocommerce_variation_option_name', $value );
571
						$product_attributes = $cart_item['data']->get_attributes();
572
						if ( isset( $product_attributes[ str_replace( 'attribute_', '', $name ) ] ) ) {
573
							$label = wc_attribute_label( $product_attributes[ str_replace( 'attribute_', '', $name ) ]['name'] );
574
						} else {
575
							$label = $name;
576
						}
577
					}
578
579
					$item_data[] = array(
580
						'key'   => $label,
581
						'value' => $value
582
					);
583
				}
584
			}
585
586
			// Filter item data to allow 3rd parties to add more to the array
587
			$item_data = apply_filters( 'woocommerce_get_item_data', $item_data, $cart_item );
588
589
			// Format item data ready to display
590
			foreach ( $item_data as $key => $data ) {
591
				// Set hidden to true to not display meta on cart.
592
				if ( ! empty( $data['hidden'] ) ) {
593
					unset( $item_data[ $key ] );
594
					continue;
595
				}
596
				$item_data[ $key ]['key']     = ! empty( $data['key'] ) ? $data['key'] : $data['name'];
597
				$item_data[ $key ]['display'] = ! empty( $data['display'] ) ? $data['display'] : $data['value'];
598
			}
599
600
			// Output flat or in list format
601
			if ( sizeof( $item_data ) > 0 ) {
602
				ob_start();
603
604
				if ( $flat ) {
605
					foreach ( $item_data as $data ) {
606
						echo esc_html( $data['key'] ) . ': ' . wp_kses_post( $data['display'] ) . "\n";
607
					}
608
				} else {
609
					wc_get_template( 'cart/cart-item-data.php', array( 'item_data' => $item_data ) );
610
				}
611
612
				return ob_get_clean();
613
			}
614
615
			return '';
616
		}
617
618
		/**
619
		 * Gets cross sells based on the items in the cart.
620
		 *
621
		 * @return array cross_sells (item ids)
622
		 */
623
		public function get_cross_sells() {
624
			$cross_sells = array();
625
			$in_cart     = array();
626
			if ( ! $this->is_empty() ) {
627
				foreach ( $this->get_cart() as $cart_item_key => $values ) {
628
					if ( $values['quantity'] > 0 ) {
629
						$cross_sells = array_merge( $values['data']->get_cross_sells(), $cross_sells );
630
						$in_cart[]   = $values['product_id'];
631
					}
632
				}
633
			}
634
			$cross_sells = array_diff( $cross_sells, $in_cart );
635
			return $cross_sells;
636
		}
637
638
		/**
639
		 * Gets the url to the cart page.
640
		 *
641
		 * @deprecated 2.5.0 in favor to wc_get_cart_url()
642
		 *
643
		 * @return string url to page
644
		 */
645
		public function get_cart_url() {
646
			return wc_get_cart_url();
647
		}
648
649
		/**
650
		 * Gets the url to the checkout page.
651
		 *
652
		 * @deprecated 2.5.0 in favor to wc_get_checkout_url()
653
		 *
654
		 * @return string url to page
655
		 */
656
		public function get_checkout_url() {
657
			return wc_get_checkout_url();
658
		}
659
660
		/**
661
		 * Gets the url to remove an item from the cart.
662
		 *
663
		 * @param  string $cart_item_key contains the id of the cart item
664
		 * @return string                url to page
665
		 */
666
		public function get_remove_url( $cart_item_key ) {
667
			$cart_page_url = wc_get_page_permalink( 'cart' );
668
			return apply_filters( 'woocommerce_get_remove_url', $cart_page_url ? wp_nonce_url( add_query_arg( 'remove_item', $cart_item_key, $cart_page_url ), 'woocommerce-cart' ) : '' );
669
		}
670
671
		/**
672
		 * Gets the url to re-add an item into the cart.
673
		 *
674
		 * @param  string $cart_item_key
675
		 * @return string                url to page
676
		 */
677
		public function get_undo_url( $cart_item_key ) {
678
			$cart_page_url = wc_get_page_permalink( 'cart' );
679
680
			$query_args = array(
681
				'undo_item' => $cart_item_key,
682
			);
683
684
			return apply_filters( 'woocommerce_get_undo_url', $cart_page_url ? wp_nonce_url( add_query_arg( $query_args, $cart_page_url ), 'woocommerce-cart' ) : '', $cart_item_key );
685
		}
686
687
		/**
688
		 * Returns the contents of the cart in an array.
689
		 *
690
		 * @return array contents of the cart
691
		 */
692
		public function get_cart() {
693
			if ( ! did_action( 'wp_loaded' ) ) {
694
				_doing_it_wrong( __FUNCTION__, __( 'Get cart should not be called before the wp_loaded action.', 'woocommerce' ), '2.3' );
695
			}
696
			if ( ! did_action( 'woocommerce_cart_loaded_from_session' ) ) {
697
				$this->get_cart_from_session();
698
			}
699
			return array_filter( (array) $this->cart_contents );
700
		}
701
702
		/**
703
		 * Returns the contents of the cart in an array without the 'data' element.
704
		 *
705
		 * @return array contents of the cart
706
		 */
707
		public function get_cart_for_session() {
708
			$cart_session = array();
709
710
			if ( $this->get_cart() ) {
711
				foreach ( $this->get_cart() as $key => $values ) {
712
					$cart_session[ $key ] = $values;
713
					unset( $cart_session[ $key ]['data'] ); // Unset product object
714
				}
715
			}
716
717
			return $cart_session;
718
		}
719
720
		/**
721
		 * Returns a specific item in the cart.
722
		 *
723
		 * @param  string $item_key Cart item key.
724
		 * @return array            Item data
725
		 */
726
		public function get_cart_item( $item_key ) {
727
			if ( isset( $this->cart_contents[ $item_key ] ) ) {
728
				return $this->cart_contents[ $item_key ];
729
			}
730
731
			return array();
732
		}
733
734
		/**
735
		 * Returns the cart and shipping taxes, merged.
736
		 *
737
		 * @return array merged taxes
738
		 */
739
		public function get_taxes() {
740
			$taxes = array();
741
742
			// Merge
743
			foreach ( array_keys( $this->taxes + $this->shipping_taxes ) as $key ) {
744
				$taxes[ $key ] = ( isset( $this->shipping_taxes[ $key ] ) ? $this->shipping_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 );
745
			}
746
747
			return apply_filters( 'woocommerce_cart_get_taxes', $taxes, $this );
748
		}
749
750
		/**
751
		 * Get taxes, merged by code, formatted ready for output.
752
		 *
753
		 * @return array
754
		 */
755
		public function get_tax_totals() {
756
			$taxes      = $this->get_taxes();
757
			$tax_totals = array();
758
759
			foreach ( $taxes as $key => $tax ) {
760
				$code = WC_Tax::get_rate_code( $key );
761
762
				if ( $code || $key === apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) ) {
763 View Code Duplication
					if ( ! isset( $tax_totals[ $code ] ) ) {
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...
764
						$tax_totals[ $code ]         = new stdClass();
765
						$tax_totals[ $code ]->amount = 0;
766
					}
767
					$tax_totals[ $code ]->tax_rate_id       = $key;
768
					$tax_totals[ $code ]->is_compound       = WC_Tax::is_compound( $key );
769
					$tax_totals[ $code ]->label             = WC_Tax::get_rate_label( $key );
770
					$tax_totals[ $code ]->amount           += wc_round_tax_total( $tax );
771
					$tax_totals[ $code ]->formatted_amount  = wc_price( wc_round_tax_total( $tax_totals[ $code ]->amount ) );
772
				}
773
			}
774
775 View Code Duplication
			if ( apply_filters( 'woocommerce_cart_hide_zero_taxes', true ) ) {
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...
776
				$amounts    = array_filter( wp_list_pluck( $tax_totals, 'amount' ) );
777
				$tax_totals = array_intersect_key( $tax_totals, $amounts );
778
			}
779
780
			return apply_filters( 'woocommerce_cart_tax_totals', $tax_totals, $this );
781
		}
782
783
		/**
784
		 * Get all tax classes for items in the cart.
785
		 * @return array
786
		 */
787
		public function get_cart_item_tax_classes() {
788
			$found_tax_classes = array();
789
790
			foreach ( WC()->cart->get_cart() as $item ) {
791
				$found_tax_classes[] = $item['data']->get_tax_class();
792
			}
793
794
			return array_unique( $found_tax_classes );
795
		}
796
797
		/**
798
		 * Determines the value that the customer spent and the subtotal
799
		 * displayed, used for things like coupon validation.
800
		 *
801
		 * Since the coupon lines are displayed based on the TAX DISPLAY value
802
		 * of cart, this is used to determine the spend.
803
		 *
804
		 * If cart totals are shown including tax, use the subtotal.
805
		 * If cart totals are shown excluding tax, use the subtotal ex tax
806
		 * (tax is shown after coupons).
807
		 *
808
		 * @since 2.6.0
809
		 * @return string
810
		 */
811
		public function get_displayed_subtotal() {
812
			if ( 'incl' === $this->tax_display_cart ) {
813
				return wc_format_decimal( $this->subtotal );
814
			} elseif ( 'excl' === $this->tax_display_cart ) {
815
				return wc_format_decimal( $this->subtotal_ex_tax );
816
			}
817
		}
818
819
	/*-----------------------------------------------------------------------------------*/
820
	/* Add to cart handling */
821
	/*-----------------------------------------------------------------------------------*/
822
823
		/**
824
		 * Check if product is in the cart and return cart item key.
825
		 *
826
		 * Cart item key will be unique based on the item and its properties, such as variations.
827
		 *
828
		 * @param  mixed  id of product to find in the cart
829
		 * @return string cart item key
830
		 */
831
		public function find_product_in_cart( $cart_id = false ) {
832
			if ( $cart_id !== false ) {
833
				if ( is_array( $this->cart_contents ) ) {
834
					foreach ( $this->cart_contents as $cart_item_key => $cart_item ) {
835
						if ( $cart_item_key == $cart_id ) {
836
							return $cart_item_key;
837
						}
838
					}
839
				}
840
			}
841
			return '';
842
		}
843
844
		/**
845
		 * Generate a unique ID for the cart item being added.
846
		 *
847
		 * @param  int    $product_id     - id of the product the key is being generated for
848
		 * @param  int    $variation_id   of the product the key is being generated for
849
		 * @param  array  $variation      data for the cart item
850
		 * @param  array  $cart_item_data other cart item data passed which affects this items uniqueness in the cart
851
		 * @return string                 cart item key
852
		 */
853
		public function generate_cart_id( $product_id, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
854
			$id_parts = array( $product_id );
855
856
			if ( $variation_id && 0 != $variation_id ) {
857
				$id_parts[] = $variation_id;
858
			}
859
860
			if ( is_array( $variation ) && ! empty( $variation ) ) {
861
				$variation_key = '';
862
				foreach ( $variation as $key => $value ) {
863
					$variation_key .= trim( $key ) . trim( $value );
864
				}
865
				$id_parts[] = $variation_key;
866
			}
867
868
			if ( is_array( $cart_item_data ) && ! empty( $cart_item_data ) ) {
869
				$cart_item_data_key = '';
870
				foreach ( $cart_item_data as $key => $value ) {
871
872
					if ( is_array( $value ) ) {
873
						$value = http_build_query( $value );
874
					}
875
					$cart_item_data_key .= trim( $key ) . trim( $value );
876
877
				}
878
				$id_parts[] = $cart_item_data_key;
879
			}
880
881
			return md5( implode( '_', $id_parts ) );
882
		}
883
884
		/**
885
		 * Add a product to the cart.
886
		 *
887
		 * @param  int         $product_id     contains the id of the product to add to the cart
888
		 * @param  int         $quantity       contains the quantity of the item to add
889
		 * @param  int         $variation_id
890
		 * @param  array       $variation      attribute values
891
		 * @param  array       $cart_item_data extra cart item data we want to pass into the item
892
		 * @return string|bool $cart_item_key
893
		 */
894
		public function add_to_cart( $product_id = 0, $quantity = 1, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
895
			// Wrap in try catch so plugins can throw an exception to prevent adding to cart
896
			try {
897
				$product_id   = absint( $product_id );
898
				$variation_id = absint( $variation_id );
899
900
				// Ensure we don't add a variation to the cart directly by variation ID
901
				if ( 'product_variation' == get_post_type( $product_id ) ) {
902
					$variation_id = $product_id;
903
					$product_id   = wp_get_post_parent_id( $variation_id );
904
				}
905
906
				// Get the product
907
				$product_data = wc_get_product( $variation_id ? $variation_id : $product_id );
908
909
				// Sanity check
910
				if ( $quantity <= 0 || ! $product_data || 'trash' === $product_data->post->post_status ) {
911
					throw new Exception();
912
				}
913
914
				// Load cart item data - may be added by other plugins
915
				$cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', $cart_item_data, $product_id, $variation_id );
916
917
				// Generate a ID based on product ID, variation ID, variation data, and other cart item data
918
				$cart_id = $this->generate_cart_id( $product_id, $variation_id, $variation, $cart_item_data );
919
920
				// Find the cart item key in the existing cart
921
				$cart_item_key = $this->find_product_in_cart( $cart_id );
0 ignored issues
show
Documentation introduced by
$cart_id is of type string, but the function expects a boolean.

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...
922
923
				// Force quantity to 1 if sold individually and check for existing item in cart
924
				if ( $product_data->is_sold_individually() ) {
925
					$quantity         = apply_filters( 'woocommerce_add_to_cart_sold_individually_quantity', 1, $quantity, $product_id, $variation_id, $cart_item_data );
926
					$in_cart_quantity = $cart_item_key ? $this->cart_contents[ $cart_item_key ]['quantity'] : 0;
927
928
					if ( $in_cart_quantity > 0 ) {
929
						throw new Exception( sprintf( '<a href="%s" class="button wc-forward">%s</a> %s', wc_get_cart_url(), __( 'View Cart', 'woocommerce' ), sprintf( __( 'You cannot add another &quot;%s&quot; to your cart.', 'woocommerce' ), $product_data->get_title() ) ) );
930
					}
931
				}
932
933
				// Check product is_purchasable
934
				if ( ! $product_data->is_purchasable() ) {
935
					throw new Exception( __( 'Sorry, this product cannot be purchased.', 'woocommerce' ) );
936
				}
937
938
				// Stock check - only check if we're managing stock and backorders are not allowed
939
				if ( ! $product_data->is_in_stock() ) {
940
					throw new Exception( sprintf( __( 'You cannot add &quot;%s&quot; to the cart because the product is out of stock.', 'woocommerce' ), $product_data->get_title() ) );
941
				}
942
943
				if ( ! $product_data->has_enough_stock( $quantity ) ) {
944
					throw new Exception( sprintf( __( 'You cannot add that amount of &quot;%s&quot; to the cart because there is not enough stock (%s remaining).', 'woocommerce' ), $product_data->get_title(), $product_data->get_stock_quantity() ) );
945
				}
946
947
				// Stock check - this time accounting for whats already in-cart
948
				if ( $managing_stock = $product_data->managing_stock() ) {
949
					$products_qty_in_cart = $this->get_cart_item_quantities();
950
951
					if ( $product_data->is_type( 'variation' ) && true === $managing_stock ) {
952
						$check_qty = isset( $products_qty_in_cart[ $variation_id ] ) ? $products_qty_in_cart[ $variation_id ] : 0;
953
					} else {
954
						$check_qty = isset( $products_qty_in_cart[ $product_id ] ) ? $products_qty_in_cart[ $product_id ] : 0;
955
					}
956
957
					/**
958
					 * Check stock based on all items in the cart.
959
					 */
960
					if ( ! $product_data->has_enough_stock( $check_qty + $quantity ) ) {
961
						throw new Exception( sprintf(
962
							'<a href="%s" class="button wc-forward">%s</a> %s',
963
							wc_get_cart_url(),
964
							__( 'View Cart', 'woocommerce' ),
965
							sprintf( __( 'You cannot add that amount to the cart &mdash; we have %s in stock and you already have %s in your cart.', 'woocommerce' ), $product_data->get_stock_quantity(), $check_qty )
966
						) );
967
					}
968
				}
969
970
				// If cart_item_key is set, the item is already in the cart
971
				if ( $cart_item_key ) {
972
					$new_quantity = $quantity + $this->cart_contents[ $cart_item_key ]['quantity'];
973
					$this->set_quantity( $cart_item_key, $new_quantity, false );
974
				} else {
975
					$cart_item_key = $cart_id;
976
977
					// Add item after merging with $cart_item_data - hook to allow plugins to modify cart item
978
					$this->cart_contents[ $cart_item_key ] = apply_filters( 'woocommerce_add_cart_item', array_merge( $cart_item_data, array(
979
						'product_id'   => $product_id,
980
						'variation_id' => $variation_id,
981
						'variation'    => $variation,
982
						'quantity'     => $quantity,
983
						'data'         => $product_data
984
					) ), $cart_item_key );
985
				}
986
987
				if ( did_action( 'wp' ) ) {
988
					$this->set_cart_cookies( ! $this->is_empty() );
989
				}
990
991
				do_action( 'woocommerce_add_to_cart', $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data );
992
993
				return $cart_item_key;
994
995
			} catch ( Exception $e ) {
996
				if ( $e->getMessage() ) {
997
					wc_add_notice( $e->getMessage(), 'error' );
998
				}
999
				return false;
1000
			}
1001
		}
1002
1003
		/**
1004
		 * Remove a cart item.
1005
		 *
1006
		 * @since  2.3.0
1007
		 * @param  string $cart_item_key
1008
		 * @return bool
1009
		 */
1010
		public function remove_cart_item( $cart_item_key ) {
1011
			if ( isset( $this->cart_contents[ $cart_item_key ] ) ) {
1012
				$this->removed_cart_contents[ $cart_item_key ] = $this->cart_contents[ $cart_item_key ];
1013
				unset( $this->removed_cart_contents[ $cart_item_key ]['data'] );
1014
1015
				do_action( 'woocommerce_remove_cart_item', $cart_item_key, $this );
1016
1017
				unset( $this->cart_contents[ $cart_item_key ] );
1018
1019
				do_action( 'woocommerce_cart_item_removed', $cart_item_key, $this );
1020
1021
				$this->calculate_totals();
1022
1023
				return true;
1024
			}
1025
1026
			return false;
1027
		}
1028
1029
		/**
1030
		 * Restore a cart item.
1031
		 *
1032
		 * @param  string $cart_item_key
1033
		 * @return bool
1034
		 */
1035
		public function restore_cart_item( $cart_item_key ) {
1036
			if ( isset( $this->removed_cart_contents[ $cart_item_key ] ) ) {
1037
				$this->cart_contents[ $cart_item_key ]         = $this->removed_cart_contents[ $cart_item_key ];
1038
				$this->cart_contents[ $cart_item_key ]['data'] = wc_get_product( $this->cart_contents[ $cart_item_key ]['variation_id'] ? $this->cart_contents[ $cart_item_key ]['variation_id'] : $this->cart_contents[ $cart_item_key ]['product_id'] );
1039
1040
				do_action( 'woocommerce_restore_cart_item', $cart_item_key, $this );
1041
1042
				unset( $this->removed_cart_contents[ $cart_item_key ] );
1043
1044
				do_action( 'woocommerce_cart_item_restored', $cart_item_key, $this );
1045
1046
				$this->calculate_totals();
1047
1048
				return true;
1049
			}
1050
1051
			return false;
1052
		}
1053
1054
		/**
1055
		 * Set the quantity for an item in the cart.
1056
		 *
1057
		 * @param  string $cart_item_key  contains the id of the cart item
1058
		 * @param  int    $quantity       contains the quantity of the item
1059
		 * @param  bool   $refresh_totals whether or not to calculate totals after setting the new qty
1060
		 *
1061
		 * @return bool
1062
		 */
1063
		public function set_quantity( $cart_item_key, $quantity = 1, $refresh_totals = true ) {
1064
			if ( $quantity == 0 || $quantity < 0 ) {
1065
				do_action( 'woocommerce_before_cart_item_quantity_zero', $cart_item_key );
1066
				unset( $this->cart_contents[ $cart_item_key ] );
1067
			} else {
1068
				$old_quantity                                      = $this->cart_contents[ $cart_item_key ]['quantity'];
1069
				$this->cart_contents[ $cart_item_key ]['quantity'] = $quantity;
1070
				do_action( 'woocommerce_after_cart_item_quantity_update', $cart_item_key, $quantity, $old_quantity );
1071
			}
1072
1073
			if ( $refresh_totals ) {
1074
				$this->calculate_totals();
1075
			}
1076
1077
			return true;
1078
		}
1079
1080
	/*-----------------------------------------------------------------------------------*/
1081
	/* Cart Calculation Functions */
1082
	/*-----------------------------------------------------------------------------------*/
1083
1084
		/**
1085
		 * Reset cart totals to the defaults. Useful before running calculations.
1086
		 *
1087
		 * @param bool $unset_session If true, the session data will be forced unset.
1088
		 * @access private
1089
		 */
1090
		private function reset( $unset_session = false ) {
1091
			foreach ( $this->cart_session_data as $key => $default ) {
1092
				$this->$key = $default;
1093
				if ( $unset_session ) {
1094
					unset( WC()->session->$key );
1095
				}
1096
			}
1097
			do_action( 'woocommerce_cart_reset', $this, $unset_session );
1098
		}
1099
1100
		/**
1101
		 * Sort by subtotal.
1102
		 * @param  array $a
1103
		 * @param  array $b
1104
		 * @return int
1105
		 */
1106
		private function sort_by_subtotal( $a, $b ) {
1107
			$first_item_subtotal  = isset( $a['line_subtotal'] ) ? $a['line_subtotal'] : 0;
1108
			$second_item_subtotal = isset( $b['line_subtotal'] ) ? $b['line_subtotal'] : 0;
1109
			if ( $first_item_subtotal === $second_item_subtotal ) {
1110
				return 0;
1111
			}
1112
			return ( $first_item_subtotal < $second_item_subtotal ) ? 1 : -1;
1113
		}
1114
1115
		/**
1116
		 * Calculate totals for the items in the cart.
1117
		 */
1118
		public function calculate_totals() {
1119
			$this->reset();
1120
			$this->coupons = $this->get_coupons();
1121
1122
			do_action( 'woocommerce_before_calculate_totals', $this );
1123
1124
			if ( $this->is_empty() ) {
1125
				$this->set_session();
1126
				return;
1127
			}
1128
1129
			$tax_rates      = array();
1130
			$shop_tax_rates = array();
1131
			$cart           = $this->get_cart();
1132
1133
			/**
1134
			 * Calculate subtotals for items. This is done first so that discount logic can use the values.
1135
			 */
1136
			foreach ( $cart as $cart_item_key => $values ) {
1137
				$_product          = $values['data'];
1138
				$line_price        = $_product->get_price() * $values['quantity'];
1139
				$line_subtotal     = 0;
1140
				$line_subtotal_tax = 0;
1141
1142
				/**
1143
				 * No tax to calculate.
1144
				 */
1145
				if ( ! $_product->is_taxable() ) {
1146
1147
					// Subtotal is the undiscounted price
1148
					$this->subtotal        += $line_price;
1149
					$this->subtotal_ex_tax += $line_price;
1150
1151
				/**
1152
				 * Prices include tax.
1153
				 *
1154
				 * To prevent rounding issues we need to work with the inclusive price where possible.
1155
				 * otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would.
1156
				 * be 8.325 leading to totals being 1p off.
1157
				 *
1158
				 * Pre tax coupons come off the price the customer thinks they are paying - tax is calculated.
1159
				 * afterwards.
1160
				 *
1161
				 * e.g. $100 bike with $10 coupon = customer pays $90 and tax worked backwards from that.
1162
				 */
1163
				} elseif ( $this->prices_include_tax ) {
1164
1165
					// Get base tax rates
1166
					if ( empty( $shop_tax_rates[ $_product->tax_class ] ) ) {
1167
						$shop_tax_rates[ $_product->tax_class ] = WC_Tax::get_base_tax_rates( $_product->tax_class );
1168
					}
1169
1170
					// Get item tax rates
1171
					if ( empty( $tax_rates[ $_product->get_tax_class() ] ) ) {
1172
						$tax_rates[ $_product->get_tax_class() ] = WC_Tax::get_rates( $_product->get_tax_class() );
1173
					}
1174
1175
					$base_tax_rates = $shop_tax_rates[ $_product->tax_class ];
1176
					$item_tax_rates = $tax_rates[ $_product->get_tax_class() ];
1177
1178
					/**
1179
					 * ADJUST TAX - Calculations when base tax is not equal to the item tax.
1180
					 *
1181
					 * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
1182
					 * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
1183
					 * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
1184
					 */
1185
					if ( $item_tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
1186
1187
						// Work out a new base price without the shop's base tax
1188
						$taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true, true );
1189
1190
						// Now we have a new item price (excluding TAX)
1191
						$line_subtotal = $line_price - array_sum( $taxes );
1192
1193
						// Now add modified taxes
1194
						$tax_result        = WC_Tax::calc_tax( $line_subtotal, $item_tax_rates );
1195
						$line_subtotal_tax = array_sum( $tax_result );
1196
1197
					/**
1198
					 * Regular tax calculation (customer inside base and the tax class is unmodified.
1199
					 */
1200
					} else {
1201
1202
						// Calc tax normally
1203
						$taxes             = WC_Tax::calc_tax( $line_price, $item_tax_rates, true );
1204
						$line_subtotal_tax = array_sum( $taxes );
1205
						$line_subtotal     = $line_price - array_sum( $taxes );
1206
					}
1207
1208
				/**
1209
				 * Prices exclude tax.
1210
				 *
1211
				 * This calculation is simpler - work with the base, untaxed price.
1212
				 */
1213
				} else {
1214
1215
					// Get item tax rates
1216
					if ( empty( $tax_rates[ $_product->get_tax_class() ] ) ) {
1217
						$tax_rates[ $_product->get_tax_class() ] = WC_Tax::get_rates( $_product->get_tax_class() );
1218
					}
1219
1220
					$item_tax_rates = $tax_rates[ $_product->get_tax_class() ];
1221
1222
					// Base tax for line before discount - we will store this in the order data
1223
					$taxes             = WC_Tax::calc_tax( $line_price, $item_tax_rates );
1224
					$line_subtotal_tax = array_sum( $taxes );
1225
1226
					$line_subtotal = $line_price;
1227
				}
1228
1229
				// Add to main subtotal
1230
				$this->subtotal        += $line_subtotal + $line_subtotal_tax;
1231
				$this->subtotal_ex_tax += $line_subtotal;
1232
			}
1233
1234
			// Order cart items by price so coupon logic is 'fair' for customers and not based on order added to cart.
1235
			uasort( $cart, array( $this, 'sort_by_subtotal' ) );
1236
1237
			/**
1238
			 * Calculate totals for items.
1239
			 */
1240
			foreach ( $cart as $cart_item_key => $values ) {
1241
1242
				$_product = $values['data'];
1243
1244
				// Prices
1245
				$base_price = $_product->get_price();
1246
				$line_price = $_product->get_price() * $values['quantity'];
1247
1248
				// Tax data
1249
				$taxes            = array();
1250
				$discounted_taxes = array();
1251
1252
				/**
1253
				 * No tax to calculate.
1254
				 */
1255
				if ( ! $_product->is_taxable() ) {
1256
1257
					// Discounted Price (price with any pre-tax discounts applied)
1258
					$discounted_price  = $this->get_discounted_price( $values, $base_price, true );
1259
					$line_subtotal_tax = 0;
1260
					$line_subtotal     = $line_price;
1261
					$line_tax          = 0;
1262
					$line_total        = round( $discounted_price * $values['quantity'], WC_ROUNDING_PRECISION );
1263
1264
				/**
1265
				 * Prices include tax.
1266
				 */
1267
				} elseif ( $this->prices_include_tax ) {
1268
1269
					$base_tax_rates = $shop_tax_rates[ $_product->tax_class ];
1270
					$item_tax_rates = $tax_rates[ $_product->get_tax_class() ];
1271
1272
					/**
1273
					 * ADJUST TAX - Calculations when base tax is not equal to the item tax.
1274
					 *
1275
					 * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
1276
					 * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
1277
					 * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
1278
					 */
1279
					if ( $item_tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
1280
1281
						// Work out a new base price without the shop's base tax
1282
						$taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true, true );
1283
1284
						// Now we have a new item price (excluding TAX)
1285
						$line_subtotal     = round( $line_price - array_sum( $taxes ), WC_ROUNDING_PRECISION );
1286
						$taxes             = WC_Tax::calc_tax( $line_subtotal, $item_tax_rates );
1287
						$line_subtotal_tax = array_sum( $taxes );
1288
1289
						// Adjusted price (this is the price including the new tax rate)
1290
						$adjusted_price = ( $line_subtotal + $line_subtotal_tax ) / $values['quantity'];
1291
1292
						// Apply discounts and get the discounted price FOR A SINGLE ITEM
1293
						$discounted_price = $this->get_discounted_price( $values, $adjusted_price, true );
1294
1295
						// Convert back to line price and round nicely
1296
						$discounted_line_price = round( $discounted_price * $values['quantity'], $this->dp );
1297
1298
						// Now use rounded line price to get taxes.
1299
						$discounted_taxes = WC_Tax::calc_tax( $discounted_line_price, $item_tax_rates, true );
1300
						$line_tax         = array_sum( $discounted_taxes );
1301
						$line_total       = $discounted_line_price - $line_tax;
1302
1303
					/**
1304
					 * Regular tax calculation (customer inside base and the tax class is unmodified.
1305
					 */
1306
					} else {
1307
1308
						// Work out a new base price without the item tax
1309
						$taxes = WC_Tax::calc_tax( $line_price, $item_tax_rates, true );
1310
1311
						// Now we have a new item price (excluding TAX)
1312
						$line_subtotal     = $line_price - array_sum( $taxes );
1313
						$line_subtotal_tax = array_sum( $taxes );
1314
1315
						// Calc prices and tax (discounted)
1316
						$discounted_price = $this->get_discounted_price( $values, $base_price, true );
1317
1318
						// Convert back to line price and round nicely
1319
						$discounted_line_price = round( $discounted_price * $values['quantity'], $this->dp );
1320
1321
						// Now use rounded line price to get taxes.
1322
						$discounted_taxes = WC_Tax::calc_tax( $discounted_line_price, $item_tax_rates, true );
1323
						$line_tax         = array_sum( $discounted_taxes );
1324
						$line_total       = $discounted_line_price - $line_tax;
1325
					}
1326
1327
					// Tax rows - merge the totals we just got
1328 View Code Duplication
					foreach ( array_keys( $this->taxes + $discounted_taxes ) as $key ) {
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...
1329
						$this->taxes[ $key ] = ( isset( $discounted_taxes[ $key ] ) ? $discounted_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 );
1330
					}
1331
1332
				/**
1333
				 * Prices exclude tax.
1334
				 */
1335
				} else {
1336
1337
					$item_tax_rates = $tax_rates[ $_product->get_tax_class() ];
1338
1339
					// Work out a new base price without the shop's base tax
1340
					$taxes = WC_Tax::calc_tax( $line_price, $item_tax_rates );
1341
1342
					// Now we have the item price (excluding TAX)
1343
					$line_subtotal     = $line_price;
1344
					$line_subtotal_tax = array_sum( $taxes );
1345
1346
					// Now calc product rates
1347
					$discounted_price      = $this->get_discounted_price( $values, $base_price, true );
1348
					$discounted_taxes      = WC_Tax::calc_tax( $discounted_price * $values['quantity'], $item_tax_rates );
1349
					$discounted_tax_amount = array_sum( $discounted_taxes );
1350
					$line_tax              = $discounted_tax_amount;
1351
					$line_total            = $discounted_price * $values['quantity'];
1352
1353
					// Tax rows - merge the totals we just got
1354 View Code Duplication
					foreach ( array_keys( $this->taxes + $discounted_taxes ) as $key ) {
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...
1355
						$this->taxes[ $key ] = ( isset( $discounted_taxes[ $key ] ) ? $discounted_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 );
1356
					}
1357
				}
1358
1359
				// Cart contents total is based on discounted prices and is used for the final total calculation
1360
				$this->cart_contents_total += $line_total;
1361
1362
				// Store costs + taxes for lines
1363
				$this->cart_contents[ $cart_item_key ]['line_total']        = $line_total;
1364
				$this->cart_contents[ $cart_item_key ]['line_tax']          = $line_tax;
1365
				$this->cart_contents[ $cart_item_key ]['line_subtotal']     = $line_subtotal;
1366
				$this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = $line_subtotal_tax;
1367
1368
				// Store rates ID and costs - Since 2.2
1369
				$this->cart_contents[ $cart_item_key ]['line_tax_data'] = array( 'total' => $discounted_taxes, 'subtotal' => $taxes );
1370
			}
1371
1372
			// Only calculate the grand total + shipping if on the cart/checkout
1373
			if ( is_checkout() || is_cart() || defined( 'WOOCOMMERCE_CHECKOUT' ) || defined( 'WOOCOMMERCE_CART' ) ) {
1374
1375
				// Calculate the Shipping
1376
				$this->calculate_shipping();
1377
1378
				// Trigger the fees API where developers can add fees to the cart
1379
				$this->calculate_fees();
1380
1381
				// Total up/round taxes and shipping taxes
1382
				if ( $this->round_at_subtotal ) {
1383
					$this->tax_total          = WC_Tax::get_tax_total( $this->taxes );
0 ignored issues
show
Documentation Bug introduced by
It seems like \WC_Tax::get_tax_total($this->taxes) can also be of type integer. However, the property $tax_total is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1384
					$this->shipping_tax_total = WC_Tax::get_tax_total( $this->shipping_taxes );
0 ignored issues
show
Documentation Bug introduced by
It seems like \WC_Tax::get_tax_total($this->shipping_taxes) can also be of type integer. However, the property $shipping_tax_total is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1385
					$this->taxes              = array_map( array( 'WC_Tax', 'round' ), $this->taxes );
1386
					$this->shipping_taxes     = array_map( array( 'WC_Tax', 'round' ), $this->shipping_taxes );
1387
				} else {
1388
					$this->tax_total          = array_sum( $this->taxes );
0 ignored issues
show
Documentation Bug introduced by
It seems like array_sum($this->taxes) can also be of type integer. However, the property $tax_total is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1389
					$this->shipping_tax_total = array_sum( $this->shipping_taxes );
0 ignored issues
show
Documentation Bug introduced by
It seems like array_sum($this->shipping_taxes) can also be of type integer. However, the property $shipping_tax_total is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1390
				}
1391
1392
				// VAT exemption done at this point - so all totals are correct before exemption
1393
				if ( WC()->customer->is_vat_exempt() ) {
1394
					$this->remove_taxes();
1395
				}
1396
1397
				// Allow plugins to hook and alter totals before final total is calculated
1398
				do_action( 'woocommerce_calculate_totals', $this );
1399
1400
				// Grand Total - Discounted product prices, discounted tax, shipping cost + tax
1401
				$this->total = max( 0, apply_filters( 'woocommerce_calculated_total', round( $this->cart_contents_total + $this->tax_total + $this->shipping_tax_total + $this->shipping_total + $this->fee_total, $this->dp ), $this ) );
1402
1403
			} else {
1404
1405
				// Set tax total to sum of all tax rows
1406
				$this->tax_total = WC_Tax::get_tax_total( $this->taxes );
1407
1408
				// VAT exemption done at this point - so all totals are correct before exemption
1409
				if ( WC()->customer->is_vat_exempt() ) {
1410
					$this->remove_taxes();
1411
				}
1412
			}
1413
1414
			do_action( 'woocommerce_after_calculate_totals', $this );
1415
1416
			$this->set_session();
1417
		}
1418
1419
		/**
1420
		 * Remove taxes.
1421
		 */
1422
		public function remove_taxes() {
1423
			$this->shipping_tax_total = $this->tax_total = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $tax_total was declared of type double, but 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
Documentation Bug introduced by
The property $shipping_tax_total was declared of type double, but $this->tax_total = 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1424
			$this->subtotal           = $this->subtotal_ex_tax;
1425
1426
			foreach ( $this->cart_contents as $cart_item_key => $item ) {
1427
				$this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = $this->cart_contents[ $cart_item_key ]['line_tax'] = 0;
1428
				$this->cart_contents[ $cart_item_key ]['line_tax_data']     = array( 'total' => array(), 'subtotal' => array() );
1429
			}
1430
1431
			// If true, zero rate is applied so '0' tax is displayed on the frontend rather than nothing.
1432
			if ( apply_filters( 'woocommerce_cart_remove_taxes_apply_zero_rate', true ) ) {
1433
				$this->taxes = $this->shipping_taxes = array( apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) => 0 );
1434
			} else {
1435
				$this->taxes = $this->shipping_taxes = array();
1436
			}
1437
		}
1438
1439
		/**
1440
		 * Looks at the totals to see if payment is actually required.
1441
		 *
1442
		 * @return bool
1443
		 */
1444
		public function needs_payment() {
1445
			return apply_filters( 'woocommerce_cart_needs_payment', $this->total > 0, $this );
1446
		}
1447
1448
	/*-----------------------------------------------------------------------------------*/
1449
	/* Shipping related functions */
1450
	/*-----------------------------------------------------------------------------------*/
1451
1452
		/**
1453
		 * Uses the shipping class to calculate shipping then gets the totals when its finished.
1454
		 */
1455
		public function calculate_shipping() {
1456
			if ( $this->needs_shipping() && $this->show_shipping() ) {
1457
				WC()->shipping->calculate_shipping( $this->get_shipping_packages() );
1458
			} else {
1459
				WC()->shipping->reset_shipping();
1460
			}
1461
1462
			// Get totals for the chosen shipping method
1463
			$this->shipping_total = WC()->shipping->shipping_total;	// Shipping Total
1464
			$this->shipping_taxes = WC()->shipping->shipping_taxes;	// Shipping Taxes
1465
		}
1466
1467
		/**
1468
		 * Get packages to calculate shipping for.
1469
		 *
1470
		 * This lets us calculate costs for carts that are shipped to multiple locations.
1471
		 *
1472
		 * Shipping methods are responsible for looping through these packages.
1473
		 *
1474
		 * By default we pass the cart itself as a package - plugins can change this.
1475
		 * through the filter and break it up.
1476
		 *
1477
		 * @since 1.5.4
1478
		 * @return array of cart items
1479
		 */
1480
		public function get_shipping_packages() {
1481
			// Packages array for storing 'carts'
1482
			$packages = array();
1483
1484
			$packages[0]['contents']                 = $this->get_cart();		// Items in the package
1485
			$packages[0]['contents_cost']            = 0;						// Cost of items in the package, set below
1486
			$packages[0]['applied_coupons']          = $this->applied_coupons;
1487
			$packages[0]['user']['ID']               = get_current_user_id();
1488
			$packages[0]['destination']['country']   = WC()->customer->get_shipping_country();
1489
			$packages[0]['destination']['state']     = WC()->customer->get_shipping_state();
1490
			$packages[0]['destination']['postcode']  = WC()->customer->get_shipping_postcode();
1491
			$packages[0]['destination']['city']      = WC()->customer->get_shipping_city();
1492
			$packages[0]['destination']['address']   = WC()->customer->get_shipping_address();
1493
			$packages[0]['destination']['address_2'] = WC()->customer->get_shipping_address_2();
1494
1495
			foreach ( $this->get_cart() as $item ) {
1496
				if ( $item['data']->needs_shipping() ) {
1497
					if ( isset( $item['line_total'] ) ) {
1498
						$packages[0]['contents_cost'] += $item['line_total'];
1499
					}
1500
				}
1501
			}
1502
1503
			return apply_filters( 'woocommerce_cart_shipping_packages', $packages );
1504
		}
1505
1506
		/**
1507
		 * Looks through the cart to see if shipping is actually required.
1508
		 *
1509
		 * @return bool whether or not the cart needs shipping
1510
		 */
1511
		public function needs_shipping() {
1512
			// If shipping is disabled or not yet configured, we can skip this.
1513
			if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( true ) ) {
1514
				return false;
1515
			}
1516
1517
			$needs_shipping = false;
1518
1519
			if ( ! empty( $this->cart_contents ) ) {
1520
				foreach ( $this->cart_contents as $cart_item_key => $values ) {
1521
					$_product = $values['data'];
1522
					if ( $_product->needs_shipping() ) {
1523
						$needs_shipping = true;
1524
					}
1525
				}
1526
			}
1527
1528
			return apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping );
1529
		}
1530
1531
		/**
1532
		 * Should the shipping address form be shown.
1533
		 *
1534
		 * @return bool
1535
		 */
1536
		public function needs_shipping_address() {
1537
			$needs_shipping_address = false;
1538
1539
			if ( $this->needs_shipping() === true && ! wc_ship_to_billing_address_only() ) {
1540
				$needs_shipping_address = true;
1541
			}
1542
1543
			return apply_filters( 'woocommerce_cart_needs_shipping_address', $needs_shipping_address );
1544
		}
1545
1546
		/**
1547
		 * Sees if the customer has entered enough data to calc the shipping yet.
1548
		 *
1549
		 * @return bool
1550
		 */
1551
		public function show_shipping() {
1552
			if ( ! wc_shipping_enabled() || ! is_array( $this->cart_contents ) ) {
1553
				return false;
1554
			}
1555
1556
			if ( 'yes' === get_option( 'woocommerce_shipping_cost_requires_address' ) ) {
1557
				if ( ! WC()->customer->has_calculated_shipping() ) {
1558
					if ( ! WC()->customer->get_shipping_country() || ( ! WC()->customer->get_shipping_state() && ! WC()->customer->get_shipping_postcode() ) ) {
1559
						return false;
1560
					}
1561
				}
1562
			}
1563
1564
			return apply_filters( 'woocommerce_cart_ready_to_calc_shipping', true );
1565
		}
1566
1567
		/**
1568
		 * Sees if we need a shipping address.
1569
		 *
1570
		 * @deprecated 2.5.0 in favor to wc_ship_to_billing_address_only()
1571
		 *
1572
		 * @return bool
1573
		 */
1574
		public function ship_to_billing_address_only() {
1575
			return wc_ship_to_billing_address_only();
1576
		}
1577
1578
		/**
1579
		 * Gets the shipping total (after calculation).
1580
		 *
1581
		 * @return string price or string for the shipping total
1582
		 */
1583
		public function get_cart_shipping_total() {
1584
			if ( isset( $this->shipping_total ) ) {
1585
				if ( $this->shipping_total > 0 ) {
1586
1587
					// Display varies depending on settings
1588
					if ( $this->tax_display_cart == 'excl' ) {
1589
1590
						$return = wc_price( $this->shipping_total );
1591
1592
						if ( $this->shipping_tax_total > 0 && $this->prices_include_tax ) {
1593
							$return .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
1594
						}
1595
1596
						return $return;
1597
1598
					} else {
1599
1600
						$return = wc_price( $this->shipping_total + $this->shipping_tax_total );
1601
1602
						if ( $this->shipping_tax_total > 0 && ! $this->prices_include_tax ) {
1603
							$return .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
1604
						}
1605
1606
						return $return;
1607
1608
					}
1609
1610
				} else {
1611
					return __( 'Free!', 'woocommerce' );
1612
				}
1613
			}
1614
1615
			return '';
1616
		}
1617
1618
	/*-----------------------------------------------------------------------------------*/
1619
	/* Coupons/Discount related functions */
1620
	/*-----------------------------------------------------------------------------------*/
1621
1622
		/**
1623
		 * Check for user coupons (now that we have billing email). If a coupon is invalid, add an error.
1624
		 *
1625
		 * Checks two types of coupons:
1626
		 * 1. Where a list of customer emails are set (limits coupon usage to those defined).
1627
		 * 2. Where a usage_limit_per_user is set (limits coupon usage to a number based on user ID and email).
1628
		 *
1629
		 * @param array $posted
1630
		 */
1631
		public function check_customer_coupons( $posted ) {
1632
			if ( ! empty( $this->applied_coupons ) ) {
1633
				foreach ( $this->applied_coupons as $code ) {
1634
					$coupon = new WC_Coupon( $code );
1635
1636
					if ( $coupon->is_valid() ) {
1637
1638
						// Limit to defined email addresses
1639
						if ( is_array( $coupon->customer_email ) && sizeof( $coupon->customer_email ) > 0 ) {
1640
							$check_emails           = array();
1641
							$coupon->customer_email = array_map( 'sanitize_email', $coupon->customer_email );
1642
1643
							if ( is_user_logged_in() ) {
1644
								$current_user   = wp_get_current_user();
1645
								$check_emails[] = $current_user->user_email;
1646
							}
1647
							$check_emails[] = $posted['billing_email'];
1648
							$check_emails   = array_map( 'sanitize_email', array_map( 'strtolower', $check_emails ) );
1649
1650
							if ( 0 == sizeof( array_intersect( $check_emails, $coupon->customer_email ) ) ) {
1651
								$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
1652
1653
								// Remove the coupon
1654
								$this->remove_coupon( $code );
1655
1656
								// Flag totals for refresh
1657
								WC()->session->set( 'refresh_totals', true );
1658
							}
1659
						}
1660
1661
						// Usage limits per user - check against billing and user email and user ID
1662
						if ( $coupon->usage_limit_per_user > 0 ) {
1663
							$check_emails = array();
1664
							$used_by      = $coupon->get_used_by();
1665
1666
							if ( is_user_logged_in() ) {
1667
								$current_user   = wp_get_current_user();
1668
								$check_emails[] = sanitize_email( $current_user->user_email );
1669
								$usage_count    = sizeof( array_keys( $used_by, get_current_user_id() ) );
1670
							} else {
1671
								$check_emails[] = sanitize_email( $posted['billing_email'] );
1672
								$user           = get_user_by( 'email', $posted['billing_email'] );
1673
								if ( $user ) {
1674
									$usage_count = sizeof( array_keys( $used_by, $user->ID ) );
1675
								} else {
1676
									$usage_count = 0;
1677
								}
1678
							}
1679
1680
							foreach ( $check_emails as $check_email ) {
1681
								$usage_count = $usage_count + sizeof( array_keys( $used_by, $check_email ) );
1682
							}
1683
1684 View Code Duplication
							if ( $usage_count >= $coupon->usage_limit_per_user ) {
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...
1685
								$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED );
1686
1687
								// Remove the coupon
1688
								$this->remove_coupon( $code );
1689
1690
								// Flag totals for refresh
1691
								WC()->session->set( 'refresh_totals', true );
1692
							}
1693
						}
1694
					}
1695
				}
1696
			}
1697
		}
1698
1699
		/**
1700
		 * Returns whether or not a discount has been applied.
1701
		 * @param  string $coupon_code
1702
		 * @return bool
1703
		 */
1704
		public function has_discount( $coupon_code = '' ) {
1705
			return $coupon_code ? in_array( apply_filters( 'woocommerce_coupon_code', $coupon_code ), $this->applied_coupons ) : sizeof( $this->applied_coupons ) > 0;
1706
		}
1707
1708
		/**
1709
		 * Applies a coupon code passed to the method.
1710
		 *
1711
		 * @param  string $coupon_code - The code to apply
1712
		 * @return bool                True if the coupon is applied, false if it does not exist or cannot be applied
1713
		 */
1714
		public function add_discount( $coupon_code ) {
1715
			// Coupons are globally disabled
1716
			if ( ! wc_coupons_enabled() ) {
1717
				return false;
1718
			}
1719
1720
			// Sanitize coupon code
1721
			$coupon_code = apply_filters( 'woocommerce_coupon_code', $coupon_code );
1722
1723
			// Get the coupon
1724
			$the_coupon = new WC_Coupon( $coupon_code );
1725
1726
			// Check it can be used with cart
1727
			if ( ! $the_coupon->is_valid() ) {
1728
				wc_add_notice( $the_coupon->get_error_message(), 'error' );
1729
				return false;
1730
			}
1731
1732
			// Check if applied
1733
			if ( $this->has_discount( $coupon_code ) ) {
1734
				$the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED );
1735
				return false;
1736
			}
1737
1738
			// If its individual use then remove other coupons
1739
			if ( $the_coupon->individual_use == 'yes' ) {
1740
				$this->applied_coupons = apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $the_coupon, $this->applied_coupons );
1741
			}
1742
1743
			if ( $this->applied_coupons ) {
1744
				foreach ( $this->applied_coupons as $code ) {
1745
					$coupon = new WC_Coupon( $code );
1746
1747
					if ( $coupon->individual_use == 'yes' && false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $the_coupon, $coupon, $this->applied_coupons ) ) {
1748
1749
						// Reject new coupon
1750
						$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY );
1751
1752
						return false;
1753
					}
1754
				}
1755
			}
1756
1757
			$this->applied_coupons[] = $coupon_code;
1758
1759
			// Choose free shipping
1760
			if ( $the_coupon->enable_free_shipping() ) {
1761
				$packages                = WC()->shipping->get_packages();
1762
				$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
1763
1764
				foreach ( $packages as $i => $package ) {
1765
					$chosen_shipping_methods[ $i ] = 'free_shipping';
1766
				}
1767
1768
				WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
1769
			}
1770
1771
			$the_coupon->add_coupon_message( WC_Coupon::WC_COUPON_SUCCESS );
1772
1773
			do_action( 'woocommerce_applied_coupon', $coupon_code );
1774
1775
			return true;
1776
		}
1777
1778
		/**
1779
		 * Get array of applied coupon objects and codes.
1780
		 * @return array of applied coupons
1781
		 */
1782
		public function get_coupons( $deprecated = null ) {
1783
			$coupons = array();
1784
1785
			if ( 'order' === $deprecated ) {
1786
				return $coupons;
1787
			}
1788
1789
			foreach ( $this->get_applied_coupons() as $code ) {
1790
				$coupon           = new WC_Coupon( $code );
1791
				$coupons[ $code ] = $coupon;
1792
			}
1793
1794
			return $coupons;
1795
		}
1796
1797
		/**
1798
		 * Gets the array of applied coupon codes.
1799
		 *
1800
		 * @return array of applied coupons
1801
		 */
1802
		public function get_applied_coupons() {
1803
			return $this->applied_coupons;
1804
		}
1805
1806
		/**
1807
		 * Get the discount amount for a used coupon.
1808
		 * @param  string $code   coupon code
1809
		 * @param  bool   $ex_tax inc or ex tax
1810
		 * @return float          discount amount
1811
		 */
1812
		public function get_coupon_discount_amount( $code, $ex_tax = true ) {
1813
			$discount_amount = isset( $this->coupon_discount_amounts[ $code ] ) ? $this->coupon_discount_amounts[ $code ] : 0;
1814
1815
			if ( ! $ex_tax ) {
1816
				$discount_amount += $this->get_coupon_discount_tax_amount( $code );
1817
			}
1818
1819
			return wc_cart_round_discount( $discount_amount, $this->dp );
1820
		}
1821
1822
		/**
1823
		 * Get the discount tax amount for a used coupon (for tax inclusive prices).
1824
		 * @param  string $code coupon code
1825
		 * @param  bool         inc or ex tax
1826
		 * @return float        discount amount
1827
		 */
1828
		public function get_coupon_discount_tax_amount( $code ) {
1829
			return wc_cart_round_discount( isset( $this->coupon_discount_tax_amounts[ $code ] ) ? $this->coupon_discount_tax_amounts[ $code ] : 0, $this->dp );
1830
		}
1831
1832
		/**
1833
		 * Remove coupons from the cart of a defined type. Type 1 is before tax, type 2 is after tax.
1834
		 */
1835
		public function remove_coupons( $deprecated = null ) {
0 ignored issues
show
Unused Code introduced by
The parameter $deprecated is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1836
			$this->applied_coupons = $this->coupon_discount_amounts = $this->coupon_discount_tax_amounts = $this->coupon_applied_count = array();
1837
			WC()->session->set( 'applied_coupons', array() );
1838
			WC()->session->set( 'coupon_discount_amounts', array() );
1839
			WC()->session->set( 'coupon_discount_tax_amounts', array() );
1840
		}
1841
1842
		/**
1843
		 * Remove a single coupon by code.
1844
		 * @param  string $coupon_code Code of the coupon to remove
1845
		 * @return bool
1846
		 */
1847
		public function remove_coupon( $coupon_code ) {
1848
			// Coupons are globally disabled
1849
			if ( ! wc_coupons_enabled() ) {
1850
				return false;
1851
			}
1852
1853
			// Get the coupon
1854
			$coupon_code = apply_filters( 'woocommerce_coupon_code', $coupon_code );
1855
			$position    = array_search( $coupon_code, $this->applied_coupons );
1856
1857
			if ( $position !== false ) {
1858
				unset( $this->applied_coupons[ $position ] );
1859
			}
1860
1861
			WC()->session->set( 'applied_coupons', $this->applied_coupons );
1862
1863
			do_action( 'woocommerce_removed_coupon', $coupon_code );
1864
1865
			return true;
1866
		}
1867
1868
		/**
1869
		 * Function to apply discounts to a product and get the discounted price (before tax is applied).
1870
		 *
1871
		 * @param  mixed $values
1872
		 * @param  mixed $price
1873
		 * @param  bool  $add_totals (default: false)
1874
		 * @return float             price
1875
		 */
1876
		public function get_discounted_price( $values, $price, $add_totals = false ) {
1877
			if ( ! $price ) {
1878
				return $price;
1879
			}
1880
1881
			$undiscounted_price = $price;
1882
1883
			if ( ! empty( $this->coupons ) ) {
1884
				$product = $values['data'];
1885
1886
				foreach ( $this->coupons as $code => $coupon ) {
1887
					if ( $coupon->is_valid() && ( $coupon->is_valid_for_product( $product, $values ) || $coupon->is_valid_for_cart() ) ) {
1888
						$discount_amount = $coupon->get_discount_amount( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ? $price : $undiscounted_price, $values, true );
1889
						$discount_amount = min( $price, $discount_amount );
1890
						$price           = max( $price - $discount_amount, 0 );
1891
1892
						// Store the totals for DISPLAY in the cart
1893
						if ( $add_totals ) {
1894
							$total_discount     = $discount_amount * $values['quantity'];
1895
							$total_discount_tax = 0;
1896
1897
							if ( wc_tax_enabled() ) {
1898
								$tax_rates                = WC_Tax::get_rates( $product->get_tax_class() );
1899
								$taxes                    = WC_Tax::calc_tax( $discount_amount, $tax_rates, $this->prices_include_tax );
1900
								$total_discount_tax       = WC_Tax::get_tax_total( $taxes ) * $values['quantity'];
1901
								$total_discount           = $this->prices_include_tax ? $total_discount - $total_discount_tax : $total_discount;
1902
								$this->discount_cart_tax += $total_discount_tax;
1903
							}
1904
1905
							$this->discount_cart += $total_discount;
1906
							$this->increase_coupon_discount_amount( $code, $total_discount, $total_discount_tax );
1907
							$this->increase_coupon_applied_count( $code, $values['quantity'] );
1908
						}
1909
					}
1910
1911
					// If the price is 0, we can stop going through coupons because there is nothing more to discount for this product.
1912
					if ( 0 >= $price ) {
1913
						break;
1914
					}
1915
				}
1916
			}
1917
1918
			return apply_filters( 'woocommerce_get_discounted_price', $price, $values, $this );
1919
		}
1920
1921
		/**
1922
		 * Store how much discount each coupon grants.
1923
		 *
1924
		 * @access private
1925
		 * @param string $code
1926
		 * @param double $amount
1927
		 * @param double $tax
1928
		 */
1929
		private function increase_coupon_discount_amount( $code, $amount, $tax ) {
1930
			$this->coupon_discount_amounts[ $code ]     = isset( $this->coupon_discount_amounts[ $code ] ) ? $this->coupon_discount_amounts[ $code ] + $amount : $amount;
1931
			$this->coupon_discount_tax_amounts[ $code ] = isset( $this->coupon_discount_tax_amounts[ $code ] ) ? $this->coupon_discount_tax_amounts[ $code ] + $tax : $tax;
1932
		}
1933
1934
		/**
1935
		 * Store how many times each coupon is applied to cart/items.
1936
		 *
1937
		 * @access private
1938
		 * @param string $code
1939
		 * @param int    $count
1940
		 */
1941
		private function increase_coupon_applied_count( $code, $count = 1 ) {
1942
			if ( empty( $this->coupon_applied_count[ $code ] ) ) {
1943
				$this->coupon_applied_count[ $code ] = 0;
1944
			}
1945
			$this->coupon_applied_count[ $code ] += $count;
1946
		}
1947
1948
	/*-----------------------------------------------------------------------------------*/
1949
	/* Fees API to add additional costs to orders */
1950
	/*-----------------------------------------------------------------------------------*/
1951
1952
		/**
1953
		 * Add additional fee to the cart.
1954
		 *
1955
		 * @param string $name      Unique name for the fee. Multiple fees of the same name cannot be added.
1956
		 * @param float  $amount    Fee amount.
1957
		 * @param bool   $taxable   (default: false) Is the fee taxable?
1958
		 * @param string $tax_class (default: '') The tax class for the fee if taxable. A blank string is standard tax class.
1959
		 */
1960
		public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) {
1961
			$new_fee_id = sanitize_title( $name );
1962
1963
			// Only add each fee once
1964
			foreach ( $this->fees as $fee ) {
1965
				if ( $fee->id == $new_fee_id ) {
1966
					return;
1967
				}
1968
			}
1969
1970
			$new_fee            = new stdClass();
1971
			$new_fee->id        = $new_fee_id;
1972
			$new_fee->name      = esc_attr( $name );
1973
			$new_fee->amount    = (float) esc_attr( $amount );
1974
			$new_fee->tax_class = $tax_class;
1975
			$new_fee->taxable   = $taxable ? true : false;
1976
			$new_fee->tax       = 0;
1977
			$new_fee->tax_data  = array();
1978
			$this->fees[]       = $new_fee;
1979
		}
1980
1981
		/**
1982
		 * Get fees.
1983
		 *
1984
		 * @return array
1985
		 */
1986
		public function get_fees() {
1987
			return array_filter( (array) $this->fees );
1988
		}
1989
1990
		/**
1991
		 * Calculate fees.
1992
		 */
1993
		public function calculate_fees() {
1994
			// Reset fees before calculation
1995
			$this->fee_total = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $fee_total was declared of type double, but 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1996
			$this->fees      = array();
1997
1998
			// Fire an action where developers can add their fees
1999
			do_action( 'woocommerce_cart_calculate_fees', $this );
2000
2001
			// If fees were added, total them and calculate tax
2002
			if ( ! empty( $this->fees ) ) {
2003
				foreach ( $this->fees as $fee_key => $fee ) {
2004
					$this->fee_total += $fee->amount;
2005
2006
					if ( $fee->taxable ) {
2007
						// Get tax rates
2008
						$tax_rates = WC_Tax::get_rates( $fee->tax_class );
2009
						$fee_taxes = WC_Tax::calc_tax( $fee->amount, $tax_rates, false );
2010
2011
						if ( ! empty( $fee_taxes ) ) {
2012
							// Set the tax total for this fee
2013
							$this->fees[ $fee_key ]->tax = array_sum( $fee_taxes );
2014
2015
							// Set tax data - Since 2.2
2016
							$this->fees[ $fee_key ]->tax_data = $fee_taxes;
2017
2018
							// Tax rows - merge the totals we just got
2019 View Code Duplication
							foreach ( array_keys( $this->taxes + $fee_taxes ) as $key ) {
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...
2020
								$this->taxes[ $key ] = ( isset( $fee_taxes[ $key ] ) ? $fee_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 );
2021
							}
2022
						}
2023
					}
2024
				}
2025
			}
2026
		}
2027
2028
	/*-----------------------------------------------------------------------------------*/
2029
	/* Get Formatted Totals */
2030
	/*-----------------------------------------------------------------------------------*/
2031
2032
		/**
2033
		 * Gets the order total (after calculation).
2034
		 *
2035
		 * @return string formatted price
2036
		 */
2037
		public function get_total() {
2038
			return apply_filters( 'woocommerce_cart_total', wc_price( $this->total ) );
2039
		}
2040
2041
		/**
2042
		 * Gets the total excluding taxes.
2043
		 *
2044
		 * @return string formatted price
2045
		 */
2046
		public function get_total_ex_tax() {
2047
			$total = $this->total - $this->tax_total - $this->shipping_tax_total;
2048
			if ( $total < 0 ) {
2049
				$total = 0;
2050
			}
2051
			return apply_filters( 'woocommerce_cart_total_ex_tax', wc_price( $total ) );
2052
		}
2053
2054
		/**
2055
		 * Gets the cart contents total (after calculation).
2056
		 *
2057
		 * @return string formatted price
2058
		 */
2059
		public function get_cart_total() {
2060
			if ( ! $this->prices_include_tax ) {
2061
				$cart_contents_total = wc_price( $this->cart_contents_total );
2062
			} else {
2063
				$cart_contents_total = wc_price( $this->cart_contents_total + $this->tax_total );
2064
			}
2065
2066
			return apply_filters( 'woocommerce_cart_contents_total', $cart_contents_total );
2067
		}
2068
2069
		/**
2070
		 * Gets the sub total (after calculation).
2071
		 *
2072
		 * @param  bool   $compound whether to include compound taxes
2073
		 * @return string           formatted price
2074
		 */
2075
		public function get_cart_subtotal( $compound = false ) {
2076
			// If the cart has compound tax, we want to show the subtotal as
2077
			// cart + shipping + non-compound taxes (after discount)
2078
			if ( $compound ) {
2079
2080
				$cart_subtotal = wc_price( $this->cart_contents_total + $this->shipping_total + $this->get_taxes_total( false, false ) );
2081
2082
			// Otherwise we show cart items totals only (before discount)
2083
			} else {
2084
2085
				// Display varies depending on settings
2086
				if ( $this->tax_display_cart == 'excl' ) {
2087
2088
					$cart_subtotal = wc_price( $this->subtotal_ex_tax );
2089
2090
					if ( $this->tax_total > 0 && $this->prices_include_tax ) {
2091
						$cart_subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
2092
					}
2093
2094
				} else {
2095
2096
					$cart_subtotal = wc_price( $this->subtotal );
2097
2098
					if ( $this->tax_total > 0 && ! $this->prices_include_tax ) {
2099
						$cart_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
2100
					}
2101
2102
				}
2103
			}
2104
2105
			return apply_filters( 'woocommerce_cart_subtotal', $cart_subtotal, $compound, $this );
2106
		}
2107
2108
		/**
2109
		 * Get the product row price per item.
2110
		 *
2111
		 * @param  WC_Product $_product
2112
		 * @return string               formatted price
2113
		 */
2114
		public function get_product_price( $_product ) {
2115
			if ( $this->tax_display_cart == 'excl' ) {
2116
				$product_price = $_product->get_price_excluding_tax();
2117
			} else {
2118
				$product_price = $_product->get_price_including_tax();
2119
			}
2120
2121
			return apply_filters( 'woocommerce_cart_product_price', wc_price( $product_price ), $_product );
2122
		}
2123
2124
		/**
2125
		 * Get the product row subtotal.
2126
		 *
2127
		 * Gets the tax etc to avoid rounding issues.
2128
		 *
2129
		 * When on the checkout (review order), this will get the subtotal based on the customer's tax rate rather than the base rate.
2130
		 *
2131
		 * @param  WC_Product $_product
2132
		 * @param  int        $quantity
2133
		 * @return string               formatted price
2134
		 */
2135
		public function get_product_subtotal( $_product, $quantity ) {
2136
			$price   = $_product->get_price();
2137
			$taxable = $_product->is_taxable();
2138
2139
			// Taxable
2140
			if ( $taxable ) {
2141
2142
				if ( $this->tax_display_cart == 'excl' ) {
2143
2144
					$row_price        = $_product->get_price_excluding_tax( $quantity );
2145
					$product_subtotal = wc_price( $row_price );
2146
2147
					if ( $this->prices_include_tax && $this->tax_total > 0 ) {
2148
						$product_subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
2149
					}
2150
2151
				} else {
2152
2153
					$row_price        = $_product->get_price_including_tax( $quantity );
2154
					$product_subtotal = wc_price( $row_price );
2155
2156
					if ( ! $this->prices_include_tax && $this->tax_total > 0 ) {
2157
						$product_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
2158
					}
2159
2160
				}
2161
2162
			// Non-taxable
2163
			} else {
2164
2165
				$row_price        = $price * $quantity;
2166
				$product_subtotal = wc_price( $row_price );
2167
2168
			}
2169
2170
			return apply_filters( 'woocommerce_cart_product_subtotal', $product_subtotal, $_product, $quantity, $this );
2171
		}
2172
2173
		/**
2174
		 * Gets the cart tax (after calculation).
2175
		 *
2176
		 * @return string formatted price
2177
		 */
2178
		public function get_cart_tax() {
2179
			$cart_total_tax = wc_round_tax_total( $this->tax_total + $this->shipping_tax_total );
2180
2181
			return apply_filters( 'woocommerce_get_cart_tax', $cart_total_tax ? wc_price( $cart_total_tax ) : '' );
2182
		}
2183
2184
		/**
2185
		 * Get a tax amount.
2186
		 * @param  string $tax_rate_id
2187
		 * @return float               amount
2188
		 */
2189
		public function get_tax_amount( $tax_rate_id ) {
2190
			return isset( $this->taxes[ $tax_rate_id ] ) ? $this->taxes[ $tax_rate_id ] : 0;
2191
		}
2192
2193
		/**
2194
		 * Get a tax amount.
2195
		 * @param  string $tax_rate_id
2196
		 * @return float               amount
2197
		 */
2198
		public function get_shipping_tax_amount( $tax_rate_id ) {
2199
			return isset( $this->shipping_taxes[ $tax_rate_id ] ) ? $this->shipping_taxes[ $tax_rate_id ] : 0;
2200
		}
2201
2202
		/**
2203
		 * Get tax row amounts with or without compound taxes includes.
2204
		 *
2205
		 * @param  bool  $compound True if getting compound taxes
2206
		 * @param  bool  $display  True if getting total to display
2207
		 * @return float           price
2208
		 */
2209
		public function get_taxes_total( $compound = true, $display = true ) {
2210
			$total = 0;
2211 View Code Duplication
			foreach ( $this->taxes as $key => $tax ) {
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...
2212
				if ( ! $compound && WC_Tax::is_compound( $key ) ) continue;
2213
				$total += $tax;
2214
			}
2215 View Code Duplication
			foreach ( $this->shipping_taxes as $key => $tax ) {
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...
2216
				if ( ! $compound && WC_Tax::is_compound( $key ) ) continue;
2217
				$total += $tax;
2218
			}
2219
			if ( $display ) {
2220
				$total = wc_round_tax_total( $total );
2221
			}
2222
			return apply_filters( 'woocommerce_cart_taxes_total', $total, $compound, $display, $this );
2223
		}
2224
2225
		/**
2226
		 * Get the total of all cart discounts.
2227
		 *
2228
		 * @return float
2229
		 */
2230
		public function get_cart_discount_total() {
2231
			return wc_cart_round_discount( $this->discount_cart, $this->dp );
2232
		}
2233
2234
		/**
2235
		 * Get the total of all cart tax discounts (used for discounts on tax inclusive prices).
2236
		 *
2237
		 * @return float
2238
		 */
2239
		public function get_cart_discount_tax_total() {
2240
			return wc_cart_round_discount( $this->discount_cart_tax, $this->dp );
2241
		}
2242
2243
		/**
2244
		 * Gets the total discount amount - both kinds.
2245
		 *
2246
		 * @return mixed formatted price or false if there are none
2247
		 */
2248
		public function get_total_discount() {
2249
			if ( $this->get_cart_discount_total() ) {
2250
				$total_discount = wc_price( $this->get_cart_discount_total() );
2251
			} else {
2252
				$total_discount = false;
2253
			}
2254
			return apply_filters( 'woocommerce_cart_total_discount', $total_discount, $this );
2255
		}
2256
2257
		/**
2258
		 * Gets the total (product) discount amount - these are applied before tax.
2259
		 *
2260
		 * @deprecated Order discounts (after tax) removed in 2.3 so multiple methods for discounts are no longer required.
2261
		 * @return mixed formatted price or false if there are none
2262
		 */
2263
		public function get_discounts_before_tax() {
2264
			_deprecated_function( 'get_discounts_before_tax', '2.3', 'get_total_discount' );
2265
			if ( $this->get_cart_discount_total() ) {
2266
				$discounts_before_tax = wc_price( $this->get_cart_discount_total() );
2267
			} else {
2268
				$discounts_before_tax = false;
2269
			}
2270
			return apply_filters( 'woocommerce_cart_discounts_before_tax', $discounts_before_tax, $this );
2271
		}
2272
2273
		/**
2274
		 * Get the total of all order discounts (after tax discounts).
2275
		 *
2276
		 * @deprecated Order discounts (after tax) removed in 2.3
2277
		 * @return int
2278
		 */
2279
		public function get_order_discount_total() {
2280
			_deprecated_function( 'get_order_discount_total', '2.3' );
2281
			return 0;
2282
		}
2283
2284
		/**
2285
		 * Function to apply cart discounts after tax.
2286
		 * @deprecated Coupons can not be applied after tax
2287
		 */
2288
		public function apply_cart_discounts_after_tax( $values, $price ) {
0 ignored issues
show
Unused Code introduced by
The parameter $values is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $price is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2289
			_deprecated_function( 'apply_cart_discounts_after_tax', '2.3' );
2290
		}
2291
2292
		/**
2293
		 * Function to apply product discounts after tax.
2294
		 * @deprecated Coupons can not be applied after tax
2295
		 */
2296
		public function apply_product_discounts_after_tax( $values, $price ) {
0 ignored issues
show
Unused Code introduced by
The parameter $values is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $price is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2297
			_deprecated_function( 'apply_product_discounts_after_tax', '2.3' );
2298
		}
2299
2300
		/**
2301
		 * Gets the order discount amount - these are applied after tax.
2302
		 * @deprecated Coupons can not be applied after tax
2303
		 */
2304
		public function get_discounts_after_tax() {
2305
			_deprecated_function( 'get_discounts_after_tax', '2.3' );
2306
		}
2307
2308
}
2309