378 lines
9.4 KiB
PHP
378 lines
9.4 KiB
PHP
<?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;
|
||
}
|
||
}
|