Search code examples
phpwoocommercehook-woocommerce

Calculate_totals() triggers early email notification with 0 order total


I'm splitting an order into 2 upon hook woocommerce_order_status_changed by creating a new order and moving order items to it. However the moment calculate_totals() is called, calculate_taxes() will call save() resulting in an immediate early email notification with order totals as 0 because the totals have not been calculated yet and bypassing the calculate_taxes() means the taxes will be wrong.

In this situation how can I best calculate totals and taxes without triggering an email notification before its calculated? I imagine I can do either:

  • Temporarily remove the email notification hooks and re-add them after calculating totals
  • Not use calculate_totals() and calculate it manually

Both are equally unappealing options, what would be a good way to do this?

public static function add_hooks() {

    // After payment is confirmed, we split the order based on order items
    add_action( 'woocommerce_order_status_pending_to_processing', [ __CLASS__, 'split_order' ], 10, 2 );
}

public static function split_order( $order_id, WC_Order $order, $force = null ) {

    // Iterate the order items and add them to $items_to_split if they need to be split into a 
    //  seperate order, reason for this is that we explicitely need them in separate orders
    $items_to_split = array();
    foreach ( $order->get_items() as $item_id => $item_data ) {
        if ( $item_data->meta_exists( '_SnapshotId' ) ) {
            $items_to_split[ $item_id ] = $item_data;
        }
    }

    if ( count( $items_to_split ) === 0 ) return;

    $new_order = wc_create_order();
    $new_order->set_customer_id( $order->get_customer_id() );
    $new_order->set_payment_method( $order->get_payment_method() );
    $new_order->set_created_via( 'split_order' );
    $new_order->set_status( $order->get_status() );

    $new_order->set_billing( $order->get_address( 'billing' ) );
    $new_order->set_shipping( $order->get_address( 'shipping' ) );

    // Move the items to split to new order
    foreach ( $items_to_split as $item_id => $item_data ) {
        /** @var WC_Order_Item_Product $item_data */
        $new_order_item = new WC_Order_Item_Product();
        // set_meta_data() doesn't accept WC_Meta_data and causes issues reusing metadata ids
        // $new_order_item->set_meta_data($item_data->get_meta_data());
        // Also can't clone $item_data because it does not truncate id & order_id on __clone

        // Add item (meta) data to new order item
        $data = $item_data->get_data();
        unset( $data['id'] );
        unset( $data['order_id'] );
        $new_order_item->set_props( $data );

        $meta_data = array_map( function ( $n ) {
            return $n->get_data();
        }, $data['meta_data'] );
        foreach ( $meta_data as $meta_data_entry ) {
            $new_order_item->add_meta_data( $meta_data_entry['key'], $meta_data_entry['value'] );
        }

        $new_order->add_item( $new_order_item );
        $order->remove_item( $item_id );
    }

    // Will call save(), causing immediate early trigger of email notifications
    //  with order totals as 0
    $new_order->calculate_totals();
    $order->calculate_totals();
}

Solution

  • Turns out the solution was to simply move the status change of the new order after calculating_totals(), because no email notifications get sent for orders with status pending.

    $new_order->calculate_totals();
    $new_order->set_status( $order->get_status() );
    $new_order->save();
    

    Thanks to LoicTheAztec for pointing that out.

    The status change hook was kept in favor of woocommerce_checkout_create_order & woocommerce_new_order because splitting the order prior to payment completion potentially results in having to pay for 2 separate orders, where our usecase wants to keep it as a payment for a single order prior to the splitting.