Completed
Push — master ( 2d30e8...90137f )
by Mike
27:20
created

WC_Cart::set_cart_cookies()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
eloc 8
nc 3
nop 1
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
379
			// Result
380
			$return = true;
381
382
			// Check cart item validity
383
			$result = $this->check_cart_item_validity();
384
385
			if ( is_wp_error( $result ) ) {
386
				wc_add_notice( $result->get_error_message(), 'error' );
387
				$return = false;
388
			}
389
390
			// Check item stock
391
			$result = $this->check_cart_item_stock();
392
393
			if ( is_wp_error( $result ) ) {
394
				wc_add_notice( $result->get_error_message(), 'error' );
395
				$return = false;
396
			}
397
398
			return $return;
399
400
		}
401
402
		/**
403
		 * Check cart coupons for errors.
404
		 */
405
		public function check_cart_coupons() {
406
			foreach ( $this->applied_coupons as $code ) {
407
				$coupon = new WC_Coupon( $code );
408
409 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...
410
					// Error message
411
					$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_INVALID_REMOVED );
412
413
					// Remove the coupon
414
					$this->remove_coupon( $code );
415
416
					// Flag totals for refresh
417
					WC()->session->set( 'refresh_totals', true );
418
				}
419
			}
420
		}
421
422
		/**
423
		 * Get cart items quantities - merged so we can do accurate stock checks on items across multiple lines.
424
		 *
425
		 * @return array
426
		 */
427
		public function get_cart_item_quantities() {
428
			$quantities = array();
429
430
			foreach ( $this->get_cart() as $cart_item_key => $values ) {
431
				$_product = $values['data'];
432
433
				if ( $_product->is_type( 'variation' ) && true === $_product->managing_stock() ) {
434
					// Variation has stock levels defined so its handled individually
435
					$quantities[ $values['variation_id'] ] = isset( $quantities[ $values['variation_id'] ] ) ? $quantities[ $values['variation_id'] ] + $values['quantity'] : $values['quantity'];
436
				} else {
437
					$quantities[ $values['product_id'] ] = isset( $quantities[ $values['product_id'] ] ) ? $quantities[ $values['product_id'] ] + $values['quantity'] : $values['quantity'];
438
				}
439
			}
440
441
			return $quantities;
442
		}
443
444
		/**
445
		 * Looks through cart items and checks the posts are not trashed or deleted.
446
		 *
447
		 * @return bool|WP_Error
448
		 */
449
		public function check_cart_item_validity() {
450
			$return = true;
451
452
			foreach ( $this->get_cart() as $cart_item_key => $values ) {
453
				$_product = $values['data'];
454
455
				if ( ! $_product || ! $_product->exists() || 'trash' === $_product->post->post_status ) {
456
					$this->set_quantity( $cart_item_key, 0 );
457
					$return = new WP_Error( 'invalid', __( 'An item which is no longer available was removed from your cart.', 'woocommerce' ) );
458
				}
459
			}
460
461
			return $return;
462
		}
463
464
		/**
465
		 * Looks through the cart to check each item is in stock. If not, add an error.
466
		 *
467
		 * @return bool|WP_Error
468
		 */
469
		public function check_cart_item_stock() {
470
			global $wpdb;
471
472
			$error               = new WP_Error();
473
			$product_qty_in_cart = $this->get_cart_item_quantities();
474
475
			// First stock check loop
476
			foreach ( $this->get_cart() as $cart_item_key => $values ) {
477
				$_product = $values['data'];
478
479
				/**
480
				 * Check stock based on stock-status.
481
				 */
482
				if ( ! $_product->is_in_stock() ) {
483
					$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() ) );
484
					return $error;
485
				}
486
487
				if ( ! $_product->managing_stock() ) {
488
					continue;
489
				}
490
491
				$check_qty = $_product->is_type( 'variation' ) && true === $_product->managing_stock() ? $product_qty_in_cart[ $values['variation_id'] ] : $product_qty_in_cart[ $values['product_id'] ];
492
493
				/**
494
				 * Check stock based on all items in the cart.
495
				 */
496
				if ( ! $_product->has_enough_stock( $check_qty ) ) {
497
					$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() ) );
498
					return $error;
499
				}
500
501
				/**
502
				 * Finally consider any held stock, from pending orders.
503
				 */
504
				if ( get_option( 'woocommerce_hold_stock_minutes' ) > 0 && ! $_product->backorders_allowed() ) {
505
					$order_id   = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : 0;
506
					$held_stock = $wpdb->get_var(
507
						$wpdb->prepare( "
508
							SELECT SUM( order_item_meta.meta_value ) AS held_qty
509
							FROM {$wpdb->posts} AS posts
510
							LEFT JOIN {$wpdb->prefix}woocommerce_order_items as order_items ON posts.ID = order_items.order_id
511
							LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id
512
							LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta2 ON order_items.order_item_id = order_item_meta2.order_item_id
513
							WHERE 	order_item_meta.meta_key   = '_qty'
514
							AND 	order_item_meta2.meta_key  = %s AND order_item_meta2.meta_value  = %d
515
							AND 	posts.post_type            IN ( '" . implode( "','", wc_get_order_types() ) . "' )
516
							AND 	posts.post_status          = 'wc-pending'
517
							AND		posts.ID                   != %d;",
518
							$_product->is_type( 'variation' ) && true === $_product->managing_stock() ? '_variation_id' : '_product_id',
519
							$_product->is_type( 'variation' ) && true === $_product->managing_stock() ? $values['variation_id'] : $values['product_id'],
520
							$order_id
521
						)
522
					);
523
524
					$not_enough_stock = false;
525
526
					if ( $_product->is_type( 'variation' ) && 'parent' === $_product->managing_stock() && $_product->parent->get_stock_quantity() < ( $held_stock + $check_qty ) ) {
527
						$not_enough_stock = true;
528
					} elseif ( $_product->get_stock_quantity() < ( $held_stock + $check_qty ) ) {
529
						$not_enough_stock = true;
530
					}
531
					if ( $not_enough_stock ) {
532
						$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' ) ) );
533
						return $error;
534
					}
535
				}
536
			}
537
538
			return true;
539
		}
540
541
		/**
542
		 * Gets and formats a list of cart item data + variations for display on the frontend.
543
		 *
544
		 * @param array $cart_item
545
		 * @param bool $flat (default: false)
546
		 * @return string
547
		 */
548
		public function get_item_data( $cart_item, $flat = false ) {
549
			$item_data = array();
550
551
			// Variation data
552
			if ( ! empty( $cart_item['data']->variation_id ) && is_array( $cart_item['variation'] ) ) {
553
554
				foreach ( $cart_item['variation'] as $name => $value ) {
555
556
					if ( '' === $value )
557
						continue;
558
559
					$taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) );
560
561
					// If this is a term slug, get the term's nice name
562
					if ( taxonomy_exists( $taxonomy ) ) {
563
						$term = get_term_by( 'slug', $value, $taxonomy );
564
						if ( ! is_wp_error( $term ) && $term && $term->name ) {
565
							$value = $term->name;
566
						}
567
						$label = wc_attribute_label( $taxonomy );
568
569
					// If this is a custom option slug, get the options name
570
					} else {
571
						$value              = apply_filters( 'woocommerce_variation_option_name', $value );
572
						$product_attributes = $cart_item['data']->get_attributes();
573
						if ( isset( $product_attributes[ str_replace( 'attribute_', '', $name ) ] ) ) {
574
							$label = wc_attribute_label( $product_attributes[ str_replace( 'attribute_', '', $name ) ]['name'] );
575
						} else {
576
							$label = $name;
577
						}
578
					}
579
580
					$item_data[] = array(
581
						'key'   => $label,
582
						'value' => $value
583
					);
584
				}
585
			}
586
587
			// Filter item data to allow 3rd parties to add more to the array
588
			$item_data = apply_filters( 'woocommerce_get_item_data', $item_data, $cart_item );
589
590
			// Format item data ready to display
591
			foreach ( $item_data as $key => $data ) {
592
				// Set hidden to true to not display meta on cart.
593
				if ( ! empty( $data['hidden'] ) ) {
594
					unset( $item_data[ $key ] );
595
					continue;
596
				}
597
				$item_data[ $key ]['key']     = ! empty( $data['key'] ) ? $data['key'] : $data['name'];
598
				$item_data[ $key ]['display'] = ! empty( $data['display'] ) ? $data['display'] : $data['value'];
599
			}
600
601
			// Output flat or in list format
602
			if ( sizeof( $item_data ) > 0 ) {
603
				ob_start();
604
605
				if ( $flat ) {
606
					foreach ( $item_data as $data ) {
607
						echo esc_html( $data['key'] ) . ': ' . wp_kses_post( $data['display'] ) . "\n";
608
					}
609
				} else {
610
					wc_get_template( 'cart/cart-item-data.php', array( 'item_data' => $item_data ) );
611
				}
612
613
				return ob_get_clean();
614
			}
615
616
			return '';
617
		}
618
619
		/**
620
		 * Gets cross sells based on the items in the cart.
621
		 *
622
		 * @return array cross_sells (item ids)
623
		 */
624
		public function get_cross_sells() {
625
			$cross_sells = array();
626
			$in_cart = array();
627
			if ( ! $this->is_empty() ) {
628
				foreach ( $this->get_cart() as $cart_item_key => $values ) {
629
					if ( $values['quantity'] > 0 ) {
630
						$cross_sells = array_merge( $values['data']->get_cross_sells(), $cross_sells );
631
						$in_cart[] = $values['product_id'];
632
					}
633
				}
634
			}
635
			$cross_sells = array_diff( $cross_sells, $in_cart );
636
			return $cross_sells;
637
		}
638
639
		/**
640
		 * Gets the url to the cart page.
641
		 *
642
		 * @deprecated 2.5.0 in favor to wc_get_cart_url()
643
		 *
644
		 * @return string url to page
645
		 */
646
		public function get_cart_url() {
647
			return wc_get_cart_url();
648
		}
649
650
		/**
651
		 * Gets the url to the checkout page.
652
		 *
653
		 * @deprecated 2.5.0 in favor to wc_get_checkout_url()
654
		 *
655
		 * @return string url to page
656
		 */
657
		public function get_checkout_url() {
658
			return wc_get_checkout_url();
659
		}
660
661
		/**
662
		 * Gets the url to remove an item from the cart.
663
		 *
664
		 * @param string $cart_item_key contains the id of the cart item
665
		 * @return string url to page
666
		 */
667
		public function get_remove_url( $cart_item_key ) {
668
			$cart_page_url = wc_get_page_permalink( 'cart' );
669
			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' ) : '' );
670
		}
671
672
		/**
673
		 * Gets the url to re-add an item into the cart.
674
		 *
675
		 * @param  string $cart_item_key
676
		 * @return string url to page
677
		 */
678
		public function get_undo_url( $cart_item_key ) {
679
			$cart_page_url = wc_get_page_permalink( 'cart' );
680
681
			$query_args = array(
682
				'undo_item' => $cart_item_key,
683
			);
684
685
			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 );
686
		}
687
688
		/**
689
		 * Returns the contents of the cart in an array.
690
		 *
691
		 * @return array contents of the cart
692
		 */
693
		public function get_cart() {
694
			if ( ! did_action( 'wp_loaded' ) ) {
695
				_doing_it_wrong( __FUNCTION__, __( 'Get cart should not be called before the wp_loaded action.', 'woocommerce' ), '2.3' );
696
			}
697
			if ( ! did_action( 'woocommerce_cart_loaded_from_session' ) ) {
698
				$this->get_cart_from_session();
699
			}
700
			return array_filter( (array) $this->cart_contents );
701
		}
702
703
		/**
704
		 * Returns the contents of the cart in an array without the 'data' element.
705
		 *
706
		 * @return array contents of the cart
707
		 */
708
		public function get_cart_for_session() {
709
			$cart_session = array();
710
711
			if ( $this->get_cart() ) {
712
				foreach ( $this->get_cart() as $key => $values ) {
713
					$cart_session[ $key ] = $values;
714
					unset( $cart_session[ $key ]['data'] ); // Unset product object
715
				}
716
			}
717
718
			return $cart_session;
719
		}
720
721
		/**
722
		 * Returns a specific item in the cart.
723
		 *
724
		 * @param string $item_key Cart item key.
725
		 * @return array Item data
726
		 */
727
		public function get_cart_item( $item_key ) {
728
			if ( isset( $this->cart_contents[ $item_key ] ) ) {
729
				return $this->cart_contents[ $item_key ];
730
			}
731
732
			return array();
733
		}
734
735
		/**
736
		 * Returns the cart and shipping taxes, merged.
737
		 *
738
		 * @return array merged taxes
739
		 */
740
		public function get_taxes() {
741
			$taxes = array();
742
743
			// Merge
744
			foreach ( array_keys( $this->taxes + $this->shipping_taxes ) as $key ) {
745
				$taxes[ $key ] = ( isset( $this->shipping_taxes[ $key ] ) ? $this->shipping_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 );
746
			}
747
748
			return apply_filters( 'woocommerce_cart_get_taxes', $taxes, $this );
749
		}
750
751
		/**
752
		 * Get taxes, merged by code, formatted ready for output.
753
		 *
754
		 * @return array
755
		 */
756
		public function get_tax_totals() {
757
			$taxes      = $this->get_taxes();
758
			$tax_totals = array();
759
760
			foreach ( $taxes as $key => $tax ) {
761
				$code = WC_Tax::get_rate_code( $key );
762
763
				if ( $code || $key === apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) ) {
764 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...
765
						$tax_totals[ $code ] = new stdClass();
766
						$tax_totals[ $code ]->amount = 0;
767
					}
768
					$tax_totals[ $code ]->tax_rate_id       = $key;
769
					$tax_totals[ $code ]->is_compound       = WC_Tax::is_compound( $key );
770
					$tax_totals[ $code ]->label             = WC_Tax::get_rate_label( $key );
771
					$tax_totals[ $code ]->amount           += wc_round_tax_total( $tax );
772
					$tax_totals[ $code ]->formatted_amount  = wc_price( wc_round_tax_total( $tax_totals[ $code ]->amount ) );
773
				}
774
			}
775
776 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...
777
				$amounts    = array_filter( wp_list_pluck( $tax_totals, 'amount' ) );
778
				$tax_totals = array_intersect_key( $tax_totals, $amounts );
779
			}
780
781
			return apply_filters( 'woocommerce_cart_tax_totals', $tax_totals, $this );
782
		}
783
784
		/**
785
		 * Get all tax classes for items in the cart.
786
		 * @return array
787
		 */
788
		public function get_cart_item_tax_classes() {
789
			$found_tax_classes = array();
790
791
			foreach ( WC()->cart->get_cart() as $item ) {
792
				$found_tax_classes[] = $item['data']->get_tax_class();
793
			}
794
795
			return array_unique( $found_tax_classes );
796
		}
797
798
		/**
799
		 * Determines the value that the customer spent and the subtotal
800
		 * displayed, used for things like coupon validation.
801
		 *
802
		 * Since the coupon lines are displayed based on the TAX DISPLAY value
803
		 * of cart, this is used to determine the spend.
804
		 *
805
		 * If cart totals are shown including tax, use the subtotal.
806
		 * If cart totals are shown excluding tax, use the subtotal ex tax
807
		 * (tax is shown after coupons).
808
		 *
809
		 * @since 2.6.0
810
		 * @return string
811
		 */
812
		public function get_displayed_subtotal() {
813
			if ( 'incl' === $this->tax_display_cart ) {
814
				return wc_format_decimal( $this->subtotal );
815
			} elseif ( 'excl' === $this->tax_display_cart ) {
816
				return wc_format_decimal( $this->subtotal_ex_tax );
817
			}
818
		}
819
820
	/*-----------------------------------------------------------------------------------*/
821
	/* Add to cart handling */
822
	/*-----------------------------------------------------------------------------------*/
823
824
		/**
825
		 * Check if product is in the cart and return cart item key.
826
		 *
827
		 * Cart item key will be unique based on the item and its properties, such as variations.
828
		 *
829
		 * @param mixed id of product to find in the cart
830
		 * @return string cart item key
831
		 */
832
		public function find_product_in_cart( $cart_id = false ) {
833
			if ( $cart_id !== false ) {
834
				if ( is_array( $this->cart_contents ) && isset( $this->cart_contents[ $cart_id ] ) ) {
835
					return $cart_id;
836
				}
837
			}
838
			return '';
839
		}
840
841
		/**
842
		 * Generate a unique ID for the cart item being added.
843
		 *
844
		 * @param int $product_id - id of the product the key is being generated for
845
		 * @param int $variation_id of the product the key is being generated for
846
		 * @param array $variation data for the cart item
847
		 * @param array $cart_item_data other cart item data passed which affects this items uniqueness in the cart
848
		 * @return string cart item key
849
		 */
850
		public function generate_cart_id( $product_id, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
851
			$id_parts = array( $product_id );
852
853
			if ( $variation_id && 0 != $variation_id ) {
854
				$id_parts[] = $variation_id;
855
			}
856
857
			if ( is_array( $variation ) && ! empty( $variation ) ) {
858
				$variation_key = '';
859
				foreach ( $variation as $key => $value ) {
860
					$variation_key .= trim( $key ) . trim( $value );
861
				}
862
				$id_parts[] = $variation_key;
863
			}
864
865
			if ( is_array( $cart_item_data ) && ! empty( $cart_item_data ) ) {
866
				$cart_item_data_key = '';
867
				foreach ( $cart_item_data as $key => $value ) {
868
					if ( is_array( $value ) || is_object( $value ) ) {
869
						$value = http_build_query( $value );
870
					}
871
					$cart_item_data_key .= trim( $key ) . trim( $value );
872
873
				}
874
				$id_parts[] = $cart_item_data_key;
875
			}
876
877
			return apply_filters( 'woocommerce_cart_id', md5( implode( '_', $id_parts ) ), $product_id, $variation_id, $variation, $cart_item_data );
878
		}
879
880
		/**
881
		 * Add a product to the cart.
882
		 *
883
		 * @param int $product_id contains the id of the product to add to the cart
884
		 * @param int $quantity contains the quantity of the item to add
885
		 * @param int $variation_id
886
		 * @param array $variation attribute values
887
		 * @param array $cart_item_data extra cart item data we want to pass into the item
888
		 * @return string|bool $cart_item_key
889
		 */
890
		public function add_to_cart( $product_id = 0, $quantity = 1, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
891
			// Wrap in try catch so plugins can throw an exception to prevent adding to cart
892
			try {
893
				$product_id   = absint( $product_id );
894
				$variation_id = absint( $variation_id );
895
896
				// Ensure we don't add a variation to the cart directly by variation ID
897
				if ( 'product_variation' == get_post_type( $product_id ) ) {
898
					$variation_id = $product_id;
899
					$product_id   = wp_get_post_parent_id( $variation_id );
900
				}
901
902
				// Get the product
903
				$product_data = wc_get_product( $variation_id ? $variation_id : $product_id );
904
905
				// Sanity check
906
				if ( $quantity <= 0 || ! $product_data || 'trash' === $product_data->post->post_status  ) {
907
					throw new Exception();
908
				}
909
910
				// Load cart item data - may be added by other plugins
911
				$cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', $cart_item_data, $product_id, $variation_id );
912
913
				// Generate a ID based on product ID, variation ID, variation data, and other cart item data
914
				$cart_id        = $this->generate_cart_id( $product_id, $variation_id, $variation, $cart_item_data );
915
916
				// Find the cart item key in the existing cart
917
				$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...
918
919
				// Force quantity to 1 if sold individually and check for existing item in cart
920
				if ( $product_data->is_sold_individually() ) {
921
					$quantity         = apply_filters( 'woocommerce_add_to_cart_sold_individually_quantity', 1, $quantity, $product_id, $variation_id, $cart_item_data );
922
					$in_cart_quantity = $cart_item_key ? $this->cart_contents[ $cart_item_key ]['quantity'] : 0;
923
924
					if ( $in_cart_quantity > 0 ) {
925
						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() ) ) );
926
					}
927
				}
928
929
				// Check product is_purchasable
930
				if ( ! $product_data->is_purchasable() ) {
931
					throw new Exception( __( 'Sorry, this product cannot be purchased.', 'woocommerce' ) );
932
				}
933
934
				// Stock check - only check if we're managing stock and backorders are not allowed
935
				if ( ! $product_data->is_in_stock() ) {
936
					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() ) );
937
				}
938
939
				if ( ! $product_data->has_enough_stock( $quantity ) ) {
940
					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() ) );
941
				}
942
943
				// Stock check - this time accounting for whats already in-cart
944
				if ( $managing_stock = $product_data->managing_stock() ) {
945
					$products_qty_in_cart = $this->get_cart_item_quantities();
946
947
					if ( $product_data->is_type( 'variation' ) && true === $managing_stock ) {
948
						$check_qty = isset( $products_qty_in_cart[ $variation_id ] ) ? $products_qty_in_cart[ $variation_id ] : 0;
949
					} else {
950
						$check_qty = isset( $products_qty_in_cart[ $product_id ] ) ? $products_qty_in_cart[ $product_id ] : 0;
951
					}
952
953
					/**
954
					 * Check stock based on all items in the cart.
955
					 */
956
					if ( ! $product_data->has_enough_stock( $check_qty + $quantity ) ) {
957
						throw new Exception( sprintf(
958
							'<a href="%s" class="button wc-forward">%s</a> %s',
959
							wc_get_cart_url(),
960
							__( 'View Cart', 'woocommerce' ),
961
							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 )
962
						) );
963
					}
964
				}
965
966
				// If cart_item_key is set, the item is already in the cart
967
				if ( $cart_item_key ) {
968
					$new_quantity = $quantity + $this->cart_contents[ $cart_item_key ]['quantity'];
969
					$this->set_quantity( $cart_item_key, $new_quantity, false );
0 ignored issues
show
Bug introduced by
It seems like $cart_item_key defined by $this->find_product_in_cart($cart_id) on line 917 can also be of type boolean; however, WC_Cart::set_quantity() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
970
				} else {
971
					$cart_item_key = $cart_id;
972
973
					// Add item after merging with $cart_item_data - hook to allow plugins to modify cart item
974
					$this->cart_contents[ $cart_item_key ] = apply_filters( 'woocommerce_add_cart_item', array_merge( $cart_item_data, array(
975
						'product_id'	=> $product_id,
976
						'variation_id'	=> $variation_id,
977
						'variation' 	=> $variation,
978
						'quantity' 		=> $quantity,
979
						'data'			=> $product_data
980
					) ), $cart_item_key );
981
				}
982
983
				if ( did_action( 'wp' ) ) {
984
					$this->set_cart_cookies( ! $this->is_empty() );
985
				}
986
987
				do_action( 'woocommerce_add_to_cart', $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data );
988
989
				return $cart_item_key;
990
991
			} catch ( Exception $e ) {
992
				if ( $e->getMessage() ) {
993
					wc_add_notice( $e->getMessage(), 'error' );
994
				}
995
				return false;
996
			}
997
		}
998
999
		/**
1000
		 * Remove a cart item.
1001
		 *
1002
		 * @since  2.3.0
1003
		 * @param  string $cart_item_key
1004
		 * @return bool
1005
		 */
1006
		public function remove_cart_item( $cart_item_key ) {
1007
			if ( isset( $this->cart_contents[ $cart_item_key ] ) ) {
1008
				$this->removed_cart_contents[ $cart_item_key ] = $this->cart_contents[ $cart_item_key ];
1009
				unset( $this->removed_cart_contents[ $cart_item_key ]['data'] );
1010
1011
				do_action( 'woocommerce_remove_cart_item', $cart_item_key, $this );
1012
1013
				unset( $this->cart_contents[ $cart_item_key ] );
1014
1015
				do_action( 'woocommerce_cart_item_removed', $cart_item_key, $this );
1016
1017
				$this->calculate_totals();
1018
1019
				return true;
1020
			}
1021
1022
			return false;
1023
		}
1024
1025
		/**
1026
		 * Restore a cart item.
1027
		 *
1028
		 * @param  string $cart_item_key
1029
		 * @return bool
1030
		 */
1031
		public function restore_cart_item( $cart_item_key ) {
1032
			if ( isset( $this->removed_cart_contents[ $cart_item_key ] ) ) {
1033
				$this->cart_contents[ $cart_item_key ] = $this->removed_cart_contents[ $cart_item_key ];
1034
				$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'] );
1035
1036
				do_action( 'woocommerce_restore_cart_item', $cart_item_key, $this );
1037
1038
				unset( $this->removed_cart_contents[ $cart_item_key ] );
1039
1040
				do_action( 'woocommerce_cart_item_restored', $cart_item_key, $this );
1041
1042
				$this->calculate_totals();
1043
1044
				return true;
1045
			}
1046
1047
			return false;
1048
		}
1049
1050
		/**
1051
		 * Set the quantity for an item in the cart.
1052
		 *
1053
		 * @param string	$cart_item_key	contains the id of the cart item
1054
		 * @param int		$quantity		contains the quantity of the item
1055
		 * @param bool      $refresh_totals	whether or not to calculate totals after setting the new qty
1056
		 *
1057
		 * @return bool
1058
		 */
1059
		public function set_quantity( $cart_item_key, $quantity = 1, $refresh_totals = true ) {
1060
			if ( $quantity == 0 || $quantity < 0 ) {
1061
				do_action( 'woocommerce_before_cart_item_quantity_zero', $cart_item_key );
1062
				unset( $this->cart_contents[ $cart_item_key ] );
1063
			} else {
1064
				$old_quantity = $this->cart_contents[ $cart_item_key ]['quantity'];
1065
				$this->cart_contents[ $cart_item_key ]['quantity'] = $quantity;
1066
				do_action( 'woocommerce_after_cart_item_quantity_update', $cart_item_key, $quantity, $old_quantity );
1067
			}
1068
1069
			if ( $refresh_totals ) {
1070
				$this->calculate_totals();
1071
			}
1072
1073
			return true;
1074
		}
1075
1076
	/*-----------------------------------------------------------------------------------*/
1077
	/* Cart Calculation Functions */
1078
	/*-----------------------------------------------------------------------------------*/
1079
1080
		/**
1081
		 * Reset cart totals to the defaults. Useful before running calculations.
1082
		 *
1083
		 * @param  bool  	$unset_session If true, the session data will be forced unset.
1084
		 * @access private
1085
		 */
1086
		private function reset( $unset_session = false ) {
1087
			foreach ( $this->cart_session_data as $key => $default ) {
1088
				$this->$key = $default;
1089
				if ( $unset_session ) {
1090
					unset( WC()->session->$key );
1091
				}
1092
			}
1093
			do_action( 'woocommerce_cart_reset', $this, $unset_session );
1094
		}
1095
1096
		/**
1097
		 * Sort by subtotal.
1098
		 * @param  array $a
1099
		 * @param  array $b
1100
		 * @return int
1101
		 */
1102
		private function sort_by_subtotal( $a, $b ) {
1103
			$first_item_subtotal  = isset( $a['line_subtotal'] ) ? $a['line_subtotal'] : 0;
1104
			$second_item_subtotal = isset( $b['line_subtotal'] ) ? $b['line_subtotal'] : 0;
1105
			if ( $first_item_subtotal === $second_item_subtotal ) {
1106
				return 0;
1107
			}
1108
			return ( $first_item_subtotal < $second_item_subtotal ) ? 1 : -1;
1109
		}
1110
1111
		/**
1112
		 * Calculate totals for the items in the cart.
1113
		 */
1114
		public function calculate_totals() {
1115
			$this->reset();
1116
			$this->coupons = $this->get_coupons();
1117
1118
			do_action( 'woocommerce_before_calculate_totals', $this );
1119
1120
			if ( $this->is_empty() ) {
1121
				$this->set_session();
1122
				return;
1123
			}
1124
1125
			$tax_rates      = array();
1126
			$shop_tax_rates = array();
1127
			$cart           = $this->get_cart();
1128
1129
			/**
1130
			 * Calculate subtotals for items. This is done first so that discount logic can use the values.
1131
			 */
1132
			foreach ( $cart as $cart_item_key => $values ) {
1133
				$_product          = $values['data'];
1134
				$line_price        = $_product->get_price() * $values['quantity'];
1135
				$line_subtotal     = 0;
1136
				$line_subtotal_tax = 0;
1137
1138
				/**
1139
				 * No tax to calculate.
1140
				 */
1141
				if ( ! $_product->is_taxable() ) {
1142
1143
					// Subtotal is the undiscounted price
1144
					$this->subtotal += $line_price;
1145
					$this->subtotal_ex_tax += $line_price;
1146
1147
				/**
1148
				 * Prices include tax.
1149
				 *
1150
				 * To prevent rounding issues we need to work with the inclusive price where possible.
1151
				 * otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would.
1152
				 * be 8.325 leading to totals being 1p off.
1153
				 *
1154
				 * Pre tax coupons come off the price the customer thinks they are paying - tax is calculated.
1155
				 * afterwards.
1156
				 *
1157
				 * e.g. $100 bike with $10 coupon = customer pays $90 and tax worked backwards from that.
1158
				 */
1159
				} elseif ( $this->prices_include_tax ) {
1160
1161
					// Get base tax rates
1162
					if ( empty( $shop_tax_rates[ $_product->tax_class ] ) ) {
1163
						$shop_tax_rates[ $_product->tax_class ] = WC_Tax::get_base_tax_rates( $_product->tax_class );
1164
					}
1165
1166
					// Get item tax rates
1167
					if ( empty( $tax_rates[ $_product->get_tax_class() ] ) ) {
1168
						$tax_rates[ $_product->get_tax_class() ] = WC_Tax::get_rates( $_product->get_tax_class() );
1169
					}
1170
1171
					$base_tax_rates = $shop_tax_rates[ $_product->tax_class ];
1172
					$item_tax_rates = $tax_rates[ $_product->get_tax_class() ];
1173
1174
					/**
1175
					 * ADJUST TAX - Calculations when base tax is not equal to the item tax.
1176
					 *
1177
 					 * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
1178
 					 * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
1179
 					 * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
1180
 					 */
1181
					if ( $item_tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
1182
1183
						// Work out a new base price without the shop's base tax
1184
						$taxes                 = WC_Tax::calc_tax( $line_price, $base_tax_rates, true, true );
1185
1186
						// Now we have a new item price (excluding TAX)
1187
						$line_subtotal         = $line_price - array_sum( $taxes );
1188
1189
						// Now add modified taxes
1190
						$tax_result            = WC_Tax::calc_tax( $line_subtotal, $item_tax_rates );
1191
						$line_subtotal_tax     = array_sum( $tax_result );
1192
1193
					/**
1194
					 * Regular tax calculation (customer inside base and the tax class is unmodified.
1195
					 */
1196
					} else {
1197
1198
						// Calc tax normally
1199
						$taxes                 = WC_Tax::calc_tax( $line_price, $item_tax_rates, true );
1200
						$line_subtotal_tax     = array_sum( $taxes );
1201
						$line_subtotal         = $line_price - array_sum( $taxes );
1202
					}
1203
1204
				/**
1205
				 * Prices exclude tax.
1206
				 *
1207
				 * This calculation is simpler - work with the base, untaxed price.
1208
				 */
1209
				} else {
1210
1211
					// Get item tax rates
1212
					if ( empty( $tax_rates[ $_product->get_tax_class() ] ) ) {
1213
						$tax_rates[ $_product->get_tax_class() ] = WC_Tax::get_rates( $_product->get_tax_class() );
1214
					}
1215
1216
					$item_tax_rates        = $tax_rates[ $_product->get_tax_class() ];
1217
1218
					// Base tax for line before discount - we will store this in the order data
1219
					$taxes                 = WC_Tax::calc_tax( $line_price, $item_tax_rates );
1220
					$line_subtotal_tax     = array_sum( $taxes );
1221
1222
					$line_subtotal         = $line_price;
1223
				}
1224
1225
				// Add to main subtotal
1226
				$this->subtotal        += $line_subtotal + $line_subtotal_tax;
1227
				$this->subtotal_ex_tax += $line_subtotal;
1228
			}
1229
1230
			// Order cart items by price so coupon logic is 'fair' for customers and not based on order added to cart.
1231
			uasort( $cart, array( $this, 'sort_by_subtotal' ) );
1232
1233
			/**
1234
			 * Calculate totals for items.
1235
			 */
1236
			foreach ( $cart as $cart_item_key => $values ) {
1237
1238
				$_product = $values['data'];
1239
1240
				// Prices
1241
				$base_price = $_product->get_price();
1242
				$line_price = $_product->get_price() * $values['quantity'];
1243
1244
				// Tax data
1245
				$taxes = array();
1246
				$discounted_taxes = array();
1247
1248
				/**
1249
				 * No tax to calculate.
1250
				 */
1251
				if ( ! $_product->is_taxable() ) {
1252
1253
					// Discounted Price (price with any pre-tax discounts applied)
1254
					$discounted_price      = $this->get_discounted_price( $values, $base_price, true );
1255
					$line_subtotal_tax     = 0;
1256
					$line_subtotal         = $line_price;
1257
					$line_tax              = 0;
1258
					$line_total            = round( $discounted_price * $values['quantity'], wc_get_rounding_precision() );
1259
1260
				/**
1261
				 * Prices include tax.
1262
				 */
1263
				} elseif ( $this->prices_include_tax ) {
1264
1265
					$base_tax_rates = $shop_tax_rates[ $_product->tax_class ];
1266
					$item_tax_rates = $tax_rates[ $_product->get_tax_class() ];
1267
1268
					/**
1269
					 * ADJUST TAX - Calculations when base tax is not equal to the item tax.
1270
					 *
1271
 					 * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
1272
 					 * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
1273
 					 * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
1274
 					 */
1275
					if ( $item_tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
1276
1277
						// Work out a new base price without the shop's base tax
1278
						$taxes             = WC_Tax::calc_tax( $line_price, $base_tax_rates, true, true );
1279
1280
						// Now we have a new item price (excluding TAX)
1281
						$line_subtotal     = round( $line_price - array_sum( $taxes ), wc_get_rounding_precision() );
1282
						$taxes             = WC_Tax::calc_tax( $line_subtotal, $item_tax_rates );
1283
						$line_subtotal_tax = array_sum( $taxes );
1284
1285
						// Adjusted price (this is the price including the new tax rate)
1286
						$adjusted_price    = ( $line_subtotal + $line_subtotal_tax ) / $values['quantity'];
1287
1288
						// Apply discounts and get the discounted price FOR A SINGLE ITEM
1289
						$discounted_price  = $this->get_discounted_price( $values, $adjusted_price, true );
1290
1291
						// Convert back to line price
1292
						$discounted_line_price = $discounted_price * $values['quantity'];
1293
1294
						// Now use rounded line price to get taxes.
1295
						$discounted_taxes  = WC_Tax::calc_tax( $discounted_line_price, $item_tax_rates, true );
1296
						$line_tax          = array_sum( $discounted_taxes );
1297
						$line_total        = $discounted_line_price - $line_tax;
1298
1299
					/**
1300
					 * Regular tax calculation (customer inside base and the tax class is unmodified.
1301
					 */
1302
					} else {
1303
1304
						// Work out a new base price without the item tax
1305
						$taxes             = WC_Tax::calc_tax( $line_price, $item_tax_rates, true );
1306
1307
						// Now we have a new item price (excluding TAX)
1308
						$line_subtotal     = $line_price - array_sum( $taxes );
1309
						$line_subtotal_tax = array_sum( $taxes );
1310
1311
						// Calc prices and tax (discounted)
1312
						$discounted_price = $this->get_discounted_price( $values, $base_price, true );
1313
1314
						// Convert back to line price
1315
						$discounted_line_price = $discounted_price * $values['quantity'];
1316
1317
						// Now use rounded line price to get taxes.
1318
						$discounted_taxes  = WC_Tax::calc_tax( $discounted_line_price, $item_tax_rates, true );
1319
						$line_tax          = array_sum( $discounted_taxes );
1320
						$line_total        = $discounted_line_price - $line_tax;
1321
					}
1322
1323
					// Tax rows - merge the totals we just got
1324 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...
1325
						$this->taxes[ $key ] = ( isset( $discounted_taxes[ $key ] ) ? $discounted_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 );
1326
					}
1327
1328
				/**
1329
				 * Prices exclude tax.
1330
				 */
1331
				} else {
1332
1333
					$item_tax_rates        = $tax_rates[ $_product->get_tax_class() ];
1334
1335
					// Work out a new base price without the shop's base tax
1336
					$taxes                 = WC_Tax::calc_tax( $line_price, $item_tax_rates );
1337
1338
					// Now we have the item price (excluding TAX)
1339
					$line_subtotal         = $line_price;
1340
					$line_subtotal_tax     = array_sum( $taxes );
1341
1342
					// Now calc product rates
1343
					$discounted_price      = $this->get_discounted_price( $values, $base_price, true );
1344
					$discounted_taxes      = WC_Tax::calc_tax( $discounted_price * $values['quantity'], $item_tax_rates );
1345
					$discounted_tax_amount = array_sum( $discounted_taxes );
1346
					$line_tax              = $discounted_tax_amount;
1347
					$line_total            = $discounted_price * $values['quantity'];
1348
1349
					// Tax rows - merge the totals we just got
1350 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...
1351
						$this->taxes[ $key ] = ( isset( $discounted_taxes[ $key ] ) ? $discounted_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 );
1352
					}
1353
				}
1354
1355
				// Cart contents total is based on discounted prices and is used for the final total calculation
1356
				$this->cart_contents_total += $line_total;
1357
1358
				/**
1359
				 * Store costs + taxes for lines. For tax inclusive prices, we do some extra rounding logic so the stored
1360
				 * values "add up" when viewing the order in admin. This does have the disadvatage of not being able to
1361
				 * recalculate the tax total/subtotal accurately in the future, but it does ensure the data looks correct.
1362
				 *
1363
				 * Tax exclusive prices are not affected.
1364
				 */
1365
				if ( ! $_product->is_taxable() || $this->prices_include_tax ) {
1366
					$this->cart_contents[ $cart_item_key ]['line_total']        = round( $line_total + $line_tax - wc_round_tax_total( $line_tax ), $this->dp );
1367
					$this->cart_contents[ $cart_item_key ]['line_subtotal']     = round( $line_subtotal + $line_subtotal_tax - wc_round_tax_total( $line_subtotal_tax ), $this->dp );
1368
					$this->cart_contents[ $cart_item_key ]['line_tax']          = wc_round_tax_total( $line_tax );
1369
					$this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = wc_round_tax_total( $line_subtotal_tax );
1370
					$this->cart_contents[ $cart_item_key ]['line_tax_data']     = array( 'total' => array_map( 'wc_round_tax_total', $discounted_taxes ), 'subtotal' => array_map( 'wc_round_tax_total', $taxes ) );
1371
				} else {
1372
					$this->cart_contents[ $cart_item_key ]['line_total']        = $line_total;
1373
					$this->cart_contents[ $cart_item_key ]['line_subtotal']     = $line_subtotal;
1374
					$this->cart_contents[ $cart_item_key ]['line_tax']          = $line_tax;
1375
					$this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = $line_subtotal_tax;
1376
					$this->cart_contents[ $cart_item_key ]['line_tax_data']     = array( 'total' => $discounted_taxes, 'subtotal' => $taxes );
1377
				}
1378
			}
1379
1380
			// Only calculate the grand total + shipping if on the cart/checkout
1381
			if ( is_checkout() || is_cart() || defined('WOOCOMMERCE_CHECKOUT') || defined('WOOCOMMERCE_CART') ) {
1382
1383
				// Calculate the Shipping
1384
				$this->calculate_shipping();
1385
1386
				// Trigger the fees API where developers can add fees to the cart
1387
				$this->calculate_fees();
1388
1389
				// Total up/round taxes and shipping taxes
1390
				if ( $this->round_at_subtotal ) {
1391
					$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...
1392
					$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...
1393
					$this->taxes              = array_map( array( 'WC_Tax', 'round' ), $this->taxes );
1394
					$this->shipping_taxes     = array_map( array( 'WC_Tax', 'round' ), $this->shipping_taxes );
1395
				} else {
1396
					$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...
1397
					$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...
1398
				}
1399
1400
				// VAT exemption done at this point - so all totals are correct before exemption
1401
				if ( WC()->customer->get_is_vat_exempt() ) {
1402
					$this->remove_taxes();
1403
				}
1404
1405
				// Allow plugins to hook and alter totals before final total is calculated
1406
				do_action( 'woocommerce_calculate_totals', $this );
1407
1408
				// Grand Total - Discounted product prices, discounted tax, shipping cost + tax
1409
				$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 ) );
1410
1411
			} else {
1412
1413
				// Set tax total to sum of all tax rows
1414
				$this->tax_total = WC_Tax::get_tax_total( $this->taxes );
1415
1416
				// VAT exemption done at this point - so all totals are correct before exemption
1417
				if ( WC()->customer->get_is_vat_exempt() ) {
1418
					$this->remove_taxes();
1419
				}
1420
			}
1421
1422
			do_action( 'woocommerce_after_calculate_totals', $this );
1423
1424
			$this->set_session();
1425
		}
1426
1427
		/**
1428
		 * Remove taxes.
1429
		 */
1430
		public function remove_taxes() {
1431
			$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...
1432
			$this->subtotal           = $this->subtotal_ex_tax;
1433
1434
			foreach ( $this->cart_contents as $cart_item_key => $item ) {
1435
				$this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = $this->cart_contents[ $cart_item_key ]['line_tax'] = 0;
1436
				$this->cart_contents[ $cart_item_key ]['line_tax_data']     = array( 'total' => array(), 'subtotal' => array() );
1437
			}
1438
1439
			// If true, zero rate is applied so '0' tax is displayed on the frontend rather than nothing.
1440
			if ( apply_filters( 'woocommerce_cart_remove_taxes_apply_zero_rate', true ) ) {
1441
				$this->taxes = $this->shipping_taxes = array( apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) => 0 );
1442
			} else {
1443
				$this->taxes = $this->shipping_taxes = array();
1444
			}
1445
		}
1446
1447
		/**
1448
		 * Looks at the totals to see if payment is actually required.
1449
		 *
1450
		 * @return bool
1451
		 */
1452
		public function needs_payment() {
1453
			return apply_filters( 'woocommerce_cart_needs_payment', $this->total > 0, $this );
1454
		}
1455
1456
	/*-----------------------------------------------------------------------------------*/
1457
	/* Shipping related functions */
1458
	/*-----------------------------------------------------------------------------------*/
1459
1460
		/**
1461
		 * Uses the shipping class to calculate shipping then gets the totals when its finished.
1462
		 */
1463
		public function calculate_shipping() {
1464
			if ( $this->needs_shipping() && $this->show_shipping() ) {
1465
				WC()->shipping->calculate_shipping( $this->get_shipping_packages() );
1466
			} else {
1467
				WC()->shipping->reset_shipping();
1468
			}
1469
1470
			// Get totals for the chosen shipping method
1471
			$this->shipping_total 		= WC()->shipping->shipping_total;	// Shipping Total
1472
			$this->shipping_taxes		= WC()->shipping->shipping_taxes;	// Shipping Taxes
1473
		}
1474
1475
		/**
1476
		 * Get packages to calculate shipping for.
1477
		 *
1478
		 * This lets us calculate costs for carts that are shipped to multiple locations.
1479
		 *
1480
		 * Shipping methods are responsible for looping through these packages.
1481
		 *
1482
		 * By default we pass the cart itself as a package - plugins can change this.
1483
		 * through the filter and break it up.
1484
		 *
1485
		 * @since 1.5.4
1486
		 * @return array of cart items
1487
		 */
1488
		public function get_shipping_packages() {
1489
			// Packages array for storing 'carts'
1490
			$packages = array();
1491
1492
			$packages[0]['contents']                 = $this->get_cart();		// Items in the package
1493
			$packages[0]['contents_cost']            = 0;						// Cost of items in the package, set below
1494
			$packages[0]['applied_coupons']          = $this->applied_coupons;
1495
			$packages[0]['user']['ID']               = get_current_user_id();
1496
			$packages[0]['destination']['country']   = WC()->customer->get_shipping_country();
1497
			$packages[0]['destination']['state']     = WC()->customer->get_shipping_state();
1498
			$packages[0]['destination']['postcode']  = WC()->customer->get_shipping_postcode();
1499
			$packages[0]['destination']['city']      = WC()->customer->get_shipping_city();
1500
			$packages[0]['destination']['address']   = WC()->customer->get_shipping_address();
1501
			$packages[0]['destination']['address_2'] = WC()->customer->get_shipping_address_2();
1502
1503
			foreach ( $this->get_cart() as $item ) {
1504
				if ( $item['data']->needs_shipping() ) {
1505
					if ( isset( $item['line_total'] ) ) {
1506
						$packages[0]['contents_cost'] += $item['line_total'];
1507
					}
1508
				}
1509
			}
1510
1511
			return apply_filters( 'woocommerce_cart_shipping_packages', $packages );
1512
		}
1513
1514
		/**
1515
		 * Looks through the cart to see if shipping is actually required.
1516
		 *
1517
		 * @return bool whether or not the cart needs shipping
1518
		 */
1519
		public function needs_shipping() {
1520
			// If shipping is disabled or not yet configured, we can skip this.
1521
			if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( true ) ) {
1522
				return false;
1523
			}
1524
1525
			$needs_shipping = false;
1526
1527
			if ( ! empty( $this->cart_contents ) ) {
1528
				foreach ( $this->cart_contents as $cart_item_key => $values ) {
1529
					$_product = $values['data'];
1530
					if ( $_product->needs_shipping() ) {
1531
						$needs_shipping = true;
1532
					}
1533
				}
1534
			}
1535
1536
			return apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping );
1537
		}
1538
1539
		/**
1540
		 * Should the shipping address form be shown.
1541
		 *
1542
		 * @return bool
1543
		 */
1544
		public function needs_shipping_address() {
1545
1546
			$needs_shipping_address = false;
1547
1548
			if ( $this->needs_shipping() === true && ! wc_ship_to_billing_address_only() ) {
1549
				$needs_shipping_address = true;
1550
			}
1551
1552
			return apply_filters( 'woocommerce_cart_needs_shipping_address', $needs_shipping_address );
1553
		}
1554
1555
		/**
1556
		 * Sees if the customer has entered enough data to calc the shipping yet.
1557
		 *
1558
		 * @return bool
1559
		 */
1560
		public function show_shipping() {
1561
			if ( ! wc_shipping_enabled() || ! is_array( $this->cart_contents ) )
1562
				return false;
1563
1564
			if ( 'yes' === get_option( 'woocommerce_shipping_cost_requires_address' ) ) {
1565
				if ( ! WC()->customer->get_calculated_shipping() ) {
1566
					if ( ! WC()->customer->get_shipping_country() || ( ! WC()->customer->get_shipping_state() && ! WC()->customer->get_shipping_postcode() ) ) {
1567
						return false;
1568
					}
1569
				}
1570
			}
1571
1572
			return apply_filters( 'woocommerce_cart_ready_to_calc_shipping', true );
1573
		}
1574
1575
		/**
1576
		 * Sees if we need a shipping address.
1577
		 *
1578
		 * @deprecated 2.5.0 in favor to wc_ship_to_billing_address_only()
1579
		 *
1580
		 * @return bool
1581
		 */
1582
		public function ship_to_billing_address_only() {
1583
			return wc_ship_to_billing_address_only();
1584
		}
1585
1586
		/**
1587
		 * Gets the shipping total (after calculation).
1588
		 *
1589
		 * @return string price or string for the shipping total
1590
		 */
1591
		public function get_cart_shipping_total() {
1592
			if ( isset( $this->shipping_total ) ) {
1593
				if ( $this->shipping_total > 0 ) {
1594
1595
					// Display varies depending on settings
1596
					if ( $this->tax_display_cart == 'excl' ) {
1597
1598
						$return = wc_price( $this->shipping_total );
1599
1600
						if ( $this->shipping_tax_total > 0 && $this->prices_include_tax ) {
1601
							$return .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
1602
						}
1603
1604
						return $return;
1605
1606
					} else {
1607
1608
						$return = wc_price( $this->shipping_total + $this->shipping_tax_total );
1609
1610
						if ( $this->shipping_tax_total > 0 && ! $this->prices_include_tax ) {
1611
							$return .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
1612
						}
1613
1614
						return $return;
1615
1616
					}
1617
1618
				} else {
1619
					return __( 'Free!', 'woocommerce' );
1620
				}
1621
			}
1622
1623
			return '';
1624
		}
1625
1626
	/*-----------------------------------------------------------------------------------*/
1627
	/* Coupons/Discount related functions */
1628
	/*-----------------------------------------------------------------------------------*/
1629
1630
		/**
1631
		 * Check for user coupons (now that we have billing email). If a coupon is invalid, add an error.
1632
		 *
1633
		 * Checks two types of coupons:
1634
		 *  1. Where a list of customer emails are set (limits coupon usage to those defined).
1635
		 *  2. Where a usage_limit_per_user is set (limits coupon usage to a number based on user ID and email).
1636
		 *
1637
		 * @param array $posted
1638
		 */
1639
		public function check_customer_coupons( $posted ) {
1640
			if ( ! empty( $this->applied_coupons ) ) {
1641
				foreach ( $this->applied_coupons as $code ) {
1642
					$coupon = new WC_Coupon( $code );
1643
1644
					if ( $coupon->is_valid() ) {
1645
1646
						// Limit to defined email addresses
1647
						if ( is_array( $coupon->get_email_restrictions() ) && sizeof( $coupon->get_email_restrictions() ) > 0 ) {
1648
							$check_emails           = array();
1649
							if ( is_user_logged_in() ) {
1650
								$current_user   = wp_get_current_user();
1651
								$check_emails[] = $current_user->user_email;
1652
							}
1653
							$check_emails[] = $posted['billing_email'];
1654
							$check_emails   = array_map( 'sanitize_email', array_map( 'strtolower', $check_emails ) );
1655
1656
							if ( 0 == sizeof( array_intersect( $check_emails, $coupon->get_email_restrictions() ) ) ) {
1657
								$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
1658
1659
								// Remove the coupon
1660
								$this->remove_coupon( $code );
1661
1662
								// Flag totals for refresh
1663
								WC()->session->set( 'refresh_totals', true );
1664
							}
1665
						}
1666
1667
						// Usage limits per user - check against billing and user email and user ID
1668
						if ( $coupon->get_usage_limit_per_user() > 0 ) {
1669
							$check_emails = array();
1670
							$used_by      = $coupon->get_used_by();
1671
1672
							if ( is_user_logged_in() ) {
1673
								$current_user   = wp_get_current_user();
1674
								$check_emails[] = sanitize_email( $current_user->user_email );
1675
								$usage_count    = sizeof( array_keys( $used_by, get_current_user_id() ) );
1676
							} else {
1677
								$check_emails[] = sanitize_email( $posted['billing_email'] );
1678
								$user           = get_user_by( 'email', $posted['billing_email'] );
1679
								if ( $user ) {
1680
									$usage_count = sizeof( array_keys( $used_by, $user->ID ) );
1681
								} else {
1682
									$usage_count = 0;
1683
								}
1684
							}
1685
1686
							foreach ( $check_emails as $check_email ) {
1687
								$usage_count = $usage_count + sizeof( array_keys( $used_by, $check_email ) );
1688
							}
1689
1690 View Code Duplication
							if ( $usage_count >= $coupon->get_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...
1691
								$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED );
1692
1693
								// Remove the coupon
1694
								$this->remove_coupon( $code );
1695
1696
								// Flag totals for refresh
1697
								WC()->session->set( 'refresh_totals', true );
1698
							}
1699
						}
1700
					}
1701
				}
1702
			}
1703
		}
1704
1705
		/**
1706
		 * Returns whether or not a discount has been applied.
1707
		 * @param string $coupon_code
1708
		 * @return bool
1709
		 */
1710
		public function has_discount( $coupon_code = '' ) {
1711
			return $coupon_code ? in_array( apply_filters( 'woocommerce_coupon_code', $coupon_code ), $this->applied_coupons ) : sizeof( $this->applied_coupons ) > 0;
1712
		}
1713
1714
		/**
1715
		 * Applies a coupon code passed to the method.
1716
		 *
1717
		 * @param string $coupon_code - The code to apply
1718
		 * @return bool	True if the coupon is applied, false if it does not exist or cannot be applied
1719
		 */
1720
		public function add_discount( $coupon_code ) {
1721
			// Coupons are globally disabled
1722
			if ( ! wc_coupons_enabled() ) {
1723
				return false;
1724
			}
1725
1726
			// Sanitize coupon code
1727
			$coupon_code = apply_filters( 'woocommerce_coupon_code', $coupon_code );
1728
1729
			// Get the coupon
1730
			$the_coupon = new WC_Coupon( $coupon_code );
1731
1732
			// Check it can be used with cart
1733
			if ( ! $the_coupon->is_valid() ) {
1734
				wc_add_notice( $the_coupon->get_error_message(), 'error' );
1735
				return false;
1736
			}
1737
1738
			// Check if applied
1739
			if ( $this->has_discount( $coupon_code ) ) {
1740
				$the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED );
1741
				return false;
1742
			}
1743
1744
			// If its individual use then remove other coupons
1745
			if ( $the_coupon->get_individual_use() ) {
1746
				$this->applied_coupons = apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $the_coupon, $this->applied_coupons );
1747
			}
1748
1749
			if ( $this->applied_coupons ) {
1750
				foreach ( $this->applied_coupons as $code ) {
1751
					$coupon = new WC_Coupon( $code );
1752
1753
					if ( $coupon->get_individual_use() && false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $the_coupon, $coupon, $this->applied_coupons ) ) {
1754
1755
						// Reject new coupon
1756
						$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY );
1757
1758
						return false;
1759
					}
1760
				}
1761
			}
1762
1763
			$this->applied_coupons[] = $coupon_code;
1764
1765
			// Choose free shipping
1766
			if ( $the_coupon->get_free_shipping() ) {
1767
				$packages = WC()->shipping->get_packages();
1768
				$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
1769
1770
				foreach ( $packages as $i => $package ) {
1771
					$chosen_shipping_methods[ $i ] = 'free_shipping';
1772
				}
1773
1774
				WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
1775
			}
1776
1777
			$the_coupon->add_coupon_message( WC_Coupon::WC_COUPON_SUCCESS );
1778
1779
			do_action( 'woocommerce_applied_coupon', $coupon_code );
1780
1781
			return true;
1782
		}
1783
1784
		/**
1785
		 * Get array of applied coupon objects and codes.
1786
		 * @return array of applied coupons
1787
		 */
1788
		public function get_coupons( $deprecated = null ) {
1789
			$coupons = array();
1790
1791
			if ( 'order' === $deprecated ) {
1792
				return $coupons;
1793
			}
1794
1795
			foreach ( $this->get_applied_coupons() as $code ) {
1796
				$coupon = new WC_Coupon( $code );
1797
				$coupons[ $code ] = $coupon;
1798
			}
1799
1800
			return $coupons;
1801
		}
1802
1803
		/**
1804
		 * Gets the array of applied coupon codes.
1805
		 *
1806
		 * @return array of applied coupons
1807
		 */
1808
		public function get_applied_coupons() {
1809
			return $this->applied_coupons;
1810
		}
1811
1812
		/**
1813
		 * Get the discount amount for a used coupon.
1814
		 * @param  string $code coupon code
1815
		 * @param  bool $ex_tax inc or ex tax
1816
		 * @return float discount amount
1817
		 */
1818
		public function get_coupon_discount_amount( $code, $ex_tax = true ) {
1819
			$discount_amount = isset( $this->coupon_discount_amounts[ $code ] ) ? $this->coupon_discount_amounts[ $code ] : 0;
1820
1821
			if ( ! $ex_tax ) {
1822
				$discount_amount += $this->get_coupon_discount_tax_amount( $code );
1823
			}
1824
1825
			return wc_cart_round_discount( $discount_amount, $this->dp );
1826
		}
1827
1828
		/**
1829
		 * Get the discount tax amount for a used coupon (for tax inclusive prices).
1830
		 * @param  string $code coupon code
1831
		 * @param  bool inc or ex tax
1832
		 * @return float discount amount
1833
		 */
1834
		public function get_coupon_discount_tax_amount( $code ) {
1835
			return wc_cart_round_discount( isset( $this->coupon_discount_tax_amounts[ $code ] ) ? $this->coupon_discount_tax_amounts[ $code ] : 0, $this->dp );
1836
		}
1837
1838
		/**
1839
		 * Remove coupons from the cart of a defined type. Type 1 is before tax, type 2 is after tax.
1840
		 */
1841
		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...
1842
			$this->applied_coupons = $this->coupon_discount_amounts = $this->coupon_discount_tax_amounts = $this->coupon_applied_count = array();
1843
			WC()->session->set( 'applied_coupons', array() );
1844
			WC()->session->set( 'coupon_discount_amounts', array() );
1845
			WC()->session->set( 'coupon_discount_tax_amounts', array() );
1846
		}
1847
1848
		/**
1849
		 * Remove a single coupon by code.
1850
		 * @param  string $coupon_code Code of the coupon to remove
1851
		 * @return bool
1852
		 */
1853
		public function remove_coupon( $coupon_code ) {
1854
			// Coupons are globally disabled
1855
			if ( ! wc_coupons_enabled() ) {
1856
				return false;
1857
			}
1858
1859
			// Get the coupon
1860
			$coupon_code  = apply_filters( 'woocommerce_coupon_code', $coupon_code );
1861
			$position     = array_search( $coupon_code, $this->applied_coupons );
1862
1863
			if ( $position !== false ) {
1864
				unset( $this->applied_coupons[ $position ] );
1865
			}
1866
1867
			WC()->session->set( 'applied_coupons', $this->applied_coupons );
1868
1869
			do_action( 'woocommerce_removed_coupon', $coupon_code );
1870
1871
			return true;
1872
		}
1873
1874
		/**
1875
		 * Function to apply discounts to a product and get the discounted price (before tax is applied).
1876
		 *
1877
		 * @param mixed $values
1878
		 * @param mixed $price
1879
		 * @param bool $add_totals (default: false)
1880
		 * @return float price
1881
		 */
1882
		public function get_discounted_price( $values, $price, $add_totals = false ) {
1883
			if ( ! $price ) {
1884
				return $price;
1885
			}
1886
1887
			$undiscounted_price = $price;
1888
1889
			if ( ! empty( $this->coupons ) ) {
1890
				$product = $values['data'];
1891
1892
				foreach ( $this->coupons as $code => $coupon ) {
1893
					if ( $coupon->is_valid() && ( $coupon->is_valid_for_product( $product, $values ) || $coupon->is_valid_for_cart() ) ) {
1894
						$discount_amount = $coupon->get_discount_amount( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ? $price : $undiscounted_price, $values, true );
1895
						$discount_amount = min( $price, $discount_amount );
1896
						$price           = max( $price - $discount_amount, 0 );
1897
1898
						// Store the totals for DISPLAY in the cart
1899
						if ( $add_totals ) {
1900
							$total_discount     = $discount_amount * $values['quantity'];
1901
							$total_discount_tax = 0;
1902
1903
							if ( wc_tax_enabled() ) {
1904
								$tax_rates          = WC_Tax::get_rates( $product->get_tax_class() );
1905
								$taxes              = WC_Tax::calc_tax( $discount_amount, $tax_rates, $this->prices_include_tax );
1906
								$total_discount_tax = WC_Tax::get_tax_total( $taxes ) * $values['quantity'];
1907
								$total_discount     = $this->prices_include_tax ? $total_discount - $total_discount_tax : $total_discount;
1908
								$this->discount_cart_tax += $total_discount_tax;
1909
							}
1910
1911
							$this->discount_cart     += $total_discount;
1912
							$this->increase_coupon_discount_amount( $code, $total_discount, $total_discount_tax );
1913
							$this->increase_coupon_applied_count( $code, $values['quantity'] );
1914
						}
1915
					}
1916
1917
					// If the price is 0, we can stop going through coupons because there is nothing more to discount for this product.
1918
					if ( 0 >= $price ) {
1919
						break;
1920
					}
1921
				}
1922
			}
1923
1924
			return apply_filters( 'woocommerce_get_discounted_price', $price, $values, $this );
1925
		}
1926
1927
		/**
1928
		 * Store how much discount each coupon grants.
1929
		 *
1930
		 * @access private
1931
		 * @param string $code
1932
		 * @param double $amount
1933
		 * @param double $tax
1934
		 */
1935
		private function increase_coupon_discount_amount( $code, $amount, $tax ) {
1936
			$this->coupon_discount_amounts[ $code ]     = isset( $this->coupon_discount_amounts[ $code ] ) ? $this->coupon_discount_amounts[ $code ] + $amount : $amount;
1937
			$this->coupon_discount_tax_amounts[ $code ] = isset( $this->coupon_discount_tax_amounts[ $code ] ) ? $this->coupon_discount_tax_amounts[ $code ] + $tax : $tax;
1938
		}
1939
1940
		/**
1941
		 * Store how many times each coupon is applied to cart/items.
1942
		 *
1943
		 * @access private
1944
		 * @param string $code
1945
		 * @param int    $count
1946
		 */
1947
		private function increase_coupon_applied_count( $code, $count = 1 ) {
1948
			if ( empty( $this->coupon_applied_count[ $code ] ) ) {
1949
				$this->coupon_applied_count[ $code ] = 0;
1950
			}
1951
			$this->coupon_applied_count[ $code ] += $count;
1952
		}
1953
1954
	/*-----------------------------------------------------------------------------------*/
1955
	/* Fees API to add additional costs to orders */
1956
	/*-----------------------------------------------------------------------------------*/
1957
1958
		/**
1959
		 * Add additional fee to the cart.
1960
		 *
1961
		 * @param string $name Unique name for the fee. Multiple fees of the same name cannot be added.
1962
		 * @param float $amount Fee amount.
1963
		 * @param bool $taxable (default: false) Is the fee taxable?
1964
		 * @param string $tax_class (default: '') The tax class for the fee if taxable. A blank string is standard tax class.
1965
		 */
1966
		public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) {
1967
1968
			$new_fee_id = sanitize_title( $name );
1969
1970
			// Only add each fee once
1971
			foreach ( $this->fees as $fee ) {
1972
				if ( $fee->id == $new_fee_id ) {
1973
					return;
1974
				}
1975
			}
1976
1977
			$new_fee            = new stdClass();
1978
			$new_fee->id        = $new_fee_id;
1979
			$new_fee->name      = esc_attr( $name );
1980
			$new_fee->amount    = (float) esc_attr( $amount );
1981
			$new_fee->tax_class = $tax_class;
1982
			$new_fee->taxable   = $taxable ? true : false;
1983
			$new_fee->tax       = 0;
1984
			$new_fee->tax_data  = array();
1985
			$this->fees[]       = $new_fee;
1986
		}
1987
1988
		/**
1989
		 * Get fees.
1990
		 *
1991
		 * @return array
1992
		 */
1993
		public function get_fees() {
1994
			return array_filter( (array) $this->fees );
1995
		}
1996
1997
		/**
1998
		 * Calculate fees.
1999
		 */
2000
		public function calculate_fees() {
2001
			// Reset fees before calculation
2002
			$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...
2003
			$this->fees      = array();
2004
2005
			// Fire an action where developers can add their fees
2006
			do_action( 'woocommerce_cart_calculate_fees', $this );
2007
2008
			// If fees were added, total them and calculate tax
2009
			if ( ! empty( $this->fees ) ) {
2010
				foreach ( $this->fees as $fee_key => $fee ) {
2011
					$this->fee_total += $fee->amount;
2012
2013
					if ( $fee->taxable ) {
2014
						// Get tax rates
2015
						$tax_rates = WC_Tax::get_rates( $fee->tax_class );
2016
						$fee_taxes = WC_Tax::calc_tax( $fee->amount, $tax_rates, false );
2017
2018
						if ( ! empty( $fee_taxes ) ) {
2019
							// Set the tax total for this fee
2020
							$this->fees[ $fee_key ]->tax = array_sum( $fee_taxes );
2021
2022
							// Set tax data - Since 2.2
2023
							$this->fees[ $fee_key ]->tax_data = $fee_taxes;
2024
2025
							// Tax rows - merge the totals we just got
2026 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...
2027
								$this->taxes[ $key ] = ( isset( $fee_taxes[ $key ] ) ? $fee_taxes[ $key ] : 0 ) + ( isset( $this->taxes[ $key ] ) ? $this->taxes[ $key ] : 0 );
2028
							}
2029
						}
2030
					}
2031
				}
2032
			}
2033
		}
2034
2035
	/*-----------------------------------------------------------------------------------*/
2036
	/* Get Formatted Totals */
2037
	/*-----------------------------------------------------------------------------------*/
2038
2039
		/**
2040
		 * Gets the order total (after calculation).
2041
		 *
2042
		 * @return string formatted price
2043
		 */
2044
		public function get_total() {
2045
			return apply_filters( 'woocommerce_cart_total', wc_price( $this->total ) );
2046
		}
2047
2048
		/**
2049
		 * Gets the total excluding taxes.
2050
		 *
2051
		 * @return string formatted price
2052
		 */
2053
		public function get_total_ex_tax() {
2054
			$total = $this->total - $this->tax_total - $this->shipping_tax_total;
2055
			if ( $total < 0 ) {
2056
				$total = 0;
2057
			}
2058
			return apply_filters( 'woocommerce_cart_total_ex_tax', wc_price( $total ) );
2059
		}
2060
2061
		/**
2062
		 * Gets the cart contents total (after calculation).
2063
		 *
2064
		 * @return string formatted price
2065
		 */
2066
		public function get_cart_total() {
2067
			if ( ! $this->prices_include_tax ) {
2068
				$cart_contents_total = wc_price( $this->cart_contents_total );
2069
			} else {
2070
				$cart_contents_total = wc_price( $this->cart_contents_total + $this->tax_total );
2071
			}
2072
2073
			return apply_filters( 'woocommerce_cart_contents_total', $cart_contents_total );
2074
		}
2075
2076
		/**
2077
		 * Gets the sub total (after calculation).
2078
		 *
2079
		 * @param bool $compound whether to include compound taxes
2080
		 * @return string formatted price
2081
		 */
2082
		public function get_cart_subtotal( $compound = false ) {
2083
2084
			// If the cart has compound tax, we want to show the subtotal as
2085
			// cart + shipping + non-compound taxes (after discount)
2086
			if ( $compound ) {
2087
2088
				$cart_subtotal = wc_price( $this->cart_contents_total + $this->shipping_total + $this->get_taxes_total( false, false ) );
2089
2090
			// Otherwise we show cart items totals only (before discount)
2091
			} else {
2092
2093
				// Display varies depending on settings
2094
				if ( $this->tax_display_cart == 'excl' ) {
2095
2096
					$cart_subtotal = wc_price( $this->subtotal_ex_tax );
2097
2098
					if ( $this->tax_total > 0 && $this->prices_include_tax ) {
2099
						$cart_subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
2100
					}
2101
2102
				} else {
2103
2104
					$cart_subtotal = wc_price( $this->subtotal );
2105
2106
					if ( $this->tax_total > 0 && !$this->prices_include_tax ) {
2107
						$cart_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
2108
					}
2109
2110
				}
2111
			}
2112
2113
			return apply_filters( 'woocommerce_cart_subtotal', $cart_subtotal, $compound, $this );
2114
		}
2115
2116
		/**
2117
		 * Get the product row price per item.
2118
		 *
2119
		 * @param WC_Product $_product
2120
		 * @return string formatted price
2121
		 */
2122
		public function get_product_price( $_product ) {
2123
			if ( $this->tax_display_cart == 'excl' ) {
2124
				$product_price = $_product->get_price_excluding_tax();
2125
			} else {
2126
				$product_price = $_product->get_price_including_tax();
2127
			}
2128
2129
			return apply_filters( 'woocommerce_cart_product_price', wc_price( $product_price ), $_product );
2130
		}
2131
2132
		/**
2133
		 * Get the product row subtotal.
2134
		 *
2135
		 * Gets the tax etc to avoid rounding issues.
2136
		 *
2137
		 * When on the checkout (review order), this will get the subtotal based on the customer's tax rate rather than the base rate.
2138
		 *
2139
		 * @param WC_Product $_product
2140
		 * @param int $quantity
2141
		 * @return string formatted price
2142
		 */
2143
		public function get_product_subtotal( $_product, $quantity ) {
2144
2145
			$price 			= $_product->get_price();
2146
			$taxable 		= $_product->is_taxable();
2147
2148
			// Taxable
2149
			if ( $taxable ) {
2150
2151
				if ( $this->tax_display_cart == 'excl' ) {
2152
2153
					$row_price        = $_product->get_price_excluding_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->ex_tax_or_vat() . '</small>';
2158
					}
2159
2160
				} else {
2161
2162
					$row_price        = $_product->get_price_including_tax( $quantity );
2163
					$product_subtotal = wc_price( $row_price );
2164
2165
					if ( ! $this->prices_include_tax && $this->tax_total > 0 ) {
2166
						$product_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
2167
					}
2168
2169
				}
2170
2171
			// Non-taxable
2172
			} else {
2173
2174
				$row_price        = $price * $quantity;
2175
				$product_subtotal = wc_price( $row_price );
2176
2177
			}
2178
2179
			return apply_filters( 'woocommerce_cart_product_subtotal', $product_subtotal, $_product, $quantity, $this );
2180
		}
2181
2182
		/**
2183
		 * Gets the cart tax (after calculation).
2184
		 *
2185
		 * @return string formatted price
2186
		 */
2187
		public function get_cart_tax() {
2188
			$cart_total_tax = wc_round_tax_total( $this->tax_total + $this->shipping_tax_total );
2189
2190
			return apply_filters( 'woocommerce_get_cart_tax', $cart_total_tax ? wc_price( $cart_total_tax ) : '' );
2191
		}
2192
2193
		/**
2194
		 * Get a tax amount.
2195
		 * @param  string $tax_rate_id
2196
		 * @return float amount
2197
		 */
2198
		public function get_tax_amount( $tax_rate_id ) {
2199
			return isset( $this->taxes[ $tax_rate_id ] ) ? $this->taxes[ $tax_rate_id ] : 0;
2200
		}
2201
2202
		/**
2203
		 * Get a tax amount.
2204
		 * @param  string $tax_rate_id
2205
		 * @return float amount
2206
		 */
2207
		public function get_shipping_tax_amount( $tax_rate_id ) {
2208
			return isset( $this->shipping_taxes[ $tax_rate_id ] ) ? $this->shipping_taxes[ $tax_rate_id ] : 0;
2209
		}
2210
2211
		/**
2212
		 * Get tax row amounts with or without compound taxes includes.
2213
		 *
2214
		 * @param  bool $compound True if getting compound taxes
2215
		 * @param  bool $display  True if getting total to display
2216
		 * @return float price
2217
		 */
2218
		public function get_taxes_total( $compound = true, $display = true ) {
2219
			$total = 0;
2220 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...
2221
				if ( ! $compound && WC_Tax::is_compound( $key ) ) continue;
2222
				$total += $tax;
2223
			}
2224 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...
2225
				if ( ! $compound && WC_Tax::is_compound( $key ) ) continue;
2226
				$total += $tax;
2227
			}
2228
			if ( $display ) {
2229
				$total = wc_round_tax_total( $total );
2230
			}
2231
			return apply_filters( 'woocommerce_cart_taxes_total', $total, $compound, $display, $this );
2232
		}
2233
2234
		/**
2235
		 * Get the total of all cart discounts.
2236
		 *
2237
		 * @return float
2238
		 */
2239
		public function get_cart_discount_total() {
2240
			return wc_cart_round_discount( $this->discount_cart, $this->dp );
2241
		}
2242
2243
		/**
2244
		 * Get the total of all cart tax discounts (used for discounts on tax inclusive prices).
2245
		 *
2246
		 * @return float
2247
		 */
2248
		public function get_cart_discount_tax_total() {
2249
			return wc_cart_round_discount( $this->discount_cart_tax, $this->dp );
2250
		}
2251
2252
		/**
2253
		 * Gets the total discount amount - both kinds.
2254
		 *
2255
		 * @return mixed formatted price or false if there are none
2256
		 */
2257
		public function get_total_discount() {
2258
			if ( $this->get_cart_discount_total() ) {
2259
				$total_discount = wc_price( $this->get_cart_discount_total() );
2260
			} else {
2261
				$total_discount = false;
2262
			}
2263
			return apply_filters( 'woocommerce_cart_total_discount', $total_discount, $this );
2264
		}
2265
2266
		/**
2267
		 * Gets the total (product) discount amount - these are applied before tax.
2268
		 *
2269
		 * @deprecated Order discounts (after tax) removed in 2.3 so multiple methods for discounts are no longer required.
2270
		 * @return mixed formatted price or false if there are none
2271
		 */
2272
		public function get_discounts_before_tax() {
2273
			_deprecated_function( 'get_discounts_before_tax', '2.3', 'get_total_discount' );
2274
			if ( $this->get_cart_discount_total() ) {
2275
				$discounts_before_tax = wc_price( $this->get_cart_discount_total() );
2276
			} else {
2277
				$discounts_before_tax = false;
2278
			}
2279
			return apply_filters( 'woocommerce_cart_discounts_before_tax', $discounts_before_tax, $this );
2280
		}
2281
2282
		/**
2283
		 * Get the total of all order discounts (after tax discounts).
2284
		 *
2285
		 * @deprecated Order discounts (after tax) removed in 2.3
2286
		 * @return int
2287
		 */
2288
		public function get_order_discount_total() {
2289
			_deprecated_function( 'get_order_discount_total', '2.3' );
2290
			return 0;
2291
		}
2292
2293
		/**
2294
		 * Function to apply cart discounts after tax.
2295
 		 * @deprecated Coupons can not be applied after tax
2296
		 */
2297
		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...
2298
			_deprecated_function( 'apply_cart_discounts_after_tax', '2.3' );
2299
		}
2300
2301
		/**
2302
		 * Function to apply product discounts after tax.
2303
		 * @deprecated Coupons can not be applied after tax
2304
		 */
2305
		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...
2306
			_deprecated_function( 'apply_product_discounts_after_tax', '2.3' );
2307
		}
2308
2309
		/**
2310
		 * Gets the order discount amount - these are applied after tax.
2311
		 * @deprecated Coupons can not be applied after tax
2312
		 */
2313
		public function get_discounts_after_tax() {
2314
			_deprecated_function( 'get_discounts_after_tax', '2.3' );
2315
		}
2316
}
2317