laipower/wp-content/plugins/easy-digital-downloads/includes/orders/functions/orders.php

1379 lines
41 KiB
PHP

<?php
/**
* Order Functions.
*
* @package EDD
* @subpackage Orders
* @copyright Copyright (c) 2018, Easy Digital Downloads, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 3.0
*/
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;
/**
* Add an order.
*
* @since 3.0
*
* @param array $data {
* Array of order data. Default empty.
*
* The `date_created` and `date_modified` parameters do not need to be passed.
* They will be automatically populated if empty.
*
* @type int $parent ID of the parent order. Default 0.
* @type string $order_number Order number, if enabled. Default empty.
* @type string $status Order status. Default `pending`.
* @type string $type Order type. Default `sale`.
* @type int $user_id WordPress user ID linked to the customer of
* the order. Default 0.
* @type int $customer_id ID of the customer of the order. Default 0.
* @type string $email Email address used for the order. Default empty.
* @type string $ip IP address of the client at checkout. Default empty.
* @type string $gateway Gateway used to process the order. Default empty.
* @type string $mode Store mode when order was placed. Default empty.
* @type string $currency Currency used for the order. Default empty.
* @type string $payment_key Payment key generated for the order. Default empty.
* @type int|null $tax_rate_id ID of the tax rate Adjustment associated with the order. Default null.
* @type float $subtotal Order subtotal. Default 0.
* @type float $discount Discount applied to the order. Default 0.
* @type float $tax Tax applied to the order. Default 0.
* @type float $total Order total. Default 0.
* @type string $date_created Optional. Automatically calculated on add/edit.
* The date & time the order was inserted.
* Format: YYYY-MM-DD HH:MM:SS. Default empty.
* @type string $date_modified Optional. Automatically calculated on add/edit.
* The date & time the order was last modified.
* Format: YYYY-MM-DD HH:MM:SS. Default empty.
* @type string|null $date_completed The date & time the order's status was
* changed to `complete`. Format: YYYY-MM-DD HH:MM:SS.
* Default null.
* @type string|null $date_refundable The date & time an order can be refunded until.
* Format: YYYY-MM-DD HH:MM:SS.
* }
* @return int|false ID of newly created order, false on error.
*/
function edd_add_order( $data = array() ) {
$orders = new EDD\Database\Queries\Order();
return $orders->add_item( $data );
}
/**
* Move an order to the trashed status
*
* @since 3.0
*
* @param $order_id
*
* @return bool true if the order was trashed successfully, false if not
*/
function edd_trash_order( $order_id ) {
if ( false === edd_is_order_trashable( $order_id ) ) {
return false;
}
$order = edd_get_order( $order_id );
$orders = new EDD\Database\Queries\Order();
$current_status = $order->status;
$trashed = $orders->update_item( $order_id, array(
'status' => 'trash',
) ); new EDD\Database\Queries\Order();
if ( ! empty( $trashed ) ) {
// If successfully trashed, store the pre-trashed status in meta, so we can possibly restore it.
edd_add_order_meta( $order_id, '_pre_trash_status', $current_status );
// Update the status of any order to 'trashed'.
$order_items = edd_get_order_items( array(
'order_id' => $order_id,
'no_found_rows' => true,
) );
$items = new EDD\Database\Queries\Order_Item();
foreach ( $order_items as $item ) {
$current_item_status = $item->status;
$item_trashed = $items->update_item( $item->id, array(
'status' => 'trash',
) );
if ( ! empty( $item_trashed ) ) {
edd_add_order_item_meta( $item->id, '_pre_trash_status', $current_item_status );
}
}
// Now look for any orders with the refund type.
$refund_orders = edd_get_orders( array(
'type' => 'refund',
'parent' => $order_id,
) );
if ( ! empty( $refund_orders ) ) {
foreach( $refund_orders as $refund ) {
$current_refund_status = $refund->status;
$refund_trashed = edd_trash_order( $refund->id );
if ( ! empty( $refund_trashed ) ) {
edd_add_order_meta( $refund->id, '_pre_trash_status', $current_refund_status );
}
}
}
}
return filter_var( $trashed, FILTER_VALIDATE_BOOLEAN );
}
/**
* Restore an order from the trashed status to it's previous status.
*
* @since 3.0
*
* @param $order_id
*
* @return bool true if the order was trashed successfully, false if not
*/
function edd_restore_order( $order_id ) {
if ( false === edd_is_order_restorable( $order_id ) ) {
return false;
}
$order = edd_get_order( $order_id );
if ( 'trash' !== $order->status ) {
return false;
}
$orders = new EDD\Database\Queries\Order();
$pre_trash_status = edd_get_order_meta( $order_id, '_pre_trash_status', true );
if ( empty( $pre_trash_status ) ) {
return false;
}
$restored = $orders->update_item( $order_id, array(
'status' => $pre_trash_status,
) );
if ( ! empty( $restored ) ) {
// If successfully trashed, store the pre-trashed status in meta, so we can possibly restore it.
edd_delete_order_meta( $order_id, '_pre_trash_status' );
// Update the status of any order to 'trashed'.
$order_items = edd_get_order_items( array(
'order_id' => $order_id,
'no_found_rows' => true,
) );
$items = new EDD\Database\Queries\Order_Item();
foreach ( $order_items as $item ) {
$pre_trash_status = edd_get_order_item_meta( $item->id, '_pre_trash_status', true );
if ( ! empty( $pre_trash_status ) ) {
$restored_item = $items->update_item( $item->id, array(
'status' => $pre_trash_status,
) );
if ( ! empty( $restored_item ) ) {
edd_delete_order_item_meta( $item->id, '_pre_trash_status' );
}
}
}
// Now look for any orders with the refund type.
$refund_orders = edd_get_orders( array(
'type' => 'refund',
'parent' => $order_id,
) );
if ( ! empty( $refund_orders ) ) {
foreach( $refund_orders as $refund ) {
edd_restore_order( $refund->id );
}
}
}
return filter_var( $restored, FILTER_VALIDATE_BOOLEAN );
}
/**
* Delete an order.
*
* @since 3.0
*
* @param int $order_id Order ID.
* @return int|false `1` if the order was deleted successfully, false on error.
*/
function edd_delete_order( $order_id = 0 ) {
$orders = new EDD\Database\Queries\Order();
return $orders->delete_item( $order_id );
}
/**
* Destroy an order.
*
* Completely deletes an order, and the items and adjustments with it.
*
* @todo switch to _destroy_ for items & adjustments
*
* @since 3.0
*
* @param int $order_id Order ID.
* @return int|false `1` if the order was deleted successfully, false on error.
*/
function edd_destroy_order( $order_id = 0 ) {
/**
* Action hook for developers to do extra work when an order is destroyed.
*
* @since 3.0
* @param int $order_id The original order ID.
*/
do_action( 'edd_pre_destroy_order', $order_id );
// Delete the order
$destroyed = edd_delete_order( $order_id );
if ( $destroyed ) {
// Get items.
$items = edd_get_order_items( array(
'order_id' => $order_id,
'no_found_rows' => true,
) );
// Destroy items (and their adjustments).
if ( ! empty( $items ) ) {
foreach ( $items as $item ) {
edd_delete_order_item( $item->id );
}
}
// Get adjustments.
$adjustments = edd_get_order_adjustments( array(
'object_id' => $order_id,
'object_type' => 'order',
'no_found_rows' => true,
) );
// Destroy adjustments.
if ( ! empty( $adjustments ) ) {
foreach ( $adjustments as $adjustment ) {
// Decrease discount code use count.
if ( 'discount' === $adjustment->type ) {
edd_decrease_discount_usage( $adjustment->description );
}
edd_delete_order_adjustment( $adjustment->id );
}
}
// Get address.
$address = edd_get_order_address_by( 'order_id', $order_id );
// Destroy address.
if ( $address ) {
edd_delete_order_address( $address->id );
}
// Now look for any orders with the refund type.
$refund_orders = edd_get_orders( array(
'type' => 'refund',
'parent' => $order_id,
) );
if ( ! empty( $refund_orders ) ) {
foreach( $refund_orders as $refund ) {
edd_destroy_order( $refund->id );
}
}
}
/**
* Action hook for developers to do extra work when an order is destroyed.
*
* @since 3.0
* @param int $order_id The original order ID.
* @param bool $destroyed Whether the order was destroyed.
*/
do_action( 'edd_order_destroyed', $order_id, $destroyed );
return $destroyed;
}
/**
* Update an order.
*
* @since 3.0
*
* @param int $order_id Order ID.
* @param array $data {
* Array of order data. Default empty.
*
* @type int $parent ID of the parent order. Default 0.
* @type string $order_number Order number, if enabled. Default empty.
* @type string $status Order status. Default `pending`.
* @type string $type Order type. Default `sale`.
* @type int $user_id WordPress user ID linked to the customer of
* the order. Default 0.
* @type int $customer_id ID of the customer of the order. Default 0.
* @type string $email Email address used for the order. Default empty.
* @type string $ip IP address of the client at checkout. Default empty.
* @type string $gateway Gateway used to process the order. Default empty.
* @type string $mode Store mode when order was placed. Default empty.
* @type string $currency Currency used for the order. Default empty.
* @type string $payment_key Payment key generated for the order. Default empty.
* @type int|float $tax_rate_id ID of the tax rate Adjustment associated with the order. Default empty.
* @type float $subtotal Order subtotal. Default 0.
* @type float $discount Discount applied to the order. Default 0.
* @type float $tax Tax applied to the order. Default 0.
* @type float $total Order total. Default 0.
* @type string $date_created Optional. Automatically calculated on add/edit.
* The date & time the order was inserted.
* Format: YYYY-MM-DD HH:MM:SS. Default empty.
* @type string $date_modified Optional. Automatically calculated on add/edit.
* The date & time the order was last modified.
* Format: YYYY-MM-DD HH:MM:SS. Default empty.
* @type string|null $date_completed The date & time the order's status was
* changed to `complete`. Format: YYYY-MM-DD HH:MM:SS.
* Default empty.
* @type string|null $date_refundable The date & time an order can be refunded until.
* Format: YYYY-MM-DD HH:MM:SS.
* }
*
* @return bool Whether or not the order was updated.
*/
function edd_update_order( $order_id = 0, $data = array() ) {
$orders = new EDD\Database\Queries\Order();
return $orders->update_item( $order_id, $data );
}
/**
* Get an order by ID.
*
* @since 3.0
*
* @param int $order_id Order ID.
* @return EDD\Orders\Order|false Order object if successful, false otherwise.
*/
function edd_get_order( $order_id = 0 ) {
$orders = new EDD\Database\Queries\Order();
$order = $orders->get_item( $order_id );
/**
* If the order is not retrieved but migration is pending, check for an old payment.
* @todo remove in 3.1
*/
if ( ! $order instanceof EDD\Orders\Order && _edd_get_final_payment_id() ) {
$post = get_post( $order_id );
if ( $post instanceof WP_Post ) {
include_once EDD_PLUGIN_DIR . 'includes/compat/class-edd-payment-compat.php';
$payment_compat = new EDD_Payment_Compat( $order_id );
return $payment_compat->order;
}
}
return $order;
}
/**
* Get an order by a specific field value.
*
* @since 3.0
*
* @param string $field Database table field.
* @param string $value Value of the row.
*
* @return EDD\Orders\Order|false Order object if successful, false otherwise.
*/
function edd_get_order_by( $field = '', $value = '' ) {
$orders = new EDD\Database\Queries\Order();
// Return order
return $orders->get_item_by( $field, $value );
}
/**
* Query for orders.
*
* @see \EDD\Database\Queries\Order::__construct()
*
* @since 3.0
*
* @param array $args Arguments. See `EDD\Database\Queries\Order` for
* accepted arguments.
* @return EDD\Orders\Order[] Array of `Order` objects.
*/
function edd_get_orders( $args = array() ) {
// Parse args
$r = wp_parse_args( $args, array(
'number' => 30,
) );
// Instantiate a query object
$orders = new EDD\Database\Queries\Order();
// Return orders
return $orders->query( $r );
}
/**
* Count orders.
*
* @see \EDD\Database\Queries\Order::__construct()
*
* @since 3.0
*
* @param array $args Arguments. See `EDD\Database\Queries\Order` for
* accepted arguments.
* @return int Number of orders returned based on query arguments passed.
*/
function edd_count_orders( $args = array() ) {
// Parse args
$r = wp_parse_args( $args, array(
'count' => true,
) );
// Query for count(s)
$orders = new EDD\Database\Queries\Order( $r );
// Return count(s)
return absint( $orders->found_items );
}
/**
* Query for and return array of order counts, keyed by status.
*
* @see \EDD\Database\Queries\Order::__construct()
*
* @since 3.0
*
* @param array $args Arguments. See `EDD\Database\Queries\Order` for
* accepted arguments.
* @return array Order counts keyed by status.
*/
function edd_get_order_counts( $args = array() ) {
// Parse args
$r = wp_parse_args( $args, array(
'count' => true,
'groupby' => 'status',
'type' => 'sale'
) );
// Query for count
$counts = new EDD\Database\Queries\Order( $r );
// Format & return
return edd_format_counts( $counts, $r['groupby'] );
}
/** Helpers *******************************************************************/
/**
* Determine if an order ID is able to be trashed.
*
* @param $order_id
*
* @return bool
*/
function edd_is_order_trashable( $order_id ) {
$order = edd_get_order( $order_id );
$is_trashable = false;
if ( empty( $order ) ) {
return $is_trashable;
}
$non_trashable_statuses = apply_filters( 'edd_non_trashable_statuses', array( 'trash' ) );
if ( ! in_array( $order->status, $non_trashable_statuses ) ) {
$is_trashable = true;
}
return (bool) apply_filters( 'edd_is_order_trashable', $is_trashable, $order );
}
/**
* Determine if an order ID is able to be restored from the trash.
*
* @param $order_id
*
* @return bool
*/
function edd_is_order_restorable( $order_id ) {
$order = edd_get_order( $order_id );
$is_restorable = false;
if ( empty( $order ) ) {
return $is_restorable;
}
if ( 'trash' === $order->status ) {
$is_restorable = true;
}
return (bool) apply_filters( 'edd_is_order_restorable', $is_restorable, $order );
}
/**
* Check if an order can be recovered.
*
* @since 3.0
*
* @param int $order_id Order ID.
* @return bool True if the order can be recovered, false otherwise.
*/
function edd_is_order_recoverable( $order_id = 0 ) {
$order = edd_get_order( $order_id );
if ( ! $order ) {
return false;
}
$recoverable_statuses = edd_recoverable_order_statuses();
$transaction_id = $order->get_transaction_id();
if ( in_array( $order->status, $recoverable_statuses, true ) && empty( $transaction_id ) ) {
return true;
}
return false;
}
/**
* Update the status of an entire order.
*
* @since 3.0
*
* @param int $order_id Order ID.
* @param string $new_status New order status.
*
* @return bool True if the status was updated successfully, false otherwise.
*/
function edd_update_order_status( $order_id = 0, $new_status = '' ) {
// Bail if order and status are empty
if ( empty( $order_id ) || empty( $new_status ) ) {
return false;
}
// Get the order
$order = edd_get_order( $order_id );
// Bail if order not found
if ( empty( $order ) ) {
return false;
}
/**
* For backwards compatibility purposes, we need an instance of EDD_Payment so that the correct actions
* are invoked.
*/
$payment = edd_get_payment( $order_id );
// Override to `publish`
if ( in_array( $new_status, array( 'completed', 'publish' ), true ) ) {
$new_status = 'complete';
}
// Get the old (current) status
$old_status = $order->status;
// We do not allow status changes if the status is the same to that stored in the database.
// This prevents the `edd_update_payment_status` action from being triggered unnecessarily.
if ( $old_status === $new_status ) {
return false;
}
// Backwards compatibility
$do_change = apply_filters( 'edd_should_update_payment_status', true, $order_id, $new_status, $old_status );
$do_change = apply_filters( 'edd_should_update_order_status', $do_change, $order_id, $new_status, $old_status );
$updated = false;
if ( ! empty( $do_change ) ) {
/**
* We need to update the status on the EDD_Payment instance so that the
* correct actions are invoked if the status is changing to something
* that requires interception by the payment gateway (e.g. refunds).
*/
$payment->status = $new_status;
$updated = $payment->save();
}
return $updated;
}
/**
* Generate the correct parameters required to insert a new order into the database
* based on the order details passed by the gateway.
*
* @since 3.0
*
* @param array $order_data Order data.
* @return int|bool Order ID if successful, false otherwise.
*/
function edd_build_order( $order_data = array() ) {
// Bail if no order data passed.
if ( empty( $order_data ) ) {
return false;
}
/* Order recovery ********************************************************/
$resume_order = false;
$existing_order = EDD()->session->get( 'edd_resume_payment' );
if ( ! empty( $existing_order ) ) {
$order = edd_get_order( $existing_order );
if ( $order ) {
$recoverable_statuses = edd_recoverable_order_statuses();
$transaction_id = $order->get_transaction_id();
if ( in_array( $order->status, $recoverable_statuses, true ) && empty( $transaction_id ) ) {
$payment = edd_get_payment( $existing_order );
$resume_order = true;
}
}
}
if ( $resume_order ) {
$payment->add_note( __( 'Payment recovery processed', 'easy-digital-downloads' ) );
// Since things could have been added/removed since we first crated this...rebuild the cart details.
foreach ( $payment->fees as $fee_index => $fee ) {
$payment->remove_fee_by( 'index', $fee_index, true );
}
foreach ( $payment->downloads as $cart_index => $download ) {
$item_args = array(
'quantity' => isset( $download['quantity'] ) ? $download['quantity'] : 1,
'cart_index' => $cart_index,
);
$payment->remove_download( $download['id'], $item_args );
}
if ( strtolower( $payment->email ) !== strtolower( $order_data['user_info']['email'] ) ) {
// Remove the payment from the previous customer.
$previous_customer = new EDD_Customer( $payment->customer_id );
$previous_customer->remove_payment( $payment->ID, false );
// Redefine the email first and last names.
$payment->email = $order_data['user_info']['email'];
$payment->first_name = $order_data['user_info']['first_name'];
$payment->last_name = $order_data['user_info']['last_name'];
}
// Remove any remainders of possible fees from items.
$payment->save();
}
/** Setup order information ***********************************************/
$gateway = ! empty( $order_data['gateway'] ) ? $order_data['gateway'] : '';
$gateway = empty( $gateway ) && isset( $_POST['edd-gateway'] ) // WPCS: CSRF ok.
? sanitize_key( $_POST['edd-gateway'] )
: $gateway;
if ( ! $resume_order ) {
// Allow for post_date to be passed in.
if ( isset( $order_data['post_date'] ) ) {
$order_data['date_created'] = $order_data['post_date'];
unset( $order_data['post_date'] );
}
}
// Build order information based on data passed from the gateway.
$order_args = array(
'parent' => ! empty( $order_data['parent'] ) ? absint( $order_data['parent'] ) : '',
'order_number' => '',
'status' => ! empty( $order_data['status'] ) ? $order_data['status'] : 'pending',
'user_id' => ! empty( $order_data['user_info']['id'] ) ? $order_data['user_info']['id'] : 0,
'email' => $order_data['user_info']['email'],
'ip' => edd_get_ip(),
'gateway' => $gateway,
'mode' => edd_is_test_mode() ? 'test' : 'live',
'currency' => ! empty( $order_data['currency'] ) ? $order_data['currency'] : edd_get_currency(),
'payment_key' => ! empty( $order_data['purchase_key'] ) ? $order_data['purchase_key'] : edd_generate_order_payment_key( $order_data['user_info']['email'] ),
'date_created' => ! empty( $order_data['date_created'] ) ? $order_data['date_created'] : '',
);
/** Setup customer ********************************************************/
$customer = new stdClass();
if ( did_action( 'edd_pre_process_purchase' ) && is_user_logged_in() ) {
$customer = new EDD_Customer( get_current_user_id(), true );
// Customer is logged in but used a different email to purchase so we need to assign that email address to their customer record.
if ( ! empty( $customer->id ) && ( $order_args['email'] !== $customer->email ) ) {
$customer->add_email( $order_args['email'] );
}
}
if ( empty( $customer->id ) ) {
$customer = new EDD_Customer( $order_args['email'] );
if ( empty( $order_data['user_info']['first_name'] ) && empty( $order_data['user_info']['last_name'] ) ) {
$name = $order_args['email'];
} else {
$name = trim( $order_data['user_info']['first_name'] . ' ' . $order_data['user_info']['last_name'] );
}
$customer->create( array(
'name' => $name,
'email' => $order_args['email'],
'user_id' => $order_args['user_id'],
) );
}
// If the customer name was initially empty, update the record to store the name used at checkout.
if ( empty( $customer->name ) ) {
$customer->update( array(
'name' => $order_data['user_info']['first_name'] . ' ' . $order_data['user_info']['last_name'],
) );
}
$order_args['customer_id'] = $customer->id;
$country = ! empty( $order_data['user_info']['address']['country'] )
? $order_data['user_info']['address']['country']
: false;
$region = ! empty( $order_data['user_info']['address']['state'] )
? $order_data['user_info']['address']['state']
: false;
// If taxes are enabled, get the tax rate for the order location.
$tax_rate = false;
if ( edd_use_taxes() ) {
$tax_rate = edd_get_tax_rate_by_location(
array(
'country' => $country,
'region' => $region,
)
);
if ( ! empty( $tax_rate->id ) ) {
$order_args['tax_rate_id'] = $tax_rate->id;
}
// If no tax rate is found, then we'll save a percentage rate in order meta later.
}
/** Insert order **********************************************************/
// Add order into the edd_orders table.
if ( true === $resume_order ) {
$order_id = $payment->ID;
unset( $order_args['date_created'] );
edd_update_order( $order_id, $order_args );
} else {
$order_args['order_number'] = edd_set_order_number();
$order_id = edd_add_order( $order_args );
}
// If there is no order ID at this point, something went wrong.
if ( empty( $order_id ) ) {
return false;
}
// Attach order to the customer record.
$customer->attach_payment( $order_id, false );
// Declare variables to store amounts for the order.
$subtotal = 0.00;
$total_tax = 0.00;
$total_discount = 0.00;
$total_fees = 0.00;
$order_total = 0.00;
/** Insert order address *************************************************/
$order_data['user_info']['address'] = isset( $order_data['user_info']['address'] )
? $order_data['user_info']['address']
: array();
$order_data['user_info']['address'] = wp_parse_args( $order_data['user_info']['address'], array(
'line1' => '',
'line2' => '',
'city' => '',
'zip' => '',
'country' => '',
'state' => '',
) );
$name = '';
if ( ! empty( $order_data['user_info']['first_name'] ) ) {
$name = $order_data['user_info']['first_name'];
}
if ( ! empty( $order_data['user_info']['last_name'] ) ) {
$name .= ' ' . $order_data['user_info']['last_name'];
}
$order_address_data = array(
'order_id' => $order_id,
'name' => $name,
'address' => $order_data['user_info']['address']['line1'],
'address2' => $order_data['user_info']['address']['line2'],
'city' => $order_data['user_info']['address']['city'],
'region' => $order_data['user_info']['address']['state'],
'country' => $order_data['user_info']['address']['country'],
'postal_code' => $order_data['user_info']['address']['zip'],
);
// Remove empty data.
$order_address_data = array_filter( $order_address_data );
// Add to edd_order_addresses table.
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'] );
edd_maybe_add_customer_address( $customer->id, $customer_address_data );
/** Insert order items ****************************************************/
$decimal_filter = edd_currency_decimal_filter();
if ( ! empty( $order_data['cart_details'] ) && is_array( $order_data['cart_details'] ) ) {
foreach ( $order_data['cart_details'] as $key => $item ) {
// First, we need to check that what is being added is a valid download.
$download = edd_get_download( $item['id'] );
// Skip if download is missing or not actually a download.
if ( empty( $download ) || ( 'download' !== $download->post_type ) ) {
continue;
}
// Get price ID.
$price_id = isset( $item['item_number']['options']['price_id'] ) && is_numeric( $item['item_number']['options']['price_id'] )
? absint( $item['item_number']['options']['price_id'] )
: null;
// Build a base array of information for each order item.
$item['discount'] = isset( $item['discount'] )
? $item['discount']
: 0.00;
$item['subtotal'] = isset( $item['subtotal'] )
? $item['subtotal']
: (float) $item['quantity'] * $item['item_price'];
$item_name = $item['name'];
$option_name = edd_get_price_option_name( $item['id'], $price_id );
if ( ! empty( $option_name ) ) {
$item_name .= ' — ' . $option_name;
}
$order_item_args = array(
'order_id' => $order_id,
'product_id' => $item['id'],
'product_name' => $item_name,
'price_id' => $price_id,
'cart_index' => $key,
'type' => 'download',
'status' => ! empty( $order_data['status'] ) ? $order_data['status'] : 'pending',
'quantity' => $item['quantity'],
'amount' => $item['item_price'],
'subtotal' => $item['subtotal'],
'discount' => $item['discount'],
'tax' => $item['tax'],
'total' => $item['price'],
'item_price' => $item['item_price'], // Added for backwards compatibility
'date_created' => ! empty( $order_data['date_created'] ) ? $order_data['date_created'] : '',
);
/**
* Allow the order item arguments to be filtered.
*
* This is here for backwards compatibility purposes.
*
* @since 3.0
*
* @param array $order_item_args Order item arguments.
* @param int $download->ID Download ID.
*/
$order_item_args = apply_filters( 'edd_payment_add_download_args', $order_item_args, $download->ID );
$order_item_args = wp_parse_args( $order_item_args, array(
'quantity' => 1,
'price_id' => null,
'amount' => false,
'item_price' => false,
'discount' => 0.00,
'tax' => 0.00,
) );
// The item_price key could have been changed by a filter.
// This exists for backwards compatibility purposes.
$order_item_args['amount'] = $order_item_args['item_price'];
unset( $order_item_args['item_price'] );
// Try to use what's passed in via the args.
if ( false !== $order_item_args['amount'] ) {
$item_price = $order_item_args['amount'];
// Deal with variable pricing.
} elseif ( $download->has_variable_prices() ) {
$prices = $download->get_prices();
if ( $order_item_args['price_id'] && array_key_exists( $order_item_args['price_id'], (array) $prices ) ) {
$item_price = $prices[ $order_item_args['price_id'] ]['amount'];
} else {
$item_price = edd_get_lowest_price_option( $download->ID );
$order_item_args['price_id'] = edd_get_lowest_price_id( $download->ID );
}
// Fallback to getting it directly.
} else {
$item_price = edd_get_download_price( $download->ID );
}
// Sanitize price & quantity.
$item_price = edd_sanitize_amount( $item_price );
$quantity = edd_item_quantities_enabled()
? absint( $order_item_args['quantity'] )
: 1;
// Subtotal needs to be updated with the sanitized amount.
$order_item_args['subtotal'] = round( $item_price * $quantity, $decimal_filter );
if ( edd_prices_include_tax() ) {
$order_item_args['subtotal'] -= round( $order_item_args['tax'], $decimal_filter );
}
$total = $order_item_args['subtotal'] - $order_item_args['discount'] + $order_item_args['tax'];
// Do not allow totals to go negative
// TODO: probably remove for handling returns
if ( $total < 0 ) {
$total = 0;
}
// Sanitize all the amounts.
$order_item_args['amount'] = round( $item_price, $decimal_filter );
$order_item_args['subtotal'] = round( $order_item_args['subtotal'], $decimal_filter );
$order_item_args['tax'] = round( $order_item_args['tax'], $decimal_filter );
$order_item_args['total'] = round( $total, $decimal_filter );
$order_item_id = edd_add_order_item( $order_item_args );
if ( ! empty( $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( $item['item_number']['options']['price_id'] );
unset( $item['item_number']['options']['quantity'] );
foreach ( $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 );
}
}
// Store order item fees as adjustments.
if ( isset( $item['fees'] ) && ! empty( $item['fees'] ) ) {
foreach ( $item['fees'] as $fee_id => $fee ) {
$adjustment_subtotal = floatval( $fee['amount'] );
$tax_rate_amount = empty( $tax_rate->amount ) ? false : $tax_rate->amount;
$tax = EDD()->fees->get_calculated_tax( $fee, $tax_rate_amount );
$adjustment_total = floatval( $fee['amount'] ) + $tax;
$adjustment_data = array(
'object_id' => $order_item_id,
'object_type' => 'order_item',
'type_key' => $fee_id,
'type' => 'fee',
'description' => $fee['label'],
'subtotal' => $adjustment_subtotal,
'tax' => $tax,
'total' => $adjustment_total,
);
// Add the adjustment.
$adjustment_id = edd_add_order_adjustment( $adjustment_data );
$total_fees += $adjustment_data['subtotal'];
$total_tax += $adjustment_data['tax'];
}
}
$subtotal += (float) $order_item_args['subtotal'];
$total_tax += (float) $order_item_args['tax'];
$total_discount += (float) $order_item_args['discount'];
}
}
/** Insert order adjustments **********************************************/
// Insert fees.
$fees = edd_get_cart_fees();
// Process fees.
if ( ! empty( $fees ) ) {
foreach ( $fees as $fee_id => $fee ) {
/*
* Skip if fee has a `download_id` assigned. If it does, it will have been added above when
* inserting order items.
*/
if ( ! empty( $fee['download_id'] ) ) {
continue;
}
add_filter( 'edd_prices_include_tax', '__return_false' );
$fee_subtotal = floatval( $fee['amount'] );
$tax_rate_amount = empty( $tax_rate->amount ) ? false : $tax_rate->amount;
$tax = EDD()->fees->get_calculated_tax( $fee, $tax_rate_amount );
$fee_total = floatval( $fee['amount'] ) + $tax;
remove_filter( 'edd_prices_include_tax', '__return_false' );
$args = array(
'object_id' => $order_id,
'object_type' => 'order',
'type_key' => $fee_id,
'type' => 'fee',
'description' => $fee['label'],
'subtotal' => $fee_subtotal,
'tax' => $tax,
'total' => $fee_total,
);
// Add the adjustment.
$adjustment_id = edd_add_order_adjustment( $args );
$total_fees += (float) $fee['amount'];
$total_tax += $tax;
}
}
// Insert discounts.
$discounts = ! empty( $order_data['user_info']['discount'] )
? $order_data['user_info']['discount']
: array();
if ( ! is_array( $discounts ) ) {
/** @var string $discounts */
$discounts = array_map( 'trim', explode( ',', $discounts ) );
}
if ( ! empty( $discounts ) && ( 'none' !== $discounts[0] ) ) {
/** @var array $discounts */
foreach ( $discounts as $discount ) {
$discount = edd_get_discount_by( 'code', $discount );
if ( false === $discount ) {
continue;
}
$discount_amount = 0;
$items = $order_data['cart_details'];
if ( is_array( $items ) && ! empty( $items ) ) {
foreach ( $items as $key => $item ) {
$discount_amount += edd_get_item_discount_amount( $item, $items, array( $discount ), $item['item_price'] );
}
}
edd_add_order_adjustment(
array(
'object_id' => $order_id,
'object_type' => 'order',
'type_id' => $discount->id,
'type' => 'discount',
'description' => $discount->code,
'subtotal' => $discount_amount,
'total' => $discount_amount,
)
);
}
}
// Calculate order total (this needs more flexibility)
$order_total =
$subtotal // Total of all items
- $total_discount // Total of all discounts
+ $total_tax // Total of all taxes
+ $total_fees; // Total of all fees
// If we have tax, but no tax rate, manually save the percentage.
if ( empty( $order_args['tax_rate_id'] ) && $total_tax > 0 ) {
$cart_tax_rate_percentage = edd_get_cart_tax_rate( $country, $region );
if ( ! empty( $cart_tax_rate_percentage ) ) {
if ( $cart_tax_rate_percentage > 0 && $cart_tax_rate_percentage < 1 ) {
$cart_tax_rate_percentage = $cart_tax_rate_percentage * 100;
}
edd_update_order_meta( $order_id, 'tax_rate', $cart_tax_rate_percentage );
}
}
// Update the order with all of the newly computed values.
edd_update_order( $order_id, array(
'subtotal' => $subtotal,
'tax' => $total_tax,
'discount' => $total_discount,
'total' => $order_total,
) );
if ( edd_get_option( 'show_agree_to_terms', false ) && ! empty( $_POST['edd_agree_to_terms'] ) ) { // WPCS: CSRF ok.
$order_data['agree_to_terms_time'] = current_time( 'timestamp' );
}
if ( edd_get_option( 'show_agree_to_privacy_policy', false ) && ! empty( $_POST['edd_agree_to_privacy_policy'] ) ) { // WPCS: CSRF ok.
$order_data['agree_to_privacy_time'] = current_time( 'timestamp' );
}
/**
* Fires after an order has been inserted.
*
* @internal This hook exists for backwards compatibility.
*
* @since 1.0
*
* @param int $order_id ID of the new order.
* @param array $order_data Array of original order data.
*/
do_action( 'edd_insert_payment', $order_id, $order_data );
/**
* Executes after an order has been fully built from the sum of its parts.
*
* @since 3.0
*
* @param int $order_id ID of the new order.
* @param array $order_data Array of original order data.
*/
do_action( 'edd_built_order', $order_id, $order_data );
// Return order ID, or false
return ! empty( $order_id )
? $order_id
: false;
}
/**
* Clone an existing order.
*
* @since 3.0
*
* @param int $order_id Order ID.
* @param boolean $clone_relationships True to clone order items and adjustments,
* false otherwise.
* @param array $args Arguments that are used in place of cloned
* order attributes.
*
* @return int|false New order ID on success, false on failure.
*/
function edd_clone_order( $order_id = 0, $clone_relationships = false, $args = array() ) {
// Bail if no order ID passed.
if ( empty( $order_id ) ) {
return false;
}
// Fetch the order.
$order = edd_get_order( $order_id );
// Bail if the order was not found.
if ( ! $order ) {
return false;
}
// Parse arguments.
$r = wp_parse_args( $args, $order->to_array() );
// Remove order ID and order number.
unset( $r['id'] );
unset( $r['order_number'] );
// Remove dates.
unset( $r['date_created'] );
unset( $r['date_modified'] );
unset( $r['date_completed'] );
unset( $r['date_refundable'] );
// Remove payment key.
unset( $r['payment_key'] );
// Remove object vars.
unset( $r['address'] );
unset( $r['adjustments'] );
unset( $r['items'] );
$new_order_id = edd_add_order( $r );
if ( $clone_relationships ) {
$items = edd_get_order_items( array(
'order_id' => $order_id,
) );
if ( $items ) {
foreach ( $items as $item ) {
$r = $item->to_array();
// Remove original item data.
unset( $r['id'] );
unset( $r['date_created'] );
unset( $r['date_modified'] );
// Point order item to the new order ID.
$r['order_id'] = $new_order_id;
$order_item_id = edd_add_order_item( $r );
$metadata = edd_get_order_item_meta( $item->id );
if ( $metadata ) {
foreach ( $metadata as $meta_key => $meta_value ) {
edd_add_order_item_meta( $order_item_id, $meta_key, $meta_value );
}
}
$adjustments = edd_get_order_adjustments( array(
'object_id' => $item->id,
'object_type' => 'order_item',
) );
if ( $adjustments ) {
foreach ( $adjustments as $adjustment ) {
$r = $adjustment->to_array();
// Remove original adjustment data.
unset( $r['id'] );
unset( $r['date_created'] );
unset( $r['date_modified'] );
// Point order item to the new order ID.
$r['object_id'] = $order_item_id;
$r['object_type'] = 'order_item';
$adjustment_id = edd_add_order_adjustment( $r );
$metadata = edd_get_order_adjustment_meta( $adjustment->id );
if ( $metadata ) {
foreach ( $metadata as $meta_key => $meta_value ) {
edd_add_order_adjustment_meta( $adjustment_id, $meta_key, $meta_value );
}
}
}
}
}
}
$adjustments = edd_get_order_adjustments( array(
'object_id' => $order_id,
'object_type' => 'order',
) );
if ( $adjustments ) {
foreach ( $adjustments as $adjustment ) {
$r = $adjustment->to_array();
// Remove original adjustment data.
unset( $r['id'] );
unset( $r['date_created'] );
unset( $r['date_modified'] );
// Point order item to the new order ID.
$r['object_id'] = $new_order_id;
$r['object_type'] = 'order';
$adjustment_id = edd_add_order_adjustment( $r );
$metadata = edd_get_order_adjustment_meta( $adjustment->id );
if ( $metadata ) {
foreach ( $metadata as $meta_key => $meta_value ) {
edd_add_order_adjustment_meta( $adjustment_id, $meta_key, $meta_value );
}
}
}
}
if ( $order->address ) {
$r = $order->address->to_array();
// Remove original address data.
unset( $r['order_id'] );
unset( $r['date_created'] );
unset( $r['date_modified'] );
$r['order_id'] = $new_order_id;
edd_add_order_address( $r );
}
}
return $new_order_id;
}
/**
* Generate unique payment key for orders.
*
* @since 3.0
* @param string $key Additional string used to help randomize key.
* @return string
*/
function edd_generate_order_payment_key( $key ) {
$auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
$payment_key = strtolower( md5( $key . gmdate( 'Y-m-d H:i:s' ) . $auth_key . uniqid( 'edd', true ) ) );
/**
* Filters the payment key
*
* @since 3.0
* @param string $payment_key The value to be filtered
* @param string $key Additional string used to help randomize key.
* @return string
*/
return apply_filters( 'edd_generate_order_payment_key', $payment_key, $key );
}
/**
* Helper function to get and maybe update the order number.
*
* @since 3.1.1.2
* @return string
*/
function edd_set_order_number() {
$order_number = new EDD\Orders\Number();
return $order_number->apply();
}