#!/usr/bin/env python3 """ WooCommerce GUEST CHECKOUT to Saleor Migration ============================================== For stores without customer accounts. All customer data comes from order fields. Two approaches: 1. PURE GUEST: Orders only, no customer accounts created 2. HYBRID (Recommended): Create customer accounts from unique emails, link orders Recommended: HYBRID - customers can later claim their account via password reset """ import os import sys import json import uuid import argparse from datetime import datetime from typing import Dict, List, Optional, Set from dataclasses import dataclass from collections import defaultdict import psycopg2 # Database configurations WP_DB_CONFIG = { 'host': os.getenv('WP_DB_HOST', 'localhost'), 'port': int(os.getenv('WP_DB_PORT', 3306)), 'user': os.getenv('WP_DB_USER', 'wordpress'), 'password': os.getenv('WP_DB_PASSWORD', ''), 'database': os.getenv('WP_DB_NAME', 'wordpress'), } SALEOR_DB_CONFIG = { 'host': os.getenv('SALEOR_DB_HOST', 'localhost'), 'port': int(os.getenv('SALEOR_DB_PORT', 5432)), 'user': os.getenv('SALEOR_DB_USER', 'saleor'), 'password': os.getenv('SALEOR_DB_PASSWORD', ''), 'database': os.getenv('SALEOR_DB_NAME', 'saleor'), } ORDER_STATUS_MAP = { 'wc-pending': 'UNCONFIRMED', 'wc-processing': 'UNFULFILLED', 'wc-on-hold': 'UNCONFIRMED', 'wc-completed': 'FULFILLED', 'wc-cancelled': 'CANCELED', 'wc-refunded': 'REFUNDED', 'wc-failed': 'CANCELED', } @dataclass class GuestCustomer: """Customer derived from order data""" email: str first_name: str last_name: str phone: Optional[str] orders_count: int total_spent: float first_order_date: datetime last_order_date: datetime billing_address: Optional[Dict] @dataclass class GuestOrder: """Order with embedded customer data""" wc_order_id: int order_number: str status: str date_created: datetime date_modified: datetime customer_email: str customer_first_name: str customer_last_name: str customer_phone: Optional[str] total: float subtotal: float tax: float shipping: float currency: str payment_method: str payment_method_title: str transaction_id: Optional[str] billing_address: Dict shipping_address: Dict customer_note: str shipping_method: str items: List[Dict] class GuestOrderExporter: """Export orders from WooCommerce (guest checkout only)""" def __init__(self, wp_db_config: Dict): try: import pymysql self.conn = pymysql.connect( host=wp_db_config['host'], port=wp_db_config['port'], user=wp_db_config['user'], password=wp_db_config['password'], database=wp_db_config['database'], cursorclass=pymysql.cursors.DictCursor ) except ImportError: raise ImportError("pymysql required. Install: pip install pymysql") def get_unique_customers(self) -> List[GuestCustomer]: """Extract unique customers from order billing data""" query = """ SELECT meta_email.meta_value as email, MAX(meta_first.meta_value) as first_name, MAX(meta_last.meta_value) as last_name, MAX(meta_phone.meta_value) as phone, COUNT(DISTINCT p.ID) as orders_count, SUM(CAST(COALESCE(meta_total.meta_value, 0) AS DECIMAL(12,2))) as total_spent, MIN(p.post_date) as first_order_date, MAX(p.post_date) as last_order_date FROM wp_posts p JOIN wp_postmeta meta_email ON p.ID = meta_email.post_id AND meta_email.meta_key = '_billing_email' LEFT JOIN wp_postmeta meta_first ON p.ID = meta_first.post_id AND meta_first.meta_key = '_billing_first_name' LEFT JOIN wp_postmeta meta_last ON p.ID = meta_last.post_id AND meta_last.meta_key = '_billing_last_name' LEFT JOIN wp_postmeta meta_phone ON p.ID = meta_phone.post_id AND meta_phone.meta_key = '_billing_phone' LEFT JOIN wp_postmeta meta_total ON p.ID = meta_total.post_id AND meta_total.meta_key = '_order_total' WHERE p.post_type = 'shop_order' AND meta_email.meta_value IS NOT NULL AND meta_email.meta_value != '' GROUP BY meta_email.meta_value HAVING meta_email.meta_value LIKE '%@%' ORDER BY orders_count DESC """ with self.conn.cursor() as cursor: cursor.execute(query) rows = cursor.fetchall() customers = [] for row in rows: # Get address from most recent order address = self._get_latest_address(row['email']) customer = GuestCustomer( email=row['email'], first_name=row['first_name'] or '', last_name=row['last_name'] or '', phone=row['phone'], orders_count=row['orders_count'], total_spent=float(row['total_spent'] or 0), first_order_date=row['first_order_date'], last_order_date=row['last_order_date'], billing_address=address ) customers.append(customer) return customers def _get_latest_address(self, email: str) -> Optional[Dict]: """Get the most recent address for an email""" query = """ SELECT p.ID as order_id, MAX(CASE WHEN pm.meta_key = '_billing_first_name' THEN pm.meta_value END) as first_name, MAX(CASE WHEN pm.meta_key = '_billing_last_name' THEN pm.meta_value END) as last_name, MAX(CASE WHEN pm.meta_key = '_billing_company' THEN pm.meta_value END) as company, MAX(CASE WHEN pm.meta_key = '_billing_address_1' THEN pm.meta_value END) as address_1, MAX(CASE WHEN pm.meta_key = '_billing_address_2' THEN pm.meta_value END) as address_2, MAX(CASE WHEN pm.meta_key = '_billing_city' THEN pm.meta_value END) as city, MAX(CASE WHEN pm.meta_key = '_billing_postcode' THEN pm.meta_value END) as postcode, MAX(CASE WHEN pm.meta_key = '_billing_country' THEN pm.meta_value END) as country, MAX(CASE WHEN pm.meta_key = '_billing_phone' THEN pm.meta_value END) as phone FROM wp_posts p JOIN wp_postmeta pm_email ON p.ID = pm_email.post_id AND pm_email.meta_key = '_billing_email' AND pm_email.meta_value = %s LEFT JOIN wp_postmeta pm ON p.ID = pm.post_id WHERE p.post_type = 'shop_order' GROUP BY p.ID ORDER BY p.post_date DESC LIMIT 1 """ with self.conn.cursor() as cursor: cursor.execute(query, (email,)) row = cursor.fetchone() if not row: return None return { 'first_name': row['first_name'] or '', 'last_name': row['last_name'] or '', 'company_name': row['company'] or '', 'street_address_1': row['address_1'] or '', 'street_address_2': row['address_2'] or '', 'city': row['city'] or '', 'postal_code': row['postcode'] or '', 'country': row['country'] or 'RS', 'phone': row['phone'] or '', } def get_orders(self, limit: Optional[int] = None, status: Optional[str] = None, email: Optional[str] = None) -> List[GuestOrder]: """Fetch orders with embedded customer data""" query = """ SELECT p.ID as wc_order_id, p.post_date as date_created, p.post_modified as date_modified, p.post_status as status, meta_total.meta_value as total, meta_subtotal.meta_value as subtotal, meta_tax.meta_value as tax, meta_shipping.meta_value as shipping, meta_currency.meta_value as currency, meta_email.meta_value as customer_email, meta_first.meta_value as customer_first_name, meta_last.meta_value as customer_last_name, meta_phone.meta_value as customer_phone, meta_payment_method.meta_value as payment_method, meta_payment_title.meta_value as payment_method_title, meta_transaction_id.meta_value as transaction_id, meta_shipping_method.meta_value as shipping_method, meta_customer_note.meta_value as customer_note FROM wp_posts p LEFT JOIN wp_postmeta meta_total ON p.ID = meta_total.post_id AND meta_total.meta_key = '_order_total' LEFT JOIN wp_postmeta meta_subtotal ON p.ID = meta_subtotal.post_id AND meta_subtotal.meta_key = '_order_subtotal' LEFT JOIN wp_postmeta meta_tax ON p.ID = meta_tax.post_id AND meta_tax.meta_key = '_order_tax' LEFT JOIN wp_postmeta meta_shipping ON p.ID = meta_shipping.post_id AND meta_shipping.meta_key = '_order_shipping' LEFT JOIN wp_postmeta meta_currency ON p.ID = meta_currency.post_id AND meta_currency.meta_key = '_order_currency' LEFT JOIN wp_postmeta meta_email ON p.ID = meta_email.post_id AND meta_email.meta_key = '_billing_email' LEFT JOIN wp_postmeta meta_first ON p.ID = meta_first.post_id AND meta_first.meta_key = '_billing_first_name' LEFT JOIN wp_postmeta meta_last ON p.ID = meta_last.post_id AND meta_last.meta_key = '_billing_last_name' LEFT JOIN wp_postmeta meta_phone ON p.ID = meta_phone.post_id AND meta_phone.meta_key = '_billing_phone' LEFT JOIN wp_postmeta meta_payment_method ON p.ID = meta_payment_method.post_id AND meta_payment_method.meta_key = '_payment_method' LEFT JOIN wp_postmeta meta_payment_title ON p.ID = meta_payment_title.post_id AND meta_payment_title.meta_key = '_payment_method_title' LEFT JOIN wp_postmeta meta_transaction_id ON p.ID = meta_transaction_id.post_id AND meta_transaction_id.meta_key = '_transaction_id' LEFT JOIN wp_postmeta meta_shipping_method ON p.ID = meta_shipping_method.post_id AND meta_shipping_method.meta_key = '_shipping_method' LEFT JOIN wp_postmeta meta_customer_note ON p.ID = meta_customer_note.post_id AND meta_customer_note.meta_key = 'customer_note' WHERE p.post_type = 'shop_order' """ params = [] if status: query += " AND p.post_status = %s" params.append(status) if email: query += " AND meta_email.meta_value = %s" params.append(email) query += " ORDER BY p.post_date DESC" if limit: query += f" LIMIT {limit}" with self.conn.cursor() as cursor: cursor.execute(query, params) rows = cursor.fetchall() orders = [] for row in rows: # Get full addresses for this order billing = self._get_order_address(row['wc_order_id'], 'billing') shipping = self._get_order_address(row['wc_order_id'], 'shipping') items = self._get_order_items(row['wc_order_id']) order = GuestOrder( wc_order_id=row['wc_order_id'], order_number=f"WC-{row['wc_order_id']}", status=row['status'], date_created=row['date_created'], date_modified=row['date_modified'], customer_email=row['customer_email'] or '', customer_first_name=row['customer_first_name'] or '', customer_last_name=row['customer_last_name'] or '', customer_phone=row['customer_phone'], total=float(row['total'] or 0) * 100, # Convert to cents subtotal=float(row['subtotal'] or 0) * 100, tax=float(row['tax'] or 0) * 100, shipping=float(row['shipping'] or 0) * 100, currency=row['currency'] or 'RSD', payment_method=row['payment_method'] or '', payment_method_title=row['payment_method_title'] or '', transaction_id=row['transaction_id'], shipping_method=row['shipping_method'] or '', customer_note=row['customer_note'] or '', billing_address=billing or self._empty_address(), shipping_address=shipping or billing or self._empty_address(), items=items ) orders.append(order) return orders def _get_order_address(self, order_id: int, prefix: str) -> Optional[Dict]: """Fetch order address from postmeta""" query = f""" SELECT MAX(CASE WHEN meta_key = '_{prefix}_first_name' THEN meta_value END) as first_name, MAX(CASE WHEN meta_key = '_{prefix}_last_name' THEN meta_value END) as last_name, MAX(CASE WHEN meta_key = '_{prefix}_company' THEN meta_value END) as company, MAX(CASE WHEN meta_key = '_{prefix}_address_1' THEN meta_value END) as address_1, MAX(CASE WHEN meta_key = '_{prefix}_address_2' THEN meta_value END) as address_2, MAX(CASE WHEN meta_key = '_{prefix}_city' THEN meta_value END) as city, MAX(CASE WHEN meta_key = '_{prefix}_postcode' THEN meta_value END) as postcode, MAX(CASE WHEN meta_key = '_{prefix}_country' THEN meta_value END) as country, MAX(CASE WHEN meta_key = '_{prefix}_phone' THEN meta_value END) as phone FROM wp_postmeta WHERE post_id = %s """ with self.conn.cursor() as cursor: cursor.execute(query, (order_id,)) row = cursor.fetchone() if not row or not row['first_name']: return None return { 'first_name': row['first_name'] or '', 'last_name': row['last_name'] or '', 'company_name': row['company'] or '', 'street_address_1': row['address_1'] or '', 'street_address_2': row['address_2'] or '', 'city': row['city'] or '', 'postal_code': row['postcode'] or '', 'country': row['country'] or 'RS', 'phone': row['phone'] or '', } def _empty_address(self) -> Dict: """Return empty address structure""" return { 'first_name': '', 'last_name': '', 'company_name': '', 'street_address_1': '', 'street_address_2': '', 'city': '', 'postal_code': '', 'country': 'RS', 'phone': '' } def _get_order_items(self, order_id: int) -> List[Dict]: """Fetch order line items""" query = """ SELECT oi.order_item_name as name, meta_product_id.meta_value as product_id, meta_sku.meta_value as sku, meta_qty.meta_value as quantity, meta_subtotal.meta_value as subtotal, meta_total.meta_value as total, meta_tax.meta_value as tax FROM wp_woocommerce_order_items oi LEFT JOIN wp_woocommerce_order_itemmeta meta_product_id ON oi.order_item_id = meta_product_id.order_item_id AND meta_product_id.meta_key = '_product_id' LEFT JOIN wp_woocommerce_order_itemmeta meta_sku ON oi.order_item_id = meta_sku.order_item_id AND meta_sku.meta_key = '_sku' LEFT JOIN wp_woocommerce_order_itemmeta meta_qty ON oi.order_item_id = meta_qty.order_item_id AND meta_qty.meta_key = '_qty' LEFT JOIN wp_woocommerce_order_itemmeta meta_subtotal ON oi.order_item_id = meta_subtotal.order_item_id AND meta_subtotal.meta_key = '_line_subtotal' LEFT JOIN wp_woocommerce_order_itemmeta meta_total ON oi.order_item_id = meta_total.order_item_id AND meta_total.meta_key = '_line_total' LEFT JOIN wp_woocommerce_order_itemmeta meta_tax ON oi.order_item_id = meta_tax.order_item_id AND meta_tax.meta_key = '_line_tax' WHERE oi.order_id = %s AND oi.order_item_type = 'line_item' """ with self.conn.cursor() as cursor: cursor.execute(query, (order_id,)) rows = cursor.fetchall() items = [] for row in rows: qty = int(row['quantity'] or 1) items.append({ 'product_id': int(row['product_id'] or 0), 'name': row['name'] or '', 'sku': row['sku'] or '', 'quantity': qty, 'subtotal': float(row['subtotal'] or 0) * 100, 'total': float(row['total'] or 0) * 100, 'tax': float(row['tax'] or 0) * 100, }) return items class GuestSaleorImporter: """Import guest orders into Saleor""" def __init__(self, saleor_db_config: Dict): self.conn = psycopg2.connect( host=saleor_db_config['host'], port=saleor_db_config['port'], user=saleor_db_config['user'], password=saleor_db_config['password'], database=saleor_db_config['database'] ) self.email_to_user_id: Dict[str, uuid.UUID] = {} self._ensure_tables() self._load_existing_mappings() def _ensure_tables(self): """Create mapping tables""" with self.conn.cursor() as cursor: cursor.execute(""" CREATE TABLE IF NOT EXISTS wc_guest_customer_mapping ( email VARCHAR(255) PRIMARY KEY, saleor_user_id UUID NOT NULL, first_name VARCHAR(255), last_name VARCHAR(255), phone VARCHAR(255), orders_count INTEGER DEFAULT 0, total_spent DECIMAL(12,2) DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() ); """) cursor.execute(""" CREATE TABLE IF NOT EXISTS wc_order_mapping ( wc_order_id BIGINT PRIMARY KEY, saleor_order_id UUID NOT NULL, customer_email VARCHAR(255), migrated_at TIMESTAMP DEFAULT NOW() ); """) self.conn.commit() def _load_existing_mappings(self): """Load existing email→user mappings""" with self.conn.cursor() as cursor: cursor.execute("SELECT email, saleor_user_id FROM wc_guest_customer_mapping") for row in cursor.fetchall(): self.email_to_user_id[row[0]] = row[1] def get_channel_id(self) -> uuid.UUID: with self.conn.cursor() as cursor: cursor.execute("SELECT id FROM channel_channel WHERE slug = 'default-channel' LIMIT 1") return cursor.fetchone()[0] def create_customer_from_email(self, customer: GuestCustomer, dry_run: bool = False) -> Optional[uuid.UUID]: """Create a Saleor user from order-derived customer data""" if customer.email in self.email_to_user_id: return self.email_to_user_id[customer.email] new_user_id = uuid.uuid4() if dry_run: print(f" [DRY RUN] Would create user: {customer.email}") return new_user_id with self.conn.cursor() as cursor: # Create user with unusable password cursor.execute(""" INSERT INTO account_user ( id, email, first_name, last_name, is_staff, is_active, date_joined, last_login, password ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( new_user_id, customer.email, customer.first_name, customer.last_name, False, True, customer.first_order_date, None, '!' )) # Create address if available if customer.billing_address: addr_id = uuid.uuid4() cursor.execute(""" INSERT INTO account_address ( id, first_name, last_name, company_name, street_address_1, street_address_2, city, postal_code, country, phone ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( addr_id, customer.billing_address['first_name'], customer.billing_address['last_name'], customer.billing_address['company_name'], customer.billing_address['street_address_1'], customer.billing_address['street_address_2'], customer.billing_address['city'], customer.billing_address['postal_code'], customer.billing_address['country'], customer.billing_address['phone'] )) cursor.execute(""" INSERT INTO account_user_addresses (user_id, address_id) VALUES (%s, %s) """, (new_user_id, addr_id)) cursor.execute(""" UPDATE account_user SET default_billing_address_id = %s, default_shipping_address_id = %s WHERE id = %s """, (addr_id, addr_id, new_user_id)) # Record mapping cursor.execute(""" INSERT INTO wc_guest_customer_mapping (email, saleor_user_id, first_name, last_name, phone, orders_count, total_spent) VALUES (%s, %s, %s, %s, %s, %s, %s) """, (customer.email, new_user_id, customer.first_name, customer.last_name, customer.phone, customer.orders_count, customer.total_spent)) self.conn.commit() self.email_to_user_id[customer.email] = new_user_id print(f" Created user: {customer.email} ({customer.orders_count} orders)") return new_user_id def import_order(self, order: GuestOrder, mode: str = 'hybrid', dry_run: bool = False) -> Optional[uuid.UUID]: """Import an order mode: 'guest' = no user account, 'hybrid' = link to created user """ # Check if already migrated with self.conn.cursor() as cursor: cursor.execute("SELECT saleor_order_id FROM wc_order_mapping WHERE wc_order_id = %s", (order.wc_order_id,)) if cursor.fetchone(): print(f" Order {order.order_number} already migrated, skipping") return None new_order_id = uuid.uuid4() channel_id = self.get_channel_id() saleor_status = ORDER_STATUS_MAP.get(order.status, 'UNCONFIRMED') # Get or create user ID user_id = None if mode == 'hybrid' and order.customer_email: user_id = self.email_to_user_id.get(order.customer_email) if dry_run: print(f" [DRY RUN] Would create order: {order.order_number}") return new_order_id with self.conn.cursor() as cursor: # Create billing address record billing_id = uuid.uuid4() cursor.execute(""" INSERT INTO order_orderbillingaddress ( id, first_name, last_name, company_name, street_address_1, street_address_2, city, postal_code, country, phone ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( billing_id, order.billing_address['first_name'], order.billing_address['last_name'], order.billing_address['company_name'], order.billing_address['street_address_1'], order.billing_address['street_address_2'], order.billing_address['city'], order.billing_address['postal_code'], order.billing_address['country'], order.billing_address['phone'] )) # Create shipping address record shipping_id = uuid.uuid4() cursor.execute(""" INSERT INTO order_ordershippingaddress ( id, first_name, last_name, company_name, street_address_1, street_address_2, city, postal_code, country, phone ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( shipping_id, order.shipping_address['first_name'], order.shipping_address['last_name'], order.shipping_address['company_name'], order.shipping_address['street_address_1'], order.shipping_address['street_address_2'], order.shipping_address['city'], order.shipping_address['postal_code'], order.shipping_address['country'], order.shipping_address['phone'] )) # Insert order cursor.execute(""" INSERT INTO order_order ( id, created_at, updated_at, status, user_email, user_id, currency, total_gross_amount, total_net_amount, shipping_price_gross_amount, shipping_price_net_amount, shipping_method_name, channel_id, billing_address_id, shipping_address_id, billing_address, shipping_address, metadata, private_metadata, origin, should_refresh_prices, tax_exemption, discount_amount, display_gross_prices, customer_note ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( new_order_id, order.date_created, order.date_modified, saleor_status, order.customer_email, user_id, order.currency, order.total, order.subtotal, order.shipping, order.shipping, order.shipping_method, channel_id, billing_id, shipping_id, json.dumps(order.billing_address), json.dumps(order.shipping_address), json.dumps({'woo_order_id': order.wc_order_id, 'guest_checkout': True}), '{}', 'BULK_CREATE', False, False, 0.0, True, order.customer_note )) # Insert order lines for item in order.items: # Look up variant by SKU cursor.execute("SELECT id FROM product_productvariant WHERE sku = %s", (item['sku'],)) variant_row = cursor.fetchone() variant_id = variant_row[0] if variant_row else None qty = item['quantity'] unit_net = item['subtotal'] / qty if qty else 0 unit_gross = (item['subtotal'] + item['tax']) / qty if qty else 0 cursor.execute(""" INSERT INTO order_orderline ( id, order_id, product_name, product_sku, quantity, currency, unit_price_net_amount, unit_price_gross_amount, total_price_net_amount, total_price_gross_amount, unit_discount_amount, unit_discount_type, tax_rate, is_shipping_required, variant_id, created_at ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( uuid.uuid4(), new_order_id, item['name'], item['sku'], qty, order.currency, unit_net, unit_gross, item['subtotal'], item['subtotal'] + item['tax'], 0.0, 'FIXED', '0.15', True, variant_id, order.date_created )) # Record mapping cursor.execute(""" INSERT INTO wc_order_mapping (wc_order_id, saleor_order_id, customer_email) VALUES (%s, %s, %s) """, (order.wc_order_id, new_order_id, order.customer_email)) self.conn.commit() user_info = f" (user: {user_id})" if user_id else " (guest)" print(f" Created order: {order.order_number}{user_info}") return new_order_id def main(): parser = argparse.ArgumentParser(description='Migrate WooCommerce Guest Orders to Saleor') parser.add_argument('--customers', action='store_true', help='Create customer accounts from unique emails') parser.add_argument('--orders', action='store_true', help='Migrate orders') parser.add_argument('--mode', choices=['guest', 'hybrid'], default='hybrid', help='guest=orders only, hybrid=create customers and link orders') parser.add_argument('--dry-run', action='store_true', help='Preview changes') parser.add_argument('--limit', type=int, help='Limit records') parser.add_argument('--status', type=str, help='Filter by order status') args = parser.parse_args() if not args.customers and not args.orders: print("Please specify --customers and/or --orders") parser.print_help() sys.exit(1) print("=== WooCommerce Guest Orders to Saleor Migration ===\n") # Connect print("Connecting to databases...") try: exporter = GuestOrderExporter(WP_DB_CONFIG) importer = GuestSaleorImporter(SALEOR_DB_CONFIG) print("Connected!\n") except Exception as e: print(f"Connection failed: {e}") sys.exit(1) # Create customers first (if hybrid mode) if args.customers or (args.orders and args.mode == 'hybrid'): print("Extracting unique customers from orders...") customers = exporter.get_unique_customers() print(f"Found {len(customers)} unique customers\n") print("Creating customer accounts...") for i, customer in enumerate(customers, 1): print(f"[{i}/{len(customers)}] {customer.email}") try: importer.create_customer_from_email(customer, dry_run=args.dry_run) except Exception as e: print(f" ERROR: {e}") print(f"\nCustomer creation {'preview' if args.dry_run else 'complete'}!\n") # Migrate orders if args.orders: print("Fetching orders...") orders = exporter.get_orders(limit=args.limit, status=args.status) print(f"Found {len(orders)} orders\n") print(f"Migrating orders (mode: {args.mode})...") for i, order in enumerate(orders, 1): print(f"[{i}/{len(orders)}] {order.order_number} - {order.customer_email}") try: importer.import_order(order, mode=args.mode, dry_run=args.dry_run) except Exception as e: print(f" ERROR: {e}") print(f"\nOrder migration {'preview' if args.dry_run else 'complete'}!\n") print("=== Summary ===") print(f"Customers: {len(importer.email_to_user_id)}") if __name__ == '__main__': main()