Skip to content

Commit

Permalink
Improve CSV download experience for Payouts (#10346)
Browse files Browse the repository at this point in the history
Co-authored-by: Jessy <[email protected]>
Co-authored-by: Shendy <[email protected]>
Co-authored-by: Nagesh Pai <[email protected]>
  • Loading branch information
4 people authored Feb 17, 2025
1 parent 93e4b14 commit fbdc898
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 200 deletions.
5 changes: 5 additions & 0 deletions changelog/update-deposits-server-export
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: dev
Comment: No need another changelog entry because there is already one that mentions the change in CSV download experience.


5 changes: 3 additions & 2 deletions client/data/deposits/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ const formatQueryFilters = ( query ) => ( {
locale: query.userLocale,
} );

export function getDepositsCSV( query ) {
export const payoutsDownloadEndpoint = `${ NAMESPACE }/deposits/download`;
export function getPayoutsCSVRequestURL( query ) {
const path = addQueryArgs(
`${ NAMESPACE }/deposits/download`,
payoutsDownloadEndpoint,
formatQueryFilters( query )
);

Expand Down
138 changes: 41 additions & 97 deletions client/deposits/list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@
/**
* External dependencies
*/
import React, { useState } from 'react';
import React from 'react';
import { recordEvent } from 'tracks';
import { __, _n, sprintf } from '@wordpress/i18n';
import { TableCard, Link } from '@woocommerce/components';
import { onQueryChange, getQuery } from '@woocommerce/navigation';
import {
downloadCSVFile,
generateCSVDataFromTable,
generateCSVFileName,
} from '@woocommerce/csv-export';
import apiFetch from '@wordpress/api-fetch';
import { useDispatch } from '@wordpress/data';
import { parseInt } from 'lodash';

Expand All @@ -32,9 +26,13 @@ import ClickableCell from 'components/clickable-cell';
import Page from '../../components/page';
import DepositsFilters from '../filters';
import DownloadButton from 'components/download-button';
import { getDepositsCSV } from 'wcpay/data/deposits/resolvers';
import {
getPayoutsCSVRequestURL,
payoutsDownloadEndpoint,
} from 'wcpay/data/deposits/resolvers';
import { applyThousandSeparator } from '../../utils/index.js';
import DepositStatusChip from 'components/deposit-status-chip';
import { useReportExport } from 'wcpay/hooks/use-report-export';

import './style.scss';
import { formatDateTimeFromString } from 'wcpay/utils/date-time';
Expand Down Expand Up @@ -96,13 +94,14 @@ const getColumns = ( sortByDate?: boolean ): DepositsTableHeader[] => [
];

export const DepositsList = (): JSX.Element => {
const [ isDownloading, setIsDownloading ] = useState( false );
const { createNotice } = useDispatch( 'core/notices' );
const { deposits, isLoading } = useDeposits( getQuery() );
const { depositsSummary, isLoading: isSummaryLoading } = useDepositsSummary(
getQuery()
);

const { requestReportExport, isExportInProgress } = useReportExport();
const { createNotice } = useDispatch( 'core/notices' );

const sortByDate = ! getQuery().orderby || 'date' === getQuery().orderby;
const columns = getColumns( sortByDate );
const { columnsToDisplay, onColumnsChange } = usePersistedColumnVisibility<
Expand Down Expand Up @@ -208,11 +207,14 @@ export const DepositsList = (): JSX.Element => {
depositsSummary.store_currencies ||
( isCurrencyFiltered ? [ getQuery().store_currency_is ] : [] );

const title = __( 'Payouts', 'woocommerce-payments' );

const downloadable = !! rows.length;

const endpointExport = async () => {
const onDownload = async () => {
recordEvent( 'wcpay_deposits_download', {
exported_deposits: rows.length,
total_deposits: depositsSummary.count,
} );

const userEmail = wcpaySettings.currentUserEmail;
const userLocale = wcpaySettings.userLocale.code;

Expand All @@ -226,6 +228,18 @@ export const DepositsList = (): JSX.Element => {
store_currency_is: storeCurrencyIs,
} = getQuery();

const exportRequestURL = getPayoutsCSVRequestURL( {
userEmail,
userLocale,
dateBefore,
dateAfter,
dateBetween,
match,
statusIs,
statusIsNot,
storeCurrencyIs,
} );

const isFiltered =
!! dateBefore ||
!! dateAfter ||
Expand All @@ -248,94 +262,23 @@ export const DepositsList = (): JSX.Element => {
totalRows < confirmThreshold ||
window.confirm( confirmMessage )
) {
try {
const {
exported_deposits: exportedDeposits,
} = await apiFetch< {
/** The total number of payouts that will be exported in the CSV */
exported_deposits: number;
} >( {
path: getDepositsCSV( {
userEmail,
userLocale,
dateAfter,
dateBefore,
dateBetween,
match,
statusIs,
statusIsNot,
storeCurrencyIs,
} ),
method: 'POST',
} );

createNotice(
'success',
sprintf(
__(
'Your export will be emailed to %s',
'woocommerce-payments'
),
userEmail
)
);
requestReportExport( {
exportRequestURL,
exportFileAvailabilityEndpoint: payoutsDownloadEndpoint,
userEmail,
} );

recordEvent( 'wcpay_deposits_download', {
exported_deposits: exportedDeposits,
total_deposits: exportedDeposits,
download_type: 'endpoint',
} );
} catch {
createNotice(
'error',
createNotice(
'success',
sprintf(
__(
'There was a problem generating your export.',
'We’re processing your export. 🎉 The file will download automatically and be emailed to %s.',
'woocommerce-payments'
)
);
}
}
};

const onDownload = async () => {
setIsDownloading( true );
const downloadType = totalRows > rows.length ? 'endpoint' : 'browser';

if ( 'endpoint' === downloadType ) {
endpointExport();
} else {
const params = getQuery();

const csvColumns = [
{
...columns[ 0 ],
label: __( 'Payout Id', 'woocommerce-payments' ),
},
...columns.slice( 1 ),
];

const csvRows = rows.map( ( row ) => [
row[ 0 ],
{
...row[ 1 ],
value: formatDateTimeFromString( row[ 1 ].value as string ),
},
...row.slice( 2 ),
] );

downloadCSVFile(
generateCSVFileName( title, params ),
generateCSVDataFromTable( csvColumns, csvRows )
),
userEmail
)
);

recordEvent( 'wcpay_deposits_download', {
exported_deposits: rows.length,
total_deposits: depositsSummary.count,
download_type: 'browser',
} );
}

setIsDownloading( false );
};

return (
Expand All @@ -357,7 +300,8 @@ export const DepositsList = (): JSX.Element => {
downloadable && (
<DownloadButton
key="download"
isDisabled={ isLoading || isDownloading }
isDisabled={ isLoading || isExportInProgress }
isBusy={ isExportInProgress }
onClick={ onDownload }
/>
),
Expand Down
78 changes: 0 additions & 78 deletions client/deposits/list/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { updateQueryString } from '@woocommerce/navigation';
import { downloadCSVFile } from '@woocommerce/csv-export';
import apiFetch from '@wordpress/api-fetch';
import os from 'os';
import { useUserPreferences } from '@woocommerce/data';

/**
* Internal dependencies
*/
import { DepositsList } from '../';
import { useDeposits, useDepositsSummary } from 'wcpay/data';
import { getUnformattedAmount } from 'wcpay/utils/test-utils';
import {
CachedDeposit,
CachedDeposits,
Expand All @@ -28,15 +25,6 @@ jest.mock( 'wcpay/data', () => ( {
useDepositsSummary: jest.fn(),
} ) );

jest.mock( '@woocommerce/csv-export', () => {
const actualModule = jest.requireActual( '@woocommerce/csv-export' );

return {
...actualModule,
downloadCSVFile: jest.fn(),
};
} );

jest.mock( '@wordpress/api-fetch', () => jest.fn() );

jest.mock( '@woocommerce/data', () => {
Expand Down Expand Up @@ -122,10 +110,6 @@ const mockUseDepositsSummary = useDepositsSummary as jest.MockedFunction<
typeof useDepositsSummary
>;

const mockDownloadCSVFile = downloadCSVFile as jest.MockedFunction<
typeof downloadCSVFile
>;

const mockUseUserPreferences = useUserPreferences as jest.MockedFunction<
typeof useUserPreferences
>;
Expand Down Expand Up @@ -289,68 +273,6 @@ describe( 'Deposits list', () => {
} );
} );

test( 'should render expected columns in CSV when the download button is clicked', () => {
const { getByRole } = render( <DepositsList /> );
getByRole( 'button', { name: 'Export' } ).click();

const expected = [
'"Payout Id"',
'Date',
'Type',
'Amount',
'Status',
'"Bank account"',
'"Bank reference ID"',
];

const csvContent = mockDownloadCSVFile.mock.calls[ 0 ][ 1 ];
const csvHeaderRow = csvContent.split( os.EOL )[ 0 ].split( ',' );
expect( csvHeaderRow ).toEqual( expected );
} );

test( 'should match the visible rows', () => {
const { getByRole, getAllByRole } = render( <DepositsList /> );
getByRole( 'button', { name: 'Export' } ).click();

const csvContent = mockDownloadCSVFile.mock.calls[ 0 ][ 1 ];
const csvRows = csvContent.split( os.EOL );
const displayRows = getAllByRole( 'row' );

expect( csvRows.length ).toEqual( displayRows.length );

const csvFirstDeposit = csvRows[ 1 ].split( ',' );
const displayFirstDeposit = Array.from(
displayRows[ 1 ].querySelectorAll( 'td' )
).map( ( td ) => td.textContent );

// Note:
//
// 1. CSV and display indexes are off by 1 because the first field in CSV is deposit 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( csvFirstDeposit[ 1 ].replace( /^"|"$/g, '' ) ).toBe(
displayFirstDeposit[ 0 ]
); // date
expect( csvFirstDeposit[ 2 ] ).toBe( displayFirstDeposit[ 1 ] ); // type
expect(
getUnformattedAmount( displayFirstDeposit[ 2 ] ).indexOf(
csvFirstDeposit[ 3 ]
)
).not.toBe( -1 ); // amount
expect( csvFirstDeposit[ 4 ] ).toBe(
`"${ displayFirstDeposit[ 3 ] }"`
); // status
expect( csvFirstDeposit[ 5 ] ).toBe(
`"${ displayFirstDeposit[ 4 ] }"`
); // bank account
expect( csvFirstDeposit[ 6 ] ).toBe(
`${ displayFirstDeposit[ 5 ] }`
); // bank reference key
} );

test( 'should fetch export after confirmation when download button is selected for unfiltered exports larger than 1000.', async () => {
window.confirm = jest.fn( () => true );
mockUseDepositsSummary.mockReturnValue( {
Expand Down
14 changes: 0 additions & 14 deletions client/disputes/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* External dependencies
*/
import { render, waitFor } from '@testing-library/react';
import { downloadCSVFile } from '@woocommerce/csv-export';
import apiFetch from '@wordpress/api-fetch';
import { useUserPreferences } from '@woocommerce/data';

Expand All @@ -19,15 +18,6 @@ import {
DisputeStatus,
} from 'wcpay/types/disputes';

jest.mock( '@woocommerce/csv-export', () => {
const actualModule = jest.requireActual( '@woocommerce/csv-export' );

return {
...actualModule,
downloadCSVFile: jest.fn(),
};
} );

jest.mock( '@wordpress/api-fetch', () => jest.fn() );

// Workaround for mocking @wordpress/data.
Expand Down Expand Up @@ -60,10 +50,6 @@ jest.mock( '@woocommerce/data', () => {
};
} );

const mockDownloadCSVFile = downloadCSVFile as jest.MockedFunction<
typeof downloadCSVFile
>;

const mockApiFetch = apiFetch as jest.MockedFunction< typeof apiFetch >;

const mockUseDisputes = useDisputes as jest.MockedFunction<
Expand Down
9 changes: 0 additions & 9 deletions client/transactions/list/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,6 @@ import { TransactionsList } from '../';
import { useTransactions, useTransactionsSummary } from 'data/index';
import type { Transaction } from 'data/transactions/hooks';

jest.mock( '@woocommerce/csv-export', () => {
const actualModule = jest.requireActual( '@woocommerce/csv-export' );

return {
...actualModule,
downloadCSVFile: jest.fn(),
};
} );

jest.mock( '@woocommerce/data', () => {
const actualModule = jest.requireActual( '@woocommerce/data' );

Expand Down
Loading

0 comments on commit fbdc898

Please sign in to comment.