2

We have a special case where we invoice our customers for payment after the order has been received instead of having them pay during checkout. Shipping charges are manually calculated and added to the order and then we add a 3% credit card fee on the grand total.

To automate this process, I created a script that calculates the 3% charge once the shipping charge has been set through the backend and adds this fee item into the order automatically. This works when we add the shipping charge and click save/recalculate the first time.

add_action( 'woocommerce_order_after_calculate_totals', "custom_order_after_calculate_totals", 10, 2);
function custom_order_after_calculate_totals($and_taxes, $order) {

    if ( did_action( 'woocommerce_order_after_calculate_totals' ) >= 2 )
    return;

    if ( is_admin() && ! defined( 'DOING_AJAX' ) )
        return;

    $percentage = 0.03;
    $total = $order->get_total();
    $surcharge = $total * $percentage;
    $feeArray = array(
        'name' => '3% CC Fee',
        'amount' =>  wc_format_decimal($surcharge),
        'taxable' => false,
        'tax_class' => ''
    );

    //Get fees
    $fees = $order->get_fees();
    if(empty($fees)){
        //Add fee
        $fee_object = (object) wp_parse_args( $feeArray );
        $order->add_fee($fee_object);
    } else {
        //Update fee
        foreach($fees as $item_id => $item_fee){
            if($item_fee->get_name() == "3% CC Fee"){
                $order->update_fee($item_id,$feeArray);
            }
        }
    }
}

If we accidentally add the wrong shipping cost and try to update it, this code above does get triggered again and updates the fee however $total does not get the new order total from the updated shipping cost and so the fee does not change. Strangely enough, if I try to delete the fee item, a new fee is calculated and is added back with the correct fee amount.

Anybody know how I can solve this?

LoicTheAztec
  • 229,944
  • 23
  • 356
  • 399
ZeroNine
  • 721
  • 1
  • 11
  • 29

2 Answers2

2

As you are using the order gran total to calculate your fee and as the hook you are using is located inside calculate_totals() method, once order get updated, you will always need to press "recalculate" button to get the correct fee total and the correct order gran total with the correct amounts.

Since WooCommerce 3 your code is outdated and a bit obsolete with some mistakes… For example add_fee() and update_fee() methods are deprecated and replaced by some other ways.

Use instead the following:

add_action( 'woocommerce_order_after_calculate_totals', "custom_order_after_calculate_totals", 10, 2 );
function custom_order_after_calculate_totals( $and_taxes, $order ) {
    if ( did_action( 'woocommerce_order_after_calculate_totals' ) >= 2 )
        return;

    $percentage = 0.03; // Fee percentage

    $fee_data   = array(
        'name'       => __('3% CC Fee'),
        'amount'     => wc_format_decimal( $order->get_total() * $percentage ),
        'tax_status' => 'none',
        'tax_class'  => ''
    );

    $fee_items  = $order->get_fees(); // Get fees

    // Add fee
    if( empty($fee_items) ){
        $item = new WC_Order_Item_Fee(); // Get an empty instance object

        $item->set_name( $fee_data['name'] );
        $item->set_amount( $fee_data['amount'] );
        $item->set_tax_class($fee_data['tax_class']);
        $item->set_tax_status($fee_data['tax_status']);
        $item->set_total($fee_data['amount']);

        $order->add_item( $item );
        $item->save(); // (optional) to be sure
    }
    // Update fee
    else {
        foreach ( $fee_items as $item_id => $item ) {
            if( $item->get_name() === $fee_data['name'] ) {
                $item->set_amount($fee_data['amount']);
                $item->set_tax_class($fee_data['tax_class']);
                $item->set_tax_status($fee_data['tax_status']);
                $item->set_total($fee_data['amount']);
                $item->save();
            }
        }
    }
}

Code goes in functions.php file of the active child theme (or active theme). Tested and works.

Once order get updated and after press on recalculate button (to get the correct orders totals) both auto added and updated fee will work nicely.

Related: Add a fee to an order programmatically in Woocommerce 3


Update

Now if it doesn't work for any reason, you should remove the related item to update and add a new one as follows:

add_action( 'woocommerce_order_after_calculate_totals', "custom_order_after_calculate_totals", 10, 2 );
function custom_order_after_calculate_totals( $and_taxes, $order ) {
    if ( did_action( 'woocommerce_order_after_calculate_totals' ) >= 2 )
        return;

    $percentage = 0.03; // Fee percentage

    $fee_data   = array(
        'name'       => __('3% CC Fee'),
        'amount'     => wc_format_decimal( $order->get_total() * $percentage ),
        'tax_status' => 'none',
        'tax_class'  => ''
    );

    $fee_items  = $order->get_fees(); // Get fees

    // Add fee
    if( empty($fee_items) ){
        $item = new WC_Order_Item_Fee(); // Get an empty instance object

        $item->set_name( $fee_data['name'] );
        $item->set_amount( $fee_data['amount'] );
        $item->set_tax_class($fee_data['tax_class']);
        $item->set_tax_status($fee_data['tax_status']);
        $item->set_total($fee_data['amount']);

        $order->add_item( $item );
        $item->save(); // (optional) to be sure
    }
    // Update fee
    else {
        foreach ( $fee_items as $item_id => $item ) {
            if( $item->get_name() === $fee_data['name'] ) {
                $item->remove_item( $item_id ); // Remove the item

                $item = new WC_Order_Item_Fee(); // Get an empty instance object

                $item->set_name( $fee_data['name'] );
                $item->set_amount( $fee_data['amount'] );
                $item->set_tax_class($fee_data['tax_class']);
                $item->set_tax_status($fee_data['tax_status']);
                $item->set_total($fee_data['amount']);

                $order->add_item( $item );
                $item->save(); // (optional) to be sure
            }
        }
    }
}
LoicTheAztec
  • 229,944
  • 23
  • 356
  • 399
  • I saw those old methods were being deprecated but couldn't figure out how to do it the updated way, so thank you for that and the explanation about the hook. I updated the code but the issue remains. I update the shipping costs and click Save. Then click Update. Then Click Recalculate. The fee is still the same amount. The only way to update the fee is to delete the fee and let it auto regenerate. – ZeroNine Jan 10 '21 at 16:29
  • @ZeroNine Sorry but I don't have this behavior with the code I provided, using last WooCommerce version on Storefront theme. So there is something else that is making trouble in your case. It can be a plugin, your theme or some other custom code made by you. – LoicTheAztec Jan 10 '21 at 16:36
0

Update:

Need to use remove_item from $order object. There is no remove_item function in WC_Order_Item_Fee Class. After removing item from order invoke the save() function on order.

$order_fees = reset( $order->get_items('fee') );
        $fee_data   = array(
            'name'       => __( 'Delivery Fee', 'dsfw' ),
            'amount'     => wc_format_decimal( $day_fee + $timeslot_fee ),
        );

        if( !empty( $order_fees ) && $order_fees instanceof WC_Order_Item_Fee ) {
            // update fee

            if( $order_fees->get_name() === $fee_data['name'] ) {
                $order->remove_item( (int)$order_fees->get_id() );
                $order->save();

                $item = new WC_Order_Item_Fee();
                $item->set_name( $fee_data['name'] );
                $item->set_amount( $fee_data['amount'] );
                $item->set_total( $fee_data['amount'] );
                $order->add_item( $item );  
                $item->save();

            }
        } else {
            // add fee
            $item = new WC_Order_Item_Fee();

            $item->set_name( $fee_data['name'] );
            $item->set_amount( $fee_data['amount'] );
            $item->set_total( $fee_data['amount'] );

            $order->add_item( $item );
            $item->save();
        }

    }