Commit 697a22e69d9 for woocommerce
commit 697a22e69d9a5820af8ed847bccc1617aa5a8f46
Author: Miguel Gasca <miguel.gasca@automattic.com>
Date: Fri Sep 11 19:52:14 2026 +0200
Fix refunds being attributed to user #1 when no user is logged in (#68428)
wc_create_refund() fell back to user ID 1 whenever get_current_user_id()
returned 0. The fallback dates to 2014 (daf14fc57e), where it guarded against
writing an empty post_author, and survived the move to CRUD props.
Refunds are routinely created with nobody logged in: a gateway webhook, a REST
call, WP-CLI, cron. Each one was recorded against whoever user 1 happens to be,
naming that person in the admin refund row and in the REST refunded_by field.
Pass get_current_user_id() through unchanged. WC_Order_Refund already defaults
refunded_by to 0, and both the HPOS and CPT stores round-trip a zero, so a
refund nobody issued now stays unattributed.
diff --git a/plugins/woocommerce/changelog/36329-fix-refund-attributed-to-user-1 b/plugins/woocommerce/changelog/36329-fix-refund-attributed-to-user-1
new file mode 100644
index 00000000000..d4745ba8633
--- /dev/null
+++ b/plugins/woocommerce/changelog/36329-fix-refund-attributed-to-user-1
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop attributing refunds to user #1 when no user is logged in. refunded_by is now 0 in REST v1-v3 and null in v4, and the admin labels these refunds as System.
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-refund.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-refund.php
index 4900f3c65c5..c9a63349bea 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-refund.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-refund.php
@@ -10,25 +10,36 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
-$who_refunded = new WP_User( $refund->get_refunded_by() );
+$refunded_by = $refund->get_refunded_by();
+$who_refunded = new WP_User( $refunded_by );
+
+if ( $who_refunded->exists() ) {
+ $refund_author = sprintf(
+ '<abbr class="refund_by" title="%1$s">%2$s</abbr>',
+ /* translators: 1: ID who refunded */
+ sprintf( esc_attr__( 'ID: %d', 'woocommerce' ), absint( $who_refunded->ID ) ),
+ esc_html( $who_refunded->display_name )
+ );
+} elseif ( ! $refunded_by ) {
+ // Nobody was logged in when the refund was created, so no person can be named.
+ $refund_author = esc_html__( 'System', 'woocommerce' );
+} else {
+ // A user was recorded but their account is gone, so there is no name left to show.
+ $refund_author = '';
+}
?>
<tr class="refund <?php echo ( ! empty( $class ) ) ? esc_attr( $class ) : ''; ?>" data-order_refund_id="<?php echo esc_attr( $refund->get_id() ); ?>">
<td class="thumb"><div></div></td>
<td class="name">
<?php
- if ( $who_refunded->exists() ) {
+ if ( $refund_author ) {
printf(
- /* translators: 1: refund id 2: refund date 3: username */
+ /* translators: 1: refund id 2: refund date 3: username, or "System" when no user issued the refund */
esc_html__( 'Refund #%1$s - %2$s by %3$s', 'woocommerce' ),
esc_html( $refund->get_id() ),
esc_html( wc_format_datetime( $refund->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) ) ),
- sprintf(
- '<abbr class="refund_by" title="%1$s">%2$s</abbr>',
- /* translators: 1: ID who refunded */
- sprintf( esc_attr__( 'ID: %d', 'woocommerce' ), absint( $who_refunded->ID ) ),
- esc_html( $who_refunded->display_name )
- )
+ $refund_author // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above, where it is built.
);
} else {
printf(
diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php
index 028062a4f39..8bf8b3833c2 100644
--- a/plugins/woocommerce/includes/wc-order-functions.php
+++ b/plugins/woocommerce/includes/wc-order-functions.php
@@ -588,7 +588,9 @@ function wc_create_refund( $args = array() ) {
$refund->set_currency( $order->get_currency() );
$refund->set_amount( $args['amount'] );
$refund->set_parent_id( absint( $args['order_id'] ) );
- $refund->set_refunded_by( get_current_user_id() ? get_current_user_id() : 1 );
+ // A refund can be created with nobody logged in (gateway webhook, REST, WP-CLI, cron). Record no
+ // user in that case; falling back to user 1 attributes the refund to whoever that happens to be.
+ $refund->set_refunded_by( get_current_user_id() );
$refund->set_prices_include_tax( $order->get_prices_include_tax() );
if ( ! is_null( $args['reason'] ) ) {
diff --git a/plugins/woocommerce/tests/php/includes/admin/meta-boxes/views/html-order-refund-test.php b/plugins/woocommerce/tests/php/includes/admin/meta-boxes/views/html-order-refund-test.php
new file mode 100644
index 00000000000..a0b51e0e7a9
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/admin/meta-boxes/views/html-order-refund-test.php
@@ -0,0 +1,90 @@
+<?php
+declare( strict_types = 1 );
+
+/**
+ * Tests for the refund row rendered inside the order items meta box.
+ *
+ * @package WooCommerce\Tests\Admin
+ */
+class Html_Order_Refund_Test extends WC_Unit_Test_Case {
+
+ /**
+ * Render the refund row markup for a refund.
+ *
+ * @param WC_Order_Refund $refund The refund to render.
+ * @return string The rendered markup.
+ */
+ private function render_refund_row( WC_Order_Refund $refund ): string {
+ // Both are read by the view and have no defaults of their own.
+ $order_taxes = array();
+ $cogs_is_enabled = false;
+
+ ob_start();
+ include WC_ABSPATH . 'includes/admin/meta-boxes/views/html-order-refund.php';
+
+ return (string) ob_get_clean();
+ }
+
+ /**
+ * Create a refund on a throwaway order, attributed to the given user.
+ *
+ * @param int $user_id The user to attribute the refund to.
+ * @return WC_Order_Refund
+ */
+ private function create_refund_attributed_to( int $user_id ): WC_Order_Refund {
+ $order = WC_Helper_Order::create_order();
+
+ $refund = new WC_Order_Refund();
+ $refund->set_amount( 10 );
+ $refund->set_parent_id( $order->get_id() );
+ $refund->set_refunded_by( $user_id );
+ $refund->save();
+
+ return $refund;
+ }
+
+ /**
+ * @testdox Should name the user who issued the refund.
+ */
+ public function test_names_the_refunding_user(): void {
+ $user_id = self::factory()->user->create(
+ array(
+ 'role' => 'administrator',
+ 'display_name' => 'Ada Lovelace',
+ )
+ );
+
+ $markup = $this->render_refund_row( $this->create_refund_attributed_to( $user_id ) );
+
+ $this->assertStringContainsString( 'Ada Lovelace', $markup );
+ }
+
+ /**
+ * @testdox Should attribute a refund with no recorded user to the system.
+ *
+ * @see https://github.com/woocommerce/woocommerce/issues/36329
+ */
+ public function test_labels_an_unattributed_refund_as_system(): void {
+ $markup = $this->render_refund_row( $this->create_refund_attributed_to( 0 ) );
+
+ $this->assertStringContainsString( 'by System', $markup );
+ }
+
+ /**
+ * @testdox Should not attribute a refund to the system when its user was deleted.
+ */
+ public function test_omits_attribution_when_the_refunding_user_was_deleted(): void {
+ $user_id = self::factory()->user->create();
+ $refund = $this->create_refund_attributed_to( $user_id );
+ wp_delete_user( $user_id );
+
+ $markup = $this->render_refund_row( $refund );
+
+ $this->assertStringNotContainsString(
+ 'by System',
+ $markup,
+ 'A deleted user is not the system; the refund was issued by a person whose account is gone.'
+ );
+ $this->assertStringNotContainsString( ' by ', $markup );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller-test.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller-test.php
index 38933b91236..e5a35e1b1e3 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller-test.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller-test.php
@@ -163,4 +163,39 @@ class WC_REST_Order_Refunds_Controller_Test extends WC_REST_Unit_Test_Case {
$this->assert_incomplete_meta_data_handled_correctly( wc_get_order( $response->get_data()['id'] ) );
}
+
+ /**
+ * @testdox Should report refunded_by as 0 when no user issued the refund.
+ *
+ * @see https://github.com/woocommerce/woocommerce/issues/36329
+ */
+ public function test_refunded_by_is_zero_when_no_user_issued_the_refund(): void {
+ wp_set_current_user( 1 );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_regular_price( '10' );
+ $product->save();
+
+ $order = wc_create_order();
+ $order->add_product( $product, 1 );
+ $order->calculate_totals();
+ $order->save();
+
+ // A refund with no recorded user, as a gateway webhook or cron run produces.
+ $refund = new WC_Order_Refund();
+ $refund->set_amount( 5 );
+ $refund->set_parent_id( $order->get_id() );
+ $refund->set_refunded_by( 0 );
+ $refund->save();
+
+ $request = new WP_REST_Request( 'GET', '/wc/v3/orders/' . $order->get_id() . '/refunds/' . $refund->get_id() );
+ $response = $this->server->dispatch( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+ $this->assertSame(
+ 0,
+ $response->get_data()['refunded_by'],
+ 'An unattributed refund reports 0 rather than the id of user 1.'
+ );
+ }
}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-controller-tests.php
index f8a0509f8a5..ae6c5ddbabd 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-controller-tests.php
@@ -297,6 +297,32 @@ class WC_REST_Refunds_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->validate_response_against_schema( $response_data, $schema_properties );
}
+ /**
+ * @testdox Should report refunded_by as null when no user issued the refund.
+ *
+ * @see https://github.com/woocommerce/woocommerce/issues/36329
+ */
+ public function test_refunds_get_endpoint_reports_null_refunded_by_when_no_user_issued_the_refund(): void {
+ $order = $this->create_test_order();
+
+ // A refund with no recorded user, as a gateway webhook or cron run produces.
+ $refund = new WC_Order_Refund();
+ $refund->set_amount( 5 );
+ $refund->set_parent_id( $order->get_id() );
+ $refund->set_refunded_by( 0 );
+ $refund->save();
+ $this->created_refunds[] = $refund->get_id();
+
+ $request = new WP_REST_Request( 'GET', '/wc/v4/refunds/' . $refund->get_id() );
+ $response = $this->server->dispatch( $request );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertNull(
+ $response->get_data()['refunded_by'],
+ 'An unattributed refund reports a null refunded_by rather than an object for user 1.'
+ );
+ }
+
/**
* Test POST /wc/v4/refunds endpoint creates refund.
*/
diff --git a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
index 8de6654215a..0e06b905eda 100644
--- a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
@@ -557,4 +557,53 @@ class WC_Order_Functions_Test extends \WC_Unit_Test_Case {
$order->delete();
$customer->delete();
}
+
+ /**
+ * @testdox Should record the logged-in user who issued the refund.
+ */
+ public function test_wc_create_refund_records_the_current_user(): void {
+ $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
+ wp_set_current_user( $user_id );
+
+ $order = WC_Helper_Order::create_order();
+ $refund = wc_create_refund(
+ array(
+ 'order_id' => $order->get_id(),
+ 'amount' => 10,
+ )
+ );
+
+ $this->assertNotWPError( $refund );
+ $this->assertSame( $user_id, $refund->get_refunded_by() );
+ $this->assertSame(
+ $user_id,
+ wc_get_order( $refund->get_id() )->get_refunded_by(),
+ 'The refunding user should survive a round trip through the data store.'
+ );
+ }
+
+ /**
+ * @testdox Should attribute a refund to nobody when no user is logged in, rather than to user 1.
+ *
+ * @see https://github.com/woocommerce/woocommerce/issues/36329
+ */
+ public function test_wc_create_refund_records_no_user_when_nobody_is_logged_in(): void {
+ wp_set_current_user( 0 );
+
+ $order = WC_Helper_Order::create_order();
+ $refund = wc_create_refund(
+ array(
+ 'order_id' => $order->get_id(),
+ 'amount' => 10,
+ )
+ );
+
+ $this->assertNotWPError( $refund );
+ $this->assertSame( 0, $refund->get_refunded_by() );
+ $this->assertSame(
+ 0,
+ wc_get_order( $refund->get_id() )->get_refunded_by(),
+ 'An unattributed refund should stay unattributed after a round trip through the data store.'
+ );
+ }
}