-
Notifications
You must be signed in to change notification settings - Fork 72
Add remediation tool to fix incorrect analytics data from canceled authorizations #11140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 11 commits
e41ef6a
e28efb3
dba00e7
1c5aad0
ac87be1
67b4ebb
72b6a74
a917c0b
b111c29
3a83f23
ea90207
f8d44ec
1aca52c
63ce0c2
f93e62e
3f7e799
978cad5
bbf7c43
dd969e7
2fbe206
ef4a458
2b0dc25
161ee72
fa03b13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' ), | ||
|
|
@@ -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' ), | ||
|
|
@@ -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(), | ||
| ], | ||
| ] | ||
| ); | ||
| } | ||
|
|
@@ -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 ); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed that here, we only checked if 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. | ||
| */ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.