PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.1.0
WooCommerce v11.1.0
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / class-wc-cart.php
woocommerce / includes Last commit date
abstracts 4 days ago admin 4 days ago blocks 1 month ago cli 2 weeks ago customizer 6 months ago data-stores 4 days ago emails 2 weeks ago export 2 weeks ago gateways 2 weeks ago import 1 week ago integrations 1 month ago interfaces 6 months ago legacy 1 month ago libraries 2 weeks ago log-handlers 1 year ago payment-tokens 6 years ago product-usage 1 year ago queue 6 months ago react-admin 1 week ago rest-api 1 day ago shipping 5 months ago shortcodes 2 weeks ago theme-support 2 years ago tracks 2 weeks ago traits 5 years ago walkers 5 years ago wccom-site 2 months ago widgets 1 month ago class-wc-ajax.php 1 day ago class-wc-auth.php 2 years ago class-wc-autoloader.php 10 months ago class-wc-background-emailer.php 3 months ago class-wc-background-updater.php 6 years ago class-wc-brands-brand-settings-manager.php 1 year ago class-wc-brands-coupons.php 1 year ago class-wc-brands.php 2 weeks ago class-wc-breadcrumb.php 6 months ago class-wc-cache-helper.php 2 months ago class-wc-cart-fees.php 2 years ago class-wc-cart-session.php 2 weeks ago class-wc-cart-totals.php 1 year ago class-wc-cart.php 1 month ago class-wc-checkout.php 1 month ago class-wc-cli.php 2 weeks ago class-wc-comments.php 1 week ago class-wc-countries.php 3 months ago class-wc-coupon.php 2 weeks ago class-wc-customer-download-log.php 6 years ago class-wc-customer-download.php 2 years ago class-wc-customer.php 1 month ago class-wc-data-exception.php 8 years ago class-wc-data-store.php 3 years ago class-wc-datetime.php 4 years ago class-wc-deprecated-action-hooks.php 2 years ago class-wc-deprecated-filter-hooks.php 5 months ago class-wc-discounts.php 2 weeks ago class-wc-download-handler.php 2 weeks ago class-wc-emails.php 1 month ago class-wc-embed.php 3 weeks ago class-wc-form-handler.php 2 weeks ago class-wc-frontend-scripts.php 1 month ago class-wc-geo-ip.php 10 months ago class-wc-geolite-integration.php 6 years ago class-wc-geolocation.php 2 weeks ago class-wc-https.php 3 years ago class-wc-install.php 1 week ago class-wc-integrations.php 6 years ago class-wc-log-levels.php 2 years ago class-wc-logger.php 6 months ago class-wc-meta-data.php 2 months ago class-wc-order-factory.php 3 months ago class-wc-order-item-coupon.php 4 years ago class-wc-order-item-fee.php 2 weeks ago class-wc-order-item-meta.php 4 years ago class-wc-order-item-product.php 2 weeks ago class-wc-order-item-shipping.php 7 months ago class-wc-order-item-tax.php 2 months ago class-wc-order-item.php 1 month ago class-wc-order-query.php 2 months ago class-wc-order-refund.php 1 year ago class-wc-order.php 1 month ago class-wc-payment-gateways.php 2 weeks ago class-wc-payment-tokens.php 3 years ago class-wc-post-data.php 1 month ago class-wc-post-types.php 2 weeks ago class-wc-privacy-background-process.php 1 year ago class-wc-privacy-erasers.php 1 year ago class-wc-privacy-exporters.php 5 years ago class-wc-privacy.php 1 year ago class-wc-product-attribute.php 6 months ago class-wc-product-download.php 1 month ago class-wc-product-external.php 1 year ago class-wc-product-factory.php 5 months ago class-wc-product-grouped.php 1 month ago class-wc-product-query.php 6 months ago class-wc-product-simple.php 1 year ago class-wc-product-variable.php 1 week ago class-wc-product-variation.php 2 weeks ago class-wc-query.php 1 month ago class-wc-rate-limiter.php 4 years ago class-wc-regenerate-images-request.php 2 weeks ago class-wc-regenerate-images.php 2 weeks ago class-wc-register-wp-admin-settings.php 5 years ago class-wc-rest-authentication.php 1 day ago class-wc-rest-exception.php 5 years ago class-wc-session-handler.php 3 weeks ago class-wc-shipping-rate.php 1 year ago class-wc-shipping-zone.php 5 years ago class-wc-shipping-zones.php 2 weeks ago class-wc-shipping.php 1 month ago class-wc-shortcodes.php 2 weeks ago class-wc-structured-data.php 2 weeks ago class-wc-tax.php 1 month ago class-wc-template-loader.php 9 months ago class-wc-tracker.php 1 month ago class-wc-validation.php 1 month ago class-wc-webhook.php 2 weeks ago class-woocommerce.php 1 day ago wc-account-functions.php 2 months ago wc-attribute-functions.php 1 month ago wc-brands-functions.php 1 year ago wc-cart-functions.php 2 weeks ago wc-conditional-functions.php 2 weeks ago wc-core-functions.php 2 weeks ago wc-coupon-functions.php 7 months ago wc-deprecated-functions.php 2 weeks ago wc-formatting-functions.php 2 weeks ago wc-interactivity-api-functions.php 3 months ago wc-notice-functions.php 7 months ago wc-order-functions.php 2 weeks ago wc-order-item-functions.php 3 years ago wc-order-step-logger-functions.php 3 weeks ago wc-page-functions.php 1 month ago wc-product-functions.php 2 weeks ago wc-rest-functions.php 9 months ago wc-stock-functions.php 1 month ago wc-template-functions.php 1 week ago wc-template-hooks.php 1 year ago wc-term-functions.php 2 weeks ago wc-update-functions.php 1 week ago wc-user-functions.php 1 month ago wc-webhook-functions.php 2 months ago wc-widget-functions.php 6 years ago
class-wc-cart.php
2494 lines
1 <?php
2 /**
3 * WooCommerce cart
4 *
5 * The WooCommerce cart class stores cart data and active coupons as well as handling customer sessions and some cart related urls.
6 * The cart class also has a price calculation function which calls upon other classes to calculate totals.
7 *
8 * @package WooCommerce\Classes
9 * @version 2.1.0
10 */
11
12 use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
13 use Automattic\WooCommerce\Enums\ProductStatus;
14 use Automattic\WooCommerce\Enums\ProductType;
15 use Automattic\WooCommerce\Enums\TaxDisplayMode;
16 use Automattic\WooCommerce\Internal\Tax\TaxRateDataStore;
17 use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
18 use Automattic\WooCommerce\Utilities\DiscountsUtil;
19 use Automattic\WooCommerce\Utilities\NumberUtil;
20 use Automattic\WooCommerce\Utilities\ShippingUtil;
21
22 defined( 'ABSPATH' ) || exit;
23
24 require_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-cart.php';
25 require_once WC_ABSPATH . 'includes/class-wc-cart-fees.php';
26 require_once WC_ABSPATH . 'includes/class-wc-cart-session.php';
27
28 /**
29 * WC_Cart class.
30 */
31 class WC_Cart extends WC_Legacy_Cart {
32
33 /**
34 * Cart context, used to determine if the cart is being used in a StoreAPI or shortcode context. This should only
35 * be used internally.
36 *
37 * @var string shortcode|store-api
38 */
39 public $cart_context = 'shortcode';
40
41 /**
42 * Contains an array of cart items.
43 *
44 * @var array
45 */
46 public $cart_contents = array();
47
48 /**
49 * Contains an array of removed cart items so we can restore them if needed.
50 *
51 * @var array
52 */
53 public $removed_cart_contents = array();
54
55 /**
56 * Contains an array of coupon codes applied to the cart.
57 *
58 * @var array
59 */
60 public $applied_coupons = array();
61
62 /**
63 * This stores the chosen shipping methods for the cart item packages.
64 *
65 * @var array
66 */
67 protected $shipping_methods;
68
69 /**
70 * Whether the shipping totals have been calculated. This will only return true if shipping was calculated, not if
71 * shipping is disabled or if there are no cart contents.
72 *
73 * @var bool
74 */
75 protected $has_calculated_shipping = false;
76
77 /**
78 * Total defaults used to reset.
79 *
80 * @var array
81 */
82 protected $default_totals = array(
83 'subtotal' => 0,
84 'subtotal_tax' => 0,
85 'shipping_total' => 0,
86 'shipping_tax' => 0,
87 'shipping_taxes' => array(),
88 'discount_total' => 0,
89 'discount_tax' => 0,
90 'cart_contents_total' => 0,
91 'cart_contents_tax' => 0,
92 'cart_contents_taxes' => array(),
93 'fee_total' => 0,
94 'fee_tax' => 0,
95 'fee_taxes' => array(),
96 'total' => 0,
97 'total_tax' => 0,
98 );
99 /**
100 * Store calculated totals.
101 *
102 * @var array
103 */
104 protected $totals = array();
105
106 /**
107 * Reference to the cart session handling class.
108 *
109 * @var WC_Cart_Session
110 */
111 protected $session;
112
113 /**
114 * Reference to the cart fees API class.
115 *
116 * @var WC_Cart_Fees
117 */
118 protected $fees_api;
119
120 /**
121 * Constructor for the cart class. Loads options and hooks in the init method.
122 *
123 * @return void
124 */
125 public function __construct() {
126 $this->session = new WC_Cart_Session( $this );
127 $this->fees_api = new WC_Cart_Fees();
128
129 // Register hooks for the objects.
130 $this->session->init();
131
132 add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 );
133 add_action( 'woocommerce_applied_coupon', array( $this, 'calculate_totals' ), 20, 0 );
134 add_action( 'woocommerce_removed_coupon', array( $this, 'calculate_totals' ), 20, 0 );
135 add_action( 'woocommerce_cart_item_removed', array( $this, 'calculate_totals' ), 20, 0 );
136 add_action( 'woocommerce_cart_item_restored', array( $this, 'calculate_totals' ), 20, 0 );
137 add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ), 1 );
138 add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_coupons' ), 1 );
139 add_action( 'woocommerce_after_checkout_validation', array( $this, 'check_customer_coupons' ), 1, 2 );
140 }
141
142 /**
143 * When cloning, ensure object properties are handled.
144 *
145 * These properties store a reference to the cart, so we use new instead of clone.
146 *
147 * @return void
148 */
149 public function __clone() {
150 $this->session = clone $this->session;
151 $this->fees_api = clone $this->fees_api;
152
153 $this->session->set_cart( $this );
154 }
155
156 /*
157 |--------------------------------------------------------------------------
158 | Getters.
159 |--------------------------------------------------------------------------
160 |
161 | Methods to retrieve class properties and avoid direct access.
162 */
163
164 /**
165 * Gets cart contents.
166 *
167 * @since 3.2.0
168 * @return array of cart items
169 */
170 public function get_cart_contents() {
171 return apply_filters( 'woocommerce_get_cart_contents', (array) $this->cart_contents );
172 }
173
174 /**
175 * Return items removed from the cart.
176 *
177 * @since 3.2.0
178 * @return array
179 */
180 public function get_removed_cart_contents() {
181 return (array) $this->removed_cart_contents;
182 }
183
184 /**
185 * Gets the array of applied coupon codes.
186 *
187 * @return array of applied coupons
188 */
189 public function get_applied_coupons() {
190 return (array) $this->applied_coupons;
191 }
192
193 /**
194 * Return all calculated coupon totals.
195 *
196 * @since 3.2.0
197 * @return array
198 */
199 public function get_coupon_discount_totals() {
200 return (array) $this->coupon_discount_totals;
201 }
202 /**
203 * Return all calculated coupon tax totals.
204 *
205 * @since 3.2.0
206 * @return array
207 */
208 public function get_coupon_discount_tax_totals() {
209 return (array) $this->coupon_discount_tax_totals;
210 }
211
212 /**
213 * Return all calculated totals.
214 *
215 * @since 3.2.0
216 * @return array
217 */
218 public function get_totals() {
219 return empty( $this->totals ) ? $this->default_totals : $this->totals;
220 }
221
222 /**
223 * Get a total.
224 *
225 * @since 3.2.0
226 * @param string $key Key of element in $totals array.
227 * @return mixed
228 */
229 protected function get_totals_var( $key ) {
230 return isset( $this->totals[ $key ] ) ? $this->totals[ $key ] : $this->default_totals[ $key ];
231 }
232
233 /**
234 * Get subtotal.
235 *
236 * @since 3.2.0
237 * @return float
238 */
239 public function get_subtotal() {
240 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'subtotal' ) );
241 }
242
243 /**
244 * Get subtotal_tax.
245 *
246 * @since 3.2.0
247 * @return float
248 */
249 public function get_subtotal_tax() {
250 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'subtotal_tax' ) );
251 }
252
253 /**
254 * Get discount_total.
255 *
256 * @since 3.2.0
257 * @return float
258 */
259 public function get_discount_total() {
260 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'discount_total' ) );
261 }
262
263 /**
264 * Get discount_tax.
265 *
266 * @since 3.2.0
267 * @return float
268 */
269 public function get_discount_tax() {
270 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'discount_tax' ) );
271 }
272
273 /**
274 * Get shipping_total.
275 *
276 * @since 3.2.0
277 * @return float
278 */
279 public function get_shipping_total() {
280 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'shipping_total' ) );
281 }
282
283 /**
284 * Get shipping_tax.
285 *
286 * @since 3.2.0
287 * @return float
288 */
289 public function get_shipping_tax() {
290 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'shipping_tax' ) );
291 }
292
293 /**
294 * Gets cart total. This is the total of items in the cart, but after discounts. Subtotal is before discounts.
295 *
296 * @since 3.2.0
297 * @return float|string|int
298 */
299 public function get_cart_contents_total() {
300 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'cart_contents_total' ) );
301 }
302
303 /**
304 * Gets cart tax amount.
305 *
306 * @since 3.2.0
307 * @return float
308 */
309 public function get_cart_contents_tax() {
310 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'cart_contents_tax' ) );
311 }
312
313 /**
314 * Gets cart total after calculation.
315 *
316 * @since 3.2.0
317 * @param string $context If the context is view, the value will be formatted for display. This keeps it compatible with pre-3.2 versions.
318 * @return float|string
319 */
320 public function get_total( $context = 'view' ) {
321 $total = apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'total' ) );
322 return 'view' === $context ? apply_filters( 'woocommerce_cart_total', wc_price( $total ) ) : $total;
323 }
324
325 /**
326 * Get total tax amount.
327 *
328 * @since 3.2.0
329 * @return float
330 */
331 public function get_total_tax() {
332 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'total_tax' ) );
333 }
334
335 /**
336 * Get total fee amount.
337 *
338 * @since 3.2.0
339 * @return float
340 */
341 public function get_fee_total() {
342 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'fee_total' ) );
343 }
344
345 /**
346 * Get total fee tax amount.
347 *
348 * @since 3.2.0
349 * @return float
350 */
351 public function get_fee_tax() {
352 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'fee_tax' ) );
353 }
354
355 /**
356 * Get taxes.
357 *
358 * @since 3.2.0
359 * @return array
360 */
361 public function get_shipping_taxes() {
362 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'shipping_taxes' ) );
363 }
364
365 /**
366 * Get taxes.
367 *
368 * @since 3.2.0
369 * @return array
370 */
371 public function get_cart_contents_taxes() {
372 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'cart_contents_taxes' ) );
373 }
374
375 /**
376 * Get taxes.
377 *
378 * @since 3.2.0
379 * @return array
380 */
381 public function get_fee_taxes() {
382 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'fee_taxes' ) );
383 }
384
385 /**
386 * Return whether or not the cart is displaying prices including tax, rather than excluding tax.
387 *
388 * @since 3.3.0
389 * @return bool
390 */
391 public function display_prices_including_tax() {
392 /**
393 * Filter whether or not the cart is displaying prices including tax.
394 *
395 * @since 3.3.0
396 *
397 * @param bool $display_prices_including_tax Whether or not the cart is displaying prices including tax.
398 */
399 return apply_filters( 'woocommerce_cart_' . __FUNCTION__, TaxDisplayMode::INCLUSIVE === $this->get_tax_price_display_mode() );
400 }
401
402 /*
403 |--------------------------------------------------------------------------
404 | Setters.
405 |--------------------------------------------------------------------------
406 |
407 | Methods to set class properties and avoid direct access.
408 */
409
410 /**
411 * Sets the contents of the cart.
412 *
413 * @param array $value Cart array.
414 * @return void
415 */
416 public function set_cart_contents( $value ) {
417 $this->cart_contents = (array) $value;
418 }
419
420 /**
421 * Set items removed from the cart.
422 *
423 * @since 3.2.0
424 * @param array $value Item array.
425 * @return void
426 */
427 public function set_removed_cart_contents( $value = array() ) {
428 $this->removed_cart_contents = (array) $value;
429 }
430
431 /**
432 * Sets the array of applied coupon codes.
433 *
434 * @param array $value List of applied coupon codes.
435 * @return void
436 */
437 public function set_applied_coupons( $value = array() ) {
438 $this->applied_coupons = (array) $value;
439 }
440
441 /**
442 * Sets the array of calculated coupon totals.
443 *
444 * @since 3.2.0
445 * @param array $value Value to set.
446 * @return void
447 */
448 public function set_coupon_discount_totals( $value = array() ) {
449 $this->coupon_discount_totals = (array) $value;
450 }
451 /**
452 * Sets the array of calculated coupon tax totals.
453 *
454 * @since 3.2.0
455 * @param array $value Value to set.
456 * @return void
457 */
458 public function set_coupon_discount_tax_totals( $value = array() ) {
459 $this->coupon_discount_tax_totals = (array) $value;
460 }
461
462 /**
463 * Set all calculated totals.
464 *
465 * @since 3.2.0
466 * @param array $value Value to set.
467 * @return void
468 */
469 public function set_totals( $value = array() ) {
470 $this->totals = wp_parse_args( $value, $this->default_totals );
471 }
472
473 /**
474 * Set subtotal.
475 *
476 * @since 3.2.0
477 * @param string $value Value to set.
478 * @return void
479 */
480 public function set_subtotal( $value ) {
481 $this->totals['subtotal'] = wc_format_decimal( $value );
482 }
483
484 /**
485 * Set subtotal.
486 *
487 * @since 3.2.0
488 * @param string $value Value to set.
489 * @return void
490 */
491 public function set_subtotal_tax( $value ) {
492 $this->totals['subtotal_tax'] = $value;
493 }
494
495 /**
496 * Set discount_total.
497 *
498 * @since 3.2.0
499 * @param string $value Value to set.
500 * @return void
501 */
502 public function set_discount_total( $value ) {
503 $this->totals['discount_total'] = $value;
504 }
505
506 /**
507 * Set discount_tax.
508 *
509 * @since 3.2.0
510 * @param string $value Value to set.
511 * @return void
512 */
513 public function set_discount_tax( $value ) {
514 $this->totals['discount_tax'] = $value;
515 }
516
517 /**
518 * Set shipping_total.
519 *
520 * @since 3.2.0
521 * @param string $value Value to set.
522 * @return void
523 */
524 public function set_shipping_total( $value ) {
525 $this->totals['shipping_total'] = wc_format_decimal( $value );
526 }
527
528 /**
529 * Set shipping_tax.
530 *
531 * @since 3.2.0
532 * @param string $value Value to set.
533 * @return void
534 */
535 public function set_shipping_tax( $value ) {
536 $this->totals['shipping_tax'] = $value;
537 }
538
539 /**
540 * Set cart_contents_total.
541 *
542 * @since 3.2.0
543 * @param string $value Value to set.
544 * @return void
545 */
546 public function set_cart_contents_total( $value ) {
547 $this->totals['cart_contents_total'] = wc_format_decimal( $value );
548 }
549
550 /**
551 * Set cart tax amount.
552 *
553 * @since 3.2.0
554 * @param string $value Value to set.
555 * @return void
556 */
557 public function set_cart_contents_tax( $value ) {
558 $this->totals['cart_contents_tax'] = $value;
559 }
560
561 /**
562 * Set cart total.
563 *
564 * @since 3.2.0
565 * @param string $value Value to set.
566 * @return void
567 */
568 public function set_total( $value ) {
569 $this->totals['total'] = wc_format_decimal( $value, wc_get_price_decimals() );
570 }
571
572 /**
573 * Set total tax amount.
574 *
575 * @since 3.2.0
576 * @param string $value Value to set.
577 * @return void
578 */
579 public function set_total_tax( $value ) {
580 // We round here because this is a total entry, as opposed to line items in other setters.
581 $this->totals['total_tax'] = wc_round_tax_total( $value );
582 }
583
584 /**
585 * Set fee amount.
586 *
587 * @since 3.2.0
588 * @param string $value Value to set.
589 * @return void
590 */
591 public function set_fee_total( $value ) {
592 $this->totals['fee_total'] = wc_format_decimal( $value );
593 }
594
595 /**
596 * Set fee tax.
597 *
598 * @since 3.2.0
599 * @param string $value Value to set.
600 * @return void
601 */
602 public function set_fee_tax( $value ) {
603 $this->totals['fee_tax'] = $value;
604 }
605
606 /**
607 * Set taxes.
608 *
609 * @since 3.2.0
610 * @param array $value Tax values.
611 * @return void
612 */
613 public function set_shipping_taxes( $value ) {
614 $this->totals['shipping_taxes'] = (array) $value;
615 }
616
617 /**
618 * Set taxes.
619 *
620 * @since 3.2.0
621 * @param array $value Tax values.
622 * @return void
623 */
624 public function set_cart_contents_taxes( $value ) {
625 $this->totals['cart_contents_taxes'] = (array) $value;
626 }
627
628 /**
629 * Set taxes.
630 *
631 * @since 3.2.0
632 * @param array $value Tax values.
633 * @return void
634 */
635 public function set_fee_taxes( $value ) {
636 $this->totals['fee_taxes'] = (array) $value;
637 }
638
639 /*
640 |--------------------------------------------------------------------------
641 | Helper methods.
642 |--------------------------------------------------------------------------
643 */
644
645 /**
646 * Returns the cart and shipping taxes, merged.
647 *
648 * @return array merged taxes
649 */
650 public function get_taxes() {
651 return apply_filters( 'woocommerce_cart_get_taxes', wc_array_merge_recursive_numeric( $this->get_shipping_taxes(), $this->get_cart_contents_taxes(), $this->get_fee_taxes() ), $this );
652 }
653
654 /**
655 * Returns the contents of the cart in an array.
656 *
657 * @return array contents of the cart
658 */
659 public function get_cart() {
660 if ( ! did_action( 'wp_loaded' ) ) {
661 wc_doing_it_wrong( __FUNCTION__, __( 'Get cart should not be called before the wp_loaded action.', 'woocommerce' ), '2.3' );
662 }
663 if ( ! did_action( 'woocommerce_load_cart_from_session' ) ) {
664 $this->session->get_cart_from_session();
665 }
666 return array_filter( $this->get_cart_contents() );
667 }
668
669 /**
670 * Returns a specific item in the cart.
671 *
672 * @param string $item_key Cart item key.
673 * @return array Item data
674 */
675 public function get_cart_item( $item_key ) {
676 return isset( $this->cart_contents[ $item_key ] ) ? $this->cart_contents[ $item_key ] : array();
677 }
678
679 /**
680 * Checks if the cart is empty.
681 *
682 * @return bool
683 */
684 public function is_empty() {
685 return 0 === count( $this->get_cart() );
686 }
687
688 /**
689 * Empties the cart and optionally the persistent cart too.
690 *
691 * @since 9.7.0 Also clears shipping methods and packages since the items they are linked to are cleared.
692 *
693 * @param bool $clear_persistent_cart Should the persistent cart be cleared too. Defaults to true.
694 * @return void
695 */
696 public function empty_cart( $clear_persistent_cart = true ) {
697 /**
698 * Fires before the cart is emptied.
699 *
700 * @since 9.7.0
701 *
702 * @param bool $clear_persistent_cart Whether the persistent cart will be cleared too.
703 */
704 do_action( 'woocommerce_before_cart_emptied', $clear_persistent_cart );
705
706 $this->cart_contents = array();
707 $this->removed_cart_contents = array();
708 $this->shipping_methods = array();
709 $this->coupon_discount_totals = array();
710 $this->coupon_discount_tax_totals = array();
711 $this->applied_coupons = array();
712 $this->totals = $this->default_totals;
713
714 if ( $clear_persistent_cart ) {
715 $this->session->persistent_cart_destroy();
716 }
717
718 $this->fees_api->remove_all_fees();
719 WC()->shipping()->reset_shipping();
720
721 /**
722 * Fires after the cart is emptied.
723 *
724 * @since 9.7.0
725 *
726 * @param bool $clear_persistent_cart Whether the persistent cart was cleared too.
727 */
728 do_action( 'woocommerce_cart_emptied', $clear_persistent_cart );
729 }
730
731 /**
732 * Get number of items in the cart.
733 *
734 * @return int
735 */
736 public function get_cart_contents_count() {
737 return apply_filters( 'woocommerce_cart_contents_count', array_sum( wp_list_pluck( $this->get_cart(), 'quantity' ) ) );
738 }
739
740 /**
741 * Get weight of items in the cart.
742 *
743 * @since 2.5.0
744 * @return float
745 */
746 public function get_cart_contents_weight() {
747 $weight = 0.0;
748
749 foreach ( $this->get_cart() as $values ) {
750 if ( $values['data']->has_weight() ) {
751 $weight += (float) $values['data']->get_weight() * $values['quantity'];
752 }
753 }
754
755 return apply_filters( 'woocommerce_cart_contents_weight', $weight );
756 }
757
758 /**
759 * Get cart items quantities - merged so we can do accurate stock checks on items across multiple lines.
760 *
761 * @return array
762 */
763 public function get_cart_item_quantities() {
764 $quantities = array();
765
766 foreach ( $this->get_cart() as $values ) {
767 $managed_by_id = $values['data']->get_stock_managed_by_id();
768 $quantities[ $managed_by_id ] = $values['quantity'] + ( $quantities[ $managed_by_id ] ?? 0 );
769 }
770
771 return $quantities;
772 }
773
774 /**
775 * Check all cart items for errors.
776 */
777 public function check_cart_items() {
778 $return = true;
779 $result = $this->check_cart_item_validity();
780
781 if ( is_wp_error( $result ) ) {
782 wc_add_notice( $result->get_error_message(), 'error' );
783 $return = false;
784 }
785
786 $result = $this->check_cart_item_sold_individually();
787
788 if ( is_wp_error( $result ) ) {
789 foreach ( $result->get_error_messages() as $message ) {
790 wc_add_notice( $message, 'error' );
791 }
792 $return = false;
793 }
794
795 $result = $this->check_cart_item_stock();
796
797 if ( is_wp_error( $result ) ) {
798 wc_add_notice( $result->get_error_message(), 'error' );
799 $return = false;
800 }
801
802 return $return;
803 }
804
805 /**
806 * Check cart coupons for errors.
807 *
808 * @return void
809 */
810 public function check_cart_coupons() {
811 foreach ( $this->get_applied_coupons() as $code ) {
812 $coupon = new WC_Coupon( $code );
813
814 if ( ! $coupon->is_valid() ) {
815 $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_INVALID_REMOVED );
816 $this->remove_coupon( $code );
817 }
818 }
819 }
820
821 /**
822 * Looks through cart items and checks the posts are not trashed or deleted.
823 *
824 * @return bool|WP_Error
825 */
826 public function check_cart_item_validity() {
827 $return = true;
828
829 foreach ( $this->get_cart() as $cart_item_key => $values ) {
830 $product = $values['data'];
831
832 if ( ! $product || ! $product->exists() || ProductStatus::TRASH === $product->get_status() ) {
833 $this->set_quantity( $cart_item_key, 0 );
834 $return = new WP_Error( 'invalid', __( 'An item which is no longer available was removed from your cart.', 'woocommerce' ) );
835 }
836 }
837
838 return $return;
839 }
840
841 /**
842 * Looks through cart items and ensures sold individually products have quantity of 1.
843 *
844 * @since 10.7.0
845 * @return bool|WP_Error
846 */
847 public function check_cart_item_sold_individually() {
848 $errors = new WP_Error();
849
850 foreach ( $this->get_cart() as $cart_item_key => $values ) {
851 $product = $values['data'];
852
853 if ( ! $product || ! $product->exists() ) {
854 continue;
855 }
856
857 $product_id = $values['variation_id'] ? $values['variation_id'] : $values['product_id'];
858 $product_to_check = wc_get_product( $product_id );
859
860 if ( ! $product_to_check || ! $product_to_check->exists() ) {
861 continue;
862 }
863
864 if ( $product_to_check->is_sold_individually() && $values['quantity'] > 1 ) {
865 // Re-fetch and overwrite to reflect product changes made after item was added to cart.
866 $this->cart_contents[ $cart_item_key ]['data'] = $product_to_check;
867 $this->set_quantity( $cart_item_key, 1, false );
868 /* translators: %s: product name */
869 $errors->add( 'sold-individually', sprintf( __( 'You can only have 1 %s in your cart.', 'woocommerce' ), $product_to_check->get_name() ) );
870 }
871 }
872
873 return $errors->has_errors() ? $errors : true;
874 }
875
876 /**
877 * Looks through the cart to check each item is in stock. If not, add an error.
878 *
879 * @return bool|WP_Error
880 */
881 public function check_cart_item_stock() {
882 $error = new WP_Error();
883 $product_qty_in_cart = $this->get_cart_item_quantities();
884 $current_session_order_id = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : absint( WC()->session->get( 'store_api_draft_order', 0 ) );
885
886 foreach ( $this->get_cart() as $values ) {
887 $product = $values['data'];
888
889 // Check stock based on stock-status.
890 if ( ! $product->is_in_stock() ) {
891 /* translators: %s: product name */
892 $error->add( 'out-of-stock', sprintf( __( 'Sorry, "%s" is not in stock. Please edit your cart and try again. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ) );
893 return $error;
894 }
895
896 // We only need to check products managing stock, with a limited stock qty.
897 if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
898 continue;
899 }
900
901 // Check stock based on all items in the cart and consider any held stock within pending orders.
902 $held_stock = wc_get_held_stock_quantity( $product, $current_session_order_id );
903 $required_stock = $product_qty_in_cart[ $product->get_stock_managed_by_id() ];
904
905 /**
906 * Allows filter if product have enough stock to get added to the cart.
907 *
908 * @since 4.6.0
909 * @param bool $has_stock If have enough stock.
910 * @param WC_Product $product Product instance.
911 * @param array $values Cart item values.
912 */
913 if ( apply_filters( 'woocommerce_cart_item_required_stock_is_not_enough', $product->get_stock_quantity() < ( $held_stock + $required_stock ), $product, $values ) ) {
914 /* translators: 1: product name 2: quantity in stock */
915 $error->add( 'out-of-stock', sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ) );
916 return $error;
917 }
918 }
919
920 return true;
921 }
922
923 /**
924 * Gets and formats a list of cart item data + variations for display on the frontend.
925 *
926 * @param array $cart_item Cart item object.
927 * @param bool $flat Should the data be returned flat or in a list.
928 * @return string
929 */
930 public function get_item_data( $cart_item, $flat = false ) {
931 wc_deprecated_function( 'WC_Cart::get_item_data', '3.3', 'wc_get_formatted_cart_item_data' );
932
933 return wc_get_formatted_cart_item_data( $cart_item, $flat );
934 }
935
936 /**
937 * Gets cross sells based on the items in the cart.
938 *
939 * @return array cross_sells (item ids)
940 */
941 public function get_cross_sells() {
942 $cross_sells = array();
943 $in_cart = array();
944 if ( ! $this->is_empty() ) {
945 foreach ( $this->get_cart() as $values ) {
946 if ( $values['quantity'] > 0 ) {
947 $cross_sells = array_merge( $values['data']->get_cross_sell_ids(), $cross_sells );
948 $in_cart[] = $values['product_id'];
949
950 // Add variations to the in cart array.
951 if ( $values['data']->is_type( ProductType::VARIATION ) ) {
952 $in_cart[] = $values['variation_id'];
953 }
954 }
955 }
956 }
957 $cross_sells = array_diff( $cross_sells, $in_cart );
958 return apply_filters( 'woocommerce_cart_crosssell_ids', wp_parse_id_list( $cross_sells ), $this );
959 }
960
961 /**
962 * Gets the url to remove an item from the cart.
963 *
964 * @param string $cart_item_key contains the id of the cart item.
965 * @return string url to page
966 */
967 public function get_remove_url( $cart_item_key ) {
968 wc_deprecated_function( 'WC_Cart::get_remove_url', '3.3', 'wc_get_cart_remove_url' );
969
970 return wc_get_cart_remove_url( $cart_item_key );
971 }
972
973 /**
974 * Gets the url to re-add an item into the cart.
975 *
976 * @param string $cart_item_key Cart item key to undo.
977 * @return string url to page
978 */
979 public function get_undo_url( $cart_item_key ) {
980 wc_deprecated_function( 'WC_Cart::get_undo_url', '3.3', 'wc_get_cart_undo_url' );
981
982 return wc_get_cart_undo_url( $cart_item_key );
983 }
984
985 /**
986 * Get taxes, merged by code, formatted ready for output.
987 *
988 * @return array
989 */
990 public function get_tax_totals() {
991 $shipping_taxes = $this->get_shipping_taxes();
992 $taxes = $this->get_taxes();
993 $tax_rate_objects = wc_get_container()->get( TaxRateDataStore::class )->get_rate_objects_for_ids( array_keys( $taxes ) );
994 $tax_totals = array();
995
996 foreach ( $taxes as $key => $tax ) {
997 $tax_rate_object_or_id = $tax_rate_objects[ $key ] ?? $key;
998 $code = WC_Tax::get_rate_code( $tax_rate_object_or_id );
999
1000 if ( $code || apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) === $key ) {
1001 if ( ! isset( $tax_totals[ $code ] ) ) {
1002 $tax_totals[ $code ] = new stdClass();
1003 $tax_totals[ $code ]->amount = 0;
1004 }
1005
1006 $tax_totals[ $code ]->tax_rate_id = $key;
1007 $tax_totals[ $code ]->is_compound = WC_Tax::is_compound( $tax_rate_object_or_id );
1008 $tax_totals[ $code ]->label = WC_Tax::get_rate_label( $tax_rate_object_or_id );
1009
1010 // Shipping taxes are rounded differently, so we will subtract from all taxes, then round and then add them back.
1011 if ( isset( $shipping_taxes[ $key ] ) ) {
1012 $tax -= $shipping_taxes[ $key ];
1013 $tax = wc_round_tax_total( $tax );
1014 $tax += NumberUtil::round( $shipping_taxes[ $key ], wc_get_price_decimals() );
1015 unset( $shipping_taxes[ $key ] );
1016 }
1017 $tax_totals[ $code ]->amount += wc_round_tax_total( $tax );
1018 $tax_totals[ $code ]->formatted_amount = wc_price( $tax_totals[ $code ]->amount );
1019 }
1020 }
1021
1022 if ( apply_filters( 'woocommerce_cart_hide_zero_taxes', true ) ) {
1023 $amounts = array_filter( wp_list_pluck( $tax_totals, 'amount' ) );
1024 $tax_totals = array_intersect_key( $tax_totals, $amounts );
1025 }
1026
1027 return apply_filters( 'woocommerce_cart_tax_totals', $tax_totals, $this );
1028 }
1029
1030 /**
1031 * Get all tax classes for items in the cart.
1032 *
1033 * @return array
1034 */
1035 public function get_cart_item_tax_classes() {
1036 $found_tax_classes = array();
1037
1038 foreach ( $this->get_cart() as $item ) {
1039 if ( $item['data'] && ( $item['data']->is_taxable() || $item['data']->is_shipping_taxable() ) ) {
1040 $found_tax_classes[] = $item['data']->get_tax_class();
1041 }
1042 }
1043
1044 return array_unique( $found_tax_classes );
1045 }
1046
1047 /**
1048 * Get all tax classes for shipping based on the items in the cart.
1049 *
1050 * @return array
1051 */
1052 public function get_cart_item_tax_classes_for_shipping() {
1053 $found_tax_classes = array();
1054
1055 foreach ( $this->get_cart() as $item ) {
1056 if ( $item['data'] && ( $item['data']->is_shipping_taxable() ) ) {
1057 $found_tax_classes[] = $item['data']->get_tax_class();
1058 }
1059 }
1060
1061 return array_unique( $found_tax_classes );
1062 }
1063
1064 /**
1065 * Determines the value that the customer spent and the subtotal
1066 * displayed, used for things like coupon validation.
1067 *
1068 * Since the coupon lines are displayed based on the TAX DISPLAY value
1069 * of cart, this is used to determine the spend.
1070 *
1071 * If cart totals are shown including tax, use the subtotal.
1072 * If cart totals are shown excluding tax, use the subtotal ex tax
1073 * (tax is shown after coupons).
1074 *
1075 * @since 2.6.0
1076 * @return float
1077 */
1078 public function get_displayed_subtotal() {
1079 return $this->display_prices_including_tax() ? $this->get_subtotal() + $this->get_subtotal_tax() : $this->get_subtotal();
1080 }
1081
1082 /**
1083 * Check if product is in the cart and return cart item key.
1084 *
1085 * Cart item key will be unique based on the item and its properties, such as variations.
1086 *
1087 * @param mixed $cart_id id of product to find in the cart.
1088 * @return string cart item key
1089 */
1090 public function find_product_in_cart( $cart_id = false ) {
1091 if ( false !== $cart_id ) {
1092 if ( is_array( $this->cart_contents ) && isset( $this->cart_contents[ $cart_id ] ) ) {
1093 return $cart_id;
1094 }
1095 }
1096 return '';
1097 }
1098
1099 /**
1100 * Generate a unique ID for the cart item being added.
1101 *
1102 * @param int $product_id - id of the product the key is being generated for.
1103 * @param int $variation_id of the product the key is being generated for.
1104 * @param array $variation data for the cart item.
1105 * @param array $cart_item_data other cart item data passed which affects this items uniqueness in the cart.
1106 * @return string cart item key
1107 */
1108 public function generate_cart_id( $product_id, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
1109 $id_parts = array( $product_id );
1110
1111 if ( $variation_id && 0 !== $variation_id ) {
1112 $id_parts[] = $variation_id;
1113 }
1114
1115 if ( is_array( $variation ) && ! empty( $variation ) ) {
1116 $variation_key = '';
1117 foreach ( $variation as $key => $value ) {
1118 $variation_key .= trim( $key ) . trim( $value );
1119 }
1120 $id_parts[] = $variation_key;
1121 }
1122
1123 if ( is_array( $cart_item_data ) && ! empty( $cart_item_data ) ) {
1124 $cart_item_data_key = '';
1125 foreach ( $cart_item_data as $key => $value ) {
1126 if ( is_array( $value ) || is_object( $value ) ) {
1127 $value = http_build_query( $value );
1128 }
1129 $cart_item_data_key .= trim( $key ) . trim( $value );
1130
1131 }
1132 $id_parts[] = $cart_item_data_key;
1133 }
1134
1135 return apply_filters( 'woocommerce_cart_id', md5( implode( '_', $id_parts ) ), $product_id, $variation_id, $variation, $cart_item_data );
1136 }
1137
1138 /**
1139 * Add a product to the cart.
1140 *
1141 * @throws Exception Plugins can throw an exception to prevent adding to cart.
1142 * @param int $product_id contains the id of the product to add to the cart.
1143 * @param int $quantity contains the quantity of the item to add.
1144 * @param int $variation_id ID of the variation being added to the cart.
1145 * @param array $variation attribute values.
1146 * @param array $cart_item_data extra cart item data we want to pass into the item.
1147 * @return string|bool $cart_item_key
1148 */
1149 public function add_to_cart( $product_id = 0, $quantity = 1, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
1150 try {
1151 $product_id = absint( $product_id );
1152 $variation_id = absint( $variation_id );
1153
1154 // Ensure we don't add a variation to the cart directly by variation ID.
1155 if ( 'product_variation' === get_post_type( $product_id ) ) {
1156 $variation_id = $product_id;
1157
1158 // Guard against wp_get_post_parent_id returning false for invalid posts.
1159 $product_id = wp_get_post_parent_id( $variation_id );
1160 if ( false === $product_id ) {
1161 return false;
1162 }
1163 }
1164
1165 $product_data = wc_get_product( $variation_id ? $variation_id : $product_id );
1166
1167 /**
1168 * Filters the change the quantity to add to cart.
1169 *
1170 * @since 3.1.0
1171 * @since 11.0.0 Added the `$variation_id` parameter.
1172 * @param number $quantity The default quantity.
1173 * @param number $product_id The product id.
1174 * @param number $variation_id The variation ID.
1175 */
1176 $quantity = apply_filters( 'woocommerce_add_to_cart_quantity', $quantity, $product_id, $variation_id );
1177
1178 if ( $quantity <= 0 || ! $product_data || ProductStatus::TRASH === $product_data->get_status() ) {
1179 return false;
1180 }
1181
1182 // Variable product cannot be added to cart without a specified variation.
1183 if ( ! $variation_id && $product_data->is_type( ProductType::VARIABLE ) ) {
1184 /* translators: 1: product link, 2: product name */
1185 throw new Exception( sprintf( __( 'Please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', 'woocommerce' ), esc_url( $product_data->get_permalink() ), esc_html( $product_data->get_name() ) ) );
1186 }
1187
1188 if ( $product_data->is_type( ProductType::VARIATION ) ) {
1189 $missing_attributes = array();
1190 $parent_data = wc_get_product( $product_data->get_parent_id() );
1191
1192 $variation_attributes = $product_data->get_variation_attributes();
1193 // Filter out 'any' variations, which are empty, as they need to be explicitly specified while adding to cart.
1194 $variation_attributes = array_filter( $variation_attributes, 'wc_array_filter_default_attributes' );
1195
1196 // Gather posted attributes.
1197 $posted_attributes = array();
1198 foreach ( $parent_data->get_attributes() as $attribute ) {
1199 if ( ! $attribute['is_variation'] ) {
1200 continue;
1201 }
1202 $attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
1203
1204 if ( isset( $variation[ $attribute_key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1205 if ( $attribute['is_taxonomy'] ) {
1206 // Don't use wc_clean as it destroys sanitized characters.
1207 $value = sanitize_title( wp_unslash( $variation[ $attribute_key ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1208 } else {
1209 $value = html_entity_decode( wc_clean( wp_unslash( $variation[ $attribute_key ] ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1210 }
1211
1212 // Don't include if it's empty.
1213 if ( ! empty( $value ) || '0' === $value ) {
1214 $posted_attributes[ $attribute_key ] = $value;
1215 }
1216 }
1217 }
1218
1219 // Merge variation attributes and posted attributes.
1220 $posted_and_variation_attributes = array_merge( $variation_attributes, $posted_attributes );
1221
1222 // If no variation ID is set, attempt to get a variation ID from posted attributes.
1223 if ( empty( $variation_id ) ) {
1224 $data_store = WC_Data_Store::load( 'product' );
1225 $variation_id = $data_store->find_matching_product_variation( $parent_data, $posted_attributes );
1226 }
1227
1228 // Do we have a variation ID?
1229 if ( empty( $variation_id ) ) {
1230 throw new Exception( __( 'Please choose product options&hellip;', 'woocommerce' ) );
1231 }
1232
1233 // Check the data we have is valid.
1234 $variation_data = wc_get_product_variation_attributes( $variation_id );
1235 $attributes = array();
1236
1237 foreach ( $parent_data->get_attributes() as $attribute ) {
1238 if ( ! $attribute['is_variation'] ) {
1239 continue;
1240 }
1241
1242 // Get valid value from variation data.
1243 $attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
1244 $valid_value = isset( $variation_data[ $attribute_key ] ) ? $variation_data[ $attribute_key ] : '';
1245
1246 /**
1247 * If the attribute value was posted, check if it's valid.
1248 *
1249 * If no attribute was posted, only error if the variation has an 'any' attribute which requires a value.
1250 */
1251 if ( isset( $posted_and_variation_attributes[ $attribute_key ] ) ) {
1252 $value = $posted_and_variation_attributes[ $attribute_key ];
1253
1254 // Allow if valid or show error.
1255 if ( $valid_value === $value ) {
1256 $attributes[ $attribute_key ] = $value;
1257 } elseif ( '' === $valid_value && in_array( $value, $attribute->get_slugs(), true ) ) {
1258 // If valid values are empty, this is an 'any' variation so get all possible values.
1259 $attributes[ $attribute_key ] = $value;
1260 } else {
1261 /* translators: %s: Attribute name. */
1262 throw new Exception( sprintf( __( 'Invalid value posted for %s', 'woocommerce' ), wc_attribute_label( $attribute['name'] ) ) );
1263 }
1264 } elseif ( '' === $valid_value ) {
1265 $missing_attributes[] = wc_attribute_label( $attribute['name'] );
1266 }
1267
1268 $variation = $attributes;
1269 }
1270 if ( ! empty( $missing_attributes ) ) {
1271 /* translators: %s: Attribute name. */
1272 throw new Exception( sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ) );
1273 }
1274 }
1275
1276 // Validate variation ID.
1277 if (
1278 0 < $variation_id && // Only check if there's any variation_id.
1279 (
1280 ! $product_data->is_type( ProductType::VARIATION ) || // Check if isn't a variation, it suppose to be a variation at this point.
1281 $product_data->get_parent_id() !== $product_id // Check if belongs to the selected variable product.
1282 )
1283 ) {
1284 $product = wc_get_product( $product_id );
1285 if ( ! ( $product instanceof WC_Product ) ) {
1286 throw new Exception( __( 'The selected product is invalid.', 'woocommerce' ) );
1287 }
1288
1289 /* translators: 1: product link, 2: product name */
1290 throw new Exception( sprintf( __( 'The selected product isn\'t a variation of %2$s, please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', 'woocommerce' ), esc_url( $product->get_permalink() ), esc_html( $product->get_name() ) ) );
1291 }
1292
1293 // Load cart item data - may be added by other plugins.
1294 $cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', $cart_item_data, $product_id, $variation_id, $quantity );
1295
1296 // Generate a ID based on product ID, variation ID, variation data, and other cart item data.
1297 $cart_id = $this->generate_cart_id( $product_id, $variation_id, $variation, $cart_item_data );
1298
1299 // Find the cart item key in the existing cart.
1300 $cart_item_key = $this->find_product_in_cart( $cart_id );
1301
1302 // Force quantity to 1 if sold individually and check for existing item in cart.
1303 if ( $product_data->is_sold_individually() ) {
1304 $quantity = apply_filters( 'woocommerce_add_to_cart_sold_individually_quantity', 1, $quantity, $product_id, $variation_id, $cart_item_data );
1305 $found_in_cart = apply_filters( 'woocommerce_add_to_cart_sold_individually_found_in_cart', $cart_item_key && $this->cart_contents[ $cart_item_key ]['quantity'] > 0, $product_id, $variation_id, $cart_item_data, $cart_id );
1306
1307 if ( $found_in_cart ) {
1308 /* translators: %s: product name */
1309 $message = sprintf( __( 'You cannot add another "%s" to your cart.', 'woocommerce' ), $product_data->get_name() );
1310
1311 /**
1312 * Filters message about more than 1 product being added to cart.
1313 *
1314 * @since 4.5.0
1315 * @param string $message Message.
1316 * @param WC_Product $product_data Product data.
1317 */
1318 $message = apply_filters( 'woocommerce_cart_product_cannot_add_another_message', $message, $product_data );
1319 $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '';
1320
1321 if ( ! CartCheckoutUtils::has_cart_page() ) {
1322 $message = sprintf( '%s', esc_html( $message ) );
1323 } else {
1324 $message = sprintf( '%s <a href="%s" class="button wc-forward%s">%s</a>', $message, esc_url( wc_get_cart_url() ), esc_attr( $wp_button_class ), __( 'View cart', 'woocommerce' ) );
1325 }
1326
1327 throw new Exception( $message );
1328 }
1329 }
1330
1331 if ( ! $product_data->is_purchasable() ) {
1332 $message = __( 'Sorry, this product cannot be purchased.', 'woocommerce' );
1333 /**
1334 * Filters message about product unable to be purchased.
1335 *
1336 * @since 3.8.0
1337 * @param string $message Message.
1338 * @param WC_Product $product_data Product data.
1339 */
1340 $message = apply_filters( 'woocommerce_cart_product_cannot_be_purchased_message', $message, $product_data );
1341 throw new Exception( $message );
1342 }
1343
1344 // Stock check - only check if we're managing stock and backorders are not allowed.
1345 if ( ! $product_data->is_in_stock() ) {
1346 /* translators: %s: product name */
1347 $message = sprintf( __( 'You cannot add &quot;%s&quot; to the cart because the product is out of stock.', 'woocommerce' ), $product_data->get_name() );
1348
1349 /**
1350 * Filters message about product being out of stock.
1351 *
1352 * @since 4.5.0
1353 * @param string $message Message.
1354 * @param WC_Product $product_data Product data.
1355 */
1356 $message = apply_filters( 'woocommerce_cart_product_out_of_stock_message', $message, $product_data );
1357 throw new Exception( $message );
1358 }
1359
1360 if ( ! $product_data->has_enough_stock( $quantity ) ) {
1361 $stock_quantity = $product_data->get_stock_quantity();
1362
1363 /* translators: 1: product name 2: quantity in stock */
1364 $message = sprintf( __( 'You cannot add that amount of &quot;%1$s&quot; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce' ), $product_data->get_name(), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ) );
1365
1366 /**
1367 * Filters message about product not having enough stock.
1368 *
1369 * @since 4.5.0
1370 * @param string $message Message.
1371 * @param WC_Product $product_data Product data.
1372 * @param int $stock_quantity Quantity remaining.
1373 */
1374 $message = apply_filters( 'woocommerce_cart_product_not_enough_stock_message', $message, $product_data, $stock_quantity );
1375
1376 throw new Exception( $message );
1377 }
1378
1379 // Stock check - this time accounting for whats already in-cart.
1380 if ( $product_data->managing_stock() ) {
1381 $products_qty_in_cart = $this->get_cart_item_quantities();
1382
1383 if ( isset( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] ) && ! $product_data->has_enough_stock( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] + $quantity ) ) {
1384 $stock_quantity = $product_data->get_stock_quantity();
1385 $stock_quantity_in_cart = $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ];
1386 $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '';
1387
1388 $message = CartCheckoutUtils::has_cart_page() ? sprintf(
1389 '%s <a href="%s" class="button wc-forward%s">%s</a>',
1390 /* translators: 1: quantity in stock 2: current quantity */
1391 sprintf( __( 'You cannot add that amount to the cart &mdash; we have %1$s in stock and you already have %2$s in your cart.', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ), wc_format_stock_quantity_for_display( $stock_quantity_in_cart, $product_data ) ),
1392 esc_url( wc_get_cart_url() ),
1393 esc_attr( $wp_button_class ),
1394 __( 'View cart', 'woocommerce' )
1395 ) : sprintf(
1396 '%s',
1397 /* translators: 1: quantity in stock 2: current quantity */
1398 sprintf( __( 'You cannot add that amount to the cart &mdash; we have %1$s in stock and you already have %2$s in your cart.', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ), wc_format_stock_quantity_for_display( $stock_quantity_in_cart, $product_data ) )
1399 );
1400
1401 /**
1402 * Filters message about product not having enough stock accounting for what's already in the cart.
1403 *
1404 * @param string $message Message.
1405 * @param WC_Product $product_data Product data.
1406 * @param int $stock_quantity Quantity remaining.
1407 * @param int $stock_quantity_in_cart
1408 *
1409 * @since 5.3.0
1410 */
1411 $message = apply_filters( 'woocommerce_cart_product_not_enough_stock_already_in_cart_message', $message, $product_data, $stock_quantity, $stock_quantity_in_cart );
1412
1413 throw new Exception( $message );
1414 }
1415 }
1416
1417 $item_was_already_in_cart = false;
1418 // If cart_item_key is set, the item is already in the cart.
1419 if ( $cart_item_key ) {
1420 $new_quantity = $quantity + $this->cart_contents[ $cart_item_key ]['quantity'];
1421 $this->set_quantity( $cart_item_key, $new_quantity, false );
1422 $item_was_already_in_cart = true;
1423 } else {
1424 $cart_item_key = $cart_id;
1425
1426 // Add item after merging with $cart_item_data - hook to allow plugins to modify cart item.
1427 $this->cart_contents[ $cart_item_key ] = apply_filters(
1428 'woocommerce_add_cart_item',
1429 array_merge(
1430 $cart_item_data,
1431 array(
1432 'key' => $cart_item_key,
1433 'product_id' => $product_id,
1434 'variation_id' => $variation_id,
1435 'variation' => $variation,
1436 'quantity' => $quantity,
1437 'data' => $product_data,
1438 'data_hash' => wc_get_cart_item_data_hash( $product_data ),
1439 )
1440 ),
1441 $cart_item_key
1442 );
1443 }
1444
1445 $this->cart_contents = apply_filters( 'woocommerce_cart_contents_changed', $this->cart_contents );
1446
1447 do_action( 'woocommerce_add_to_cart', $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data );
1448
1449 return $cart_item_key;
1450
1451 } catch ( Exception $e ) {
1452 if ( $e->getMessage() ) {
1453 wc_add_notice( $e->getMessage(), 'error' );
1454 }
1455 return false;
1456 }
1457 }
1458
1459 /**
1460 * Remove a cart item.
1461 *
1462 * @since 2.3.0
1463 * @param string $cart_item_key Cart item key to remove from the cart.
1464 * @return bool
1465 */
1466 public function remove_cart_item( $cart_item_key ) {
1467 if ( isset( $this->cart_contents[ $cart_item_key ] ) ) {
1468 $this->removed_cart_contents[ $cart_item_key ] = $this->cart_contents[ $cart_item_key ];
1469
1470 unset( $this->removed_cart_contents[ $cart_item_key ]['data'] );
1471
1472 do_action( 'woocommerce_remove_cart_item', $cart_item_key, $this );
1473
1474 unset( $this->cart_contents[ $cart_item_key ] );
1475
1476 do_action( 'woocommerce_cart_item_removed', $cart_item_key, $this );
1477
1478 return true;
1479 }
1480 return false;
1481 }
1482
1483 /**
1484 * Restore a cart item.
1485 *
1486 * @param string $cart_item_key Cart item key to restore to the cart.
1487 * @return bool
1488 */
1489 public function restore_cart_item( $cart_item_key ) {
1490 if ( isset( $this->removed_cart_contents[ $cart_item_key ] ) ) {
1491 $restore_item = $this->removed_cart_contents[ $cart_item_key ];
1492 $this->cart_contents[ $cart_item_key ] = $restore_item;
1493 $this->cart_contents[ $cart_item_key ]['data'] = wc_get_product( $restore_item['variation_id'] ? $restore_item['variation_id'] : $restore_item['product_id'] );
1494
1495 do_action( 'woocommerce_restore_cart_item', $cart_item_key, $this );
1496
1497 unset( $this->removed_cart_contents[ $cart_item_key ] );
1498
1499 do_action( 'woocommerce_cart_item_restored', $cart_item_key, $this );
1500
1501 return true;
1502 }
1503 return false;
1504 }
1505
1506 /**
1507 * Set the quantity for an item in the cart using it's key.
1508 *
1509 * @param string $cart_item_key contains the id of the cart item.
1510 * @param int $quantity contains the quantity of the item.
1511 * @param bool $refresh_totals whether or not to calculate totals after setting the new qty. Can be used to defer calculations if setting quantities in bulk.
1512 * @return bool
1513 */
1514 public function set_quantity( $cart_item_key, $quantity = 1, $refresh_totals = true ) {
1515 if ( 0 === $quantity || $quantity < 0 ) {
1516 wc_do_deprecated_action( 'woocommerce_before_cart_item_quantity_zero', array( $cart_item_key, $this ), '3.7.0', 'woocommerce_remove_cart_item' );
1517 // If we're setting qty to 0 we're removing the item from the cart.
1518 return $this->remove_cart_item( $cart_item_key );
1519 }
1520
1521 // Update qty.
1522 $old_quantity = $this->cart_contents[ $cart_item_key ]['quantity'];
1523 $this->cart_contents[ $cart_item_key ]['quantity'] = $quantity;
1524
1525 do_action( 'woocommerce_after_cart_item_quantity_update', $cart_item_key, $quantity, $old_quantity, $this );
1526
1527 if ( $refresh_totals ) {
1528 $this->calculate_totals();
1529 }
1530
1531 /**
1532 * Fired after qty has been changed.
1533 *
1534 * @since 3.6.0
1535 * @param string $cart_item_key contains the id of the cart item. This may be empty if the cart item does not exist any more.
1536 * @param int $quantity contains the quantity of the item.
1537 * @param WC_Cart $this Cart class.
1538 */
1539 do_action( 'woocommerce_cart_item_set_quantity', $cart_item_key, $quantity, $this );
1540
1541 return true;
1542 }
1543
1544 /**
1545 * Get cart's owner.
1546 *
1547 * @since 3.2.0
1548 * @return \WC_Customer
1549 */
1550 public function get_customer() {
1551 return WC()->customer;
1552 }
1553
1554 /**
1555 * Calculate totals for the items in the cart.
1556 *
1557 * @uses WC_Cart_Totals
1558 * @return void
1559 */
1560 public function calculate_totals() {
1561 $this->reset_totals();
1562
1563 if ( $this->is_empty() ) {
1564 $this->session->set_session();
1565 return;
1566 }
1567
1568 do_action( 'woocommerce_before_calculate_totals', $this );
1569
1570 new WC_Cart_Totals( $this );
1571
1572 do_action( 'woocommerce_after_calculate_totals', $this );
1573 }
1574
1575 /**
1576 * Looks at the totals to see if payment is actually required.
1577 *
1578 * @return bool
1579 */
1580 public function needs_payment() {
1581 return apply_filters( 'woocommerce_cart_needs_payment', 0 < $this->get_total( 'edit' ), $this );
1582 }
1583
1584 /*
1585 * Shipping related functions.
1586 */
1587
1588 /**
1589 * Get selected shipping methods after calculation.
1590 *
1591 * @return array
1592 */
1593 public function get_shipping_methods() {
1594 return $this->shipping_methods;
1595 }
1596
1597 /**
1598 * Whether the shipping totals have been calculated.
1599 *
1600 * @return bool
1601 */
1602 public function has_calculated_shipping() {
1603 return $this->has_calculated_shipping;
1604 }
1605
1606 /**
1607 * Uses the shipping class to calculate shipping then gets the totals when its finished.
1608 *
1609 * @return array
1610 */
1611 public function calculate_shipping() {
1612 // Reset totals.
1613 $this->set_shipping_total( 0 );
1614 $this->set_shipping_tax( 0 );
1615 $this->set_shipping_taxes( array() );
1616 $this->shipping_methods = array();
1617 $this->has_calculated_shipping = false;
1618
1619 if ( ! $this->needs_shipping() || ! $this->show_shipping() ) {
1620 return $this->shipping_methods;
1621 }
1622
1623 $this->has_calculated_shipping = true;
1624 $this->shipping_methods = $this->get_chosen_shipping_methods( WC()->shipping()->calculate_shipping( $this->get_shipping_packages() ) );
1625
1626 $shipping_costs = wp_list_pluck( $this->shipping_methods, 'cost' );
1627 $shipping_taxes = wp_list_pluck( $this->shipping_methods, 'taxes' );
1628 $merged_taxes = array();
1629 foreach ( $shipping_taxes as $taxes ) {
1630 foreach ( $taxes as $tax_id => $tax_amount ) {
1631 $merged_taxes[ $tax_id ] = ( $merged_taxes[ $tax_id ] ?? 0 ) + $tax_amount;
1632 }
1633 }
1634
1635 $this->set_shipping_total( array_sum( array_filter( $shipping_costs ) ) );
1636 $this->set_shipping_tax( array_sum( $merged_taxes ) );
1637 $this->set_shipping_taxes( $merged_taxes );
1638
1639 return $this->shipping_methods;
1640 }
1641
1642 /**
1643 * Given a set of packages with rates, get the chosen ones only.
1644 *
1645 * @since 3.2.0
1646 * @param array $calculated_shipping_packages Array of packages.
1647 * @return array
1648 */
1649 protected function get_chosen_shipping_methods( $calculated_shipping_packages = array() ) {
1650 return ShippingUtil::get_selected_shipping_rates_from_packages( $calculated_shipping_packages );
1651 }
1652
1653 /**
1654 * Filter items needing shipping callback.
1655 *
1656 * @since 3.0.0
1657 * @param array $item Item to check for shipping.
1658 * @return bool
1659 */
1660 protected function filter_items_needing_shipping( $item ) {
1661 $product = $item['data'];
1662 return $product && $product->needs_shipping();
1663 }
1664
1665 /**
1666 * Get only items that need shipping.
1667 *
1668 * @since 3.0.0
1669 * @return array
1670 */
1671 protected function get_items_needing_shipping() {
1672 return array_filter( $this->get_cart(), array( $this, 'filter_items_needing_shipping' ) );
1673 }
1674
1675 /**
1676 * Get packages to calculate shipping for.
1677 *
1678 * This lets us calculate costs for carts that are shipped to multiple locations.
1679 *
1680 * Shipping methods are responsible for looping through these packages.
1681 *
1682 * By default we pass the cart itself as a package - plugins can change this.
1683 * through the filter and break it up.
1684 *
1685 * @since 1.5.4
1686 * @return array of cart items
1687 */
1688 public function get_shipping_packages() {
1689 /**
1690 * Filters the shipping packages for the cart.
1691 *
1692 * @since 1.5.4
1693 * @param array $packages The shipping packages.
1694 * @return array The shipping packages.
1695 */
1696 $shipping_packages = apply_filters(
1697 'woocommerce_cart_shipping_packages',
1698 array(
1699 array(
1700 'contents' => $this->get_items_needing_shipping(),
1701 'contents_cost' => array_sum( wp_list_pluck( $this->get_items_needing_shipping(), 'line_total' ) ),
1702 'applied_coupons' => $this->get_applied_coupons(),
1703 'user' => array(
1704 'ID' => get_current_user_id(),
1705 ),
1706 'destination' => array(
1707 'country' => $this->get_customer()->get_shipping_country(),
1708 'state' => $this->get_customer()->get_shipping_state(),
1709 'postcode' => $this->get_customer()->get_shipping_postcode(),
1710 'city' => $this->get_customer()->get_shipping_city(),
1711 'address' => $this->get_customer()->get_shipping_address(), // This is an alias of address_1, provided for backwards compatibility.
1712 'address_1' => $this->get_customer()->get_shipping_address_1(),
1713 'address_2' => $this->get_customer()->get_shipping_address_2(),
1714 ),
1715 'cart_subtotal' => $this->get_displayed_subtotal(),
1716 ),
1717 )
1718 );
1719
1720 // Return empty array if invalid object supplied by the filter or no packages.
1721 if ( ! is_array( $shipping_packages ) || empty( $shipping_packages ) ) {
1722 return array();
1723 }
1724
1725 // Remove any invalid packages before adding package IDs.
1726 $shipping_packages = array_filter(
1727 $shipping_packages,
1728 function ( $package ) {
1729 return ! empty( $package ) && is_array( $package );
1730 }
1731 );
1732
1733 // Add package ID and package name to each package after the filter is applied.
1734 $index = 1;
1735 foreach ( $shipping_packages as $key => $package ) {
1736 $shipping_packages[ $key ]['package_id'] = $package['package_id'] ?? $key;
1737 $shipping_packages[ $key ]['package_name'] = $this->get_shipping_package_name( $shipping_packages[ $key ], $index, count( $shipping_packages ) );
1738 ++$index;
1739 }
1740
1741 return $shipping_packages;
1742 }
1743
1744 /**
1745 * Get the package name.
1746 *
1747 * @param array $package Shipping package data.
1748 * @param int $index Package number.
1749 * @param int $total_packages Total number of packages.
1750 * @return string
1751 */
1752 private function get_shipping_package_name( $package, $index, $total_packages ) {
1753
1754 $shipping_package_name = _x( 'Shipment', 'shipping packages', 'woocommerce' );
1755
1756 /**
1757 * If there are multiple packages, use the index to show the package number.
1758 */
1759 if ( 1 !== $total_packages ) {
1760 $shipping_package_name = sprintf(
1761 /* translators: %d: shipping package number. */
1762 _x( 'Shipment %d', 'shipping packages', 'woocommerce' ),
1763 $index
1764 );
1765 }
1766
1767 /**
1768 * Filters the shipping package name.
1769 *
1770 * @since 4.3.0
1771 * @param string $shipping_package_name Shipping package name.
1772 * @param string $package_id Shipping package ID.
1773 * @param array $package Shipping package from WooCommerce.
1774 * @param int $total_packages Total number of shipping packages.
1775 * @return string Shipping package name.
1776 */
1777 return apply_filters(
1778 'woocommerce_shipping_package_name',
1779 $shipping_package_name,
1780 $package['package_id'],
1781 $package,
1782 $total_packages
1783 );
1784 }
1785
1786 /**
1787 * Looks through the cart to see if shipping is actually required.
1788 *
1789 * @return bool whether or not the cart needs shipping
1790 */
1791 public function needs_shipping() {
1792 if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( true ) ) {
1793 return false;
1794 }
1795
1796 $needs_shipping = false;
1797
1798 foreach ( $this->get_cart_contents() as $values ) {
1799 if ( $values['data']->needs_shipping() ) {
1800 $needs_shipping = true;
1801 break;
1802 }
1803 }
1804
1805 return apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping );
1806 }
1807
1808 /**
1809 * Should the shipping address form be shown.
1810 *
1811 * @return bool
1812 */
1813 public function needs_shipping_address() {
1814 return apply_filters( 'woocommerce_cart_needs_shipping_address', true === $this->needs_shipping() && ! wc_ship_to_billing_address_only() );
1815 }
1816
1817 /**
1818 * Sees if the customer has entered enough data to calculate shipping.
1819 *
1820 * @return bool
1821 */
1822 public function show_shipping() {
1823 // If there are no shipping methods or no cart contents, no need to calculate shipping.
1824 if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( true ) || ! $this->get_cart_contents() ) {
1825 return false;
1826 }
1827
1828 if ( 'yes' === get_option( 'woocommerce_shipping_cost_requires_address' ) ) {
1829 // If local pickup is enabled, shipping should be shown so that pickup locations are visible before address entry.
1830 if ( LocalPickupUtils::is_local_pickup_enabled() ) {
1831 /**
1832 * This filter is documented below.
1833 *
1834 * @since 2.7.0
1835 */
1836 return apply_filters( 'woocommerce_cart_ready_to_calc_shipping', true );
1837 }
1838
1839 if ( 'shortcode' === $this->cart_context ) {
1840 $country = $this->get_customer()->get_shipping_country();
1841 if ( ! $country ) {
1842 return false;
1843 }
1844 $country_fields = WC()->countries->get_address_fields( $country, 'shipping_' );
1845 $checkout_fields = WC()->checkout()->get_checkout_fields();
1846
1847 /**
1848 * Filter to not require shipping state for shipping calculation, even if it is required at checkout.
1849 * This can be used to allow shipping calculations to be done without a state.
1850 *
1851 * @since 8.4.0
1852 *
1853 * @param bool $show_state Whether to use the state field. Default true.
1854 */
1855 $state_enabled = apply_filters( 'woocommerce_shipping_calculator_enable_state', true );
1856 $state_required = isset( $country_fields['shipping_state'] ) && $country_fields['shipping_state']['required'];
1857 // Takes care of late unsetting of checkout fields via hooks (woocommerce_checkout_fields, woocommerce_shipping_fields).
1858 $checkout_state_field_exists = isset( $checkout_fields['shipping']['shipping_state'] );
1859 if ( $state_enabled && $state_required && ! $this->get_customer()->get_shipping_state() && $checkout_state_field_exists ) {
1860 return false;
1861 }
1862 /**
1863 * Filter to not require shipping postcode for shipping calculation, even if it is required at checkout.
1864 * This can be used to allow shipping calculations to be done without a postcode.
1865 *
1866 * @since 8.4.0
1867 *
1868 * @param bool $show_postcode Whether to use the postcode field. Default true.
1869 */
1870 $postcode_enabled = apply_filters( 'woocommerce_shipping_calculator_enable_postcode', true );
1871 $postcode_required = isset( $country_fields['shipping_postcode'] ) && $country_fields['shipping_postcode']['required'];
1872 // Takes care of late unsetting of checkout fields via hooks (woocommerce_checkout_fields, woocommerce_shipping_fields).
1873 $checkout_postcode_field_exists = isset( $checkout_fields['shipping']['shipping_postcode'] );
1874 if ( $postcode_enabled && $postcode_required && '' === $this->get_customer()->get_shipping_postcode() && $checkout_postcode_field_exists ) {
1875 return false;
1876 }
1877 } else {
1878 $customer = $this->get_customer();
1879
1880 if ( ! $customer instanceof \WC_Customer || ! $customer->has_full_shipping_address() ) {
1881 return false;
1882 }
1883 }
1884 }
1885
1886 /**
1887 * Filter to allow plugins to prevent shipping calculations.
1888 *
1889 * @since 2.7.0
1890 *
1891 * @param bool $ready Whether the cart is ready to calculate shipping.
1892 */
1893 return apply_filters( 'woocommerce_cart_ready_to_calc_shipping', true );
1894 }
1895
1896 /**
1897 * Gets the shipping total (after calculation).
1898 *
1899 * @return string price or string for the shipping total
1900 */
1901 public function get_cart_shipping_total() {
1902
1903 // Default total assumes Free shipping.
1904 $total = __( 'Free!', 'woocommerce' );
1905
1906 if ( 0 < $this->get_shipping_total() ) {
1907
1908 if ( $this->display_prices_including_tax() ) {
1909 $total = wc_price( $this->shipping_total + $this->shipping_tax_total );
1910
1911 if ( $this->shipping_tax_total > 0 && ! wc_prices_include_tax() ) {
1912 $total .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
1913 }
1914 } else {
1915 $total = wc_price( $this->shipping_total );
1916
1917 if ( $this->shipping_tax_total > 0 && wc_prices_include_tax() ) {
1918 $total .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
1919 }
1920 }
1921 }
1922 return apply_filters( 'woocommerce_cart_shipping_total', $total, $this );
1923 }
1924
1925 /**
1926 * Check for user coupons (now that we have billing email). If a coupon is invalid, add an error.
1927 *
1928 * Checks two types of coupons:
1929 * 1. Where a list of customer emails are set (limits coupon usage to those defined).
1930 * 2. Where a usage_limit_per_user is set (limits coupon usage to a number based on user ID and email).
1931 *
1932 * @param array $posted Post data.
1933 * @return void
1934 */
1935 public function check_customer_coupons( $posted ) {
1936 foreach ( $this->get_applied_coupons() as $code ) {
1937 $coupon = new WC_Coupon( $code );
1938
1939 if ( $coupon->is_valid() ) {
1940
1941 // Get user and posted emails to compare.
1942 $current_user = wp_get_current_user();
1943 $billing_email = isset( $posted['billing_email'] ) ? $posted['billing_email'] : '';
1944 $check_emails = array_unique(
1945 array_filter(
1946 array_map(
1947 'strtolower',
1948 array_map(
1949 'sanitize_email',
1950 array(
1951 $billing_email,
1952 $current_user->user_email,
1953 )
1954 )
1955 )
1956 )
1957 );
1958
1959 // Limit to defined email addresses.
1960 $restrictions = $coupon->get_email_restrictions();
1961
1962 if ( is_array( $restrictions ) && 0 < count( $restrictions ) && ! DiscountsUtil::is_coupon_emails_allowed( $check_emails, $restrictions ) ) {
1963 $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
1964 $this->remove_coupon( $code );
1965 }
1966
1967 $coupon_usage_limit = $coupon->get_usage_limit_per_user();
1968 if ( 0 < $coupon_usage_limit && 0 === get_current_user_id() ) {
1969 // For guest, usage per user has not been enforced yet. Enforce it now.
1970 $coupon_data_store = $coupon->get_data_store();
1971 $billing_email = strtolower( sanitize_email( $billing_email ) );
1972 if ( $coupon_data_store && $coupon_data_store->get_usage_by_email( $coupon, $billing_email ) >= $coupon_usage_limit ) {
1973 if ( $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $billing_email ) ) ) {
1974 $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST );
1975 } else {
1976 $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED );
1977 }
1978 }
1979 }
1980 }
1981 }
1982 }
1983
1984 /**
1985 * Checks if the given email address(es) matches the ones specified on the coupon.
1986 *
1987 * @param array $check_emails Array of customer email addresses.
1988 * @param array $restrictions Array of allowed email addresses.
1989 *
1990 * @return bool
1991 * @deprecated 9.0.0 In favor of static method Automattic\WooCommerce\Utilities\DiscountsUtil::is_coupon_emails_allowed.
1992 */
1993 public function is_coupon_emails_allowed( $check_emails, $restrictions ) {
1994 wc_doing_it_wrong(
1995 'WC_Cart::is_coupon_emails_allowed',
1996 __( 'This method has been deprecated and will be removed soon. Use Automattic\WooCommerce\Utilities\DiscountsUtil::is_coupon_emails_allowed instead.', 'woocommerce' ),
1997 '9.0.0'
1998 );
1999
2000 return DiscountsUtil::is_coupon_emails_allowed( $check_emails, $restrictions );
2001 }
2002
2003
2004 /**
2005 * Returns whether or not a discount has been applied.
2006 *
2007 * @param string $coupon_code Coupon code to check.
2008 * @return bool
2009 */
2010 public function has_discount( $coupon_code = '' ) {
2011 $applied_coupons = $this->get_applied_coupons();
2012
2013 if ( ! $coupon_code ) {
2014 return count( $applied_coupons ) > 0;
2015 }
2016
2017 $coupon_code = wc_format_coupon_code( $coupon_code );
2018
2019 // Check if the coupon is in applied coupons using case-insensitive comparison.
2020 foreach ( $applied_coupons as $applied_coupon ) {
2021 if ( wc_is_same_coupon( $applied_coupon, $coupon_code ) ) {
2022 return true;
2023 }
2024 }
2025
2026 return false;
2027 }
2028
2029 /**
2030 * Applies a coupon code passed to the method.
2031 *
2032 * @param string $coupon_code - The code to apply.
2033 * @return bool True if the coupon is applied, false if it does not exist or cannot be applied.
2034 */
2035 public function apply_coupon( $coupon_code ) {
2036 // Coupons are globally disabled.
2037 if ( ! wc_coupons_enabled() ) {
2038 return false;
2039 }
2040
2041 // Sanitize coupon code.
2042 $coupon_code = wc_format_coupon_code( $coupon_code );
2043
2044 // Get the coupon.
2045 $the_coupon = new WC_Coupon( $coupon_code );
2046
2047 // Prevent adding coupons by post ID.
2048 if ( ! wc_is_same_coupon( $the_coupon->get_code(), $coupon_code ) ) {
2049 $the_coupon->set_code( $coupon_code );
2050 $the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_EXIST );
2051 return false;
2052 }
2053
2054 // Check it can be used with cart.
2055 if ( ! $the_coupon->is_valid() ) {
2056 wc_add_notice( $the_coupon->get_error_message(), 'error' );
2057 return false;
2058 }
2059
2060 // Check if applied.
2061 if ( $this->has_discount( $coupon_code ) ) {
2062 $the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED );
2063 return false;
2064 }
2065
2066 // If its individual use then remove other coupons.
2067 if ( $the_coupon->get_individual_use() ) {
2068 $coupons_to_keep = apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $the_coupon, $this->applied_coupons );
2069
2070 foreach ( $this->applied_coupons as $applied_coupon ) {
2071 $keep_key = array_search( $applied_coupon, $coupons_to_keep, true );
2072 if ( false === $keep_key ) {
2073 $this->remove_coupon( $applied_coupon );
2074 } else {
2075 unset( $coupons_to_keep[ $keep_key ] );
2076 }
2077 }
2078
2079 if ( ! empty( $coupons_to_keep ) ) {
2080 $this->applied_coupons += $coupons_to_keep;
2081 }
2082 }
2083
2084 // Check to see if an individual use coupon is set.
2085 if ( $this->applied_coupons ) {
2086 foreach ( $this->applied_coupons as $code ) {
2087 $coupon = new WC_Coupon( $code );
2088
2089 if ( $coupon->get_individual_use() && false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $the_coupon, $coupon, $this->applied_coupons ) ) {
2090
2091 // Reject new coupon.
2092 $coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY );
2093
2094 return false;
2095 }
2096 }
2097 }
2098
2099 $this->applied_coupons[] = $coupon_code;
2100
2101 // Choose free shipping.
2102 if ( $the_coupon->get_free_shipping() ) {
2103 $packages = WC()->shipping()->get_packages();
2104 $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
2105
2106 foreach ( $packages as $i => $package ) {
2107 $chosen_shipping_methods[ $i ] = 'free_shipping';
2108 }
2109
2110 WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
2111 }
2112
2113 $the_coupon->add_coupon_message( WC_Coupon::WC_COUPON_SUCCESS );
2114
2115 /**
2116 * Action ran after a coupon is applied.
2117 *
2118 * @since 2.0.0
2119 * @param string $coupon_code The coupon code that was applied.
2120 */
2121 do_action( 'woocommerce_applied_coupon', $coupon_code );
2122
2123 return true;
2124 }
2125
2126 /**
2127 * Get array of applied coupon objects and codes.
2128 *
2129 * @param null $deprecated No longer used.
2130 * @return array of applied coupons
2131 */
2132 public function get_coupons( $deprecated = null ) {
2133 $coupons = array();
2134
2135 if ( 'order' === $deprecated ) {
2136 return $coupons;
2137 }
2138
2139 foreach ( $this->get_applied_coupons() as $code ) {
2140 $coupon = new WC_Coupon( $code );
2141 $coupons[ $code ] = $coupon;
2142 }
2143
2144 return $coupons;
2145 }
2146
2147 /**
2148 * Get the discount amount for a used coupon.
2149 *
2150 * @param string $code coupon code.
2151 * @param bool $ex_tax inc or ex tax.
2152 * @return float discount amount
2153 */
2154 public function get_coupon_discount_amount( $code, $ex_tax = true ) {
2155 $totals = $this->get_coupon_discount_totals();
2156 $discount_amount = 0;
2157 foreach ( $totals as $key => $value ) {
2158 if ( wc_is_same_coupon( $key, $code ) ) {
2159 $discount_amount = $value;
2160 break;
2161 }
2162 }
2163
2164 if ( ! $ex_tax ) {
2165 $discount_amount += $this->get_coupon_discount_tax_amount( $code );
2166 }
2167
2168 return wc_cart_round_discount( $discount_amount, wc_get_price_decimals() );
2169 }
2170
2171 /**
2172 * Get the discount tax amount for a used coupon (for tax inclusive prices).
2173 *
2174 * @param string $code coupon code.
2175 * @return float discount amount
2176 */
2177 public function get_coupon_discount_tax_amount( $code ) {
2178 $totals = $this->get_coupon_discount_tax_totals();
2179 $tax_amount = 0;
2180 foreach ( $totals as $key => $value ) {
2181 if ( wc_is_same_coupon( $key, $code ) ) {
2182 $tax_amount = $value;
2183 break;
2184 }
2185 }
2186 return wc_cart_round_discount( $tax_amount, wc_get_price_decimals() );
2187 }
2188
2189 /**
2190 * Remove coupons from the cart of a defined type. Type 1 is before tax, type 2 is after tax.
2191 *
2192 * @param null $deprecated No longer used.
2193 * @return void
2194 */
2195 public function remove_coupons( $deprecated = null ) {
2196 $this->set_coupon_discount_totals( array() );
2197 $this->set_coupon_discount_tax_totals( array() );
2198 $this->set_applied_coupons( array() );
2199 $this->session->set_session();
2200 }
2201
2202 /**
2203 * Remove a single coupon by code.
2204 *
2205 * @param string $coupon_code Code of the coupon to remove.
2206 * @return bool
2207 */
2208 public function remove_coupon( $coupon_code ) {
2209 $coupon_code = wc_format_coupon_code( $coupon_code );
2210
2211 // Find the coupon in applied coupons using case-insensitive comparison.
2212 foreach ( $this->get_applied_coupons() as $key => $applied_coupon ) {
2213 if ( wc_is_same_coupon( $applied_coupon, $coupon_code ) ) {
2214 unset( $this->applied_coupons[ $key ] );
2215 break;
2216 }
2217 }
2218
2219 WC()->session->set( 'refresh_totals', true );
2220
2221 do_action( 'woocommerce_removed_coupon', $coupon_code );
2222
2223 return true;
2224 }
2225
2226 /**
2227 * Trigger an action so 3rd parties can add custom fees.
2228 *
2229 * @since 2.0.0
2230 * @return void
2231 */
2232 public function calculate_fees() {
2233 do_action( 'woocommerce_cart_calculate_fees', $this );
2234 }
2235
2236 /**
2237 * Return reference to fees API.
2238 *
2239 * @since 3.2.0
2240 * @return WC_Cart_Fees
2241 */
2242 public function fees_api() {
2243 return $this->fees_api;
2244 }
2245
2246 /**
2247 * Add additional fee to the cart.
2248 *
2249 * This method should be called on a callback attached to the
2250 * woocommerce_cart_calculate_fees action during cart/checkout. Fees do not
2251 * persist.
2252 *
2253 * @uses WC_Cart_Fees::add_fee
2254 * @param string $name Unique name for the fee. Multiple fees of the same name cannot be added.
2255 * @param float $amount Fee amount (do not enter negative amounts).
2256 * @param bool $taxable Is the fee taxable? (default: false).
2257 * @param string $tax_class The tax class for the fee if taxable. A blank string is standard tax class. (default: '').
2258 * @return void
2259 */
2260 public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) {
2261 $this->fees_api()->add_fee(
2262 array(
2263 'name' => $name,
2264 'amount' => (float) $amount,
2265 'taxable' => $taxable,
2266 'tax_class' => $tax_class,
2267 )
2268 );
2269 }
2270
2271 /**
2272 * Return all added fees from the Fees API.
2273 *
2274 * @uses WC_Cart_Fees::get_fees
2275 * @return array
2276 */
2277 public function get_fees() {
2278 $fees = $this->fees_api()->get_fees();
2279
2280 if ( property_exists( $this, 'fees' ) ) {
2281 $fees = $fees + (array) $this->fees;
2282 }
2283 return $fees;
2284 }
2285
2286 /**
2287 * Gets the total excluding taxes.
2288 *
2289 * @return string formatted price
2290 */
2291 public function get_total_ex_tax() {
2292 return apply_filters( 'woocommerce_cart_total_ex_tax', wc_price( max( 0, $this->get_total( 'edit' ) - $this->get_total_tax() ) ) );
2293 }
2294
2295 /**
2296 * Gets the cart contents total (after calculation).
2297 *
2298 * @return string formatted price
2299 */
2300 public function get_cart_total() {
2301 return apply_filters( 'woocommerce_cart_contents_total', wc_price( wc_prices_include_tax() ? $this->get_cart_contents_total() + $this->get_cart_contents_tax() : $this->get_cart_contents_total() ) );
2302 }
2303
2304 /**
2305 * Gets the sub total (after calculation).
2306 *
2307 * @param bool $compound whether to include compound taxes.
2308 * @return string formatted price
2309 */
2310 public function get_cart_subtotal( $compound = false ) {
2311 /**
2312 * If the cart has compound tax, we want to show the subtotal as cart + shipping + non-compound taxes (after discount).
2313 */
2314 if ( $compound ) {
2315 $cart_subtotal = wc_price( $this->get_cart_contents_total() + $this->get_shipping_total() + $this->get_taxes_total( false, false ) );
2316
2317 } elseif ( $this->display_prices_including_tax() ) {
2318 $cart_subtotal = wc_price( $this->get_subtotal() + $this->get_subtotal_tax() );
2319
2320 if ( $this->get_subtotal_tax() > 0 && ! wc_prices_include_tax() ) {
2321 $cart_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
2322 }
2323 } else {
2324 $cart_subtotal = wc_price( $this->get_subtotal() );
2325
2326 if ( $this->get_subtotal_tax() > 0 && wc_prices_include_tax() ) {
2327 $cart_subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
2328 }
2329 }
2330
2331 return apply_filters( 'woocommerce_cart_subtotal', $cart_subtotal, $compound, $this );
2332 }
2333
2334 /**
2335 * Get the product row price per item.
2336 *
2337 * @param WC_Product $product Product object.
2338 * @return string formatted price
2339 */
2340 public function get_product_price( $product ) {
2341 if ( $this->display_prices_including_tax() ) {
2342 $product_price = wc_get_price_including_tax( $product );
2343 } else {
2344 $product_price = wc_get_price_excluding_tax( $product );
2345 }
2346 return apply_filters( 'woocommerce_cart_product_price', wc_price( $product_price ), $product );
2347 }
2348
2349 /**
2350 * Get the product row subtotal.
2351 *
2352 * Gets the tax etc to avoid rounding issues.
2353 *
2354 * When on the checkout (review order), this will get the subtotal based on the customer's tax rate rather than the base rate.
2355 *
2356 * @param WC_Product $product Product object.
2357 * @param int $quantity Quantity being purchased.
2358 * @return string formatted price
2359 */
2360 public function get_product_subtotal( $product, $quantity ) {
2361 $price = $product->get_price();
2362
2363 if ( $product->is_taxable() ) {
2364
2365 if ( $this->display_prices_including_tax() ) {
2366 $row_price = wc_get_price_including_tax( $product, array( 'qty' => $quantity ) );
2367 $product_subtotal = wc_price( $row_price );
2368
2369 if ( ! wc_prices_include_tax() && $this->get_subtotal_tax() > 0 ) {
2370 $product_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
2371 }
2372 } else {
2373 $row_price = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) );
2374 $product_subtotal = wc_price( $row_price );
2375
2376 if ( wc_prices_include_tax() && $this->get_subtotal_tax() > 0 ) {
2377 $product_subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
2378 }
2379 }
2380 } else {
2381 $row_price = (float) $price * (float) $quantity;
2382 $product_subtotal = wc_price( $row_price );
2383 }
2384
2385 return apply_filters( 'woocommerce_cart_product_subtotal', $product_subtotal, $product, $quantity, $this );
2386 }
2387
2388 /**
2389 * Gets the cart tax (after calculation).
2390 *
2391 * @return string formatted price
2392 */
2393 public function get_cart_tax() {
2394 $cart_total_tax = wc_round_tax_total( $this->get_cart_contents_tax() + $this->get_shipping_tax() + $this->get_fee_tax() );
2395
2396 return apply_filters( 'woocommerce_get_cart_tax', $cart_total_tax ? wc_price( $cart_total_tax ) : '' );
2397 }
2398
2399 /**
2400 * Get a tax amount.
2401 *
2402 * @param string $tax_rate_id ID of the tax rate to get taxes for.
2403 * @return float amount
2404 */
2405 public function get_tax_amount( $tax_rate_id ) {
2406 $taxes = wc_array_merge_recursive_numeric( $this->get_cart_contents_taxes(), $this->get_fee_taxes() );
2407 return $taxes[ $tax_rate_id ] ?? 0;
2408 }
2409
2410 /**
2411 * Get a tax amount.
2412 *
2413 * @param string $tax_rate_id ID of the tax rate to get taxes for.
2414 * @return float amount
2415 */
2416 public function get_shipping_tax_amount( $tax_rate_id ) {
2417 $taxes = $this->get_shipping_taxes();
2418 return $taxes[ $tax_rate_id ] ?? 0;
2419 }
2420
2421 /**
2422 * Get tax row amounts with or without compound taxes includes.
2423 *
2424 * @param bool $compound True if getting compound taxes.
2425 * @param bool $display True if getting total to display.
2426 * @return float price
2427 */
2428 public function get_taxes_total( $compound = true, $display = true ) {
2429 $taxes = $this->get_taxes();
2430
2431 // Skip compounding taxes if requested.
2432 if ( ! $compound ) {
2433 $tax_rate_objects = wc_get_container()->get( TaxRateDataStore::class )->get_rate_objects_for_ids( array_keys( $taxes ) );
2434 foreach ( $taxes as $key => $tax ) {
2435 if ( WC_Tax::is_compound( $tax_rate_objects[ $key ] ?? $key ) ) {
2436 unset( $taxes[ $key ] );
2437 }
2438 }
2439 }
2440
2441 $total = array_sum( $taxes );
2442 $total = $display ? wc_format_decimal( $total, wc_get_price_decimals() ) : $total;
2443
2444 return apply_filters( 'woocommerce_cart_taxes_total', $total, $compound, $display, $this );
2445 }
2446
2447 /**
2448 * Gets the total discount amount.
2449 *
2450 * @return mixed formatted price or false if there are none
2451 */
2452 public function get_total_discount() {
2453 return apply_filters( 'woocommerce_cart_total_discount', $this->get_discount_total() ? wc_price( $this->get_discount_total() ) : false, $this );
2454 }
2455
2456 /**
2457 * Reset cart totals to the defaults. Useful before running calculations.
2458 *
2459 * @return void
2460 */
2461 private function reset_totals() {
2462 $this->totals = $this->default_totals;
2463 $this->fees_api->remove_all_fees();
2464 do_action( 'woocommerce_cart_reset', $this, false );
2465 }
2466
2467 /**
2468 * Returns 'incl' if tax should be included in cart, otherwise returns 'excl'.
2469 *
2470 * @return string
2471 */
2472 public function get_tax_price_display_mode() {
2473 if ( $this->get_customer() && $this->get_customer()->get_is_vat_exempt() ) {
2474 return TaxDisplayMode::EXCLUSIVE;
2475 }
2476
2477 return get_option( 'woocommerce_tax_display_cart' );
2478 }
2479
2480 /**
2481 * Returns the hash based on cart contents.
2482 *
2483 * @since 3.6.0
2484 * @return string hash for cart content
2485 */
2486 public function get_cart_hash() {
2487 $cart_session = $this->session->get_cart_for_session();
2488 $hash = $cart_session ? md5( wp_json_encode( $cart_session ) . $this->get_total( 'edit' ) ) : '';
2489 $hash = apply_filters_deprecated( 'woocommerce_add_to_cart_hash', array( $hash, $cart_session ), '3.6.0', 'woocommerce_cart_hash' );
2490
2491 return apply_filters( 'woocommerce_cart_hash', $hash, $cart_session );
2492 }
2493 }
2494