Example #1
0
 /**
  * Move download permissions from original order to the new subscription created for the order.
  *
  * @param WC_Subscription $subscription A subscription object
  * @param int $subscription_item_id ID of the product line item on the subscription
  * @param WC_Order $original_order The original order that was created to purchase the subscription
  * @since 2.0
  */
 private static function migrate_download_permissions($subscription, $subscription_item_id, $order)
 {
     global $wpdb;
     $product_id = wcs_get_canonical_product_id(wcs_get_order_item($subscription_item_id, $subscription));
     $rows_affected = $wpdb->update($wpdb->prefix . 'woocommerce_downloadable_product_permissions', array('order_id' => $subscription->id, 'order_key' => $subscription->order_key), array('order_id' => $order->id, 'order_key' => $order->order_key, 'product_id' => $product_id, 'user_id' => absint($subscription->get_user_id())), array('%d', '%s'), array('%d', '%s', '%d', '%d'));
     WCS_Upgrade_Logger::add(sprintf('For subscription %d: migrated %d download permissions for product %d', $subscription->id, $rows_affected, $product_id));
 }
 /**
  * Checks a given product / coupon combination to determine if the subscription should be discounted
  *
  * @since 1.2
  */
 private static function is_subscription_discountable($cart_item, $coupon)
 {
     $product_cats = wp_get_post_terms($cart_item['product_id'], 'product_cat', array('fields' => 'ids'));
     $this_item_is_discounted = false;
     // Specific products get the discount
     if (sizeof($coupon->product_ids) > 0) {
         if (in_array(wcs_get_canonical_product_id($cart_item), $coupon->product_ids) || in_array($cart_item['data']->get_parent(), $coupon->product_ids)) {
             $this_item_is_discounted = true;
         }
         // Category discounts
     } elseif (sizeof($coupon->product_categories) > 0) {
         if (sizeof(array_intersect($product_cats, $coupon->product_categories)) > 0) {
             $this_item_is_discounted = true;
         }
     } else {
         // No product ids - all items discounted
         $this_item_is_discounted = true;
     }
     // Specific product ID's excluded from the discount
     if (sizeof($coupon->exclude_product_ids) > 0) {
         if (in_array(wcs_get_canonical_product_id($cart_item), $coupon->exclude_product_ids) || in_array($cart_item['data']->get_parent(), $coupon->exclude_product_ids)) {
             $this_item_is_discounted = false;
         }
     }
     // Specific categories excluded from the discount
     if (sizeof($coupon->exclude_product_categories) > 0) {
         if (sizeof(array_intersect($product_cats, $coupon->exclude_product_categories)) > 0) {
             $this_item_is_discounted = false;
         }
     }
     // Apply filter
     return apply_filters('woocommerce_item_is_discounted', $this_item_is_discounted, $cart_item, $before_tax = false);
 }
 /**
  * Returns the billing interval for a each subscription product in an order.
  *
  * For example, this would return 3 for a subscription charged every 3 months or 1 for a subscription charged every month.
  *
  * @param mixed $order A WC_Order object or the ID of the order which the subscription was purchased in.
  * @param int $product_id (optional) The post ID of the subscription WC_Product object purchased in the order. Defaults to the ID of the first product purchased in the order.
  * @return int The billing interval for a each subscription product in an order.
  * @since 1.0
  */
 public static function get_subscription_interval($order, $product_id = '')
 {
     _deprecated_function(__METHOD__, '2.0', 'the billing interval for each individual subscription object. Since Subscriptions v2.0, an order can be used to create multiple different subscriptions with different billing schedules, so use the subscription object');
     $billing_interval = '';
     foreach (wcs_get_subscriptions_for_order($order, array('order_type' => 'parent')) as $subscription) {
         // Find the billing interval for all recurring items
         if (empty($product_id)) {
             $billing_interval = $subscription->billing_interval;
             break;
         } else {
             // We want the billing interval for a specific item (so we need to find if this subscription contains that item)
             foreach ($subscription->get_items() as $line_item) {
                 if (wcs_get_canonical_product_id($line_item) == $product_id) {
                     $billing_interval = $subscription->billing_interval;
                     break 2;
                 }
             }
         }
     }
     return $billing_interval;
 }
 /**
  * When adding an item to an order/subscription via the Add/Edit Subscription administration interface, check if we should be setting
  * the sync meta on the subscription.
  *
  * @param int The order item ID of an item that was just added to the order
  * @param array The order item details
  * @since 2.0
  */
 public static function ajax_maybe_add_meta_for_item($item_id, $item)
 {
     check_ajax_referer('order-item', 'security');
     if (self::is_product_synced(wcs_get_canonical_product_id($item))) {
         self::maybe_add_subscription_meta(absint($_POST['order_id']));
     }
 }
 /**
  * Revoke download permissions granted on the old switch item.
  *
  * @since 2.0.9
  * @param WC_Subscription $subscription
  * @param array $new_item
  * @param array $old_item
  */
 public static function remove_download_permissions_after_switch($subscription, $new_item, $old_item)
 {
     if (!is_object($subscription)) {
         $subscription = wcs_get_subscription($subscription);
     }
     $product_id = wcs_get_canonical_product_id($old_item);
     WCS_Download_Handler::revoke_downloadable_file_permission($product_id, $subscription->id, $subscription->customer_user);
 }
 /**
  * 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);
 }
 /**
  * WooCommerce's function receives the original order ID, the item and the list of files. This does not work for
  * download permissions stored on the subscription rather than the original order as the URL would have the wrong order
  * key. This function takes the same parameters, but queries the database again for download ids belonging to all the
  * subscriptions that were in the original order. Then for all subscriptions, it checks all items, and if the item
  * passed in here is in that subscription, it creates the correct download link to be passsed to the email.
  *
  * @param array $files List of files already included in the list
  * @param array $item An item (you get it by doing $order->get_items())
  * @param WC_Order $order The original order
  * @return array List of files with correct download urls
  */
 public static function get_item_downloads($files, $item, $order)
 {
     global $wpdb;
     if (wcs_order_contains_subscription($order, 'any')) {
         $subscriptions = wcs_get_subscriptions_for_order($order, array('order_type' => array('any')));
     } else {
         return $files;
     }
     $product_id = wcs_get_canonical_product_id($item);
     foreach ($subscriptions as $subscription) {
         foreach ($subscription->get_items() as $subscription_item) {
             if (wcs_get_canonical_product_id($subscription_item) === $product_id) {
                 $files = $subscription->get_item_downloads($subscription_item);
             }
         }
     }
     return $files;
 }
 /**
  * Add a cart item to a subscription.
  *
  * @since 2.0
  */
 public static function add_cart_item($subscription, $cart_item, $cart_item_key)
 {
     $item_id = $subscription->add_product($cart_item['data'], $cart_item['quantity'], array('variation' => $cart_item['variation'], 'totals' => array('subtotal' => $cart_item['line_subtotal'], 'subtotal_tax' => $cart_item['line_subtotal_tax'], 'total' => $cart_item['line_total'], 'tax' => $cart_item['line_tax'], 'tax_data' => $cart_item['line_tax_data'])));
     if (!$item_id) {
         // translators: placeholder is an internal error number
         throw new Exception(sprintf(__('Error %d: Unable to create subscription. Please try again.', 'woocommerce-subscriptions'), 402));
     }
     $cart_item_product_id = 0 != $cart_item['variation_id'] ? $cart_item['variation_id'] : $cart_item['product_id'];
     if (WC_Subscriptions_Product::get_trial_length(wcs_get_canonical_product_id($cart_item)) > 0) {
         wc_add_order_item_meta($item_id, '_has_trial', 'true');
     }
     // Allow plugins to add order item meta
     do_action('woocommerce_add_order_item_meta', $item_id, $cart_item, $cart_item_key);
     do_action('woocommerce_add_subscription_item_meta', $item_id, $cart_item, $cart_item_key);
     return $item_id;
 }
 /**
  * Check if a given line item on the subscription had a sign-up fee, and if so, return the value of the sign-up fee.
  *
  * The single quantity sign-up fee will be returned instead of the total sign-up fee paid. For example, if 3 x a product
  * with a 10 BTC sign-up fee was purchased, a total 30 BTC was paid as the sign-up fee but this function will return 10 BTC.
  *
  * @param array|int Either an order item (in the array format returned by self::get_items()) or the ID of an order item.
  * @return bool
  * @since 2.0
  */
 public function get_items_sign_up_fee($line_item)
 {
     if (!is_array($line_item)) {
         $line_item = wcs_get_order_item($line_item, $this);
     }
     // If there was no original order, nothing was paid up-front which means no sign-up fee
     if (empty($this->order)) {
         $sign_up_fee = 0;
     } else {
         $original_order_item = '';
         // Find the matching item on the order
         foreach ($this->order->get_items() as $order_item) {
             if (wcs_get_canonical_product_id($line_item) == wcs_get_canonical_product_id($order_item)) {
                 $original_order_item = $order_item;
                 break;
             }
         }
         // No matching order item, so this item wasn't purchased in the original order
         if (empty($original_order_item)) {
             $sign_up_fee = 0;
         } elseif (isset($line_item['item_meta']['_has_trial'])) {
             // Sign up was was total amount paid for this item on original order
             $sign_up_fee = $original_order_item['line_total'] / $original_order_item['qty'];
         } else {
             // Sign-up fee is any amount on top of recurring amount
             $sign_up_fee = max($original_order_item['line_total'] / $original_order_item['qty'] - $line_item['line_total'] / $line_item['qty'], 0);
         }
     }
     return apply_filters('woocommerce_subscription_items_sign_up_fee', $sign_up_fee, $line_item, $this);
 }
 /**
  * Check if a given line item on the subscription had a sign-up fee, and if so, return the value of the sign-up fee.
  *
  * The single quantity sign-up fee will be returned instead of the total sign-up fee paid. For example, if 3 x a product
  * with a 10 BTC sign-up fee was purchased, a total 30 BTC was paid as the sign-up fee but this function will return 10 BTC.
  *
  * @param array|int Either an order item (in the array format returned by self::get_items()) or the ID of an order item.
  * @param  string $tax_inclusive_or_exclusive Whether or not to adjust sign up fee if prices inc tax - ensures that the sign up fee paid amount includes the paid tax if inc
  * @return bool
  * @since 2.0
  */
 public function get_items_sign_up_fee($line_item, $tax_inclusive_or_exclusive = 'exclusive_of_tax')
 {
     if (!is_array($line_item)) {
         $line_item = wcs_get_order_item($line_item, $this);
     }
     // If there was no original order, nothing was paid up-front which means no sign-up fee
     if (empty($this->order)) {
         $sign_up_fee = 0;
     } else {
         $original_order_item = '';
         // Find the matching item on the order
         foreach ($this->order->get_items() as $order_item) {
             if (wcs_get_canonical_product_id($line_item) == wcs_get_canonical_product_id($order_item)) {
                 $original_order_item = $order_item;
                 break;
             }
         }
         // No matching order item, so this item wasn't purchased in the original order
         if (empty($original_order_item)) {
             $sign_up_fee = 0;
         } elseif (isset($line_item['item_meta']['_has_trial'])) {
             // Sign up was was total amount paid for this item on original order
             $sign_up_fee = $original_order_item['line_total'] / $original_order_item['qty'];
         } else {
             // Sign-up fee is any amount on top of recurring amount
             $sign_up_fee = max($original_order_item['line_total'] / $original_order_item['qty'] - $line_item['line_total'] / $line_item['qty'], 0);
         }
         // If prices inc tax, ensure that the sign up fee amount includes the tax
         if ('inclusive_of_tax' === $tax_inclusive_or_exclusive && !empty($original_order_item) && 'yes' == $this->prices_include_tax) {
             $proportion = $sign_up_fee / ($original_order_item['line_total'] / $original_order_item['qty']);
             $sign_up_fee += round($original_order_item['line_tax'] * $proportion, 2);
         }
     }
     return apply_filters('woocommerce_subscription_items_sign_up_fee', $sign_up_fee, $line_item, $this, $tax_inclusive_or_exclusive);
 }
 /**
  * Check if a subscription was created prior to 2.0.0 and has some dates that need to be updated
  * because the meta was borked during the 2.0.0 upgrade process. If it does, then update the dates
  * to the new values.
  *
  * @return bool true if the subscription was repaired, otherwise false
  */
 protected static function maybe_repair_subscription($subscription)
 {
     $repaired_subscription = false;
     // if the subscription doesn't have an order, it must have been created in 2.0, so we can ignore it
     if (false === $subscription->order) {
         WCS_Upgrade_Logger::add(sprintf('For subscription %d: no need to repair: it has no order.', $subscription->id));
         return $repaired_subscription;
     }
     $subscription_line_items = $subscription->get_items();
     // if the subscription has more than one line item, it must have been created in 2.0, so we can ignore it
     if (count($subscription_line_items) > 1) {
         WCS_Upgrade_Logger::add(sprintf('For subscription %d: no need to repair: it has more than one line item.', $subscription->id));
         return $repaired_subscription;
     }
     $subscription_line_item_id = key($subscription_line_items);
     $subscription_line_item = array_shift($subscription_line_items);
     // Get old order item's meta
     foreach ($subscription->order->get_items() as $line_item_id => $line_item) {
         if (wcs_get_canonical_product_id($line_item) == wcs_get_canonical_product_id($subscription_line_item)) {
             $matching_line_item_id = $line_item_id;
             $matching_line_item = $line_item;
             break;
         }
     }
     // we couldn't find a matching line item so we can't repair it
     if (!isset($matching_line_item)) {
         WCS_Upgrade_Logger::add(sprintf('For subscription %d: can not repair: it has no matching line item.', $subscription->id));
         return $repaired_subscription;
     }
     $matching_line_item_meta = $matching_line_item['item_meta'];
     // if the order item doesn't have migrated subscription data, the subscription wasn't migrated from 1.5
     if (!isset($matching_line_item_meta['_wcs_migrated_subscription_status']) && !isset($matching_line_item_meta['_wcs_migrated_subscription_start_date'])) {
         WCS_Upgrade_Logger::add(sprintf('For subscription %d: no need to repair: matching line item has no migrated meta data.', $subscription->id));
         return $repaired_subscription;
     }
     if (false !== self::maybe_repair_line_tax_data($subscription_line_item_id, $matching_line_item_id, $matching_line_item)) {
         WCS_Upgrade_Logger::add(sprintf('For subscription %d: repaired missing line tax data.', $subscription->id));
         $repaired_subscription = true;
     } else {
         WCS_Upgrade_Logger::add(sprintf('For subscription %d: line tax data not added.', $subscription->id));
     }
     // if the subscription has been cancelled, we don't need to repair any other data
     if ($subscription->has_status(array('pending-cancel', 'cancelled'))) {
         WCS_Upgrade_Logger::add(sprintf('For subscription %d: no need to repair: it has cancelled status.', $subscription->id));
         return $repaired_subscription;
     }
     $dates_to_update = array();
     if (false !== ($repair_date = self::check_trial_end_date($subscription, $matching_line_item_meta))) {
         $dates_to_update['trial_end'] = $repair_date;
     }
     if (false !== ($repair_date = self::check_next_payment_date($subscription))) {
         $dates_to_update['next_payment'] = $repair_date;
     }
     if (false !== ($repair_date = self::check_end_date($subscription, $matching_line_item_meta))) {
         $dates_to_update['end'] = $repair_date;
     }
     if (!empty($dates_to_update)) {
         WCS_Upgrade_Logger::add(sprintf('For subscription %d: repairing dates = %s', $subscription->id, str_replace(array('{', '}', '"'), '', wcs_json_encode($dates_to_update))));
         try {
             $subscription->update_dates($dates_to_update);
             WCS_Upgrade_Logger::add(sprintf('For subscription %d: repaired dates = %s', $subscription->id, str_replace(array('{', '}', '"'), '', wcs_json_encode($dates_to_update))));
         } catch (Exception $e) {
             WCS_Upgrade_Logger::add(sprintf('!! For subscription %d: unable to repair dates (%s), exception "%s"', $subscription->id, str_replace(array('{', '}', '"'), '', wcs_json_encode($dates_to_update)), $e->getMessage()));
         }
         try {
             self::maybe_repair_status($subscription, $matching_line_item_meta, $dates_to_update);
         } catch (Exception $e) {
             WCS_Upgrade_Logger::add(sprintf('!! For subscription %d: unable to repair status. Exception: "%s"', $subscription->id, $e->getMessage()));
         }
         $repaired_subscription = true;
     }
     if (!empty($subscription->order->customer_note) && empty($subscription->customer_note)) {
         $post_data = array('ID' => $subscription->id, 'post_excerpt' => $subscription->order->customer_note);
         $updated_post_id = wp_update_post($post_data, true);
         if (!is_wp_error($updated_post_id)) {
             WCS_Upgrade_Logger::add(sprintf('For subscription %d: repaired missing customer note.', $subscription->id));
             $repaired_subscription = true;
         } else {
             WCS_Upgrade_Logger::add(sprintf('!! For subscription %d: unable to repair missing customer note. Exception: "%s"', $subscription->id, $updated_post_id->get_error_message()));
         }
     }
     return $repaired_subscription;
 }
 /**
  * Set the subscription prices to be used in calculating totals by @see WC_Subscriptions_Cart::calculate_subscription_totals()
  *
  * @since 2.0
  */
 public static function calculate_prorated_totals($cart)
 {
     if (false === self::cart_contains_switches()) {
         return;
     }
     // Maybe charge an initial amount to account for upgrading from a cheaper subscription
     $apportion_recurring_price = get_option(WC_Subscriptions_Admin::$option_prefix . '_apportion_recurring_price', 'no');
     $apportion_sign_up_fee = get_option(WC_Subscriptions_Admin::$option_prefix . '_apportion_sign_up_fee', 'no');
     $apportion_length = get_option(WC_Subscriptions_Admin::$option_prefix . '_apportion_length', 'no');
     foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
         if (!isset($cart_item['subscription_switch']['subscription_id'])) {
             continue;
         }
         $subscription = wcs_get_subscription($cart_item['subscription_switch']['subscription_id']);
         $existing_item = wcs_get_order_item($cart_item['subscription_switch']['item_id'], $subscription);
         if (empty($existing_item)) {
             WC()->cart->remove_cart_item($cart_item_key);
             continue;
         }
         $item_data = $cart_item['data'];
         $product_id = wcs_get_canonical_product_id($cart_item);
         $product = get_product($product_id);
         $is_virtual_product = $product->is_virtual();
         // Set when the first payment and end date for the new subscription should occur
         WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['first_payment_timestamp'] = $cart_item['subscription_switch']['next_payment_timestamp'];
         WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['end_timestamp'] = $end_timestamp = strtotime(WC_Subscriptions_Product::get_expiration_date($product_id, $subscription->get_date('last_payment')));
         // Add any extra sign up fees required to switch to the new subscription
         if ('yes' == $apportion_sign_up_fee) {
             // Because product add-ons etc. don't apply to sign-up fees, it's safe to use the product's sign-up fee value rather than the cart item's
             $sign_up_fee_due = $product->subscription_sign_up_fee;
             $sign_up_fee_paid = $subscription->get_items_sign_up_fee($existing_item);
             // Make sure total prorated sign-up fee is prorated across total amount of sign-up fee so that customer doesn't get extra discounts
             if ($cart_item['quantity'] > $existing_item['qty']) {
                 $sign_up_fee_paid = $sign_up_fee_paid * $existing_item['qty'] / $cart_item['quantity'];
             }
             WC()->cart->cart_contents[$cart_item_key]['data']->subscription_sign_up_fee = max($sign_up_fee_due - $sign_up_fee_paid, 0);
         } elseif ('no' == $apportion_sign_up_fee) {
             // $0 the initial sign-up fee
             WC()->cart->cart_contents[$cart_item_key]['data']->subscription_sign_up_fee = 0;
         }
         // Get the current subscription's last payment date
         $last_payment_timestamp = $subscription->get_time('last_payment');
         $days_since_last_payment = floor((gmdate('U') - $last_payment_timestamp) / (60 * 60 * 24));
         // Get the current subscription's next payment date
         $next_payment_timestamp = $cart_item['subscription_switch']['next_payment_timestamp'];
         $days_until_next_payment = ceil(($next_payment_timestamp - gmdate('U')) / (60 * 60 * 24));
         // Find the number of days between the two
         $days_in_old_cycle = $days_until_next_payment + $days_since_last_payment;
         // Find the actual recurring amount charged for the old subscription (we need to use the '_recurring_line_total' meta here rather than '_subscription_recurring_amount' because we want the recurring amount to include extra from extensions, like Product Add-ons etc.)
         $old_recurring_total = $existing_item['line_total'];
         if ('yes' == $subscription->prices_include_tax || true === $subscription->prices_include_tax) {
             // WC_Abstract_Order::$prices_include_tax can be set to true in __construct() or to 'yes' in populate()
             $old_recurring_total += $existing_item['line_tax'];
         }
         // Find the $price per day for the old subscription's recurring total
         $old_price_per_day = $old_recurring_total / $days_in_old_cycle;
         // Find the price per day for the new subscription's recurring total
         // If the subscription uses the same billing interval & cycle as the old subscription,
         if ($item_data->subscription_period == $subscription->billing_period && $item_data->subscription_period_interval == $subscription->billing_interval) {
             $days_in_new_cycle = $days_in_old_cycle;
             // Use $days_in_old_cycle to make sure they're consistent
         } else {
             // We need to figure out the price per day for the new subscription based on its billing schedule
             switch ($item_data->subscription_period) {
                 case 'day':
                     $days_in_new_cycle = $item_data->subscription_period_interval;
                     break;
                 case 'week':
                     $days_in_new_cycle = $item_data->subscription_period_interval * 7;
                     break;
                 case 'month':
                     $days_in_new_cycle = $item_data->subscription_period_interval * 30.4375;
                     // Average days per month over 4 year period
                     break;
                 case 'year':
                     $days_in_new_cycle = $item_data->subscription_period_interval * 365.25;
                     // Average days per year over 4 year period
                     break;
             }
         }
         // We need to use the cart items price to ensure we include extras added by extensions like Product Add-ons
         $new_price_per_day = $item_data->price * $cart_item['quantity'] / $days_in_new_cycle;
         if ($old_price_per_day < $new_price_per_day) {
             WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['upgraded_or_downgraded'] = 'upgraded';
         } elseif ($old_price_per_day > $new_price_per_day && $new_price_per_day > 0) {
             WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['upgraded_or_downgraded'] = 'downgraded';
         }
         // Now lets see if we should add a prorated amount to the sign-up fee (for upgrades) or extend the next payment date (for downgrades)
         if (in_array($apportion_recurring_price, array('yes', 'yes-upgrade')) || in_array($apportion_recurring_price, array('virtual', 'virtual-upgrade')) && $is_virtual_product) {
             // If the customer is upgrading, we may need to add a gap payment to the sign-up fee or to reduce the pre-paid period (or both)
             if ($old_price_per_day < $new_price_per_day) {
                 // The new subscription may be more expensive, but it's also on a shorter billing cycle, so reduce the next pre-paid term
                 if ($days_in_old_cycle > $days_in_new_cycle) {
                     // Find out how many days at the new price per day the customer would receive for the total amount already paid
                     // (e.g. if the customer paid $10 / month previously, and was switching to a $5 / week subscription, she has pre-paid 14 days at the new price)
                     $pre_paid_days = 0;
                     do {
                         $pre_paid_days++;
                         $new_total_paid = $pre_paid_days * $new_price_per_day;
                     } while ($new_total_paid < $old_recurring_total);
                     // If the total amount the customer has paid entitles her to more days at the new price than she has received, there is no gap payment, just shorten the pre-paid term the appropriate number of days
                     if ($days_since_last_payment < $pre_paid_days) {
                         WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['first_payment_timestamp'] = $last_payment_timestamp + $pre_paid_days * 60 * 60 * 24;
                         // If the total amount the customer has paid entitles her to the same or less days at the new price then start the new subscription from today
                     } else {
                         WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['first_payment_timestamp'] = 0;
                     }
                 } else {
                     $extra_to_pay = $days_until_next_payment * ($new_price_per_day - $old_price_per_day);
                     // when calculating a subscription with one length (no more next payment date and the end date may have been pushed back) we need to pay for those extra days at the new price per day between the old next payment date and new end date
                     if (1 == $item_data->subscription_length) {
                         $days_to_new_end = floor(($end_timestamp - $next_payment_timestamp) / (60 * 60 * 24));
                         if ($days_to_new_end > 0) {
                             $extra_to_pay += $days_to_new_end * $new_price_per_day;
                         }
                     }
                     // We need to find the per item extra to pay so we can set it as the sign-up fee (WC will then multiply it by the quantity)
                     $extra_to_pay = $extra_to_pay / $cart_item['quantity'];
                     // Keep a record of the two separate amounts so we store these and calculate future switch amounts correctly
                     WC()->cart->cart_contents[$cart_item_key]['data']->subscription_sign_up_fee_prorated = WC()->cart->cart_contents[$cart_item_key]['data']->subscription_sign_up_fee;
                     WC()->cart->cart_contents[$cart_item_key]['data']->subscription_price_prorated = round($extra_to_pay, 2);
                     WC()->cart->cart_contents[$cart_item_key]['data']->subscription_sign_up_fee += round($extra_to_pay, 2);
                 }
                 // If the customer is downgrading, set the next payment date and maybe extend it if downgrades are prorated
             } elseif ($old_price_per_day > $new_price_per_day && $new_price_per_day > 0) {
                 $old_total_paid = $old_price_per_day * $days_until_next_payment;
                 $new_total_paid = $new_price_per_day;
                 // if downgrades are apportioned, extend the next payment date for n more days
                 if (in_array($apportion_recurring_price, array('virtual', 'yes'))) {
                     // Find how many more days at the new lower price it takes to exceed the amount already paid
                     for ($days_to_add = 0; $new_total_paid <= $old_total_paid; $days_to_add++) {
                         $new_total_paid = $days_to_add * $new_price_per_day;
                     }
                     $days_to_add -= $days_until_next_payment;
                 } else {
                     $days_to_add = 0;
                 }
                 WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['first_payment_timestamp'] = $next_payment_timestamp + $days_to_add * 60 * 60 * 24;
             }
             // The old price per day == the new price per day, no need to change anything
             if (WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['first_payment_timestamp'] != $cart_item['subscription_switch']['next_payment_timestamp']) {
                 WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['recurring_payment_prorated'] = true;
             }
         }
         // Finally, if we need to make sure the initial total doesn't include any recurring amount, we can by spoofing a free trial
         if (0 != WC()->cart->cart_contents[$cart_item_key]['subscription_switch']['first_payment_timestamp']) {
             WC()->cart->cart_contents[$cart_item_key]['data']->subscription_trial_length = 1;
         }
         if ('yes' == $apportion_length || 'virtual' == $apportion_length && $is_virtual_product) {
             $base_length = WC_Subscriptions_Product::get_length($product_id);
             $completed_payments = $subscription->get_completed_payment_count();
             $length_remaining = $base_length - $completed_payments;
             // Default to the base length if more payments have already been made than this subscription requires
             if ($length_remaining <= 0) {
                 $length_remaining = $base_length;
             }
             WC()->cart->cart_contents[$cart_item_key]['data']->subscription_length = $length_remaining;
         }
     }
 }
 /**
  * Process the remove or re-add a line item from a subscription request.
  *
  * @since 2.0
  */
 public static function maybe_remove_or_add_item_to_subscription()
 {
     if (isset($_GET['subscription_id']) && (isset($_GET['remove_item']) || isset($_GET['undo_remove_item'])) && isset($_GET['_wpnonce'])) {
         $subscription = wcs_is_subscription($_GET['subscription_id']) ? wcs_get_subscription($_GET['subscription_id']) : false;
         $undo_request = isset($_GET['undo_remove_item']) ? true : false;
         $item_id = $undo_request ? $_GET['undo_remove_item'] : $_GET['remove_item'];
         if (false === $subscription) {
             wc_add_notice(sprintf(_x('Subscription #%d does not exist.', 'hash before subscription ID', 'woocommerce-subscriptions'), $_GET['subscription_id']), 'error');
             wp_safe_redirect(wc_get_page_permalink('myaccount'));
             exit;
         }
         if (self::validate_remove_items_request($subscription, $item_id, $undo_request)) {
             if ($undo_request) {
                 // handle undo request
                 $removed_item = WC()->session->get('removed_subscription_items', array());
                 if (!empty($removed_item[$item_id]) && $subscription->id == $removed_item[$item_id]) {
                     // restore the item
                     wc_update_order_item($item_id, array('order_item_type' => 'line_item'));
                     unset($removed_item[$item_id]);
                     WC()->session->set('removed_subscription_items', $removed_item);
                     // restore download permissions for this item
                     $line_items = $subscription->get_items();
                     $line_item = $line_items[$item_id];
                     $_product = $subscription->get_product_from_item($line_item);
                     $product_id = wcs_get_canonical_product_id($line_item);
                     if ($_product && $_product->exists() && $_product->is_downloadable()) {
                         $downloads = $_product->get_files();
                         foreach (array_keys($downloads) as $download_id) {
                             wc_downloadable_file_permission($download_id, $product_id, $subscription, $line_item['qty']);
                         }
                     }
                     // translators: 1$: product name, 2$: product id
                     $subscription->add_order_note(sprintf(_x('Customer added "%1$s" (Product ID: #%2$d) via the My Account page.', 'used in order note', 'woocommerce-subscriptions'), wcs_get_line_item_name($line_item), $product_id));
                 } else {
                     wc_add_notice(__('Your request to undo your previous action was unsuccessful.', 'woocommerce-subscriptions'));
                 }
             } else {
                 // handle remove item requests
                 WC()->session->set('removed_subscription_items', array($item_id => $subscription->id));
                 // remove download access for the item
                 $line_items = $subscription->get_items();
                 $line_item = $line_items[$item_id];
                 $product_id = wcs_get_canonical_product_id($line_item);
                 WCS_Download_Handler::revoke_downloadable_file_permission($product_id, $subscription->id, $subscription->get_user_id());
                 // remove the line item from subscription but preserve its data in the DB
                 wc_update_order_item($item_id, array('order_item_type' => 'line_item_removed'));
                 // translators: 1$: product name, 2$: product id
                 $subscription->add_order_note(sprintf(_x('Customer removed "%1$s" (Product ID: #%2$d) via the My Account page.', 'used in order note', 'woocommerce-subscriptions'), wcs_get_line_item_name($line_item), $product_id));
                 // translators: placeholders are 1$: item name, and, 2$: opening and, 3$: closing link tags
                 wc_add_notice(sprintf(__('You have successfully removed "%1$s" from your subscription. %2$sUndo?%3$s', 'woocommerce-subscriptions'), $line_item['name'], '<a href="' . esc_url(self::get_undo_remove_url($subscription->id, $item_id, $subscription->get_view_order_url())) . '" >', '</a>'));
             }
         }
         $subscription->calculate_totals();
         wp_safe_redirect($subscription->get_view_order_url());
         exit;
     }
 }
 /**
  * Creates a 2.0 updated version of the "subscriptions_switched" callback for developers to hook onto.
  *
  * The subscription passed to the new `woocommerce_subscriptions_switched_item` callback is strictly the subscription
  * to which the `$new_order_item` belongs to; this may be a new or the original subscription.
  *
  * @since 2.0.5
  * @param WC_Order $order
  */
 public static function maybe_add_switched_callback($order)
 {
     if (wcs_order_contains_switch($order)) {
         $subscriptions = wcs_get_subscriptions_for_order($order);
         foreach ($subscriptions as $subscription) {
             foreach ($subscription->get_items() as $new_order_item) {
                 if (isset($new_order_item['switched_subscription_item_id'])) {
                     $product_id = wcs_get_canonical_product_id($new_order_item);
                     // we need to check if the switch order contains the line item that has just been switched so that we don't call the hook on items that were previously switched in another order
                     foreach ($order->get_items() as $order_item) {
                         if (wcs_get_canonical_product_id($order_item) == $product_id) {
                             do_action('woocommerce_subscriptions_switched_item', $subscription, $new_order_item, WC_Subscriptions_Order::get_item_by_id($new_order_item['switched_subscription_item_id']));
                             break;
                         }
                     }
                 }
             }
         }
     }
 }