WooCommerce offers a refund option right from the order edit screen. But what if you need to process WooCommerce refunds automatically through code? In this tutorial, we will create the code that will be able to process refunds for a single order.

WooCommerce Refunds

WooCommerce refunds can be created using their own API function wc_create_refund. Let’s first understand what we can pass to that function.

There are 7 arguments that we can pass in an array to that function:

  • amount – The amount to be refunded
  • reason – Reason of the refund
  • order_id – ID of the order we want to refund
  • refund_id – ID of the refund we want to use again and retry
  • line_items – Array of line items which we want to refund from the order
  • refund_payment – Boolean. If true, the refund process will also try to refund the payment through the payment gateway
  • restock_items – If true, it will restock the items back by the quantity of each line item that we have refunded

Line Items

The array of line items will contain various information. Each array item will have the item’s ID as the array key.

Each item that has been assigned to the array by the key, will have:

  • qty – Quantity
  • refund_total – Total amount to be refunded for that item
  • refund_tax – Tax to be refunded

Function to Process WooCommerce Refunds

We will now start creating our own function. This function will receive the Order ID and the refund reason.

In this function we are getting the order by using the function wc_get_order. Then we are checking if the order is of type WC_Order. If it’s not, then we have received a different type of WooCommerce Order which can be something like WC_Order_Refund. We don’t want that.

We also don’t want to refund an order that has already been refunded, so we check for its status. If everything is correct, then we are getting the order items for which we will process the refund. We also define the variable to hold our refund amount and one for the line items.

Getting the Line Items

We will check if we have any order items, and if we do, we will save those line items.

For each line item, we are getting the tax data to calculate the tax refund. We also calculate the refund amount for that line item and sum it up with the current refund amount.

After that, we add all that information to the array of line items.

Creating the Refund

We have everything we need now and we just need to create the refund.

We are passing all our information into an array, with the arguments mentioned at the beginning of this tutorial.

Conclusion

By using the WooCommerce API we can do a lot of stuff automatically. In this tutorial, we got introduced with the WooCommerce refunds and how to create them using the WooCommerce API.

Have you ever had to process refunds? Would you like to process them automatically on some events such as failed products?

If you are a developer who wants to learn more about WooCommerce, check out my eBook:

Book WooCommerce for Developer to buy

Become a Sponsor

Posted by Igor Benic

Web Developer who mainly uses WordPress for projects. Working on various project through Codeable & Toptal. Author of several ebooks at https://leanpub.com/u/igorbenic.

23 Comments

  1. Hi

    Does Woo commerce provide any hook or filter when refund is done through admin panel ?

    Reply

    1. You can only hook into 'wp_ajax_woocommerce_refund_line_items' since that one is done when processing refund through admin panel. But that is not something that would provide you with refund information. You could maybe try and create a hook (inside the previous one) such as a simple do_action('processing_refund_admin_panel') before the refund is processed. After that, you can hook into the wc_create_refund actions (check which exist there) and when check in your function if( did_action('processing_refund_admin_panel') ){}. You could maybe be sure that this refund is processed through the admin panel. I haven’t tried this yet, but you can try and see if it works.

      Reply

  2. Very helpful post Igor. Many thanks. Where does the $item_meta variable come from?

    Reply

    1. Hi Drake, sorry about that. I’ve added a new line above to show how the item meta is retrieved.

      Reply

      1. Nice work Igor! Many thanks…

        Reply

  3. I am curious about the $refund_tax value. It never seems to be created because $tax_data[0] is never an array. $tax_data is an array but not the first element. Is that the correct way to get the $refund_tax value? It is always set to 0 for me even when I do have taxes that need to be refunded.

    Reply

  4. Hello Igor,

    thanks for this example code. I have a bit of the same problem. The tax never seems to be refunded, even though in the $line_items it is set.

    I have altered your code a bit, since it seemed $tax_data, as Nick says, didn’t always contain the right info.

    The tax_data is now present in the $line_items and is passed to the function, but it doesn’t appear on the refund.

    Maybe anyone has an idea why?

    Also, I can’t seem to find how to return shipping. Hopefully you, or anyone here, has experience wit that?

    Regards,

    Mark

    Reply

  5. Hello Igor,
    Does the wc_create_refund() supposed to automatically change order status to ‘wc-refunded’ ?
    I used your function but the action wc_create_refund() does nothing on my order i can not see any refund and status is not changing.

    Reply

  6. I just added :
    $order->update_status(‘wc-refunded’, ‘order has been refunded’);
    in your function before return $refund;
    now it works but it’s weird because this only line of code seems to do the job itself (even if i comment the call of wc_create_refund) and i can see the total refund amount in the order..

    Reply

    1. Hi Tedd, the WooCommerce function wc_create_refund will set the order status to wc-refunded only if it was possible to refund the order. You can always directly set the Order status to ‘wc-refunded’ with update_status, similar to how you would do manually through the admin area. But that does not mean that the order was actually refunded through the Gateway that was used to pay for that Order.

      You can check the code of that function here https://docs.woocommerce.com/wc-apidocs/source-function-wc_create_refund.html#469 and you’ll see that the function will set the status only if it was fully refunded.

      Reply

  7. Thanks a lot Igor !
    in fact i had set the ‘refund_payment’ to true , but i didn’t have any payment gateway for now.
    It worked changing refund_payment to false for my case

    Reply

  8. Thanks!

    I also need this functionality. I want to refund the order if someone buy product A and did not buy another product B(Which is related to A) within 24 hours.

    This helps me to start with.

    Reply

  9. this is just what I have been looking for. As a noob to wordpress, what other include files do I need to include in this script to get it to work as a stand alone option?

    Obviously the database connection script, but what other core wordpress files?

    Thanks!

    Reply

    1. Hi James,

      WordPress includes everything on load so you don’t have to think about it. This is a code that you put in your own WordPress plugin or theme to use that programmatically.

      Reply

  10. Does this automatically tie in to the gateway? For example, if this is done, will Stripe actually process the refund to the customer’s card?

    Reply

    1. Hi Nathan, if the Gateway does support refunds, the payment should be refunded. You can check the wc_create_refund function: https://docs.woocommerce.com/wc-apidocs/source-function-wc_create_refund.html#481-630 and you’ll see they use the function wc_refund_payment (https://docs.woocommerce.com/wc-apidocs/source-function-wc_create_refund.html#642)

      This function will get the gateway from the order and if it supports the refund, it will try to refund the payment.

      Reply

  11. How can I set a specific gateway for a refund ?

    I accept multiple payment methods, but I want it to be refunded only to a specific gateway method which is a virtual “Terra Wallet”

    Reply

    1. Hi Tarik, programmatically, this can be done using the filter woocommerce_payment_gateway_supports where you could do something like this:

      add_filter( 'woocommerce_payment_gateway_supports', 'disable_my_payment_gateways', 20, 3 );
      function disable_my_payment_gateways( $supports, $feature, $gateway ) {
      if ( 'refunds' !== $feature ) {
      return $supports;
      }
      // Add gateways Ids you want to allow only. Gateways Ids can be found in code on each.
      $allow_only_for( 'paypal', 'stripe' );
      if ( in_array( $gateway->id, $allow_only_for ) ) {
      return $supports;
      }

      return false;
      }

      Reply

  12. Awesome resource! Really helped me out but I have a question…

    Does the ‘amount’ value need to match the sum of the item ‘refund_total’ amounts? For example, I want to issue a partial refund for 2 of 3 items on the order but I also want to deduct $20 for restocking fee. So if Item 1 is $50 and Item 2 is $40 (total $90) then deduct $20 can I just use the ‘amount’ as $70 or will the wc_create_refund use the sum of the items over amount? Or do I have to create a ‘fee’ line item and use it somewhere?

    Thanks for any help!

    Reply

    1. Hi John, as far as I know, WC does allow partial refunds so you should be able to define a lower amount and refund it.

      Reply

  13. Thanks a lot. BIG LOVE TO YOU.

    Reply

  14. Hi all,
    this goes a long way, and I couldn’t have solved programmatically refunding without this.
    I did have to enhance it: As someone already mentioned, the shipping is not included, for that you need to explicitly mention gather the data when constructing the $line_items, unfortunately from different fields, see the switch statement in this code below. Note that I solve a special case where an existing refund is deleted and replaced with one that lists all order items correctly. It should be easy to adapt to other cases, though.
    This code uses the phpdoc way to make it callable through the wp command line, so as long as this php file is included any way, you will have the “wp tvn fixrefund” command available.
    ~~~php
    <?php

    /**
    * Implements example command.
    */
    class TVN_Commands
    {

    /**
    * Prints a greeting.
    *
    * ## OPTIONS
    *
    *
    * : ID of the order refund to fix
    *
    * [–orig=]
    * : the order ID of the original order
    * —
    *
    * ## EXAMPLES
    *
    * wp tvn fixrefund 309528
    *
    * @when after_wp_load
    */
    function fixrefund($args, $assoc_args)
    {
    $refund_id = $args[0];
    $refund = wc_get_order($refund_id);
    $order_id = $refund->get_parent_id();

    $refund->delete(true);
    do_action(‘woocommerce_refund_deleted’, $refund_id, $order_id);

    $fixed_refund = $this->tvn_wc_refund_order($order_id, “refund über wp tvn fixrefund”);
    if ($fixed_refund->errors) {
    WP_CLI::error(“Fehler: ” . print_r($fixed_refund->error_data, true));
    } elseif (empty($fixed_refund)) {
    WP_CLI::error(“Fehler: neuer Refund nicht erfolgreich erstellt!”);
    } else {
    WP_CLI::success(“Refund nachgetragen. Refund $refund_id ist gelöscht und durch refund ” . $fixed_refund->get_id() . ” ersetzt. ”
    . “Link zur erstatteten Bestellung“);
    }
    }

    /**
    * Process Order Refund through Code
    * @return WC_Order_Refund
    */
    private function tvn_wc_refund_order($order_id, $refund_reason = ”)
    {

    $order = wc_get_order($order_id);

    // If it’s something else such as a WC_Order_Refund, we don’t want that.
    if (!is_a($order, ‘WC_Order’)) {
    return new WP_Error(‘wc-order’, __(‘Provided ID is not a WC Order’, ‘yourtextdomain’));
    }
    /* // This is maybe not so good – I might want to update refunded orders.
    if (‘refunded’ == $order->get_status()) {
    return new WP_Error(‘wc-order’, __(‘Order has been already refunded’, ‘yourtextdomain’));
    }
    */

    // Get Items
    $order_items = $order->get_items(array(‘line_item’, ‘shipping’));

    // Refund Amount
    $refund_amount = 0;

    // Prepare line items which we are refunding
    $line_items = array();

    if ($order_items) {
    foreach ($order_items as $item_id => $item) {
    $item_meta = $order->get_item_meta($item_id);
    switch ($item->get_type()) {
    case “line_item”:
    $tax_data = $item_meta[‘_line_tax_data’];
    $line_total = $item_meta[‘_line_total’];
    break;
    case “shipping”:
    $tax_data = $item_meta[‘taxes’];
    $line_total = $item_meta[‘cost’];
    }
    $refund_tax = 0;
    $total_tax = maybe_unserialize($tax_data[0]);
    if (is_array($total_tax)) {
    $refund_tax = array_map(‘wc_format_decimal’, $total_tax);
    }

    $refund_amount = wc_format_decimal($refund_amount) + wc_format_decimal($line_total[0] + array_sum($refund_tax[‘total’]));

    $line_items[$item_id] = array(
    ‘refund_total’ => wc_format_decimal($line_total[0]),
    ‘refund_tax’ => $refund_tax[‘total’]);

    if ($item_meta[‘_qty’]) {
    $line_items[$item_id][‘qty’] = $item_meta[‘_qty’][0];
    }

    }
    }

    // Order Items were processed. We can now create a refund
    // $default_args = array(
    // ‘amount’ => 0,
    // ‘reason’ => null,
    // ‘order_id’ => 0,
    // ‘refund_id’ => 0,
    // ‘line_items’ => array(),
    // ‘refund_payment’ => false,
    // ‘restock_items’ => false,
    // );

    $refund = wc_create_refund(array(
    ‘amount’ => $refund_amount,
    ‘reason’ => $refund_reason,
    ‘order_id’ => $order_id,
    ‘line_items’ => $line_items,
    ‘refund_payment’ => false,
    ‘restock_items’ => false
    ));

    return $refund;
    }

    }

    WP_CLI::add_command(‘tvn’, ‘TVN_Commands’);
    ~~~

    Reply

    1. Wow, thank you very much for sharing the whole code! It will help someone for sure!

      Reply

Leave a reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.