laipower/wp-content/plugins/easy-digital-downloads/includes/admin/upgrades/v3/class-data-migrator.php

1514 lines
50 KiB
PHP
Raw Normal View History

<?php
/**
* 3.0 Data Migration - Data Migrator.
*
* @subpackage Admin/Upgrades/v3
* @copyright Copyright (c) 2018, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.0
*/
namespace EDD\Admin\Upgrades\v3;
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/**
* Data_Migrator Class.
*
* This class holds all the logic for migrating data to custom tables as part
* of EDD 3.0.
*
* @since 3.0
*/
class Data_Migrator {
/**
* Customer addresses.
*
* @since 3.0
*
* @param object $data Data to migrate.
* @param string $type The type of address this is.
*/
public static function customer_addresses( $data = null, $type = 'billing' ) {
// Bail if no data passed.
if ( ! $data ) {
return;
}
$address = maybe_unserialize( $data->meta_value );
$user_id = absint( $data->user_id );
$customer = edd_get_customer_by( 'user_id', $user_id );
$address = wp_parse_args( $address, array(
'line1' => '',
'line2' => '',
'city' => '',
'state' => '',
'zip' => '',
'country' => '',
) );
$address_to_check = array_filter( $address );
// Do not migrate empty addresses.
if ( empty( $address_to_check ) ) {
return;
}
if ( $customer ) {
edd_maybe_add_customer_address(
array(
'customer_id' => $customer->id,
'is_primary' => true,
'name' => $customer->name,
'address' => $address['line1'],
'address2' => $address['line2'],
'city' => $address['city'],
'region' => $address['state'],
'postal_code' => $address['zip'],
'country' => $address['country'],
'date_created' => $customer->date_created,
)
);
}
}
/**
* Customer email addresses.
*
* @since 3.0
*
* @param object $data Data to migrate.
*/
public static function customer_email_addresses( $data = null ) {
// Bail if no data passed.
if ( ! isset( $data->edd_customer_id ) || ! isset( $data->meta_value ) ) {
return;
}
$customer = edd_get_customer( absint( $data->edd_customer_id ) );
if ( ! $customer ) {
return;
}
edd_add_customer_email_address(
array(
'customer_id' => $customer->id,
'email' => $data->meta_value,
'date_created' => $customer->date_created,
)
);
}
/**
* Customer notes.
*
* @since 3.0
*
* @param object $data Data to migrate.
*/
public static function customer_notes( $data = null ) {
// Bail if no data passed.
if ( ! $data ) {
return;
}
$customer_id = absint( $data->id );
if ( property_exists( $data, 'notes' ) && ! empty( $data->notes ) ) {
$notes = array_reverse( array_filter( explode( "\n\n", $data->notes ) ) );
$notes = array_map( function( $val ) {
return explode( ' - ', $val );
}, $notes );
if ( ! empty( $notes ) ) {
foreach ( $notes as $note ) {
try {
$date = isset( $note[0] )
? EDD()->utils->date( $note[0], edd_get_timezone_id() )->setTimezone( 'UTC' )->toDateTimeString()
: '';
} catch ( \Exception $e ) {
// An empty date will be changed to current time in BerlinDB.
$date = '';
}
$note_content = isset( $note[1] )
? $note[1]
: '';
edd_add_note( array(
'user_id' => 0,
'object_id' => $customer_id,
'object_type' => 'customer',
'content' => $note_content,
'date_created' => $date,
'date_modified' => $date,
) );
}
}
}
}
/**
* Discounts.
*
* @since 3.0
*
* @param object $data Data to migrate.
*/
public static function discounts( $data = null ) {
// Bail if no data passed.
if ( ! $data ) {
return;
}
$data = get_post( $data->ID );
$args = array();
$meta = get_post_custom( $data->ID );
$meta_to_migrate = array();
$core_meta = array(
'code',
'name',
'status',
'uses',
'max_uses',
'amount',
'start',
'expiration',
'type',
'min_price',
'product_reqs',
'product_condition',
'excluded_products',
'is_not_global',
'is_single_use',
);
foreach ( $meta as $key => $value ) {
$value = maybe_unserialize( $value[0] );
if ( false === strpos( $key, '_edd_discount' ) ) {
// This is custom meta from another plugin that needs to be migrated to the new meta table.
$meta_to_migrate[ $key ] = $value;
continue;
}
$meta_key = str_replace( '_edd_discount_', '', $key );
if ( ! in_array( $meta_key, $core_meta, true ) ) {
$meta_to_migrate[ $meta_key ] = $value;
continue;
}
$args[ $meta_key ] = $value;
}
// If the discount name was not stored in post_meta, use value from the WP_Post object.
if ( ! isset( $args['name'] ) ) {
$args['name'] = $data->post_title;
}
$args['id'] = $data->ID;
$args['date_created'] = $data->post_date_gmt;
$args['date_modified'] = $data->post_modified_gmt;
// Use edd_store_discount() so any legacy data is handled correctly.
$discount_id = edd_store_discount( $args );
// Migrate any additional meta.
if ( ! empty( $meta_to_migrate ) ) {
foreach ( $meta_to_migrate as $key => $value ) {
edd_add_adjustment_meta( $discount_id, $key, $value );
}
}
}
/**
* Logs.
*
* @since 3.0
*
* @param object $data Data to migrate.
*/
public static function logs( $data = null ) {
global $wpdb;
// Bail if no data passed.
if ( ! $data ) {
return;
}
$meta_to_migrate = array();
if ( 'file_download' === $data->slug ) {
$meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d", absint( $data->ID ) ) );
$post_meta = array();
foreach ( $meta as $meta_item ) {
$post_meta[ $meta_item->meta_key ] = maybe_unserialize( $meta_item->meta_value );
}
$log_data = array(
'product_id' => $data->post_parent,
/*
* Custom Deliverables was overriding the file ID to be a string instead of an integer. The preg_replace
* allows us to try to salvage the file ID from that string.
*/
'file_id' => isset( $post_meta['_edd_log_file_id'] ) ? preg_replace( '/[^0-9]/', '', $post_meta['_edd_log_file_id'] ) : 0,
'order_id' => isset( $post_meta['_edd_log_payment_id'] ) ? $post_meta['_edd_log_payment_id'] : 0,
'price_id' => isset( $post_meta['_edd_log_price_id'] ) ? $post_meta['_edd_log_price_id'] : 0,
'customer_id' => isset( $post_meta['_edd_log_customer_id'] ) ? $post_meta['_edd_log_customer_id'] : 0,
'ip' => isset( $post_meta['_edd_log_ip'] ) ? $post_meta['_edd_log_ip'] : '',
'date_created' => $data->post_date_gmt,
'date_modified' => $data->post_modified_gmt,
);
$meta_to_remove = array(
'_edd_log_file_id',
'_edd_log_payment_id',
'_edd_log_price_id',
'_edd_log_customer_id',
'_edd_log_ip',
'_edd_log_user_id',
);
// If the log doesn't have a customer ID, but does have a user ID, keep the user ID as metadata.
if ( empty( $log_data['customer_id'] ) && ! empty( $post_meta['_edd_log_user_id'] ) && ! in_array( $post_meta['_edd_log_user_id'], array( 0, -1 ) ) ) {
$meta_to_remove = array_diff( $meta_to_remove, array( '_edd_log_user_id' ) );
}
$meta_to_migrate = $post_meta;
$new_log_id = edd_add_file_download_log( $log_data );
$add_meta_function = 'edd_add_file_download_log_meta';
/**
* Triggers after a file download log has been migrated.
*
* @since 3.0
*
* @param int $new_log_id ID of the newly created log.
* @param object $data Data from the posts table. (Essentially a `WP_Post`, without being that object.)
* @param array $post_meta All meta associated with this log.
*/
do_action( 'edd_30_migrate_file_download_log', $new_log_id, $data, $post_meta );
} elseif ( 'api_request' === $data->slug ) {
$meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d", absint( $data->ID ) ) );
$post_meta = array();
foreach ( $meta as $meta_item ) {
$post_meta[ $meta_item->meta_key ] = maybe_unserialize( $meta_item->meta_value );
}
$post_meta = wp_parse_args(
$post_meta,
array(
'_edd_log_request_ip' => '',
'_edd_log_user' => 0,
'_edd_log_key' => 'public',
'_edd_log_token' => 'public',
'_edd_log_version' => '',
'_edd_log_time' => '',
)
);
if ( empty( $post_meta['_edd_log_token'] ) ) {
$post_meta['_edd_log_token'] = 'public' === $post_meta['_edd_log_key'] ? 'public' : '';
}
$log_data = array(
'ip' => $post_meta['_edd_log_request_ip'],
'user_id' => $post_meta['_edd_log_user'],
'api_key' => $post_meta['_edd_log_key'],
'token' => $post_meta['_edd_log_token'],
'version' => $post_meta['_edd_log_version'],
'time' => $post_meta['_edd_log_time'],
'request' => $data->post_excerpt,
'error' => $data->post_content,
'date_created' => $data->post_date_gmt,
'date_modified' => $data->post_modified_gmt,
);
$meta_to_remove = array(
'_edd_log_request_ip',
'_edd_log_user',
'_edd_log_key',
'_edd_log_token',
'_edd_log_version',
'_edd_log_time',
);
$meta_to_migrate = $post_meta;
$new_log_id = edd_add_api_request_log( $log_data );
$add_meta_function = 'edd_add_api_request_log_meta';
} else {
$post_meta = get_post_custom( $data->ID );
foreach ( $post_meta as $key => $value ) {
$meta_to_migrate[ $key ] = maybe_unserialize( $value[0] );
}
$log_data = array(
'object_id' => $data->post_parent,
'object_type' => 'download',
'user_id' => ! empty( $meta_to_migrate['_edd_log_user'] ) ? $meta_to_migrate['_edd_log_user'] : $data->post_author,
'type' => $data->slug,
'title' => $data->post_title,
'content' => $data->post_content,
'date_created' => $data->post_date_gmt,
'date_modified' => $data->post_modified_gmt,
);
$meta_to_remove = array(
'_edit_lock',
'_edd_log_user',
);
$new_log_id = edd_add_log( $log_data );
$add_meta_function = 'edd_add_log_meta';
}
if ( ! is_callable( $add_meta_function ) || empty( $meta_to_migrate ) ) {
return;
}
foreach ( $meta_to_migrate as $key => $value ) {
if ( ! in_array( $key, $meta_to_remove, true ) ) {
// Strip off `_edd_log_` prefix.
$key = str_replace( '_edd_log_', '', $key );
$add_meta_function( $new_log_id, $key, $value );
}
}
}
/**
* Order notes.
*
* @since 3.0
*
* @param object $data Data to migrate.
*/
public static function order_notes( $data = null ) {
// Bail if no data passed.
if ( ! $data ) {
return;
}
$note_data = array(
'object_id' => $data->object_id,
'object_type' => 'order',
'date_created' => $data->comment_date_gmt,
'date_modified' => $data->comment_date_gmt,
'content' => $data->comment_content,
'user_id' => $data->user_id,
);
$id = edd_add_note( $note_data );
$meta = get_comment_meta( $data->comment_ID );
if ( ! empty( $meta ) ) {
foreach ( $meta as $key => $value ) {
edd_add_note_meta( $id, $key, $value );
}
}
}
public static function orders( $data = null ) {
// Bail if no data passed.
if ( ! $data ) {
return false;
}
/** Create a new order ***************************************/
global $wpdb;
// Get's all the post meta for this payment.
$meta = get_post_custom( $data->ID );
$payment_meta = maybe_unserialize( $meta['_edd_payment_meta'][0] );
$user_info = isset( $payment_meta['user_info'] ) ? maybe_unserialize( $payment_meta['user_info'] ) : array();
// Some old EDD data has the user info serialized, but starting with something other than a: so it can't be unserialized
$user_info = self::fix_possible_serialization( $user_info );
$user_info = maybe_unserialize( $user_info );
if ( ! is_array( $user_info ) ) {
$user_info = array();
}
/**
* Last chance to filter payment meta before we use it!
* Note: If modifying `cart_details`, then it's recommended that you first run
* `EDD\Admin\Upgrades\v3\Data_Migrator::fix_possible_serialization()`
* before making adjustments.
*
* @since 3.0
*
* @param array $payment_meta Payment meta.
* @param int $payment_id ID of the payment.
* @param array $meta All post meta.
*/
$payment_meta = apply_filters( 'edd_30_migration_payment_meta', $payment_meta, $data->ID, $meta );
$order_number = isset( $meta['_edd_payment_number'][0] ) ? $meta['_edd_payment_number'][0] : '';
$user_id = isset( $meta['_edd_payment_user_id'][0] ) && ! empty( $meta['_edd_payment_user_id'][0] ) ? $meta['_edd_payment_user_id'][0] : 0;
$ip = isset( $meta['_edd_payment_user_ip'][0] ) ? $meta['_edd_payment_user_ip'][0] : '';
$mode = isset( $meta['_edd_payment_mode'][0] ) ? $meta['_edd_payment_mode'][0] : 'live';
$gateway = isset( $meta['_edd_payment_gateway'][0] ) && ! empty( $meta['_edd_payment_gateway'][0] ) ? $meta['_edd_payment_gateway'][0] : 'manual';
$customer_id = isset( $meta['_edd_payment_customer_id'][0] ) ? $meta['_edd_payment_customer_id'][0] : 0;
$date_completed = isset( $meta['_edd_completed_date'][0] ) ? $meta['_edd_completed_date'][0] : null;
$purchase_key = isset( $meta['_edd_payment_purchase_key'][0]) ? $meta['_edd_payment_purchase_key'][0] : false;
$purchase_email = isset( $meta['_edd_payment_user_email'][0] ) ? $meta['_edd_payment_user_email'][0] : $payment_meta['email'];
// Get the customer object
if ( ! empty( $customer_id ) ) {
$customer = edd_get_customer( $customer_id );
} else if ( ! empty( $purchase_email ) ) {
$customer = edd_get_customer_by( 'email', $purchase_email );
if ( $customer ) {
$customer_id = $customer->id;
}
}
if ( false === $purchase_key ) {
$purchase_key = isset( $payment_meta['key'] ) ? $payment_meta['key'] : '';
}
// Do not use -1 as the user ID.
$user_id = ( -1 === $user_id )
? 0
: $user_id;
// Account for possible double serialization of the cart_details
$cart_details = isset( $payment_meta['cart_details'] ) ? maybe_unserialize( $payment_meta['cart_details'] ) : array();
// Some old EDD data has the cart details serialized, but starting with something other than a: so it can't be unserialized
$cart_details = self::fix_possible_serialization( $cart_details );
// Some old cart data does not contain subtotal or discount information. Normalize it.
$cart_details = self::normalize_cart_details( $cart_details );
// Account for possible double serialization of the cart_details
$cart_downloads = isset( $payment_meta['downloads'] ) ? maybe_unserialize( $payment_meta['downloads'] ) : array();
// Some old EDD data has the downloads serialized, but starting with something other than a: so it can't be unserialized
$cart_downloads = self::fix_possible_serialization( $cart_downloads );
// If the order status is 'publish' convert it to the new 'complete' status.
$order_status = 'publish' === $data->post_status ? 'complete' : $data->post_status;
// If there are no items, and it's abandoned, just return, since this isn't a valid order.
if ( 'abandoned' === $order_status && empty( $cart_downloads ) && empty( $cart_details ) ) {
edd_debug_log( 'Skipping order ' . $data->ID . ' due to abandoned status and no products.', true );
return false;
}
$order_subtotal = 0;
$order_tax = 0;
$order_discount = 0;
$order_total = 0;
// Track the total value of added fees in case the Order was initially migrated
// without _edd_payment_total or _edd_payment_tax and manual calculation was needed.
$order_fees_tax = 0;
$order_fees_total = 0;
$order_items_fees_tax = 0;
// Retrieve the tax amount from metadata if available.
$meta_tax = isset( $meta['_edd_payment_tax'] )
? $meta['_edd_payment_tax']
: false;
if ( false !== $meta_tax ) {
$meta_tax = maybe_unserialize( $meta_tax );
$order_tax = (float) $meta_tax[0];
}
$meta_total = false;
// Retrieve the total amount from metadata if available.
if ( isset( $meta['_edd_payment_total'] ) ) {
$meta_total = maybe_unserialize( $meta['_edd_payment_total'] );
$order_total = (float) $meta_total[0];
} elseif ( isset( $payment_meta['amount'] ) ) {
$meta_total = maybe_unserialize( $payment_meta['amount'] );
$order_total = (float) $meta_total;
}
// In some cases (very few) there is no cart details...so we have to just avoid this part.
if ( ! empty( $cart_details ) && is_array( $cart_details ) ) {
// Loop through the items in the purchase to build the totals.
foreach ( $cart_details as $cart_item ) {
$order_subtotal += $cart_item['subtotal'];
// Add the cart line item tax amount if a total is not available on the order.
if ( false === $meta_tax ) {
$order_tax += $cart_item['tax'];
}
$order_discount += $cart_item['discount'];
// Add the cart line item price amount (includes tax, order item fee, _but not order item fee tax_)
// if a total is not available on the order.
if ( false === $meta_total ) {
$order_total += $cart_item['price'];
}
}
}
// Account for a situation where the post_date_gmt is set to 0000-00-00 00:00:00
$date_created_gmt = $data->post_date_gmt;
if ( '0000-00-00 00:00:00' === $date_created_gmt ) {
$date_created_gmt = new \DateTime( $data->post_date );
$modified_time = new \DateTime( $data->post_modified );
$modified_time_gmt = new \DateTime( $data->post_modified_gmt );
if ( $modified_time != $modified_time_gmt ) {
$diff = $modified_time_gmt->diff( $modified_time );
$time_diff = 'PT';
// Add hours to the offset string.
if ( ! empty( $diff->h ) ) {
$time_diff .= $diff->h . 'H';
}
// Add minutes to the offset string.
if ( ! empty( $diff->i ) ) {
$time_diff .= $diff->i . 'M';
}
// Account for -/+ GMT offsets.
try {
if ( 1 === $diff->invert ) {
$date_created_gmt->add( new \DateInterval( $time_diff ) );
} else {
$date_created_gmt->sub( new \DateInterval( $time_diff ) );
}
} catch ( \Exception $e ) {
}
}
$date_created_gmt = $date_created_gmt->format('Y-m-d H:i:s');
}
// Maybe convert the date completed to UTC or backfill the date_completed.
$non_completed_statuses = apply_filters( 'edd_30_noncomplete_statuses', edd_get_incomplete_order_statuses() );
if ( ! in_array( $order_status, $non_completed_statuses, true ) ) {
if ( ! empty( $date_completed ) ) { // Update the data_completed to the UTC.
try {
$date_completed = EDD()->utils->date( $date_completed, edd_get_timezone_id() )->setTimezone( 'UTC' )->toDateTimeString();
} catch ( \Exception $e ) {
$date_completed = $date_created_gmt;
}
} elseif ( is_null( $date_completed ) ) { // Backfill a missing date_completed (for things like recurring payments).
$date_completed = $date_created_gmt;
}
}
if ( 'manual_purchases' === $gateway && isset( $meta['_edd_payment_total'][0] ) ) {
$gateway = 'manual';
$order_total = $meta['_edd_payment_total'][0];
}
/*
* Build up the order address data. Actual insertion happens later, but we need this now to figure out the tax rate.
*/
// First & last name.
$user_info['first_name'] = ! empty( $user_info['first_name'] )
? $user_info['first_name']
: '';
$user_info['last_name'] = ! empty( $user_info['last_name'] )
? $user_info['last_name']
: '';
// Add order address.
$user_info['address'] = ! empty( $user_info['address'] )
? $user_info['address']
: array();
$user_info['address'] = wp_parse_args( $user_info['address'], array(
'line1' => '',
'line2' => '',
'city' => '',
'zip' => '',
'country' => '',
'state' => '',
) );
$order_address_data = array(
'name' => trim( $user_info['first_name'] . ' ' . $user_info['last_name'] ),
'address' => isset( $user_info['address']['line1'] ) ? $user_info['address']['line1'] : '',
'address2' => isset( $user_info['address']['line2'] ) ? $user_info['address']['line2'] : '',
'city' => isset( $user_info['address']['city'] ) ? $user_info['address']['city'] : '',
'region' => isset( $user_info['address']['state'] ) ? $user_info['address']['state'] : '',
'country' => isset( $user_info['address']['country'] ) && array_key_exists( strtoupper( $user_info['address']['country'] ), edd_get_country_list() )
? $user_info['address']['country']
: '',
'postal_code' => isset( $user_info['address']['zip'] ) ? $user_info['address']['zip'] : '',
'date_created' => $date_created_gmt,
);
$tax_rate_id = null;
$tax_rate = isset( $meta['_edd_payment_tax_rate'][0] )
? (float) $meta['_edd_payment_tax_rate'][0]
: 0.00;
/*
* Previously tax rates were stored as a decimal (e.g. `0.2`) but they're now stored as a percentage
* (e.g. `20`). So we need to convert.
*/
if ( $tax_rate < 1 ) {
$tax_rate = $tax_rate * 100;
}
$set_tax_rate_meta = false;
if ( ! empty( $tax_rate ) ) {
// Fetch the actual tax rate object for the order region & country.
$tax_rate_object = edd_get_tax_rate_by_location( array(
'country' => $order_address_data['country'],
'region' => $order_address_data['region'],
) );
if ( ! empty( $tax_rate_object->id ) && $tax_rate_object->amount == $tax_rate ) {
$tax_rate_id = $tax_rate_object->id;
}
}
/*
* If we cannot find a matching Adjustment object, we should save this in order meta so it isn't lost.
*/
if ( ! empty( $tax_rate ) && empty( $tax_rate_id ) ) {
$set_tax_rate_meta = true;
}
// Build the order data before inserting.
$order_data = array(
'id' => $data->ID,
'parent' => $data->post_parent,
'order_number' => $order_number,
'status' => $order_status,
'type' => 'sale',
'date_created' => $date_created_gmt, // GMT is stored in the database as the offset is applied by the new query classes.
'date_modified' => $data->post_modified_gmt, // GMT is stored in the database as the offset is applied by the new query classes.
'date_completed' => $date_completed,
'user_id' => $user_id,
'customer_id' => $customer_id,
'email' => $purchase_email,
'ip' => $ip,
'gateway' => $gateway,
'mode' => $mode,
'currency' => ! empty( $payment_meta['currency'] ) ? $payment_meta['currency'] : edd_get_currency(),
'payment_key' => $purchase_key,
'tax_rate_id' => $tax_rate_id,
'subtotal' => $order_subtotal,
'tax' => $order_tax,
'discount' => $order_discount,
'total' => $order_total,
);
/**
* Filters the data used to create the order.
*
* @since 3.0
*
* @param array $order_data Order creation arguments.
* @param array $payment_meta Payment meta.
* @param array $cart_details Cart details.
* @param array $meta All payment meta.
*/
$order_data = apply_filters( 'edd_30_migration_order_creation_data', $order_data, $payment_meta, $cart_details, $meta );
// Remove all order status transition actions.
remove_all_actions( 'edd_transition_order_status' );
remove_all_actions( 'edd_transition_order_item_status' );
remove_action( 'edd_order_item_added', 'edd_recalculate_order_item_download' );
remove_action( 'edd_order_item_updated', 'edd_recalculate_order_item_download' );
remove_action( 'edd_order_item_deleted', 'edd_recalculate_order_item_download' );
remove_action( 'edd_order_adjustment_added', 'edd_recalculate_order_adjustment_download' );
remove_action( 'edd_order_adjustment_updated', 'edd_recalculate_order_adjustment_download' );
$order_id = edd_add_order( $order_data );
// Save an un-matched tax rate in order meta.
if ( $set_tax_rate_meta ) {
edd_add_order_meta( $order_id, 'tax_rate', $tax_rate );
}
// Do not pass the original order ID into other arrays
unset( $order_data['id'] );
// Reset the $refund_id variable so that we don't end up accidentally creating refunds.
$refund_id = 0;
// If the order status is 'refunded', we need to generate a new order with the type of 'refund'.
if ( 'refunded' === $order_status ) {
// Since the refund is a near copy of the original order, copy over the arguments.
$refund_data = $order_data;
$refund_data['parent'] = $order_id;
$refund_data['order_number'] = $order_id . apply_filters( 'edd_order_refund_suffix', '-R-' ) . '1';
$refund_data['type'] = 'refund';
$refund_data['status'] = 'complete';
// Negate the amounts
$refund_data['subtotal'] = edd_negate_amount( $order_subtotal );
$refund_data['tax'] = edd_negate_amount( $order_tax );
$refund_data['discount'] = edd_negate_amount( $order_discount );
$refund_data['total'] = edd_negate_amount( $order_total );
// These are the best guess at the date it was refunded since we didn't store that prior.
$refund_data['date_created'] = $data->post_modified_gmt;
$refund_data['date_modified'] = $data->post_modified_gmt;
$refund_id = edd_add_order( $refund_data );
}
// Remove empty data.
$order_address_data = array_filter( $order_address_data );
if ( ! empty( $order_address_data ) ) {
// Add to edd_order_addresses table.
$order_address_data['order_id'] = $order_id;
edd_add_order_address( $order_address_data );
}
// Maybe add the address to the edd_customer_addresses.
$customer_address_data = $order_address_data;
// We don't need to pass this data to edd_maybe_add_customer_address().
unset( $customer_address_data['order_id'] );
unset( $customer_address_data['first_name'] );
unset( $customer_address_data['last_name'] );
// If possible, set the order date as the address creation date.
$customer_address_data['date_created'] = $date_created_gmt;
// Maybe add address to customer record.
edd_maybe_add_customer_address( $customer_id, $customer_address_data );
// Maybe add email address to customer record
if ( ! empty( $customer ) && $customer instanceof \EDD_Customer ) {
$type = ( $customer->email === $purchase_email ) ? 'primary' : 'secondary';
edd_add_customer_email_address(
array(
'customer_id' => $customer_id,
'date_created' => $date_created_gmt,
'email' => $purchase_email,
'type' => $type,
)
);
}
/** Migrate meta *********************************************/
// Unlimited downloads meta is not an order property, so we set it on the order meta for the new order ID.
if ( isset( $meta['_edd_payment_unlimited_downloads'] ) && ! empty( $meta['_edd_payment_unlimited_downloads'][0] ) ) {
edd_add_order_meta( $order_id, 'unlimited_downloads', $meta['_edd_payment_unlimited_downloads'][0] );
}
// Transaction IDs are no longer meta, and have their own table and data set, so we need to add the transactions.
$transaction_id = ! empty( $meta['_edd_payment_transaction_id'][0] ) ? $meta['_edd_payment_transaction_id'][0] : false;
// If we have no transaction ID & the gateway was PayPal, let's check in old payment notes.
if ( empty( $transaction_id ) && false !== strpos( $gateway, 'paypal' ) ) {
$transaction_id = self::find_transaction_id_from_notes( $order_id );
}
if ( ! empty( $transaction_id ) ) {
edd_add_order_transaction( array(
'object_id' => $order_id,
'object_type' => 'order',
'transaction_id' => $transaction_id,
'gateway' => $gateway,
'status' => 'complete',
'total' => $order_total,
'date_created' => $date_completed,
'date_modified' => $date_completed,
) );
}
/**
* By default, this is what is stored in payment meta. These array keys are part of the core payment meta in 2.x
* but are not needed as part of the order meta and will not be migrated.
* Extensions can add their keys to this filter if they use the payment meta array to store data and have
* established a migration process to keep the data intact with the new order tables.
*
* @since 3.0
* @param array The array of payment meta keys.
*/
$core_meta_keys = apply_filters( 'edd_30_payment_meta_keys_not_migrated', array(
'fees',
'key',
'email',
'date',
'downloads',
'cart_details',
'currency',
'discount',
'subtotal',
'tax',
'amount',
'user_id',
) );
// Remove core keys from `user_info`.
$remaining_user_info = false;
if ( ! empty( $user_info ) ) {
/**
* Array keys which are part of the core `user_info` in payment meta which are not needed as part of the order meta.
* Extensions can add their keys to this filter if they use the `user_info` array to store data and have
* established a migration process to keep the data intact with the new order tables.
*
* @since 3.0
* @param array The array of user info keys.
*/
$core_user_info = apply_filters( 'edd_30_core_user_info', array( 'id', 'email', 'first_name', 'last_name', 'discount', 'address', 'user_id' ) );
$remaining_user_info = array_diff_key( $user_info, array_flip( $core_user_info ) );
}
// If an extension has added data to `user_info`, migrate it.
if ( $remaining_user_info ) {
$payment_meta['user_info'] = $remaining_user_info;
} else {
$core_meta_keys[] = 'user_info';
}
// Remove all the core payment meta from the array, and...
if ( is_array( $payment_meta ) ) {
$remaining_payment_meta = array_diff_key( $payment_meta, array_flip( $core_meta_keys ) );
// ..If we have extra payment meta, it needs to be migrated across.
if ( 0 < count( $remaining_payment_meta ) ) {
edd_add_order_meta( $order_id, 'payment_meta', $remaining_payment_meta );
}
}
/** Create order items ***************************************/
// Now we iterate through all the cart items and make rows in the order items table.
if ( ! empty( $cart_details ) ) {
foreach ( $cart_details as $key => $cart_item ) {
// Reset any conditional IDs to be safe.
$refund_order_item_id = 0;
// Get product name.
$product_name = isset( $cart_item['name'] )
? $cart_item['name']
: '';
// Get price ID.
$price_id = self::get_valid_price_id_for_cart_item( $cart_item );
if ( ! empty( $product_name ) ) {
$option_name = edd_get_price_option_name( $cart_item['id'], $price_id );
if ( ! empty( $option_name ) ) {
$product_name .= ' — ' . $option_name;
}
}
$order_item_args = array(
'order_id' => $order_id,
'product_id' => $cart_item['id'],
'product_name' => $product_name,
'price_id' => $price_id,
'cart_index' => $key,
'type' => 'download',
'status' => $order_status,
'quantity' => $cart_item['quantity'],
'amount' => (float) $cart_item['item_price'],
'subtotal' => (float) $cart_item['subtotal'],
'discount' => (float) $cart_item['discount'],
'tax' => $cart_item['tax'],
'total' => (float) $cart_item['price'],
'date_created' => $date_created_gmt,
'date_modified' => $data->post_modified_gmt,
);
/**
* Filters the arguments used to create the order item.
*
* @since 1.0
*
* @param array $order_item_args Order item arguments.
* @param array $cart_item Original cart item.
* @param array $payment_meta Payment meta.
* @param array $meta All meta.
*/
$order_item_args = apply_filters( 'edd_30_migration_order_item_creation_data', $order_item_args, $cart_item, $payment_meta, $meta );
$order_item_id = edd_add_order_item( $order_item_args );
if ( ! empty( $cart_item['item_number']['options'] ) ) {
// Collect any item_number options and store them.
// Remove our price_id and quantity, as they are columns on the order item now.
unset( $cart_item['item_number']['options']['price_id'] );
unset( $cart_item['item_number']['options']['quantity'] );
foreach ( $cart_item['item_number']['options'] as $option_key => $value ) {
$option_key = '_option_' . sanitize_key( $option_key );
edd_add_order_item_meta( $order_item_id, $option_key, $value );
}
}
// If the order status is refunded, we also need to add all the refunded order items on the refund order as well.
if ( ! empty( $refund_id ) ) {
// Since the refund is a near copy of the original order, copy over the arguments.
$refund_item_args = $order_item_args;
$refund_item_args['parent'] = $order_item_id;
$refund_item_args['order_id'] = $refund_id;
$refund_item_args['status'] = 'complete';
// Subtotal is actually set to subtotal - discount.
$refund_item_args['subtotal'] = $refund_item_args['subtotal'] - $refund_item_args['discount'];
// Negate the amounts
$refund_item_args['quantity'] = edd_negate_int( $cart_item['quantity'] );
foreach( array( 'amount', 'subtotal', 'tax', 'total' ) as $field_to_negate ) {
$refund_item_args[ $field_to_negate ] = edd_negate_amount( $refund_item_args[ $field_to_negate ] );
}
// These are our best estimates since we did not store the refund date previously.
$refund_item_args['date_crated'] = $data->post_modified_gmt;
$refund_item_args['date_modified'] = $data->post_modified_gmt;
$refund_order_item_id = edd_add_order_item( $refund_item_args );
if ( ! empty( $cart_item['item_number']['options'] ) ) {
// Collect any item_number options and store them.
// Remove our price_id and quantity, as they are columns on the order item now.
unset( $cart_item['item_number']['options']['price_id'] );
unset( $cart_item['item_number']['options']['quantity'] );
foreach ( $cart_item['item_number']['options'] as $option_key => $value ) {
$option_key = '_option_' . sanitize_key( $option_key );
edd_add_order_item_meta( $refund_order_item_id, $option_key, $value );
}
}
}
// Store order item fees as adjustments.
if ( isset( $cart_item['fees'] ) && ! empty( $cart_item['fees'] ) ) {
foreach ( $cart_item['fees'] as $fee_id => $fee ) {
// Reset any conditional IDs to be safe.
$refund_adjustment_id = 0;
$tax = EDD()->fees->get_calculated_tax( $fee, $tax_rate );
$total = floatval( $fee['amount'] ) + $tax;
// Track order item fees tax to adjust order if needed.
$order_items_fees_tax += $tax;
// Add the adjustment.
$adjustment_args = array(
'object_id' => $order_item_id,
'object_type' => 'order_item',
'type_key' => $fee_id,
'type' => 'fee',
'description' => $fee['label'],
'subtotal' => floatval( $fee['amount'] ),
'tax' => $tax,
'total' => floatval( $fee['amount'] ) + $tax,
);
/**
* Filters the arguments used to create an order item adjustment.
*
* @since 3.0
*
* @param array $adjustment_args Adjustment arguments for a fee.
* @param array $fee Original fee data.
* @param array $cart_item Cart item this fee is part of.
* @param array $payment_meta Payment meta.
* @param array $meta All meta.
*/
$adjustment_args = apply_filters( 'edd_30_migration_order_item_adjustment_creation_data', $adjustment_args, $fee, $cart_item, $payment_meta, $meta );
$adjustment_id = edd_add_order_adjustment( $adjustment_args );
// If we refunded the main order, the fees also need to be added to the refund order type we created.
if ( ! empty( $refund_id ) ) {
$refund_adjustment_args = $adjustment_args;
$refund_adjustment_args['parent'] = $adjustment_id;
$refund_adjustment_args['object_id'] = $refund_order_item_id;
$refund_adjustment_args['subtotal'] = edd_negate_amount( floatval( $fee['amount'] ) );
$refund_adjustment_args['tax'] = edd_negate_amount( $tax );
$refund_adjustment_args['total'] = edd_negate_amount( floatval( $fee['amount'] ) + $tax );
$refund_adjustment_id = edd_add_order_adjustment( $refund_adjustment_args );
}
}
}
}
// Compatibility with older versions of EDD.
// Older versions stored a single dimensional array of download IDs.
} elseif ( is_array( $cart_downloads ) && count( $cart_downloads ) === count( $cart_downloads, COUNT_RECURSIVE ) ) {
foreach ( $cart_downloads as $cart_index => $download_id ) {
$download = edd_get_download( $download_id );
$order_item_args = array(
'order_id' => $order_id,
'product_id' => $download_id,
'product_name' => $download->post_name,
'price_id' => null,
'cart_index' => $cart_index,
'type' => 'download',
'quantity' => 1,
'amount' => (float) $payment_meta['amount'],
'subtotal' => (float) $payment_meta['amount'],
'discount' => 0.00,
'tax' => 0.00,
'total' => (float) $payment_meta['amount'],
'date_created' => $date_created_gmt,
'date_modified' => $data->post_modified_gmt,
);
$order_item_id = edd_add_order_item( $order_item_args );
// If the order was refunded, we also need to add these items to the refund order.
if ( ! empty( $refund_id ) ) {
// Since the refund is a near copy of the original order, copy over the arguments.
$refund_item_args = $order_item_args;
$refund_item_args['parent'] = $order_item_id;
$refund_item_args['order_id'] = $refund_id;
$refund_item_args['quantity'] = edd_negate_int( 1 );
$refund_item_args['amount'] = edd_negate_amount( (float) $payment_meta['amount'] );
$refund_item_args['subtotal'] = edd_negate_amount( (float) $payment_meta['amount'] );
$refund_item_args['total'] = edd_negate_amount( (float) $payment_meta['amount'] );
// These are the best guess at the time, since we didn't store this data previously.
$refund_item_args['date_created'] = $data->post_modified_gmt;
$refund_item_args['date_modified'] = $data->post_modified_gmt;
edd_add_order_item( $order_item_args );
}
}
}
/** Create order adjustments *********************************/
if ( isset( $payment_meta['fees'] ) && ! empty( $payment_meta['fees'] ) ) {
foreach ( $payment_meta['fees'] as $fee_id => $fee ) {
// Reset any conditional IDs to be safe.
$refund_adjustment_id = 0;
if ( ! empty( $fee['download_id'] ) ) {
continue;
}
$tax = EDD()->fees->get_calculated_tax( $fee, $tax_rate );
$total = floatval( $fee['amount'] ) + $tax;
$order_fees_tax += $tax;
$order_fees_total += $total;
// Add the adjustment.
$adjustment_args = array(
'object_id' => $order_id,
'object_type' => 'order',
'type_key' => $fee_id,
'type' => 'fee',
'description' => $fee['label'],
'subtotal' => floatval( $fee['amount'] ),
'tax' => $tax,
'total' => $total,
'date_created' => $date_created_gmt,
'date_modified' => $data->post_modified_gmt,
);
/**
* Filters the order adjustment arguments.
*
* @since 3.0
*
* @param array $adjustment_args Arguments used to create the order adjustment.
* @param array $fee Fee data.
* @param array $payment_meta Payment meta.
* @param array $meta All meta.
*/
$adjustment_args = apply_filters( 'edd_30_migration_order_adjustment_creation_data', $adjustment_args, $fee, $payment_meta, $meta );
$adjustment_id = edd_add_order_adjustment( $adjustment_args );
if ( ! empty( $refund_id ) ) {
// Since the refund is a near copy of the original order, copy over the arguments.
$refund_adjustment_args = $adjustment_args;
$refund_adjustment_args['parent'] = $adjustment_id;
$refund_adjustment_args['object_id'] = $refund_id;
// Negate the amounts.
$refund_adjustment_args['subtotal'] = edd_negate_amount( floatval( $fee['amount'] ) );
$refund_adjustment_args['tax'] = edd_negate_amount( $tax );
$refund_adjustment_args['total'] = edd_negate_amount( floatval( $fee['amount'] ) + $tax );
$refund_adjustment_id = edd_add_order_adjustment( $refund_adjustment_args );
}
}
}
// Add fee taxes (order and order item) if the order tax amount was previously manually calculated.
if ( false === $meta_tax ) {
edd_update_order( $order_id, array(
'tax' => $order_tax + $order_fees_tax + $order_items_fees_tax,
) );
}
// Add fee totals (order and order item) if the order tax amount was previously manually calculated.
// Order item fees were previously included in the total calculation. We must manually include
// order item fee tax amounts, and order fees total (subtotal + tax).
if ( false === $meta_total ) {
edd_update_order( $order_id, array(
'total' => $order_total + $order_fees_total + $order_items_fees_tax,
) );
}
// Insert discounts.
$discounts = ! empty( $user_info['discount'] )
? $user_info['discount']
: array();
if ( ! is_array( $discounts ) ) {
$discounts = explode( ',', $discounts );
}
if ( ! empty( $discounts ) && ( 'none' !== $discounts[0] ) ) {
if ( 1 === count( $discounts ) ) {
$discount_code = reset( $discounts );
/** @var \EDD_Discount $discount_object */
$discount_object = edd_get_discount_by( 'code', $discount_code );
if ( $discount_object instanceof \EDD_Discount ) {
$discount_args = array(
'object_id' => $order_id,
'object_type' => 'order',
'type_id' => $discount_object->id,
'type' => 'discount',
'description' => $discount_object->code,
'subtotal' => $order_discount,
'total' => $order_discount,
'date_created' => $date_created_gmt,
'date_modified' => $data->post_modified_gmt,
);
/**
* Filters the arguments used to create a discount adjustment.
*
* @since 3.0
*
* @param array $discount_args Order adjustment arguments.
* @param \EDD_Discount $discount_object Discount object.
* @param float $order_subtotal Order subtotal.
* @param array $user_info User info array.
* @param array $payment_meta Payment meta.
* @param array $meta All post meta.
*/
$discount_args = apply_filters( 'edd_30_migration_order_discount_creation_data', $discount_args, $discount_object, $order_subtotal, $user_info, $payment_meta, $meta );
$new_discount_id = edd_add_order_adjustment( $discount_args );
if ( $order_discount <= 0 ) {
edd_add_order_adjustment_meta(
$new_discount_id,
'migrated_order_discount_unknown',
(int) $order_id,
true
);
}
}
} else {
foreach ( $discounts as $discount_code ) {
/** @var \EDD_Discount $discount_object */
$discount_object = edd_get_discount_by( 'code', $discount_code );
if ( false === $discount_object ) {
continue;
}
$calculated_discount = $order_subtotal - $discount_object->get_discounted_amount( $order_subtotal );
$discount_args = array(
'object_id' => $order_id,
'object_type' => 'order',
'type_id' => $discount_object->id,
'type' => 'discount',
'description' => $discount_object->code,
'subtotal' => $calculated_discount,
'total' => $calculated_discount,
'date_created' => $date_created_gmt,
'date_modified' => $data->post_modified_gmt,
);
/**
* Filters the arguments used to create a discount adjustment.
*
* @since 3.0
*
* @param array $discount_args Order adjustment arguments.
* @param \EDD_Discount $discount_object Discount object.
* @param float $order_subtotal Order subtotal.
* @param array $user_info User info array.
* @param array $payment_meta Payment meta.
* @param array $meta All post meta.
*/
$discount_args = apply_filters( 'edd_30_migration_order_discount_creation_data', $discount_args, $discount_object, $order_subtotal, $user_info, $payment_meta, $meta );
$new_discount_id = edd_add_order_adjustment( $discount_args );
if ( $calculated_discount <= 0 ) {
edd_add_order_adjustment_meta(
$new_discount_id,
'migrated_order_discount_unknown',
(int) $order_id,
true
);
}
}
}
}
/** Create order meta ****************************************/
$core_meta_keys = array(
'_edd_payment_user_email',
'_edd_payment_customer_id',
'_edd_payment_user_id',
'_edd_payment_user_ip',
'_edd_payment_purchase_key',
'_edd_payment_total',
'_edd_payment_mode',
'_edd_payment_gateway',
'_edd_payment_meta',
'_edd_payment_tax',
'_edd_payment_tax_rate',
'_edd_completed_date',
'_edd_payment_unlimited_downloads',
'_edd_payment_number',
'_edd_payment_transaction_id',
);
// Determine what main payment meta keys were from core and what were custom...
$remaining_meta = array_diff_key( $meta, array_flip( $core_meta_keys ) );
// ...and whatever is not from core, needs to be added as new order meta.
foreach ( $remaining_meta as $meta_key => $meta_value ) {
$meta_value = maybe_unserialize( $meta_value[0] );
edd_add_order_meta( $order_id, $meta_key, $meta_value );
}
/**
* Now that we're done, let's run a hook here so we can allow extensions to make any necessary changes.
*
* @since 3.0
* @param int $order_id The order ID.
* @param array $payment_meta The `_edd_payment_meta` value for the original payment.
* @param array $meta All post meta associated with the payment.
*/
do_action( 'edd_30_migrate_order', $order_id, $payment_meta, $meta );
return $order_id;
}
/**
* Retrieves a valid price ID for a given cart item.
* If the product does not have variable prices, then `null` is always returned.
* If the supplied price ID does not match a price ID that actually exists, then the default
* variable price is returned instead of the supplied one.
*
* @since 3.0
*
* @param array $cart_item Array of cart item details.
*
* @return int|null
*/
protected static function get_valid_price_id_for_cart_item( $cart_item ) {
// If the product doesn't have variable prices, just return `null`.
if ( ! edd_has_variable_prices( $cart_item['id'] ) ) {
return null;
}
$variable_prices = edd_get_variable_prices( $cart_item['id'] );
if ( ! is_array( $variable_prices ) || empty( $variable_prices ) ) {
return null;
}
// Return the price ID that's set to the cart item right now, if not numeric return NULL.
return isset( $cart_item['item_number']['options']['price_id'] ) && is_numeric( $cart_item['item_number']['options']['price_id'] )
? absint( $cart_item['item_number']['options']['price_id'] )
: null;
}
/**
* Attempts to locate a PayPal transaction ID from legacy payment notes.
*
* @since 3.0
*
* @param int $payment_id
*
* @return string|false Transaction ID on success, false if not found.
*/
private static function find_transaction_id_from_notes( $payment_id ) {
global $wpdb;
$payment_notes = $wpdb->get_col( $wpdb->prepare(
"SELECT comment_content FROM {$wpdb->comments} WHERE comment_post_ID = %d",
$payment_id
) );
if ( empty( $payment_notes ) || ! is_array( $payment_notes ) ) {
return false;
}
foreach ( $payment_notes as $note ) {
if ( preg_match( '/^PayPal Transaction ID: ([^\s]+)/', $note, $match ) ) {
return $match[1];
}
}
return false;
}
/**
* Tax rates.
*
* @since 3.0
*
* @param object $data Data to migrate.
*/
public static function tax_rates( $data = null ) {
// Bail if no data passed.
if ( ! $data ) {
return;
}
$scope = ! empty( $data['global'] )
? 'country'
: 'region';
// If the scope is 'country', look for other active rates that are country wide and set them as 'inactive'.
if ( 'country' === $scope ) {
$tax_rates = edd_get_adjustments(
array(
'type' => 'tax_rate',
'status' => 'active',
'scope' => 'country',
'name' => $data['country'],
)
);
if ( ! empty( $tax_rates ) ) {
foreach ( $tax_rates as $tax_rate ) {
edd_update_adjustment(
$tax_rate->id,
array( 'status' => 'inactive', )
);
}
}
}
$adjustment_data = array(
'name' => $data['country'],
'scope' => $scope,
'amount' => floatval( $data['rate'] ),
);
if ( ! empty( $data['state'] ) ) {
$adjustment_data['description'] = sanitize_text_field( $data['state'] );
}
edd_add_tax_rate( $adjustment_data );
}
/**
* Normalizes and backfills legacy payment cart data.
*
* @since 3.0.0
*
* @param array|string $cart_details Cart details. No action is performed if a string
* (array cannot be unserialized) is provided.
* @return array|string
*/
private static function normalize_cart_details( $cart_details ) {
if ( ! is_array( $cart_details ) ) {
return $cart_details;
}
foreach ( $cart_details as &$cart_item ) {
// Get price.
$cart_item['price'] = isset( $cart_item['price'] )
? (float) $cart_item['price']
: 0.00;
// Get item price.
$cart_item['item_price'] = isset( $cart_item['item_price'] )
? (float) $cart_item['item_price']
: (float) $cart_item['price'];
// Get quantity.
$cart_item['quantity'] = isset( $cart_item['quantity'] )
? $cart_item['quantity']
: 1;
// Get subtotal.
$cart_item['subtotal'] = isset( $cart_item['subtotal'] )
? (float) $cart_item['subtotal']
: (float) $cart_item['quantity'] * $cart_item['item_price'];
// Get discount.
$cart_item['discount'] = isset( $cart_item['discount'] )
? (float) $cart_item['discount']
: 0.00;
// Get tax.
$cart_item['tax'] = isset( $cart_item['tax'] )
? (float) $cart_item['tax']
: 0.00;
}
return $cart_details;
}
/**
* Given that some data quite possible has bad serialization, we need to possibly fix the bad serialization.
*
* @since 3.0.0
*
* @param $data
*
* @return mixed
*/
public static function fix_possible_serialization( $data ) {
if ( ! is_array( $data ) && is_string( $data ) ) {
$data = substr_replace( $data, 'a', 0, 1 );
}
return $data;
}
}