delete( $wpdb->prefix . 'wc_order_product_lookup', array( 'order_item_id'=>(int) $item_id, 'order_id'=>(int) $order_id ) ); } /** The once-per-store step (hooks()): at most a second of batches per request, until the flag says it is done. */ public static function upgrade(): void { if ( get_option( self::LINES_DROPPED ) === 'yes' ) { return; } self::drop_quote_lines( 1.0 ); } /** * The quote lines WooCommerce wrote into its product lookup before quote_line() took them out (0.65): only rows of orders * whose Analytics status is a quote status, checked again by each DELETE, so it is idempotent and an order accepted * meanwhile keeps its lines. In batches of DROP_BATCH rows (review of 0.65 L2: one unbatched DELETE could hold a large B2B * store's lookup), for at most $seconds (0: until none is left). Once none is left (or Analytics has no tables, so nothing * was ever written) LINES_DROPPED is set and the step never runs again. Returns how many rows it removed. */ public static function drop_quote_lines( float $seconds = 0.0 ): int { global $wpdb; $lookup = $wpdb->prefix . 'wc_order_product_lookup'; $stats = $wpdb->prefix . 'wc_order_stats'; foreach ( array( $lookup, $stats ) as $table ) { $found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ); if ( $wpdb->last_error !== '' ) { return 0; } // Tried again by the next admin or cron request. if ( $found !== $table ) { update_option( self::LINES_DROPPED, 'yes', true ); return 0; } } $statuses = array_map( static fn( $s ) => 'wc-' . $s, self::QUOTES ); $until = $seconds > 0 ? microtime( true ) + $seconds : 0.0; $removed = 0; while ( true ) { // The IN () lists are placeholders only (implode of array_fill); every value is bound by prepare(). $items = $wpdb->get_col( $wpdb->prepare( 'SELECT l.order_item_id FROM %i l INNER JOIN %i s ON s.order_id=l.order_id WHERE s.status IN (' . implode( ',', array_fill( 0, count( $statuses ), '%s' ) ) . ') ORDER BY l.order_item_id LIMIT %d', array_merge( array( $lookup, $stats ), $statuses, array( self::DROP_BATCH ) ) ) ); if ( $wpdb->last_error !== '' || ! is_array( $items ) ) { return $removed; } if ( ! $items ) { update_option( self::LINES_DROPPED, 'yes', true ); return $removed; } $items = array_map( 'intval', $items ); $gone = $wpdb->query( $wpdb->prepare( 'DELETE l FROM %i l INNER JOIN %i s ON s.order_id=l.order_id WHERE s.status IN (' . implode( ',', array_fill( 0, count( $statuses ), '%s' ) ) . ') AND l.order_item_id IN (' . implode( ',', array_fill( 0, count( $items ), '%d' ) ) . ')', array_merge( array( $lookup, $stats ), $statuses, $items ) ) ); if ( false === $gone ) { return $removed; } $removed += (int) $gone; if ( count( $items ) < self::DROP_BATCH ) { update_option( self::LINES_DROPPED, 'yes', true ); return $removed; } if ( $until && microtime( true ) >= $until ) { return $removed; } } } /** WooCommerce Analytics (revenue, orders, products, customers) leaves quotes out; it adds the wc- prefix itself. */ public static function analytics( $statuses ): array { return array_values( array_unique( array_merge( is_array( $statuses ) ? $statuses : array(), self::QUOTES ) ) ); } public static function quote( $order ): bool { return $order instanceof \WC_Order && $order->has_status( self::QUOTES ); } /** Every order status WooCommerce knows but the quotes, prefixed as wc_get_orders() takes them. */ public static function orders(): array { return array_values( array_diff( array_keys( wc_get_order_statuses() ), array_map( static fn( $s ) => 'wc-' . $s, self::QUOTES ) ) ); } /** Refuses order work (preparing, handing over, a shipping label, a return) on a quote. */ public static function refuse_quote( \WC_Order $order ): void { if ( self::quote( $order ) ) { throw new \InvalidArgumentException( I18n::t( 'This is a quote, not an order yet. It becomes an order when the customer accepts it.' ) ); } } /** The quote statuses as an SQL list over the order index, which keeps bare statuses. */ public static function quotes_sql(): string { return "'" . implode( "','", self::QUOTES ) . "'"; } }