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
This commit is contained in:
Unchained
2026-03-21 12:36:21 +02:00
parent db1914d69b
commit 7b94537670
27 changed files with 7879 additions and 3 deletions

785
scripts/migrate_complete.py Normal file
View File

@@ -0,0 +1,785 @@
#!/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()