Completed
Pull Request — master (#11889)
by Mike
19:28
created

WC_Cart::calculate_totals()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 0
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * @todo
8
 *
9
 * - Handle checkout
10
 * - Item types
11
 * - Coupon totals
12
 * - Orers same system
13
 * - Apply backend coupons
14
 * - Unit tests
15
 */
16
include_once( WC_ABSPATH . 'includes/class-wc-cart-coupons.php' );
17
include_once( WC_ABSPATH . 'includes/class-wc-cart-fees.php' );
18
include_once( WC_ABSPATH . 'includes/class-wc-cart-item.php' );
19
include_once( WC_ABSPATH . 'includes/class-wc-cart-items.php' );
20
include_once( WC_ABSPATH . 'includes/class-wc-cart-session.php' );
21
include_once( WC_ABSPATH . 'includes/class-wc-cart-totals.php' );
22
23
/**
24
 * Main cart class.
25
 *
26
 * @version		2.7.0
27
 * @package		WooCommerce/Classes
28
 * @category	Class
29
 * @author 		WooThemes
30
 */
31
class WC_Cart extends WC_Cart_Session {
32
33
	/**
34
	 * Cart totals class.
35
	 * @var WC_Cart_Totals
36
	 */
37
	protected $totals;
38
39
	/**
40
	 * This stores the chosen shipping methods for the cart item packages.
41
	 * @var array
42
	 */
43
	protected $shipping_methods;
44
45
	/**
46
	 * Constructor for the cart class.
47
	 */
48
	public function __construct() {
49
		parent::__construct();
50
51
		$this->totals = new WC_Cart_Totals;
52
53
		add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 );
54
		add_action( 'woocommerce_applied_coupon', array( $this, 'calculate_totals' ), 20, 0 );
55
		add_action( 'woocommerce_cart_item_removed', array( $this, 'calculate_totals' ), 20, 0 );
56
		add_action( 'woocommerce_cart_item_restored', array( $this, 'calculate_totals' ), 20, 0 );
57
	}
58
59
	/**
60
	 * Add an item to the cart.
61
	 */
62
	public function add_item( $args ) {
63
		$item_key   = $this->items->generate_key( $args );
64
		$cart_items = $this->items->get_items();
65
66
		if ( $item = $this->items->get_item_by_key( $item_key ) ) {
67
			$item->set_quantity( $item->get_quantity() + $args['quantity']  );
68
		} else {
69
			$item                    = new WC_Item_Product( $args );
70
			$cart_items[ $item_key ] = apply_filters( 'woocommerce_add_cart_item', $item, $item_key );
71
		}
72
73
		$this->items->set_items( $cart_items );
74
75
		return $item_key;
76
	}
77
78
	/**
79
	 * Add additional fee to the cart.
80
	 *
81
	 * @param string $name Unique name for the fee. Multiple fees of the same name cannot be added.
82
	 * @param float $amount Fee amount.
83
	 * @param bool $taxable (default: false) Is the fee taxable?
84
	 * @param string $tax_class (default: '') The tax class for the fee if taxable. A blank string is standard tax class.
85
	 */
86
	public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) {
87
		$fee_key = $this->fees->generate_key( $name );
88
		$fees    = $this->fees->get_fees();
89
90
		// Only add each fee once
91
		if ( isset( $fees[ $fee_key ] ) ) {
92
			return;
93
		}
94
95
		$fees[ $fee_key ] = (object) array(
96
			'id'        => $fee_key,
97
			'name'      => wc_clean( $name ),
98
			'amount'    => (float) $amount,
99
			'tax_class' => $tax_class,
100
			'taxable'   => (bool) $taxable,
101
		);
102
103
		$this->fees->set_fees( $fees );
104
105
		return $fee_key;
106
	}
107
108
	/**
109
	 * Applies a coupon code.
110
	 * @since 2.7.0
111
	 * @param string $coupon_code - The code to apply
112
	 * @return bool	True if the coupon is applied, false if it does not exist or cannot be applied
113
	 */
114
	public function add_coupon( $coupon_code ) {
115
		if ( ! wc_coupons_enabled() ) {
116
			return false;
117
		}
118
		try {
119
			$coupon  = new WC_Coupon( $coupon_code );
120
			$coupons = array_keys( $this->get_coupons() );
121
122
			if ( ! $coupon->is_valid() ) {
123
				throw new Exception( $coupon->get_error_message() );
124
			}
125
126
			if ( $this->has_coupon( $coupon_code ) ) {
127
				throw new Exception( '', WC_Coupon::E_WC_COUPON_ALREADY_APPLIED );
128
			}
129
130
			if ( $coupon->get_individual_use() ) {
131
				$coupons = apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $coupon, $coupons );
132
			}
133
134
			$coupons[] = $coupon_code;
135
136
			$this->coupons->set_coupons( $coupons );
137
138
			do_action( 'woocommerce_applied_coupon', $coupon_code, $coupon );
139
		} catch ( Exception $e ) {
140
			wc_add_notice( $e->getCode() ? WC_Coupon::get_coupon_error( $e->getCode() ) : $e->getMessage(), 'error' );
141
			return false;
142
		}
143
		return true;
144
	}
145
146
	/**
147
	 * Returns the contents of the cart in an array.
148
	 * @return array contents of the cart
149
	 */
150
	public function get_cart() {
151
		if ( ! did_action( 'wp_loaded' ) ) {
152
			_doing_it_wrong( __FUNCTION__, __( 'Get cart should not be called before the wp_loaded action.', 'woocommerce' ), '2.3' );
153
		}
154
		if ( ! did_action( 'woocommerce_cart_loaded_from_session' ) ) {
155
			$this->get_cart_from_session();
156
		}
157
		return $this->items->get_items();
158
	}
159
160
	/**
161
	 * Get array of fees.
162
	 * @return array of fees
163
	 */
164
	public function get_fees() {
165
		return $this->fees->get_fees();
166
	}
167
168
	/**
169
	 * Get array of applied coupon objects and codes.
170
	 * @return array of applied coupons
171
	 */
172
	public function get_coupons() {
173
		return $this->coupons->get_coupons();
174
	}
175
176
	/**
177
	* Checks if the cart is empty.
178
	* @return bool
179
	*/
180
	public function is_empty() {
181
		return ! sizeof( $this->get_cart() );
182
	}
183
184
	/**
185
	 * Empties the cart and optionally the persistent cart too.
186
	 */
187
	public function empty_cart() {
188
		$this->items->set_items( false );
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a array.

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...
189
		$this->fees->set_fees( false );
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a array<integer,object>.

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...
190
		$this->coupons->set_coupons( false );
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a array.

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...
191
		$this->shipping_methods = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $shipping_methods.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
192
		$this->totals = new WC_Cart_Totals;
193
		do_action( 'woocommerce_cart_emptied' );
194
	}
195
196
	/**
197
	 * Set the quantity for an item in the cart.
198
	 * @param string	$cart_item_key	contains the id of the cart item
0 ignored issues
show
Documentation introduced by
There is no parameter named $cart_item_key. Did you maybe mean $item_key?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
199
	 * @param int		$quantity		contains the quantity of the item
200
	 */
201
	public function set_quantity( $item_key, $quantity = 1, $deprecated = true ) {
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...
202
		if ( $quantity <= 0 ) {
203
			$this->items->remove_item( $item_key );
204
		} elseif ( $item = $this->items->get_item_by_key( $item_key ) ) {
205
			$old_quantity = $item->get_quantity();
206
			$item->set_quantity( $quantity );
207
			do_action( 'woocommerce_after_cart_item_quantity_update', $item_key, $quantity, $old_quantity );
208
		}
209
	}
210
211
	/**
212
	 * Remove item from cart.
213
	 * @param string $item_key Cart item key.
214
	 */
215
	public function remove_cart_item( $item_key ) {
216
		$this->items->remove_item( $item_key );
217
	}
218
219
	/**
220
	 * Remove a single coupon by code.
221
	 * @param  string $coupon_code Code of the coupon to remove
222
	 * @return bool
223
	 */
224
	public function remove_coupon( $coupon_code ) {
225
		return $this->coupons->remove_coupon( $coupon_code );
226
	}
227
228
	/**
229
	 * Remove all coupons.
230
	 */
231
	public function remove_coupons() {
232
		return $this->coupons->set_coupons( false );
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a array.

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...
233
	}
234
235
	/**
236
	 * Returns a specific item in the cart.
237
	 *
238
	 * @param string $item_key Cart item key.
239
	 * @return array Item data
240
	 */
241
	public function get_cart_item( $item_key ) {
242
		 return $this->items->get_item_by_key( $item_key );
243
	}
244
245
	/**
246
	 * Restore item from cart.
247
	 * @param string $item_key Cart item key.
248
	 */
249
	public function restore_cart_item( $item_key ) {
250
		$this->items->restore_item( $item_key );
251
	}
252
253
	/**
254
	 * Get all tax classes for items in the cart.
255
	 * @return array
256
	 */
257
	public function get_cart_item_tax_classes() {
258
		return $this->items->get_tax_classes();
259
	}
260
261
	/**
262
	 * Get weight of items in the cart.
263
	 * @return int
264
	 */
265
	public function get_cart_contents_weight() {
266
		return $this->items->get_weight();
267
	}
268
269
	/**
270
	 * Get number of items in the cart.
271
	 * @return int
272
	 */
273
	public function get_cart_contents_count() {
274
		return $this->items->get_quantity();
275
	}
276
277
	/**
278
	 * Returns whether or not a coupon has been applied.
279
	 * @param string $coupon_code
280
	 * @return bool
281
	 */
282
	public function has_coupon( $coupon_code = '' ) {
283
		$coupons = array_keys( $this->get_coupons() );
284
		return $coupon_code ? in_array( wc_format_coupon_code( $coupon_code ), $coupons ) : sizeof( $coupons );
285
	}
286
287
	/*
288
	|--------------------------------------------------------------------------
289
	| Cart shipping calculation.
290
	|--------------------------------------------------------------------------
291
	*/
292
293
	/**
294
	 * Looks through the cart to see if shipping is actually required.
295
	 * @return bool whether or not the cart needs shipping
296
	 */
297
	public function needs_shipping() {
298
		if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( true ) ) {
299
			return false;
300
		}
301
		$needs_shipping = false;
302
303
		foreach ( $this->get_cart() as $cart_item_key => $item ) {
304
			$product = $item->get_product();
305
			if ( $product && $product->needs_shipping() ) {
306
				$needs_shipping = true;
307
				break;
308
			}
309
		}
310
		return apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping );
311
	}
312
313
	/**
314
	 * Should the shipping address form be shown.
315
	 * @return bool
316
	 */
317
	public function needs_shipping_address() {
318
		return apply_filters( 'woocommerce_cart_needs_shipping_address', $this->needs_shipping() && ! wc_ship_to_billing_address_only() );
319
	}
320
321
	/**
322
	 * Get packages to calculate shipping for.
323
	 *
324
	 * This lets us calculate costs for carts that are shipped to multiple locations.
325
	 *
326
	 * Shipping methods are responsible for looping through these packages.
327
	 *
328
	 * By default we pass the cart itself as a package - plugins can change this.
329
	 * through the filter and break it up.
330
	 *
331
	 * @since 1.5.4
332
	 * @return array of cart items
333
	 */
334
	public function get_shipping_packages() {
335
		return apply_filters( 'woocommerce_cart_shipping_packages',
336
			array(
337
				array(
338
					'contents'        => $this->items->get_items_needing_shipping(),
339
					'contents_cost'   => array_sum( wc_list_pluck( $this->items->get_items_needing_shipping(), 'get_price' ) ),
340
					'applied_coupons' => array_keys( $this->get_coupons() ),
341
					'user'            => array(
342
						'ID' => get_current_user_id(),
343
					),
344
					'destination' => WC()->customer->get_shipping(),
345
				)
346
			)
347
		);
348
	}
349
350
	/**
351
	 * Uses the shipping class to calculate shipping then gets the totals when its finished.
352
	 */
353
	public function calculate_shipping() {
354
		return $this->shipping_methods = $this->needs_shipping() ? $this->get_chosen_shipping_methods( WC()->shipping->calculate_shipping( $this->get_shipping_packages() ) ) : array();
355
	}
356
357
	/**
358
	 * Given a set of packages with rates, get the chosen ones only.
359
	 * @since 2.7.0
360
	 * @param array $calculated_shipping_packages
361
	 * @return array
362
	 */
363
	protected function get_chosen_shipping_methods( $calculated_shipping_packages ) {
364
		$chosen_methods = array();
365
366
		// Get chosen methods for each package to get our totals.
367
		foreach ( $calculated_shipping_packages as $key => $package ) {
368
			$chosen_method          = wc_get_chosen_shipping_method_for_package( $key, $package );
369
			if ( $chosen_method ) {
370
				$chosen_methods[ $key ] = $package['rates'][ $chosen_method ];
371
			}
372
		}
373
374
		return $chosen_methods;
375
	}
376
377
	/*
378
	|--------------------------------------------------------------------------
379
	| Cart Totals.
380
	|--------------------------------------------------------------------------
381
	*/
382
383
	/**
384
	 * Calculate totals for the items in the cart.
385
	 */
386
	public function calculate_totals() {
387
		do_action( 'woocommerce_before_calculate_totals', $this );
388
389
		// Calculate line item totals
390
		$this->totals->set_coupons( $this->get_coupons() );
391
		$this->totals->set_items( $this->get_cart() );
392
		$this->totals->set_calculate_tax( ! WC()->customer->get_is_vat_exempt() );
393
		$this->totals->calculate_item_totals();
394
395
		// Calculate fees
396
		$this->totals->set_fees( $this->fees->calculate_fees() );
397
398
		// Calculate shipping costs
399
		$this->totals->set_shipping( $this->calculate_shipping() );
400
401
		// Calc grand totals
402
		$this->totals->calculate_totals();
403
404
		do_action( 'woocommerce_after_calculate_totals', $this );
405
	}
406
407
	/**
408
	 * Looks at the totals to see if payment is actually required.
409
	 *
410
	 * @return bool
411
	 */
412
	public function needs_payment() {
413
		return apply_filters( 'woocommerce_cart_needs_payment', $this->totals->get_total() > 0, $this );
414
	}
415
416
	/**
417
	 * Get item totals from totals class.
418
	 * @return array
419
	 */
420
	public function get_item_totals() {
421
		return $this->totals->get_item_totals();
422
	}
423
424
	/**
425
	 * Gets the order subtotal with or without tax.
426
	 * @param bool $including_tax
427
	 * @return string formatted price
428
	 */
429
	public function get_subtotal( $including_tax = false ) {
430
		return apply_filters( 'woocommerce_cart_subtotal', $this->totals->get_items_subtotal( $including_tax ), $including_tax );
431
	}
432
433
	/**
434
	 * Gets the order total (after calculation).
435
	 * @return string formatted price
436
	 */
437
	public function get_total() {
438
		return apply_filters( 'woocommerce_cart_total', $this->totals->get_total() );
439
	}
440
441
	/**
442
	 * Gets all taxes.
443
	 * @return array of taxes
444
	 */
445
	public function get_taxes() {
446
		return apply_filters( 'woocommerce_cart_get_taxes', $this->totals->get_taxes() );
447
	}
448
449
	/**
450
	 * Gets all taxes which will be output.
451
	 * @return array
452
	 */
453 View Code Duplication
	public function get_tax_totals() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
454
		$taxes = $this->get_taxes();
455
456
		if ( apply_filters( 'woocommerce_cart_hide_zero_taxes', true ) ) {
457
			$zero_amounts = array_filter( wp_list_pluck( $taxes, 'amount' ) );
458
			$taxes        = array_intersect_key( $taxes, $zero_amounts );
459
		}
460
461
		return apply_filters( 'woocommerce_cart_formatted_tax_totals', $taxes );
462
	}
463
464
	/**
465
	 * Get tax row amounts with or without compound taxes includes.
466
	 *
467
	 * @param  bool $compound True if getting compound taxes
468
	 * @param  bool $round  True if getting total to display
469
	 * @return float price
470
	 */
471
	public function get_taxes_total( $compound = true, $round = true ) {
472
		$total = 0;
473
		foreach ( $this->get_taxes() as $key => $tax ) {
474
			if ( ! $compound && $tax->get_compound() ) {
475
				continue;
476
			}
477
			$total += $tax;
478
		}
479
		return apply_filters( 'woocommerce_cart_taxes_total', $round ? wc_round_tax_total( $total ) : $total, $compound, $display, $this );
480
	}
481
482
	/**
483
	 * Get the total tax on the cart.
484
	 * @return float
485
	 */
486
	public function get_tax_total() {
487
		return $this->totals->get_tax_total();
488
	}
489
490
	/**
491
	 * Get the total tax on shipping.
492
	 * @return float
493
	 */
494
	public function get_shipping_tax_total() {
495
		return $this->totals->get_shipping_tax_total();
496
	}
497
498
	/**
499
	 * Get the total cost of shipping, with or without taxes included.
500
	 * @param  bool $including_tax
501
	 * @return float
502
	 */
503
	public function get_shipping_total( $including_tax = false ) {
504
		return $this->totals->get_shipping_total( $including_tax );
505
	}
506
507
	/**
508
	 * Return the total amount of discount granted by coupons.
509
	 * @return float
510
	 */
511
	public function get_cart_discount_total() {
512
		return $this->totals->get_discount_total();
513
	}
514
515
	/**
516
	 * Return the tax on the total amount of discount granted by coupons.
517
	 * @return float
518
	 */
519
	public function get_cart_discount_tax_total() {
520
		return $this->totals->get_discount_total_tax();
521
	}
522
523
	/**
524
	 * Get the total discount amount for a specific coupon code.
525
	 * @param string $code
526
	 * @param bool $ex_tax
527
	 * @return float
528
	 */
529
	public function get_coupon_discount_amount( $code, $ex_tax = true ) {
530
		$coupon_totals = $this->totals->get_coupons();
531
		$coupon_total  = isset( $coupon_totals[ $code ] ) ? $coupon_totals[ $code ] : array( 'total' => 0, 'total_tax' => 0 );
532
		$amount        = $coupon_total['total'];
533
534
		if ( ! $ex_tax ) {
535
			$amount += $coupon_total['total_tax'];
536
		}
537
538
		return wc_cart_round_discount( $amount, wc_get_price_decimals() );
539
	}
540
541
	/**
542
	 * Get the total discount tax amount for a specific coupon code.
543
	 * @param string $code
544
	 * @return float
545
	 */
546
	public function get_coupon_discount_tax_amount( $code ) {
547
		$coupon_totals = $this->totals->get_coupons();
548
		$coupon_total  = isset( $coupon_totals[ $code ] ) ? $coupon_totals[ $code ] : array( 'total' => 0, 'total_tax' => 0 );
549
		$amount        = $coupon_total['total_tax'];
550
551
		return wc_cart_round_discount( $amount, wc_get_price_decimals() );
552
	}
553
}
554