Файловый менеджер - Редактировать - /home/harasnat/www/labour/wp-content/plugins/woocommerce-payments/includes/subscriptions/class-wc-payments-subscriptions-migrator.php
Назад
<?php /** * Class WC_Payments_Subscriptions_Migrator * * @package WooCommerce\Payments */ use WCPay\Exceptions\API_Exception; require_once __DIR__ . '/class-wc-payments-subscription-migration-log-handler.php'; /** * Handles migrating WCPay Subscriptions to tokenized subscriptions. * * This class extends the WCS_Background_Repairer for scheduling and running the individual migration actions. */ class WC_Payments_Subscriptions_Migrator extends WCS_Background_Repairer { /** * Valid subscription statuses to cancel a subscription at Stripe. * * @var array $active_statuses */ private $active_statuses = [ 'active', 'past_due', 'trialing', 'paused' ]; /** * WCPay Subscription meta keys for migrated data. * * @var array $migrated_meta_keys */ private $meta_keys_to_migrate = [ WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, WC_Payments_Invoice_Service::ORDER_INVOICE_ID_KEY, WC_Payments_Invoice_Service::PENDING_INVOICE_ID_KEY, WC_Payments_Subscription_Service::SUBSCRIPTION_DISCOUNT_IDS_META_KEY, ]; /** * WC_Payments_API_Client instance. * * @var WC_Payments_API_Client */ private $api_client; /** * WC_Payments_Token_Service instance. * * @var WC_Payments_Token_Service */ private $token_service; /** * WC_Payments_Subscription_Migration_Log_Handler instance. * * @var WC_Payments_Subscription_Migration_Log_Handler */ protected $logger; /** * The Action Scheduler hook used to find and schedule individual migrations of WCPay Subscriptions. * * @var string */ public $scheduled_hook = 'wcpay_schedule_subscription_migrations'; /** * The Action Scheduler hook to migrate a WCPay Subscription. * * @var string */ public $migrate_hook = 'wcpay_migrate_subscription'; /** * The option name used to store a batch identifier for the current migration batch. * * @var string */ private $migration_batch_identifier_option = 'wcpay_subscription_migration_batch'; /** * Constructor. * * @param WC_Payments_API_Client|null $api_client WC_Payments_API_Client instance. * @param WC_Payments_Token_Service|null $token_service WC_Payments_Token_Service instance. */ public function __construct( $api_client = null, $token_service = null ) { $this->api_client = $api_client; $this->token_service = $token_service; $this->logger = new WC_Payments_Subscription_Migration_Log_Handler(); // Don't copy migrated subscription meta keys to related orders. add_filter( 'wc_subscriptions_object_data', [ $this, 'exclude_migrated_meta' ], 10, 1 ); // Add manual migration tool to WooCommerce > Status > Tools. add_filter( 'woocommerce_debug_tools', [ $this, 'add_manual_migration_tool' ] ); // Schedule the single migration action with two args. This is needed because the WCS_Background_Repairer parent class only hooks on with one arg. add_action( $this->migrate_hook . '_retry', [ $this, 'migrate_wcpay_subscription' ], 10, 2 ); $this->init(); } /** * Migrates a WCPay Subscription to a tokenized WooPayments subscription powered by WC Subscriptions * * Migration process: * 1. Validate the request to migrate subscription * 2. Fetches the subscription from Stripe * 3. Cancels the subscription at Stripe if it is active * 4. Update the subscription meta to indicate that it has been migrated * 5. Add an order note on the subscription * * @param int $subscription_id The ID of the subscription to migrate. * @param int $attempt The number of times migration has been attempted. */ public function migrate_wcpay_subscription( $subscription_id, $attempt = 0 ) { try { add_action( 'action_scheduler_unexpected_shutdown', [ $this, 'handle_unexpected_shutdown' ], 10, 2 ); add_action( 'action_scheduler_failed_execution', [ $this, 'handle_unexpected_action_failure' ], 10, 2 ); $this->logger->log( sprintf( 'Migrating subscription #%1$d.%2$s', $subscription_id, ( $attempt > 0 ? ' Attempt: ' . ( (int) $attempt + 1 ) : '' ) ) ); $subscription = $this->validate_subscription_to_migrate( $subscription_id ); $wcpay_subscription = $this->fetch_wcpay_subscription( $subscription ); $this->maybe_cancel_wcpay_subscription( $wcpay_subscription ); if ( $subscription->has_status( 'active' ) ) { $this->update_next_payment_date( $subscription, $wcpay_subscription ); } // If the subscription is active or on-hold, verify the payment method is valid and set correctly that it continues to renew. if ( $subscription->has_status( [ 'active', 'on-hold' ] ) ) { $this->verify_subscription_payment_token( $subscription, $wcpay_subscription ); } $this->update_wcpay_subscription_meta( $subscription ); if ( WC_Payment_Gateway_WCPay::GATEWAY_ID === $subscription->get_payment_method() ) { $subscription->add_order_note( __( 'This subscription has been successfully migrated to a WooPayments tokenized subscription.', 'woocommerce-payments' ) ); } $this->logger->log( sprintf( '---- Subscription #%d migration complete.', $subscription_id ) ); } catch ( \Exception $e ) { $this->logger->log( $e->getMessage() ); $this->maybe_reschedule_migration( $subscription_id, $attempt, $e ); } remove_action( 'action_scheduler_unexpected_shutdown', [ $this, 'handle_unexpected_shutdown' ] ); remove_action( 'action_scheduler_failed_execution', [ $this, 'handle_unexpected_action_failure' ] ); } /** * Validates the request to migrate a WCPay Subscription. * * Only allows migration if: * - The WooCommerce Subscription extension is active * - Store is not in staging mode or is a duplicate site * - The subscription ID is a valid subscription * - The subscription has not already been migrated * * @param int $subscription_id The ID of the subscription to migrate. * * @throws \Exception Skip the migration if the request is invalid. */ private function validate_subscription_to_migrate( $subscription_id ) { if ( ! class_exists( 'WC_Subscriptions' ) ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. The WooCommerce Subscriptions extension is not active.', $subscription_id ) ); } if ( WC_Payments_Subscriptions::is_duplicate_site() ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. Site is in staging mode.', $subscription_id ) ); } $subscription = wcs_get_subscription( $subscription_id ); if ( ! $subscription ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. Subscription not found.', $subscription_id ) ); } $migrated_wcpay_subscription_id = $subscription->get_meta( '_migrated_wcpay_subscription_id', true ); if ( ! empty( $migrated_wcpay_subscription_id ) ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%1$d (%2$s). Subscription has already been migrated.', $subscription_id, $migrated_wcpay_subscription_id ) ); } return $subscription; } /** * Fetches the subscription from Stripe and verifies it has a valid ID and status. * * Returns false if the request returns an unexpected result. * * @param WC_Subscription $subscription The WC subscription to migrate. * * @return array * * @throws \Exception If there's an error fetching the subscription from Stripe. */ private function fetch_wcpay_subscription( $subscription ) { $wcpay_subscription_id = WC_Payments_Subscription_Service::get_wcpay_subscription_id( $subscription ); if ( ! $wcpay_subscription_id ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. Subscription is not a WCPay Subscription.', $subscription->get_id() ) ); } try { // Fetch the subscription from Stripe. $wcpay_subscription = $this->api_client->get_subscription( $wcpay_subscription_id ); } catch ( API_Exception $e ) { throw new \Exception( sprintf( '---- ERROR: Failed to fetch subscription #%1$d (%2$s) from Stripe. %3$s', $subscription->get_id(), $wcpay_subscription_id, $e->getMessage() ) ); } if ( empty( $wcpay_subscription['id'] ) || empty( $wcpay_subscription['status'] ) ) { throw new \Exception( sprintf( '---- ERROR: Cannot migrate subscription #%1$d (%2$s). Invalid data fetched from Stripe: %3$s', $subscription->get_id(), $wcpay_subscription_id, var_export( $wcpay_subscription, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export } return $wcpay_subscription; } /** * Cancels the subscription at Stripe if it is active. * * This function checks the status on the subscription at Stripe then cancels it if it's a valid status and logs any errors. * * We skip canceling any subscriptions at Stripe that are: * - incomplete: the subscription was created but no payment method was added to the subscription * - incomplete_expired: the incomplete subscription expired after 24hrs of no payment method being added. * - canceled: the subscription is already canceled * - unpaid: this status is not used by subscriptions in WooCommerce Payments * * @param array $wcpay_subscription The subscription data from Stripe. * * @throws \Exception If there's an error canceling the subscription at Stripe. */ private function maybe_cancel_wcpay_subscription( $wcpay_subscription ) { // Valid statuses to cancel subscription at Stripe: active, past_due, trialing, paused. if ( in_array( $wcpay_subscription['status'], $this->active_statuses, true ) ) { $this->logger->log( sprintf( '---- Stripe subscription (%1$s) has "%2$s" status. Canceling the subscription.', $wcpay_subscription['id'], $this->get_wcpay_subscription_status( $wcpay_subscription ) ) ); try { // Cancel the subscription in Stripe. $wcpay_subscription = $this->api_client->cancel_subscription( $wcpay_subscription['id'] ); } catch ( API_Exception $e ) { throw new \Exception( sprintf( '---- ERROR: Failed to cancel the Stripe subscription (%1$s). %2$s', $wcpay_subscription['id'], $e->getMessage() ) ); } $this->logger->log( sprintf( '---- Stripe subscription (%1$s) successfully canceled.', $wcpay_subscription['id'] ) ); } else { // Statuses that don't need to be canceled: incomplete, incomplete_expired, canceled, unpaid. $this->logger->log( sprintf( '---- Stripe subscription (%1$s) has "%2$s" status. Skipping canceling the subscription at Stripe.', $wcpay_subscription['id'], $this->get_wcpay_subscription_status( $wcpay_subscription ) ) ); } } /** * Migrates WCPay Subscription related metadata to a new key prefixed with `_migrated` and deletes the old meta. * * @param WC_Subscription $subscription The subscription with wcpay meta saved. */ private function update_wcpay_subscription_meta( $subscription ) { $updated = false; /** * If this subscription is being migrated while scheduling individual actions is on-going, make sure we store meta on the subscription * so that it's still returned by the query in @see get_items_to_repair() to not affect the limit and pagination. */ $migration_start = get_option( $this->migration_batch_identifier_option, 0 ); if ( 0 !== $migration_start ) { $subscription->update_meta_data( '_wcpay_subscription_migrated_during', $migration_start ); $updated = true; } foreach ( $this->meta_keys_to_migrate as $meta_key ) { if ( $subscription->meta_exists( $meta_key ) ) { $subscription->update_meta_data( '_migrated' . $meta_key, $subscription->get_meta( $meta_key, true ) ); $subscription->delete_meta_data( $meta_key ); $updated = true; } } if ( $updated ) { $subscription->save(); } } /** * Updates the subscription's next payment date in WooCommerce to ensure a smooth transition to on-site billing. * * There's a scenario where a WCPay subscription is active but has no pending renewal scheduled action. * Once migrated, this results in an active subscription that will remain active forever, without processing a renewal order. * * To ensure that all migrated subscriptions have a pending scheduled action, we need to reschedule the next payment date by * updating the date on the subscription. * * In priority order the new next payment date will be: * - The existing WooCommerce next payment date if it's in the future. * - The Stripe subscription's current_period_end if it's in the future. * - A newly calculated next payment date using the WC_Subscription::calculate_date() method. * * @param WC_Subscription $subscription The WC Subscription being migrated. * @param array $wcpay_subscription The subscription data from Stripe. */ private function update_next_payment_date( $subscription, $wcpay_subscription ) { try { // Just update the existing WC Subscription's next payment date if it's in the future. if ( $subscription->get_time( 'next_payment' ) > time() ) { $new_next_payment = gmdate( 'Y-m-d H:i:s', $subscription->get_time( 'next_payment' ) + 1 ); $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); $this->logger->log( sprintf( '---- Next payment date updated to %1$s to ensure subscription #%2$d has a pending scheduled payment.', $new_next_payment, $subscription->get_id() ) ); return; } // If the subscription was still using WooPayments, use the Stripe subscription's next payment time (current_period_end) if it's in the future. if ( WC_Payment_Gateway_WCPay::GATEWAY_ID === $subscription->get_payment_method() && isset( $wcpay_subscription['current_period_end'] ) && absint( $wcpay_subscription['current_period_end'] ) > time() ) { $new_next_payment = gmdate( 'Y-m-d H:i:s', absint( $wcpay_subscription['current_period_end'] ) ); $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); $this->logger->log( sprintf( '---- Next payment date updated to %1$s to match Stripe subscription record and to ensure subscription #%2$d has a pending scheduled payment.', $new_next_payment, $subscription->get_id() ) ); return; } // Lastly calculate the next payment date. $new_next_payment = $subscription->calculate_date( 'next_payment' ); if ( wcs_date_to_time( $new_next_payment ) > time() ) { $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); $this->logger->log( sprintf( '---- Calculated a new next payment date (%1$s) to ensure subscription #%2$d has a pending scheduled payment in the future.', $new_next_payment, $subscription->get_id() ) ); return; } // If we got here the next payment date is in the past, the Stripe subscription is missing a "current_period_end" or it's in the past, and calculating a new date also failed. Log an error. $this->logger->log( sprintf( '---- ERROR: Failed to update subscription #%1$d next payment date. Current next payment date (%2$s) is in the past, Stripe "current_period_end" data is invalid (%3$s) and an attempt to calculate a new date also failed (%4$s).', $subscription->get_id(), gmdate( 'Y-m-d H:i:s', $subscription->get_time( 'next_payment' ) ), isset( $wcpay_subscription['current_period_end'] ) ? gmdate( 'Y-m-d H:i:s', absint( $wcpay_subscription['current_period_end'] ) ) : 'no data', $new_next_payment ) ); } catch ( \Exception $e ) { $this->logger->log( sprintf( '---- ERROR: Failed to update subscription #%1$d next payment date. %2$s', $subscription->get_id(), $e->getMessage() ) ); } } /** * Returns the subscription status from the WCPay subscription data for logging purposes. * * If a subscription is on-hold in WC we wouldn't have changed the status of the subscription at Stripe, instead, the * subscription would remain active and set `pause_collection` behavior to `void` so that the subscription is not charged. * * The purpose of this function is to handle the `paused_collection` value when mapping the subscription status at Stripe to * a status for logging. * * @param array $wcpay_subscription The subscription data from Stripe. * * @return string The WCPay subscription status for logging purposes. */ private function get_wcpay_subscription_status( $wcpay_subscription ) { if ( empty( $wcpay_subscription['status'] ) ) { return 'unknown'; } if ( 'active' === $wcpay_subscription['status'] && ! empty( $wcpay_subscription['pause_collection']['behavior'] ) && 'void' === $wcpay_subscription['pause_collection']['behavior'] ) { return 'paused'; } return $wcpay_subscription['status']; } /** * Verifies the payment token on the subscription matches the default payment method on the WCPay Subscription. * * This function does two things: * 1. If the subscription doesn't have a WooPayments payment token, set it to the default payment method from Stripe Billing. * 2. If the subscription has a token, verify the token matches the token on the Stripe Billing subscription * * @param WC_Subscription $subscription The subscription to verify the payment token on. * @param array $wcpay_subscription The subscription data from Stripe. */ private function verify_subscription_payment_token( $subscription, $wcpay_subscription ) { // If the subscription's payment method isn't set to WooPayments, we skip this token step. if ( $subscription->get_payment_method() !== WC_Payment_Gateway_WCPay::GATEWAY_ID ) { $this->logger->log( sprintf( '---- Skipped verifying the payment token. Subscription #%1$d has "%2$s" as the payment method.', $subscription->get_id(), $subscription->get_payment_method() ) ); return; } if ( empty( $wcpay_subscription['default_payment_method'] ) ) { $this->logger->log( sprintf( '---- Could not verify the payment method. Stripe Billing subscription (%1$s) does not have a default payment method.', $wcpay_subscription['id'] ?? 'unknown' ) ); return; } $tokens = $subscription->get_payment_tokens(); $token_id = end( $tokens ); $token = ! $token_id ? null : WC_Payment_Tokens::get( $token_id ); // If the token matches the default payment method on the Stripe Billing subscription, we're done here. if ( $token && $token->get_token() === $wcpay_subscription['default_payment_method'] ) { $this->logger->log( sprintf( '---- Payment token on subscription #%1$d matches the payment method on the Stripe Billing subscription (%2$s).', $subscription->get_id(), $wcpay_subscription['id'] ?? 'unknown' ) ); return; } // At this point we know the subscription doesn't have a token or the token doesn't match, add one using the default payment method on the WCPay Subscription. $new_token = $this->maybe_create_and_update_payment_token( $subscription, $wcpay_subscription ); if ( $new_token ) { $this->logger->log( sprintf( '---- Payment token on subscription #%1$d has been updated (from %2$s to %3$s) to match the payment method on the Stripe Billing subscription.', $subscription->get_id(), $token ? $token->get_token() : 'missing', $wcpay_subscription['default_payment_method'] ) ); } } /** * Locates a payment token or creates one if it doesn't exist, then updates the subscription with the new token. * * @param WC_Subscription $subscription The subscription to add the payment token to. * @param array $wcpay_subscription The subscription data from Stripe. * * @return WC_Payment_Token|false The new payment token or false if the token couldn't be created. */ private function maybe_create_and_update_payment_token( $subscription, $wcpay_subscription ) { $token = false; $user = new WP_User( $subscription->get_user_id() ); $customer_tokens = WC_Payment_Tokens::get_tokens( [ 'user_id' => $user->ID, 'gateway_id' => WC_Payment_Gateway_WCPay::GATEWAY_ID, 'limit' => WC_Payment_Gateway_WCPay::USER_FORMATTED_TOKENS_LIMIT, ] ); foreach ( $customer_tokens as $customer_token ) { if ( $customer_token->get_token() === $wcpay_subscription['default_payment_method'] ) { $token = $customer_token; break; } } // If we didn't find a token linked to the subscription customer, create one. if ( ! $token ) { try { $token = $this->token_service->add_payment_method_to_user( $wcpay_subscription['default_payment_method'], $user ); $this->logger->log( sprintf( '---- Created a new payment token (%1$s) for subscription #%2$d.', $token->get_token(), $subscription->get_id() ) ); } catch ( \Exception $e ) { $this->logger->log( sprintf( '---- WARNING: Subscription #%1$d is missing a payment token and we failed to create one. Error: %2$s', $subscription->get_id(), $e->getMessage() ) ); return; } } // Prevent the WC_Payments_Subscriptions class from attempting to update the Stripe Billing subscription's payment method while we set the token. remove_action( 'woocommerce_payment_token_added_to_order', [ WC_Payments_Subscriptions::get_subscription_service(), 'update_wcpay_subscription_payment_method' ], 10 ); $subscription->add_payment_token( $token ); // Reattach. add_action( 'woocommerce_payment_token_added_to_order', [ WC_Payments_Subscriptions::get_subscription_service(), 'update_wcpay_subscription_payment_method' ], 10, 3 ); return $token; } /** * Prevents migrated WCPay subscription metadata being copied to subscription related orders (renewal/switch/resubscribe). * * @param array $meta_data The meta data to be copied. * @return array The meta data to be copied. */ public function exclude_migrated_meta( $meta_data ) { foreach ( $this->meta_keys_to_migrate as $key ) { unset( $meta_data[ '_migrated' . $key ] ); } return $meta_data; } /** * Logs any fatal errors that occur while processing a scheduled migrate WCPay Subscription action. * * @param string $action_id The Action Scheduler action ID. * @param array $error The error data. */ public function handle_unexpected_shutdown( $action_id, $error = null ) { $migration_args = $this->get_migration_action_args( $action_id ); if ( ! isset( $migration_args['migrate_subscription'], $migration_args['attempt'] ) ) { return; } if ( ! empty( $error['type'] ) && in_array( $error['type'], [ E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ], true ) ) { $this->logger->log( sprintf( '---- ERROR: Unexpected shutdown while migrating subscription #%1$d: %2$s in %3$s on line %4$s.', $migration_args['migrate_subscription'], $error['message'] ?? 'No message', $error['file'] ?? 'no file found', $error['line'] ?? '0' ) ); } $this->maybe_reschedule_migration( $migration_args['migrate_subscription'], $migration_args['attempt'] ); } /** * Handles any unexpected failures that occur while processing a single migration action * by logging an error message and rescheduling the action to retry. * * @param string $action_id The Action Scheduler action ID. * @param Exception $exception The exception thrown during action processing. */ public function handle_unexpected_action_failure( $action_id, $exception ) { $migration_args = $this->get_migration_action_args( $action_id ); if ( ! isset( $migration_args['migrate_subscription'], $migration_args['attempt'] ) ) { return; } $this->logger->log( sprintf( '---- ERROR: Unexpected failure while migrating subscription #%1$d: %2$s', $migration_args['migrate_subscription'], $exception->getMessage() ) ); $this->maybe_reschedule_migration( $migration_args['migrate_subscription'], $migration_args['attempt'] ); } /** * Adds a manual migration tool to WooCommerce > Status > Tools. * * This tool is only loaded on stores that have: * - WC Subscriptions extension activated * - Subscriptions with WooPayments feature disabled * - Existing WCPay Subscriptions that can be migrated * * @param array $tools List of WC debug tools. * * @return array List of WC debug tools. */ public function add_manual_migration_tool( $tools ) { if ( WC_Payments_Features::is_wcpay_subscriptions_enabled() || ! class_exists( 'WC_Subscriptions' ) ) { return $tools; } // Get number of WCPay Subscriptions that can be migrated. $wcpay_subscriptions_count = $this->get_stripe_billing_subscription_count(); if ( $wcpay_subscriptions_count < 1 ) { return $tools; } // Disable the button if a migration is currently in progress. $disabled = $this->is_migrating(); $tools['migrate_wcpay_subscriptions'] = [ 'name' => __( 'Migrate Stripe Billing subscriptions', 'woocommerce-payments' ), 'button' => $disabled ? __( 'Migration in progress', 'woocommerce-payments' ) . '…' : __( 'Migrate Subscriptions', 'woocommerce-payments' ), 'desc' => sprintf( // translators: %1$s is a new line character and %2$d is the number of subscriptions. __( 'This tool will migrate all Stripe Billing subscriptions to tokenized subscriptions with WooPayments.%1$sNumber of Stripe Billing subscriptions found: %2$d', 'woocommerce-payments' ), '<br>', $wcpay_subscriptions_count, ), 'callback' => [ $this, 'schedule_migrate_wcpay_subscriptions_action' ], 'disabled' => $disabled, 'requires_refresh' => true, ]; return $tools; } /** * Schedules the initial migration action which signals the start of the migration process. */ public function schedule_migrate_wcpay_subscriptions_action() { if ( as_next_scheduled_action( $this->scheduled_hook ) ) { return; } update_option( $this->migration_batch_identifier_option, time() ); $this->logger->log( 'Started scheduling subscription migrations.' ); $this->schedule_repair(); } /** * Gets the subscription ID and number of attempts from the action args. * * @param int $action_id The action ID to get data from. * * @return array */ private function get_migration_action_args( $action_id ) { $action = ActionScheduler_Store::instance()->fetch_action( $action_id ); if ( ! $action || ( $this->migrate_hook !== $action->get_hook() && $this->migrate_hook . '_retry' !== $action->get_hook() ) ) { return []; } $action_args = $action->get_args(); if ( ! isset( $action_args['migrate_subscription'] ) ) { return []; } return array_merge( [ 'migrate_subscription' => 0, 'attempt' => 0, ], $action_args ); } /** * Reschedules a subscription migration with increasing delays depending on number of attempts. * * After max retries, an exception is thrown if one was passed. * * @param int $subscription_id The ID of the subscription to retry. * @param int $attempt The number of times migration has been attempted. * @param \Exception|null $exception The exception thrown during migration. * * @throws \Exception If max attempts and exception passed is not null. */ public function maybe_reschedule_migration( $subscription_id, $attempt = 0, $exception = null ) { // Number of seconds to wait before retrying the migration, increasing with each attempt up to 7 attempts (12 hours). $retry_schedule = [ 60, 300, 600, 1800, HOUR_IN_SECONDS, 6 * HOUR_IN_SECONDS, 12 * HOUR_IN_SECONDS ]; // If the exception thrown contains "Skipping migration", don't reschedule the migration. if ( $exception && false !== strpos( $exception->getMessage(), 'Skipping migration' ) ) { return; } if ( isset( $retry_schedule[ $attempt ] ) && $attempt < 7 ) { $this->logger->log( sprintf( '---- Rescheduling migration of subscription #%1$d.', $subscription_id ) ); as_schedule_single_action( gmdate( 'U' ) + $retry_schedule[ $attempt ], $this->migrate_hook . '_retry', [ 'migrate_subscription' => $subscription_id, 'attempt' => $attempt + 1, ] ); } else { $this->logger->log( sprintf( '---- FAILED: Subscription #%d could not be migrated.', $subscription_id ) ); if ( $exception ) { // Before throwing the exception, remove the action_scheduler failure hook to prevent the exception being logged again. remove_action( 'action_scheduler_failed_execution', [ $this, 'handle_unexpected_action_failure' ] ); throw $exception; } } } /** * Override WCS_Background_Repairer methods. */ /** * Initialize class variables and hooks to handle scheduling and running migration hooks in the background. */ public function init() { $this->repair_hook = $this->migrate_hook; parent::init(); } /** * Schedules an individual action to migrate a subscription. * * Overrides the parent class function to make two changes: * 1. Don't schedule an action if one already exists. * 2. Schedules the migration to happen in one minute instead of in one hour. * * @param int $item The ID of the subscription to migrate. */ public function update_item( $item ) { if ( ! as_next_scheduled_action( $this->migrate_hook, [ 'migrate_subscription' => $item ] ) ) { as_schedule_single_action( gmdate( 'U' ) + 60, $this->migrate_hook, [ 'migrate_subscription' => $item ] ); } unset( $this->items_to_repair[ $item ] ); } /** * Migrates an individual subscription. * * The repair_item() function is called by the parent class when the individual scheduled action is run. * This acts as a wrapper for the migrate_wcpay_subscription() function. * * @param int $item The ID of the subscription to migrate. */ public function repair_item( $item ) { $this->migrate_wcpay_subscription( $item ); } /** * Gets a batch of 100 subscriptions to migrate. * * Because this function fetches items in batches using limit and paged query args, we need to make sure * the paging of this query is consistent regardless of whether some subscriptions have been repaired/migrated in between. * * To do this, we use the $this->migration_batch_identifier_option value to identify subscriptions previously returned by * this function that have been migrated so they will still be considered for paging. * * @param int $page The page of results to fetch. * * @return int[] The IDs of the subscriptions to migrate. */ public function get_items_to_repair( $page ) { $items_to_migrate = wcs_get_orders_with_meta_query( [ 'return' => 'ids', 'type' => 'shop_subscription', 'limit' => 100, 'status' => 'any', 'paged' => $page, 'order' => 'ASC', 'orderby' => 'ID', 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'OR', [ 'key' => WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, 'compare' => 'EXISTS', ], // We need to include subscriptions which have already been migrated as part of this migration group to make // sure correct paging is maintained. As subscriptions are migrated they would migrate the WCPay subscription ID // meta key and therefore fall out of this query's scope - messing with the paging of future queries. // Subscriptions with the `migrated_during` meta aren't expected to be returned by this query, they are included to pad out the earlier pages. [ 'key' => '_wcpay_subscription_migrated_during', 'value' => get_option( $this->migration_batch_identifier_option, 0 ), 'compare' => '=', ], ], ] ); if ( empty( $items_to_migrate ) ) { $this->logger->log( 'Finished scheduling subscription migrations.' ); } return $items_to_migrate; } /** * Gets the total number of subscriptions to migrate. * * @return int The total number of subscriptions to migrate. */ public function get_stripe_billing_subscription_count() { $result = wcs_get_orders_with_meta_query( [ 'status' => 'any', 'return' => 'ids', 'type' => 'shop_subscription', 'limit' => - 1, 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ 'key' => WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, 'compare' => 'EXISTS', ], ], ] ); return is_countable( $result ) ? count( $result ) : 0; } /** * Determines if a migration is currently in progress. * * A migration is considered to be in progress if the initial migration action or an individual subscription * action (or retry) is scheduled. * * @return bool True if a migration is in progress, false otherwise. */ public function is_migrating() { return (bool) as_next_scheduled_action( $this->scheduled_hook ) || (bool) as_next_scheduled_action( $this->migrate_hook ) || (bool) as_next_scheduled_action( $this->migrate_hook . '_retry' ); } /** * Runs any actions that need to handle the completion of the migration. */ protected function unschedule_background_updates() { parent::unschedule_background_updates(); delete_option( $this->migration_batch_identifier_option ); } }
| ver. 1.4 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0.01 |
proxy
|
phpinfo
|
Настройка