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

Controller::preview_itempublicWC 10.9.0

Preview a refund without creating it.

Method of the class: Controller{}

No Hooks.

Returns

WP_REST_Response|WP_Error.

Usage

$Controller = new Controller();
$Controller->preview_item( $request );
$request(required)
.

Changelog

Since 10.9.0 Introduced.

Controller::preview_item() code WC 11.0.1

public function preview_item( $request ) {
	$order = wc_get_order( $request['order_id'] );

	// wc_get_order returns WC_Order|WC_Order_Refund|false; only a WC_Order
	// (shop_order) is previewable here — refunds and missing IDs are rejected.
	if ( ! $order instanceof WC_Order ) {
		return $this->get_route_error_by_code( self::INVALID_ID );
	}

	// Round caller-supplied refund_total values once, up front, so validation and
	// the computed preview use the same precision the create flow stores. Reused
	// for both validate and build below.
	$line_items = $this->data_utils->normalize_refund_totals( $request['line_items'] );

	$validation_error = $this->data_utils->validate_preview_line_items( $line_items, $order );

	if ( is_wp_error( $validation_error ) ) {
		$error_data = $validation_error->get_error_data();
		$status     = is_array( $error_data ) && isset( $error_data['status'] ) ? (int) $error_data['status'] : WP_Http::BAD_REQUEST;
		return $this->get_route_error_response_from_object( $validation_error, $status );
	}

	try {
		$preview = $this->data_utils->build_refund_preview( $order, $line_items );
	} catch ( \InvalidArgumentException $e ) {
		// validate_preview_line_items above should have caught any bad input.
		// If build_refund_preview still throws InvalidArgumentException, treat
		// it as a server-side invariant violation, log for observability, and
		// return a generic message (do not leak internal IDs to clients).
		wc_get_logger()->error(
			sprintf( 'Refund preview invariant violation on order %d: %s', $order->get_id(), $e->getMessage() ),
			array( 'source' => 'wc-v4-refunds' )
		);
		return $this->get_route_error_response(
			'invalid_preview_request',
			__( 'The refund preview could not be generated due to an unexpected error.', 'woocommerce' ),
			WP_Http::INTERNAL_SERVER_ERROR
		);
	} catch ( \Throwable $e ) {
		wc_get_logger()->error(
			sprintf( 'Refund preview unexpected error on order %d: %s', $order->get_id(), $e->getMessage() ),
			array( 'source' => 'wc-v4-refunds' )
		);
		return $this->get_route_error_response(
			'unexpected_preview_error',
			__( 'An unexpected error occurred while generating the refund preview.', 'woocommerce' ),
			WP_Http::INTERNAL_SERVER_ERROR
		);
	}

	// Reject a non-positive aggregate total up front, mirroring create_item()'s
	// `0 > $refund_amount || ! $refund_amount` guard. A refund of only a negative
	// discount line, or a product plus discount that nets to zero, would otherwise
	// preview successfully and then fail at create with 'invalid_refund_amount'.
	if ( (float) $preview['total'] <= 0 ) {
		return $this->get_route_error_response(
			'invalid_refund_amount',
			__( 'Refund total must be greater than zero.', 'woocommerce' )
		);
	}

	// Final guard: even when per-line validation passes, the aggregate
	// preview total can still exceed the order's remaining refundable
	// amount (e.g. an amount-only partial refund applied previously).
	// Reject up-front so the eventual create call doesn't fail with the
	// generic 'cannot_create_refund' error from wc_create_refund.
	// `total` is already tax-inclusive; compare directly against max_refundable.
	$preview_total_with_tax = abs( (float) $preview['total'] );
	if ( $preview_total_with_tax > (float) $preview['max_refundable'] ) {
		return $this->get_route_error_response(
			'preview_exceeds_max_refundable',
			sprintf(
				/* translators: 1: requested preview total including tax, 2: remaining refundable */
				__( 'Requested refund preview (%1$s) exceeds the remaining refundable amount (%2$s).', 'woocommerce' ),
				wc_format_decimal( $preview_total_with_tax, wc_get_price_decimals() ),
				$preview['max_refundable']
			),
			WP_Http::UNPROCESSABLE_ENTITY
		);
	}

	return rest_ensure_response( $preview );
}