Conversation
Introduces a namespaced, class-based migration system under src/Migration/ for migrating WooCommerce subscriptions from legacy PayPal Express to PPCP. Key components: - Migration_Controller: facade/entry point with singleton pattern - Subscription_Migration_Service: core migration logic - Action_Scheduler_Batch_Processor: async batch processing - HPOS_Migration_State_Storage: HPOS-compatible state tracking - Migration_Admin_Page: WP admin UI with AJAX handlers - DTOs, Enums, Contracts for type safety Fixes applied during integration: - Removed duplicate get_failed_subscriptions() method that caused parameter type mismatch (string vs array) breaking retry_failed() - Updated retry_failed() to work with detailed result objects - Removed double mark_started() call in retry() preventing duplicate attempt counter increments - Made AJAX handlers accept payment_method from POST data instead of hardcoding 'paypal_express' Co-Authored-By: Claude Opus 4.6 <[email protected]>
Load the refactored migration autoloader and initialize it during PPCP core initialization, alongside the existing migration class. Co-Authored-By: Claude Opus 4.6 <[email protected]>
The new migration code uses PHP 8.1 features (enums, readonly properties, union types). Update composer.json to reflect the actual minimum supported version. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Includes tests for: - Migration_Status enum (terminal states, failure checks, labels, CSS classes) - Migration_Result DTO (factory methods, validation, serialization) - Batch_Result DTO (aggregation, success rate, filtering) - Payment_Token_Validator (format validation, masking, multi-key lookup) - Subscription_Migration_Service (process_single, process_batch, retry, error handling, edge cases) Tests use WP/WC function stubs so they run without a full WordPress environment. 48 tests, 143 assertions. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR introduces a refactored, namespaced subscription migration subsystem under src/Migration/ to migrate WooCommerce Subscriptions from legacy PayPal Express to PPCP, including state tracking, Action Scheduler batch processing, an admin management UI, and a PHPUnit 10 unit test suite (and raising the project’s Composer PHP requirement to 8.1).
Changes:
- Added a new
AngellEYE\PayPal\Migrationmodule (controller, services, DTOs, enums, HPOS-aware state storage, Action Scheduler batch processor, and an admin UI page). - Wired the migration module into the PPCP bootstrap and provided a standalone autoloader + init hooks.
- Added PHPUnit-based unit tests and a
composer testscript; updatedcomposer.jsonminimum PHP to>=8.1.0.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 21 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Migration/stubs.php | WP/WC stubs to run unit tests without WordPress. |
| tests/Migration/phpunit.xml | PHPUnit 10 configuration for Migration unit tests. |
| tests/Migration/bootstrap.php | Loads stubs + Migration autoloader for tests. |
| tests/Migration/Unit/Test_Subscription_Migration_Service.php | Unit tests for batch/single/retry orchestration. |
| tests/Migration/Unit/Test_Payment_Token_Validator.php | Unit tests for token validation + masking. |
| tests/Migration/Unit/Test_Migration_Status_Enum.php | Unit tests for enum helpers. |
| tests/Migration/Unit/Test_Migration_Result_DTO.php | Unit tests for Migration_Result DTO behavior. |
| tests/Migration/Unit/Test_Batch_Result_DTO.php | Unit tests for Batch_Result DTO aggregation. |
| tests/Migration/.phpunit.cache/test-results | PHPUnit cache artifact (should not be committed). |
| src/Migration/autoload.php | Custom autoloader + init functions + AS hook registration. |
| src/Migration/State/HPOS_Migration_State_Storage.php | HPOS-aware state meta read/write + stats + pending queries. |
| src/Migration/Services/Subscription_Migration_Service.php | Core migration logic: validate token, update method, track state. |
| src/Migration/Services/Payment_Token_Validator.php | Token meta scanning, format checks, masking, diagnostics. |
| src/Migration/Services/Payment_Method_Updater.php | Applies payment method switch + notes + hooks. |
| src/Migration/Queue/Action_Scheduler_Batch_Processor.php | AS-based scheduling, execution, logging, lifecycle controls. |
| src/Migration/Migration_Controller.php | Facade API: start/stop/stats/retry/reset and failed listing. |
| src/Migration/Enums/Migration_Status.php | Enum for lifecycle states + labels + CSS classes. |
| src/Migration/DTOs/Migration_Stats.php | Migration stats DTO (completion %, success rate, estimates). |
| src/Migration/DTOs/Migration_Result.php | Migration result DTO (success/failed/skipped) + serialization. |
| src/Migration/DTOs/Batch_Result.php | Batch result DTO with aggregation helpers + serialization. |
| src/Migration/Contracts/Migration_Step_Interface.php | Interface placeholder for step-based migrations. |
| src/Migration/Contracts/Migration_State_Storage_Interface.php | Contract + meta key constants for state storage. |
| src/Migration/Contracts/Batch_Processor_Interface.php | Contract for batch processing implementations. |
| src/Migration/Admin/Migration_Admin_Page.php | Admin UI page + AJAX handlers + export/retry/reset actions. |
| ppcp-gateway/includes/trait-angelleye-ppcp-core.php | Boots the new migration module from PPCP core bootstrap. |
| composer.json | Raises PHP min to 8.1, adds PHPUnit 10 + test script. |
| CLAUDE.md | Repository contributor guidance (now needs updates for PHP/tests). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| as_unschedule_action( | ||
| $this->hook_name, | ||
| [ | ||
| 'from' => $from_payment_method, | ||
| 'to' => $to_payment_method, | ||
| ], |
There was a problem hiding this comment.
pause() unschedules using only from/to args, but scheduled actions also include batch_size. If Action Scheduler requires exact arg matches, this call may fail to pause migrations. Consider including batch_size in the unschedule args or changing the scheduled args shape so pause/cancel can match.
| as_unschedule_action( | |
| $this->hook_name, | |
| [ | |
| 'from' => $from_payment_method, | |
| 'to' => $to_payment_method, | |
| ], | |
| // Unschedule all pending migration actions for this hook and group. | |
| // Using null for $args ensures we don't rely on a specific argument shape, | |
| // which might include additional parameters like batch_size. | |
| as_unschedule_all_actions( | |
| $this->hook_name, | |
| null, |
| $controller = \AngellEYE\PayPal\Migration\Migration_Controller::instance(); | ||
| $controller->process_batch($from_payment_method, $to_payment_method, $batch_size); | ||
| } |
There was a problem hiding this comment.
The global Action Scheduler callback processes a batch but doesn’t reschedule the next batch when there’s more work. If this is the only callback available in a cron request, the migration will stop after a single batch. Route scheduled actions through the batch processor’s handler (which schedules the next batch).
| vendor/bin/phpcbf --standard=phpcs.xml.dist path/to/file.php | ||
| ``` | ||
|
|
||
| No build step — JS/CSS are maintained directly (no Webpack/Gulp). No automated test suite exists. |
There was a problem hiding this comment.
This line says no automated test suite exists, but this PR adds PHPUnit tests and a composer test script. Update the guidance so contributors discover and run the new test suite.
| No build step — JS/CSS are maintained directly (no Webpack/Gulp). No automated test suite exists. | |
| No build step — JS/CSS are maintained directly (no Webpack/Gulp). PHPUnit test suite available — run via `composer test`. |
| $asset_url = plugins_url('assets/', PAYPAL_FOR_WOOCOMMERCE_PLUGIN_FILE); | ||
|
|
||
| // CSS | ||
| wp_enqueue_style( | ||
| 'angelleye-ppcp-migration-admin', | ||
| $asset_url . 'css/migration-admin.css', | ||
| [], | ||
| '1.0.0' | ||
| ); |
There was a problem hiding this comment.
This enqueues assets/css/migration-admin.css, but that file doesn’t appear to exist in the repository’s assets/css/ directory. This will result in a 404 and unstyled admin UI. Either add the CSS file at that path or update the enqueue to point at the actual stylesheet location.
| $asset_url = plugins_url('assets/', PAYPAL_FOR_WOOCOMMERCE_PLUGIN_FILE); | |
| // CSS | |
| wp_enqueue_style( | |
| 'angelleye-ppcp-migration-admin', | |
| $asset_url . 'css/migration-admin.css', | |
| [], | |
| '1.0.0' | |
| ); | |
| $asset_url = plugins_url('assets/', PAYPAL_FOR_WOOCOMMERCE_PLUGIN_FILE); | |
| // CSS | |
| $css_file_path = $asset_path . 'css/migration-admin.css'; | |
| if ( file_exists( $css_file_path ) ) { | |
| wp_enqueue_style( | |
| 'angelleye-ppcp-migration-admin', | |
| $asset_url . 'css/migration-admin.css', | |
| [], | |
| '1.0.0' | |
| ); | |
| } |
| 'type' => 'shop_subscription', | ||
| 'status' => ['wc-active', 'wc-on-hold', 'wc-pending-cancel'], | ||
| 'payment_method' => $payment_method, | ||
| 'limit' => -1, | ||
| 'return' => 'ids', | ||
| 'meta_query' => [ | ||
| [ | ||
| 'key' => self::META_STATUS, | ||
| 'value' => $status->value, | ||
| 'compare' => '=', | ||
| ], | ||
| ], | ||
| ]; | ||
|
|
||
| $status_counts[$status->value] = count(wc_get_orders($args)); | ||
| } | ||
|
|
||
| // Count not started | ||
| $args = [ | ||
| 'type' => 'shop_subscription', | ||
| 'status' => ['wc-active', 'wc-on-hold', 'wc-pending-cancel'], | ||
| 'payment_method' => $payment_method, | ||
| 'limit' => -1, | ||
| 'return' => 'ids', | ||
| 'meta_query' => [ | ||
| [ | ||
| 'key' => self::META_STATUS, | ||
| 'compare' => 'NOT EXISTS', | ||
| ], | ||
| ], | ||
| ]; | ||
| $status_counts['not_started'] = count(wc_get_orders($args)); |
There was a problem hiding this comment.
get_stats() counts orders by fetching all matching IDs (limit => -1, return => ids) and then count()ing them. On large stores and frequent AJAX refreshes this can be very expensive. Prefer an approach that returns totals without loading all IDs (e.g., a paginated query with total, or a dedicated COUNT query) and/or cache short-term.
| 'type' => 'shop_subscription', | |
| 'status' => ['wc-active', 'wc-on-hold', 'wc-pending-cancel'], | |
| 'payment_method' => $payment_method, | |
| 'limit' => -1, | |
| 'return' => 'ids', | |
| 'meta_query' => [ | |
| [ | |
| 'key' => self::META_STATUS, | |
| 'value' => $status->value, | |
| 'compare' => '=', | |
| ], | |
| ], | |
| ]; | |
| $status_counts[$status->value] = count(wc_get_orders($args)); | |
| } | |
| // Count not started | |
| $args = [ | |
| 'type' => 'shop_subscription', | |
| 'status' => ['wc-active', 'wc-on-hold', 'wc-pending-cancel'], | |
| 'payment_method' => $payment_method, | |
| 'limit' => -1, | |
| 'return' => 'ids', | |
| 'meta_query' => [ | |
| [ | |
| 'key' => self::META_STATUS, | |
| 'compare' => 'NOT EXISTS', | |
| ], | |
| ], | |
| ]; | |
| $status_counts['not_started'] = count(wc_get_orders($args)); | |
| 'type' => 'shop_subscription', | |
| 'status' => ['wc-active', 'wc-on-hold', 'wc-pending-cancel'], | |
| 'payment_method' => $payment_method, | |
| 'paginate' => true, | |
| 'return' => 'ids', | |
| 'meta_query' => [ | |
| [ | |
| 'key' => self::META_STATUS, | |
| 'value' => $status->value, | |
| 'compare' => '=', | |
| ], | |
| ], | |
| ]; | |
| $result = wc_get_orders( $args ); | |
| $status_counts[ $status->value ] = isset( $result['total'] ) ? (int) $result['total'] : 0; | |
| } | |
| // Count not started | |
| $args = [ | |
| 'type' => 'shop_subscription', | |
| 'status' => ['wc-active', 'wc-on-hold', 'wc-pending-cancel'], | |
| 'payment_method' => $payment_method, | |
| 'paginate' => true, | |
| 'return' => 'ids', | |
| 'meta_query' => [ | |
| [ | |
| 'key' => self::META_STATUS, | |
| 'compare' => 'NOT EXISTS', | |
| ], | |
| ], | |
| ]; | |
| $result = wc_get_orders( $args ); | |
| $status_counts['not_started'] = isset( $result['total'] ) ? (int) $result['total'] : 0; |
| $migration_autoload = PAYPAL_FOR_WOOCOMMERCE_PLUGIN_DIR . '/src/Migration/autoload.php'; | ||
| if (file_exists($migration_autoload) && !function_exists('angelleye_ppcp_migration_init')) { | ||
| include_once $migration_autoload; | ||
| angelleye_ppcp_migration_init(); |
There was a problem hiding this comment.
The refactored migration code uses PHP 8.1+ features (enums, etc.). This include happens without a PHP-version guard, so on older PHP it can fatal as soon as it’s loaded. If older PHP may still run this plugin, add a PHP_VERSION_ID/version_compare() check before including src/Migration/autoload.php / calling angelleye_ppcp_migration_init().
| $migration_autoload = PAYPAL_FOR_WOOCOMMERCE_PLUGIN_DIR . '/src/Migration/autoload.php'; | |
| if (file_exists($migration_autoload) && !function_exists('angelleye_ppcp_migration_init')) { | |
| include_once $migration_autoload; | |
| angelleye_ppcp_migration_init(); | |
| if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 80100) { | |
| $migration_autoload = PAYPAL_FOR_WOOCOMMERCE_PLUGIN_DIR . '/src/Migration/autoload.php'; | |
| if (file_exists($migration_autoload) && !function_exists('angelleye_ppcp_migration_init')) { | |
| include_once $migration_autoload; | |
| angelleye_ppcp_migration_init(); | |
| } |
| $pending_actions = as_get_scheduled_actions([ | ||
| 'hook' => $this->hook_name, | ||
| 'args' => [ | ||
| 'from' => $from_payment_method, | ||
| 'to' => $to_payment_method, | ||
| ], | ||
| 'status' => \ActionScheduler_Store::STATUS_PENDING, | ||
| 'group' => $this->group, | ||
| ]); | ||
|
|
||
| return !empty($pending_actions); |
There was a problem hiding this comment.
is_running() queries scheduled actions by from/to only, but scheduled actions also include batch_size. If args must match exactly, is_running() may return false even when a migration is queued, allowing duplicate schedules.
| $pending_actions = as_get_scheduled_actions([ | |
| 'hook' => $this->hook_name, | |
| 'args' => [ | |
| 'from' => $from_payment_method, | |
| 'to' => $to_payment_method, | |
| ], | |
| 'status' => \ActionScheduler_Store::STATUS_PENDING, | |
| 'group' => $this->group, | |
| ]); | |
| return !empty($pending_actions); | |
| // Query all pending actions for this hook/group, then match by args in PHP. | |
| $pending_action_ids = as_get_scheduled_actions([ | |
| 'hook' => $this->hook_name, | |
| 'status' => \ActionScheduler_Store::STATUS_PENDING, | |
| 'group' => $this->group, | |
| 'return' => 'ids', | |
| ]); | |
| foreach ($pending_action_ids as $action_id) { | |
| $action = as_get_scheduled_action($action_id); | |
| if (! $action) { | |
| continue; | |
| } | |
| $args = $action->get_args(); | |
| if ( | |
| isset($args['from'], $args['to']) && | |
| $args['from'] === $from_payment_method && | |
| $args['to'] === $to_payment_method | |
| ) { | |
| return true; | |
| } | |
| } | |
| return false; |
| @@ -0,0 +1 @@ | |||
| {"version":2,"defects":[],"times":{"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_calculates_totals_correctly":0.003,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_success_rate_calculation":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_success_rate_returns_zero_for_empty_batch":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_get_failures_returns_only_failed_results":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_get_successes_returns_only_successful_results":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_get_skipped_returns_only_skipped_results":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_has_failures_returns_true_when_failures_exist":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_has_failures_returns_false_when_no_failures":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_has_more_flag_is_stored":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_to_array_contains_all_fields":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_success_creates_completed_result":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_failed_creates_failure_result":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_failed_throws_exception_for_non_failure_status":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_skipped_creates_skipped_result":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_to_array_contains_all_fields":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_processed_at_is_set_automatically":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_is_skipped_returns_true_for_skipped_statuses":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_is_skipped_returns_false_for_non_skipped":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_terminal_returns_true_for_completed":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_terminal_returns_true_for_failed_no_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_terminal_returns_false_for_in_progress":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_terminal_returns_false_for_not_started":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_failure_returns_true_for_failed_statuses":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_failure_returns_false_for_non_failed_statuses":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_label_returns_expected_strings":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_css_class_returns_expected_values":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_all_statuses_can_be_instantiated_from_string":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_tryFrom_returns_null_for_invalid_value":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_true_for_payment_tokens_id":0.005,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_true_for_ppec_billing_agreement":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_true_for_paypal_subscription_id":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_false_for_invalid_subscription_id":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_false_when_no_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_false_for_short_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_get_token_details_returns_correct_info":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_get_token_details_masks_token_value":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_get_all_token_attempts_returns_all_checks":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_skips_already_processed":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_fails_for_missing_subscription":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_fails_when_no_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_succeeds_with_valid_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_catches_update_exception":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_batch_returns_correct_counts":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_batch_empty_returns_zero_results":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_batch_sets_has_more_when_remaining":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_retry_calls_process_single_and_returns_result":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_retry_throws_for_nonexistent_subscription":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_retry_uses_old_payment_method_meta_when_available":0}} No newline at end of file | |||
There was a problem hiding this comment.
PHPUnit cache artifacts are committed (tests/Migration/.phpunit.cache/test-results). These files are machine-specific and should typically be gitignored rather than versioned. Consider removing this file from the repo and adding tests/Migration/.phpunit.cache/ (or .phpunit.cache/) to .gitignore.
| {"version":2,"defects":[],"times":{"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_calculates_totals_correctly":0.003,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_success_rate_calculation":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_success_rate_returns_zero_for_empty_batch":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_get_failures_returns_only_failed_results":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_get_successes_returns_only_successful_results":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_get_skipped_returns_only_skipped_results":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_has_failures_returns_true_when_failures_exist":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_has_failures_returns_false_when_no_failures":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_has_more_flag_is_stored":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Batch_Result_DTO::test_to_array_contains_all_fields":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_success_creates_completed_result":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_failed_creates_failure_result":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_failed_throws_exception_for_non_failure_status":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_skipped_creates_skipped_result":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_to_array_contains_all_fields":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_processed_at_is_set_automatically":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_is_skipped_returns_true_for_skipped_statuses":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Result_DTO::test_is_skipped_returns_false_for_non_skipped":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_terminal_returns_true_for_completed":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_terminal_returns_true_for_failed_no_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_terminal_returns_false_for_in_progress":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_terminal_returns_false_for_not_started":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_failure_returns_true_for_failed_statuses":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_is_failure_returns_false_for_non_failed_statuses":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_label_returns_expected_strings":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_css_class_returns_expected_values":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_all_statuses_can_be_instantiated_from_string":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Migration_Status_Enum::test_tryFrom_returns_null_for_invalid_value":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_true_for_payment_tokens_id":0.005,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_true_for_ppec_billing_agreement":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_true_for_paypal_subscription_id":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_false_for_invalid_subscription_id":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_false_when_no_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_has_valid_token_returns_false_for_short_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_get_token_details_returns_correct_info":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_get_token_details_masks_token_value":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Payment_Token_Validator::test_get_all_token_attempts_returns_all_checks":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_skips_already_processed":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_fails_for_missing_subscription":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_fails_when_no_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_succeeds_with_valid_token":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_single_catches_update_exception":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_batch_returns_correct_counts":0.001,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_batch_empty_returns_zero_results":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_process_batch_sets_has_more_when_remaining":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_retry_calls_process_single_and_returns_result":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_retry_throws_for_nonexistent_subscription":0,"AngellEYE\\PayPal\\Migration\\Tests\\Unit\\Test_Subscription_Migration_Service::test_retry_uses_old_payment_method_meta_when_available":0}} | |
| # Placeholder: PHPUnit .phpunit.cache/test-results file should not contain committed cache data. |
| return; | ||
| } | ||
|
|
||
| $asset_path = plugin_dir_path(PAYPAL_FOR_WOOCOMMERCE_PLUGIN_FILE) . 'assets/'; |
There was a problem hiding this comment.
enqueue_assets() assigns $asset_path but never uses it. This is dead code and can be removed to avoid confusion (or use it for file existence/versioning if that was the intent).
| $asset_path = plugin_dir_path(PAYPAL_FOR_WOOCOMMERCE_PLUGIN_FILE) . 'assets/'; |
| $total = $stats['total'] ?? 0; | ||
| $completed = $stats['completed'] ?? 0; | ||
| $failed = $stats['failed'] ?? 0; | ||
| $pending = $total - $completed - $failed; |
There was a problem hiding this comment.
$pending is computed as total - completed - failed, which ignores skipped statuses. If any subscriptions are marked skipped_*, the banner will overstate “remaining” subscriptions. Consider computing pending as not_started + in_progress (or subtract skipped as well).
| $total = $stats['total'] ?? 0; | |
| $completed = $stats['completed'] ?? 0; | |
| $failed = $stats['failed'] ?? 0; | |
| $pending = $total - $completed - $failed; | |
| $total = $stats['total'] ?? 0; | |
| $completed = $stats['completed'] ?? 0; | |
| $failed = $stats['failed'] ?? 0; | |
| $not_started = $stats['not_started'] ?? null; | |
| $in_progress = $stats['in_progress'] ?? null; | |
| if ($not_started !== null || $in_progress !== null) { | |
| $pending = (int) ($not_started ?? 0) + (int) ($in_progress ?? 0); | |
| } else { | |
| // Fallback to legacy calculation when detailed status counts are unavailable. | |
| $pending = $total - $completed - $failed; | |
| } |
Close #2133
Summary
src/Migration/) for migrating WooCommerce subscriptions from legacy PayPal Express to PPCPKey fixes applied during integration
get_failed_subscriptions()method that caused parameter type mismatch breakingretry_failed()mark_started()call inretry()that was incrementing attempt counters twicepayment_methodfrom POST data instead of hardcodingpaypal_expressTest plan
composer test)🤖 Generated with Claude Code