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

DataUtils::build_refund_previewpublicWC 10.9.0

Build a refund preview showing authoritative totals and breakdowns.

Callers must invoke {@see validate_preview_line_items()} first — this method assumes inputs have been validated and throws on missing items.

Each line item must have 'line_item_id' and at least one of 'quantity' (positive int) or 'refund_total' (positive tax-inclusive float). When 'refund_total' is present and positive it is used directly; otherwise the total is computed from quantity via {@see compute_line_item_refund_total()}.

Method of the class: DataUtils{}

No Hooks.

Returns

Array. The structured preview response.

Usage

$DataUtils = new DataUtils();
$DataUtils->build_refund_preview( $order, $line_items ): array;
$order(WC_Order) (required)
The order being previewed for refund.
$line_items(array) (required)
Line items. Each: array{line_item_id: int, quantity?: int, refund_total?: float}.

Changelog

Since 10.9.0 Introduced.

DataUtils::build_refund_preview() code WC 11.0.1

public function build_refund_preview( WC_Order $order, array $line_items ): array {
	$price_decimals = wc_get_price_decimals();
	$sections       = array(
		'products' => array(
			'items'    => array(),
			'subtotal' => 0.0,
			'tax'      => 0.0,
			'total'    => 0.0,
		),
		'shipping' => array(
			'items'    => array(),
			'subtotal' => 0.0,
			'tax'      => 0.0,
			'total'    => 0.0,
		),
		'fees'     => array(
			'items'    => array(),
			'subtotal' => 0.0,
			'tax'      => 0.0,
			'total'    => 0.0,
		),
	);

	foreach ( $line_items as $line_item ) {
		$item = $order->get_item( $line_item['line_item_id'] );
		if ( ! $item ) {
			// Exception message is developer-facing only; both values are typed ints and the format is a literal string.
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			throw new \InvalidArgumentException( sprintf( 'Line item %d not found on order %d.', (int) $line_item['line_item_id'], (int) $order->get_id() ) );
		}

		/**
		 * Validated by validate_preview_line_items() upstream.
		 *
		 * @var WC_Order_Item_Product|WC_Order_Item_Shipping|WC_Order_Item_Fee $item
		 */
		// When the caller provides an explicit refund_total (partial-amount form) use it
		// directly. The quantity-based form computes the tax-inclusive total from unit price.
		// A non-zero check (not > 0) mirrors validate_preview_line_items(), which accepts a
		// negative refund_total for a negative discount line and rejects a present-but-zero
		// one before this method runs — so a signed value is honoured rather than falling
		// through to the (possibly absent) quantity.
		$refund_total_with_tax = isset( $line_item['refund_total'] ) && is_numeric( $line_item['refund_total'] ) && 0.0 !== (float) $line_item['refund_total']
			? NumberUtil::round( (float) $line_item['refund_total'], $price_decimals )
			: $this->compute_line_item_refund_total( $item, (int) $line_item['quantity'] );

		// Split by the line's own stored total/tax ratio so the preview reflects what
		// was actually charged and matches the split create stores (both call this).
		$split    = $this->split_inclusive_by_stored_ratio( $refund_total_with_tax, $item, $price_decimals );
		$subtotal = $split['subtotal'];
		$tax      = $split['total_tax'];

		$item_data = array(
			'id'       => $line_item['line_item_id'],
			'quantity' => $line_item['quantity'] ?? null,
			'subtotal' => wc_format_decimal( $subtotal, $price_decimals ),
			'tax'      => wc_format_decimal( $tax, $price_decimals ),
			'total'    => wc_format_decimal( $refund_total_with_tax, $price_decimals ),
		);

		$item_data['name'] = $item->get_name();

		if ( $item instanceof WC_Order_Item_Product ) {
			$variation_id            = $item->get_variation_id();
			$item_data['product_id'] = $variation_id > 0 ? $variation_id : $item->get_product_id();
			$section_key             = 'products';
		} elseif ( $item instanceof WC_Order_Item_Shipping ) {
			$section_key = 'shipping';
		} else {
			$section_key = 'fees';
		}

		$sections[ $section_key ]['items'][]   = $item_data;
		$sections[ $section_key ]['subtotal'] += $subtotal;
		$sections[ $section_key ]['tax']      += $tax;
		$sections[ $section_key ]['total']    += $refund_total_with_tax;
	}

	$format_section = function ( array $section ) use ( $price_decimals ): array {
		return array(
			'items'    => $section['items'],
			'subtotal' => wc_format_decimal( $section['subtotal'], $price_decimals ),
			'tax'      => wc_format_decimal( $section['tax'], $price_decimals ),
			'total'    => wc_format_decimal( $section['total'], $price_decimals ),
		);
	};

	$grand_subtotal = $sections['products']['subtotal'] + $sections['shipping']['subtotal'] + $sections['fees']['subtotal'];
	$grand_tax      = $sections['products']['tax'] + $sections['shipping']['tax'] + $sections['fees']['tax'];
	$grand_total    = $sections['products']['total'] + $sections['shipping']['total'] + $sections['fees']['total'];

	return array(
		'breakdown'      => array(
			'products' => $format_section( $sections['products'] ),
			'shipping' => $format_section( $sections['shipping'] ),
			'fees'     => $format_section( $sections['fees'] ),
		),
		'subtotal'       => wc_format_decimal( $grand_subtotal, $price_decimals ),
		'tax'            => wc_format_decimal( $grand_tax, $price_decimals ),
		'total'          => wc_format_decimal( $grand_total, $price_decimals ),
		'max_refundable' => wc_format_decimal( $order->get_remaining_refund_amount(), $price_decimals ),
	);
}