Last active
July 7, 2026 17:27
-
-
Save vapvarun/ae97381279067011e0f157f02c9be2e1 to your computer and use it in GitHub Desktop.
Migrate Woo Sell Services -> WP Sell Services (services + reviews, with product->service ID map). Idempotent, dry-run first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Migrate Woo Sell Services -> WP Sell Services | |
| * ------------------------------------------------- | |
| * Moves your existing WooCommerce-based "services" (Woo Sell Services) into the | |
| * new standalone WP Sell Services. The focus is your SERVICES and their REVIEWS | |
| * (the reviews are the reason to migrate). A product -> service ID map is kept so | |
| * every review lands on the right service and re-running never duplicates anything. | |
| * | |
| * WHAT MAPS TO WHAT | |
| * Service WC product (product_type = services) -> CPT `wpss_service` | |
| * Price _price / _regular_price -> _wpss_starting_price | |
| * Reviews WC product reviews (comments) -> {prefix}wpss_reviews table <-- the point | |
| * ID map (new) old product ID -> new service ID stored on each service | |
| * as `_wpss_source_product_id` + option `wpss_wss_product_map`. | |
| * Questions OPTIONAL - off by default; set $WPSS_UNIFORM_QUESTIONS to apply a set. | |
| * | |
| * HOW TO RUN (WP-CLI, recommended - back up the DB first): | |
| * wp eval-file wpss-migrate-from-woo.php --url=https://yoursite.com | |
| * Start with DRY_RUN = true to see the plan, then flip to false to write. | |
| * | |
| * IDEMPOTENT: a product already in the map is skipped. Safe to re-run. | |
| * NON-DESTRUCTIVE: never deletes the old Woo products/reviews - it only creates | |
| * new WP Sell Services records, so you can verify before removing the old plugin. | |
| * | |
| * @package wpss-migration-helper | |
| */ | |
| if ( ! defined( 'ABSPATH' ) ) { exit; } | |
| /* ---------------------------------------------------------------------------- | |
| * CONFIG - review these three before running. | |
| * ------------------------------------------------------------------------- */ | |
| // Preview only (no writes) when true. Flip to false to actually migrate. | |
| const DRY_RUN = true; | |
| /** | |
| * ALREADY RE-CREATED YOUR SERVICES IN WP SELL SERVICES? Map them here so the | |
| * script attaches the old reviews to your EXISTING services instead of creating | |
| * duplicates. Format: old WC product ID => existing wpss_service ID. | |
| * | |
| * $WPSS_EXISTING_MAP = array( 123 => 4567, 124 => 4568 ); | |
| * | |
| * Not sure of the IDs? Run in DRY_RUN with $WPSS_SUGGEST_MAP = true (below) and | |
| * the script prints a side-by-side of old products and same-titled new services | |
| * so you can copy the pairs into this array. | |
| */ | |
| $WPSS_EXISTING_MAP = array(); | |
| /** | |
| * EASIEST for a small, known set (Adeniyi's case: 10 services). | |
| * List your OLD Woo product IDs and your NEW WP Sell Services service IDs in the | |
| * SAME ORDER - they're paired by position (row 1 -> row 1, row 2 -> row 2, ...). | |
| * No title matching needed. Leave both empty to skip this and use the options above. | |
| * | |
| * $WPSS_OLD_PRODUCT_IDS = array( 812, 813, 814, 815, 816, 817, 818, 819, 820, 821 ); | |
| * $WPSS_NEW_SERVICE_IDS = array( 4567,4568,4569,4570,4571,4572,4573,4574,4575,4576 ); | |
| * | |
| * Get the old IDs from WooCommerce > Products (the ID column), and the new IDs | |
| * from WP Sell Services > Services (the post=<ID> in each service's edit URL). | |
| */ | |
| $WPSS_OLD_PRODUCT_IDS = array(); | |
| $WPSS_NEW_SERVICE_IDS = array(); | |
| // When no explicit pair is given for a product, try to match an existing | |
| // wpss_service by identical title before creating a new one. Safe + convenient | |
| // if your new services use the same names as the old ones. | |
| const MATCH_BY_TITLE = true; | |
| // DRY_RUN helper: print a suggested old-product -> new-service pairing table. | |
| const WPSS_SUGGEST_MAP = true; | |
| /** | |
| * Intake questions for the migrated services - OPTIONAL. | |
| * | |
| * This migration is focused on SERVICES + REVIEWS (the reviews are the point). | |
| * Questions are OFF by default (empty array) so nothing surprising is written. | |
| * If you also want a set of intake questions applied to every migrated service, | |
| * fill this array; each item is | |
| * [ 'question' => '...', 'type' => text|textarea|file|select, 'required' => bool ] | |
| * You can always add/edit questions per service later in wp-admin. | |
| */ | |
| $WPSS_UNIFORM_QUESTIONS = array(); // e.g. array( array( 'question' => 'What do you need?', 'type' => 'textarea', 'required' => true ) ) | |
| /* ---------------------------------------------------------------------------- | |
| * MIGRATION | |
| * ------------------------------------------------------------------------- */ | |
| global $wpdb, $WPSS_UNIFORM_QUESTIONS, $WPSS_EXISTING_MAP, $WPSS_OLD_PRODUCT_IDS, $WPSS_NEW_SERVICE_IDS; | |
| $reviews_table = $wpdb->prefix . 'wpss_reviews'; | |
| $map = get_option( 'wpss_wss_product_map', array() ); // old product ID => new service ID | |
| $stats = array( 'services' => 0, 'reused' => 0, 'skipped' => 0, 'reviews' => 0, 'errors' => 0 ); | |
| // Positional pairing (easiest for a small set): zip the two ordered lists into | |
| // the explicit map. Any hand-written $WPSS_EXISTING_MAP pair still wins. | |
| if ( $WPSS_OLD_PRODUCT_IDS && count( $WPSS_OLD_PRODUCT_IDS ) === count( $WPSS_NEW_SERVICE_IDS ) ) { | |
| $WPSS_EXISTING_MAP = array_replace( | |
| array_combine( array_map( 'intval', $WPSS_OLD_PRODUCT_IDS ), array_map( 'intval', $WPSS_NEW_SERVICE_IDS ) ), | |
| $WPSS_EXISTING_MAP | |
| ); | |
| echo sprintf( "Positional map: %d ordered product/service pair(s).\n", count( $WPSS_OLD_PRODUCT_IDS ) ); | |
| } elseif ( $WPSS_OLD_PRODUCT_IDS || $WPSS_NEW_SERVICE_IDS ) { | |
| echo "WARNING: \$WPSS_OLD_PRODUCT_IDS and \$WPSS_NEW_SERVICE_IDS must have the SAME number of rows - positional map ignored.\n"; | |
| } | |
| // Old Woo Sell Services stored each service as a WC product of type "services". | |
| $products = wc_get_products( array( 'type' => 'services', 'limit' => -1, 'status' => 'publish' ) ); | |
| echo sprintf( "Found %d Woo Sell Services product(s). DRY_RUN = %s\n", count( $products ), DRY_RUN ? 'true' : 'false' ); | |
| // Suggestion table: help you build $WPSS_EXISTING_MAP if you already re-created services. | |
| if ( DRY_RUN && WPSS_SUGGEST_MAP ) { | |
| echo "\nSuggested product -> existing service pairing (fill \$WPSS_EXISTING_MAP with these):\n"; | |
| foreach ( $products as $p ) { | |
| $existing = wpss_find_service_by_title( $p->get_name() ); | |
| echo sprintf( " %d => %s // \"%s\"%s\n", | |
| $p->get_id(), $existing ? $existing : 'NEW (no title match - will be created)', | |
| $p->get_name(), $existing ? '' : '' ); | |
| } | |
| echo "\n"; | |
| } | |
| foreach ( $products as $product ) { | |
| $pid = $product->get_id(); | |
| if ( isset( $map[ $pid ] ) && get_post_status( $map[ $pid ] ) ) { | |
| $stats['skipped']++; | |
| echo " skip product #{$pid} -> already linked to service #{$map[$pid]}\n"; | |
| continue; | |
| } | |
| // --- 1) RESOLVE which service this product maps to --------------------- | |
| // Priority: (a) explicit pair you set, (b) same-title existing service, | |
| // (c) none -> create a fresh service. | |
| $service_id = 0; | |
| $how = 'create'; | |
| if ( isset( $WPSS_EXISTING_MAP[ $pid ] ) && get_post_status( $WPSS_EXISTING_MAP[ $pid ] ) === 'publish' ) { | |
| $service_id = (int) $WPSS_EXISTING_MAP[ $pid ]; // you told us the pair | |
| $how = 'mapped'; | |
| } elseif ( MATCH_BY_TITLE ) { | |
| $match = wpss_find_service_by_title( $product->get_name() ); // same-name service | |
| if ( $match ) { $service_id = $match; $how = 'title-match'; } | |
| } | |
| $reviews = wpss_get_woo_reviews( $pid ); | |
| if ( DRY_RUN ) { | |
| echo sprintf( " PLAN product #%d \"%s\" -> %s%s | %d review(s) to move\n", | |
| $pid, $product->get_name(), $how, | |
| $service_id ? " service #{$service_id}" : '', count( $reviews ) ); | |
| $service_id ? $stats['reused']++ : $stats['services']++; | |
| continue; | |
| } | |
| // --- 2) create the service ONLY if nothing to reuse ------------------- | |
| if ( ! $service_id ) { | |
| $service_id = wp_insert_post( array( | |
| 'post_type' => 'wpss_service', | |
| 'post_status' => 'publish', | |
| 'post_title' => $product->get_name(), | |
| 'post_content' => $product->get_description(), | |
| 'post_excerpt' => $product->get_short_description(), | |
| 'post_author' => (int) $product->get_post_data()->post_author, // vendor = author | |
| ), true ); | |
| if ( is_wp_error( $service_id ) ) { $stats['errors']++; echo " ERROR product #{$pid}: {$service_id->get_error_message()}\n"; continue; } | |
| $thumb = get_post_thumbnail_id( $pid ); | |
| if ( $thumb ) { set_post_thumbnail( $service_id, $thumb ); } | |
| update_post_meta( $service_id, '_wpss_starting_price', (float) $product->get_regular_price() ); | |
| update_post_meta( $service_id, '_wpss_requirements', is_array( $WPSS_UNIFORM_QUESTIONS ) ? $WPSS_UNIFORM_QUESTIONS : array() ); | |
| $stats['services']++; | |
| echo " created service #{$service_id} for product #{$pid}\n"; | |
| } else { | |
| // Reusing your existing service - do NOT overwrite its title/price/content, | |
| // only attach the reviews below. | |
| $stats['reused']++; | |
| echo " linked product #{$pid} -> existing service #{$service_id} ({$how})\n"; | |
| } | |
| update_post_meta( $service_id, '_wpss_source_product_id', $pid ); // provenance + idempotency | |
| $map[ $pid ] = $service_id; | |
| // --- 3) move this product's reviews onto the resolved service -------- | |
| $vendor_id = (int) $product->get_post_data()->post_author; | |
| foreach ( $reviews as $c ) { | |
| if ( get_comment_meta( $c->comment_ID, '_wpss_migrated', true ) ) { continue; } // no dupes | |
| $rating = (int) get_comment_meta( $c->comment_ID, 'rating', true ); | |
| $wpdb->insert( $reviews_table, array( | |
| 'order_id' => 0, | |
| 'reviewer_id' => (int) $c->user_id, | |
| 'reviewee_id' => $vendor_id, | |
| 'service_id' => $service_id, | |
| 'customer_id' => (int) $c->user_id, | |
| 'vendor_id' => $vendor_id, | |
| 'rating' => $rating ? $rating : 5, | |
| 'review' => $c->comment_content, | |
| 'review_type' => 'customer_to_vendor', | |
| 'status' => ( '1' === $c->comment_approved || 'approve' === $c->comment_approved ) ? 'approved' : 'pending', | |
| 'is_public' => 1, | |
| 'created_at' => $c->comment_date, | |
| ) ); | |
| add_comment_meta( $c->comment_ID, '_wpss_migrated', 1, true ); | |
| $stats['reviews']++; | |
| } | |
| // --- 4) recompute the service's rating summary meta --- | |
| $row = $wpdb->get_row( $wpdb->prepare( | |
| "SELECT COUNT(*) c, AVG(rating) a FROM {$reviews_table} WHERE service_id = %d AND status = 'approved'", | |
| $service_id | |
| ) ); | |
| update_post_meta( $service_id, '_wpss_review_count', (int) $row->c ); | |
| update_post_meta( $service_id, '_wpss_rating_count', (int) $row->c ); | |
| update_post_meta( $service_id, '_wpss_rating_average', $row->a ? round( (float) $row->a, 2 ) : 0 ); | |
| } | |
| if ( ! DRY_RUN ) { update_option( 'wpss_wss_product_map', $map, false ); } | |
| echo sprintf( "\nDONE new services: %d linked to existing: %d already done: %d reviews: %d errors: %d\n", | |
| $stats['services'], $stats['reused'], $stats['skipped'], $stats['reviews'], $stats['errors'] ); | |
| echo DRY_RUN ? "This was a DRY RUN - set DRY_RUN=false to write.\n" : "Migration written. Verify in wp-admin before removing Woo Sell Services.\n"; | |
| /* ---------------------------------------------------------------------------- | |
| * Helpers | |
| * ------------------------------------------------------------------------- */ | |
| /** Find an existing wpss_service by exact (case-insensitive) title. 0 if none. */ | |
| function wpss_find_service_by_title( string $title ): int { | |
| $found = get_posts( array( | |
| 'post_type' => 'wpss_service', | |
| 'post_status' => array( 'publish', 'draft', 'pending' ), | |
| 'title' => $title, // exact title match (WP 6.7+) | |
| 'posts_per_page' => 1, | |
| 'fields' => 'ids', | |
| 'no_found_rows' => true, | |
| 'update_post_meta_cache' => false, | |
| 'update_post_term_cache' => false, | |
| ) ); | |
| if ( $found ) { return (int) $found[0]; } | |
| // Fallback for older WP without the `title` arg: manual compare. | |
| foreach ( get_posts( array( 'post_type' => 'wpss_service', 'numberposts' => -1, 'fields' => 'ids' ) ) as $sid ) { | |
| if ( 0 === strcasecmp( trim( get_the_title( $sid ) ), trim( $title ) ) ) { return (int) $sid; } | |
| } | |
| return 0; | |
| } | |
| /** Approved WC product reviews for a product (the reviews we're carrying over). */ | |
| function wpss_get_woo_reviews( int $product_id ): array { | |
| return get_comments( array( | |
| 'post_id' => $product_id, | |
| 'type' => 'review', // WooCommerce stores reviews as comment_type 'review' | |
| 'status' => 'approve', | |
| ) ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment