<?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',
		)
	);

	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_destroy_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 addresses.
		$addresses = edd_get_order_addresses(
			array(
				'order_id' => $order_id,
			)
		);

		// Destroy addresses.
		if ( $addresses ) {
			foreach ( $addresses as $address ) {
				edd_delete_order_address( $address->id );
			}
		}

		// Get order transactions.
		$transactions = edd_get_order_transactions(
			array(
				'object_id'   => $order_id,
				'object_type' => 'order',
			)
		);

		// Destroy transactions.
		if ( ! empty( $transactions ) ) {
			foreach ( $transactions as $transaction ) {
				edd_delete_order_transaction( $transaction->id );
			}
		}

		// Get notes.
		$notes = edd_get_notes(
			array(
				'object_id'   => $order_id,
				'object_type' => 'order',
				'number'      => 9999,
			)
		);

		// Delete notes.
		if ( ! empty( $notes ) ) {
			foreach ( $notes as $note ) {
				edd_delete_note( $note->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 ) ) {
		edd_debug_log( 'No order data passed to edd_build_order' );
		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 ) ) {
				$resume_order = true;
			}
		}
	}

	if ( $resume_order ) {
		$date                       = EDD()->utils->date( 'now' );
		$order_data['date_created'] = $date->format( 'Y-m-d H:i:s' );
		edd_add_note(
			array(
				'object_id'   => $order->id,
				'object_type' => 'order',
				'user_id'     => is_admin() ? get_current_user_id() : 0,
				'content'     => __( 'Payment recovery processed', 'easy-digital-downloads' ),
			)
		);

		// Since things could have been added/removed since we first crated this...rebuild the cart details.
		foreach ( $order->get_items() as $item ) {
			$adjustments = $item->adjustments;
			if ( ! empty( $adjustments ) ) {
				foreach ( $adjustments as $adjustment ) {
					edd_delete_order_adjustment( $adjustment->id );
				}
			}
			edd_delete_order_item( $item->id );
		}

		// Remove any remainders of possible fees and discounts from the order.
		foreach ( $order->get_adjustments() as $adjustment ) {
			edd_delete_order_adjustment( $adjustment->id );
		}

		if ( strtolower( $order->email ) !== strtolower( $order_data['user_info']['email'] ) ) {

			// Remove the payment from the previous customer.
			$previous_customer = edd_get_customer( $order->customer_id );
			if ( $previous_customer ) {
				$previous_customer->remove_payment( $order->id, false );
			}

			// Redefine the email first and last names.
			edd_update_order(
				$order->id,
				array(
					'email'      => $order_data['user_info']['email'],
					'first_name' => $order_data['user_info']['first_name'],
					'last_name'  => $order_data['user_info']['last_name'],
				)
			);
		}
	}

	/** 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'] ) : '',
		'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 = false;

	if ( did_action( 'edd_pre_process_purchase' ) && is_user_logged_in() ) {
		$customer = edd_get_customer_by( 'user_id', get_current_user_id() );

		// 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'] );
		}
	}

	$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'];
	}

	if ( ! $customer ) {
		$customer = edd_get_customer_by( 'email', $order_args['email'] );

		if ( ! $customer ) {
			$customer = new EDD_Customer( $order_args['email'] );
			$customer->create(
				array(
					'name'    => $name ? $name : $order_args['email'],
					'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 ( $customer && $name && empty( $customer->name ) ) {
		$customer->update(
			array(
				'name' => $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 = $order->id;
		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 ) ) {
		edd_debug_log( 'Failed to create order. Order data: ' . var_export( $order_args, true ) );
		return false;
	}

	// If there is an active purchase session, store the order ID for later use.
	if ( edd_get_purchase_session() ) {
		EDD()->session->set( 'edd_resume_payment', $order_id );
	}

	// Attach order to the customer record.
	$customer->attach_payment( $order_id, false );

	// Set $has_tax to false for now.
	$has_tax = false;

	/** 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'   => '',
		)
	);

	$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 );

	if ( ! empty( $order_data['user_info']['address']['phone'] ) ) {
		edd_add_order_meta( $order_id, '_edd_phone', sanitize_text_field( $order_data['user_info']['address']['phone'] ) );
		edd_update_customer_meta( $customer->id, 'phone', sanitize_text_field( $order_data['user_info']['address']['phone'] ) );
	}

	/** 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'] );
					$adjustment_total    = floatval( $fee['amount'] );
					$adjustment_tax      = 0;
					if ( ! empty( $tax_rate->amount ) && empty( $fee['no_tax'] ) ) {
						$adjustment_tax   = EDD()->fees->get_calculated_tax( $fee, $tax_rate->amount );
						$adjustment_total = floatval( $fee['amount'] ) + $adjustment_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'         => $adjustment_tax,
						'total'       => $adjustment_total,
					);

					// Add the adjustment.
					$adjustment_id = edd_add_order_adjustment( $adjustment_data );

					if ( ! empty( $adjustment_data['tax'] ) ) {
						$has_tax = true;
					}

					edd_add_extra_fee_order_adjustment_meta( $adjustment_id, $fee );
				}
			}

			if ( ! $has_tax && ! empty( $order_item_args['tax'] ) ) {
				$has_tax = true;
			}
		}
	}

	/** 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'] );
			$fee_total    = floatval( $fee['amount'] );
			$fee_tax      = 0;
			if ( ! empty( $tax_rate->amount ) && empty( $fee['no_tax'] ) ) {
				$fee_tax   = EDD()->fees->get_calculated_tax( $fee, $tax_rate->amount );
				$fee_total = floatval( $fee['amount'] ) + $fee_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'         => $fee_tax,
				'total'       => $fee_total,
			);

			// Add the adjustment.
			$adjustment_id = edd_add_order_adjustment( $args );

			if ( ! $has_tax && ! empty( $fee_tax ) ) {
				$has_tax = true;
			}

			edd_add_extra_fee_order_adjustment_meta( $adjustment_id, $fee );
		}
	}

	// 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,
				)
			);
		}
	}

	// If we have tax, but no tax rate, manually save the percentage.
	if ( empty( $order_args['tax_rate_id'] ) && $has_tax ) {
		$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 order data.
	$order = edd_get_order( $order_id );
	$order->recalculate();

	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();
}
