cart total and coupons, we need to recalculate its availability when obtaining shipping methods for a recurring cart add_filter( 'woocommerce_shipping_free_shipping_is_available', __CLASS__ . '::maybe_recalculate_shipping_method_availability', 10, 2 ); add_filter( 'woocommerce_add_to_cart_handler', __CLASS__ . '::add_to_cart_handler', 10, 2 ); add_action( 'woocommerce_cart_calculate_fees', __CLASS__ . '::apply_recurring_fees', 1000, 1 ); add_action( 'woocommerce_checkout_update_order_review', __CLASS__ . '::update_chosen_shipping_methods' ); } /** * Attaches the "set_subscription_prices_for_calculation" filter to the WC Product's woocommerce_get_price hook. * * This function is hooked to "woocommerce_before_calculate_totals" so that WC will calculate a subscription * product's total based on the total of it's price per period and sign up fee (if any). * * @since 1.2 */ public static function add_calculation_price_filter() { WC()->cart->recurring_carts = array(); // Only hook when cart contains a subscription if ( ! self::cart_contains_subscription() ) { return; } // Set which price should be used for calculation if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { add_filter( 'woocommerce_get_price', __CLASS__ . '::set_subscription_prices_for_calculation', 100, 2 ); } else { add_filter( 'woocommerce_product_get_price', __CLASS__ . '::set_subscription_prices_for_calculation', 100, 2 ); add_filter( 'woocommerce_product_variation_get_price', __CLASS__ . '::set_subscription_prices_for_calculation', 100, 2 ); } } /** * Removes the "set_subscription_prices_for_calculation" filter from the WC Product's woocommerce_get_price hook once * calculations are complete. * * @since 1.2 */ public static function remove_calculation_price_filter() { if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { remove_filter( 'woocommerce_get_price', __CLASS__ . '::set_subscription_prices_for_calculation', 100 ); } else { remove_filter( 'woocommerce_product_get_price', __CLASS__ . '::set_subscription_prices_for_calculation', 100 ); remove_filter( 'woocommerce_product_variation_get_price', __CLASS__ . '::set_subscription_prices_for_calculation', 100 ); } } /** * Use WC core add-to-cart handlers for subscription products. * * @param string $handler The name of the handler to use when adding product to the cart * @param WC_Product $product */ public static function add_to_cart_handler( $handler, $product ) { if ( WC_Subscriptions_Product::is_subscription( $product ) ) { switch ( $handler ) { case 'variable-subscription' : $handler = 'variable'; break; case 'subscription' : $handler = 'simple'; break; } } return $handler; } /** * If we are running a custom calculation, we need to set the price returned by a product * to be the appropriate value. This may include just the sign-up fee, a combination of the * sign-up fee and recurring amount or just the recurring amount (default). * * If there are subscriptions in the cart and the product is not a subscription, then * set the recurring total to 0. * * @since 1.2 */ public static function set_subscription_prices_for_calculation( $price, $product ) { if ( WC_Subscriptions_Product::is_subscription( $product ) ) { // For original calculations, we need the items price to account for sign-up fees and/or free trial if ( 'none' == self::$calculation_type ) { $sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product ); // Extra check to make sure that the sign up fee is numeric before using it $sign_up_fee = is_numeric( $sign_up_fee ) ? (float) $sign_up_fee : 0; $trial_length = WC_Subscriptions_Product::get_trial_length( $product ); if ( $trial_length > 0 ) { $price = $sign_up_fee; } else { $price += $sign_up_fee; } } // else $price = recurring amount already as WC_Product->get_price() returns subscription price $price = apply_filters( 'woocommerce_subscriptions_cart_get_price', $price, $product ); // Make sure the recurring amount for any non-subscription products in the cart with a subscription is $0 } elseif ( 'recurring_total' == self::$calculation_type ) { $price = 0; } return $price; } /** * Calculate the initial and recurring totals for all subscription products in the cart. * * We need to group subscriptions by billing schedule to make the display and creation of recurring totals sane, * when there are multiple subscriptions in the cart. To do that, we use an array with keys of the form: * '{billing_interval}_{billing_period}_{trial_interval}_{trial_period}_{length}_{billing_period}'. This key * is used to reference WC_Cart objects for each recurring billing schedule and these are stored in the master * cart with the billing schedule key. * * After we have calculated and grouped all recurring totals, we need to checks the structure of the subscription * product prices to see whether they include sign-up fees and/or free trial periods and then recalculates the * appropriate totals by using the @see self::$calculation_type flag and cloning the cart to run @see WC_Cart::calculate_totals() * * @since 1.3.5 * @version 2.0 */ public static function calculate_subscription_totals( $total, $cart ) { if ( ! self::cart_contains_subscription() && ! wcs_cart_contains_resubscribe() ) { // cart doesn't contain subscription return $total; } elseif ( 'none' != self::$calculation_type ) { // We're in the middle of a recalculation, let it run return $total; } // Save the original cart values/totals, as we'll use this when there is no sign-up fee WC()->cart->total = ( $total < 0 ) ? 0 : $total; do_action( 'woocommerce_subscription_cart_before_grouping' ); $subscription_groups = array(); // Group the subscription items by their cart item key based on billing schedule foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { if ( WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { $subscription_groups[ self::get_recurring_cart_key( $cart_item ) ][] = $cart_item_key; } } do_action( 'woocommerce_subscription_cart_after_grouping' ); $recurring_carts = array(); // Back up the shipping method. Chances are WC is going to wipe the chosen_shipping_methods data WC()->session->set( 'wcs_shipping_methods', WC()->session->get( 'chosen_shipping_methods', array() ) ); // Now let's calculate the totals for each group of subscriptions self::$calculation_type = 'recurring_total'; foreach ( $subscription_groups as $recurring_cart_key => $subscription_group ) { // Create a clone cart to calculate and store totals for this group of subscriptions $recurring_cart = clone WC()->cart; $product = null; self::$recurring_cart_key = $recurring_cart->recurring_cart_key = $recurring_cart_key; // Remove any items not in this subscription group foreach ( $recurring_cart->get_cart() as $cart_item_key => $cart_item ) { if ( ! in_array( $cart_item_key, $subscription_group ) ) { unset( $recurring_cart->cart_contents[ $cart_item_key ] ); continue; } if ( null === $product ) { $product = $cart_item['data']; } } $recurring_cart->start_date = apply_filters( 'wcs_recurring_cart_start_date', gmdate( 'Y-m-d H:i:s' ), $recurring_cart ); $recurring_cart->trial_end_date = apply_filters( 'wcs_recurring_cart_trial_end_date', WC_Subscriptions_Product::get_trial_expiration_date( $product, $recurring_cart->start_date ), $recurring_cart, $product ); $recurring_cart->next_payment_date = apply_filters( 'wcs_recurring_cart_next_payment_date', WC_Subscriptions_Product::get_first_renewal_payment_date( $product, $recurring_cart->start_date ), $recurring_cart, $product ); $recurring_cart->end_date = apply_filters( 'wcs_recurring_cart_end_date', WC_Subscriptions_Product::get_expiration_date( $product, $recurring_cart->start_date ), $recurring_cart, $product ); // Before calculating recurring cart totals, store this recurring cart object self::$cached_recurring_cart = $recurring_cart; // No fees recur (yet) if ( is_callable( array( $recurring_cart, 'fees_api' ) ) ) { // WC 3.2 + $recurring_cart->fees_api()->remove_all_fees(); } else { $recurring_cart->fees = array(); } $recurring_cart->fee_total = 0; WC()->shipping->reset_shipping(); self::maybe_restore_shipping_methods(); $recurring_cart->calculate_totals(); // Store this groups cart details $recurring_carts[ $recurring_cart_key ] = clone $recurring_cart; // And remove some other floatsam $recurring_carts[ $recurring_cart_key ]->removed_cart_contents = array(); $recurring_carts[ $recurring_cart_key ]->cart_session_data = array(); // Keep a record of the shipping packages so we can add them to the global packages later self::$recurring_shipping_packages[ $recurring_cart_key ] = WC()->shipping->get_packages(); } self::$calculation_type = self::$recurring_cart_key = 'none'; // We need to reset the packages and totals stored in WC()->shipping too WC()->shipping->reset_shipping(); self::maybe_restore_shipping_methods(); WC()->cart->calculate_shipping(); // We no longer need our backup of shipping methods unset( WC()->session->wcs_shipping_methods ); // If there is no sign-up fee and a free trial, and no products being purchased with the subscription, we need to zero the fees for the first billing period if ( 0 == self::get_cart_subscription_sign_up_fee() && self::all_cart_items_have_free_trial() ) { $cart_fees = WC()->cart->get_fees(); if ( WC_Subscriptions::is_woocommerce_pre( '3.2' ) ) { foreach ( $cart_fees as $fee_index => $fee ) { WC()->cart->fees[ $fee_index ]->amount = 0; WC()->cart->fees[ $fee_index ]->tax = 0; } } else { foreach ( $cart_fees as $fee ) { $fee->amount = 0; $fee->tax = 0; $fee->total = 0; } WC()->cart->fees_api()->set_fees( $cart_fees ); } WC()->cart->fee_total = 0; } WC()->cart->recurring_carts = $recurring_carts; $total = max( 0, round( WC()->cart->cart_contents_total + WC()->cart->tax_total + WC()->cart->shipping_tax_total + WC()->cart->shipping_total + WC()->cart->fee_total, WC()->cart->dp ) ); if ( ! self::charge_shipping_up_front() ) { $total = max( 0, $total - WC()->cart->shipping_tax_total - WC()->cart->shipping_total ); WC()->cart->shipping_taxes = array(); WC()->cart->shipping_tax_total = 0; WC()->cart->shipping_total = 0; } return apply_filters( 'woocommerce_subscriptions_calculated_total', $total ); } /** * Check whether shipping should be charged on the initial order. * * When the cart contains a physical subscription with a free trial and no other physical items, shipping * should not be charged up-front. * * @since 1.5.4 */ public static function charge_shipping_up_front() { $charge_shipping_up_front = true; if ( self::all_cart_items_have_free_trial() ) { $charge_shipping_up_front = false; $other_items_need_shipping = false; foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { if ( ! WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) && $cart_item['data']->needs_shipping() ) { $other_items_need_shipping = true; } } if ( false === $other_items_need_shipping ) { $charge_shipping_up_front = false; } } return apply_filters( 'woocommerce_subscriptions_cart_shipping_up_front', $charge_shipping_up_front ); } /** * The cart needs shipping only if it needs shipping up front and/or for recurring items. * * @since 2.0 */ public static function cart_needs_shipping( $needs_shipping ) { if ( self::cart_contains_subscription() ) { if ( 'none' == self::$calculation_type ) { if ( true == $needs_shipping && ! self::charge_shipping_up_front() && ! self::cart_contains_subscriptions_needing_shipping() ) { $needs_shipping = false; } elseif ( false == $needs_shipping && ( self::charge_shipping_up_front() || self::cart_contains_subscriptions_needing_shipping() ) ) { $needs_shipping = false; } } elseif ( 'recurring_total' == self::$calculation_type ) { $cart = ( isset( self::$cached_recurring_cart ) ) ? self::$cached_recurring_cart : WC()->cart; if ( true == $needs_shipping && ! self::cart_contains_subscriptions_needing_shipping( $cart ) ) { $needs_shipping = false; } elseif ( false == $needs_shipping && self::cart_contains_subscriptions_needing_shipping( $cart ) ) { $needs_shipping = true; } } } return $needs_shipping; } /** * Remove all recurring shipping methods stored in the session (i.e. methods with a key that is a string) * * This is attached as a callback to hooks triggered whenever a product is removed from the cart. * * @param $cart_item_key string The key for a cart item about to be removed from the cart. * @return null * @since 2.0.15 */ public static function maybe_reset_chosen_shipping_methods( $cart_item_key ) { if ( isset( WC()->cart->cart_contents[ $cart_item_key ] ) ) { $chosen_methods = WC()->session->get( 'chosen_shipping_methods', array() ); // Remove all recurring methods foreach ( $chosen_methods as $key => $methods ) { if ( ! is_numeric( $key ) ) { unset( $chosen_methods[ $key ] ); } } WC()->session->set( 'chosen_shipping_methods', $chosen_methods ); } } /** * Parse recurring shipping rates from the front end and put them into the $_POST['shipping_method'] used by WooCommerce. * * When WooCommerce takes the value of inputs for shipping methods selection from the cart and checkout pages, it uses a * JavaScript array and therefore, can only use numerical indexes. This works for WC core, because it only needs shipping * selection for different packages. However, we want to use string indexes to differentiate between different recurring * cart shipping selection inputs *and* packages. To do this, we need to get our shipping methods from the $_POST['post_data'] * values and manually add them $_POST['shipping_method'] array. * * We can't do this on the cart page unfortunately because it doesn't pass the entire forms post data and instead only * sends the shipping methods with a numerical index. * * @return null * @since 2.0.12 */ public static function add_shipping_method_post_data() { if ( ! WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) { return; } check_ajax_referer( 'update-order-review', 'security' ); parse_str( $_POST['post_data'], $form_data ); // In case we have only free trials/sync'd products in the cart and shipping methods aren't being displayed if ( ! isset( $_POST['shipping_method'] ) ) { $_POST['shipping_method'] = array(); } if ( ! isset( $form_data['shipping_method'] ) ) { $form_data['shipping_method'] = array(); } foreach ( $form_data['shipping_method'] as $key => $methods ) { if ( ! is_numeric( $key ) && ! array_key_exists( $key, $_POST['shipping_method'] ) ) { $_POST['shipping_method'][ $key ] = $methods; } } } /** * When WooCommerce calculates rates for a recurring shipping package, we need to make sure there is a * different number of rates to make sure WooCommerce updates the chosen method for the recurring cart * and the 'woocommerce_shipping_chosen_method' filter is called, which we use to make sure the chosen * method is the recurring method, not the initial method. * * This function is hooked to 'woocommerce_shipping_packages' called by WC_Shipping->calculate_shipping() * which is why it accepts and returns the $packages array. It is also attached with a very high priority * to avoid conflicts with any 3rd party plugins that may use the method count session value (only a couple * of other hooks, including 'woocommerce_shipping_chosen_method' and 'woocommerce_shipping_method_chosen' * are triggered between when this callback runs on 'woocommerce_shipping_packages' and when the session * value is set again by WC_Shipping->calculate_shipping()). * * For more details, see: https://github.com/Prospress/woocommerce-subscriptions/pull/1187#issuecomment-186091152 * * @param array $packages An array of shipping package of the form returned by WC_Cart->get_shipping_packages() which includes the package's contents, cost, customer, destination and alternative rates * @since 2.0.19 */ public static function reset_shipping_method_counts( $packages ) { if ( 'none' !== self::$recurring_cart_key ) { WC()->session->set( 'shipping_method_counts', array() ); } return $packages; } /** * Set the chosen shipping method for recurring cart calculations * * In WC_Shipping::calculate_shipping(), WooCommerce tries to determine the chosen shipping method * based on the package index and stores rates. However, for recurring cart shipping selection, we * use the recurring cart key instead of numeric index. Therefore, we need to hook in to override * the default shipping method when WooCommerce could not find a matching shipping method. * * @param string $default_method the default shipping method for the customer/store returned by WC_Shipping::get_default_method() * @param array $available_methods set of shipping rates for this calculation * @param int $package_index WC doesn't pass the package index to callbacks on the 'woocommerce_shipping_chosen_method' filter (yet) so we set a default value of 0 for it in the function params * @since 2.0.12 */ public static function set_chosen_shipping_method( $default_method, $available_methods, $package_index = 0 ) { $chosen_methods = WC()->session->get( 'chosen_shipping_methods', array() ); $recurring_cart_package_key = self::get_recurring_shipping_package_key( self::$recurring_cart_key, $package_index ); if ( 'none' !== self::$recurring_cart_key && isset( $chosen_methods[ $recurring_cart_package_key ] ) && isset( $available_methods[ $chosen_methods[ $recurring_cart_package_key ] ] ) ) { $default_method = $chosen_methods[ $recurring_cart_package_key ]; // Set the chosen shipping method (if available) to workaround WC_Shipping::get_default_method() setting the default shipping method whenever method count changes } elseif ( isset( $chosen_methods[ $package_index ] ) && $default_method !== $chosen_methods[ $package_index ] && isset( $available_methods[ $chosen_methods[ $package_index ] ] ) ) { $default_method = $chosen_methods[ $package_index ]; } return $default_method; } /** * Create a shipping package index for a given shipping package on a recurring cart. * * @param string $recurring_cart_key a cart key of the form returned by @see self::get_recurring_cart_key() * @param int $package_index the index of a package * @since 2.0.12 */ public static function get_recurring_shipping_package_key( $recurring_cart_key, $package_index ) { return $recurring_cart_key . '_' . $package_index; } /** * Add the shipping packages stored in @see self::$recurring_shipping_packages to WooCommerce's global * set of packages in WC()->shipping->packages so that plugins attempting to get the details of recurring * packages can get them with WC()->shipping->get_packages() like any other packages. * * @since 2.0.13 */ public static function set_global_recurring_shipping_packages() { foreach ( self::$recurring_shipping_packages as $recurring_cart_key => $packages ) { foreach ( $packages as $package_index => $package ) { WC()->shipping->packages[ self::get_recurring_shipping_package_key( $recurring_cart_key, $package_index ) ] = $package; } } } /** * Check whether all the subscription product items in the cart have a free trial. * * Useful for determining if certain up-front amounts should be charged. * * @since 2.0 */ public static function all_cart_items_have_free_trial() { $all_items_have_free_trial = true; foreach ( WC()->cart->get_cart() as $cart_item ) { if ( ! WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { $all_items_have_free_trial = false; break; } else { if ( 0 == WC_Subscriptions_Product::get_trial_length( $cart_item['data'] ) ) { $all_items_have_free_trial = false; break; } } } return apply_filters( 'woocommerce_subscriptions_all_cart_items_have_free_trial', $all_items_have_free_trial ); } /** * Check if the cart contains a subscription which requires shipping. * * @since 1.5.4 */ public static function cart_contains_subscriptions_needing_shipping( $cart = null ) { if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) { return false; } if ( null === $cart ) { $cart = WC()->cart; } $cart_contains_subscriptions_needing_shipping = false; if ( self::cart_contains_subscription() ) { foreach ( $cart->cart_contents as $cart_item_key => $values ) { $_product = $values['data']; if ( WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && false === WC_Subscriptions_Product::needs_one_time_shipping( $_product ) ) { $cart_contains_subscriptions_needing_shipping = true; } } } return apply_filters( 'woocommerce_cart_contains_subscriptions_needing_shipping', $cart_contains_subscriptions_needing_shipping ); } /** * Filters the cart contents to remove any subscriptions with free trials (or synchronised to a date in the future) * to make sure no shipping amount is calculated for them. * * @since 2.0 */ public static function set_cart_shipping_packages( $packages ) { if ( self::cart_contains_subscription() ) { if ( 'none' == self::$calculation_type ) { foreach ( $packages as $index => $package ) { foreach ( $package['contents'] as $cart_item_key => $cart_item ) { if ( WC_Subscriptions_Product::get_trial_length( $cart_item['data'] ) > 0 ) { unset( $packages[ $index ]['contents'][ $cart_item_key ] ); } } if ( empty( $packages[ $index ]['contents'] ) ) { unset( $packages[ $index ] ); } } } elseif ( 'recurring_total' == self::$calculation_type ) { foreach ( $packages as $index => $package ) { foreach ( $package['contents'] as $cart_item_key => $cart_item ) { if ( WC_Subscriptions_Product::needs_one_time_shipping( $cart_item['data'] ) ) { $packages[ $index ]['contents_cost'] -= $cart_item['line_total']; unset( $packages[ $index ]['contents'][ $cart_item_key ] ); } } if ( empty( $packages[ $index ]['contents'] ) ) { unset( $packages[ $index ] ); } else { // we need to make sure the package is different for recurring carts to bypass WC's cache $packages[ $index ]['recurring_cart_key'] = self::$recurring_cart_key; } } } } return $packages; } /* Formatted Totals Functions */ /** * Returns the subtotal for a cart item including the subscription period and duration details * * @since 1.0 */ public static function get_formatted_product_subtotal( $product_subtotal, $product, $quantity, $cart ) { if ( WC_Subscriptions_Product::is_subscription( $product ) && ! wcs_cart_contains_renewal() ) { if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { $product_price_filter = 'woocommerce_get_price'; } else { $product_price_filter = is_a( $product, 'WC_Product_Variation' ) ? 'woocommerce_product_variation_get_price' : 'woocommerce_product_get_price'; } // Avoid infinite loop remove_filter( 'woocommerce_cart_product_subtotal', __CLASS__ . '::get_formatted_product_subtotal', 11, 4 ); add_filter( $product_price_filter, 'WC_Subscriptions_Product::get_sign_up_fee_filter', 100, 2 ); // And get the appropriate sign up fee string $sign_up_fee_string = $cart->get_product_subtotal( $product, $quantity ); remove_filter( $product_price_filter, 'WC_Subscriptions_Product::get_sign_up_fee_filter', 100, 2 ); add_filter( 'woocommerce_cart_product_subtotal', __CLASS__ . '::get_formatted_product_subtotal', 11, 4 ); $product_subtotal = WC_Subscriptions_Product::get_price_string( $product, array( 'price' => $product_subtotal, 'sign_up_fee' => $sign_up_fee_string, 'tax_calculation' => WC()->cart->tax_display_cart, ) ); $inc_tax_or_vat_string = WC()->countries->inc_tax_or_vat(); $ex_tax_or_vat_string = WC()->countries->ex_tax_or_vat(); if ( ! empty( $inc_tax_or_vat_string ) && false !== strpos( $product_subtotal, $inc_tax_or_vat_string ) ) { $product_subtotal = str_replace( WC()->countries->inc_tax_or_vat(), '', $product_subtotal ) . ' ' . WC()->countries->inc_tax_or_vat() . ''; } if ( ! empty( $ex_tax_or_vat_string ) && false !== strpos( $product_subtotal, $ex_tax_or_vat_string ) ) { $product_subtotal = str_replace( WC()->countries->ex_tax_or_vat(), '', $product_subtotal ) . ' ' . WC()->countries->ex_tax_or_vat() . ''; } $product_subtotal = '' . $product_subtotal . ''; } return $product_subtotal; } /* * Helper functions for extracting the details of subscriptions in the cart */ /** * Checks the cart to see if it contains a subscription product. * * @since 1.0 */ public static function cart_contains_subscription() { $contains_subscription = false; if ( ! empty( WC()->cart->cart_contents ) && ! wcs_cart_contains_renewal() ) { foreach ( WC()->cart->cart_contents as $cart_item ) { if ( WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { $contains_subscription = true; break; } } } return $contains_subscription; } /** * Checks the cart to see if it contains a subscription product with a free trial * * @since 1.2 */ public static function cart_contains_free_trial() { $cart_contains_free_trial = false; if ( self::cart_contains_subscription() ) { foreach ( WC()->cart->cart_contents as $cart_item ) { if ( WC_Subscriptions_Product::get_trial_length( $cart_item['data'] ) > 0 ) { $cart_contains_free_trial = true; break; } } } return $cart_contains_free_trial; } /** * Gets the cart calculation type flag * * @since 1.2 */ public static function get_calculation_type() { return self::$calculation_type; } /** * Sets the cart calculation type flag * * @since 2.0 */ public static function set_calculation_type( $calculation_type ) { self::$calculation_type = $calculation_type; return $calculation_type; } /** * Gets the subscription sign up fee for the cart and returns it * * Currently short-circuits to return just the sign-up fee of the first subscription, because only * one subscription can be purchased at a time. * * @since 1.0 */ public static function get_cart_subscription_sign_up_fee() { $sign_up_fee = 0; if ( self::cart_contains_subscription() || wcs_cart_contains_renewal() ) { $renewal_item = wcs_cart_contains_renewal(); foreach ( WC()->cart->cart_contents as $cart_item ) { // Renewal items do not have sign-up fees if ( $renewal_item == $cart_item ) { continue; } $cart_item_sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $cart_item['data'] ); // Extra check to make sure that the sign up fee is numeric before using it $cart_item_sign_up_fee = is_numeric( $cart_item_sign_up_fee ) ? (float) $cart_item_sign_up_fee : 0; $sign_up_fee += $cart_item_sign_up_fee; } } return apply_filters( 'woocommerce_subscriptions_cart_sign_up_fee', $sign_up_fee ); } /** * Check whether the cart needs payment even if the order total is $0 * * @param bool $needs_payment The existing flag for whether the cart needs payment or not. * @param WC_Cart $cart The WooCommerce cart object. * @return bool */ public static function cart_needs_payment( $needs_payment, $cart ) { if ( false === $needs_payment && self::cart_contains_subscription() && $cart->total == 0 && false === WC_Subscriptions_Switcher::cart_contains_switches() && 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) { $recurring_total = 0; $is_one_period = true; $contains_synced = false; $contains_expiring_limited_coupon = false; foreach ( WC()->cart->recurring_carts as $recurring_cart ) { $recurring_total += $recurring_cart->total; $subscription_length = wcs_cart_pluck( $recurring_cart, 'subscription_length' ); $contains_synced = $contains_synced || (bool) WC_Subscriptions_Synchroniser::cart_contains_synced_subscription( $recurring_cart ); $contains_expiring_limited_coupon = $contains_expiring_limited_coupon || WC_Subscriptions_Coupon::recurring_cart_contains_expiring_coupon( $recurring_cart ); if ( 0 == $subscription_length || wcs_cart_pluck( $recurring_cart, 'subscription_period_interval' ) != $subscription_length ) { $is_one_period = false; } } $has_trial = self::cart_contains_free_trial(); if ( $contains_expiring_limited_coupon || $recurring_total > 0 && ( ! $is_one_period || $has_trial || $contains_synced ) ) { $needs_payment = true; } } return $needs_payment; } /** * Restore shipping method, as well as cost and tax estimate when on the cart page. * * The WC_Shortcode_Cart actually calculates shipping when the "Calculate Shipping" form is submitted on the * cart page. Because of that, our own @see self::calculate_totals() method calculates incorrect values on * the cart page because it triggers the method multiple times for multiple different pricing structures. * This uses the same logic found in WC_Shortcode_Cart::output() to determine the correct estimate. * * @since 1.4.10 */ private static function maybe_restore_shipping_methods() { if ( ! empty( $_POST['calc_shipping'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-cart' ) && function_exists( 'WC' ) ) { try { WC()->shipping->reset_shipping(); $country = wc_clean( $_POST['calc_shipping_country'] ); $state = isset( $_POST['calc_shipping_state'] ) ? wc_clean( $_POST['calc_shipping_state'] ) : ''; $postcode = apply_filters( 'woocommerce_shipping_calculator_enable_postcode', true ) ? wc_clean( $_POST['calc_shipping_postcode'] ) : ''; $city = apply_filters( 'woocommerce_shipping_calculator_enable_city', false ) ? wc_clean( $_POST['calc_shipping_city'] ) : ''; if ( $postcode && ! WC_Validation::is_postcode( $postcode, $country ) ) { throw new Exception( __( 'Please enter a valid postcode/ZIP.', 'woocommerce-subscriptions' ) ); } elseif ( $postcode ) { $postcode = wc_format_postcode( $postcode, $country ); } if ( $country ) { WC()->customer->set_location( $country, $state, $postcode, $city ); WC()->customer->set_shipping_location( $country, $state, $postcode, $city ); } else { WC()->customer->set_to_base(); WC()->customer->set_shipping_to_base(); } WC()->customer->calculated_shipping( true ); do_action( 'woocommerce_calculated_shipping' ); } catch ( Exception $e ) { if ( ! empty( $e ) ) { wc_add_notice( $e->getMessage(), 'error' ); } } } // If we had one time shipping in the carts, we may have wiped the WC chosen shippings. Restore them. self::maybe_restore_chosen_shipping_method(); if ( isset( $_POST['shipping_method'] ) && is_array( $_POST['shipping_method'] ) ) { // Now make sure the correct shipping method is set $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', array() ); foreach ( $_POST['shipping_method'] as $i => $value ) { $chosen_shipping_methods[ $i ] = wc_clean( $value ); } WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); } } /** * Make sure cart product prices correctly include/exclude taxes. * * @since 1.5.8 */ public static function cart_product_price( $price, $product ) { if ( WC_Subscriptions_Product::is_subscription( $product ) ) { $price = WC_Subscriptions_Product::get_price_string( $product, array( 'price' => $price, 'tax_calculation' => WC()->cart->tax_display_cart ) ); } return $price; } /** * Make sure cart totals are calculated when the cart widget is populated via the get_refreshed_fragments() method * so that @see self::get_formatted_cart_subtotal() returns the correct subtotal price string. * * @since 1.5.11 */ public static function pre_get_refreshed_fragments() { if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && ! defined( 'WOOCOMMERCE_CART' ) ) { define( 'WOOCOMMERCE_CART', true ); WC()->cart->calculate_totals(); } } /** * Display the recurring totals for items in the cart * * @since 2.0 */ public static function display_recurring_totals() { if ( self::cart_contains_subscription() ) { // We only want shipping for recurring amounts, and they need to be calculated again here self::$calculation_type = 'recurring_total'; $shipping_methods = array(); $carts_with_multiple_payments = 0; // Create new subscriptions for each subscription product in the cart (that is not a renewal) foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) { // Cart contains more than one payment if ( 0 != $recurring_cart->next_payment_date ) { $carts_with_multiple_payments++; } } if ( $carts_with_multiple_payments >= 1 ) { wc_get_template( 'checkout/recurring-totals.php', array( 'shipping_methods' => $shipping_methods, 'recurring_carts' => WC()->cart->recurring_carts, 'carts_with_multiple_payments' => $carts_with_multiple_payments ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); } self::$calculation_type = 'none'; } } /** * Construct a cart key based on the billing schedule of a subscription product. * * Subscriptions groups products by billing schedule when calculating cart totals, so that shipping and other "per order" amounts * can be calculated for each group of items for each renewal. This method constructs a cart key based on the billing schedule * to allow products on the same billing schedule to be grouped together - free trials and synchronisation is accounted for by * using the first renewal date (if any) for the susbcription. * * @since 2.0 */ public static function get_recurring_cart_key( $cart_item, $renewal_time = '' ) { $cart_key = ''; $product = $cart_item['data']; $product_id = wcs_get_canonical_product_id( $product ); $renewal_time = ! empty( $renewal_time ) ? $renewal_time : WC_Subscriptions_Product::get_first_renewal_payment_time( $product_id ); $interval = WC_Subscriptions_Product::get_interval( $product ); $period = WC_Subscriptions_Product::get_period( $product ); $length = WC_Subscriptions_Product::get_length( $product ); $trial_period = WC_Subscriptions_Product::get_trial_period( $product ); $trial_length = WC_Subscriptions_Product::get_trial_length( $product ); if ( $renewal_time > 0 ) { $cart_key .= gmdate( 'Y_m_d_', $renewal_time ); } // First start with the billing interval and period switch ( $interval ) { case 1 : if ( 'day' == $period ) { $cart_key .= 'daily'; // always gotta be one exception } else { $cart_key .= sprintf( '%sly', $period ); } break; case 2 : $cart_key .= sprintf( 'every_2nd_%s', $period ); break; case 3 : $cart_key .= sprintf( 'every_3rd_%s', $period ); // or sometimes two exceptions it would seem break; default: $cart_key .= sprintf( 'every_%dth_%s', $interval, $period ); break; } if ( $length > 0 ) { $cart_key .= '_for_'; $cart_key .= sprintf( '%d_%s', $length, $period ); if ( $length > 1 ) { $cart_key .= 's'; } } if ( $trial_length > 0 ) { $cart_key .= sprintf( '_after_a_%d_%s_trial', $trial_length, $trial_period ); } return apply_filters( 'woocommerce_subscriptions_recurring_cart_key', $cart_key, $cart_item ); } /** * Don't allow new subscription products to be added to the cart if it contains a subscription renewal already. * * @since 2.0 */ public static function check_valid_add_to_cart( $is_valid, $product_id, $quantity, $variation_id = '', $variations = array(), $item_data = array() ) { if ( $is_valid && ! isset( $item_data['subscription_renewal'] ) && wcs_cart_contains_renewal() && WC_Subscriptions_Product::is_subscription( $product_id ) ) { wc_add_notice( __( 'That subscription product can not be added to your cart as it already contains a subscription renewal.', 'woocommerce-subscriptions' ), 'error' ); $is_valid = false; } return $is_valid; } /** * When calculating shipping for recurring carts, return a revised list of shipping methods that apply to this recurring cart. * * When WooCommerce determines the taxable address for local pick up methods, we only want to return pick up shipping methods * chosen for the recurring cart being calculated instead of all methods. * * @param array $shipping_methods * * @since 2.0.13 */ public static function filter_recurring_cart_chosen_shipping_method( $shipping_methods ) { if ( 'recurring_total' == self::$calculation_type && 'none' !== self::$recurring_cart_key ) { $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', array() ); $standard_package_methods = array(); $recurring_cart_shipping_methods = array(); foreach ( $chosen_shipping_methods as $key => $method ) { if ( is_numeric( $key ) ) { $standard_package_methods[ $key ] = $method; } else if ( strpos( $key, self::$recurring_cart_key ) !== false ) { $recurring_cart_shipping_methods[ $key ] = $method; } } // pick which chosen methods apply to this recurring cart. Defaults to standard methods if there is no specific recurring cart shipping methods chosen. $applicable_chosen_shipping_methods = ( empty( $recurring_cart_shipping_methods ) ) ? $standard_package_methods : $recurring_cart_shipping_methods; $shipping_methods = array_intersect( $applicable_chosen_shipping_methods, $shipping_methods ); } return $shipping_methods; } /** * Validate the chosen recurring shipping methods for each recurring shipping package. * Ensures there is at least one chosen shipping method and that the chosen method is valid considering the available * package rates. * * @since 2.0.14 */ public static function validate_recurring_shipping_methods() { $shipping_methods = WC()->checkout()->shipping_methods; $added_invalid_notice = false; $standard_packages = WC()->shipping->get_packages(); // temporarily store the current calculation type and recurring cart key so we can restore them later $calculation_type = self::$calculation_type; self::$calculation_type = 'recurring_total'; $recurring_cart_key_flag = self::$recurring_cart_key; foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) { if ( false === $recurring_cart->needs_shipping() || 0 == $recurring_cart->next_payment_date ) { continue; } self::$recurring_cart_key = $recurring_cart_key; $packages = $recurring_cart->get_shipping_packages(); foreach ( $packages as $package_index => $base_package ) { $package = self::get_calculated_shipping_for_package( $base_package ); if ( ( isset( $standard_packages[ $package_index ] ) && $package['rates'] == $standard_packages[ $package_index ]['rates'] ) && apply_filters( 'wcs_cart_totals_shipping_html_price_only', true, $package, WC()->cart->recurring_carts[ $recurring_cart_key ] ) ) { // the recurring package rates match the initial package rates, there won't be a selected shipping method for this recurring cart package // move on to the next package continue; } $recurring_shipping_package_key = WC_Subscriptions_Cart::get_recurring_shipping_package_key( $recurring_cart_key, $package_index ); if ( ! isset( $package['rates'][ $shipping_methods[ $recurring_shipping_package_key ] ] ) ) { if ( ! $added_invalid_notice ) { wc_add_notice( __( 'Invalid recurring shipping method.', 'woocommerce-subscriptions' ), 'error' ); $added_invalid_notice = true; } $shipping_methods[ $recurring_shipping_package_key ] = ''; } } } // If there was an invalid recurring shipping method found, we need to apply the changes to WC()->checkout()->shipping_methods. if ( $added_invalid_notice ) { WC()->checkout()->shipping_methods = $shipping_methods; } self::$calculation_type = $calculation_type; self::$recurring_cart_key = $recurring_cart_key_flag; } /** * Checks the cart to see if it contains a specific product. * * @param int The product ID or variation ID to look for. * @return bool Whether the product is in the cart. * @since 2.0.13 */ public static function cart_contains_product( $product_id ) { $cart_contains_product = false; if ( ! empty( WC()->cart->cart_contents ) ) { foreach ( WC()->cart->cart_contents as $cart_item ) { if ( wcs_get_canonical_product_id( $cart_item ) == $product_id ) { $cart_contains_product = true; break; } } } return $cart_contains_product; } /** * Cache the package rates calculated by @see WC_Shipping::calculate_shipping_for_package() to avoid multiple calls of calculate_shipping_for_package() per request. * * @param array $rates A set of WC_Shipping_Rate objects. * @param array $package A shipping package in the form returned by @see WC_Cart->get_shipping_packages() * @return array $rates An unaltered set of WC_Shipping_Rate objects passed to the function * @since 2.0.18 */ public static function cache_package_rates( $rates, $package ) { self::$shipping_rates[ self::get_package_shipping_rates_cache_key( $package ) ] = $rates; return $rates; } /** * Calculates the shipping rates for a package. * * This function will check cached rates based on a hash of the package contents to avoid re-calculation per page load. * If there are no rates stored in the cache for this package, it will fall back to @see WC_Shipping::calculate_shipping_for_package() * * @param array $package A shipping package in the form returned by @see WC_Cart->get_shipping_packages() * @return array $package * @since 2.0.18 */ public static function get_calculated_shipping_for_package( $package ) { $key = self::get_package_shipping_rates_cache_key( $package ); if ( isset( self::$shipping_rates[ $key ] ) ) { $package['rates'] = apply_filters( 'woocommerce_package_rates', self::$shipping_rates[ $key ], $package ); } else { $package = WC()->shipping->calculate_shipping_for_package( $package ); } return $package; } /** * Generate a unique package key for a given shipping package to be used for caching package rates. * * @param array $package A shipping package in the form returned by WC_Cart->get_shipping_packages(). * @return string key hash * @since 2.0.18 */ private static function get_package_shipping_rates_cache_key( $package ) { return md5( json_encode( array( array_keys( $package['contents'] ), $package['contents_cost'], $package['applied_coupons'] ) ) ); } /** * When calculating the free shipping method availability, WC uses the WC->cart object. During shipping calculations for * recurring carts we need the recurring cart's total and coupons to be the base for checking its availability * * @param bool $is_available * @param array $package * @return bool $is_available a revised version of is_available based off the recurring cart object * * @since 2.0.20 */ public static function maybe_recalculate_shipping_method_availability( $is_available, $package ) { if ( isset( $package['recurring_cart_key'] ) && isset( self::$cached_recurring_cart ) && $package['recurring_cart_key'] == self::$cached_recurring_cart->recurring_cart_key ) { // Take a copy of the WC global cart object so we can temporarily set it to base shipping method availability on the cached recurring cart $global_cart = WC()->cart; WC()->cart = self::$cached_recurring_cart; foreach ( WC()->shipping->get_shipping_methods() as $shipping_method ) { if ( $shipping_method->id == 'free_shipping' ) { remove_filter( 'woocommerce_shipping_free_shipping_is_available', __METHOD__ ); $is_available = $shipping_method->is_available( $package ); add_filter( 'woocommerce_shipping_free_shipping_is_available', __METHOD__, 10, 2 ); break; } } WC()->cart = $global_cart; } return $is_available; } /** * Allow third-parties to apply fees which apply to the cart to recurring carts. * * @param WC_Cart * @since 2.2.16 */ public static function apply_recurring_fees( $cart ) { if ( ! empty( $cart->recurring_cart_key ) ) { foreach ( WC()->cart->get_fees() as $fee ) { if ( apply_filters( 'woocommerce_subscriptions_is_recurring_fee', false, $fee, $cart ) ) { if ( is_callable( array( $cart, 'fees_api' ) ) ) { // WC 3.2 + $cart->fees_api()->add_fee( $fee ); } else { $cart->add_fee( $fee->name, $fee->amount, $fee->taxable, $fee->tax_class ); } } } } } /** * Update the chosen recurring package shipping methods from posted checkout form data. * * Between requests, the presence of recurring package chosen shipping methods in posted * checkout data can change. For example, when the number of available shipping methods * change and cause the hidden elements (generated by @see wcs_cart_print_shipping_input()) * to be displayed or not displayed. * * When this occurs, we need to remove those chosen shipping methods from the session so * that those packages no longer use the previously selected shipping method. * * @param string $encoded_form_data Encoded checkout form data. * @since 2.3.0 */ public static function update_chosen_shipping_methods( $encoded_form_data ) { $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', array() ); parse_str( $encoded_form_data, $form_data ); foreach ( $chosen_shipping_methods as $package_index => $method ) { // Remove the chosen shipping methods for recurring packages which are no longer present in posted checkout data. if ( ! is_numeric( $package_index ) && ! isset( $form_data['shipping_method'][ $package_index ] ) ) { unset( $chosen_shipping_methods[ $package_index ] ); } } WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); } /* Deprecated */ /** * Checks the cart to see if it contains a subscription product renewal. * * Returns the cart_item containing the product renewal, else false. * * @deprecated 2.0 * @since 1.3 */ public static function cart_contains_subscription_renewal( $role = '' ) { _deprecated_function( __METHOD__, '2.0', 'wcs_cart_contains_renewal( $role )' ); return wcs_cart_contains_renewal( $role ); } /** * Checks the cart to see if it contains a subscription product renewal. * * Returns the cart_item containing the product renewal, else false. * * @deprecated 2.0 * @since 1.4 */ public static function cart_contains_failed_renewal_order_payment() { _deprecated_function( __METHOD__, '2.0', 'wcs_cart_contains_failed_renewal_order_payment()' ); return wcs_cart_contains_failed_renewal_order_payment(); } /** * Restore renewal flag when cart is reset and modify Product object with * renewal order related info * * @since 1.3 */ public static function get_cart_item_from_session( $session_data, $values, $key ) { _deprecated_function( __METHOD__, '2.0', 'WCS_Cart_Renewal::get_cart_item_from_session( $session_data, $values, $key )' ); } /** * For subscription renewal via cart, use original order discount * * @since 1.3 */ public static function before_calculate_totals( $cart ) { _deprecated_function( __METHOD__, '2.0', 'WCS_Cart_Renewal::set_renewal_discounts( $cart )' ); } /** * For subscription renewal via cart, previously adjust item price by original order discount * * No longer required as of 1.3.5 as totals are calculated correctly internally. * * @since 1.3 */ public static function get_discounted_price_for_renewal( $price, $values, $cart ) { _deprecated_function( __METHOD__, '2.0', 'WCS_Cart_Renewal::get_discounted_price_for_renewal( $price, $values, $cart )' ); } /** * Returns a string with the cart discount and subscription period. * * @return mixed formatted price or false if there are none * @since 1.2 * @deprecated 2.0 */ public static function get_formatted_discounts_before_tax( $discount, $cart ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $discount; } /** * Gets the order discount amount - these are applied after tax * * @return mixed formatted price or false if there are none * @since 1.2 * @deprecated 2.0 */ public static function get_formatted_discounts_after_tax( $discount, $cart ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $discount; } /** * Returns an individual coupon's formatted discount amount for WooCommerce 2.1+ * * @param string $discount_html String of the coupon's discount amount * @param string $coupon WC_Coupon object for the coupon to which this line item relates * @return string formatted subscription price string if the cart includes a coupon being applied to recurring amount * @since 1.4.6 * @deprecated 2.0 */ public static function cart_coupon_discount_amount_html( $discount_html, $coupon ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $discount_html; } /** * Returns individual coupon's formatted discount amount for WooCommerce 2.1+ * * @param string $discount_html String of the coupon's discount amount * @param string $coupon WC_Coupon object for the coupon to which this line item relates * @return string formatted subscription price string if the cart includes a coupon being applied to recurring amount * @since 1.4.6 * @deprecated 2.0 */ public static function cart_totals_fee_html( $cart_totals_fee_html, $fee ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $cart_totals_fee_html; } /** * Includes the sign-up fee total in the cart total (after calculation). * * @since 1.5.10 * @return string formatted price * @deprecated 2.0 */ public static function get_formatted_cart_total( $cart_contents_total ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $cart_contents_total; } /** * Includes the sign-up fee subtotal in the subtotal displayed in the cart. * * @since 1.2 * @deprecated 2.0 */ public static function get_formatted_cart_subtotal( $cart_subtotal, $compound, $cart ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $cart_subtotal; } /** * Returns an array of taxes merged by code, formatted with recurring amount ready for output. * * @return array Array of tax_id => tax_amounts for items in the cart * @since 1.3.5 * @deprecated 2.0 */ public static function get_recurring_tax_totals( $tax_totals, $cart ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return apply_filters( 'woocommerce_cart_recurring_tax_totals', $tax_totals, $cart ); } /** * Returns a string of the sum of all taxes in the cart for initial payment and * recurring amount. * * @return array Array of tax_id => tax_amounts for items in the cart * @since 1.4.10 * @deprecated 2.0 */ public static function get_taxes_total_html( $total ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $total; } /** * Appends the cart subscription string to a cart total using the @see self::get_cart_subscription_string and then returns it. * * @return string Formatted subscription price string for the cart total. * @since 1.2 * @deprecated 2.0 */ public static function get_formatted_total( $total ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $total; } /** * Appends the cart subscription string to a cart total using the @see self::get_cart_subscription_string and then returns it. * * @return string Formatted subscription price string for the cart total. * @since 1.2 * @deprecated 2.0 */ public static function get_formatted_total_ex_tax( $total_ex_tax ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $total_ex_tax; } /** * Returns an array of the recurring total fields * * @since 1.2 * @deprecated 2.0 */ public static function get_recurring_totals_fields() { _deprecated_function( __METHOD__, '2.0', 'recurring total values stored in WC()->cart->recurring_carts' ); return array(); } /** * Gets the subscription period from the cart and returns it as an array (eg. array( 'month', 'day' ) ) * * Deprecated because a cart can now contain multiple subscription products, so there is no single period for the entire cart. * * @since 1.0 * @deprecated 2.0 */ public static function get_cart_subscription_period() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); if ( self::cart_contains_subscription() ) { foreach ( WC()->cart->cart_contents as $cart_item ) { if ( WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { $period = WC_Subscriptions_Product::get_period( $cart_item['data'] ); break; } } } return apply_filters( 'woocommerce_subscriptions_cart_period', $period ); } /** * Gets the subscription period from the cart and returns it as an array (eg. array( 'month', 'day' ) ) * * Deprecated because a cart can now contain multiple subscription products, so there is no single interval for the entire cart. * * @since 1.0 * @deprecated 2.0 */ public static function get_cart_subscription_interval() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); foreach ( WC()->cart->cart_contents as $cart_item ) { if ( WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { $interval = WC_Subscriptions_Product::get_interval( $cart_item['data'] ); break; } } return apply_filters( 'woocommerce_subscriptions_cart_interval', $interval ); } /** * Gets the subscription length from the cart and returns it as an array (eg. array( 'month', 'day' ) ) * * Deprecated because a cart can now contain multiple subscription products, so there is no single length for the entire cart. * * @since 1.1 * @deprecated 2.0 */ public static function get_cart_subscription_length() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $length = 0; if ( self::cart_contains_subscription() ) { foreach ( WC()->cart->cart_contents as $cart_item ) { if ( WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { $length = WC_Subscriptions_Product::get_length( $cart_item['data'] ); break; } } } return apply_filters( 'woocommerce_subscriptions_cart_length', $length ); } /** * Gets the subscription length from the cart and returns it as an array (eg. array( 'month', 'day' ) ) * * Deprecated because a cart can now contain multiple subscription products, so there is no single trial length for the entire cart. * * @since 1.1 * @deprecated 2.0 */ public static function get_cart_subscription_trial_length() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $trial_length = 0; if ( self::cart_contains_subscription() ) { foreach ( WC()->cart->cart_contents as $cart_item ) { if ( WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { $trial_length = WC_Subscriptions_Product::get_trial_length( $cart_item['data'] ); break; } } } return apply_filters( 'woocommerce_subscriptions_cart_trial_length', $trial_length ); } /** * Gets the subscription trial period from the cart and returns it as an array (eg. array( 'month', 'day' ) ) * * Deprecated because a cart can now contain multiple subscription products, so there is no single trial period for the entire cart. * * @since 1.2 * @deprecated 2.0 */ public static function get_cart_subscription_trial_period() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $trial_period = ''; // Get the original trial period if ( self::cart_contains_subscription() ) { foreach ( WC()->cart->cart_contents as $cart_item ) { if ( WC_Subscriptions_Product::is_subscription( $cart_item['data'] ) ) { $trial_period = WC_Subscriptions_Product::get_trial_period( $cart_item['data'] ); break; } } } return apply_filters( 'woocommerce_subscriptions_cart_trial_period', $trial_period ); } /** * Get tax row amounts with or without compound taxes includes * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return float price * @deprecated 2.0 */ public static function get_recurring_cart_contents_total() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { if ( ! $cart->prices_include_tax ) { $recurring_total += $cart->cart_contents_total; } else { $recurring_total += $cart->cart_contents_total + $cart->tax_total; } } return $recurring_total; } /** * Returns the proportion of cart discount that is recurring for the product specified with $product_id * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring item subtotal amount less tax for items in the cart. * @since 1.2 */ public static function get_recurring_subtotal_ex_tax() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total += $cart->subtotal_ex_tax; } return $recurring_total; } /** * Returns the proportion of cart discount that is recurring for the product specified with $product_id * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring item subtotal amount for items in the cart. * @since 1.2 */ public static function get_recurring_subtotal() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total += $cart->subtotal; } return $recurring_total; } /** * Returns the proportion of cart discount that is recurring for the product specified with $product_id * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring cart discount amount for items in the cart. * @since 1.2 */ public static function get_recurring_discount_cart() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total += $cart->discount_cart; } return $recurring_total; } /** * Returns the cart discount tax amount for WC 2.3 and newer * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double * @since 2.0 */ public static function get_recurring_discount_cart_tax() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total += $cart->discount_cart_tax; } return $recurring_total; } /** * Returns the proportion of total discount that is recurring for the product specified with $product_id * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring discount amount for items in the cart. * @since 1.2 */ public static function get_recurring_discount_total() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total += $cart->discount_total; } return $recurring_total; } /** * Returns the amount of shipping tax that is recurring. As shipping only applies * to recurring payments, and only 1 subscription can be purchased at a time, * this is equal to @see WC_Cart::$shipping_tax_total * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring shipping tax amount for items in the cart. * @since 1.2 */ public static function get_recurring_shipping_tax_total() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total += $cart->shipping_tax_total; } return $recurring_total; } /** * Returns the recurring shipping price . As shipping only applies to recurring * payments, and only 1 subscription can be purchased at a time, this is * equal to @see WC_Cart::shipping_total * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring shipping amount for items in the cart. * @since 1.2 */ public static function get_recurring_shipping_total() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total += $cart->shipping_total; } return $recurring_total; } /** * Returns an array of taxes on an order with their recurring totals. * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return array Array of tax_id => tax_amounts for items in the cart * @since 1.2 */ public static function get_recurring_taxes() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $taxes = array(); $recurring_fees = array(); foreach ( WC()->cart->recurring_carts as $cart ) { foreach ( array_keys( $cart->taxes + $cart->shipping_taxes ) as $key ) { $taxes[ $key ] = ( isset( $cart->shipping_taxes[ $key ] ) ? $cart->shipping_taxes[ $key ] : 0 ) + ( isset( $cart->taxes[ $key ] ) ? $cart->taxes[ $key ] : 0 ); } } return $taxes; } /** * Returns an array of recurring fees. * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return array Array of fee_id => fee_details for items in the cart * @since 1.4.9 */ public static function get_recurring_fees() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_fees = array(); foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_fees = array_merge( $recurring_fees, $cart->get_fees() ); } return $recurring_fees; } /** * Get tax row amounts with or without compound taxes includes * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring tax amount tax for items in the cart (maybe not including compound taxes) * @since 1.2 */ public static function get_recurring_taxes_total( $compound = true ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { foreach ( $cart->taxes as $key => $tax ) { if ( ! $compound && WC_Tax::is_compound( $key ) ) { continue; } $recurring_total += $tax; } foreach ( $cart->shipping_taxes as $key => $tax ) { if ( ! $compound && WC_Tax::is_compound( $key ) ) { continue; } $recurring_total += $tax; } } return $recurring_total; } /** * Returns the proportion of total tax on an order that is recurring for the product specified with $product_id * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring tax amount tax for items in the cart. * @since 1.2 */ public static function get_recurring_total_tax() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total += $cart->tax_total; } return $recurring_total; } /** * Returns the proportion of total before tax on an order that is recurring for the product specified with $product_id * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring amount less tax for items in the cart. * @since 1.2 */ public static function get_recurring_total_ex_tax() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return self::get_recurring_total() - self::get_recurring_total_tax() - self::get_recurring_shipping_tax_total(); } /** * Returns the price per period for a subscription in an order. * * Deprecated because the cart can now contain subscriptions on multiple billing schedules so there is no one "total" * * @return double The total recurring amount for items in the cart. * @since 1.2 */ public static function get_recurring_total() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); $recurring_total = 0; foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total += $cart->get_total(); } return $recurring_total; } /** * Calculate the total amount of recurring shipping needed. Removes any item from the calculation that * is not a subscription and calculates the totals. * * @since 1.5 * @deprecated 2.0 */ public static function calculate_recurring_shipping() { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); foreach ( WC()->cart->recurring_carts as $cart ) { $recurring_total = $cart->shipping_total; } return $recurring_total; } /** * Creates a string representation of the subscription period/term for each item in the cart * * @param string $initial_amount The initial amount to be displayed for the subscription as passed through the @see woocommerce_price() function. * @param float $recurring_amount The price to display in the subscription. * @param array $args (optional) Flags to customise to display the trial and length of the subscription. Default to false - don't display. * @since 1.0 * @deprecated 2.0 */ public static function get_cart_subscription_string( $initial_amount, $recurring_amount, $args = array() ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); if ( ! is_array( $args ) ) { _deprecated_argument( __CLASS__ . '::' . __FUNCTION__, '1.4', 'Third parameter is now an array of name => value pairs. Use array( "include_lengths" => true ) instead.' ); $args = array( 'include_lengths' => $args, ); } $args = wp_parse_args( $args, array( 'include_lengths' => false, 'include_trial' => true, ) ); $subscription_details = array( 'initial_amount' => $initial_amount, 'initial_description' => __( 'now', 'woocommerce-subscriptions' ), 'recurring_amount' => $recurring_amount, 'subscription_interval' => self::get_cart_subscription_interval(), 'subscription_period' => self::get_cart_subscription_period(), 'trial_length' => self::get_cart_subscription_trial_length(), 'trial_period' => self::get_cart_subscription_trial_period(), ); $is_one_payment = ( self::get_cart_subscription_length() > 0 && self::get_cart_subscription_length() == self::get_cart_subscription_interval() ) ? true : false; // Override defaults when subscription is for one billing period if ( $is_one_payment ) { $subscription_details['subscription_length'] = self::get_cart_subscription_length(); } else { if ( true === $args['include_lengths'] ) { $subscription_details['subscription_length'] = self::get_cart_subscription_length(); } if ( false === $args['include_trial'] ) { $subscription_details['trial_length'] = 0; } } $initial_amount_string = ( is_numeric( $subscription_details['initial_amount'] ) ) ? wc_price( $subscription_details['initial_amount'] ) : $subscription_details['initial_amount']; $recurring_amount_string = ( is_numeric( $subscription_details['recurring_amount'] ) ) ? wc_price( $subscription_details['recurring_amount'] ) : $subscription_details['recurring_amount']; // Don't show up front fees when there is no trial period and no sign up fee and they are the same as the recurring amount if ( self::get_cart_subscription_trial_length() == 0 && self::get_cart_subscription_sign_up_fee() == 0 && $initial_amount_string == $recurring_amount_string ) { $subscription_details['initial_amount'] = ''; } elseif ( wc_price( 0 ) == $initial_amount_string && false === $is_one_payment && self::get_cart_subscription_trial_length() > 0 ) { // don't show $0.00 initial amount (i.e. a free trial with no non-subscription products in the cart) unless the recurring period is the same as the billing period $subscription_details['initial_amount'] = ''; } // Include details of a synced subscription in the cart if ( $synchronised_cart_item = WC_Subscriptions_Synchroniser::cart_contains_synced_subscription() ) { $subscription_details += array( 'is_synced' => true, 'synchronised_payment_day' => WC_Subscriptions_Synchroniser::get_products_payment_day( $synchronised_cart_item['data'] ), ); } $subscription_details = apply_filters( 'woocommerce_cart_subscription_string_details', $subscription_details, $args ); $subscription_string = wcs_price_string( $subscription_details ); return $subscription_string; } /** * Uses the a subscription's combined price total calculated by WooCommerce to determine the * total price that should be charged per period. * * @since 1.2 * @deprecated 2.0 */ public static function set_calculated_total( $total ) { _deprecated_function( __METHOD__, '2.0', 'values from WC()->cart->recurring_carts' ); return $total; } /** * Get the recurring amounts values from the session * * @since 1.0 */ public static function get_cart_from_session() { _deprecated_function( __METHOD__, '2.0' ); } /** * Store the sign-up fee cart values in the session * * @since 1.0 */ public static function set_session() { _deprecated_function( __METHOD__, '2.0' ); } /** * Reset the sign-up fee fields in the current session * * @since 1.0 */ public static function reset() { _deprecated_function( __METHOD__, '2.0' ); } /** * Returns a cart item's product ID. For a variation, this will be a variation ID, for a simple product, * it will be the product's ID. * * @since 1.5 */ public static function get_items_product_id( $cart_item ) { _deprecated_function( __METHOD__, '2.0', 'wcs_get_canonical_product_id( $cart_item )' ); return wcs_get_canonical_product_id( $cart_item ); } /** * Store how much discount each coupon grants. * * @param mixed $code * @param mixed $amount * @return void */ public static function increase_coupon_discount_amount( $code, $amount ) { _deprecated_function( __METHOD__, '2.0', 'WC_Subscriptions_Coupon::increase_coupon_discount_amount( WC()->cart, $code, $amount )' ); if ( empty( WC()->cart->coupon_discount_amounts[ $code ] ) ) { WC()->cart->coupon_discount_amounts[ $code ] = 0; } if ( 'recurring_total' != self::$calculation_type ) { WC()->cart->coupon_discount_amounts[ $code ] += $amount; } } /** * Don't display shipping prices if the initial order won't require shipping (i.e. all the products in the cart are subscriptions with a free trial or synchronised to a date in the future) * * @return string Label for a shipping method * @since 1.3 */ public static function get_cart_shipping_method_full_label( $label, $method ) { _deprecated_function( __METHOD__, '2.0.12' ); if ( ! self::charge_shipping_up_front() ) { $label = $method->label; } return $label; } /** * One time shipping can null the need for shipping needs. WooCommerce treats that as no need to ship, therefore it will call * WC()->shipping->reset() on it, which will wipe the preferences saved. That can cause the chosen shipping method for the one * time shipping feature to be lost, and the first default to be applied instead. To counter that, we save the chosen shipping * method to a key that's not going to get wiped by WC's method, and then later restore it. */ public static function maybe_restore_chosen_shipping_method() { $chosen_shipping_method_cache = WC()->session->get( 'wcs_shipping_methods', false ); $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', array() ); if ( false !== $chosen_shipping_method_cache && empty( $chosen_shipping_methods ) ) { WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_method_cache ); } } /** * When WooCommerce calculates rates for a recurring shipping package, previously we would return both a different number * of rates, and a unique set of rates for the recurring shipping package to make sure WooCommerce updated the * chosen method for the recurring cart (and the 'woocommerce_shipping_chosen_method' filter was called, which * we use to make sure the chosen method is the recurring method, not the initial method). * * This is no longer necessary with the introductino of self::reset_shipping_method_counts() which achieves the same thing * via a different means, while allowing WooCommerce's cached rates to be used and avoiding the issue reported in * https://github.com/Prospress/woocommerce-subscriptions/issues/1583 * * This function is hooked to 'woocommerce_package_rates' called by WC_Shipping->calculate_shipping_for_package() * * @param array $package_rates A set of shipping method objects in the form of WC_Shipping_Rate->id => WC_Shipping_Rate with the cost for that rate * @param array $package A shipping package of the form returned by WC_Cart->get_shipping_packages() which includes the package's contents, cost, customer, destination and alternative rates * @since 2.0.12 */ public static function filter_package_rates( $package_rates, $package ) { _deprecated_function( __METHOD__, '2.0.19' ); return $package_rates; } } WC_Subscriptions_Cart::init();