Commit 17d9e02e2d8 for woocommerce

commit 17d9e02e2d83b463c7fc32323e9bb0d021417a18
Author: Peter Petrov <peter.petrov89@gmail.com>
Date:   Thu Aug 20 14:05:21 2026 +0300

    Move role column to end of customers report table for consistent CSV order (#67853)

diff --git a/plugins/woocommerce/changelog/role-column-last-in-customers-table b/plugins/woocommerce/changelog/role-column-last-in-customers-table
new file mode 100644
index 00000000000..f9783f6b0d3
--- /dev/null
+++ b/plugins/woocommerce/changelog/role-column-last-in-customers-table
@@ -0,0 +1,5 @@
+Significance: patch
+Type: tweak
+Comment: Aligns the browser-side Customers CSV with the server export order from #67843; the Role column feature (#67653) is unreleased so no shipped behavior changes.
+
+
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/customers/table.js b/plugins/woocommerce/client/admin/client/analytics/report/customers/table.js
index cfe49039982..565a1074f86 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/customers/table.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/customers/table.js
@@ -35,6 +35,11 @@ function CustomersReportTable( {
 		};
 	} );

+	// This order is also the browser-side CSV export order (ReportTable exports
+	// the table as-is when the report fits one page), so it must match
+	// Controller::get_export_columns() in
+	// src/Admin/API/Reports/Customers/Controller.php, which the same Download
+	// button uses for larger reports. New columns go last in both.
 	const getHeadersContent = () => {
 		return [
 			{
@@ -49,10 +54,6 @@ function CustomersReportTable( {
 				key: 'username',
 				hiddenByDefault: true,
 			},
-			{
-				label: __( 'Role', 'woocommerce' ),
-				key: 'role',
-			},
 			{
 				label: __( 'Last active', 'woocommerce' ),
 				key: 'date_last_active',
@@ -119,6 +120,10 @@ function CustomersReportTable( {
 				key: 'shipping_phone',
 				hiddenByDefault: true,
 			},
+			{
+				label: __( 'Role', 'woocommerce' ),
+				key: 'role',
+			},
 		];
 	};

@@ -206,10 +211,6 @@ function CustomersReportTable( {
 					display: username,
 					value: username,
 				},
-				{
-					display: role,
-					value: role,
-				},
 				{
 					display: dateLastActiveDisplay,
 					value: dateLastActive,
@@ -262,6 +263,10 @@ function CustomersReportTable( {
 					display: shippingPhone,
 					value: shippingPhone,
 				},
+				{
+					display: role,
+					value: role,
+				},
 			];
 		} );
 	};
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/customers/test/table.test.js b/plugins/woocommerce/client/admin/client/analytics/report/customers/test/table.test.js
index 1a0a96d6ff2..c1b54ae7ca0 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/customers/test/table.test.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/customers/test/table.test.js
@@ -71,8 +71,30 @@ const baseCustomer = {
 	country: '',
 };

-// Country cell is the 10th column (0-indexed: 9) per getHeadersContent in table.js.
-const COUNTRY_COL = 9;
+// getHeadersContent in table.js, in order. This is also the browser-side CSV
+// export order, so it must match Controller::get_export_columns() in
+// src/Admin/API/Reports/Customers/Controller.php.
+const HEADER_KEYS = [
+	'name',
+	'username',
+	'date_last_active',
+	'date_registered',
+	'email',
+	'orders_count',
+	'total_spend',
+	'avg_order_value',
+	'country',
+	'city',
+	'state',
+	'postcode',
+	'billing_phone',
+	'shipping_phone',
+	'role',
+];
+
+const col = ( key ) => HEADER_KEYS.indexOf( key );
+
+const COUNTRY_COL = col( 'country' );

 function getCountryCell( customer ) {
 	captured.getRowsContent = null;
@@ -160,10 +182,63 @@ describe( 'CustomersReportTable country cell', () => {
 	} );
 } );

+describe( 'CustomersReportTable column order', () => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+	} );
+
+	// The same Download button exports the table client-side for single-page
+	// reports and server-side for larger ones, so drift between the two orders
+	// makes identical reports produce differently ordered CSVs.
+	it( 'keeps the header order in sync with the server-side CSV export', () => {
+		mockCountriesStore( [] );
+		captured.getHeadersContent = null;
+		render( <CustomersReportTable query={ {} } /> );
+
+		expect(
+			captured.getHeadersContent().map( ( header ) => header.key )
+		).toEqual( HEADER_KEYS );
+	} );
+
+	it( 'emits every cell under its own header', () => {
+		mockCountriesStore( [] );
+		captured.getRowsContent = null;
+		render( <CustomersReportTable query={ {} } /> );
+
+		// One distinct value per field, so a cell landing under the wrong
+		// header surfaces as a mismatch instead of passing by coincidence.
+		const customer = {
+			name: 'Alice',
+			username: 'alice',
+			date_last_active: '2026-08-01T00:00:00',
+			date_registered: '2026-07-01T00:00:00',
+			email: 'alice@example.com',
+			orders_count: 7,
+			total_spend: 123,
+			avg_order_value: 45,
+			country: 'FR',
+			city: 'Paris',
+			state: 'IDF',
+			postcode: '75001',
+			billing_phone: '555-32123',
+			shipping_phone: '555-99887',
+			role: 'Customer',
+		};
+
+		const row = captured.getRowsContent( [ customer ] )[ 0 ];
+
+		expect( row ).toHaveLength( HEADER_KEYS.length );
+		expect(
+			Object.fromEntries(
+				HEADER_KEYS.map( ( key, index ) => [ key, row[ index ].value ] )
+			)
+		).toEqual( customer );
+	} );
+} );
+
 describe( 'CustomersReportTable phone cells', () => {
-	// Phone cells are the last two columns per getHeadersContent in table.js.
-	const BILLING_PHONE_COL = 13;
-	const SHIPPING_PHONE_COL = 14;
+	const BILLING_PHONE_COL = col( 'billing_phone' );
+	const SHIPPING_PHONE_COL = col( 'shipping_phone' );

 	beforeEach( () => {
 		jest.clearAllMocks();
@@ -193,6 +268,5 @@ describe( 'CustomersReportTable phone cells', () => {

 		expect( headers[ BILLING_PHONE_COL ].key ).toBe( 'billing_phone' );
 		expect( headers[ SHIPPING_PHONE_COL ].key ).toBe( 'shipping_phone' );
-		expect( headers ).toHaveLength( SHIPPING_PHONE_COL + 1 );
 	} );
 } );
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php
index 1b673ccea04..97e34d35830 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php
@@ -751,6 +751,10 @@ class Controller extends GenericController implements ExportableInterface {
 	 * @return array Key value pair of Column ID => Label.
 	 */
 	public function get_export_columns() {
+		// Appending keeps positional consumers of the released columns working, and
+		// this order must match getHeadersContent() in
+		// client/admin/client/analytics/report/customers/table.js, which produces the
+		// browser-side CSV for single-page reports from the same Download button.
 		$export_columns = array(
 			'name'            => __( 'Name', 'woocommerce' ),
 			'username'        => __( 'Username', 'woocommerce' ),
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
index 0e386ea709a..9b6630d94d2 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
@@ -257,7 +257,6 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
 		$export_columns = $controller->get_export_columns();
 		$this->assertArrayHasKey( 'role', $export_columns, 'CSV export should include a role column' );
 		$this->assertEquals( 'Role', $export_columns['role'] );
-		$this->assertSame( 'role', array_key_last( $export_columns ), 'Role must stay the last CSV column so positional consumers of the pre-existing columns are unaffected' );

 		$export_roles_by_user_id = array();
 		foreach ( $reports as $report ) {
@@ -270,6 +269,39 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
 		$this->assertSame( '', $export_roles_by_user_id[0], 'CSV export should leave the role empty for guests' );
 	}

+	/**
+	 * The same Download button exports single-page reports in the browser from the
+	 * table column order and larger ones from here, so the two must not drift.
+	 *
+	 * @testdox Should keep the CSV export column order in sync with the report table.
+	 */
+	public function test_export_column_order_matches_report_table() {
+		// Mirrors getHeadersContent() in
+		// client/admin/client/analytics/report/customers/table.js. Keys differ
+		// between the two, the order must not.
+		$expected_order = array(
+			'name',
+			'username',
+			'last_active',
+			'registered',
+			'email',
+			'orders_count',
+			'total_spend',
+			'avg_order_value',
+			'country',
+			'city',
+			'region',
+			'postcode',
+			'billing_phone',
+			'shipping_phone',
+			'role',
+		);
+
+		$controller = new \Automattic\WooCommerce\Admin\API\Reports\Customers\Controller();
+
+		$this->assertSame( $expected_order, array_keys( $controller->get_export_columns() ), 'New CSV columns must be appended, and table.js must be updated to match' );
+	}
+
 	/**
 	 * Test getting reports.
 	 *