Automattic\WooCommerce\Internal\RestApi\Routes\V4\Refunds
DataUtils::split_inclusive_by_stored_ratio
Split a tax-inclusive amount into net subtotal and per-tax-ID tax amounts using the line item's own stored total/tax ratio.
Splitting by the line's actual stored proportion — rather than re-deriving tax from the order tax item's rate percent — returns exactly what was charged. It stays correct when the stored tax is not an exact rate% of net (manually edited tax, or a rate that changed after the order) and when a taxed line's rate resolves to zero. Preview ({@see build_refund_preview()}) and create ({@see convert_line_items_to_internal_format()}) share this method so a previewed split always matches the split stored on the created refund.
Per-ID amounts are rounded and the subtotal is derived as amount - sum(tax), so the invariant subtotal + total_tax == amount holds exactly at $dp precision.
Method of the class: DataUtils{}
No Hooks.
Returns
Array{subtotal:. float, total_tax: float, taxes: array<int, float>} Net subtotal, summed tax, and per-tax-ID amounts.
Usage
// protected - for code of main (parent) or child class $result = $this->split_inclusive_by_stored_ratio( $amount, $item, $dp ): array;
- $amount(float) (required)
- Tax-inclusive amount to split. Rounded to
$dpbefore splitting. - $item(WC_Order_Item_Product|WC_Order_Item_Shipping|WC_Order_Item_Fee) (required)
- Order item supplying the stored total/tax ratio.
- $dp(int) (required)
- Price decimal places.
Changelog
| Since 10.9.0 | Introduced. |
DataUtils::split_inclusive_by_stored_ratio() DataUtils::split inclusive by stored ratio code WC 11.0.1
protected function split_inclusive_by_stored_ratio( float $amount, $item, int $dp ): array {
$amount = NumberUtil::round( $amount, $dp );
$stored_total = (float) $item->get_total();
// Keep only non-zero numeric stored taxes (positive or negative). A negative-tax
// discount fee must retain its breakdown; a zero entry contributes nothing.
$stored_taxes = array_filter(
$item->get_taxes()['total'] ?? array(),
function ( $t ) {
return is_numeric( $t ) && 0.0 !== (float) $t;
}
);
$stored_tax_total = array_sum( array_map( 'floatval', $stored_taxes ) );
$stored_with_tax = $stored_total + $stored_tax_total;
// Fallback used whenever the stored data can't yield a sane proportional split:
// treat the whole amount as net (no tax) and log for observability.
$unsplittable = function ( string $reason ) use ( $amount, $item ) {
wc_get_logger()->warning(
sprintf(
'Refund tax split: cannot split tax for item %d on order %d (%s).',
(int) $item->get_id(),
(int) $item->get_order_id(),
$reason
),
array( 'source' => 'wc-v4-refunds' )
);
return array(
'subtotal' => $amount,
'total_tax' => 0.0,
'taxes' => array(),
);
};
// No tax on the line: the whole amount is net (not an error, no log).
if ( empty( $stored_taxes ) ) {
return array(
'subtotal' => $amount,
'total_tax' => 0.0,
'taxes' => array(),
);
}
// A zero-value line (stored total nets to zero while a tax was charged) can't be
// split proportionally — avoid division by zero.
if ( 0.0 === (float) $stored_with_tax ) {
return $unsplittable( 'stored total incl. tax is zero' );
}
// Scale each stored tax by the share of the line being refunded.
$taxes = array();
foreach ( $stored_taxes as $tax_id => $stored_tax ) {
$taxes[ (int) $tax_id ] = NumberUtil::round( $amount * ( (float) $stored_tax / $stored_with_tax ), $dp );
}
$total_tax = NumberUtil::round( array_sum( $taxes ), $dp );
// Sanity clamp: the tax portion of a tax-inclusive amount can never exceed the
// amount itself. A larger value means the stored total/tax nearly cancel (e.g. a
// near-zero inclusive total from manually edited data), which would explode the
// ratio. Fall back rather than emit a nonsensical negative subtotal.
if ( abs( $total_tax ) > abs( $amount ) ) {
return $unsplittable( 'stored total and tax nearly cancel' );
}
$subtotal = NumberUtil::round( $amount - $total_tax, $dp );
return array(
'subtotal' => $subtotal,
'total_tax' => $total_tax,
'taxes' => $taxes,
);
}