Files

377 lines
9.3 KiB
PHP
Raw Permalink Normal View History

2025-02-05 23:15:46 +01:00
<?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::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 ) {
// Ensure an array is always returned, even if the API call fails.
return [];
}
$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;
}
}