A common issue when using WooPayments and Wallet for WooCommerce is that there is no way to refund to wallet. This actually applies to all orders / payment-gateways. In this post we talk about how we can add a “Refund (Manually)” style button that actually refunds to the wallet itself.
Requirements:
Wallet For WooCommerce – https://woocommerce.com/products/wallet-for-woocommerce/
My Experience:
- Don’t forget to enable PARTIAL payments
- Tested with WooPayments
- Tested with PayPal for WooCommerce

Workflow:
- Refund > Refund to Wallet
- Adds order note information
- Marks order as “Partially Refunded” for Email Trigger
- Marks order as “Completed” to register in HPOS
- Adds $refundvalue into Wallet for that user
Use code at your own risk
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
/* Register custom status the WooCommerce way (HPOS-safe).*/
add_filter( 'woocommerce_register_shop_order_post_statuses', function( $statuses ) {
$statuses['wc-partially-refunded'] = array(
'label' => 'Partially Refunded',
'public' => true,
'exclude_from_search' => false,
'show_in_admin_all_list' => true,
'show_in_admin_status_list' => true,
'label_count' => _n_noop(
'Partially Refunded <span class="count">(%s)</span>',
'Partially Refunded <span class="count">(%s)</span>'
),
);
return $statuses;
}, 20 );
/* Add it to WooCommerce’s known statuses list. */
add_filter( 'wc_order_statuses', function( $statuses ) {
$new = array();
foreach ( $statuses as $key => $label ) {
$new[ $key ] = $label;
// Place it after Completed (nice + logical)
if ( 'wc-completed' === $key ) {
$new['wc-partially-refunded'] = 'Partially Refunded';
}
}
// Fallback if "completed" wasn't present for some reason
if ( ! isset( $new['wc-partially-refunded'] ) ) {
$new['wc-partially-refunded'] = 'Partially Refunded';
}
return $new;
}, 20 );
/* Treat "partially-refunded" as a paid status (prevents "Pending payment" weirdness). */
add_filter( 'woocommerce_order_is_paid_statuses', function( $paid_statuses ) {
if ( ! in_array( 'partially-refunded', $paid_statuses, true ) ) {
$paid_statuses[] = 'partially-refunded';
}
return $paid_statuses;
}, 20 );
/**
* Admin JS: inject Refund to Wallet button on order edit screens.
*/
add_action( 'admin_enqueue_scripts', function( $hook ) {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
$is_legacy = ( 'post.php' === $hook && $screen && 'shop_order' === $screen->post_type );
$is_hpos = ( 'woocommerce_page_wc-orders' === $hook );
if ( ! $is_legacy && ! $is_hpos ) {
return;
}
wp_register_script( 'v8-refund-to-wallet', '', array( 'jquery' ), '1.0.4', true );
wp_enqueue_script( 'v8-refund-to-wallet' );
wp_localize_script( 'v8-refund-to-wallet', 'V8RefundToWallet', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'v8_refund_to_wallet_nonce' ),
) );
$js = <<<JS
jQuery(function($){
function getOrderId() {
try {
var params = new URLSearchParams(window.location.search);
var id = params.get('id');
if (id) return id;
} catch(e) {}
return $('#post_ID').val() || $('input[name="post_ID"]').val() || $('input[name="id"]').val() || $('input[name="order_id"]').val() || '';
}
function getRefundAmount() {
return $('input[name="refund_amount"]').val() || $('#refund_amount').val() || '';
}
function getRefundReason() {
return $('input[name="refund_reason"]').val() || $('#refund_reason').val() || '';
}
function injectButton() {
if ($('#v8_refund_to_wallet_btn').length) return;
// Target the existing gateway refund button inside refund UI
var \$gatewayBtn = $('button.button-primary').filter(function(){
var t = ($(this).text() || '').toLowerCase();
return t.indexOf('refund') !== -1 && t.indexOf('wallet') === -1;
}).last();
if (!\$gatewayBtn.length) return;
var \$btn = $('<button/>', {
type: 'button',
id: 'v8_refund_to_wallet_btn',
class: 'button',
text: 'Refund to Wallet',
css: { marginLeft: '8px' }
});
\$btn.on('click', function(){
var orderId = getOrderId();
var amount = getRefundAmount();
var reason = getRefundReason();
var restock = $('#restock_refunded_items').is(':checked') ? 'yes' : 'no';
amount = (amount || '').toString().trim();
if (!orderId) { alert('Order ID not found.'); return; }
if (!amount || isNaN(amount) || parseFloat(amount) <= 0) { alert('Enter a valid refund amount first.'); return; }
\$btn.prop('disabled', true).text('Refunding to Wallet...');
$.post(V8RefundToWallet.ajaxurl, {
action: 'v8_refund_to_wallet',
nonce: V8RefundToWallet.nonce,
order_id: orderId,
amount: amount,
reason: reason,
restock: restock
}).done(function(resp){
if (resp && resp.success) {
window.location.reload();
} else {
var msg = (resp && resp.data && resp.data.message) ? resp.data.message : 'Refund failed.';
alert(msg);
\$btn.prop('disabled', false).text('Refund to Wallet');
}
}).fail(function(){
alert('Refund failed (AJAX error).');
\$btn.prop('disabled', false).text('Refund to Wallet');
});
});
\$gatewayBtn.after(\$btn);
}
injectButton();
setInterval(injectButton, 600);
});
JS;
wp_add_inline_script( 'v8-refund-to-wallet', $js );
}, 20 );
/* AJAX: Refund to Wallet (no gateway refund).*/
add_action( 'wp_ajax_v8_refund_to_wallet', function() {
if ( ! current_user_can( 'edit_shop_orders' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 );
}
$nonce = isset($_POST['nonce']) ? sanitize_text_field( wp_unslash($_POST['nonce']) ) : '';
if ( ! wp_verify_nonce( $nonce, 'v8_refund_to_wallet_nonce' ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce.' ), 400 );
}
if ( ! function_exists( 'wal_credit_wallet_fund' ) ) {
wp_send_json_error( array( 'message' => 'Wallet plugin not active (wal_credit_wallet_fund missing).' ), 400 );
}
$order_id = isset($_POST['order_id']) ? absint($_POST['order_id']) : 0;
$amount = isset($_POST['amount']) ? wc_format_decimal( wp_unslash($_POST['amount']), wc_get_price_decimals() ) : 0;
$reason = isset($_POST['reason']) ? sanitize_text_field( wp_unslash($_POST['reason']) ) : '';
$restock = ( isset($_POST['restock']) && 'yes' === sanitize_text_field( wp_unslash($_POST['restock']) ) );
if ( ! $order_id ) {
wp_send_json_error( array( 'message' => 'Missing order ID.' ), 400 );
}
if ( $amount <= 0 ) {
wp_send_json_error( array( 'message' => 'Refund amount must be greater than 0.' ), 400 );
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
wp_send_json_error( array( 'message' => 'Order not found.' ), 404 );
}
$user_id = (int) $order->get_user_id();
if ( ! $user_id ) {
wp_send_json_error( array( 'message' => 'This order has no customer user (guest checkout). Wallet refund requires a user account.' ), 400 );
}
$max_refundable = (float) $order->get_total() - (float) $order->get_total_refunded();
$max_refundable = max( 0, $max_refundable );
if ( (float) $amount > (float) $max_refundable + 0.00001 ) {
wp_send_json_error( array(
'message' => 'Refund amount exceeds remaining refundable total for this order.',
), 400 );
}
try {
$event_message = 'Refund to wallet for Order #' . $order_id;
if ( $reason ) {
$event_message .= ' - ' . $reason;
}
$transaction_log_id = wal_credit_wallet_fund( array(
'user_id' => $user_id,
'order_id' => $order_id,
'amount' => (float) $amount,
'event_id' => 20,
'event_message' => $event_message,
'currency' => $order->get_currency(),
'mode' => 'manual',
) );
if ( ! $transaction_log_id ) {
throw new Exception( 'Wallet credit failed.' );
}
$refund = wc_create_refund( array(
'order_id' => $order_id,
'amount' => (float) $amount,
'reason' => $reason ? $reason : $event_message,
'refund_payment' => false,
'restock_items' => $restock,
) );
if ( is_wp_error( $refund ) ) {
throw new Exception( $refund->get_error_message() );
}
$order->add_order_note(
sprintf(
'Refunded %s to wallet. Wallet transaction log ID: %d',
wc_price( (float) $amount, array( 'currency' => $order->get_currency() ) ),
(int) $transaction_log_id
)
);
$remaining_after = (float) $order->get_total() - (float) $order->get_total_refunded();
$remaining_after = max( 0, $remaining_after );
if ( $remaining_after <= 0.00001 ) {
$order->update_status( 'refunded', 'Fully refunded via wallet.', true );
} else {
// Your requested fallback: partial refunds should behave like Completed.
$order->update_status( 'completed', 'Partially refunded via wallet (credited to wallet).', true );
// Optional marker if you ever want filtering later
$order->update_meta_data( '_v8_wallet_partially_refunded', 'yes' );
}
$order->save();
wp_send_json_success( array(
'message' => 'Refunded to wallet.',
'transaction_log_id' => (int) $transaction_log_id,
'refund_id' => is_object( $refund ) ? $refund->get_id() : 0,
) );
} catch ( Exception $e ) {
wp_send_json_error( array( 'message' => $e->getMessage() ), 500 );
}
} );


