#!/usr/bin/env python3 """ WooCommerce COMPLETE User & Order Migration to Saleor ======================================================= ASSUMPTION: For COD stores, ALL orders = fulfilled (paid) EXCEPT cancelled In early WooCommerce stores, order status tracking was inconsistent, but if an order was not cancelled, the COD payment was collected. This script treats: - wc-completed, wc-pending, wc-processing, wc-on-hold = FULFILLED (PAID) - wc-cancelled, wc-refunded, wc-failed = CANCELLED (NOT PAID) Migrates ALL WordPress users (not just customers with orders): - Customers with orders (1,172) → Active customers - Users without orders (3,714) → Leads/Prospects for reactivation Segmentation Strategy: - VIP: 4+ orders - Repeat: 2-3 orders - One-time: 1 order - Prospect: 0 orders Use cases after migration: 1. Email reactivation campaigns for prospects 2. Win-back campaigns for inactive customers 3. Welcome series for new registrations 4. Segmented marketing based on activity """ 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, field from collections import defaultdict import psycopg2 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'), } # COD Status Mapping - SIMPLIFIED # ALL orders are treated as FULFILLED (paid) EXCEPT cancelled # For COD stores: if not cancelled, payment was collected ORDER_STATUS_MAP = { 'wc-pending': 'FULFILLED', # All treated as completed 'wc-processing': 'FULFILLED', 'wc-on-hold': 'FULFILLED', 'wc-completed': 'FULFILLED', 'wc-cancelled': 'CANCELED', # Only cancelled = not paid 'wc-refunded': 'CANCELED', # Refunded = not paid 'wc-failed': 'CANCELED', } # Statuses that indicate payment was collected (for COD) # Everything EXCEPT cancelled/refunded/failed PAID_STATUSES = ['wc-completed', 'wc-pending', 'wc-processing', 'wc-on-hold'] @dataclass class WPUser: """WordPress user with activity tracking""" wp_user_id: int email: str first_name: str last_name: str date_registered: datetime phone: Optional[str] = None billing_address: Optional[Dict] = None shipping_address: Optional[Dict] = None # Activity tracking - UPDATED to count pending/processing as paid order_count: int = 0 paid_orders: int = 0 # completed + pending + processing cancelled_orders: int = 0 total_spent: float = 0.0 last_order_date: Optional[datetime] = None first_order_date: Optional[datetime] = None # Segmentation @property def segment(self) -> str: """Determine customer segment for marketing""" # Simplified: all non-cancelled orders = paid if self.paid_orders >= 4: return "VIP_CUSTOMER" elif self.paid_orders >= 2: return "REPEAT_CUSTOMER" elif self.paid_orders == 1: return "ONE_TIME" else: return "PROSPECT" @property def ltv(self) -> float: """Lifetime value in RSD""" return self.total_spent @dataclass class CODOrder: """COD Order - updated to mark pending/processing as paid""" 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 # in cents 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 # True for completed, pending, processing wp_user_id: Optional[int] = None # Link to WordPress user if registered class CompleteExporter: """Export ALL WordPress users and orders""" 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") def get_all_users_with_activity(self) -> List[WPUser]: """Get ALL WordPress users with their order activity - UPDATED""" query = """ 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, -- Order activity - count pending/processing as paid COUNT(DISTINCT p.ID) as order_count, COUNT(DISTINCT CASE WHEN p.post_status IN ('wc-completed', 'wc-pending', 'wc-processing') THEN p.ID END) as paid_orders, COUNT(DISTINCT CASE WHEN p.post_status = 'wc-cancelled' THEN p.ID END) as cancelled_orders, SUM(CASE WHEN p.post_status IN ('wc-completed', 'wc-pending', 'wc-processing') THEN CAST(COALESCE(meta_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_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' LEFT JOIN wp_postmeta pm ON pm.meta_key = '_customer_user' AND pm.meta_value = u.ID LEFT JOIN wp_posts p ON p.ID = pm.post_id AND p.post_type = 'shop_order' LEFT JOIN wp_postmeta meta_total ON p.ID = meta_total.post_id AND meta_total.meta_key = '_order_total' GROUP BY u.ID, u.user_email, u.user_registered, um_first.meta_value, um_last.meta_value, um_phone.meta_value ORDER BY u.ID """ with self.conn.cursor() as cursor: cursor.execute(query) rows = cursor.fetchall() users = [] for row in rows: # Get address from most recent order or usermeta address = self._get_user_address(row['wp_user_id']) user = WPUser( wp_user_id=row['wp_user_id'], email=row['email'], first_name=row['first_name'] or '', last_name=row['last_name'] or '', date_registered=row['date_registered'], phone=row['phone'], billing_address=address, shipping_address=address, order_count=row['order_count'] or 0, paid_orders=row['paid_orders'] or 0, cancelled_orders=row['cancelled_orders'] or 0, total_spent=float(row['total_spent'] or 0), first_order_date=row['first_order_date'], last_order_date=row['last_order_date'] ) users.append(user) return users def get_orders(self, limit: Optional[int] = None, status: Optional[str] = None) -> List[CODOrder]: """Fetch orders with user linking""" 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_shipping_method.meta_value as shipping_method, meta_customer_note.meta_value as customer_note, meta_customer_id.meta_value as wp_user_id 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' LEFT JOIN wp_postmeta meta_customer_id ON p.ID = meta_customer_id.post_id AND meta_customer_id.meta_key = '_customer_user' WHERE p.post_type = 'shop_order' """ params = [] if status: # Handle multiple statuses statuses = status.split(',') if len(statuses) == 1: query += " AND p.post_status = %s" params.append(status) else: placeholders = ','.join(['%s'] * len(statuses)) query += f" AND p.post_status IN ({placeholders})" params.extend(statuses) 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: billing = self._get_address(row['wc_order_id'], 'billing') shipping = self._get_address(row['wc_order_id'], 'shipping') items = self._get_items(row['wc_order_id']) # UPDATED: Treat pending/processing as paid is_paid = row['status'] in PAID_STATUSES wp_user_id = int(row['wp_user_id']) if row['wp_user_id'] else None order = CODOrder( 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=is_paid, wp_user_id=wp_user_id ) orders.append(order) return orders def _get_user_address(self, user_id: int) -> Optional[Dict]: """Get address from user's most recent order or usermeta""" # Try to get from most recent order first query = """ SELECT 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_postmeta pm_customer JOIN wp_posts p ON p.ID = pm_customer.post_id AND p.post_type = 'shop_order' JOIN wp_postmeta pm ON pm.post_id = p.ID WHERE pm_customer.meta_key = '_customer_user' AND pm_customer.meta_value = %s ORDER BY p.post_date DESC LIMIT 1 """ with self.conn.cursor() as cursor: cursor.execute(query, (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': 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 '', } # Fall back to usermeta query = """ 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_company' THEN meta_value END) as company, 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 """ with self.conn.cursor() as cursor: cursor.execute(query, (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': 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 '', } return None def _get_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 all users and orders with segmentation""" 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.wp_id_to_saleor_id: Dict[int, uuid.UUID] = {} self._ensure_tables() self._load_mappings() def _ensure_tables(self): """Create mapping and segmentation tables""" with self.conn.cursor() as cursor: # User mapping with segmentation data - UPDATED schema cursor.execute(""" CREATE TABLE IF NOT EXISTS wc_complete_user_mapping ( wp_user_id BIGINT PRIMARY KEY, saleor_user_id UUID NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, segment VARCHAR(50) NOT NULL, order_count INTEGER DEFAULT 0, paid_orders INTEGER DEFAULT 0, total_spent DECIMAL(12,2) DEFAULT 0, first_order_date TIMESTAMP, last_order_date TIMESTAMP, date_registered TIMESTAMP, 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, wp_user_id BIGINT, 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 wp_user_id, saleor_user_id FROM wc_complete_user_mapping") for row in cursor.fetchall(): self.wp_id_to_saleor_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_user(self, user: WPUser, dry_run: bool = False) -> Optional[uuid.UUID]: """Import a WordPress user with segmentation metadata""" if user.wp_user_id in self.wp_id_to_saleor_id: return self.wp_id_to_saleor_id[user.wp_user_id] user_id = uuid.uuid4() if dry_run: print(f" [{user.segment}] Would create: {user.email} (Paid orders: {user.paid_orders}, LTV: {user.ltv:.0f} RSD)") return user_id with self.conn.cursor() as cursor: # Create user with segmentation metadata metadata = { 'wp_user_id': user.wp_user_id, 'segment': user.segment, 'order_count': user.order_count, 'paid_orders': user.paid_orders, 'total_spent': user.total_spent, 'imported_from': 'woocommerce', 'registration_date': user.date_registered.isoformat() if user.date_registered else None } 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) """, ( user_id, user.email, user.first_name, user.last_name, False, True, user.date_registered, '!', json.dumps(metadata) )) # Create address if available if user.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, user.billing_address['first_name'], user.billing_address['last_name'], user.billing_address['company_name'], user.billing_address['street_address_1'], user.billing_address['street_address_2'], user.billing_address['city'], user.billing_address['postal_code'], user.billing_address['country'], user.phone or '' )) cursor.execute(""" INSERT INTO account_user_addresses (user_id, address_id) VALUES (%s, %s) """, (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, user_id)) # Record mapping with segmentation cursor.execute(""" INSERT INTO wc_complete_user_mapping (wp_user_id, saleor_user_id, email, segment, order_count, paid_orders, total_spent, first_order_date, last_order_date, date_registered) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( user.wp_user_id, user_id, user.email, user.segment, user.order_count, user.paid_orders, user.total_spent, user.first_order_date, user.last_order_date, user.date_registered )) self.conn.commit() self.wp_id_to_saleor_id[user.wp_user_id] = user_id print(f" [{user.segment}] Created: {user.email} (Paid: {user.paid_orders}, LTV: {user.ltv:.0f} RSD)") return user_id def import_order(self, order: CODOrder, dry_run: bool = False) -> Optional[uuid.UUID]: """Import an order linked to the user - UPDATED for COD assumption""" 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 ID if this was a registered user user_id = None if order.wp_user_id and order.wp_user_id in self.wp_id_to_saleor_id: user_id = self.wp_id_to_saleor_id[order.wp_user_id] if dry_run: paid_marker = "✅" if order.is_paid else "❌" print(f" Order {order.order_number} {paid_marker} (Status: {order.status})") 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) """, (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) """, (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, user_id, order.currency, order.total, order.subtotal, order.shipping, order.shipping, order.shipping_method, channel_id, bill_id, 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, 'wp_user_id': order.wp_user_id }), '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) """, (uuid.uuid4(), 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)) # UPDATED: Create payment record for ALL paid orders (completed, pending, processing) 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) """, (uuid.uuid4(), 'mirumee.payments.dummy', # Dummy gateway for COD False, # Not active (completed) False, order_id, order.total, order.total, # Fully captured (COD collected) 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, wp_user_id, customer_email) VALUES (%s, %s, %s, %s) """, (order.wc_order_id, order_id, order.wp_user_id, order.customer_email)) self.conn.commit() return order_id def main(): parser = argparse.ArgumentParser( description='Complete WooCommerce Migration (All Users + Orders) - ASSUMES pending=completed for COD' ) parser.add_argument('--users', action='store_true', help='Migrate all WordPress users') parser.add_argument('--orders', action='store_true', help='Migrate all orders') parser.add_argument('--dry-run', action='store_true', help='Preview only') parser.add_argument('--limit-users', type=int, help='Limit user count') parser.add_argument('--limit-orders', type=int, help='Limit order count') parser.add_argument('--segment', type=str, choices=['VIP_CUSTOMER', 'REPEAT_CUSTOMER', 'ONE_TIME', 'PROSPECT'], help='Migrate only specific segment') parser.add_argument('--status', type=str, help='Order statuses to migrate (default: all except cancelled)') args = parser.parse_args() if not args.users and not args.orders: parser.print_help() sys.exit(1) print("=" * 70) print("COMPLETE WOOCOMMERCE TO SALEOR MIGRATION") print("=" * 70) print() print("ASSUMPTION: ALL orders = FULFILLED (paid) EXCEPT cancelled") print("For COD: if not cancelled, payment was collected on delivery.") print() print("Statuses treated as PAID:", ', '.join(PAID_STATUSES)) print("=" * 70) 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 users if args.users: print("Fetching all WordPress users...") users = exporter.get_all_users_with_activity() if args.segment: users = [u for u in users if u.segment == args.segment] if args.limit_users: users = users[:args.limit_users] print(f"Found {len(users)} users to migrate\n") # Segment breakdown segments = defaultdict(int) for u in users: segments[u.segment] += 1 print("Segment breakdown:") for seg, count in sorted(segments.items(), key=lambda x: -x[1]): print(f" {seg}: {count}") print() print("Migrating users...") for i, user in enumerate(users, 1): print(f"[{i}/{len(users)}]", end=" ") try: importer.import_user(user, dry_run=args.dry_run) except Exception as e: print(f"ERROR: {e}") print(f"\nUser migration {'preview' if args.dry_run else 'complete'}!\n") # Migrate orders if args.orders: print("Fetching orders...") # Default to ALL statuses except cancelled if args.status: status_filter = args.status else: # Exclude cancelled by default status_filter = 'wc-completed,wc-pending,wc-processing,wc-on-hold' orders = exporter.get_orders(limit=args.limit_orders, status=status_filter) print(f"Found {len(orders)} orders (statuses: {status_filter})\n") paid = sum(1 for o in orders if o.is_paid) print(f"Breakdown: {paid} fulfilled (paid), {len(orders)-paid} cancelled\n") print("Migrating orders...") for i, order in enumerate(orders, 1): marker = "✅" if order.is_paid else "❌" print(f"[{i}/{len(orders)}] {order.order_number} {marker}", end=" ") try: importer.import_order(order, dry_run=args.dry_run) print() except Exception as e: print(f"ERROR: {e}") print(f"\nOrder migration {'preview' if args.dry_run else 'complete'}!\n") # Summary print("=" * 70) print("MIGRATION SUMMARY") print("=" * 70) print(f"Users migrated: {len(importer.wp_id_to_saleor_id)}") if args.users: print("\nBy segment:") with importer.conn.cursor() as cursor: cursor.execute(""" SELECT segment, COUNT(*) as count FROM wc_complete_user_mapping GROUP BY segment ORDER BY count DESC """) for row in cursor.fetchall(): print(f" {row[0]}: {row[1]}") if __name__ == '__main__': main()