/** * Takes a subscription key and calculates the date on which the subscription is scheduled to expire * or 0 if it will never expire. * * @param $subscription_key string A subscription key of the form created by @see self::get_subscription_key() * @param $user_id int The ID of the user who owns the subscriptions. Although this parameter is optional, if you have the User ID you should pass it to improve performance. * @param $type string (optional) The format for the Either 'mysql' or 'timestamp'. * @since 1.1 */ public static function calculate_trial_expiration_date($subscription_key, $user_id = '', $type = 'mysql') { $subscription = self::get_subscription($subscription_key, $user_id); if (empty($subscription)) { $expiration_date = 0; } else { $order = new WC_Order($subscription['order_id']); $subscription_trial_length = WC_Subscriptions_Order::get_subscription_trial_length($order, $subscription['product_id']); $subscription_trial_period = WC_Subscriptions_Order::get_subscription_trial_period($order, $subscription['product_id']); if ($subscription_trial_length > 0) { if (isset($subscription['start_date']) && !empty($subscription['start_date'])) { $start_date = $subscription['start_date']; } elseif (!empty($order->order_date)) { $start_date = get_gmt_from_date($order->order_date); } else { $start_date = gmdate('Y-m-d H:i:s'); } if ('month' == $subscription_trial_period) { $expiration_date = WC_Subscriptions::add_months(strtotime($start_date), $subscription_trial_length); } else { // Safe to just add the billing periods $expiration_date = strtotime("+ {$subscription_trial_length} {$subscription_trial_period}", strtotime($start_date)); } } else { $expiration_date = 0; } } $expiration_date = 'mysql' == $type && 0 != $expiration_date ? date('Y-m-d H:i:s', $expiration_date) : $expiration_date; return apply_filters('woocommerce_subscription_calculated_trial_expiration_date', $expiration_date, $subscription_key, $user_id); }
/** * Calculate the first payment date for a synced subscription. * * The date is calculated in UTC timezone. * * @param WC_Product $product A subscription product. * @param string $type (optional) The format to return the first payment date in, either 'mysql' or 'timestamp'. Default 'mysql'. * @param string $from_date (optional) The date to calculate the first payment from in GMT/UTC timzeone. If not set, it will use the current date. This should not include any trial period on the product. * @since 1.5 */ public static function calculate_first_payment_date($product, $type = 'mysql', $from_date = '') { if (!is_object($product)) { $product = WC_Subscriptions::get_product($product); } if (!self::is_product_synced($product)) { return 0; } $period = WC_Subscriptions_Product::get_period($product); $trial_period = WC_Subscriptions_Product::get_trial_period($product); $trial_length = WC_Subscriptions_Product::get_trial_length($product); $from_date_param = $from_date; if (empty($from_date)) { $from_date = gmdate('Y-m-d H:i:s'); } // If the subscription has a free trial period, the first payment should be synced to a day after the free trial if ($trial_length > 0) { $from_date = WC_Subscriptions_Product::get_trial_expiration_date($product, $from_date); } $from_timestamp = strtotime($from_date) + get_option('gmt_offset') * 3600; // Site time $payment_day = self::get_products_payment_day($product); if ('week' == $period) { // strtotime() only handles English, so can't use $wp_locale->weekday here $weekdays = array(1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', 7 => 'Sunday'); // strtotime() will figure out if the day is in the future or today (see: https://gist.github.com/thenbrent/9698083) $first_payment_timestamp = strtotime($weekdays[$payment_day], $from_timestamp); } elseif ('month' == $period) { // strtotime() needs to know the month, so we need to determine if the specified day has occured this month yet or if we want the last day of the month (see: https://gist.github.com/thenbrent/9698083) if ($payment_day > 27) { // we actually want the last day of the month $payment_day = gmdate('t', $from_timestamp); $month = gmdate('F', $from_timestamp); } elseif (gmdate('j', $from_timestamp) > $payment_day) { // today is later than specified day in the from date, we need the next month $month = date('F', WC_Subscriptions::add_months($from_timestamp, 1)); } else { // specified day is either today or still to come in the month of the from date $month = gmdate('F', $from_timestamp); } $first_payment_timestamp = strtotime("{$payment_day} {$month}", $from_timestamp); } elseif ('year' == $period) { // We can't use $wp_locale here because it is translated switch ($payment_day['month']) { case 1: $month = 'January'; break; case 2: $month = 'February'; break; case 3: $month = 'March'; break; case 4: $month = 'April'; break; case 5: $month = 'May'; break; case 6: $month = 'June'; break; case 7: $month = 'July'; break; case 8: $month = 'August'; break; case 9: $month = 'September'; break; case 10: $month = 'October'; break; case 11: $month = 'November'; break; case 12: $month = 'December'; break; } $first_payment_timestamp = strtotime("{$payment_day['day']} {$month}", $from_timestamp); } // Make sure the next payment is in the future and after the $from_date, as strtotime() will return the date this year for any day in the past when adding months or years (see: https://gist.github.com/thenbrent/9698083) if ('year' == $period || 'month' == $period) { // First make sure the day is in the past so that we don't end up jumping a month or year because of a few hours difference between now and the billing date if (gmdate('j', $first_payment_timestamp) < gmdate('j') && gmdate('n', $first_payment_timestamp) <= gmdate('n') && gmdate('Y', $first_payment_timestamp) <= gmdate('Y')) { $i = 1; // Then make sure the date and time of the payment is in the future while (($first_payment_timestamp < gmdate('U') || $first_payment_timestamp < $from_timestamp) && $i < 30) { $first_payment_timestamp = strtotime("+ 1 {$period}", $first_payment_timestamp); $i = $i + 1; } } } // We calculated a timestamp for midnight on the specific day in the site's timezone, let's push it to 3am to account for any daylight savings changes $first_payment_timestamp += 3 * HOUR_IN_SECONDS; // And convert it to the UTC equivalent of 3am on that day $first_payment_timestamp -= get_option('gmt_offset') * HOUR_IN_SECONDS; $first_payment = 'mysql' == $type && 0 != $first_payment_timestamp ? date('Y-m-d H:i:s', $first_payment_timestamp) : $first_payment_timestamp; return apply_filters('woocommerce_subscriptions_synced_first_payment_date', $first_payment, $product, $type, $from_date, $from_date_param); }
/** * Takes a subscription product's ID and returns the date on which the subscription trial will expire, * based on the subscription's trial length and calculated from either the $from_date if specified, * or the current date/time. * * @param int $product_id The product/post ID of the subscription * @param mixed $from_date A MySQL formatted date/time string from which to calculate the expiration date (in UTC timezone), or empty (default), which will use today's date/time (in UTC timezone). * @since 1.0 */ public static function get_trial_expiration_date($product_id, $from_date = '') { $trial_period = self::get_trial_period($product_id); $trial_length = self::get_trial_length($product_id); if ($trial_length > 0) { if (empty($from_date)) { $from_date = gmdate('Y-m-d H:i:s'); } if ('month' == $trial_period) { $trial_expiration_date = date('Y-m-d H:i:s', WC_Subscriptions::add_months(strtotime($from_date), $trial_length)); } else { // Safe to just add the billing periods $trial_expiration_date = date('Y-m-d H:i:s', strtotime("+ {$trial_length} {$trial_period}s", strtotime($from_date))); } } else { $trial_expiration_date = 0; } return apply_filters('woocommerce_subscriptions_product_trial_expiration_date', $trial_expiration_date, $product_id, $from_date); }
/** * Takes a subscription product's ID and calculates the date on which the next payment is due. * * Calculation is based on $from_date if specified, otherwise it will fall back to the last * completed payment, the subscription's start time, or the current date/time, in that order. * * The next payment date will occur after any free trial period and up to any expiration date. * * @param mixed $order A WC_Order object or the ID of the order which the subscription was purchased in. * @param int $product_id The product/post ID of the subscription * @param string $type (optional) The format for the Either 'mysql' or 'timestamp'. * @param mixed $from_date A MySQL formatted date/time string from which to calculate the next payment date, or empty (default), which will use the last payment on the subscription, or today's date/time if no previous payments have been made. * @return mixed If there is no future payment set, returns 0, otherwise it will return a date of the next payment in the form specified by $type * @since 1.0 */ public static function calculate_next_payment_date($order, $product_id, $type = 'mysql', $from_date = '') { if (!is_object($order)) { $order = new WC_Order($order); } $from_date_arg = $from_date; $subscription = WC_Subscriptions_Manager::get_subscription(WC_Subscriptions_Manager::get_subscription_key($order->id, $product_id)); $subscription_period = self::get_subscription_period($order, $product_id); $subscription_interval = self::get_subscription_interval($order, $product_id); $subscription_trial_length = self::get_subscription_trial_length($order, $product_id); $subscription_trial_period = self::get_subscription_trial_period($order, $product_id); $subscription_length = self::get_subscription_length($order, $product_id); $trial_end_time = !empty($subscription['trial_expiry_date']) ? $subscription['trial_expiry_date'] : WC_Subscriptions_Product::get_trial_expiration_date($product_id, get_gmt_from_date($order->order_date)); $trial_end_time = strtotime($trial_end_time); // If the subscription is not active, there is no next payment date if ($subscription['status'] != 'active' || $subscription_interval == $subscription_length) { $next_payment_timestamp = 0; // If the subscription has a free trial period, and we're still in the free trial period, the next payment is due at the end of the free trial } elseif ($subscription_trial_length > 0 && $trial_end_time > gmdate('U') + 60 * 60 * 23 + 120) { // Make sure trial expiry is more than 23+ hours in the future to account for trial expiration dates incorrectly stored in non-UTC/GMT timezone and also for any potential changes to the site's timezone $next_payment_timestamp = $trial_end_time; // The next payment date is {interval} billing periods from the from date } else { // We have a timestamp if (!empty($from_date) && is_numeric($from_date)) { $from_date = date('Y-m-d H:i:s', $from_date); } if (empty($from_date)) { if (!empty($subscription['completed_payments'])) { $from_date = array_pop($subscription['completed_payments']); $add_failed_payments = true; } else { if (!empty($subscription['start_date'])) { $from_date = $subscription['start_date']; $add_failed_payments = true; } else { $from_date = gmdate('Y-m-d H:i:s'); $add_failed_payments = false; } } $failed_payment_count = self::get_failed_payment_count($order, $product_id); // Maybe take into account any failed payments if (true === $add_failed_payments && $failed_payment_count > 0) { $failed_payment_periods = $failed_payment_count * $subscription_interval; $from_timestamp = strtotime($from_date); if ('month' == $subscription_period) { $from_date = date('Y-m-d H:i:s', WC_Subscriptions::add_months($from_timestamp, $failed_payment_periods)); } else { // Safe to just add the billing periods $from_date = date('Y-m-d H:i:s', strtotime("+ {$failed_payment_periods} {$subscription_period}", $from_timestamp)); } } } $from_timestamp = strtotime($from_date); if ('month' == $subscription_period) { // Workaround potential PHP issue $next_payment_timestamp = WC_Subscriptions::add_months($from_timestamp, $subscription_interval); } else { $next_payment_timestamp = strtotime("+ {$subscription_interval} {$subscription_period}", $from_timestamp); } // Make sure the next payment is in the future $i = 1; while ($next_payment_timestamp < gmdate('U') && $i < 30) { if ('month' == $subscription_period) { $next_payment_timestamp = WC_Subscriptions::add_months($next_payment_timestamp, $subscription_interval); } else { // Safe to just add the billing periods $next_payment_timestamp = strtotime("+ {$subscription_interval} {$subscription_period}", $next_payment_timestamp); } $i = $i + 1; } } // If the subscription has an expiry date and the next billing period comes after the expiration, return 0 if (isset($subscription['expiry_date']) && 0 != $subscription['expiry_date'] && $next_payment_timestamp + 120 > strtotime($subscription['expiry_date'])) { $next_payment_timestamp = 0; } $next_payment = 'mysql' == $type && 0 != $next_payment_timestamp ? date('Y-m-d H:i:s', $next_payment_timestamp) : $next_payment_timestamp; return apply_filters('woocommerce_subscriptions_calculated_next_payment_date', $next_payment, $order, $product_id, $type, $from_date, $from_date_arg); }