#!/usr/bin/env python3 """ COMPLETE WooCommerce to Saleor Migration ======================================== Migrates: 1. ALL 4,886 WordPress users (including 3,700+ who never ordered = PROSPECTS) 2. ALL 1,786 orders linked to customers by email Principles: - Every WP user becomes a Saleor customer (prospects for marketing) - Orders linked by email (catches "guest" checkouts too) - Pending/processing/completed = FULFILLED (COD collected) - Cancelled = CANCELLED (but still linked to customer) """ import os import sys import json import uuid import argparse from datetime import datetime from typing import Dict, List, Optional, Set, Tuple from dataclasses import dataclass from collections import defaultdict import psycopg2 WP_DB_CONFIG = { 'host': os.getenv('WP_DB_HOST', '10.43.245.156'), 'port': int(os.getenv('WP_DB_PORT', 3306)), 'user': os.getenv('WP_DB_USER', 'DUjqYuqsYvaGUFV4'), 'password': os.getenv('WP_DB_PASSWORD', 'voP0UzecALE0WRNJQcTCf0STMcxIiX99'), 'database': os.getenv('WP_DB_NAME', 'wordpress'), } SALEOR_DB_CONFIG = { 'host': os.getenv('SALEOR_DB_HOST', '10.43.42.251'), 'port': int(os.getenv('SALEOR_DB_PORT', 5432)), 'user': os.getenv('SALEOR_DB_USER', 'saleor'), 'password': os.getenv('SALEOR_DB_PASSWORD', 'saleor123'), 'database': os.getenv('SALEOR_DB_NAME', 'saleor'), } ORDER_STATUS_MAP = { 'wc-pending': 'FULFILLED', 'wc-processing': 'FULFILLED', 'wc-on-hold': 'FULFILLED', 'wc-completed': 'FULFILLED', 'wc-cancelled': 'CANCELED', 'wc-refunded': 'CANCELED', 'wc-failed': 'CANCELED', } NON_CANCELLED_STATUSES = ['wc-completed', 'wc-pending', 'wc-processing', 'wc-on-hold'] @dataclass class Customer: """Customer from WP users OR order billing data""" source: str # 'wp_user' or 'order_email' email: str first_name: str last_name: str phone: Optional[str] date_registered: datetime billing_address: Optional[Dict] # Order stats (from joined data) total_orders: int = 0 cancelled_orders: int = 0 completed_orders: int = 0 total_spent: float = 0.0 first_order_date: Optional[datetime] = None last_order_date: Optional[datetime] = None @property def segment(self) -> str: if self.completed_orders >= 4: return "VIP" elif self.completed_orders >= 2: return "REPEAT" elif self.completed_orders == 1: return "ONE_TIME" elif self.total_orders > 0: return "CANCELLED_ONLY" else: return "PROSPECT" @dataclass class OrderToMigrate: """Order 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 billing_address: Dict shipping_address: Dict customer_note: str shipping_method: str items: List[Dict] is_paid: bool class CompleteExporter: """Export all users and orders""" def __init__(self, wp_db_config: Dict): 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 ) def get_all_customers(self) -> Dict[str, Customer]: """Get ALL customers: WP users + order emails merged""" customers: Dict[str, Customer] = {} # Step 1: Get all WordPress users (these are prospects if no orders) with self.conn.cursor() as cursor: cursor.execute(""" SELECT u.ID as wp_user_id, u.user_email as email, u.user_registered as date_registered, um_first.meta_value as first_name, um_last.meta_value as last_name, um_phone.meta_value as phone FROM wp_users u LEFT JOIN wp_usermeta um_first ON u.ID = um_first.user_id AND um_first.meta_key = 'first_name' LEFT JOIN wp_usermeta um_last ON u.ID = um_last.user_id AND um_last.meta_key = 'last_name' LEFT JOIN wp_usermeta um_phone ON u.ID = um_phone.user_id AND um_phone.meta_key = 'billing_phone' WHERE u.user_email IS NOT NULL AND u.user_email != '' """) for row in cursor.fetchall(): email = row['email'].lower().strip() address = self._get_user_address(row['wp_user_id']) customers[email] = Customer( source='wp_user', email=email, first_name=row['first_name'] or '', last_name=row['last_name'] or '', phone=row['phone'], date_registered=row['date_registered'], billing_address=address, total_orders=0, cancelled_orders=0, completed_orders=0, total_spent=0.0 ) # Step 2: Get order stats for all customers (including those not in WP users) with self.conn.cursor() as cursor: cursor.execute(""" SELECT LOWER(TRIM(pm_email.meta_value)) as email, MAX(pm_first.meta_value) as first_name, MAX(pm_last.meta_value) as last_name, MAX(pm_phone.meta_value) as phone, COUNT(*) as total_orders, SUM(CASE WHEN p.post_status = 'wc-cancelled' THEN 1 ELSE 0 END) as cancelled_orders, SUM(CASE WHEN p.post_status != 'wc-cancelled' THEN 1 ELSE 0 END) as completed_orders, SUM(CASE WHEN p.post_status != 'wc-cancelled' THEN CAST(COALESCE(pm_total.meta_value, 0) AS DECIMAL(12,2)) ELSE 0 END) 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 pm_email ON p.ID = pm_email.post_id AND pm_email.meta_key = '_billing_email' LEFT JOIN wp_postmeta pm_first ON p.ID = pm_first.post_id AND pm_first.meta_key = '_billing_first_name' LEFT JOIN wp_postmeta pm_last ON p.ID = pm_last.post_id AND pm_last.meta_key = '_billing_last_name' LEFT JOIN wp_postmeta pm_phone ON p.ID = pm_phone.post_id AND pm_phone.meta_key = '_billing_phone' LEFT JOIN wp_postmeta pm_total ON p.ID = pm_total.post_id AND pm_total.meta_key = '_order_total' WHERE p.post_type = 'shop_order' AND pm_email.meta_value IS NOT NULL AND pm_email.meta_value != '' GROUP BY LOWER(TRIM(pm_email.meta_value)) """) for row in cursor.fetchall(): email = row['email'] if email in customers: # Update existing WP user with order stats existing = customers[email] existing.total_orders = row['total_orders'] existing.cancelled_orders = row['cancelled_orders'] existing.completed_orders = row['completed_orders'] existing.total_spent = float(row['total_spent'] or 0) existing.first_order_date = row['first_order_date'] existing.last_order_date = row['last_order_date'] # Use order data for name/phone if WP data is empty if not existing.first_name: existing.first_name = row['first_name'] or '' if not existing.last_name: existing.last_name = row['last_name'] or '' if not existing.phone: existing.phone = row['phone'] else: # New customer from order (guest checkout) address = { 'first_name': row['first_name'] or '', 'last_name': row['last_name'] or '', 'company_name': '', 'street_address_1': '', 'street_address_2': '', 'city': '', 'postal_code': '', 'country': 'RS', 'phone': row['phone'] or '', } customers[email] = Customer( source='order_email', email=email, first_name=row['first_name'] or '', last_name=row['last_name'] or '', phone=row['phone'], date_registered=row['first_order_date'] or datetime.now(), billing_address=address, total_orders=row['total_orders'], cancelled_orders=row['cancelled_orders'], completed_orders=row['completed_orders'], total_spent=float(row['total_spent'] or 0), first_order_date=row['first_order_date'], last_order_date=row['last_order_date'] ) return customers def _get_user_address(self, user_id: int) -> Optional[Dict]: """Get address from usermeta or latest order""" # Try usermeta first with self.conn.cursor() as cursor: cursor.execute(""" SELECT MAX(CASE WHEN meta_key = 'billing_first_name' THEN meta_value END) as first_name, MAX(CASE WHEN meta_key = 'billing_last_name' THEN meta_value END) as last_name, MAX(CASE WHEN meta_key = 'billing_address_1' THEN meta_value END) as address_1, MAX(CASE WHEN meta_key = 'billing_address_2' THEN meta_value END) as address_2, MAX(CASE WHEN meta_key = 'billing_city' THEN meta_value END) as city, MAX(CASE WHEN meta_key = 'billing_postcode' THEN meta_value END) as postcode, MAX(CASE WHEN meta_key = 'billing_country' THEN meta_value END) as country, MAX(CASE WHEN meta_key = 'billing_phone' THEN meta_value END) as phone FROM wp_usermeta WHERE user_id = %s """, (user_id,)) row = cursor.fetchone() if row and row['first_name']: return { 'first_name': row['first_name'] or '', 'last_name': row['last_name'] or '', 'company_name': '', '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 '', } return None def get_all_orders(self, limit: Optional[int] = None) -> List[OrderToMigrate]: """Get ALL orders""" 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, LOWER(TRIM(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_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_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' ORDER BY p.post_date DESC """ if limit: query += f" LIMIT {limit}" with self.conn.cursor() as cursor: cursor.execute(query) rows = cursor.fetchall() orders = [] for row in rows: billing = self._get_order_address(row['wc_order_id'], 'billing') shipping = self._get_order_address(row['wc_order_id'], 'shipping') items = self._get_items(row['wc_order_id']) orders.append(OrderToMigrate( 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, 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', billing_address=billing or self._empty_address(), shipping_address=shipping or billing or self._empty_address(), shipping_method=row['shipping_method'] or 'Cash on Delivery', customer_note=row['customer_note'] or '', items=items, is_paid=row['status'] in NON_CANCELLED_STATUSES )) return orders def _get_order_address(self, order_id: int, prefix: str) -> Optional[Dict]: 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 { 'first_name': '', 'last_name': '', 'company_name': '', 'street_address_1': '', 'street_address_2': '', 'city': '', 'postal_code': '', 'country': 'RS', 'phone': '' } def _get_items(self, order_id: int) -> List[Dict]: query = """ SELECT oi.order_item_name as name, 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_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({ '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 CompleteImporter: """Import customers and orders""" 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_mappings() def _ensure_tables(self): with self.conn.cursor() as cursor: cursor.execute(""" CREATE TABLE IF NOT EXISTS wc_complete_mapping ( email VARCHAR(255) PRIMARY KEY, saleor_user_id UUID NOT NULL, source VARCHAR(50) NOT NULL, segment VARCHAR(50) NOT NULL, total_orders INTEGER DEFAULT 0, completed_orders INTEGER DEFAULT 0, cancelled_orders INTEGER DEFAULT 0, total_spent DECIMAL(12,2) DEFAULT 0, migrated_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_mappings(self): with self.conn.cursor() as cursor: cursor.execute("SELECT email, saleor_user_id FROM wc_complete_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 import_customer(self, customer: Customer, dry_run: bool = False) -> uuid.UUID: """Create a customer""" if customer.email in self.email_to_user_id: return self.email_to_user_id[customer.email] user_id = uuid.uuid4() if dry_run: status = "✅" if customer.completed_orders > 0 else "👤" print(f" {status} [{customer.segment}] {customer.email} ({customer.source}, {customer.completed_orders} orders)") return user_id with self.conn.cursor() as cursor: metadata = { 'source': customer.source, 'segment': customer.segment, 'total_orders': customer.total_orders, 'completed_orders': customer.completed_orders, 'cancelled_orders': customer.cancelled_orders, 'total_spent': float(customer.total_spent) if customer.total_spent else 0.0, } cursor.execute(""" INSERT INTO account_user (id, email, first_name, last_name, is_staff, is_active, date_joined, password, metadata) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( str(user_id), customer.email, customer.first_name, customer.last_name, False, True, customer.date_registered, '!', json.dumps(metadata) )) 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) """, ( str(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.phone or '' )) cursor.execute(""" INSERT INTO account_user_addresses (user_id, address_id) VALUES (%s, %s) """, (str(user_id), str(addr_id))) cursor.execute(""" UPDATE account_user SET default_billing_address_id = %s, default_shipping_address_id = %s WHERE id = %s """, (str(addr_id), str(addr_id), str(user_id))) cursor.execute(""" INSERT INTO wc_complete_mapping (email, saleor_user_id, source, segment, total_orders, completed_orders, cancelled_orders, total_spent) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) """, ( customer.email, str(user_id), customer.source, customer.segment, customer.total_orders, customer.completed_orders, customer.cancelled_orders, float(customer.total_spent) if customer.total_spent else 0.0 )) self.conn.commit() self.email_to_user_id[customer.email] = user_id return user_id def import_order(self, order: OrderToMigrate, dry_run: bool = False) -> Optional[uuid.UUID]: """Import an order""" 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(): return None order_id = uuid.uuid4() channel_id = self.get_channel_id() saleor_status = ORDER_STATUS_MAP.get(order.status, 'UNCONFIRMED') # Get user by email user_id = self.email_to_user_id.get(order.customer_email) if dry_run: marker = "✅" if order.is_paid else "❌" linked = "→" if user_id else "⚠" print(f" {order.order_number} {marker} {linked} {order.customer_email}") return order_id with self.conn.cursor() as cursor: # Create billing address bill_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) """, (str(bill_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 ship_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) """, (str(ship_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, 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) """, ( order_id, order.date_created, order.date_modified, saleor_status, order.customer_email, str(user_id) if user_id else None, order.currency, order.total, order.subtotal, order.shipping, order.shipping, order.shipping_method, str(channel_id), str(bill_id), str(ship_id), json.dumps(order.billing_address), json.dumps(order.shipping_address), json.dumps({ 'woo_order_id': order.wc_order_id, 'cod_payment': True, 'payment_collected': order.is_paid, 'original_status': order.status }), 'BULK_CREATE', False, False, 0.0, True, order.customer_note )) # Insert order lines for item in order.items: cursor.execute("SELECT id FROM product_productvariant WHERE sku = %s", (item['sku'],)) variant = cursor.fetchone() variant_id = variant[0] if variant 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) """, (str(uuid.uuid4()), str(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)) # Create payment record for paid orders if order.is_paid: cursor.execute(""" INSERT INTO payment_payment ( id, gateway, is_active, to_confirm, order_id, total, captured_amount, currency, charge_status, partial, modified_at, created_at ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, (str(uuid.uuid4()), 'mirumee.payments.dummy', False, False, str(order_id), order.total, order.total, order.currency, 'FULLY_CHARGED', False, order.date_modified, 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, str(order_id), order.customer_email)) self.conn.commit() return order_id def main(): parser = argparse.ArgumentParser( description='Complete WooCommerce Migration - ALL 4,886 users + ALL 1,786 orders' ) parser.add_argument('--customers', action='store_true', help='Migrate ALL 4,886 WordPress users + order customers') parser.add_argument('--orders', action='store_true', help='Migrate ALL 1,786 orders') parser.add_argument('--dry-run', action='store_true', help='Preview only') parser.add_argument('--limit', type=int, help='Limit for testing') args = parser.parse_args() if not args.customers and not args.orders: parser.print_help() sys.exit(1) print("=" * 70) print("COMPLETE WOOCOMMERCE TO SALEOR MIGRATION") print("=" * 70) print() print("Scope:") print(" ✓ ALL 4,886 WordPress users (including 3,700+ prospects)") print(" ✓ ALL customers from order billing emails") print(" ✓ ALL 1,786 orders") print(" ✓ Pending/Processing = FULFILLED (COD collected)") print(" ✓ Cancelled = CANCELLED") print() print("Connecting to databases...") try: exporter = CompleteExporter(WP_DB_CONFIG) importer = CompleteImporter(SALEOR_DB_CONFIG) print("Connected!\n") except Exception as e: print(f"Failed: {e}") sys.exit(1) # Migrate customers first if args.customers: print("Fetching ALL customers (WP users + order emails)...") customers = exporter.get_all_customers() if args.limit: customers = dict(list(customers.items())[:args.limit]) print(f"Found {len(customers)} unique customers\n") # Segment breakdown segments = defaultdict(int) sources = defaultdict(int) for c in customers.values(): segments[c.segment] += 1 sources[c.source] += 1 print("Sources:") for src, count in sorted(sources.items()): print(f" {src}: {count}") print() print("Segments:") for seg, count in sorted(segments.items(), key=lambda x: -x[1]): print(f" {seg}: {count}") print() print("Creating customers...") for i, (email, customer) in enumerate(customers.items(), 1): print(f"[{i}/{len(customers)}]", end=" ") try: importer.import_customer(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 ALL orders...") orders = exporter.get_all_orders(limit=args.limit) print(f"Found {len(orders)} orders\n") paid = sum(1 for o in orders if o.is_paid) cancelled = len(orders) - paid print(f"Breakdown: {paid} fulfilled, {cancelled} cancelled\n") print("Migrating orders...") for i, order in enumerate(orders, 1): print(f"[{i}/{len(orders)}]", end=" ") try: importer.import_order(order, 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") # Summary print("=" * 70) print("SUMMARY") print("=" * 70) print(f"Customers: {len(importer.email_to_user_id)}") if args.customers: print("\nBy segment:") with importer.conn.cursor() as cursor: cursor.execute(""" SELECT segment, COUNT(*) as count, SUM(total_spent) as revenue FROM wc_complete_mapping GROUP BY segment ORDER BY count DESC """) for row in cursor.fetchall(): print(f" {row[0]}: {row[1]} ({row[2] or 0:,.0f} RSD)") if __name__ == '__main__': main()