Files
manoon-headless/scripts/migrate_guest_orders.py
Unchained 7b94537670 feat(saleor): Phase 1 - GraphQL Client Setup
- Add Apollo Client for Saleor GraphQL API
- Create GraphQL fragments (Product, Variant, Checkout)
- Create GraphQL queries (Products, Checkout)
- Create GraphQL mutations (Checkout operations)
- Add TypeScript types for Saleor entities
- Add product helper functions
- Install @apollo/client and graphql dependencies

Part of WordPress/WooCommerce → Saleor migration
2026-03-21 12:36:21 +02:00

737 lines
32 KiB
Python

#!/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()