gateway for this order does not exist.', 'woocommerce' ) );
}
if ( ! $gateway->supports( 'refunds' ) ) {
throw new Exception( __( 'The payment gateway for this order does not support automatic refunds.', 'woocommerce' ) );
}
$result = $gateway->process_refund( $order->get_id(), $amount, $reason );
if ( ! $result ) {
throw new Exception( __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ) );
}
if ( is_wp_error( $result ) ) {
throw new Exception( $result->get_error_message() );
}
return true;
} catch ( Exception $e ) {
return new WP_Error( 'error', $e->getMessage() );
}
}
/**
* Restock items during refund.
*
* @since 3.0.0
* @param WC_Order $order Order instance.
* @param array $refunded_line_items Refunded items list.
*/
function wc_restock_refunded_items( $order, $refunded_line_items ) {
if ( ! apply_filters( 'woocommerce_can_restock_refunded_items', true, $order, $refunded_line_items ) ) {
return;
}
$line_items = $order->get_items();
foreach ( $line_items as $item_id => $item ) {
if ( ! isset( $refunded_line_items[ $item_id ], $refunded_line_items[ $item_id ]['qty'] ) ) {
continue;
}
$product = $item->get_product();
$item_stock_reduced = $item->get_meta( '_reduced_stock', true );
$restock_refunded_items = (int) $item->get_meta( '_restock_refunded_items', true );
$qty_to_refund = $refunded_line_items[ $item_id ]['qty'];
if ( ! $item_stock_reduced || ! $qty_to_refund || ! $product || ! $product->managing_stock() ) {
continue;
}
$old_stock = $product->get_stock_quantity();
$new_stock = wc_update_product_stock( $product, $qty_to_refund, 'increase' );
// Update _reduced_stock meta to track changes.
$item_stock_reduced = $item_stock_reduced - $qty_to_refund;
// Keeps track of total running tally of reduced stock.
$item->update_meta_data( '_reduced_stock', $item_stock_reduced );
// Keeps track of only refunded items that needs restock.
$item->update_meta_data( '_restock_refunded_items', $qty_to_refund + $restock_refunded_items );
/* translators: 1: product ID 2: old stock level 3: new stock level */
$restock_note = sprintf( __( 'Item #%1$s stock increased from %2$s to %3$s.', 'woocommerce' ), $product->get_id(), $old_stock, $new_stock );
/**
* Allow the restock note to be modified.
*
* @since 6.4.0
*
* @param string $restock_note The original note.
* @param int $old_stock The old stock.
* @param bool|int|null $new_stock The new stock.
* @param WC_Order $order The order the refund was done for.
* @param bool|WC_Product $product The product the refund was done for.
*/
$restock_note = apply_filters( 'woocommerce_refund_restock_note', $restock_note, $old_stock, $new_stock, $order, $product );
$order->add_order_note( $restock_note );
$item->save();
do_action( 'woocommerce_restock_refunded_item', $product->get_id(), $old_stock, $new_stock, $order, $product );
}
}
/**
* Get tax class by tax id.
*
* @since 2.2
* @param int $tax_id Tax ID.
* @return string
*/
function wc_get_tax_class_by_tax_id( $tax_id ) {
global $wpdb;
return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_class FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d", $tax_id ) );
}
/**
* Get payment gateway class by order data.
*
* @since 2.2
* @param int|WC_Order $order Order instance.
* @return WC_Payment_Gateway|bool
*/
function wc_get_payment_gateway_by_order( $order ) {
if ( WC()->payment_gateways() ) {
$payment_gateways = WC()->payment_gateways()->payment_gateways();
} else {
$payment_gateways = array();
}
if ( ! is_object( $order ) ) {
$order_id = absint( $order );
$order = wc_get_order( $order_id );
}
return is_a( $order, 'WC_Order' ) && isset( $payment_gateways[ $order->get_payment_method() ] ) ? $payment_gateways[ $order->get_payment_method() ] : false;
}
/**
* When refunding an order, create a refund line item if the partial refunds do not match order total.
*
* This is manual; no gateway refund will be performed.
*
* @since 2.4
* @param int $order_id Order ID.
*/
function wc_order_fully_refunded( $order_id ) {
$order = wc_get_order( $order_id );
$max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded() );
if ( ! $max_refund ) {
return;
}
// Create the refund object.
wc_switch_to_site_locale();
wc_create_refund(
array(
'amount' => $max_refund,
'reason' => __( 'Order fully refunded.', 'woocommerce' ),
'order_id' => $order_id,
'line_items' => array(),
)
);
wc_restore_locale();
$order->add_order_note( __( 'Order status set to refunded. To return funds to the customer you will need to issue a refund through your payment gateway.', 'woocommerce' ) );
}
add_action( 'woocommerce_order_status_refunded', 'wc_order_fully_refunded' );
/**
* Search orders.
*
* @since 2.6.0
* @param string $term Term to search.
* @return array List of orders ID.
*/
function wc_order_search( $term ) {
$data_store = WC_Data_Store::load( 'order' );
return $data_store->search_orders( str_replace( 'Order #', '', wc_clean( $term ) ) );
}
/**
* Update total sales amount for each product within a paid order.
*
* @since 3.0.0
* @param int $order_id Order ID.
*/
function wc_update_total_sales_counts( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$recorded_sales = $order->get_data_store()->get_recorded_sales( $order );
$reflected_order = in_array( $order->get_status(), array( OrderStatus::CANCELLED, OrderStatus::TRASH ), true );
if ( ! $reflected_order && 'woocommerce_before_delete_order' === current_action() ) {
$reflected_order = true;
}
if ( $recorded_sales xor $reflected_order ) {
return;
}
$operation = $recorded_sales && $reflected_order ? 'decrease' : 'increase';
if ( count( $order->get_items() ) > 0 ) {
foreach ( $order->get_items() as $item ) {
$product_id = $item->get_product_id();
if ( $product_id ) {
$data_store = WC_Data_Store::load( 'product' );
$data_store->update_product_sales( $product_id, absint( $item->get_quantity() ), $operation );
}
}
}
if ( 'decrease' === $operation ) {
$order->get_data_store()->set_recorded_sales( $order, false );
} else {
$order->get_data_store()->set_recorded_sales( $order, true );
}
/**
* Called when sales for an order are recorded
*
* @param int $order_id order id
*/
do_action( 'woocommerce_recorded_sales', $order_id );
}
add_action( 'woocommerce_order_status_completed', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_order_status_processing', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_order_status_on-hold', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_order_status_completed_to_cancelled', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_order_status_processing_to_cancelled', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_order_status_on-hold_to_cancelled', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_trash_order', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_untrash_order', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_before_delete_order', 'wc_update_total_sales_counts' );
/**
* Update used coupon amount for each coupon within an order.
*
* @since 3.0.0
* @param int $order_id Order ID.
*/
function wc_update_coupon_usage_counts( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$has_recorded = $order->get_data_store()->get_recorded_coupon_usage_counts( $order );
$invalid_statuses = array( OrderStatus::CANCELLED, OrderStatus::FAILED, OrderStatus::TRASH );
/**
* Allow invalid order status filtering for updating coupon usage.
*
* @since 9.0.0
*
* @param array $invalid_statuses Array of statuses to consider invalid.
*/
$invalid_statuses = apply_filters(
'woocommerce_update_coupon_usage_invalid_statuses',
$invalid_statuses
);
if ( $order->has_status( $invalid_statuses ) && $has_recorded ) {
$action = 'reduce';
$order->get_data_store()->set_recorded_coupon_usage_counts( $order, false );
} elseif ( ! $order->has_status( $invalid_statuses ) && ! $has_recorded ) {
$action = 'increase';
$order->get_data_store()->set_recorded_coupon_usage_counts( $order, true );
} elseif ( $order->has_status( $invalid_statuses ) ) {
wc_release_coupons_for_order( $order );
return;
} else {
return;
}
if ( count( $order->get_coupon_codes() ) > 0 ) {
foreach ( $order->get_coupon_codes() as $code ) {
if ( StringUtil::is_null_or_whitespace( $code ) ) {
continue;
}
$coupon = new WC_Coupon( $code );
$used_by = $order->get_user_id();
if ( ! $used_by ) {
$used_by = $order->get_billing_email();
}
switch ( $action ) {
case 'reduce':
$coupon->decrease_usage_count( $used_by );
break;
case 'increase':
$coupon->increase_usage_count( $used_by, $order );
break;
}
}
wc_release_coupons_for_order( $order );
}
}
add_action( 'woocommerce_order_status_pending', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_order_status_completed', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_order_status_processing', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_order_status_on-hold', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_order_status_cancelled', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_order_status_failed', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_trash_order', 'wc_update_coupon_usage_counts' );
/**
* Cancel all unpaid orders after held duration to prevent stock lock for those products.
*/
function wc_cancel_unpaid_orders() {
$held_duration = get_option( 'woocommerce_hold_stock_minutes' );
// Re-schedule the event before cancelling orders
// this way in case of a DB timeout or (plugin) crash the event is always scheduled for retry.
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) );
wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) {
return;
}
$data_store = WC_Data_Store::load( 'order' );
$unpaid_orders = $data_store->get_unpaid_orders( strtotime( '-' . absint( $held_duration ) . ' MINUTES', current_time( 'timestamp' ) ) );
if ( $unpaid_orders ) {
foreach ( $unpaid_orders as $unpaid_order ) {
$order = wc_get_order( $unpaid_order );
if ( apply_filters( 'woocommerce_cancel_unpaid_order', 'checkout' === $order->get_created_via(), $order ) ) {
$order->update_status( OrderStatus::CANCELLED, __( 'Unpaid order cancelled - time limit reached.', 'woocommerce' ) );
}
}
}
}
add_action( 'woocommerce_cancel_unpaid_orders', 'wc_cancel_unpaid_orders' );
/**
* Sanitize order id removing unwanted characters.
*
* E.g Users can sometimes try to track an order id using # with no success.
* This function will fix this.
*
* @since 3.1.0
* @param int $order_id Order ID.
*/
function wc_sanitize_order_id( $order_id ) {
return (int) filter_var( $order_id, FILTER_SANITIZE_NUMBER_INT );
}
add_filter( 'woocommerce_shortcode_order_tracking_order_id', 'wc_sanitize_order_id' );
/**
* Get an order note.
*
* @since 3.2.0
* @param int|WP_Comment $data Note ID (or WP_Comment instance for internal use only).
* @return stdClass|null Object with order note details or null when does not exists.
*/
function wc_get_order_note( $data ) {
if ( is_numeric( $data ) ) {
$data = get_comment( $data );
}
if ( ! is_a( $data, 'WP_Comment' ) ) {
return null;
}
return (object) apply_filters(
'woocommerce_get_order_note',
array(
'id' => (int) $data->comment_ID,
'date_created' => wc_string_to_datetime( $data->comment_date ),
'content' => $data->comment_content,
'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ),
'added_by' => __( 'WooCommerce', 'woocommerce' ) === $data->comment_author ? 'system' : $data->comment_author,
'order_id' => absint( $data->comment_post_ID ),
),
$data
);
}
/**
* Get order notes.
*
* @since 3.2.0
* @param array $args Query arguments {
* Array of query parameters.
*
* @type string $limit Maximum number of notes to retrieve.
* Default empty (no limit).
* @type int $order_id Limit results to those affiliated with a given order ID.
* Default 0.
* @type array $order__in Array of order IDs to include affiliated notes for.
* Default empty.
* @type array $order__not_in Array of order IDs to exclude affiliated notes for.
* Default empty.
* @type string $orderby Define how should sort notes.
* Accepts 'date_created', 'date_created_gmt' or 'id'.
* Default: 'id'.
* @type string $order How to order retrieved notes.
* Accepts 'ASC' or 'DESC'.
* Default: 'DESC'.
* @type string $type Define what type of note should retrieve.
* Accepts 'customer', 'internal' or empty for both.
* Default empty.
* }
* @return stdClass[] Array of stdClass objects with order notes details.
*/
function wc_get_order_notes( $args ) {
$key_mapping = array(
'limit' => 'number',
'order_id' => 'post_id',
'order__in' => 'post__in',
'order__not_in' => 'post__not_in',
);
foreach ( $key_mapping as $query_key => $db_key ) {
if ( isset( $args[ $query_key ] ) ) {
$args[ $db_key ] = $args[ $query_key ];
unset( $args[ $query_key ] );
}
}
// Define orderby.
$orderby_mapping = array(
'date_created' => 'comment_date',
'date_created_gmt' => 'comment_date_gmt',
'id' => 'comment_ID',
);
$args['orderby'] = ! empty( $args['orderby'] ) && in_array( $args['orderby'], array( 'date_created', 'date_created_gmt', 'id' ), true ) ? $orderby_mapping[ $args['orderby'] ] : 'comment_ID';
// Set WooCommerce order type.
if ( isset( $args['type'] ) && 'customer' === $args['type'] ) {
$args['meta_query'] = array( // WPCS: slow query ok.
array(
'key' => 'is_customer_note',
'value' => 1,
'compare' => '=',
),
);
} elseif ( isset( $args['type'] ) && 'internal' === $args['type'] ) {
$args['meta_query'] = array( // WPCS: slow query ok.
array(
'key' => 'is_customer_note',
'compare' => 'NOT EXISTS',
),
);
}
// Set correct comment type.
$args['type'] = 'order_note';
// Always approved.
$args['status'] = 'approve';
// Does not support 'count' or 'fields'.
unset( $args['count'], $args['fields'] );
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$notes = get_comments( $args );
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
return array_filter( array_map( 'wc_get_order_note', $notes ) );
}
/**
* Create an order note.
*
* @since 3.2.0
* @param int $order_id Order ID.
* @param string $note Note to add.
* @param bool $is_customer_note If is a costumer note.
* @param bool $added_by_user If note is create by an user.
* @return int|WP_Error Integer when created or WP_Error when found an error.
*/
function wc_create_order_note( $order_id, $note, $is_customer_note = false, $added_by_user = false ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return new WP_Error( 'invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 400 ) );
}
return $order->add_order_note( $note, (int) $is_customer_note, $added_by_user );
}
/**
* Delete an order note.
*
* @since 3.2.0
* @param int $note_id Order note.
* @return bool True on success, false on failure.
*/
function wc_delete_order_note( $note_id ) {
$note = wc_get_order_note( $note_id );
if ( $note && wp_delete_comment( $note_id, true ) ) {
/**
* Action hook fired after an order note is deleted.
*
* @param int $note_id Order note ID.
* @param stdClass $note Object with the deleted order note details.
*
* @since 9.1.0
*/
do_action( 'woocommerce_order_note_deleted', $note_id, $note );
return true;
}
return false;
}