Refund to wallet

Refund to Wallet Button for WooCommerce

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
Partial Payments Enable

Workflow:

  1. Refund > Refund to Wallet
  2. Adds order note information
  3. Marks order as “Partially Refunded” for Email Trigger
  4. Marks order as “Completed” to register in HPOS
  5. Adds $refundvalue into Wallet for that user

<?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 );
	}
} );