From fe2d2034db74acaa4d7fc0cce58c008410392d46 Mon Sep 17 00:00:00 2001 From: Shendy <73803630+shendy-a8c@users.noreply.github.com> Date: Thu, 13 Feb 2025 02:20:17 +0700 Subject: [PATCH] Download Disputes CSV from service (#10300) Co-authored-by: Jessy Co-authored-by: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com> Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Co-authored-by: Nagesh Pai --- ...e-7188-immediate-csv-download-for-disputes | 5 + client/data/disputes/resolvers.js | 6 +- client/disputes/index.tsx | 169 +++++------------- client/disputes/test/index.tsx | 80 --------- ...s-wc-rest-payments-disputes-controller.php | 21 ++- .../class-wc-payments-api-client.php | 14 +- 6 files changed, 87 insertions(+), 208 deletions(-) create mode 100644 changelog/update-7188-immediate-csv-download-for-disputes diff --git a/changelog/update-7188-immediate-csv-download-for-disputes b/changelog/update-7188-immediate-csv-download-for-disputes new file mode 100644 index 00000000000..82a6e593f63 --- /dev/null +++ b/changelog/update-7188-immediate-csv-download-for-disputes @@ -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. + + diff --git a/client/data/disputes/resolvers.js b/client/data/disputes/resolvers.js index ccc72c001f3..7e9dd706568 100644 --- a/client/data/disputes/resolvers.js +++ b/client/data/disputes/resolvers.js @@ -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; } diff --git a/client/disputes/index.tsx b/client/disputes/index.tsx index 73837d8e865..6e372cf6f9a 100644 --- a/client/disputes/index.tsx +++ b/client/disputes/index.tsx @@ -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'; /** @@ -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[] => [ { @@ -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 @@ -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(); @@ -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 || @@ -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; @@ -546,7 +466,8 @@ export const DisputesList = (): JSX.Element => { downloadable && ( ), diff --git a/client/disputes/test/index.tsx b/client/disputes/test/index.tsx index 4940ff3742c..7ffc9c6d4f6 100644 --- a/client/disputes/test/index.tsx +++ b/client/disputes/test/index.tsx @@ -5,7 +5,6 @@ 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'; /** @@ -13,14 +12,12 @@ import { useUserPreferences } from '@woocommerce/data'; */ 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' ); @@ -339,82 +336,5 @@ describe( 'Disputes list', () => { } ); } ); } ); - - test( 'should render expected columns in CSV when the download button is clicked ', () => { - const { getByRole } = render( ); - 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( ); - 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 - } ); } ); } ); diff --git a/includes/admin/class-wc-rest-payments-disputes-controller.php b/includes/admin/class-wc-rest-payments-disputes-controller.php index 64e1e478a21..a861569413a 100644 --- a/includes/admin/class-wc-rest-payments-disputes-controller.php +++ b/includes/admin/class-wc-rest-payments-disputes-controller.php @@ -34,6 +34,15 @@ public function register_routes() { 'permission_callback' => [ $this, 'check_permission' ], ] ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/download/(?P.*)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_export_url' ], + 'permission_callback' => [ $this, 'check_permission' ], + ] + ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/summary', @@ -93,7 +102,17 @@ public function get_disputes( WP_REST_Request $request ) { } /** - * Retrieve transactions summary to respond with via API. + * Get the disputes export URL for a given export ID, if available. + * + * @param WP_REST_Request $request Full data about the request. + */ + public function get_export_url( $request ) { + $export_id = $request->get_param( 'export_id' ); + return $this->forward_request( 'get_disputes_export_url', [ $export_id ] ); + } + + /** + * Retrieve disputes summary to respond with via API. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error 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 63fde1064a6..a3c8e1d3b9d 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -456,7 +456,7 @@ public function get_transactions_export( $filters = [], $user_email = '', $depos } /** - * Get the export URL for a given export ID, if available. + * Get the transactions export URL for a given export ID, if available. * * @param string $export_id The export ID. * @@ -467,6 +467,18 @@ public function get_transactions_export_url( string $export_id ): array { return $this->request( [], self::TRANSACTIONS_API . "/download/{$export_id}", self::GET ); } + /** + * Get the disputes export URL for a given export ID, if available. + * + * @param string $export_id The export ID. + * + * @return array The export URL response. + * @throws API_Exception - Exception thrown on request failure. + */ + public function get_disputes_export_url( string $export_id ): array { + return $this->request( [], self::DISPUTES_API . "/download/{$export_id}", self::GET ); + } + /** * Fetch account recommended payment methods data for a given country. *