Files
2026-04-28 15:13:50 +02:00

378 lines
9.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Class WC_Payments_Task_Disputes
*
* @package WooCommerce\Payments\Tasks
*/
namespace WooCommerce\Payments\Tasks;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use WCPay\Database_Cache;
use WC_Payments_Utils;
use WC_Payments_API_Client;
defined( 'ABSPATH' ) || exit;
/**
* WC Onboarding Task displayed if disputes awaiting response.
*
* 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;
/**
* A memory cache of all disputes needing response.
*
* @var array|null
*/
private $disputes_needing_response = null;
/**
* 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();
}
/**
* Initialize the task.
*/
private function fetch_relevant_disputes() {
$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.
*
* @return string
*/
public function get_id() {
return 'woocommerce_payments_disputes_task';
}
/**
* Gets the task title.
*
* @return string
*/
public function get_title() {
if ( null === $this->disputes_needing_response ) {
$this->fetch_relevant_disputes();
}
if ( count( (array) $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( (array) $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();
if ( ! is_array( $active_disputes ) || count( $active_disputes ) === 0 ) {
return '';
}
$dispute_currencies = array_unique( array_column( $active_disputes, 'currency' ) );
// If multiple currencies, use simple task title without total amounts.
if ( count( $dispute_currencies ) > 1 ) {
return sprintf(
// translators: %d is a number greater than 1.
__( 'Respond to %d active disputes', 'woocommerce-payments' ),
count( $active_disputes )
);
}
// If single currency, calculate total amount and include in task title.
$dispute_total = array_reduce(
$active_disputes,
function ( $total, $dispute ) {
return $total + ( $dispute['amount'] ?? 0 );
},
0
);
$dispute_total_formatted = WC_Payments_Utils::format_currency(
WC_Payments_Utils::interpret_stripe_amount( $dispute_total, $dispute_currencies[0] ),
$dispute_currencies[0]
);
return sprintf(
/* translators: %d is a number greater than 1. %s is a formatted amount, eg: $10.00 */
__( 'Respond to %1$d active disputes for a total of %2$s', 'woocommerce-payments' ),
count( $active_disputes ),
$dispute_total_formatted
);
}
/**
* Get the parent list ID.
*
* This function prior to WC 6.4.0 was abstract and so is needed for backwards compatibility.
*
* @return string
*/
public function get_parent_id() {
// WC 6.4.0 compatibility.
if ( is_callable( [ parent::class, 'get_parent_id' ] ) ) {
return parent::get_parent_id();
}
return 'extended';
}
/**
* Gets the task subtitle.
*
* @return string
*/
public function get_additional_info() {
if ( count( (array) $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( (array) $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( (array) $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( (array) $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( (array) $this->disputes_due_within_7d )
);
}
/**
* Gets the task's action URL.
*
* @return string
*/
public function get_action_url() {
$disputes = $this->disputes_due_within_7d;
if ( count( (array) $disputes ) === 1 ) {
$dispute = $disputes[0];
return admin_url(
add_query_arg(
[
'page' => 'wc-admin',
'path' => '%2Fpayments%2Ftransactions%2Fdetails',
'id' => $dispute['charge_id'],
],
'admin.php'
)
);
}
return admin_url(
add_query_arg(
[
'page' => 'wc-admin',
'path' => '%2Fpayments%2Fdisputes',
'filter' => 'awaiting_response',
],
'admin.php'
)
);
}
/**
* Get the estimated time to complete the task.
*
* @return string
*/
public function get_time() {
return '';
}
/**
* Gets the task content.
*
* @return string
*/
public function get_content() {
return '';
}
/**
* Get whether the task is completed.
*
* @return bool
*/
public function is_complete() {
return false;
}
/**
* Get whether the task is visible.
*
* @return bool
*/
public function can_view() {
if ( null === $this->disputes_needing_response ) {
$this->fetch_relevant_disputes();
}
return count( (array) $this->disputes_due_within_7d ) > 0;
}
/**
* Get disputes needing response within the given number of days.
*
* @param int $num_days Number of days in the future to check for disputes needing response.
*
* @return array Disputes needing response within the given number of days.
*/
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 ( $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;
}
}
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() {
if ( null !== $this->disputes_needing_response ) {
return $this->disputes_needing_response;
}
$this->disputes_needing_response = $this->database_cache->get_or_add(
Database_Cache::ACTIVE_DISPUTES_KEY,
function () {
try {
$response = $this->api_client->get_disputes(
[
'pagesize' => 50,
'search' => [ 'warning_needs_response', 'needs_response' ],
]
);
} catch ( \Exception $e ) {
// Return null so Database_Cache::get_or_add treats this as an error
// and does not cache the empty result (which would hide active disputes).
return null;
}
$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;
},
'is_array'
);
return $this->disputes_needing_response;
}
}