Skip to content

Commit

Permalink
Download Disputes CSV from service (#10300)
Browse files Browse the repository at this point in the history
Co-authored-by: Jessy <[email protected]>
Co-authored-by: Jessy Pappachan <[email protected]>
Co-authored-by: Eric Jinks <[email protected]>
Co-authored-by: Nagesh Pai <[email protected]>
  • Loading branch information
5 people authored Feb 12, 2025
1 parent 56ab77c commit fe2d203
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 208 deletions.
5 changes: 5 additions & 0 deletions changelog/update-7188-immediate-csv-download-for-disputes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: dev
Comment: No need changelog entry as there's already a changelog about the CSV export experience improvement from https://github.com/Automattic/woocommerce-payments/pull/10211.


6 changes: 4 additions & 2 deletions client/data/disputes/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ const formatQueryFilters = ( query ) => ( {
locale: query.userLocale,
} );

export function getDisputesCSV( query ) {
export const disputesDownloadEndpoint = `${ NAMESPACE }/disputes/download`;
export function getDisputesCSVRequestURL( query ) {
const path = addQueryArgs(
`${ NAMESPACE }/disputes/download`,
disputesDownloadEndpoint,
formatQueryFilters( query )
);

return path;
}

Expand Down
169 changes: 45 additions & 124 deletions client/disputes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,14 @@
/**
* External dependencies
*/
import React, { useState } from 'react';
import React from 'react';
import { recordEvent } from 'tracks';
import { _n, __, sprintf } from '@wordpress/i18n';
import moment from 'moment';
import { Button } from '@wordpress/components';
import { TableCard, Link } from '@woocommerce/components';
import { onQueryChange, getQuery, getHistory } from '@woocommerce/navigation';
import {
downloadCSVFile,
generateCSVDataFromTable,
generateCSVFileName,
} from '@woocommerce/csv-export';
import classNames from 'classnames';
import apiFetch from '@wordpress/api-fetch';
import { useDispatch } from '@wordpress/data';
import NoticeOutlineIcon from 'gridicons/dist/notice-outline';

/**
Expand All @@ -38,15 +31,19 @@ import {
} from 'multi-currency/interface/functions';
import DisputesFilters from './filters';
import DownloadButton from 'components/download-button';
import disputeStatusMapping from 'components/dispute-status-chip/mappings';
import { CachedDispute, DisputesTableHeader } from 'wcpay/types/disputes';
import { getDisputesCSV } from 'wcpay/data/disputes/resolvers';
import {
getDisputesCSVRequestURL,
disputesDownloadEndpoint,
} from 'wcpay/data/disputes/resolvers';
import { applyThousandSeparator } from 'wcpay/utils';
import { useSettings } from 'wcpay/data';
import { isAwaitingResponse } from 'wcpay/disputes/utils';
import './style.scss';
import { formatDateTimeFromString } from 'wcpay/utils/date-time';
import { usePersistedColumnVisibility } from 'wcpay/hooks/use-persisted-table-column-visibility';
import { useReportExport } from 'wcpay/hooks/use-report-export';
import { useDispatch } from '@wordpress/data';

const getHeaders = ( sortColumn?: string ): DisputesTableHeader[] => [
{
Expand Down Expand Up @@ -199,14 +196,16 @@ export const DisputesList = (): JSX.Element => {
// pre-fetching the settings.
useSettings();

const [ isDownloading, setIsDownloading ] = useState( false );
const { createNotice } = useDispatch( 'core/notices' );
const { disputes, isLoading } = useDisputes( getQuery() );

const { disputesSummary, isLoading: isSummaryLoading } = useDisputesSummary(
getQuery()
);

const { requestReportExport, isExportInProgress } = useReportExport();

const { createNotice } = useDispatch( 'core/notices' );

const headers = getHeaders( getQuery().orderby );
const { columnsToDisplay, onColumnsChange } = usePersistedColumnVisibility<
DisputesTableHeader
Expand Down Expand Up @@ -345,7 +344,12 @@ export const DisputesList = (): JSX.Element => {

const downloadable = !! rows.length;

const endpointExport = async () => {
const onDownload = async () => {
recordEvent( 'wcpay_disputes_download', {
exported_disputes: rows.length,
total_disputes: disputesSummary.count,
} );

// We destructure page and path to get the right params.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { page, path, ...params } = getQuery();
Expand All @@ -362,6 +366,18 @@ export const DisputesList = (): JSX.Element => {
status_is_not: statusIsNot,
} = getQuery();

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

const isFiltered =
!! dateBefore ||
!! dateAfter ||
Expand All @@ -383,122 +399,26 @@ export const DisputesList = (): JSX.Element => {
totalRows < confirmThreshold ||
window.confirm( confirmMessage )
) {
try {
const {
exported_disputes: exportedDisputes,
} = await apiFetch< {
/** The total number of disputes that will be exported in the CSV. */
exported_disputes: number;
} >( {
path: getDisputesCSV( {
userEmail,
userLocale,
dateAfter,
dateBefore,
dateBetween,
match,
filter,
statusIs,
statusIsNot,
} ),
method: 'POST',
} );

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

recordEvent( 'wcpay_disputes_download', {
exported_disputes: exportedDisputes,
total_disputes: exportedDisputes,
download_type: 'endpoint',
} );
} catch {
createNotice(
'error',
createNotice(
'success',
sprintf(
__(
'There was a problem generating your export.',
'Now processing your export. The file will download automatically and will be emailed to %s.',
'woocommerce-payments'
)
);
}
}
};

const onDownload = async () => {
setIsDownloading( true );
const title = __( 'Disputes', 'woocommerce-payments' );
const downloadType = totalRows > rows.length ? 'endpoint' : 'browser';

if ( 'endpoint' === downloadType ) {
endpointExport();
} else {
const csvColumns = [
),
userEmail
),
{
...headers[ 0 ],
label: __( 'Dispute Id', 'woocommerce-payments' ),
},
...headers.slice( 1, -1 ), // Remove details (position 0) and action (last position) column headers.
];

const csvRows = rows.map( ( row ) => {
return [
...row.slice( 0, 3 ), // Amount, Currency, Status.
{
// Reason.
...row[ 3 ],
value:
disputeStatusMapping[ row[ 3 ].value ?? '' ]
.message,
},
{
// Source.
...row[ 4 ],
value: formatStringValue(
( row[ 4 ].value ?? '' ).toString()
),
},
...row.slice( 5, 10 ), // Order #, Customer, Email, Country.
{
// Disputed On.
...row[ 10 ],
value: formatDateTimeFromString(
row[ 10 ].value as string
),
},
{
// Respond by.
...row[ 11 ],
value: formatDateTimeFromString(
row[ 11 ].value as string,
{
includeTime: true,
}
),
},
];
} );

downloadCSVFile(
generateCSVFileName( title, getQuery() ),
generateCSVDataFromTable( csvColumns, csvRows )
icon: '✅',
}
);

recordEvent( 'wcpay_disputes_download', {
exported_disputes: csvRows.length,
total_disputes: disputesSummary.count,
download_type: 'browser',
} );
}

setIsDownloading( false );
};

let summary;
Expand Down Expand Up @@ -546,7 +466,8 @@ export const DisputesList = (): JSX.Element => {
downloadable && (
<DownloadButton
key="download"
isDisabled={ isLoading || isDownloading }
isDisabled={ isLoading || isExportInProgress }
isBusy={ isExportInProgress }
onClick={ onDownload }
/>
),
Expand Down
80 changes: 0 additions & 80 deletions client/disputes/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,19 @@
import { render, waitFor } from '@testing-library/react';
import { downloadCSVFile } from '@woocommerce/csv-export';
import apiFetch from '@wordpress/api-fetch';
import os from 'os';
import { useUserPreferences } from '@woocommerce/data';

/**
* Internal dependencies
*/
import DisputesList from '..';
import { useDisputes, useDisputesSummary, useSettings } from 'data/index';
import { getUnformattedAmount } from 'wcpay/utils/test-utils';
import React from 'react';
import {
CachedDispute,
DisputeReason,
DisputeStatus,
} from 'wcpay/types/disputes';
import { formatDateTimeFromString } from 'wcpay/utils/date-time';

jest.mock( '@woocommerce/csv-export', () => {
const actualModule = jest.requireActual( '@woocommerce/csv-export' );
Expand Down Expand Up @@ -339,82 +336,5 @@ describe( 'Disputes list', () => {
} );
} );
} );

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

const expected = [
'"Dispute Id"',
'Amount',
'Currency',
'Status',
'Reason',
'Source',
'"Order #"',
'Customer',
'Email',
'Country',
'"Disputed on"',
'"Respond by"',
];

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( <DisputesList /> );
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 csvFirstDispute = csvRows[ 1 ].split( ',' );
const displayFirstDispute = Array.from(
displayRows[ 1 ].querySelectorAll( 'td' )
).map( ( td ) => td.textContent );

// Note:
//
// 1. CSV and display indexes are off by 2 because:
// - the first field in CSV is dispute id, which is missing in display.
// - the third field in CSV is currency, which is missing in display (it's displayed in "amount" column).
//
// 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(
getUnformattedAmount( displayFirstDispute[ 0 ] ).indexOf(
csvFirstDispute[ 1 ]
)
).not.toBe( -1 ); // amount

expect( csvFirstDispute[ 2 ] ).toBe( 'usd' );

expect( csvFirstDispute[ 3 ] ).toBe(
`"${ displayFirstDispute[ 1 ] }"`
); //status

expect( csvFirstDispute[ 4 ] ).toBe(
`"${ displayFirstDispute[ 2 ] }"`
); // reason

expect( csvFirstDispute[ 6 ] ).toBe( displayFirstDispute[ 4 ] ); // order

expect( csvFirstDispute[ 7 ] ).toBe(
`"${ displayFirstDispute[ 5 ] }"`
); // customer

expect( csvFirstDispute[ 11 ].replace( /^"|"$/g, '' ) ).toBe(
formatDateTimeFromString( mockDisputes[ 0 ].due_by, {
includeTime: true,
} )
); // date respond by
} );
} );
} );
Loading

0 comments on commit fe2d203

Please sign in to comment.