Automattic\WooCommerce\Internal\RestApi\Routes\V4\Refunds

DataUtils::split_inclusive_by_stored_ratioprotectedWC 10.9.0

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 $dp before 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() 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,
	);
}