woocommerce-subscriptions/includes/core/upgrades/class-wcs-repair-2-0-2.php

422 lines
20 KiB
PHP

<?php
/**
* Repair subscriptions data corrupted with the v2.0.0 upgrade process
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin/Upgrades
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.0.2
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* @deprecated
*/
class WCS_Repair_2_0_2 {
/**
* Get a batch of subscriptions subscriptions that haven't already been checked for repair.
*
* @return array IDs of subscription that have not been checked or repaired
*/
public static function get_subscriptions_to_repair( $batch_size ) {
// Get any subscriptions that haven't already been checked for repair
$subscription_ids_to_repair = get_posts( array(
'post_type' => 'shop_subscription',
'post_status' => 'any',
'posts_per_page' => $batch_size,
'fields' => 'ids',
'orderby' => 'ID',
'order' => 'ASC',
'meta_query' => array(
array(
'key' => '_wcs_repaired_2_0_2',
'compare' => 'NOT EXISTS',
),
),
) );
return $subscription_ids_to_repair;
}
/**
* Update any subscription that need to be repaired.
*
* @return array The counts of repaired and unrepaired subscriptions
*/
public static function maybe_repair_subscriptions( $subscription_ids_to_repair ) {
global $wpdb;
// don't allow data to be half upgraded on a subscription in case of a script timeout or other non-recoverable error
$wpdb->query( 'START TRANSACTION' );
$repaired_count = $unrepaired_count = 0;
foreach ( $subscription_ids_to_repair as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( false !== $subscription && self::maybe_repair_subscription( $subscription ) ) {
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: repair completed', $subscription->get_id() ) );
$repaired_count++;
update_post_meta( $subscription_id, '_wcs_repaired_2_0_2', 'true' );
} else {
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: no repair needed', $subscription->get_id() ) );
$unrepaired_count++;
update_post_meta( $subscription_id, '_wcs_repaired_2_0_2', 'false' );
}
}
$wpdb->query( 'COMMIT' );
return array(
'repaired_count' => $repaired_count,
'unrepaired_count' => $unrepaired_count,
);
}
/**
* 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;
$parent_order = $subscription->get_parent();
// if the subscription doesn't have an order, it must have been created in 2.0, so we can ignore it
if ( false === $parent_order ) {
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: no need to repair: it has no order.', $subscription->get_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->get_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 ( $parent_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->get_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->get_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->get_id() ) );
$repaired_subscription = true;
} else {
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: line tax data not added.', $subscription->get_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->get_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->get_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->get_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->get_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->get_id(), $e->getMessage() ) );
}
$repaired_subscription = true;
}
if ( '' !== wcs_get_objects_property( $parent_order, 'customer_note' ) && '' == $subscription->get_customer_note() ) {
$post_data = array(
'ID' => $subscription->get_id(),
'post_excerpt' => wcs_get_objects_property( $parent_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->get_id() ) );
$repaired_subscription = true;
} else {
WCS_Upgrade_Logger::add( sprintf( '!! For subscription %d: unable to repair missing customer note. Exception: "%s"', $subscription->get_id(), $updated_post_id->get_error_message() ) );
}
}
return $repaired_subscription;
}
/**
* If we have a trial end date and that value is not the same as the old end date prior to upgrade, it was most likely
* corrupted, so we will reset it to the value in meta.
*
* @param WC_Subscription $subscription the subscription to check
* @param array $former_order_item_meta the order item meta data for the line item on the original order that formerly represented the subscription
* @return string|bool false if the date does not need to be repaired or the new date if it should be repaired
*/
protected static function check_trial_end_date( $subscription, $former_order_item_meta ) {
$new_trial_end_time = $subscription->get_time( 'trial_end' );
if ( $new_trial_end_time > 0 ) {
$old_trial_end_date = isset( $former_order_item_meta['_wcs_migrated_subscription_trial_expiry_date'][0] ) ? $former_order_item_meta['_wcs_migrated_subscription_trial_expiry_date'][0] : 0;
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: new trial end date = %s.', $subscription->get_id(), var_export( $subscription->get_date( 'trial_end' ), true ) ) );
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: old trial end date = %s.', $subscription->get_id(), var_export( $old_trial_end_date, true ) ) );
// if the subscription has a trial end time whereas previously it didn't, we need it to be deleted
if ( 0 == $old_trial_end_date ) {
$repair_date = 0;
} else {
$repair_date = false;
}
} else {
$repair_date = false;
}
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: repair trial end date = %s.', $subscription->get_id(), var_export( $repair_date, true ) ) );
return $repair_date;
}
/**
* Because the upgrader may have attempted to set an invalid end date on the subscription, it could
* lead to the entire date update process failing, which would mean that a next payment date would
* not be set even when one existed.
*
* This method checks if a given subscription has no next payment date, and if it doesn't, it checks
* if one was previously scheduled for the old subscription. If one was, and that date is in the future,
* it will pass that date back for being set on the subscription. If a date was scheduled but that is now
* in the past, it will recalculate it.
*
* @param WC_Subscription $subscription the subscription to check
* @return string|bool false if the date does not need to be repaired or the new date if it should be repaired
*/
protected static function check_next_payment_date( $subscription ) {
global $wpdb;
// the subscription doesn't have a next payment date set, let's see if it should
if ( 0 == $subscription->get_time( 'next_payment' ) && $subscription->has_status( 'active' ) ) {
$old_hook_args = array(
'user_id' => (int) $subscription->get_user_id(),
'subscription_key' => wcs_get_old_subscription_key( $subscription ),
);
// get the latest scheduled subscription payment in v1.5
$old_next_payment_date = $wpdb->get_var( $wpdb->prepare(
"SELECT post_date_gmt FROM $wpdb->posts
WHERE post_type = %s
AND post_content = %s
AND post_title = 'scheduled_subscription_payment'
ORDER BY post_date_gmt DESC",
ActionScheduler_wpPostStore::POST_TYPE,
wcs_json_encode( $old_hook_args )
) );
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: new next payment date = %s.', $subscription->get_id(), var_export( $subscription->get_date( 'next_payment' ), true ) ) );
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: old next payment date = %s.', $subscription->get_id(), var_export( $old_next_payment_date, true ) ) );
// if we have a date, make sure it's valid
if ( null !== $old_next_payment_date ) {
if ( wcs_date_to_time( $old_next_payment_date ) <= gmdate( 'U' ) ) {
$repair_date = $subscription->calculate_date( 'next_payment' );
if ( 0 == $repair_date ) {
$repair_date = false;
}
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: old next payment date is in the past, setting it to %s.', $subscription->get_id(), var_export( $repair_date, true ) ) );
} else {
$repair_date = $old_next_payment_date;
}
} else {
// let's just double check we shouldn't have a date set by recalculating it
$calculated_next_payment_date = $subscription->calculate_date( 'next_payment' );
if ( 0 != $calculated_next_payment_date && wcs_date_to_time( $calculated_next_payment_date ) > gmdate( 'U' ) ) {
$repair_date = $calculated_next_payment_date;
} else {
$repair_date = false;
}
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: no old next payment date, setting it to %s.', $subscription->get_id(), var_export( $repair_date, true ) ) );
}
} else {
$repair_date = false;
}
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: repair next payment date = %s.', $subscription->get_id(), var_export( $repair_date, true ) ) );
return $repair_date;
}
/**
* Check if the old subscription meta had an end date recorded and make sure that end date is now being used for the new subscription.
*
* In Subscriptions prior to 2.0 a subscription could have both an end date and an expiration date. The end date represented a date in the past
* on which the subscription expired or was cancelled. The expiration date represented a date on which the subscription was set to expire (this
* could be in the past or future and could be the same as the end date or different). Because the end date is a definitive even, in this function
* we first check if it exists before falling back to the expiration date to check against.
*
* @param WC_Subscription $subscription the subscription to check
* @param array $former_order_item_meta the order item meta data for the line item on the original order that formerly represented the subscription
* @return string|bool false if the date does not need to be repaired or the new date if it should be repaired
*/
protected static function check_end_date( $subscription, $former_order_item_meta ) {
$new_end_time = $subscription->get_time( 'end' );
if ( $new_end_time > 0 ) {
$old_end_date = isset( $former_order_item_meta['_wcs_migrated_subscription_end_date'][0] ) ? $former_order_item_meta['_wcs_migrated_subscription_end_date'][0] : 0;
// if the subscription hadn't expired or been cancelled yet, it wouldn't have an end date, but it may still have had an expiry date, so use that instead
if ( 0 == $old_end_date ) {
$old_end_date = isset( $former_order_item_meta['_wcs_migrated_subscription_expiry_date'][0] ) ? $former_order_item_meta['_wcs_migrated_subscription_expiry_date'][0] : 0;
}
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: new end date = %s.', $subscription->get_id(), var_export( $subscription->get_date( 'end' ), true ) ) );
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: old end date = %s.', $subscription->get_id(), var_export( $old_end_date, true ) ) );
// if the subscription has an end time whereas previously it didn't, we need it to be deleted so set it 0
if ( 0 == $old_end_date ) {
$repair_date = 0;
} else {
$repair_date = false;
}
} else {
$repair_date = false;
}
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: repair end date = %s.', $subscription->get_id(), var_export( $repair_date, true ) ) );
return $repair_date;
}
/**
* If the subscription has expired since upgrading and the end date is not the original expiration date,
* we need to unexpire it, which in the case of a previously active subscription means activate it, and
* in any other case, leave it as on-hold (a cancelled subscription wouldn't have been expired, so the
* status must be on-hold or active).
*
* @param WC_Subscription $subscription data about the subscription
* @return bool true if the trial date was repaired, otherwise false
*/
protected static function maybe_repair_status( $subscription, $former_order_item_meta, $dates_to_update ) {
if ( $subscription->has_status( 'expired' ) && 'expired' != $former_order_item_meta['_wcs_migrated_subscription_status'][0] && isset( $dates_to_update['end'] ) ) {
try {
// we need to bypass the update_status() method here because normally an expired subscription can't have it's status changed, we also don't want normal status change hooks to be fired
wp_update_post(
array(
'ID' => $subscription->get_id(),
'post_status' => 'wc-on-hold',
)
);
// if the payment method doesn't support date changes, we still want to reactivate the subscription but we also need to process a special failed payment at the next renewal to fix up the payment method so we'll set a special flag in post meta to handle that
if ( ! $subscription->payment_method_supports( 'subscription_date_changes' ) && $subscription->get_total() > 0 ) {
update_post_meta( $subscription->get_id(), '_wcs_repaired_2_0_2_needs_failed_payment', 'true' );
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: payment method does not support "subscription_date_changes" and total > 0, setting "_wcs_repaired_2_0_2_needs_failed_payment" post meta flag.', $subscription->get_id() ) );
}
if ( 'active' == $former_order_item_meta['_wcs_migrated_subscription_status'][0] && $subscription->can_be_updated_to( 'active' ) ) {
$subscription->update_status( 'active' );
}
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: repaired status. Status was "expired", it is now "%s".', $subscription->get_id(), $subscription->get_status() ) );
$repair_status = true;
} catch ( Exception $e ) {
WCS_Upgrade_Logger::add( sprintf( '!!! For subscription %d: unable to repair status, exception "%s"', $subscription->get_id(), $e->getMessage() ) );
$repair_status = false;
}
} else {
WCS_Upgrade_Logger::add( sprintf( 'For subscription %d: no need to repair status, current status: %s; former status: %s.', $subscription->get_id(), $subscription->get_status(), $former_order_item_meta['_wcs_migrated_subscription_status'][0] ) );
$repair_status = false;
}
return $repair_status;
}
/**
* There was a bug in the WCS_Upgrade_2_0::add_line_tax_data() method in Subscriptions 2.0.0 and 2.0.1 which
* prevented recurring line tax data from being copied correctly to newly created subscriptions. This bug was
* fixed in 2.0.2, so we can now use that method to make sure line tax data is set correctly. But to do that,
* we first need to massage some of the deprecated line item meta to use the original meta keys.
*
* @param int $subscription_line_item_id ID of the new subscription line item
* @param int $old_order_item_id ID of the old order line item
* @param array $old_order_item The old line item
* @return bool|int the meta ID of the newly added '_line_tax_data' meta data row, or false if no line tax data was added.
*/
protected static function maybe_repair_line_tax_data( $subscription_line_item_id, $old_order_item_id, $old_order_item ) {
// we need item meta in the old format so that we can use the (now fixed) WCS_Upgrade_2_0::add_line_tax_data() method and save duplicating its code
$old_order_item['item_meta']['_recurring_line_total'] = isset( $old_order_item['item_meta']['_wcs_migrated_recurring_line_total'] ) ? $old_order_item['item_meta']['_wcs_migrated_recurring_line_total'] : 0;
$old_order_item['item_meta']['_recurring_line_tax'] = isset( $old_order_item['item_meta']['_wcs_migrated_recurring_line_tax'] ) ? $old_order_item['item_meta']['_wcs_migrated_recurring_line_tax'] : 0;
$old_order_item['item_meta']['_recurring_line_subtotal_tax'] = isset( $old_order_item['item_meta']['_wcs_migrated_recurring_line_subtotal_tax'] ) ? $old_order_item['item_meta']['_wcs_migrated_recurring_line_subtotal_tax'] : 0;
if ( isset( $old_order_item['item_meta']['_wcs_migrated_recurring_line_tax_data'] ) ) {
$old_order_item['item_meta']['_recurring_line_tax_data'] = $old_order_item['item_meta']['_wcs_migrated_recurring_line_tax_data'];
}
return WCS_Upgrade_2_0::add_line_tax_data( $subscription_line_item_id, $old_order_item_id, $old_order_item );
}
}