Skip to content

Instantly share code, notes, and snippets.

@vapvarun
Last active July 7, 2026 17:27
Show Gist options
  • Select an option

  • Save vapvarun/ae97381279067011e0f157f02c9be2e1 to your computer and use it in GitHub Desktop.

Select an option

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.
<?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