From def5c52697e1812ad9e39e37db22558292352ba7 Mon Sep 17 00:00:00 2001 From: Shendy <73803630+shendy-a8c@users.noreply.github.com> Date: Fri, 23 Jun 2023 09:49:57 +0800 Subject: [PATCH] WC Home active disputes task: improve wording and reduce `due_by` threshold to align with Payments Overview task (#6548) Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Co-authored-by: bruce aldridge --- changelog/update-6505-dispute-task-wc-home | 4 + client/overview/task-list/dispute-tasks.ts | 30 +- client/overview/task-list/test/tasks.js | 21 +- .../tasks/class-wc-payments-task-disputes.php | 259 +++++++++++++++--- includes/class-database-cache.php | 7 + ...wc-payments-webhook-processing-service.php | 9 +- .../class-wc-payments-api-client.php | 17 +- .../test-class-wc-payments-task-disputes.php | 224 ++++++++++++--- 8 files changed, 476 insertions(+), 95 deletions(-) create mode 100644 changelog/update-6505-dispute-task-wc-home diff --git a/changelog/update-6505-dispute-task-wc-home b/changelog/update-6505-dispute-task-wc-home new file mode 100644 index 00000000000..4ef0b92ad8d --- /dev/null +++ b/changelog/update-6505-dispute-task-wc-home @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Improve the wording of the "Active Disputes" task list item on the WooCommerce → Home screen to better communicate the urgency of resolving these disputes. diff --git a/client/overview/task-list/dispute-tasks.ts b/client/overview/task-list/dispute-tasks.ts index 1cf1f8f8251..2b375f21cef 100644 --- a/client/overview/task-list/dispute-tasks.ts +++ b/client/overview/task-list/dispute-tasks.ts @@ -2,6 +2,7 @@ * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; +import { dateI18n } from '@wordpress/date'; import moment from 'moment'; import { getHistory } from '@woocommerce/navigation'; @@ -28,12 +29,10 @@ const isDueWithin = ( dispute: CachedDispute, days: number ) => { if ( dispute.due_by === '' ) { return false; } - const now = moment(); - const dueBy = moment( dispute.due_by ); - return ( - dueBy.diff( now, 'hours' ) > 0 && - dueBy.diff( now, 'hours' ) <= 24 * days - ); + // Get current time in UTC. + const now = moment().utc(); + const dueBy = moment.utc( dispute.due_by ); + return dueBy.diff( now, 'hours' ) > 0 && dueBy.diff( now, 'days' ) <= days; }; /** @@ -153,15 +152,22 @@ export const getDisputeResolutionTask = ( numDisputesDueWithin24h >= 1 ? sprintf( __( 'Respond today by %s', 'woocommerce-payments' ), - // Show due_by time in local time. - moment( dispute.due_by ).format( 'h:mm A' ) // E.g. "11:59 PM". + // Show due_by time in local timezone: e.g. "11:59 PM". + dateI18n( + 'g:i A', + moment.utc( dispute.due_by ).local().toISOString() + ) ) : sprintf( __( 'By %s – %s left to respond', 'woocommerce-payments' ), - moment( dispute.due_by ).format( 'MMM D, YYYY' ), // E.g. "Jan 1, 2021". + // Show due_by date in local timezone: e.g. "Jan 1, 2021". + dateI18n( + 'M j, Y', + moment.utc( dispute.due_by ).local().toISOString() + ), moment( dispute.due_by ).fromNow( true ) // E.g. "2 days". ); @@ -204,18 +210,18 @@ export const getDisputeResolutionTask = ( disputeTotalAmounts ); disputeTask.content = - // Final day / Last week to respond for N of the disputes + // Final day / Last week to respond to N of the disputes numDisputesDueWithin24h >= 1 ? sprintf( __( - 'Final day to respond for %d of the disputes', + 'Final day to respond to %d of the disputes', 'woocommerce-payments' ), numDisputesDueWithin24h ) : sprintf( __( - 'Last week to respond for %d of the disputes', + 'Last week to respond to %d of the disputes', 'woocommerce-payments' ), numDisputesDueWithin7Days diff --git a/client/overview/task-list/test/tasks.js b/client/overview/task-list/test/tasks.js index 03c19026c04..f2e327ce19c 100644 --- a/client/overview/task-list/test/tasks.js +++ b/client/overview/task-list/test/tasks.js @@ -1,5 +1,11 @@ /** @format */ +/** + * External dependencies + */ + +import moment from 'moment'; + /** * Internal dependencies */ @@ -97,7 +103,13 @@ const mockActiveDisputes = [ ]; describe( 'getTasks()', () => { + // Get current timezone + const currentTimezone = moment.tz.guess(); + beforeEach( () => { + // set local timezone to EST (not daylight savings time) + // Note Etc/GMT+5 === UTC-5 + moment.tz.setDefault( 'Etc/GMT+5' ); // mock Date.now that moment library uses to get current date for testing purposes Date.now = jest.fn( () => new Date( '2023-02-01T08:00:00.000Z' ) ); @@ -122,6 +134,7 @@ describe( 'getTasks()', () => { afterEach( () => { // roll it back Date.now = () => new Date(); + moment.tz.setDefault( currentTimezone ); } ); it( 'should include business details when flag is set', () => { const actual = getTasks( { @@ -316,7 +329,7 @@ describe( 'getTasks()', () => { it( 'should not include the dispute resolution task if dispute due_by > 7 days', () => { // Set Date.now to - 7 days to reduce urgency of disputes. - Date.now = jest.fn( () => new Date( '2023-01-25T08:00:00.000Z' ) ); + Date.now = jest.fn( () => new Date( '2023-01-24T08:00:00.000Z' ) ); const actual = getTasks( { accountStatus: { status: 'restricted_soon', @@ -354,7 +367,7 @@ describe( 'getTasks()', () => { completed: false, level: 1, title: 'Respond to a dispute for $10.00 – Last day', - content: 'Respond today by 11:59 PM', + content: 'Respond today by 6:59 PM', // shown in local timezone. actionLabel: 'Respond now', } ), ] ) @@ -413,7 +426,7 @@ describe( 'getTasks()', () => { level: 1, title: 'Respond to 3 active disputes for a total of $20.00, €10.00', - content: 'Final day to respond for 1 of the disputes', + content: 'Final day to respond to 1 of the disputes', actionLabel: 'See disputes', } ), ] ) @@ -444,7 +457,7 @@ describe( 'getTasks()', () => { level: 1, title: 'Respond to 3 active disputes for a total of $20.00, €10.00', - content: 'Last week to respond for 1 of the disputes', + content: 'Last week to respond to 2 of the disputes', actionLabel: 'See disputes', } ), ] ) diff --git a/includes/admin/tasks/class-wc-payments-task-disputes.php b/includes/admin/tasks/class-wc-payments-task-disputes.php index f15dac4361a..e6fffa2d5f7 100644 --- a/includes/admin/tasks/class-wc-payments-task-disputes.php +++ b/includes/admin/tasks/class-wc-payments-task-disputes.php @@ -9,6 +9,8 @@ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; use WCPay\Database_Cache; +use WC_Payments_Utils; +use WC_Payments_API_Client; defined( 'ABSPATH' ) || exit; @@ -18,6 +20,53 @@ * Note: this task is separate to the Payments → Overview disputes task, which is defined in client/overview/task-list/tasks.js. */ class WC_Payments_Task_Disputes extends Task { + /** + * Client for making requests to the WooCommerce Payments API + * + * @var WC_Payments_API_Client + */ + private $api_client; + + /** + * Database_Cache instance. + * + * @var Database_Cache + */ + private $database_cache; + + + /** + * Disputes due within 7 days. + * + * @var array|null + */ + private $disputes_due_within_7d; + + /** + * Disputes due within 1 day. + * + * @var array|null + */ + private $disputes_due_within_1d; + + /** + * WC_Payments_Task_Disputes constructor. + */ + public function __construct() { + + $this->api_client = \WC_Payments::get_payments_api_client(); + $this->database_cache = \WC_Payments::get_database_cache(); + parent::__construct(); + $this->init(); + } + + /** + * Initialize the task. + */ + private function init() { + $this->disputes_due_within_7d = $this->get_disputes_needing_response_within_days( 7 ); + $this->disputes_due_within_1d = $this->get_disputes_needing_response_within_days( 1 ); + } /** * Gets the task ID. @@ -34,10 +83,47 @@ public function get_id() { * @return string */ public function get_title() { - $dispute_count = $this->get_disputes_awaiting_response_count(); + if ( count( $this->disputes_due_within_7d ) === 1 ) { + $dispute = $this->disputes_due_within_7d[0]; + $amount = WC_Payments_Utils::interpret_stripe_amount( $dispute['amount'], $dispute['currency'] ); + $amount_formatted = WC_Payments_Utils::format_currency( $amount, $dispute['currency'] ); + if ( count( $this->disputes_due_within_1d ) > 0 ) { + return sprintf( + /* translators: %s is a currency formatted amount */ + __( 'Respond to a dispute for %s – Last day', 'woocommerce-payments' ), + $amount_formatted + ); + } + return sprintf( + /* translators: %s is a currency formatted amount */ + __( 'Respond to a dispute for %s', 'woocommerce-payments' ), + $amount_formatted + ); + } + + $active_disputes = $this->get_disputes_needing_response(); + $currencies_map = []; + foreach ( $active_disputes as $dispute ) { + if ( ! isset( $currencies_map[ $dispute['currency'] ] ) ) { + $currencies_map[ $dispute['currency'] ] = 0; + } + $currencies_map[ $dispute['currency'] ] += $dispute['amount']; + } + + $currencies = array_keys( $currencies_map ); + $formatted_amounts = []; + foreach ( $currencies as $currency ) { + $amount = WC_Payments_Utils::interpret_stripe_amount( $currencies_map[ $currency ], $currency ); + $formatted_amounts[] = WC_Payments_Utils::format_currency( $amount, $currency ); + } + $dispute_total_amounts = implode( ', ', $formatted_amounts ); - // Translators: The placeholder is the number of disputes. - return sprintf( _n( '%d disputed payment needs your response', '%d disputed payments need your response', $dispute_count, 'woocommerce-payments' ), $dispute_count ); + return sprintf( + /* translators: %d is a number. %s is a currency formatted amounts (potentially multiple), eg: €10.00, $20.00 */ + __( 'Respond to %1$d active disputes for a total of %2$s', 'woocommerce-payments' ), + count( $active_disputes ), + $dispute_total_amounts + ); } /** @@ -57,30 +143,58 @@ public function get_parent_id() { } /** - * Gets the task content. - * - * @return string - */ - public function get_content() { - return ''; - } - - /** - * Get the additional info. + * Gets the task subtitle. * * @return string */ public function get_additional_info() { - return __( 'View and respond', 'woocommerce-payments' ); - } + if ( count( $this->disputes_due_within_7d ) === 1 ) { + $local_timezone = new \DateTimeZone( wp_timezone_string() ); + $dispute = $this->disputes_due_within_7d[0]; + $due_by_local_time = ( new \DateTime( $dispute['due_by'] ) )->setTimezone( $local_timezone ); + // Sum of Unix timestamp and timezone offset in seconds. + $due_by_ts = $due_by_local_time->getTimestamp() + $due_by_local_time->getOffset(); + + if ( count( $this->disputes_due_within_1d ) > 0 ) { + return sprintf( + /* translators: %s is time, eg: 11:59 PM */ + __( 'Respond today by %s', 'woocommerce-payments' ), + date_i18n( wc_time_format(), $due_by_ts ) + ); + } + + $now = new \DateTime( 'now', $local_timezone ); + $diff = $now->diff( $due_by_local_time ); + + return sprintf( + /* translators: %1$s is a date, eg: Jan 1, 2021. %2$s is the number of days left, eg: 2 days. */ + __( 'By %1$s – %2$s left to respond', 'woocommerce-payments' ), + date_i18n( wc_date_format(), $due_by_ts ), + /* translators: %s is the number of days left, e.g. 1 day. */ + sprintf( _n( '%d day', '%d days', $diff->days, 'woocommerce-payments' ), $diff->days ) + ); + } + + if ( count( $this->disputes_due_within_1d ) > 0 ) { + return sprintf( + /* translators: %d is the number of disputes. */ + __( + 'Final day to respond to %d of the disputes', + 'woocommerce-payments' + ), + count( $this->disputes_due_within_1d ) + ); + } + + return sprintf( + /* translators: %d is the number of disputes. */ + __( + 'Last week to respond to %d of the disputes', + 'woocommerce-payments' + ), + count( $this->disputes_due_within_7d ) + ); - /** - * Gets the task's action label. - * - * @return string - */ - public function get_action_label() { - return __( 'Disputes', 'woocommerce-payments' ); } /** @@ -89,6 +203,21 @@ public function get_action_label() { * @return string */ public function get_action_url() { + $disputes = $this->disputes_due_within_7d; + if ( count( $disputes ) === 1 ) { + $dispute = $disputes[0]; + return admin_url( + add_query_arg( + [ + 'page' => 'wc-admin', + 'path' => '%2Fpayments%2Fdisputes%2Fdetails', + 'id' => $dispute['dispute_id'], + ], + 'admin.php' + ) + ); + } + return admin_url( add_query_arg( [ @@ -110,6 +239,15 @@ public function get_time() { return ''; } + /** + * Gets the task content. + * + * @return string + */ + public function get_content() { + return ''; + } + /** * Get whether the task is completed. * @@ -125,25 +263,80 @@ public function is_complete() { * @return bool */ public function can_view() { - return $this->get_disputes_awaiting_response_count() > 0; + return count( $this->disputes_due_within_7d ) > 0; } /** - * Gets the number of disputes which need a response. + * Get disputes needing response within the given number of days. * - * Because this task is initialized before WC Payments, we can only fetch from the cache (via "get()"). - * The dispute status cache cannot be populated via this task. If this value hasn't been cached yet, this task won't show until it is. + * @param int $num_days Number of days in the future to check for disputes needing response. * - * @return int The number of disputes which need a response. + * @return array Disputes needing response within the given number of days. */ - private function get_disputes_awaiting_response_count() { - $disputes_status_counts = \WC_Payments::get_database_cache()->get( Database_Cache::DISPUTE_STATUS_COUNTS_KEY ); + private function get_disputes_needing_response_within_days( $num_days ) { + $to_return = []; + + $active_disputes = $this->get_disputes_needing_response(); + if ( ! is_array( $active_disputes ) ) { + return $to_return; + } + + foreach ( $active_disputes as $dispute ) { + if ( ! $dispute['due_by'] ) { + continue; + } + + // Compare UTC times. + $now_utc = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) ); + $due_by_utc = new \DateTime( $dispute['due_by'], new \DateTimeZone( 'UTC' ) ); - if ( empty( $disputes_status_counts ) ) { - return 0; + if ( $now_utc > $due_by_utc ) { + continue; + } + + $diff = $now_utc->diff( $due_by_utc ); + // If the dispute is due within the given number of days, add it to the list. + if ( $diff->days <= $num_days ) { + $to_return[] = $dispute; + } } - $needs_response_statuses = [ 'needs_response', 'warning_needs_response' ]; - return (int) array_sum( array_intersect_key( $disputes_status_counts, array_flip( $needs_response_statuses ) ) ); + return $to_return; + } + + /** + * Gets disputes awaiting a response. ie have a 'needs_response' or 'warning_needs_response' status. + * + * @return array|null Array of disputes awaiting a response. Null on failure. + */ + private function get_disputes_needing_response() { + return $this->database_cache->get_or_add( + Database_Cache::ACTIVE_DISPUTES_KEY, + function() { + $response = $this->api_client->get_disputes( + [ + 'pagesize' => 50, + 'search' => [ 'warning_needs_response', 'needs_response' ], + ] + ); + + $active_disputes = $response['data'] ?? []; + + // sort by due_by date ascending. + usort( + $active_disputes, + function( $a, $b ) { + $a_due_by = new \DateTime( $a['due_by'] ); + $b_due_by = new \DateTime( $b['due_by'] ); + + return $a_due_by <=> $b_due_by; + } + ); + + return $active_disputes; + }, + // We'll consider all array values to be valid as the cache is only invalidated when it is deleted or it expires. + 'is_array' + ); } } diff --git a/includes/class-database-cache.php b/includes/class-database-cache.php index ff87e1050bb..ab50287a1cb 100644 --- a/includes/class-database-cache.php +++ b/includes/class-database-cache.php @@ -31,6 +31,13 @@ class Database_Cache { */ const DISPUTE_STATUS_COUNTS_KEY = 'wcpay_dispute_status_counts_cache'; + /** + * Active disputes cache key. + * + * @var string + */ + const ACTIVE_DISPUTES_KEY = 'wcpay_active_dispute_cache'; + /** * Cache key for authorization summary data like count, total amount, etc. * diff --git a/includes/class-wc-payments-webhook-processing-service.php b/includes/class-wc-payments-webhook-processing-service.php index 88df3222d4c..7997bae28e9 100644 --- a/includes/class-wc-payments-webhook-processing-service.php +++ b/includes/class-wc-payments-webhook-processing-service.php @@ -551,8 +551,9 @@ private function process_webhook_dispute_created( $event_body ) { $this->order_service->mark_payment_dispute_created( $order, $dispute_id, $amount, $reason, $due_by ); - // Clear the dispute statuses cache to trigger a fetch of new data. + // Clear dispute caches to trigger a fetch of new data. $this->database_cache->delete( DATABASE_CACHE::DISPUTE_STATUS_COUNTS_KEY ); + $this->database_cache->delete( DATABASE_CACHE::ACTIVE_DISPUTES_KEY ); } /** @@ -583,8 +584,9 @@ private function process_webhook_dispute_closed( $event_body ) { $this->order_service->mark_payment_dispute_closed( $order, $dispute_id, $status ); - // Clear the dispute statuses cache to trigger a fetch of new data. + // Clear dispute caches to trigger a fetch of new data. $this->database_cache->delete( DATABASE_CACHE::DISPUTE_STATUS_COUNTS_KEY ); + $this->database_cache->delete( DATABASE_CACHE::ACTIVE_DISPUTES_KEY ); } /** @@ -639,8 +641,9 @@ private function process_webhook_dispute_updated( $event_body ) { $order->add_order_note( $note ); - // Clear the dispute statuses cache to trigger a fetch of new data. + // Clear dispute caches to trigger a fetch of new data. $this->database_cache->delete( DATABASE_CACHE::DISPUTE_STATUS_COUNTS_KEY ); + $this->database_cache->delete( DATABASE_CACHE::ACTIVE_DISPUTES_KEY ); } /** 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 a5369871719..5e6e03babee 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -527,6 +527,17 @@ public function get_dispute_status_counts() { return $this->request( [], self::DISPUTES_API . '/status_counts', self::GET ); } + /** + * Fetch disputes by provided query. + * + * @param array $filters Query to be used to get disputes. + * @return array Disputes. + * @throws API_Exception - Exception thrown on request failure. + */ + public function get_disputes( array $filters = [] ) { + return $this->request( $filters, self::DISPUTES_API, self::GET ); + } + /** * Fetch a single dispute with provided id. * @@ -562,8 +573,9 @@ public function update_dispute( $dispute_id, $evidence, $submit, $metadata ) { ]; $dispute = $this->request( $request, self::DISPUTES_API . '/' . $dispute_id, self::POST ); - // Invalidate the dispute status cache. + // Invalidate the dispute caches. \WC_Payments::get_database_cache()->delete( Database_Cache::DISPUTE_STATUS_COUNTS_KEY ); + \WC_Payments::get_database_cache()->delete( Database_Cache::ACTIVE_DISPUTES_KEY ); if ( is_wp_error( $dispute ) ) { return $dispute; @@ -581,8 +593,9 @@ public function update_dispute( $dispute_id, $evidence, $submit, $metadata ) { */ public function close_dispute( $dispute_id ) { $dispute = $this->request( [], self::DISPUTES_API . '/' . $dispute_id . '/close', self::POST ); - // Invalidate the dispute status cache. + // Invalidate the dispute caches. \WC_Payments::get_database_cache()->delete( Database_Cache::DISPUTE_STATUS_COUNTS_KEY ); + \WC_Payments::get_database_cache()->delete( Database_Cache::ACTIVE_DISPUTES_KEY ); if ( is_wp_error( $dispute ) ) { return $dispute; diff --git a/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php b/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php index 2ae215c3a5e..6957848743e 100644 --- a/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php +++ b/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php @@ -30,67 +30,209 @@ public function tear_down() { parent::tear_down(); } - public function test_disputes_task_returns_the_correct_content() { - $disputes_task = new WC_Payments_Task_Disputes(); + public function test_disputes_task_with_single_dispute_outside_7days() { + $disputes_task = new WC_Payments_Task_Disputes(); + $mock_active_disputes = [ + [ + 'wcpay_disputes_cache_id' => 21, + 'stripe_account_id' => 'acct_abc', + 'dispute_id' => 'dp_2', + 'charge_id' => 'ch_2', + 'amount' => 2000, + 'currency' => 'eur', + 'reason' => 'product_not_received', + 'source' => 'visa', + 'order_number' => 14, + 'customer_name' => 'customer', + 'customer_email' => 'email@email.com', + 'customer_country' => 'US', + 'status' => 'needs_response', + 'created' => gmdate( 'Y-m-d H:i:s', strtotime( '-14 days' ) ), + 'due_by' => gmdate( 'Y-m-d H:i:s', strtotime( '+9 days' ) ), + ], + ]; + $this->mock_cache->method( 'get_or_add' )->willReturn( + $mock_active_disputes + ); - $this->assertEquals( '', $disputes_task->get_content() ); - $this->assertEquals( '', $disputes_task->get_time() ); - $this->assertEquals( 'woocommerce_payments_disputes_task', $disputes_task->get_id() ); - $this->assertEquals( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=%2Fpayments%2Fdisputes&filter=awaiting_response', $disputes_task->get_action_url() ); - $this->assertEquals( 'Disputes', $disputes_task->get_action_label() ); - $this->assertEquals( 'View and respond', $disputes_task->get_additional_info() ); - $this->assertEquals( false, $disputes_task->is_complete() ); + $this->assertEquals( false, $disputes_task->can_view() ); } - public function test_disputes_task_returns_the_correct_content_with_empty_needs_response() { + public function test_disputes_task_with_single_dispute_within_7days() { + $mock_active_disputes = [ + [ + 'wcpay_disputes_cache_id' => 21, + 'stripe_account_id' => 'acct_abc', + 'dispute_id' => 'dp_2', + 'charge_id' => 'ch_2', + 'amount' => 2000, + 'currency' => 'eur', + 'reason' => 'product_not_received', + 'source' => 'visa', + 'order_number' => 14, + 'customer_name' => 'customer', + 'customer_email' => 'email@email.com', + 'customer_country' => 'US', + 'status' => 'needs_response', + 'created' => gmdate( 'Y-m-d H:i:s', strtotime( '-14 days' ) ), + 'due_by' => gmdate( 'Y-m-d H:i:s', strtotime( '+6 days' ) ), + ], + ]; + $this->mock_cache->method( 'get_or_add' )->willReturn( + $mock_active_disputes + ); $disputes_task = new WC_Payments_Task_Disputes(); - $this->mock_cache - ->expects( $this->exactly( 2 ) ) - ->method( 'get' ) - ->with( 'wcpay_dispute_status_counts_cache' ) - ->willReturn( [ 'needs_response' => 0 ] ); + $this->assertEquals( 'Respond to a dispute for 20,00 €', $disputes_task->get_title() ); + // "By days left to respond" + $this->assertMatchesRegularExpression( '/By \w+ \d{1,2}, \d{4} – \d+ days left to respond/', $disputes_task->get_additional_info() ); + $this->assertEquals( true, $disputes_task->can_view() ); - $this->assertEquals( '0 disputed payments need your response', $disputes_task->get_title() ); - $this->assertEquals( false, $disputes_task->can_view() ); } - public function test_disputes_task_returns_the_correct_content_with_empty_cache() { + public function test_disputes_task_with_single_dispute_within_24h() { + + $mock_active_disputes = [ + [ + 'wcpay_disputes_cache_id' => 21, + 'stripe_account_id' => 'acct_abc', + 'dispute_id' => 'dp_2', + 'charge_id' => 'ch_2', + 'amount' => 2000, + 'currency' => 'eur', + 'reason' => 'product_not_received', + 'source' => 'visa', + 'order_number' => 14, + 'customer_name' => 'customer', + 'customer_email' => 'email@email.com', + 'customer_country' => 'US', + 'status' => 'needs_response', + 'created' => gmdate( 'Y-m-d H:i:s', strtotime( '-14 days' ) ), + 'due_by' => gmdate( 'Y-m-d H:i:s', strtotime( '+23 hours' ) ), + ], + ]; + $this->mock_cache->method( 'get_or_add' )->willReturn( + $mock_active_disputes + ); $disputes_task = new WC_Payments_Task_Disputes(); - $this->mock_cache - ->expects( $this->exactly( 2 ) ) - ->method( 'get' ) - ->with( 'wcpay_dispute_status_counts_cache' ) - ->willReturn( [] ); + $this->assertEquals( 'Respond to a dispute for 20,00 € – Last day', $disputes_task->get_title() ); + // "Respond today by