diff --git a/changelog/refactor-server-export-sync-download b/changelog/refactor-server-export-sync-download
new file mode 100644
index 00000000000..eaff6dbbf0c
--- /dev/null
+++ b/changelog/refactor-server-export-sync-download
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Change Transactions sync download to use server export.
diff --git a/client/data/transactions/resolvers.js b/client/data/transactions/resolvers.js
index 54d96fb7127..9bcefa2ce56 100644
--- a/client/data/transactions/resolvers.js
+++ b/client/data/transactions/resolvers.js
@@ -89,10 +89,10 @@ export function* getTransactions( query ) {
}
export function getTransactionsCSV( query ) {
- const path = addQueryArgs(
- `${ NAMESPACE }/transactions/download`,
- formatQueryFilters( query )
- );
+ const path = addQueryArgs( `${ NAMESPACE }/transactions/download`, {
+ ...formatQueryFilters( query ),
+ download_type: query.downloadType,
+ } );
return path;
}
diff --git a/client/transactions/list/index.tsx b/client/transactions/list/index.tsx
index f33586e26f5..c1735c6c9ba 100644
--- a/client/transactions/list/index.tsx
+++ b/client/transactions/list/index.tsx
@@ -7,6 +7,7 @@ import React, { Fragment, useState } from 'react';
import { uniq } from 'lodash';
import { useDispatch } from '@wordpress/data';
import { __, _n, sprintf } from '@wordpress/i18n';
+
import {
TableCard,
Search,
@@ -18,11 +19,6 @@ import {
getQuery,
updateQueryString,
} from '@woocommerce/navigation';
-import {
- downloadCSVFile,
- generateCSVDataFromTable,
- generateCSVFileName,
-} from '@woocommerce/csv-export';
import apiFetch from '@wordpress/api-fetch';
/**
@@ -84,6 +80,11 @@ interface Column extends TableCardColumn {
labelInCsv?: string;
}
+interface TransactionExportResponse {
+ download_url?: string;
+ exported_transactions: number;
+}
+
const getPaymentSourceDetails = ( txn: Transaction ) => {
if ( ! txn.source_identifier ) {
return ;
@@ -580,6 +581,7 @@ export const TransactionsList = (
const downloadable = !! rows.length;
const endpointExport = async () => {
+ const downloadType = totalRows > rows.length ? 'async' : 'sync';
// We destructure page and path to get the right params.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { page, path, ...params } = getQuery();
@@ -642,7 +644,7 @@ export const TransactionsList = (
window.confirm( confirmMessage )
) {
try {
- await apiFetch( {
+ const response = await apiFetch< TransactionExportResponse >( {
path: getTransactionsCSV( {
userEmail,
userLocale,
@@ -666,20 +668,29 @@ export const TransactionsList = (
riskLevelIs,
riskLevelIsNot,
depositId,
+ downloadType,
} ),
method: 'POST',
} );
- createNotice(
- 'success',
- sprintf(
- __(
- 'Your export will be emailed to %s',
- 'woocommerce-payments'
- ),
- userEmail
- )
- );
+ if ( response?.download_url ) {
+ const link = document.createElement( 'a' );
+ // Add force_download=true to the URL to force the download, which adds the appropriate `Content-Disposition: attachment` header when using production server.
+ link.href = response.download_url + '?force_download=true';
+ link.click();
+ } else {
+ // Show email notification if no direct download URL
+ createNotice(
+ 'success',
+ sprintf(
+ __(
+ 'Your export will be emailed to %s',
+ 'woocommerce-payments'
+ ),
+ userEmail
+ )
+ );
+ }
} catch {
createNotice(
'error',
@@ -698,29 +709,14 @@ export const TransactionsList = (
// We destructure page and path to get the right params.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { page, path, ...params } = getQuery();
- const downloadType = totalRows > rows.length ? 'endpoint' : 'browser';
recordEvent( 'wcpay_transactions_download_csv_click', {
location: props.depositId ? 'deposit_details' : 'transactions',
- download_type: downloadType,
exported_transactions: rows.length,
total_transactions: transactionsSummary.count,
} );
- if ( 'endpoint' === downloadType ) {
- endpointExport();
- } else {
- const columnsToDisplayInCsv = columnsToDisplay.map( ( column ) => {
- if ( column.labelInCsv ) {
- return { ...column, label: column.labelInCsv };
- }
- return column;
- } );
- downloadCSVFile(
- generateCSVFileName( title, params ),
- generateCSVDataFromTable( columnsToDisplayInCsv, rows )
- );
- }
+ endpointExport();
setIsDownloading( false );
};
diff --git a/client/transactions/list/test/index.tsx b/client/transactions/list/test/index.tsx
index 5d4efbf827e..ea38361a826 100644
--- a/client/transactions/list/test/index.tsx
+++ b/client/transactions/list/test/index.tsx
@@ -7,13 +7,9 @@ import * as React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import user from '@testing-library/user-event';
import apiFetch from '@wordpress/api-fetch';
-import { dateI18n } from '@wordpress/date';
-import { downloadCSVFile } from '@woocommerce/csv-export';
import { getQuery, updateQueryString } from '@woocommerce/navigation';
import { useUserPreferences } from '@woocommerce/data';
import { getUserTimeZone } from 'wcpay/utils/test-utils';
-import moment from 'moment';
-import os from 'os';
/**
* Internal dependencies
@@ -72,10 +68,6 @@ jest.mock( '@wordpress/date', () => ( {
} ),
} ) );
-const mockDownloadCSVFile = downloadCSVFile as jest.MockedFunction<
- typeof downloadCSVFile
->;
-
const mockApiFetch = apiFetch as jest.MockedFunction< typeof apiFetch >;
const mockUseTransactions = useTransactions as jest.MockedFunction<
@@ -213,18 +205,6 @@ const getMockTransactions: () => Transaction[] = () => [
},
];
-function getUnformattedAmount( formattedAmount: string ) {
- const amount = formattedAmount.replace( /[^0-9,.' ]/g, '' ).trim();
- return amount.replace( ',', '.' ); // Euro fix
-}
-
-function formatDate( date: string ) {
- return dateI18n(
- 'M j, Y / g:iA',
- moment.utc( date ).local().toISOString()
- );
-}
-
describe( 'Transactions list', () => {
beforeEach( () => {
jest.clearAllMocks();
@@ -664,115 +644,33 @@ describe( 'Transactions list', () => {
getByRole( 'button', { name: 'Download' } ).click();
+ // Check if the API request is made with the correct download type = 'async'.
await waitFor( () => {
expect( mockApiFetch ).toHaveBeenCalledTimes( 1 );
expect( mockApiFetch ).toHaveBeenCalledWith( {
method: 'POST',
path: `/wc/v3/payments/transactions/download?user_email=mock%40example.com&deposit_id=po_mock&user_timezone=${ encodeURIComponent(
getUserTimeZone()
- ) }&locale=en`,
+ ) }&locale=en&download_type=async`,
} );
} );
} );
- test( 'should render expected columns in CSV when the download button is clicked', () => {
+ test( 'should fetch export when number of transactions is less than 100', async () => {
const { getByRole } = render( );
getByRole( 'button', { name: 'Download' } ).click();
- const expected = [
- '"Transaction ID"',
- '"Date / Time (UTC)"',
- 'Type',
- 'Channel',
- '"Paid Currency"',
- '"Amount Paid"',
- '"Payout Currency"',
- 'Amount',
- 'Fees',
- 'Net',
- '"Order #"',
- '"Payment Method"',
- 'Customer',
- 'Email',
- 'Country',
- '"Risk level"',
- '"Payout ID"',
- '"Payout date"',
- '"Payout status"',
- ];
-
- // checking if columns in CSV are rendered correctly
- expect(
- mockDownloadCSVFile.mock.calls[ 0 ][ 1 ]
- .split( '\n' )[ 0 ]
- .split( ',' )
- ).toEqual( expected );
- } );
-
- test( 'should match the visible rows', () => {
- const { getByRole, getAllByRole } = render( );
-
- getByRole( 'button', { name: 'Download' } ).click();
-
- const csvContent = mockDownloadCSVFile.mock.calls[ 0 ][ 1 ];
- const csvRows = csvContent.split( os.EOL );
- const displayRows: HTMLElement[] = getAllByRole( 'row' );
-
- expect( csvRows.length ).toEqual( displayRows.length );
-
- const csvFirstTransaction = csvRows[ 1 ].split( ',' );
- const displayFirstTransaction: string[] = Array.from(
- displayRows[ 1 ].querySelectorAll( 'td' )
- ).map( ( td: HTMLElement ) => td.textContent || '' );
-
- // Date/Time column is a th
- // Extract is separately and prepend to csvFirstTransaction
- const displayFirstRowHead: string[] = Array.from(
- displayRows[ 1 ].querySelectorAll( 'th' )
- ).map( ( th: HTMLElement ) => th.textContent || '' );
- displayFirstTransaction.unshift( displayFirstRowHead[ 0 ] );
-
- // Note:
- //
- // 1. CSV and display indexes are off by 1 because the first field in CSV is transaction id,
- // which is missing in display.
- //
- // 2. The indexOf check in amount's expect is because the amount in CSV may not contain
- // trailing zeros as in the display amount.
- //
- expect( displayFirstTransaction[ 0 ] ).toBe(
- formatDate( csvFirstTransaction[ 1 ].replace( /['"]+/g, '' ) ) // strip extra quotes
- ); // date
- expect( displayFirstTransaction[ 1 ] ).toBe(
- csvFirstTransaction[ 2 ]
- ); // type
- expect( displayFirstTransaction[ 2 ] ).toBe(
- csvFirstTransaction[ 3 ]
- ); // channel
- expect(
- getUnformattedAmount( displayFirstTransaction[ 3 ] ).indexOf(
- csvFirstTransaction[ 7 ]
- )
- ).not.toBe( -1 ); // amount
- expect(
- -Number( getUnformattedAmount( displayFirstTransaction[ 4 ] ) )
- ).toEqual(
- Number(
- csvFirstTransaction[ 8 ].replace( /['"]+/g, '' ) // strip extra quotes
- )
- ); // fees
- expect(
- getUnformattedAmount( displayFirstTransaction[ 5 ] ).indexOf(
- csvFirstTransaction[ 9 ]
- )
- ).not.toBe( -1 ); // net
- expect( displayFirstTransaction[ 6 ] ).toBe(
- csvFirstTransaction[ 10 ]
- ); // order number
- expect( displayFirstTransaction[ 8 ] ).toBe(
- csvFirstTransaction[ 12 ].replace( /['"]+/g, '' ) // strip extra quotes
- ); // customer
+ // Check if the API request is made with the correct download type = 'sync'.
+ await waitFor( () => {
+ expect( mockApiFetch ).toHaveBeenCalledTimes( 1 );
+ expect( mockApiFetch ).toHaveBeenCalledWith( {
+ method: 'POST',
+ path: `/wc/v3/payments/transactions/download?user_email=mock%40example.com&user_timezone=${ encodeURIComponent(
+ getUserTimeZone()
+ ) }&locale=en&download_type=sync`,
+ } );
+ } );
} );
} );
} );
diff --git a/includes/admin/class-wc-rest-payments-transactions-controller.php b/includes/admin/class-wc-rest-payments-transactions-controller.php
index 9e7c10b76a4..5eb7f7fe635 100644
--- a/includes/admin/class-wc-rest-payments-transactions-controller.php
+++ b/includes/admin/class-wc-rest-payments-transactions-controller.php
@@ -162,12 +162,13 @@ public function get_fraud_outcome_transactions_export( $request ) {
* @param WP_REST_Request $request Full data about the request.
*/
public function get_transactions_export( $request ) {
- $user_email = $request->get_param( 'user_email' );
- $deposit_id = $request->get_param( 'deposit_id' );
- $locale = $request->get_param( 'locale' );
- $filters = $this->get_transactions_filters( $request );
+ $user_email = $request->get_param( 'user_email' );
+ $deposit_id = $request->get_param( 'deposit_id' );
+ $locale = $request->get_param( 'locale' );
+ $download_type = $request->get_param( 'download_type' );
+ $filters = $this->get_transactions_filters( $request );
- return $this->forward_request( 'get_transactions_export', [ $filters, $user_email, $deposit_id, $locale ] );
+ return $this->forward_request( 'get_transactions_export', [ $filters, $user_email, $deposit_id, $locale, $download_type ] );
}
/**
diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php
index e90094d57de..e40fea5b964 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -432,12 +432,13 @@ public function get_fraud_outcome_transactions_export( $request ) {
* @param string $user_email The email to search for.
* @param string $deposit_id The deposit to filter on.
* @param string $locale Site locale.
+ * @param string $download_type The type of download to perform.
*
* @return array Export summary
*
* @throws API_Exception - Exception thrown on request failure.
*/
- public function get_transactions_export( $filters = [], $user_email = '', $deposit_id = null, $locale = null ) {
+ public function get_transactions_export( $filters = [], $user_email = '', $deposit_id = null, $locale = null, $download_type = null ) {
// Map Order # terms to the actual charge id to be used in the server.
if ( ! empty( $filters['search'] ) ) {
$filters['search'] = WC_Payments_Utils::map_search_orders_to_charge_ids( $filters['search'] );
@@ -451,6 +452,9 @@ public function get_transactions_export( $filters = [], $user_email = '', $depos
if ( ! empty( $locale ) ) {
$filters['locale'] = $locale;
}
+ if ( ! empty( $download_type ) ) {
+ $filters['download_type'] = $download_type;
+ }
return $this->request( $filters, self::TRANSACTIONS_API . '/download', self::POST );
}