Category: Resource

  • Salesforce to WooCommerce: Solving Complex Membership and Subscription Imports

    When migrating customer data from Salesforce into WooCommerce, particularly when using WooCommerce Memberships, Subscriptions, the standard import tools fall short. I’m going to walkthrough how we overcame those limitations by building a custom WordPress plugin that operates at the subscription level to ensure accurate member assignment, product bundle tracking, and data syncing.

    This plugin is production-ready, extensible, and well-suited to organizations dealing with legacy Salesforce data and complex product hierarchies in WooCommerce.


    The Initial Problem

    Context

    WooCommerce’s Memberships plugin provides a basic importer that maps CSV data directly to individual memberships. However, our real-world data was organized around Salesforce Orders, not memberships. This presented three problems:

    1. Subscriptions drive membership access, not individual memberships.
    2. Group products and bundles needed to be tracked and assigned correctly.
    3. Multiple items were tied to a single order and had to be grouped without duplicating data.

    We needed to:

    • Start from Salesforce order exports.
    • Group and assign order items into core subscriptions.
    • Automatically build WooCommerce memberships.
    • Tie in metadata like Salesforce IDs.

    Our Solution

    Instead of trying to patch the existing Memberships importer, we implemented the WooCommerce Subscriptions Importer and built a custom system around it.

    Our importer:

    • Uses a CSV with enriched metadata.
    • Orchestrates import logic through a custom plugin.
    • Caches state between rows using wp_cache_set().
    • Imports subscriptions, creates parent orders, and assigns memberships dynamically.

    The Workflow in Action

    At a high level, the import works like this:

    1. Row is read from the CSV (containing an sf_order_number, product_code, etc.).
    2. The plugin determines if we are starting a new order or continuing an in-progress import.
    3. If new, we:
      • Create a WooCommerce subscription.
      • Assign a parent WooCommerce order.
      • Generate a user membership from the plan.
    4. If continuing:
      • Add the item to the existing subscription.
      • Assign bundled products to the parent order.

    Core Plugin Components

    1. Job.php

    Creates a representation of the current import job.

    $job['wp_user_id'] = get_customer_wp_id( $data['customer_email'] );
    $job['subscription_product'] = membership_product( $job['customer_type'], 'subscription_product' );
    

    This collects all contextual information for one order row: user ID, membership plan, Woo product IDs, etc.

    2. ProductBundler.php

    The main controller class. It drives the logic:

    • Determines if the row is a new order
    • Creates subscriptions
    • Adds order items
    • Links metadata
    if ( empty( $this->job['core_subscription_id'] ) ) {
        create_core_subscription( $subscription_product_sku, $this->data );
    }
    

    It avoids duplicate creation by checking cached values:

    Cache::get('core_subscription_id');
    

    3. SubscriptionHelper.php

    Handles subscription-level actions like:

    • Running WCS_Importer::import_subscription()
    • Adding order items to subscriptions
    function add_item_to_subscription_order( int $core_subscription_id, int $product_id, string $order_item, ... ) {
        $subscription_order_item_id = wc_add_order_item( ... );
        wc_add_order_item_meta( $subscription_order_item_id, '_product_id', $product_id );
    }
    

    It also adjusts legacy membership lengths by modifying subscription dates.

    4. MembershipHelper.php

    Creates the actual Memberships via:

    wc_memberships_create_user_membership( [
      'plan_id' => $plan_id,
      'user_id' => $user_id,
      'order_id' => $order_id,
      'product_id' => $product_id,
    ], 'create' );
    

    Also assigns the created Membership to the correct subscription:

    update_post_meta( $user_membership_id, '_subscription_id', $subscription_id );
    

    5. ParentOrder.php

    If no parent order exists, we create a shell order and set it as the subscription’s parent:

    $parent_order = new \WC_Order();
    $parent_order->set_created_via( 'importer' );
    ...
    wp_update_post([
      'ID' => $job['core_subscription_id'],
      'post_parent' => $parent_order->get_id(),
    ]);
    

    6. Cache.php

    We cache import state between rows:

    Cache::set( 'core_subscription_id', $subscription_id );
    Cache::get( 'product_code' );
    

    This approach ensures continuity during multi-row imports and minimizes redundancy.


    Challenges We Solved

    Problem: Tracking order context across CSV rows

    We solved this by caching state across rows (wp_cache_*). This allowed us to:

    • Avoid duplicate order creations.
    • Accumulate bundled items into a single subscription.

    Problem: Backward compatibility

    We needed to support legacy SKUs like MEMBER2YR, which required extending subscription lengths.

    $dates['next_payment'] = strtotime( "$start_date +$length years" );
    

    Problem: Preventing duplicate user creation

    We always resolve the WP user ID via email at the start:

    get_user_by( 'email', $customer_email );
    

    Final Thoughts

    This project transformed our import process from a manual, error-prone task into a seamless pipeline that:

    • Handles bundles
    • Links Salesforce data
    • Avoids duplicate orders
    • Ensures WooCommerce state integrity

    If you’re managing complex memberships in WooCommerce backed by Salesforce (or any CRM), this system gives you full control over your import logic. Remaining extensible for future needs like REST syncing, Sentry tracing, or CLI support.