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:
calculate_totals()
and calculate it manuallyBoth 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();
}
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.