Repository: Automattic/woocommerce-payments
Pull request #10677: Improve refund failure handling with detailed error messages
Author: login: mgascam
Labels: name: pr: needs review
Created: 2025-04-04T15:12:21Z | Merged: 2025-04-11T16:23:31Z

Fixes WOOPMNT-4892

This PR enhances the refund failure handling. The changes make it easier for merchants to understand why a refund failed and what actions they can take to resolve the issue.

#### Changes proposed in this Pull Request

- Added new Refund_Failure_Reason constants class with user-friendly messages
- Updated handle_failed_refund to accept and process failure reasons
- Improved error messages in order notes for different failure scenarios
- Consolidated insufficient balance handling into the main refund failure flow

**Screenshot**

Before:

<img width="279" alt="Screenshot 2025-04-09 at 11 08 46" src="https://github.com/user-attachments/assets/a490bfdb-d5cf-48c4-beae-9beb53055286" />

After:

<img width="279" alt="Screenshot 2025-04-09 at 11 11 02" src="https://github.com/user-attachments/assets/9437a7de-466d-45f5-9c7b-7bed72222856" />


<!--
Title: A descriptive, yet concise, title.
-->

<!--
Description: Write a brief summary about this PR. As you compose your summary, consider each of these questions and address them if appropriate. Why is this change needed? What does this change do? Were there other solutions you considered? Why did you choose to pursue this solution? Describe any trade-offs you might have had to make.
-->

<!--
Questions for PR author:
- How can this code break?
- What are we doing to make sure this code doesn't break?
-->

<!--
Images or gifs: Include before and after screenshots or gifs/videos when it makes sense.
-->

#### Testing instructions

<!--
Testing instructions: How should this be tested and how can a reviewer test the end-user functionality? Are there known issues that you plan to address in a future PR? Are there any side effects that readers should be aware of?
-->

<!--
Add as many details as possible to help others reproduce the issue and test the fix.
"Before / After" screenshots can also be very helpful when the change is visual.
-->

#### 1. Test Refund Failure Scenarios:
- Create a test order and attempt to refund it
- Verify that different failure scenarios show appropriate error messages

#### 2. Test Order Notes:
- Check that order notes are properly formatted and include:
  - The refund amount
  - The failure reason in a user-friendly format

#### 3. Test Insufficient Balance Handling:
- For accounts with FROD support:
  - Verify that the FROD support note is added
- For accounts without FROD support:
  - Verify that the insufficient balance note is added

#### 4. Test Cancellation Flow:
- Test merchant-initiated refund cancellations
- Verify that the cancellation message is properly displayed
- Check that the order status is updated correctly

-------------------

- [ ] Run `npm run changelog` to add a changelog file, choose `patch` to leave it empty if the change is not significant. You can add multiple changelog files in one PR by running this command a few times. 
- [ ] Covered with tests (or have a good reason not to test in description ☝️)
- [ ] Tested on mobile (or does not apply)

**Post merge**

<!--
Make sure you edit the page for the current release when adding testing instructions.
We often create a blank page ahead of time for the next release.
If this PR need not be QA tested, edit to 'QA Testing Not Applicable'
-->

- [ ] Link to testing instructions from [release testing doc](https://github.com/Automattic/woocommerce-payments/wiki/Release-testing-instructions) following [these instructions](https://github.com/Automattic/woocommerce-payments/wiki/How-to-write-good-manual-testing-scenarios) : _Add link here / 'QA Testing Not Applicable'_
- [ ] Add or update [critical flows](https://github.com/Automattic/woocommerce-payments/wiki/Critical-flows) and [testing instructions for critical flows](https://github.com/Automattic/woocommerce-payments/wiki/Testing-instructions-for-critical-flows), if applicable.
- [ ] Add what's changed (description, screenshot, demo videos etc.) to the release announcement post, if applicable.



Diff:
diff --git a/changelog/woopmnt-4892-handle-more-refund-failure-reason-codes-in-refund-notes b/changelog/woopmnt-4892-handle-more-refund-failure-reason-codes-in-refund-notes
new file mode 100644
index 00000000000..3dc54a4da63
--- /dev/null
+++ b/changelog/woopmnt-4892-handle-more-refund-failure-reason-codes-in-refund-notes
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Simplified refund handling with clear errors and standard reasons to aid resolution.
diff --git a/includes/class-wc-payments-order-service.php b/includes/class-wc-payments-order-service.php
index 23b6a9cc2a7..91d1be4f0fd 100644
--- a/includes/class-wc-payments-order-service.php
+++ b/includes/class-wc-payments-order-service.php
@@ -10,6 +10,7 @@
 use WCPay\Constants\Intent_Status;
 use WCPay\Constants\Payment_Method;
 use WCPay\Constants\Refund_Status;
+use WCPay\Constants\Refund_Failure_Reason;
 use WCPay\Exceptions\Order_Not_Found_Exception;
 use WCPay\Fraud_Prevention\Models\Rule;
 use WCPay\Logger;
@@ -1477,42 +1478,53 @@ public function add_note_and_metadata_for_created_refund( WC_Order $order, WC_Or
 	 * @param string               $currency The currency code.
 	 * @param WC_Order_Refund|null $wc_refund The WC refund object to delete if provided.
 	 * @param bool                 $is_cancelled Whether this is a cancellation rather than a failure. Default false.
+	 * @param string|null          $failure_reason The reason for the refund failure. Default null.
 	 * @return void
 	 */
-	public function handle_failed_refund( WC_Order $order, string $refund_id, int $amount, string $currency, ?WC_Order_Refund $wc_refund = null, bool $is_cancelled = false ): void {
+	public function handle_failed_refund( WC_Order $order, string $refund_id, int $amount, string $currency, ?WC_Order_Refund $wc_refund = null, bool $is_cancelled = false, ?string $failure_reason = null ): void {
 		// Delete the refund if it exists.
 		if ( $wc_refund ) {
 			$wc_refund->delete();
 		}
 
-		$note = sprintf(
-			WC_Payments_Utils::esc_interpolated_html(
-				/* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund */
-				__( 'A refund of %1$s was <strong>%4$s</strong> using %2$s (<code>%3$s</code>).', 'woocommerce-payments' ),
-				[
-					'strong' => '<strong>',
-					'code'   => '<code>',
-				]
-			),
-			WC_Payments_Explicit_Price_Formatter::get_explicit_price(
-				wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ),
-				$order
-			),
-			'WooPayments',
-			$refund_id,
-			$is_cancelled ? __( 'cancelled', 'woocommerce-payments' ) : __( 'unsuccessful', 'woocommerce-payments' )
+		$formatted_amount = WC_Payments_Explicit_Price_Formatter::get_explicit_price(
+			wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ),
+			$order
 		);
 
-		if ( $this->order_note_exists( $order, $note ) ) {
-			return;
+		// Handle insufficient balance case first to avoid duplicate notes.
+		if ( Refund_Failure_Reason::INSUFFICIENT_FUNDS === $failure_reason ) {
+			$this->handle_insufficient_balance_for_refund( $order, $amount );
+		} else {
+
+			$note = sprintf(
+				WC_Payments_Utils::esc_interpolated_html(
+					/* translators: %1$s: the refund amount, %2$s: status (cancelled/unsuccessful), %3$s: WooPayments, %4$s: ID of the refund, %5$s: failure message or period */
+					__( 'A refund of %1$s was <strong>%2$s</strong> using %3$s (<code>%4$s</code>)%5$s', 'woocommerce-payments' ),
+					[
+						'strong' => '<strong>',
+						'code'   => '<code>',
+					]
+				),
+				$formatted_amount,
+				$is_cancelled ? __( 'cancelled', 'woocommerce-payments' ) : __( 'unsuccessful', 'woocommerce-payments' ),
+				'WooPayments',
+				$refund_id,
+				$is_cancelled ? '.' : ': ' . Refund_Failure_Reason::get_failure_message( $failure_reason ?? Refund_Failure_Reason::UNKNOWN ),
+			);
+
+			if ( $this->order_note_exists( $order, $note ) ) {
+				return;
+			}
+
+			$order->add_order_note( $note );
 		}
 
-		// If order has been fully refunded.
+		// If order has been fully refunded, change status to failed.
 		if ( Order_Status::REFUNDED === $order->get_status() ) {
 			$order->update_status( Order_Status::FAILED );
 		}
 
-		$order->add_order_note( $note );
 		$this->set_wcpay_refund_status_for_order( $order, Refund_Status::FAILED );
 		$order->save();
 	}
diff --git a/includes/class-wc-payments-webhook-processing-service.php b/includes/class-wc-payments-webhook-processing-service.php
index 10ab6173152..627d109d966 100644
--- a/includes/class-wc-payments-webhook-processing-service.php
+++ b/includes/class-wc-payments-webhook-processing-service.php
@@ -15,6 +15,7 @@
 use WCPay\Exceptions\Rest_Request_Exception;
 use WCPay\Logger;
 use WCPay\Constants\Refund_Status;
+use WCPay\Constants\Refund_Failure_Reason;
 
 if ( ! defined( 'ABSPATH' ) ) {
 	exit; // Exit if accessed directly.
@@ -299,13 +300,10 @@ private function process_webhook_refund_updated( $event_body ) {
 		// Refund update webhook events can be either failed, cancelled (basically it's also a failure but triggered by the merchant), succeeded only.
 		switch ( $status ) {
 			case Refund_Status::FAILED:
-				$this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, $matched_wc_refund );
-				if (
-					$this->has_webhook_property( $event_object, 'failure_reason' )
-					&& 'insufficient_funds' === $this->read_webhook_property( $event_object, 'failure_reason' )
-				) {
-					$this->order_service->handle_insufficient_balance_for_refund( $order, $amount );
-				}
+				$failure_reason = $this->has_webhook_property( $event_object, 'failure_reason' )
+					? $this->read_webhook_property( $event_object, 'failure_reason' )
+					: null;
+				$this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, $matched_wc_refund, false, $failure_reason );
 				break;
 			case Refund_Status::CANCELED:
 				$this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, $matched_wc_refund, true );
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 7ce117d9bd2..51fac38e289 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -484,6 +484,7 @@ public static function init() {
 		include_once __DIR__ . '/constants/class-payment-initiated-by.php';
 		include_once __DIR__ . '/constants/class-intent-status.php';
 		include_once __DIR__ . '/constants/class-refund-status.php';
+		include_once __DIR__ . '/constants/class-refund-failure-reason.php';
 		include_once __DIR__ . '/constants/class-payment-intent-status.php';
 		include_once __DIR__ . '/constants/class-payment-capture-type.php';
 		include_once __DIR__ . '/constants/class-payment-method.php';
diff --git a/includes/constants/class-refund-failure-reason.php b/includes/constants/class-refund-failure-reason.php
new file mode 100644
index 00000000000..6c8992d5bdf
--- /dev/null
+++ b/includes/constants/class-refund-failure-reason.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Class Refund_Failure_Reason
+ *
+ * @package WooCommerce\Payments
+ */
+
+namespace WCPay\Constants;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+
+/**
+ * This class gives a list of all the possible WCPay refund failure reason constants and their user-friendly messages.
+ * Based on Stripe's refund failure reasons: https://docs.stripe.com/api/refunds/object#refund_object-failure_reason
+ *
+ * @psalm-immutable
+ */
+class Refund_Failure_Reason extends Base_Constant {
+	const LOST_OR_STOLEN_CARD                = 'lost_or_stolen_card';
+	const EXPIRED_OR_CANCELED_CARD           = 'expired_or_canceled_card';
+	const CHARGE_FOR_PENDING_REFUND_DISPUTED = 'charge_for_pending_refund_disputed';
+	const INSUFFICIENT_FUNDS                 = 'insufficient_funds';
+	const DECLINED                           = 'declined';
+	const MERCHANT_REQUEST                   = 'merchant_request';
+	const UNKNOWN                            = 'unknown';
+
+	/**
+	 * Get user-friendly message for a failure reason.
+	 *
+	 * @param string $failure_reason The failure reason code.
+	 * @return string The user-friendly message.
+	 */
+	public static function get_failure_message( string $failure_reason ): string {
+		$messages = [
+			self::LOST_OR_STOLEN_CARD                => __( 'The card used for the original payment has been reported lost or stolen.', 'woocommerce-payments' ),
+			self::EXPIRED_OR_CANCELED_CARD           => __( 'The card used for the original payment has expired or been canceled.', 'woocommerce-payments' ),
+			self::CHARGE_FOR_PENDING_REFUND_DISPUTED => __( 'The charge for this refund is being disputed by the customer.', 'woocommerce-payments' ),
+			self::INSUFFICIENT_FUNDS                 => __( 'Insufficient funds in your WooPayments balance.', 'woocommerce-payments' ),
+			self::DECLINED                           => __( 'The refund was declined by the card issuer.', 'woocommerce-payments' ),
+			self::MERCHANT_REQUEST                   => __( 'The refund was canceled at your request.', 'woocommerce-payments' ),
+			self::UNKNOWN                            => __( 'An unknown error occurred while processing the refund.', 'woocommerce-payments' ),
+		];
+
+		return $messages[ $failure_reason ] ?? $messages[ self::UNKNOWN ];
+	}
+}
diff --git a/tests/unit/contants/test-class-refund-failure-reason.php b/tests/unit/contants/test-class-refund-failure-reason.php
new file mode 100644
index 00000000000..bb7cd88d04b
--- /dev/null
+++ b/tests/unit/contants/test-class-refund-failure-reason.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Class Refund_Failure_Reason_Test
+ *
+ * @package WooCommerce\Payments\Tests
+ */
+
+use WCPay\Constants\Refund_Failure_Reason;
+
+/**
+ * Refund_Failure_Reason unit tests.
+ */
+class Refund_Failure_Reason_Test extends WCPAY_UnitTestCase {
+
+	/**
+	 * Test that all failure reason constants are defined.
+	 */
+	public function test_failure_reason_constants_are_defined() {
+		$this->assertTrue( defined( 'WCPay\Constants\Refund_Failure_Reason::LOST_OR_STOLEN_CARD' ) );
+		$this->assertTrue( defined( 'WCPay\Constants\Refund_Failure_Reason::EXPIRED_OR_CANCELED_CARD' ) );
+		$this->assertTrue( defined( 'WCPay\Constants\Refund_Failure_Reason::CHARGE_FOR_PENDING_REFUND_DISPUTED' ) );
+		$this->assertTrue( defined( 'WCPay\Constants\Refund_Failure_Reason::INSUFFICIENT_FUNDS' ) );
+		$this->assertTrue( defined( 'WCPay\Constants\Refund_Failure_Reason::DECLINED' ) );
+		$this->assertTrue( defined( 'WCPay\Constants\Refund_Failure_Reason::MERCHANT_REQUEST' ) );
+		$this->assertTrue( defined( 'WCPay\Constants\Refund_Failure_Reason::UNKNOWN' ) );
+	}
+
+	/**
+	 * Test that get_failure_message returns correct messages for all failure reasons.
+	 */
+	public function test_get_failure_message_returns_correct_messages() {
+		$test_cases = [
+			Refund_Failure_Reason::LOST_OR_STOLEN_CARD => 'The card used for the original payment has been reported lost or stolen.',
+			Refund_Failure_Reason::EXPIRED_OR_CANCELED_CARD => 'The card used for the original payment has expired or been canceled.',
+			Refund_Failure_Reason::CHARGE_FOR_PENDING_REFUND_DISPUTED => 'The charge for this refund is being disputed by the customer.',
+			Refund_Failure_Reason::INSUFFICIENT_FUNDS  => 'Insufficient funds in your WooPayments balance.',
+			Refund_Failure_Reason::DECLINED            => 'The refund was declined by the card issuer.',
+			Refund_Failure_Reason::MERCHANT_REQUEST    => 'The refund was canceled at your request.',
+			Refund_Failure_Reason::UNKNOWN             => 'An unknown error occurred while processing the refund.',
+		];
+
+		foreach ( $test_cases as $reason => $expected_message ) {
+			$this->assertEquals( $expected_message, Refund_Failure_Reason::get_failure_message( $reason ) );
+		}
+	}
+
+	/**
+	 * Test that get_failure_message returns unknown message for invalid reason.
+	 */
+	public function test_get_failure_message_returns_unknown_for_invalid_reason() {
+		$invalid_reason   = 'invalid_reason';
+		$expected_message = 'An unknown error occurred while processing the refund.';
+
+		$this->assertEquals( $expected_message, Refund_Failure_Reason::get_failure_message( $invalid_reason ) );
+	}
+
+	/**
+	 * Test that get_failure_message returns unknown message for empty reason.
+	 */
+	public function test_get_failure_message_returns_unknown_for_empty_reason() {
+		$empty_reason     = '';
+		$expected_message = 'An unknown error occurred while processing the refund.';
+
+		$this->assertEquals( $expected_message, Refund_Failure_Reason::get_failure_message( $empty_reason ) );
+	}
+}
diff --git a/tests/unit/test-class-wc-payments-order-service.php b/tests/unit/test-class-wc-payments-order-service.php
index 3e507472fdf..6f9bc953d32 100644
--- a/tests/unit/test-class-wc-payments-order-service.php
+++ b/tests/unit/test-class-wc-payments-order-service.php
@@ -11,6 +11,7 @@
 use WCPay\Constants\Payment_Method;
 use WCPay\Fraud_Prevention\Models\Rule;
 use WCPay\Constants\Refund_Status;
+use WCPay\Constants\Refund_Failure_Reason;
 
 /**
  * WC_Payments_Order_Service unit tests.
@@ -1562,8 +1563,20 @@ public function test_handle_failed_refund( string $initial_order_status, bool $h
 
 		// Assert: Check order note was added.
 		$notes = wc_get_order_notes( [ 'order_id' => $order->get_id() ] );
-		$this->assertStringContainsString( 'unsuccessful', $notes[0]->content );
-		$this->assertStringContainsString( $refund_id, $notes[0]->content );
+
+		// There should be at least two notes - one for status change and one for the failed refund.
+		$this->assertGreaterThanOrEqual( 2, count( $notes ) );
+
+		// Find our custom note about the unsuccessful refund.
+		$found_unsuccessful_note = false;
+		foreach ( $notes as $note ) {
+			if ( strpos( $note->content, 'unsuccessful' ) !== false ) {
+				$found_unsuccessful_note = true;
+				$this->assertStringContainsString( $refund_id, $note->content );
+				break;
+			}
+		}
+		$this->assertTrue( $found_unsuccessful_note, 'Could not find note about unsuccessful refund' );
 
 		// Assert: If refund existed, it was deleted.
 		if ( $has_refund ) {
@@ -1688,4 +1701,65 @@ public function test_handle_failed_refund_cancelled_updates_order_status(): void
 
 		WC_Helper_Order::delete_order( $order->get_id() );
 	}
+
+	public function test_handle_insufficient_balance_for_refund() {
+		// Create a test order and refund.
+		$order = WC_Helper_Order::create_order();
+		$order->save();
+
+		$refund_amount = 10;
+		$refund        = wc_create_refund(
+			[
+				'amount'   => $refund_amount,
+				'reason'   => 'Testing refund',
+				'order_id' => $order->get_id(),
+			]
+		);
+
+		// Test handling insufficient balance.
+		$this->order_service->handle_insufficient_balance_for_refund( $order, $refund_amount );
+
+		// Check that only one note was added for insufficient funds.
+		$notes = array_filter(
+			wc_get_order_notes( [ 'order_id' => $order->get_id() ] ),
+			function ( $note ) {
+				return strpos( $note->content, 'insufficient funds' ) !== false;
+			}
+		);
+		$this->assertCount( 1, $notes );
+
+		// Clean up.
+		WC_Helper_Order::delete_order( $order->get_id() );
+	}
+
+	/**
+	 * Tests that handle_failed_refund correctly handles the insufficient funds case.
+	 */
+	public function test_handle_failed_refund_with_insufficient_funds() {
+		// Create a test order.
+		$order = WC_Helper_Order::create_order();
+		$order->save();
+
+		$refund_id = 're_123456789';
+		$amount    = 1000; // $10.00
+		$currency  = 'usd';
+
+		// Test handling failed refund with insufficient funds.
+		$this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, null, false, Refund_Failure_Reason::INSUFFICIENT_FUNDS );
+
+		// Check that only one note was added for insufficient funds.
+		$notes = array_filter(
+			wc_get_order_notes( [ 'order_id' => $order->get_id() ] ),
+			function ( $note ) {
+				return strpos( $note->content, 'insufficient funds' ) !== false;
+			}
+		);
+		$this->assertCount( 1, $notes );
+
+		// Check that the refund status was set to failed.
+		$this->assertSame( Refund_Status::FAILED, $this->order_service->get_wcpay_refund_status_for_order( $order ) );
+
+		// Clean up.
+		WC_Helper_Order::delete_order( $order->get_id() );
+	}
 }
diff --git a/tests/unit/test-class-wc-payments-webhook-processing-service.php b/tests/unit/test-class-wc-payments-webhook-processing-service.php
index 665f617e474..f7016c945db 100644
--- a/tests/unit/test-class-wc-payments-webhook-processing-service.php
+++ b/tests/unit/test-class-wc-payments-webhook-processing-service.php
@@ -10,6 +10,7 @@
 use WCPay\Constants\Intent_Status;
 use WCPay\Constants\Payment_Method;
 use WCPay\Constants\Refund_Status;
+use WCPay\Constants\Refund_Failure_Reason;
 use WCPay\Database_Cache;
 use WCPay\Exceptions\Invalid_Payment_Method_Exception;
 use WCPay\Exceptions\Invalid_Webhook_Data_Exception;
@@ -422,8 +423,8 @@ public function test_failed_refund_update_webhook_with_insufficient_funds() {
 
 		$this->order_service
 			->expects( $this->once() )
-			->method( 'handle_insufficient_balance_for_refund' )
-			->with( $this->mock_order, 999 );
+			->method( 'handle_failed_refund' )
+			->with( $this->mock_order, 'test_refund_id', 999, 'gbp', null, false, 'insufficient_funds' );
 
 		$this->webhook_processing_service->process( $this->event_body );
 	}
@@ -2016,4 +2017,75 @@ public function test_payment_intent_failed_handles_terminal_payment() {
 
 		$this->webhook_processing_service->process( $this->event_body );
 	}
+
+	/**
+	 * @dataProvider refund_failure_reason_data_provider
+	 */
+	public function test_process_webhook_refund_updated_handles_different_failure_reasons( string $failure_reason ): void {
+		$this->event_body['type']           = 'charge.refund.updated';
+		$this->event_body['livemode']       = true;
+		$this->event_body['data']['object'] = [
+			'status'         => 'failed',
+			'charge'         => 'test_charge_id',
+			'id'             => 'test_refund_id',
+			'amount'         => 1999,
+			'currency'       => 'usd',
+			'failure_reason' => $failure_reason,
+		];
+
+		$this->mock_db_wrapper
+			->expects( $this->once() )
+			->method( 'order_from_charge_id' )
+			->with( 'test_charge_id' )
+			->willReturn( $this->mock_order );
+
+		$this->order_service
+			->expects( $this->once() )
+			->method( 'handle_failed_refund' )
+			->with(
+				$this->mock_order,
+				'test_refund_id',
+				1999,
+				'usd',
+				null,
+				false,
+				$failure_reason
+			);
+
+		$this->webhook_processing_service->process( $this->event_body );
+	}
+
+	/**
+	 * Data provider for refund failure reason tests.
+	 *
+	 * @return array
+	 */
+	public function refund_failure_reason_data_provider(): array {
+		return [
+			'insufficient_funds'  => [
+				Refund_Failure_Reason::INSUFFICIENT_FUNDS,
+				'Insufficient funds to process the refund',
+			],
+			'declined'            => [
+				Refund_Failure_Reason::DECLINED,
+				'The refund was declined',
+			],
+			'expired_card'        => [
+				Refund_Failure_Reason::EXPIRED_OR_CANCELED_CARD,
+				'The card used for the original payment has expired or been canceled',
+			],
+			'lost_or_stolen_card' => [
+				Refund_Failure_Reason::LOST_OR_STOLEN_CARD,
+				'The card used for the original payment was reported as lost or stolen',
+			],
+			'merchant_request'    => [
+				Refund_Failure_Reason::MERCHANT_REQUEST,
+				'The refund was canceled at your request',
+			],
+			'unknown'             => [
+				Refund_Failure_Reason::UNKNOWN,
+				'An unknown error occurred while processing the refund',
+			],
+		];
+	}
 }


Reviews:
user: login: htdat
body: Thanks for working on this. Some initial feedback.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T08:05:49Z
user: login: htdat
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T08:15:07Z
user: login: mgascam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:47:23Z
user: login: mgascam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:49:19Z
user: login: mgascam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:53:53Z
user: login: htdat
body: ✅ Code change looks good. 
✅ Manual tests work well - [screenshot](https://github.com/user-attachments/assets/5f20b57b-4b8a-4b91-b65d-6b4eed5cf5e7)
ℹ️ I have just one suggestion to simplify the note content creation a bit. 

Pre-approve! 🚢 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-11T04:43:32Z

Review comments:
diff_hunk: @@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Improved refund failure handling with detailed error messages and standardized failure reasons to help merchants better understand and resolve refund issues.
path: changelog/woopmnt-4892-handle-more-refund-failure-reason-codes-in-refund-notes
user: login: htdat
body: 🤔 Maybe make this a bit shorter. 

> Simplified refund handling with clear errors and standard reasons to aid resolution.
created_at: 2025-04-10T07:17:34Z
author_association: MEMBER
diff_hunk: @@ -1477,42 +1479,78 @@ public function add_note_and_metadata_for_created_refund( WC_Order $order, WC_Or
 	 * @param string               $currency The currency code.
 	 * @param WC_Order_Refund|null $wc_refund The WC refund object to delete if provided.
 	 * @param bool                 $is_cancelled Whether this is a cancellation rather than a failure. Default false.
+	 * @param string|null          $failure_reason The reason for the refund failure. Default null.
 	 * @return void
 	 */
-	public function handle_failed_refund( WC_Order $order, string $refund_id, int $amount, string $currency, ?WC_Order_Refund $wc_refund = null, bool $is_cancelled = false ): void {
+	public function handle_failed_refund( WC_Order $order, string $refund_id, int $amount, string $currency, ?WC_Order_Refund $wc_refund = null, bool $is_cancelled = false, ?string $failure_reason = null ): void {
 		// Delete the refund if it exists.
 		if ( $wc_refund ) {
 			$wc_refund->delete();
 		}
 
-		$note = sprintf(
-			WC_Payments_Utils::esc_interpolated_html(
-				/* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund */
-				__( 'A refund of %1$s was <strong>%4$s</strong> using %2$s (<code>%3$s</code>).', 'woocommerce-payments' ),
-				[
-					'strong' => '<strong>',
-					'code'   => '<code>',
-				]
-			),
-			WC_Payments_Explicit_Price_Formatter::get_explicit_price(
-				wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ),
-				$order
-			),
-			'WooPayments',
-			$refund_id,
-			$is_cancelled ? __( 'cancelled', 'woocommerce-payments' ) : __( 'unsuccessful', 'woocommerce-payments' )
+		$formatted_amount = WC_Payments_Explicit_Price_Formatter::get_explicit_price(
+			wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ),
+			$order
 		);
 
+		if ( $is_cancelled ) {
+			$note = sprintf(
+				WC_Payments_Utils::esc_interpolated_html(
+					/* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund */
+					__( 'A refund of %1$s was <strong>cancelled</strong> using %2$s (<code>%3$s</code>).', 'woocommerce-payments' ),
+					[
+						'strong' => '<strong>',
+						'code'   => '<code>',
+					]
+				),
+				$formatted_amount,
+				'WooPayments',
+				$refund_id
+			);
+		} else {
+			$failure_message = $failure_reason ? Refund_Failure_Reason::get_failure_message( $failure_reason ) : __( 'Unknown error occurred', 'woocommerce-payments' );
+			$note            = sprintf(
+				WC_Payments_Utils::esc_interpolated_html(
+					/* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund, %4: failure message */
+					__( 'A refund of %1$s was <strong>unsuccessful</strong> using %2$s (<code>%3$s</code>): %4$s', 'woocommerce-payments' ),
+					[
+						'strong' => '<strong>',
+						'code'   => '<code>',
+					]
+				),
+				$formatted_amount,
+				'WooPayments',
+				$refund_id,
+				$failure_message
+			);
+		}
+
 		if ( $this->order_note_exists( $order, $note ) ) {
 			return;
 		}
 
-		// If order has been fully refunded.
+		Logger::log( $note );
+		$order->add_order_note( $note );
+
+		// Track the refund failure event.
+		Tracker::track_admin( 'wcpay_edit_order_refund_failure', [ 'reason' => $note ] );
+
+		// Handle insufficient balance case.
+		if ( 'insufficient_balance_for_refund' === $failure_reason ) {
+			$account_country = WC_Payments::get_account_service()->get_account_country();
+
+			if ( $this->is_frod_supported( $account_country ) ) {
+				$order->add_order_note( $this->get_frod_support_note( $formatted_amount ) );
+			} else {
+				$order->add_order_note( $this->get_insufficient_balance_note( $formatted_amount ) );
+			}
+		}
path: includes/class-wc-payments-order-service.php
user: login: htdat
body: _Update_: based on https://github.com/Automattic/woocommerce-payments/pull/10677#discussion_r2036786382, I think we do not need this special handling. Instead, we may need to handle it for `Refund_Failure_Reason::INSUFFICIENT_FUNDS` instead.

-- 

Sounds like this special handling can reuse https://github.com/Automattic/woocommerce-payments/blob/e7058eec53c5a8890a9796e0d9de9abd0936723e/includes/class-wc-payments-order-service.php#L2304


created_at: 2025-04-10T08:00:21Z
author_association: MEMBER
diff_hunk: @@ -2311,23 +2312,21 @@ static function ( $refund ) use ( $refund_amount ) {
 			$currency = strtoupper( $refund['currency'] );
 			Tracker::track_admin( 'wcpay_edit_order_refund_success' );
 		} catch ( Exception $e ) {
-			if ( $e instanceof API_Exception && 'insufficient_balance_for_refund' === $e->get_error_code() ) {
-				// Handle insufficient_balance_for_refund error.
-				$this->order_service->handle_insufficient_balance_for_refund( $order, WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) );
+			if ( $e instanceof API_Exception ) {
+				$failure_reason = $e->get_error_code();
path: includes/class-wc-payment-gateway-wcpay.php
user: login: htdat
body: Based on https://docs.stripe.com/refunds#failed-refunds

> When using the API, a [Refund](https://docs.stripe.com/api#refund_object) object’s status transitions to failed and includes these attributes... 

In other words, `failure_reason` is only available in webhook events. 

For the sync request here, as well as [class-wc-rest-payments-refunds-controller.php](https://github.com/Automattic/woocommerce-payments/blob/e7058eec53c5a8890a9796e0d9de9abd0936723e/includes/admin/class-wc-rest-payments-refunds-controller.php#L72-L73), we can not get the failure reason. Therefore, we should keep the current logic, i.e. not making changes here.
created_at: 2025-04-10T08:15:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Improved refund failure handling with detailed error messages and standardized failure reasons to help merchants better understand and resolve refund issues.
path: changelog/woopmnt-4892-handle-more-refund-failure-reason-codes-in-refund-notes
user: login: mgascam
body: Yep your suggestion reads better, changed it here [163fb46](https://github.com/Automattic/woocommerce-payments/pull/10677/commits/163fb4648a2c63d0074d27b5c97bff6ba4e9e7eb)
created_at: 2025-04-10T13:47:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2311,23 +2312,21 @@ static function ( $refund ) use ( $refund_amount ) {
 			$currency = strtoupper( $refund['currency'] );
 			Tracker::track_admin( 'wcpay_edit_order_refund_success' );
 		} catch ( Exception $e ) {
-			if ( $e instanceof API_Exception && 'insufficient_balance_for_refund' === $e->get_error_code() ) {
-				// Handle insufficient_balance_for_refund error.
-				$this->order_service->handle_insufficient_balance_for_refund( $order, WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) );
+			if ( $e instanceof API_Exception ) {
+				$failure_reason = $e->get_error_code();
path: includes/class-wc-payment-gateway-wcpay.php
user: login: mgascam
body: Thanks so much for catching this Dat! I don't know what I was thinking 🤦  I reverted this part in this commit [b1fdc52](https://github.com/Automattic/woocommerce-payments/pull/10677/commits/b1fdc52d75774956f0be73598fa1d3f16c433d94)
created_at: 2025-04-10T13:49:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1477,42 +1479,78 @@ public function add_note_and_metadata_for_created_refund( WC_Order $order, WC_Or
 	 * @param string               $currency The currency code.
 	 * @param WC_Order_Refund|null $wc_refund The WC refund object to delete if provided.
 	 * @param bool                 $is_cancelled Whether this is a cancellation rather than a failure. Default false.
+	 * @param string|null          $failure_reason The reason for the refund failure. Default null.
 	 * @return void
 	 */
-	public function handle_failed_refund( WC_Order $order, string $refund_id, int $amount, string $currency, ?WC_Order_Refund $wc_refund = null, bool $is_cancelled = false ): void {
+	public function handle_failed_refund( WC_Order $order, string $refund_id, int $amount, string $currency, ?WC_Order_Refund $wc_refund = null, bool $is_cancelled = false, ?string $failure_reason = null ): void {
 		// Delete the refund if it exists.
 		if ( $wc_refund ) {
 			$wc_refund->delete();
 		}
 
-		$note = sprintf(
-			WC_Payments_Utils::esc_interpolated_html(
-				/* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund */
-				__( 'A refund of %1$s was <strong>%4$s</strong> using %2$s (<code>%3$s</code>).', 'woocommerce-payments' ),
-				[
-					'strong' => '<strong>',
-					'code'   => '<code>',
-				]
-			),
-			WC_Payments_Explicit_Price_Formatter::get_explicit_price(
-				wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ),
-				$order
-			),
-			'WooPayments',
-			$refund_id,
-			$is_cancelled ? __( 'cancelled', 'woocommerce-payments' ) : __( 'unsuccessful', 'woocommerce-payments' )
+		$formatted_amount = WC_Payments_Explicit_Price_Formatter::get_explicit_price(
+			wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ),
+			$order
 		);
 
+		if ( $is_cancelled ) {
+			$note = sprintf(
+				WC_Payments_Utils::esc_interpolated_html(
+					/* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund */
+					__( 'A refund of %1$s was <strong>cancelled</strong> using %2$s (<code>%3$s</code>).', 'woocommerce-payments' ),
+					[
+						'strong' => '<strong>',
+						'code'   => '<code>',
+					]
+				),
+				$formatted_amount,
+				'WooPayments',
+				$refund_id
+			);
+		} else {
+			$failure_message = $failure_reason ? Refund_Failure_Reason::get_failure_message( $failure_reason ) : __( 'Unknown error occurred', 'woocommerce-payments' );
+			$note            = sprintf(
+				WC_Payments_Utils::esc_interpolated_html(
+					/* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund, %4: failure message */
+					__( 'A refund of %1$s was <strong>unsuccessful</strong> using %2$s (<code>%3$s</code>): %4$s', 'woocommerce-payments' ),
+					[
+						'strong' => '<strong>',
+						'code'   => '<code>',
+					]
+				),
+				$formatted_amount,
+				'WooPayments',
+				$refund_id,
+				$failure_message
+			);
+		}
+
 		if ( $this->order_note_exists( $order, $note ) ) {
 			return;
 		}
 
-		// If order has been fully refunded.
+		Logger::log( $note );
+		$order->add_order_note( $note );
+
+		// Track the refund failure event.
+		Tracker::track_admin( 'wcpay_edit_order_refund_failure', [ 'reason' => $note ] );
+
+		// Handle insufficient balance case.
+		if ( 'insufficient_balance_for_refund' === $failure_reason ) {
+			$account_country = WC_Payments::get_account_service()->get_account_country();
+
+			if ( $this->is_frod_supported( $account_country ) ) {
+				$order->add_order_note( $this->get_frod_support_note( $formatted_amount ) );
+			} else {
+				$order->add_order_note( $this->get_insufficient_balance_note( $formatted_amount ) );
+			}
+		}
path: includes/class-wc-payments-order-service.php
user: login: mgascam
body: 100%, I overlooked those are different errors, again 🤦‍♂️ Your suggestion to reuse `handle_insufficient_balance_for_refund` makes a lot of sense. I'm going with it in this commit [713b4e7](https://github.com/Automattic/woocommerce-payments/pull/10677/commits/713b4e7dc607368ad9b6f977d642e8f931e2fe64) and also making a refactor to try to simplify it a bit. Thanks again!
created_at: 2025-04-10T13:53:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1477,42 +1478,57 @@ public function add_note_and_metadata_for_created_refund( WC_Order $order, WC_Or
 	 * @param string               $currency The currency code.
 	 * @param WC_Order_Refund|null $wc_refund The WC refund object to delete if provided.
 	 * @param bool                 $is_cancelled Whether this is a cancellation rather than a failure. Default false.
+	 * @param string|null          $failure_reason The reason for the refund failure. Default null.
 	 * @return void
 	 */
-	public function handle_failed_refund( WC_Order $order, string $refund_id, int $amount, string $currency, ?WC_Order_Refund $wc_refund = null, bool $is_cancelled = false ): void {
+	public function handle_failed_refund( WC_Order $order, string $refund_id, int $amount, string $currency, ?WC_Order_Refund $wc_refund = null, bool $is_cancelled = false, ?string $failure_reason = null ): void {
 		// Delete the refund if it exists.
 		if ( $wc_refund ) {
 			$wc_refund->delete();
 		}
 
-		$note = sprintf(
-			WC_Payments_Utils::esc_interpolated_html(
-				/* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund */
-				__( 'A refund of %1$s was <strong>%4$s</strong> using %2$s (<code>%3$s</code>).', 'woocommerce-payments' ),
-				[
-					'strong' => '<strong>',
-					'code'   => '<code>',
-				]
-			),
-			WC_Payments_Explicit_Price_Formatter::get_explicit_price(
-				wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ),
-				$order
-			),
-			'WooPayments',
-			$refund_id,
-			$is_cancelled ? __( 'cancelled', 'woocommerce-payments' ) : __( 'unsuccessful', 'woocommerce-payments' )
+		$formatted_amount = WC_Payments_Explicit_Price_Formatter::get_explicit_price(
+			wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ),
+			$order
 		);
 
-		if ( $this->order_note_exists( $order, $note ) ) {
-			return;
+		// Handle insufficient balance case first to avoid duplicate notes.
+		if ( Refund_Failure_Reason::INSUFFICIENT_FUNDS === $failure_reason ) {
+			$this->handle_insufficient_balance_for_refund( $order, $amount );
+		} else {
+			$note_data = [
+				'status'  => $is_cancelled ? __( 'cancelled', 'woocommerce-payments' ) : __( 'unsuccessful', 'woocommerce-payments' ),
+				'message' => $is_cancelled ? '' : ( $failure_reason ? Refund_Failure_Reason::get_failure_message( $failure_reason ) : __( 'Unknown error occurred', 'woocommerce-payments' ) ),
+			];
+
+			$note = sprintf(
+				WC_Payments_Utils::esc_interpolated_html(
+					/* translators: %1$s: the refund amount, %2$s: status (cancelled/unsuccessful), %3$s: WooPayments, %4$s: ID of the refund, %5$s: failure message or period */
+					__( 'A refund of %1$s was <strong>%2$s</strong> using %3$s (<code>%4$s</code>)%5$s', 'woocommerce-payments' ),
+					[
+						'strong' => '<strong>',
+						'code'   => '<code>',
+					]
+				),
+				$formatted_amount,
+				$note_data['status'],
+				'WooPayments',
+				$refund_id,
+				$note_data['message'] ? ': ' . $note_data['message'] : '.'
path: includes/class-wc-payments-order-service.php
user: login: htdat
body: I think we might simplify a bit and no longer need $note_data. 

```suggestion
				$is_cancelled ? __( 'cancelled', 'woocommerce-payments' ) : __( 'unsuccessful', 'woocommerce-payments' ),
				'WooPayments',
				$refund_id,
				$is_cancelled ? '.' : ': ' . Refund_Failure_Reason::get_failure_message( $failure_reason ??  Refund_Failure_Reason::UNKNOWN ),
```
created_at: 2025-04-11T04:31:12Z
author_association: MEMBER

PR comments:
user: login: botwoo
created_at: 2025-04-04T15:16:15Z
body: ### Test the build 

#### Option 1. Jetpack Beta

- Install and activate [Jetpack Beta](https://jetpack.com/download-jetpack-beta/).
- Use this build by searching for PR number `10677` or branch name `woopmnt-4892-handle-more-refund-failure-reason-codes-in-refund-notes` in your-test.site/wp-admin/admin.php?page=jetpack-beta&plugin=woocommerce-payments          

#### Option 2. Jurassic Ninja - available for logged-in A12s

:rocket: [Launch a JN site with this branch](https://jurassic.ninja/create/?jetpack-beta&shortlived&nojetpack&woocommerce&woocommerce-payments-dev-tools&branches.woocommerce-payments=woopmnt-4892-handle-more-refund-failure-reason-codes-in-refund-notes) :rocket:

:information_source: Install this [Tampermonkey script](https://github.com/Automattic/woocommerce-payments/tree/develop/bin/wcpay-live-branches) to get more options.

---

Build info: 

- Latest commit: c3e31c4c0984f32d94a87dbb97f4241628377793
- Build time: 2025-04-11 16:13:15 UTC

_Note: the build is updated when a new commit is pushed to this PR._
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-04-04T15:19:51Z
body: **Size Change:** 0 B 

**Total Size:** 1.27 MB



<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `release/woocommerce-payments/assets/css/admin.css` | 1.45 kB |
| `release/woocommerce-payments/assets/css/admin.rtl.css` | 1.45 kB |
| `release/woocommerce-payments/assets/css/success.css` | 1.06 kB |
| `release/woocommerce-payments/assets/css/success.rtl.css` | 1.06 kB |
| `release/woocommerce-payments/dist/blocks-checkout-rtl.css` | 2.74 kB |
| `release/woocommerce-payments/dist/blocks-checkout.css` | 2.74 kB |
| `release/woocommerce-payments/dist/blocks-checkout.js` | 56.2 kB |
| `release/woocommerce-payments/dist/cart-block.js` | 17.3 kB |
| `release/woocommerce-payments/dist/cart.js` | 5.73 kB |
| `release/woocommerce-payments/dist/checkout-rtl.css` | 1.28 kB |
| `release/woocommerce-payments/dist/checkout.css` | 1.28 kB |
| `release/woocommerce-payments/dist/checkout.js` | 34.7 kB |
| `release/woocommerce-payments/dist/express-checkout-rtl.css` | 323 B |
| `release/woocommerce-payments/dist/express-checkout.css` | 323 B |
| `release/woocommerce-payments/dist/express-checkout.js` | 15.9 kB |
| `release/woocommerce-payments/dist/frontend-tracks.js` | 854 B |
| `release/woocommerce-payments/dist/index-rtl.css` | 19.5 kB |
| `release/woocommerce-payments/dist/index.css` | 19.5 kB |
| `release/woocommerce-payments/dist/index.js` | 238 kB |
| `release/woocommerce-payments/dist/multi-currency-analytics.js` | 1.08 kB |
| `release/woocommerce-payments/dist/multi-currency-rtl.css` | 4.39 kB |
| `release/woocommerce-payments/dist/multi-currency-switcher-block.js` | 60.9 kB |
| `release/woocommerce-payments/dist/multi-currency.css` | 4.39 kB |
| `release/woocommerce-payments/dist/multi-currency.js` | 56.4 kB |
| `release/woocommerce-payments/dist/order-rtl.css` | 740 B |
| `release/woocommerce-payments/dist/order.css` | 740 B |
| `release/woocommerce-payments/dist/order.js` | 42.3 kB |
| `release/woocommerce-payments/dist/payment-gateways-rtl.css` | 1.34 kB |
| `release/woocommerce-payments/dist/payment-gateways.css` | 1.34 kB |
| `release/woocommerce-payments/dist/payment-gateways.js` | 37.4 kB |
| `release/woocommerce-payments/dist/plugins-page-rtl.css` | 375 B |
| `release/woocommerce-payments/dist/plugins-page.css` | 375 B |
| `release/woocommerce-payments/dist/plugins-page.js` | 20.1 kB |
| `release/woocommerce-payments/dist/product-details-rtl.css` | 433 B |
| `release/woocommerce-payments/dist/product-details.css` | 436 B |
| `release/woocommerce-payments/dist/product-details.js` | 12.7 kB |
| `release/woocommerce-payments/dist/settings-rtl.css` | 11.9 kB |
| `release/woocommerce-payments/dist/settings.css` | 11.8 kB |
| `release/woocommerce-payments/dist/settings.js` | 222 kB |
| `release/woocommerce-payments/dist/subscription-edit-page.js` | 703 B |
| `release/woocommerce-payments/dist/subscription-product-onboarding-modal-rtl.css` | 524 B |
| `release/woocommerce-payments/dist/subscription-product-onboarding-modal.css` | 524 B |
| `release/woocommerce-payments/dist/subscription-product-onboarding-modal.js` | 20.2 kB |
| `release/woocommerce-payments/dist/subscription-product-onboarding-toast.js` | 730 B |
| `release/woocommerce-payments/dist/subscriptions-empty-state-rtl.css` | 120 B |
| `release/woocommerce-payments/dist/subscriptions-empty-state.css` | 120 B |
| `release/woocommerce-payments/dist/subscriptions-empty-state.js` | 19.3 kB |
| `release/woocommerce-payments/dist/success.js` | 6.03 kB |
| `release/woocommerce-payments/dist/tokenized-express-checkout-rtl.css` | 323 B |
| `release/woocommerce-payments/dist/tokenized-express-checkout.css` | 323 B |
| `release/woocommerce-payments/dist/tokenized-express-checkout.js` | 17.1 kB |
| `release/woocommerce-payments/dist/tos-rtl.css` | 235 B |
| `release/woocommerce-payments/dist/tos.css` | 235 B |
| `release/woocommerce-payments/dist/tos.js` | 21.8 kB |
| `release/woocommerce-payments/dist/woopay-direct-checkout.js` | 6.13 kB |
| `release/woocommerce-payments/dist/woopay-express-button.js` | 23.5 kB |
| `release/woocommerce-payments/dist/woopay-rtl.css` | 4.31 kB |
| `release/woocommerce-payments/dist/woopay.css` | 4.28 kB |
| `release/woocommerce-payments/dist/woopay.js` | 71.1 kB |
| `release/woocommerce-payments/includes/subscriptions/assets/css/plugin-page.css` | 625 B |
| `release/woocommerce-payments/includes/subscriptions/assets/js/plugin-page.js` | 814 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-assets/build/i18n-loader.js` | 2.46 kB |
| `release/woocommerce-payments/vendor/automattic/jetpack-assets/build/jetpack-script-data.js` | 957 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-assets/src/js/i18n-loader.js` | 1.02 kB |
| `release/woocommerce-payments/vendor/automattic/jetpack-assets/src/js/script-data.js` | 69 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/babel.config.js` | 163 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/identity-crisis.css` | 2.47 kB |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/identity-crisis.js` | 14.3 kB |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/identity-crisis.rtl.css` | 2.47 kB |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-connection.css` | 10.1 kB |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-connection.js` | 29.7 kB |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-connection.rtl.css` | 10.1 kB |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-admin-create-user.css` | 198 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-admin-create-user.js` | 280 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-admin-create-user.rtl.css` | 198 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-login.css` | 625 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-login.js` | 333 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-login.rtl.css` | 626 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-users.js` | 417 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-users-connection.js` | 161 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/tracks-ajax.js` | 521 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/tracks-callables.js` | 585 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-admin-create-user.css` | 215 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-admin-create-user.js` | 521 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-login.css` | 721 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-login.js` | 412 B |
| `release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-users.js` | 625 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/about.css` | 1.04 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/admin-empty-state.css` | 294 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/admin-order-statuses.css` | 408 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/admin.css` | 3.59 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/checkout.css` | 301 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/modal.css` | 746 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/view-subscription.css` | 574 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/wcs-upgrade.css` | 414 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/admin-pointers.js` | 543 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/admin.js` | 9.4 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/jstz.js` | 6.78 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/jstz.min.js` | 3.84 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/meta-boxes-coupon.js` | 545 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/meta-boxes-subscription.js` | 2.52 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/moment.js` | 22.2 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/moment.min.js` | 11.7 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/payment-method-restrictions.js` | 1.29 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/wcs-meta-boxes-order.js` | 507 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/frontend/payment-methods.js` | 358 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/frontend/single-product.js` | 428 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/frontend/view-subscription.js` | 1.38 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/frontend/wcs-cart.js` | 782 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/modal.js` | 1.09 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/wcs-upgrade.js` | 1.26 kB |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/build/index.css` | 391 B |
| `release/woocommerce-payments/vendor/woocommerce/subscriptions-core/build/index.js` | 3.04 kB |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: mgascam
created_at: 2025-04-10T14:55:33Z
body: Ready for new round of reviews. I noticed a shopper E2E test is failing but seems unrelated to this change 🤔 
author_association: CONTRIBUTOR