Automattic\WooCommerce\Internal\RestApi\Routes\V4\Refunds
DataUtils::compute_refund_preview_or_error
Run the full refund preview pipeline for one request.
Normalizes the caller-supplied refund_total values, validates the line items, builds the preview, and applies the two aggregate guards (a non-positive total, and a total exceeding the order's remaining refundable amount). Shared by the wc/v3 and wc/v4 preview endpoints so a fix lands once; error codes are emitted unprefixed and each caller applies its own surface convention (wc/v3 prefixes with woocommerce_rest_, wc/v4 wraps in its error envelope).
Method of the class: DataUtils{}
No Hooks.
Returns
array|WP_Error. Preview data, or WP_Error carrying its HTTP status in the error data.
Usage
$DataUtils = new DataUtils(); $DataUtils->compute_refund_preview_or_error( $order, $line_items, $log_source );
- $order(WC_Order) (required)
- The order the preview is computed for.
- $line_items(array) (required)
- Line items in schema format (line_item_id keyed).
- $log_source(string) (required)
- Log source for invariant violations, per caller.
Changelog
| Since 11.1.0 | Introduced. |
DataUtils::compute_refund_preview_or_error() DataUtils::compute refund preview or error code WC 11.1.2
public function compute_refund_preview_or_error( WC_Order $order, array $line_items, string $log_source ) {
// 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->normalize_refund_totals( $line_items );
$validation_error = $this->validate_preview_line_items( $line_items, $order );
if ( is_wp_error( $validation_error ) ) {
return $validation_error;
}
try {
$preview = $this->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' => $log_source )
);
return new WP_Error(
'invalid_preview_request',
__( 'The refund preview could not be generated due to an unexpected error.', 'woocommerce' ),
array( 'status' => 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' => $log_source )
);
return new WP_Error(
'unexpected_preview_error',
__( 'An unexpected error occurred while generating the refund preview.', 'woocommerce' ),
array( 'status' => WP_Http::INTERNAL_SERVER_ERROR )
);
}
// Reject a non-positive aggregate total up front. 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 time.
if ( (float) $preview['total'] <= 0 ) {
return new WP_Error(
'invalid_refund_amount',
__( 'Refund total must be greater than zero.', 'woocommerce' ),
array( 'status' => WP_Http::BAD_REQUEST )
);
}
// 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).
// `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 new WP_Error(
'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']
),
array( 'status' => WP_Http::UNPROCESSABLE_ENTITY )
);
}
return $preview;
}