Automattic\WooCommerce\Internal\RestApi\Routes\V4\Refunds
DataUtils::prepare_refund_creation_or_error
Run the shared refund creation preparation pipeline for one request.
Fills missing refund_total values from quantities, validates the line items against the order's refund history, converts them to the internal wc_create_refund() format, resolves the refund amount, and applies the three aggregate guards (non-positive amount, an override below the line items total, and an amount exceeding the order's remaining refundable amount). Shared by the wc/v3 compute_totals path and the wc/v4 creation endpoint so a fix lands once; error codes are emitted unprefixed with their HTTP status in the error data, and each caller applies its own surface convention. Request mutation, hooks, metadata, and the wc_create_refund() call itself stay with the callers.
The amount override is passed as a flag plus raw value because an explicitly supplied zero or null must be rejected, never silently replaced with the calculated total: a request meaning "refund nothing" must not refund the full computed amount.
Method of the class: DataUtils{}
No Hooks.
Returns
array|WP_Error. Array with internal-format line_items, filled schema-format schema_line_items, and the resolved raw amount, or WP_Error carrying its HTTP status in the error data.
Usage
$DataUtils = new DataUtils(); $DataUtils->prepare_refund_creation_or_error( $order, $line_items, $has_amount_override, $amount_override, $log_source );
- $order(WC_Order) (required)
- The order the refund is prepared for.
- $line_items(array) (required)
- Line items in schema format (line_item_id keyed).
- $has_amount_override(true|false) (required)
- Whether the client explicitly supplied an amount.
- $amount_override(mixed) (required)
- The raw client-supplied amount, when
$has_amount_override. - $log_source(string) (required)
- Log source for invariant violations, per caller.
Changelog
| Since 11.1.0 | Introduced. |
DataUtils::prepare_refund_creation_or_error() DataUtils::prepare refund creation or error code WC 11.1.2
public function prepare_refund_creation_or_error( WC_Order $order, array $line_items, bool $has_amount_override, $amount_override, string $log_source ) {
// Fill in refund_total for any line items that omit it. The simplified
// request form sends only {line_item_id, quantity}; the backend derives
// the tax-inclusive total from the order's unit price × quantity. Scoped
// try: compute_line_item_refund_total throws InvalidArgumentException on
// quantity < 1, but fill_missing_refund_totals pre-checks that condition,
// so this branch is defensive against a future invariant break only.
try {
$line_items = $this->fill_missing_refund_totals( $line_items, $order );
} catch ( \InvalidArgumentException $e ) {
wc_get_logger()->error(
sprintf( 'Refund creation invariant violation on order %d (%s): %s', $order->get_id(), get_class( $e ), $e->getMessage() ),
array( 'source' => $log_source )
);
return new WP_Error(
'invalid_refund_request',
__( 'The refund could not be created due to an unexpected error.', 'woocommerce' ),
array( 'status' => WP_Http::INTERNAL_SERVER_ERROR )
);
}
// The WP_Error already carries its HTTP status (400/422) in the error
// data, so create and preview return the same code for the same invalid
// input.
$validation_error = $this->validate_line_items( $line_items, $order );
if ( is_wp_error( $validation_error ) ) {
return $validation_error;
}
// Convert line items to internal format. refund_total is tax-inclusive when no
// explicit refund_tax is supplied (auto-computed values, or client values) — the
// converter splits the tax portion out via the line's stored total/tax ratio
// (split_inclusive_by_stored_ratio(), the same method the preview uses). When
// the client supplies an explicit refund_tax breakdown, refund_total is the
// tax-exclusive subtotal and the tax is added on top (core Woo semantics).
// Either way calculate_refund_amount sums refund_total + refund_tax to the
// gross line amount, so mixing the two forms across line items is well-defined.
$internal_line_items = $this->convert_line_items_to_internal_format( $line_items, $order );
$calculated_total = ! empty( $line_items ) ? $this->calculate_refund_amount( $line_items ) : 0;
$amount = $has_amount_override ? $amount_override : $calculated_total;
if ( (float) $amount <= 0 ) {
return new WP_Error(
'invalid_refund_amount',
__( 'Refund total must be greater than zero.', 'woocommerce' ),
array( 'status' => WP_Http::BAD_REQUEST )
);
}
// Prevent under-refunding: the amount cannot be less than the calculated line
// items total. Over-refunding is allowed for goodwill/compensation scenarios.
if ( $has_amount_override && $calculated_total > 0 && NumberUtil::round( (float) $amount, wc_get_price_decimals() ) < NumberUtil::round( $calculated_total, wc_get_price_decimals() ) ) {
return new WP_Error(
'invalid_refund_amount',
sprintf(
/* translators: %1$s: refund amount, %2$s: calculated total from line items */
__( 'Refund amount (%1$s) cannot be less than the total of line items (%2$s).', 'woocommerce' ),
wc_format_decimal( $amount, wc_get_price_decimals() ),
wc_format_decimal( $calculated_total, wc_get_price_decimals() )
),
array( 'status' => WP_Http::BAD_REQUEST )
);
}
// Over-refunding line items is allowed (goodwill), but the amount can never
// exceed the order's remaining refundable amount. Reject up-front with a clear
// 422 rather than relying on wc_create_refund's generic failure, mirroring the
// preview pipeline's preview_exceeds_max_refundable guard.
$remaining_refundable = (float) $order->get_remaining_refund_amount();
if ( NumberUtil::round( (float) $amount, wc_get_price_decimals() ) > NumberUtil::round( $remaining_refundable, wc_get_price_decimals() ) ) {
return new WP_Error(
'refund_exceeds_remaining',
sprintf(
/* translators: %1$s: requested refund amount, %2$s: remaining refundable amount */
__( 'Refund amount (%1$s) exceeds the remaining refundable amount (%2$s).', 'woocommerce' ),
wc_format_decimal( $amount, wc_get_price_decimals() ),
wc_format_decimal( $remaining_refundable, wc_get_price_decimals() )
),
array( 'status' => WP_Http::UNPROCESSABLE_ENTITY )
);
}
return array(
'line_items' => $internal_line_items,
'schema_line_items' => $line_items,
'amount' => $amount,
);
}