<?php
/**
 * Repair subscriptions data to v2.0
 *
 * @author      Prospress
 * @category    Admin
 * @package     WooCommerce Subscriptions/Admin/Upgrades
 * @version     2.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WCS_Repair_2_0 {

	/**
	 * Takes care of undefine notices in the upgrade process
	 *
	 * @param  array $order_item item meta
	 * @return array             repaired item meta
	 */
	public static function maybe_repair_order_item( $order_item ) {
		foreach ( array( 'qty', 'tax_class', 'product_id', 'variation_id', 'recurring_line_subtotal', 'recurring_line_total', 'recurring_line_subtotal_tax', 'recurring_line_tax' ) as $key ) {
			if ( ! array_key_exists( $key, $order_item ) ) {
				$order_item[ $key ] = '';
			}
		}

		return $order_item;
	}

	/**
	 * Does sanity check on every subscription, and repairs them as needed
	 *
	 * @param  array $subscription subscription data to be upgraded
	 * @param  integer $item_id      id of order item meta
	 * @return array               a repaired subscription array
	 */
	public static function maybe_repair_subscription( $subscription, $item_id ) {
		global $wpdb;

		$item_meta = get_metadata( 'order_item', $item_id );

		foreach ( self::integrity_check( $subscription ) as $function ) {
			$subscription = call_user_func( 'WCS_Repair_2_0::repair_' . $function, $subscription, $item_id, $item_meta );
		}

		return $subscription;
	}

	/**
	 * Checks for missing data on a subscription
	 *
	 * @param  array $subscription data about the subscription
	 * @return array               a list of repair functions to run on the subscription
	 */
	public static function integrity_check( $subscription ) {
		$repairs_needed = array();

		foreach (
			array(
				'order_id',
				'product_id',
				'variation_id',
				'subscription_key',
				'status',
				'period',
				'interval',
				'length',
				'start_date',
				'trial_expiry_date',
				'expiry_date',
				'end_date',
			) as $meta ) {
				if ( ! array_key_exists( $meta, $subscription ) || '' === $subscription[ $meta ] ) {
					$repairs_needed[] = $meta;
				}
		}

		return $repairs_needed;
	}

	/**
	 * 'order_id': a subscription can exist without an original order in v2.0, so technically the order ID is no longer required.
	 * However, if some or all order item meta data that constitutes a subscription exists without a corresponding parent order,
	 * we can deem the issue to be that the subscription meta data was not deleted, not that the subscription should exist. Meta
	 * data could be orphaned in v1.n if the order row in the wp_posts table was deleted directly in the database, or the
	 * subscription/order were for a customer that was deleted in WordPress administration interface prior to Subscriptions v1.3.8.
	 * In both cases, the subscription, including meta data, should have been permanently deleted. However, deleting data is not a
	 * good idea during an upgrade. So I propose instead that we create a subscription without a parent order, but move it to the trash.
	 *
	 * Additional idea was to check whether the given order_id exists, but since that's another database read, it would slow down a lot of things.
	 *
	 * A subscription will not make it to this point if it doesn't have an order id, so this function will practically never be run
	 *
	 * @param  array $subscription data about the subscription
	 * @return array               repaired data about the subscription
	 */
	public static function repair_order_id( $subscription ) {
		WCS_Upgrade_Logger::add( '-- Repairing order_id for subscription that is missing order id: Status changed to trash' );
		WCS_Upgrade_Logger::add( '-- Shop owner: please review new trashed subscriptions. There is at least one with missing order id.' );

		$subscription['status'] = 'trash';

		return $subscription;
	}

	/**
	 * Combined functionality for the following functions:
	 * - repair_product_id
	 * - repair_variation_id
	 * - repair_recurring_line_total
	 * - repair_recurring_line_tax
	 * - repair_recurring_line_subtotal
	 * - repair_recurring_line_subtotal_tax
	 *
	 * @param  array   $subscription          data about the subscription
	 * @param  numeric $item_id               the id of the product we're missing the id for
	 * @param  array   $item_meta             meta data about the product
	 * @param  string  $item_meta_key         the meta key for the data on the item meta
	 * @param  string  $subscription_meta_key the meta key for the data on the subscription
	 * @return array                          repaired data about the subscription
	 */
	public static function repair_from_item_meta( array $subscription, $item_id, $item_meta, $subscription_meta_key = null, $item_meta_key = null, $default_value = '' ) {
		if ( ! is_array( $subscription ) || ! is_numeric( $item_id ) || ! is_array( $item_meta ) || ! is_string( $subscription_meta_key ) || ! is_string( $item_meta_key ) || ( ! is_string( $default_value ) && ! is_numeric( $default_value ) ) ) {
			return $subscription;
		}

		if ( array_key_exists( $item_meta_key, $item_meta ) && ! empty( $item_meta[ $item_meta_key ] ) ) {
			// only do the copy if the value on item meta is actually different to what the subscription has
			// otherwise it'd be an extra line in the log file for no actual use
			if ( ! array_key_exists( $subscription_meta_key, $subscription ) || $item_meta[ $item_meta_key ][0] != $subscription[ $subscription_meta_key ] ) {
				WCS_Upgrade_Logger::add( sprintf( '-- For order %d: copying %s from item_meta to %s on subscription.', $subscription['order_id'], $item_meta_key, $subscription_meta_key ) );
				$subscription[ $subscription_meta_key ] = $item_meta[ $item_meta_key ][0];
			}
		} elseif ( ! array_key_exists( $item_meta_key, $item_meta ) ) {
			WCS_Upgrade_Logger::add( sprintf( '-- For order %d: setting an empty %s on old subscription, item meta was not helpful.', $subscription['order_id'], $subscription_meta_key ) );
			$subscription[ $subscription_meta_key ] = $default_value;
		}

		return $subscription;
	}

	/**
	 * '_product_id': the only way to derive a order item's product ID would be to match the order item's name to a product name/title.
	 * This is quite hacky, so we may be better copying the empty product ID to the new subscription. A subscription to a deleted
	 * produced should be able to exist.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing the id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_product_id( $subscription, $item_id, $item_meta ) {
		return self::repair_from_item_meta( $subscription, $item_id, $item_meta, 'product_id', '_product_id' );
	}

	/**
	 * '_variation_id': the only way to derive a order item's product ID would be to match the order item's name to a product name/title.
	 * This is quite hacky, so we may be better copying the empty product ID to the new subscription. A subscription to a deleted produced
	 * should be able to exist.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_variation_id( $subscription, $item_id, $item_meta ) {
		return self::repair_from_item_meta( $subscription, $item_id, $item_meta, 'variation_id', '_variation_id' );
	}

	/**
	 * If the subscription does not have a subscription key for whatever reason (probably becuase the product_id was missing), then this one
	 * fills in the blank.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_subscription_key( $subscription, $item_id, $item_meta ) {
		if ( ! is_numeric( $item_id ) ) {
			// because item_id can be either product id or variation id, we can't use
			// item meta to backfill this
			$subscription['subscription_key'] = '';
		} else {
			$subscription['subscription_key'] = $subscription['order_id'] . '_' . $item_id;
		}

		return $subscription;
	}

	/**
	 * '_subscription_status': we could default to cancelled (and then potentially trash) if no status exists because the cancelled status
	 * is irreversible. But we can also take this a step further. If the subscription has a '_subscription_expiry_date' value and a
	 * '_subscription_end_date' value, and they are within a few minutes of each other, we can assume the subscription's status should be
	 * expired. If there is a '_subscription_end_date' value that is different to the '_subscription_expiry_date' value (either because the
	 * expiration value is 0 or some other date), then we can assume the status should be cancelled). If there is no end date value, we're
	 * a bit lost as technically the subscription hasn't ended, but we should make sure it is not active, so cancelled is still the best
	 * default.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_status( $subscription, $item_id, $item_meta ) {
		// only reset this if we didn't repair the order_id
		if ( ! array_key_exists( 'order_id', $subscription ) || empty( $subscription['order_id'] ) ) {
			WCS_Upgrade_Logger::add( '-- Tried to repair status. Previously set it to trash with order_id missing, bailing.' );
			return $subscription;
		}
		WCS_Upgrade_Logger::add( sprintf( '-- For order %d: repairing status for subscription.', $subscription['order_id'] ) );

		// if expiry_date and end_date are within 4 minutes (arbitrary), let it be expired
		if ( array_key_exists( 'expiry_date', $subscription ) && ! empty( $subscription['expiry_date'] ) && array_key_exists( 'end_date', $subscription ) && ! empty( $subscription['end_date'] ) && ( 4 * MINUTE_IN_SECONDS ) >= self::time_diff( $subscription['expiry_date'], $subscription['end_date'] ) ) {
			WCS_Upgrade_Logger::add( sprintf( '-- For order %d: there are end dates and expiry dates, they are close to each other, setting status to "expired" and returning.', $subscription['order_id'] ) );
			$subscription['status'] = 'expired';
		} else {
			// default to cancelled
			WCS_Upgrade_Logger::add( sprintf( '-- For order %d: setting the default to "cancelled".', $subscription['order_id'] ) );
			$subscription['status'] = 'cancelled';
		}
		self::log_store_owner_review( $subscription );
		WCS_Upgrade_Logger::add( sprintf( '-- For order %d: returning the status with %s', $subscription['order_id'], $subscription['status'] ) );
		return $subscription;
	}

	/**
	 * '_subscription_period': we can attempt to derive this from the time between renewal orders. For example, if there are two renewal
	 * orders found 3 months apart, the billing period would be month. If there are not two or more renewal orders (we can't use a single
	 * renewal order because that would account for the free trial) and a _product_id value , if the product still exists, we can use the
	 * current value set on that product. It won't always be correct, but it's the closest we can get to an accurate estimate.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_period( $subscription, $item_id, $item_meta ) {
		WCS_Upgrade_Logger::add( sprintf( '-- For order %d: repairing period for subscription', $subscription['order_id'] ) );

		// Get info from the product
		$subscription = self::repair_from_item_meta( $subscription, $item_id, $item_meta, 'period', '_subscription_period', '' );

		if ( '' !== $subscription['period'] ) {
			return $subscription;
		}

		// let's get the renewal orders
		$renewal_orders = self::get_renewal_orders( $subscription );

		if ( count( $renewal_orders ) < 2 ) {
			// default to month. Because we're defaulting, we also need to cancel this to avoid charging customers on a schedule they didn't
			// agree to.
			WCS_Upgrade_Logger::add( sprintf( '-- For order %d: setting default subscription period to month.', $subscription['order_id'] ) );
			self::log_store_owner_review( $subscription );
			$subscription['period'] = 'month';
			$subscription['status'] = 'cancelled';
			return $subscription;
		}

		// let's get the last 2 renewal orders
		$last_renewal_order       = array_shift( $renewal_orders );
		$last_renewal_date        = wcs_get_datetime_utc_string( wcs_get_objects_property( $last_renewal_order, 'date_created' ) );
		$last_renewal_timestamp   = wcs_date_to_time( $last_renewal_date );

		$second_renewal_order     = array_shift( $renewal_orders );
		$second_renewal_date      = wcs_get_datetime_utc_string( wcs_get_objects_property( $second_renewal_order, 'date_created' ) );
		$second_renewal_timestamp = wcs_date_to_time( $second_renewal_date );

		$interval = 1;

		// if we have an interval, let's pass this along too, because then it's a known variable
		if ( array_key_exists( 'interval', $subscription ) && ! empty( $subscription['interval'] ) ) {
			$interval = $subscription['interval'];
		}

		WCS_Upgrade_Logger::add( sprintf( '-- For order %d: calling wcs_estimate_period_between().', $subscription['order_id'] ) );
		$period = wcs_estimate_period_between( $last_renewal_date, $second_renewal_date, $interval );

		// if we have 3 renewal orders, do a double check
		if ( ! empty( $renewal_orders ) ) {
			WCS_Upgrade_Logger::add( sprintf( '-- For order %d: we have 3 renewal orders, trying to make sure we are right.', $subscription['order_id'] ) );

			$third_renewal_order = array_shift( $renewal_orders );
			$third_renewal_date = wcs_get_datetime_utc_string( wcs_get_objects_property( $third_renewal_order, 'date_created' ) );

			$period2 = wcs_estimate_period_between( $second_renewal_date, $third_renewal_date, $interval );

			if ( $period == $period2 ) {
				WCS_Upgrade_Logger::add( sprintf( '-- For order %d: second check confirmed, we are very confident period is %s.', $subscription['order_id'], $period ) );
				$subscription['period'] = $period;
			}
		}

		$subscription['period'] = $period;

		return $subscription;
	}

	/**
	 * '_subscription_interval': we can attempt to derive this from the time between renewal orders. For example, if there are two renewal
	 * orders found 3 months apart, the billing period would be month. If there are not two or more renewal orders (we can't use a single
	 * renewal order because that would account for the free trial) and a _product_id value , if the product still exists, we can use the
	 * current value set on that product. It won't always be correct, but it's the closest we can get to an accurate estimate.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_interval( $subscription, $item_id, $item_meta ) {

		// Get info from the product
		if ( array_key_exists( '_subscription_interval', $item_meta ) && ! empty( $item_meta['_subscription_interval'] ) ) {
			WCS_Upgrade_Logger::add( sprintf( '-- For order %d: getting interval from item meta and returning.', $subscription['order_id'] ) );

			$subscription['interval'] = $item_meta['_subscription_interval'][0];
			return $subscription;
		}

		// by this time we already have a period on our hand
		// let's get the renewal orders
		$renewal_orders = self::get_renewal_orders( $subscription );

		if ( count( $renewal_orders ) < 2 ) {
			// default to 1
			WCS_Upgrade_Logger::add( sprintf( '-- For order %d: setting default subscription interval to 1.', $subscription['order_id'] ) );
			self::log_store_owner_review( $subscription );
			$subscription['interval'] = 1;
			$subscription['status']   = 'cancelled';
			return $subscription;
		}

		// let's get the last 2 renewal orders
		$last_renewal_order       = array_shift( $renewal_orders );
		$last_renewal_date        = wcs_get_datetime_utc_string( wcs_get_objects_property( $last_renewal_order, 'date_created' ) );
		$last_renewal_timestamp   = wcs_date_to_time( $last_renewal_date );

		$second_renewal_order     = array_shift( $renewal_orders );
		$second_renewal_date      = wcs_get_datetime_utc_string( wcs_get_objects_property( $second_renewal_order, 'date_created' ) );
		$second_renewal_timestamp = wcs_date_to_time( $second_renewal_date );

		$subscription['interval'] = wcs_estimate_periods_between( $second_renewal_timestamp, $last_renewal_timestamp, $subscription['period'] );

		return $subscription;
	}

	/**
	 * '_subscription_length': if there are '_subscription_expiry_date' and '_subscription_start_date' values, we can use those to
	 * determine how many billing periods fall between them, and therefore, the length of the subscription. This data is low value however as
	 * it is no longer stored in v2.0 and mainly used to determine the expiration date.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_length( $subscription, $item_id, $item_meta ) {
		// Let's see if the item meta has that
		$subscription = self::repair_from_item_meta( $subscription, $item_id, $item_meta, 'length', '_subscription_length', '' );

		if ( '' !== $subscription['length'] ) {
			return $subscription;
		}

		$effective_start_date = self::get_effective_start_date( $subscription );

		// If we can calculate it from the effective date and expiry date
		if ( 'expired' == $subscription['status'] && array_key_exists( 'expiry_date', $subscription ) && ! empty( $subscription['expiry_date'] ) && null !== $effective_start_date && array_key_exists( 'period', $subscription ) && ! empty( $subscription['period'] ) && array_key_exists( 'interval', $subscription ) && ! empty( $subscription['interval'] ) ) {
			$intervals = wcs_estimate_periods_between( wcs_date_to_time( $effective_start_date ), wcs_date_to_time( $subscription['expiry_date'] ), $subscription['period'], 'floor' );
			$subscription['length'] = $intervals;
		} else {
			$subscription['length'] = 0;
		}

		return $subscription;
	}

	/**
	 * '_subscription_start_date': the original order's '_paid_date' value (stored in post meta) can be used as the subscription's start date.
	 * If no '_paid_date' exists, because the order used a payment method that doesn't call $order->payment_complete(), like BACs or Cheque,
	 * then we can use the post_date_gmt column in the wp_posts table of the original order.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_start_date( $subscription, $item_id, $item_meta ) {
		global $wpdb;

		$start_date = get_post_meta( $subscription['order_id'], '_paid_date', true );

		WCS_Upgrade_Logger::add( sprintf( 'Repairing start_date for order %d: Trying to use the _paid date for start date.', $subscription['order_id'] ) );

		if ( empty( $start_date ) ) {
			WCS_Upgrade_Logger::add( '-- start_date from _paid date failed. Using post_date_gmt' );

			$start_date = $wpdb->get_var( $wpdb->prepare( "SELECT post_date_gmt FROM {$wpdb->posts} WHERE ID = %d", $subscription['order_id'] ) );
		}

		$subscription['start_date'] = $start_date;
		return $subscription;
	}

	/**
	 * '_subscription_trial_expiry_date': if the subscription has at least one renewal order, we can set the trial expiration date to the date
	 * of the first renewal order. However, this is generally safe to default to 0 if it is not set. Especially if the subscription is
	 * inactive and/or has 1 or more renewals (because its no longer used and is simply for record keeping).
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_trial_expiry_date( $subscription, $item_id, $item_meta ) {
		$subscription['trial_expiry_date'] = self::maybe_get_date_from_action_scheduler( 'scheduled_subscription_trial_end', $subscription );
		return $subscription;
	}

	/**
	 * '_subscription_expiry_date': if the subscription has a '_subscription_length' value, that can be used to calculate the expiration date
	 * (from the '_subscription_start_date' or '_subscription_trial_expiry_date' if one is set). If no length is set, but the subscription has
	 * an expired status, the '_subscription_end_date' can be used. In most other cases, this is generally safe to default to 0 if the
	 * subscription is cancelled because its no longer used and is simply for record keeping.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_expiry_date( $subscription, $item_id, $item_meta ) {
		$subscription['expiry_date'] = self::maybe_get_date_from_action_scheduler( 'scheduled_subscription_expiration', $subscription );
		return $subscription;
	}

	/**
	 * '_subscription_end_date': if the subscription has a '_subscription_length' value and status of expired, the length can be used to
	 * calculate the end date as it will be the same as the expiration date. If no length is set, or the subscription has a cancelled status,
	 * some time within 24 hours after the last renewal order's date can be used to provide a rough estimate.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_end_date( $subscription, $item_id, $item_meta ) {

		$subscription = self::repair_from_item_meta( $subscription, $item_id, $item_meta, 'end_date', '_subscription_end_date', '' );

		if ( '' !== $subscription['end_date'] ) {
			return $subscription;
		}

		if ( 'expired' == $subscription['status'] && array_key_exists( 'expiry_date', $subscription ) && ! empty( $subscription['expiry_date'] ) ) {

			$subscription['end_date'] = $subscription['expiry_date'];

		} elseif ( 'cancelled' == $subscription['status'] || ! array_key_exists( 'length', $subscription ) || empty( $subscription['length'] ) ) {

			// get renewal orders
			$renewal_orders = self::get_renewal_orders( $subscription );
			$last_order = array_shift( $renewal_orders );

			if ( empty( $last_order ) ) {

				$subscription['end_date'] = 0;

			} else {

				$subscription['end_date'] = wcs_add_time( 5, 'hours', wcs_get_objects_property( $last_order, 'date_created' )->getTimestamp() );

			}
		} else {

			// if everything failed, let's have an empty one
			$subscription['end_date'] = 0;

		}

		return $subscription;
	}

	/**
	 * _recurring_line_total': if the subscription has at least one renewal order, this value can be derived from the '_line_total' value of
	 * that order. If no renewal orders exist, it can be derived roughly by deducting the '_subscription_sign_up_fee' value from the original
	 * order's total if there is no trial expiration date.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_recurring_line_total( $subscription, $item_id, $item_meta ) {
		return self::repair_from_item_meta( $subscription, $item_id, $item_meta, 'recurring_line_total', '_line_total', 0 );
	}

	/**
	 * _recurring_line_total': if the subscription has at least one renewal order, this value can be derived from the '_line_total' value
	 * of that order. If no renewal orders exist, it can be derived roughly by deducting the '_subscription_sign_up_fee' value from the
	 * original order's total if there is no trial expiration date.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_recurring_line_tax( $subscription, $item_id, $item_meta ) {
		return self::repair_from_item_meta( $subscription, $item_id, $item_meta, 'recurring_line_tax', '_line_tax', 0 );
	}

	/**
	 * _recurring_line_total': if the subscription has at least one renewal order, this value can be derived from the '_line_total' value of
	 * that order. If no renewal orders exist, it can be derived roughly by deducting the '_subscription_sign_up_fee' value from the original
	 * order's total if there is no trial expiration date
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_recurring_line_subtotal( $subscription, $item_id, $item_meta ) {
		return self::repair_from_item_meta( $subscription, $item_id, $item_meta, 'recurring_line_subtotal', '_line_subtotal', 0 );
	}

	/**
	 * _recurring_line_total': if the subscription has at least one renewal order, this value can be derived from the '_line_total' value of
	 * that order. If no renewal orders exist, it can be derived roughly by deducting the '_subscription_sign_up_fee' value from the original
	 * order's total if there is no trial expiration date.
	 *
	 * @param  array $subscription data about the subscription
	 * @param  numeric $item_id    the id of the product we're missing variation id for
	 * @param  array $item_meta    meta data about the product
	 * @return array               repaired data about the subscription
	 */
	public static function repair_recurring_line_subtotal_tax( $subscription, $item_id, $item_meta ) {
		return self::repair_from_item_meta( $subscription, $item_id, $item_meta, 'recurring_line_subtotal_tax', '_line_subtotal_tax', 0 );
	}

	/**
	 * Utility function to calculate the seconds between two timestamps. Order is not important, it's just the difference.
	 *
	 * @param  string $to   mysql timestamp
	 * @param  string $from mysql timestamp
	 * @return integer       number of seconds between the two
	 */
	private static function time_diff( $to, $from ) {
		$to   = wcs_date_to_time( $to );
		$from = wcs_date_to_time( $from );

		return abs( $to - $from );
	}

	/**
	 * Utility function to get all renewal orders in the old structure.
	 *
	 * @param  array $subscription the sub we're looking for the renewal orders
	 * @return array               of WC_Orders
	 */
	private static function get_renewal_orders( $subscription ) {
		$related_orders = array();

		$related_post_ids = get_posts( array(
			'posts_per_page' => -1,
			'post_type'      => 'shop_order',
			'post_status'    => 'any',
			'fields'         => 'ids',
			'orderby'        => 'date',
			'order'          => 'DESC',
			'post_parent'    => $subscription['order_id'],
		) );

		foreach ( $related_post_ids as $post_id ) {
			$related_orders[ $post_id ] = wc_get_order( $post_id );
		}

		return $related_orders;
	}

	/**
	 * Utility method to check the action scheduler for dates
	 *
	 * @param  string $type             the type of scheduled action
	 * @param  string $subscription_key key of subscription in the format of order_id_item_id
	 * @return string                   either 0 or mysql date
	 */
	private static function maybe_get_date_from_action_scheduler( $type, $subscription ) {
		$action_args = array(
			'user_id'          => intval( $subscription['user_id'] ),
			'subscription_key' => $subscription['subscription_key'],
		);

		WCS_Upgrade_Logger::add( sprintf( '-- For order %d: Repairing date type "%s" from action scheduler...', $subscription['order_id'], $type ) );
		WCS_Upgrade_Logger::add( '-- This is the arguments: ' . PHP_EOL . print_r( array( $action_args, 'hook' => $type ), true ) . PHP_EOL ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

		$next_date_timestamp = as_next_scheduled_action( $type, $action_args );

		if ( false === $next_date_timestamp ) {
			// set it to 0 as default
			$formatted_date = 0;
			WCS_Upgrade_Logger::add( sprintf( '-- For order %d: Repairing date type "%s": fetch of date unsuccessfull: no action present. Date is 0.', $subscription['order_id'], $type ) );
		} else {
			$formatted_date = gmdate( 'Y-m-d H:i:s', $next_date_timestamp );
			WCS_Upgrade_Logger::add( sprintf( '-- For order %d: Repairing date type "%s": fetch of date successfull. New date is %s', $subscription['order_id'], $type, $formatted_date ) );
		}

		return $formatted_date;
	}

	/**
	 * Utility function to return the effective start date for interval calculations (end of trial period -> start date -> null )
	 *
	 * @param  array $subscription subscription data
	 * @return mixed               mysql formatted date, or null if none found
	 */
	public static function get_effective_start_date( $subscription ) {

		if ( array_key_exists( 'trial_expiry_date', $subscription ) && ! empty( $subscription['trial_expiry_date'] ) ) {

			$effective_date = $subscription['trial_expiry_date'];

		} elseif ( array_key_exists( 'trial_period', $subscription ) && ! empty( $subscription['trial_period'] ) && array_key_exists( 'trial_length', $subscription ) && ! empty( $subscription['trial_length'] ) && array_key_exists( 'start_date', $subscription ) && ! empty( $subscription['start_date'] ) ) {

			// calculate the end of trial from interval, period and start date
			$effective_date = gmdate( 'Y-m-d H:i:s', wcs_add_time( $subscription['trial_length'], $subscription['trial_period'], wcs_date_to_time( $subscription['start_date'] ) ) );

		} elseif ( array_key_exists( 'start_date', $subscription ) && ! empty( $subscription['start_date'] ) ) {

			$effective_date = $subscription['start_date'];

		} else {

			$effective_date = null;

		}

		return $effective_date;
	}


	/**
	 * Logs an entry for the store owner to review an issue.
	 *
	 * @param array $subscription subscription data
	 */
	protected static function log_store_owner_review( $subscription ) {
		WCS_Upgrade_Logger::add( sprintf( '-- For order %d: shop owner please review subscription.', $subscription['order_id'] ) );
	}
}
