Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions changelog/cancel-auth-fee-remediation
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: add

Add remediation tool to fix incorrect analytics data from canceled authorizations
227 changes: 225 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,21 @@ public function debug_tools( $tools ) {
),
'callback' => [ $this, 'delete_test_orders' ],
],
'remediate_canceled_auth_fees_dry_run' => [
'name' => __( 'Preview canceled authorization fix (Dry Run)', 'woocommerce-payments' ),
'button' => $this->get_dry_run_button_text(),
'desc' => __( 'Preview what orders would be affected by the canceled authorization fix without making any changes. Results are logged to WooCommerce > Status > Logs.', 'woocommerce-payments' ),
'callback' => [ $this, 'schedule_canceled_auth_dry_run' ],
'disabled' => $this->is_remediation_running_or_complete(),
],
'remediate_canceled_auth_fees' => [
'name' => __( 'Fix canceled authorization analytics', 'woocommerce-payments' ),
'button' => $this->get_remediation_button_text(),
'desc' => $this->get_remediation_description(),
'confirm' => __( 'This will update order metadata and delete incorrect refund records for affected orders. This fixes negative values in WooCommerce Analytics. Make sure you have a recent backup before proceeding. Continue?', 'woocommerce-payments' ),
'callback' => [ $this, 'schedule_canceled_auth_remediation' ],
'disabled' => $this->is_remediation_running_or_complete(),
],
]
);
}
Expand Down Expand Up @@ -155,6 +170,214 @@ 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()
);
}
}

/**
* Schedules the canceled authorization fee remediation dry run.
*
* This previews what orders would be affected without making changes.
*
* @return string Success or error message.
*/
public function schedule_canceled_auth_dry_run() {
// 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' ) ) {
if ( as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK ) ||
as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::DRY_RUN_ACTION_HOOK ) ) {
return __( 'Remediation is already in progress. Check the Action Scheduler for status.', 'woocommerce-payments' );
}
}

// Schedule the dry run.
$remediation->schedule_dry_run();

return __( 'Dry run has been scheduled and will run in the background. Check WooCommerce > Status > Logs for results (source: wcpay-fee-remediation).', 'woocommerce-payments' );

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

/**
* Get the button text for the dry run tool based on current status.
*
* @return string Button text.
*/
private function get_dry_run_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 __( 'Preview', 'woocommerce-payments' );
}

/**
* 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 @@ -492,6 +499,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 +560,7 @@ 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();

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

Expand All @@ -564,6 +573,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 Expand Up @@ -1537,6 +1547,9 @@ public static function add_woo_admin_notes() {

require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php';
WC_Payments_Notes_Stripe_Billing_Deprecation::possibly_add_note();

require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-canceled-auth-remediation.php';
WC_Payments_Notes_Canceled_Auth_Remediation::possibly_add_note();
}

if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '7.5', '<' ) && get_woocommerce_currency() === 'NOK' ) {
Expand Down Expand Up @@ -1603,6 +1616,9 @@ public static function remove_woo_admin_notes() {

require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php';
WC_Payments_Notes_Stripe_Billing_Deprecation::possibly_delete_note();

require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-canceled-auth-remediation.php';
WC_Payments_Notes_Canceled_Auth_Remediation::possibly_delete_note();
}
}

Expand Down
Loading
Loading