Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e41ef6a
Add remediation for canceled authorization fees
mgascam Nov 18, 2025
e28efb3
Merge branch 'develop' into cancel-auth-fee-remediation
mgascam Nov 26, 2025
dba00e7
feat: add HPOS support for affected orders remediation
mgascam Nov 26, 2025
1c5aad0
Merge branch 'develop' into cancel-auth-fee-remediation
mgascam Nov 26, 2025
ac87be1
Address copilot comments
mgascam Nov 26, 2025
67b4ebb
feat: enhance remediation logic for canceled authorization fees and i…
mgascam Nov 26, 2025
72b6a74
feat: implement admin notice and remediation tool for canceled author…
mgascam Nov 26, 2025
a917c0b
refactor: remove unused plugin update method and clean up test imports
mgascam Nov 26, 2025
b111c29
feat: enhance remediation tool for canceled authorization fees with d…
mgascam Nov 26, 2025
3a83f23
feat: update remediation logic to change order status from 'refunded'…
mgascam Nov 26, 2025
ea90207
feat: add refund stats cleanup logic and corresponding unit tests for…
mgascam Nov 26, 2025
f8d44ec
Merge branch 'develop' into cancel-auth-fee-remediation
mgascam Nov 27, 2025
1aca52c
feat: add confirmation message for canceled authorization remediation…
mgascam Nov 27, 2025
63ce0c2
feat: remove deprecated admin notice for canceled authorization fee r…
mgascam Nov 27, 2025
f93e62e
feat: remove action for plugin update on upgrader process completion
mgascam Nov 27, 2025
3f7e799
fix: update SQL queries to use IN clause for transaction fee meta key…
mgascam Nov 27, 2025
978cad5
feat: enhance refund deletion message to include deleted refund IDs
mgascam Nov 27, 2025
bbf7c43
Add changelog
mgascam Nov 27, 2025
dd969e7
Merge branch 'develop' into cancel-auth-fee-remediation
mgascam Dec 3, 2025
2fbe206
fix: enhance confirmation message for canceled authorization remediat…
mgascam Dec 4, 2025
ef4a458
Merge branch 'develop' into cancel-auth-fee-remediation
mgascam Dec 4, 2025
2b0dc25
Fix linter error
mgascam Dec 4, 2025
161ee72
fix: update remediation process to trigger WooCommerce refund deletio…
mgascam Dec 4, 2025
fa03b13
feat: add dry run functionality for canceled authorization fee remedi…
mgascam Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php
/**
* Admin notice for canceled authorization fee remediation.
*
* @package WooCommerce\Payments\Admin
*/

defined( 'ABSPATH' ) || exit;

/**
* Class WC_Payments_Admin_Notice_Canceled_Auth_Remediation
*
* Displays an admin notice to merchants who may be affected by the canceled
* authorization analytics bug, prompting them to run the remediation tool.
*/
class WC_Payments_Admin_Notice_Canceled_Auth_Remediation {

/**
* Option key for tracking if notice has been dismissed.
*/
const NOTICE_DISMISSED_OPTION = 'wcpay_canceled_auth_remediation_notice_dismissed';

/**
* Initialize hooks.
*
* @return void
*/
public function init_hooks(): void {
add_action( 'admin_notices', [ $this, 'maybe_show_notice' ] );
add_action( 'wp_ajax_wcpay_dismiss_canceled_auth_notice', [ $this, 'dismiss_notice' ] );
}

/**
* Maybe show the admin notice.
*
* @return void
*/
public function maybe_show_notice(): void {
// Only show to users who can manage WooCommerce.
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}

// Don't show if already dismissed.
if ( get_option( self::NOTICE_DISMISSED_OPTION, false ) ) {
return;
}

// Don't show if remediation is already complete.
if ( $this->is_remediation_complete() ) {
return;
}

// Don't show if remediation is already running.
if ( $this->is_remediation_running() ) {
return;
}

// Only show if there are affected orders.
if ( ! $this->has_affected_orders() ) {
// Mark as dismissed so we don't keep checking.
update_option( self::NOTICE_DISMISSED_OPTION, true );
return;
}

$this->render_notice();
}

/**
* Check if remediation is complete.
*
* @return bool
*/
private function is_remediation_complete(): bool {
return 'completed' === get_option( 'wcpay_fee_remediation_status', '' );
}

/**
* Check if remediation is currently running.
*
* @return bool
*/
private function is_remediation_running(): bool {
if ( ! function_exists( 'as_has_scheduled_action' ) ) {
return false;
}

include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php';
return as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK );
}

/**
* Check if there are orders that need remediation.
*
* @return bool
*/
private function has_affected_orders(): bool {
include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php';
$remediation = new WC_Payments_Remediate_Canceled_Auth_Fees();
return $remediation->has_affected_orders();
}

/**
* Render the admin notice.
*
* @return void
*/
private function render_notice(): void {
$tools_url = admin_url( 'admin.php?page=wc-status&tab=tools' );
?>
<div class="notice notice-warning is-dismissible wcpay-canceled-auth-notice">
<p>
<strong><?php esc_html_e( 'WooPayments: Action Required', 'woocommerce-payments' ); ?></strong>
</p>
<p>
<?php
echo wp_kses(
sprintf(
/* translators: %s: URL to WooCommerce Tools page */
__( 'Some orders with canceled payment authorizations have incorrect data that may cause negative values in your WooCommerce Analytics. This affects stores using manual capture (authorize and capture separately). <a href="%s">Run the fix tool</a> to correct this.', 'woocommerce-payments' ),
esc_url( $tools_url )
),
[ 'a' => [ 'href' => [] ] ]
);
?>
</p>
</div>
<script type="text/javascript">
jQuery( document ).ready( function( $ ) {
$( '.wcpay-canceled-auth-notice' ).on( 'click', '.notice-dismiss', function() {
$.post( ajaxurl, {
action: 'wcpay_dismiss_canceled_auth_notice',
_wpnonce: '<?php echo esc_js( wp_create_nonce( 'wcpay_dismiss_canceled_auth_notice' ) ); ?>'
} );
} );
} );
</script>
<?php
}

/**
* AJAX handler to dismiss the notice.
*
* @return void
*/
public function dismiss_notice(): void {
check_ajax_referer( 'wcpay_dismiss_canceled_auth_notice' );

if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( -1 );
}

update_option( self::NOTICE_DISMISSED_OPTION, true );
wp_die();
}
}
156 changes: 154 additions & 2 deletions includes/class-wc-payments-status.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function debug_tools( $tools ) {
return array_merge(
$tools,
[
'clear_wcpay_account_cache' => [
'clear_wcpay_account_cache' => [
'name' => sprintf(
/* translators: %s: WooPayments */
__( 'Clear %s account cache', 'woocommerce-payments' ),
Expand All @@ -80,7 +80,7 @@ public function debug_tools( $tools ) {
),
'callback' => [ $this->account, 'refresh_account_data' ],
],
'delete_wcpay_test_orders' => [
'delete_wcpay_test_orders' => [
'name' => sprintf(
/* translators: %s: WooPayments */
__( 'Delete %s test orders', 'woocommerce-payments' ),
Expand All @@ -94,6 +94,13 @@ public function debug_tools( $tools ) {
),
'callback' => [ $this, 'delete_test_orders' ],
],
'remediate_canceled_auth_fees' => [
'name' => __( 'Fix canceled authorization analytics', 'woocommerce-payments' ),
'button' => $this->get_remediation_button_text(),
'desc' => $this->get_remediation_description(),
'callback' => [ $this, 'schedule_canceled_auth_remediation' ],
'disabled' => $this->is_remediation_running_or_complete(),
],
]
);
}
Expand Down Expand Up @@ -155,6 +162,151 @@ public function delete_test_orders() {
}
}

/**
* Schedules the canceled authorization fee remediation.
*
* This tool fixes incorrect refund records and fee data from orders where
* payment authorization was canceled but never captured.
*
* @return string Success or error message.
*/
public function schedule_canceled_auth_remediation() {
// Add explicit capability check.
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return __( 'You do not have permission to run this tool.', 'woocommerce-payments' );
}

try {
include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php';
$remediation = new WC_Payments_Remediate_Canceled_Auth_Fees();

// Check if already complete.
if ( $remediation->is_complete() ) {
return __( 'Remediation has already been completed.', 'woocommerce-payments' );
}

// Check if already running.
if ( function_exists( 'as_has_scheduled_action' ) && as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK ) ) {
return __( 'Remediation is already in progress. Check the Action Scheduler for status.', 'woocommerce-payments' );
}

// Schedule the remediation.
$remediation->schedule_remediation();

return __( 'Remediation has been scheduled and will run in the background. You can monitor progress in the Action Scheduler.', 'woocommerce-payments' );

} catch ( Exception $e ) {
return sprintf(
/* translators: %s: error message */
__( 'Error scheduling remediation: %s', 'woocommerce-payments' ),
$e->getMessage()
);
}
}

/**
* Get the button text for the remediation tool based on current status.
*
* @return string Button text.
*/
private function get_remediation_button_text(): string {
$status = get_option( 'wcpay_fee_remediation_status', '' );

if ( 'completed' === $status ) {
return __( 'Completed', 'woocommerce-payments' );
}

if ( 'running' === $status || $this->is_remediation_action_scheduled() ) {
return __( 'Running...', 'woocommerce-payments' );
}

return __( 'Run', 'woocommerce-payments' );
}

/**
* Get the description for the remediation tool including current status.
*
* @return string Tool description with status.
*/
private function get_remediation_description(): string {
$base_desc = __( 'This tool removes incorrect refund records and fee data from orders where payment authorization was canceled (not captured). This fixes negative values appearing in WooCommerce Analytics for stores using manual capture.', 'woocommerce-payments' );

$status = get_option( 'wcpay_fee_remediation_status', '' );

if ( 'completed' === $status ) {
$stats = get_option( 'wcpay_fee_remediation_stats', [] );
$processed = isset( $stats['processed'] ) ? (int) $stats['processed'] : 0;
$remediated = isset( $stats['remediated'] ) ? (int) $stats['remediated'] : 0;

if ( $processed > 0 ) {
return sprintf(
/* translators: 1: base description, 2: number of orders processed, 3: number of orders remediated */
__( '%1$s <strong>Status: Completed.</strong> Processed %2$d orders, remediated %3$d.', 'woocommerce-payments' ),
$base_desc,
$processed,
$remediated
);
}

return sprintf(
/* translators: %s: base description */
__( '%s <strong>Status: Completed.</strong> No affected orders found.', 'woocommerce-payments' ),
$base_desc
);
}

if ( 'running' === $status || $this->is_remediation_action_scheduled() ) {
$stats = get_option( 'wcpay_fee_remediation_stats', [] );
$processed = isset( $stats['processed'] ) ? (int) $stats['processed'] : 0;

if ( $processed > 0 ) {
return sprintf(
/* translators: 1: base description, 2: number of orders processed so far */
__( '%1$s <strong>Status: Running...</strong> Processed %2$d orders so far. Check the Action Scheduler for details.', 'woocommerce-payments' ),
$base_desc,
$processed
);
}

return sprintf(
/* translators: %s: base description */
__( '%s <strong>Status: Running...</strong> Check the Action Scheduler for details.', 'woocommerce-payments' ),
$base_desc
);
}

return $base_desc;
}

/**
* Check if the remediation is currently running or already complete.
*
* @return bool True if running or complete.
*/
private function is_remediation_running_or_complete(): bool {
$status = get_option( 'wcpay_fee_remediation_status', '' );

if ( 'completed' === $status || 'running' === $status ) {
return true;
}

return $this->is_remediation_action_scheduled();
}

/**
* Check if the remediation action is scheduled in Action Scheduler.
*
* @return bool True if action is scheduled.
*/
private function is_remediation_action_scheduled(): bool {
if ( ! function_exists( 'as_has_scheduled_action' ) ) {
return false;
}

include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php';
return as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that here, we only checked if WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK is scheduled. But on the schedule_canceled_auth_dry_run method, we check both WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK and WC_Payments_Remediate_Canceled_Auth_Fees::DRY_RUN_ACTION_HOOK.

Could this lead to some inconsistent UI state? I'm unsure if we need to check for the "dry run" action here, as well 🤷

}

/**
* Renders WCPay information on the status page.
*/
Expand Down
16 changes: 16 additions & 0 deletions includes/class-wc-payments.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,13 @@ class WC_Payments {
*/
private static $payment_method_service;

/**
* Instance of WC_Payments_Remediate_Canceled_Auth_Fees, created in init function
*
* @var WC_Payments_Remediate_Canceled_Auth_Fees
*/
private static $fee_remediation;

/**
* Entry point to the initialization logic.
*/
Expand Down Expand Up @@ -352,6 +359,7 @@ public static function init() {
add_action( 'admin_init', [ __CLASS__, 'add_woo_admin_notes' ] );
add_action( 'admin_init', [ __CLASS__, 'remove_deprecated_notes' ] );
add_action( 'init', [ __CLASS__, 'install_actions' ] );
add_action( 'upgrader_process_complete', [ __CLASS__, 'on_plugin_update' ], 10, 2 );

add_action( 'woocommerce_blocks_payment_method_type_registration', [ __CLASS__, 'register_checkout_gateway' ] );

Expand Down Expand Up @@ -492,6 +500,7 @@ public static function init() {
include_once __DIR__ . '/class-wc-payments-order-service.php';
include_once __DIR__ . '/class-wc-payments-order-success-page.php';
include_once __DIR__ . '/class-wc-payments-file-service.php';
include_once __DIR__ . '/migrations/class-wc-payments-remediate-canceled-auth-fees.php';
include_once __DIR__ . '/class-wc-payments-webhook-processing-service.php';
include_once __DIR__ . '/class-wc-payments-webhook-reliability-service.php';
include_once __DIR__ . '/fraud-prevention/class-fraud-prevention-service.php';
Expand Down Expand Up @@ -552,6 +561,12 @@ public static function init() {
self::$incentives_service = new WC_Payments_Incentives_Service( self::$database_cache );
self::$duplicate_payment_prevention_service = new Duplicate_Payment_Prevention_Service();
self::$duplicates_detection_service = new Duplicates_Detection_Service();
self::$fee_remediation = new WC_Payments_Remediate_Canceled_Auth_Fees();

// Initialize admin notice for canceled auth fee remediation.
include_once __DIR__ . '/admin/class-wc-payments-admin-notice-canceled-auth-remediation.php';
$canceled_auth_notice = new WC_Payments_Admin_Notice_Canceled_Auth_Remediation();
$canceled_auth_notice->init_hooks();

( new WooPay_Scheduler( self::$api_client ) )->init();

Expand All @@ -564,6 +579,7 @@ public static function init() {
self::$compatibility_service->init_hooks();
self::$customer_service->init_hooks();
self::$token_service->init_hooks();
self::$fee_remediation->init();

/**
* FLAG: PAYMENT_METHODS_LIST
Expand Down
Loading
Loading