d}. The order keeps the rows it had and reports the way it did before. It is recorded as a failed analytics import, so it can be retried from Analytics settings.", array( 'source' => 'wc-order-tax-lookup-migration' ) ); OrdersScheduler::record_failed_order_import( $order_id ); } } // Step past every order in the batch, including any that could not be rebuilt, which are // left to the failed import retry. See CURSOR_OPTION. update_option( self::CURSOR_OPTION, max( array_map( 'absint', $batch ) ), false ); ReportsCache::invalidate(); } /** * Default (preferred) batch size to pass to 'get_next_batch_to_process'. * * A batch is a `wc_get_order()` and a handful of writes per order, so it is sized like the * analytics order importer rather than like a single-query migration. * * @return int Default batch size. */ public function get_default_batch_size(): int { return 100; } /** * Add the tool to start or stop the background rebuild. * * @param array $tools Old tools array. * @return array Updated tools array. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function handle_woocommerce_debug_tools( array $tools ): array { // A failed re-key would otherwise go unseen here: with no order counting as pending, the // tool would say there is nothing to rebuild. Say what is actually missing instead. if ( ! TaxesDataStore::lookup_is_keyed_by_order_item() ) { $tools['rebuild_analytics_tax_data'] = array( 'name' => __( 'Rebuild analytics tax data', 'woocommerce' ), 'button' => __( 'Rebuild', 'woocommerce' ), 'disabled' => true, 'desc' => __( 'This will rebuild the Analytics tax data of orders recorded before WooCommerce kept a record of every tax line. The database change the rebuild needs is missing on this store. Run "Verify base database tables" to apply it, then come back here.', 'woocommerce' ), ); return $tools; } $batch_processor = wc_get_container()->get( BatchProcessingController::class ); $pending_count = $this->get_total_pending_count(); // The count stops at PENDING_COUNT_LIMIT, so say "or more" rather than a number the store // has already gone past. $pending_label = $pending_count < self::PENDING_COUNT_LIMIT ? number_format_i18n( $pending_count ) /* translators: %s: number of orders, where there are at least that many. */ : sprintf( __( '%s+', 'woocommerce' ), number_format_i18n( self::PENDING_COUNT_LIMIT ) ); if ( 0 === $pending_count ) { $tools['rebuild_analytics_tax_data'] = array( 'name' => __( 'Rebuild analytics tax data', 'woocommerce' ), 'button' => __( 'Rebuild', 'woocommerce' ), 'disabled' => true, 'desc' => __( 'This will rebuild the Analytics tax data of orders recorded before WooCommerce kept a record of every tax line. There are currently no orders to rebuild.', 'woocommerce' ), ); } elseif ( $batch_processor->is_enqueued( self::class ) ) { $tools['stop_rebuild_analytics_tax_data'] = array( 'name' => __( 'Stop rebuilding analytics tax data', 'woocommerce' ), 'button' => __( 'Stop rebuilding', 'woocommerce' ), 'requires_refresh' => true, 'desc' => sprintf( /* translators: %s: number of orders still to rebuild. */ _n( 'This will stop the background process that rebuilds the Analytics tax data of orders recorded before WooCommerce kept a record of every tax line. There is currently %s order left to rebuild.', 'This will stop the background process that rebuilds the Analytics tax data of orders recorded before WooCommerce kept a record of every tax line. There are currently %s orders left to rebuild.', $pending_count, 'woocommerce' ), $pending_label ), 'callback' => array( $this, 'dequeue' ), ); } else { $tools['rebuild_analytics_tax_data'] = array( 'name' => __( 'Rebuild analytics tax data', 'woocommerce' ), 'button' => __( 'Rebuild', 'woocommerce' ), 'requires_refresh' => true, 'desc' => sprintf( /* translators: %s: number of orders to rebuild. */ _n( 'This will rebuild the Analytics tax data of orders recorded before WooCommerce kept a record of every tax line. The rebuild happens over time in the background (via Action Scheduler). There is currently %s order to rebuild.', 'This will rebuild the Analytics tax data of orders recorded before WooCommerce kept a record of every tax line. The rebuild happens over time in the background (via Action Scheduler). There are currently %s orders to rebuild.', $pending_count, 'woocommerce' ), $pending_label ), 'callback' => array( $this, 'enqueue' ), ); } return $tools; } /** * Start the background rebuild. * * @return string Informative string to show after the tool is triggered in UI. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function enqueue(): string { $batch_processor = wc_get_container()->get( BatchProcessingController::class ); if ( $batch_processor->is_enqueued( self::class ) ) { return __( 'Background process for rebuilding analytics tax data already started, nothing done.', 'woocommerce' ); } $batch_processor->enqueue_processor( self::class ); return __( 'Background process for rebuilding analytics tax data started.', 'woocommerce' ); } /** * Stop the background rebuild. * * @return string Informative string to show after the tool is triggered in UI. * * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. */ public function dequeue(): string { $batch_processor = wc_get_container()->get( BatchProcessingController::class ); if ( ! $batch_processor->is_enqueued( self::class ) ) { return __( 'Background process for rebuilding analytics tax data not started, nothing done.', 'woocommerce' ); } $batch_processor->remove_processor( self::class ); return __( 'Background process for rebuilding analytics tax data stopped.', 'woocommerce' ); } /** * Whether the order has a row in the order stats table, which is the table every analytics * report reads orders through. * * @param int $order_id Order id. * @return bool */ private function order_has_stats_row( int $order_id ): bool { global $wpdb; $table_name = OrderStatsDataStore::get_db_table_name(); return (bool) $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is not user input. "SELECT 1 FROM {$table_name} WHERE order_id = %d", $order_id ) ); } /** * Highest order id the processor has been through. * * @return int */ private function get_cursor(): int { return (int) get_option( self::CURSOR_OPTION, 0 ); } }