dashboard
This commit is contained in:
360
scripts/migrate-woo-orders.mjs
Normal file
360
scripts/migrate-woo-orders.mjs
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Migración directa de pedidos WooCommerce (MySQL) a cache local (PostgreSQL)
|
||||
*
|
||||
* WooCommerce 8.x+ usa HPOS (High Performance Order Storage)
|
||||
*
|
||||
* Uso:
|
||||
* node scripts/migrate-woo-orders.mjs [--tenant-id=xxx] [--batch-size=500] [--dry-run]
|
||||
*
|
||||
* Requiere en .env:
|
||||
* WOO_MYSQL_HOST, WOO_MYSQL_PORT, WOO_MYSQL_USER, WOO_MYSQL_PASSWORD, WOO_MYSQL_DATABASE
|
||||
* WOO_TABLE_PREFIX (default: wp_)
|
||||
* DATABASE_URL (PostgreSQL)
|
||||
*/
|
||||
|
||||
import mysql from "mysql2/promise";
|
||||
import pg from "pg";
|
||||
import "dotenv/config";
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
// --- Configuración ---
|
||||
const TENANT_ID = process.argv.find(a => a.startsWith("--tenant-id="))?.split("=")[1]
|
||||
|| process.env.DEFAULT_TENANT_ID
|
||||
|| "eb71b9a7-9ccf-430e-9b25-951a0c589c0f"; // tenant de piaf
|
||||
|
||||
const BATCH_SIZE = parseInt(process.argv.find(a => a.startsWith("--batch-size="))?.split("=")[1] || "500", 10);
|
||||
const DRY_RUN = process.argv.includes("--dry-run");
|
||||
const TABLE_PREFIX = process.env.WOO_TABLE_PREFIX || "wp_";
|
||||
|
||||
// --- Conexiones ---
|
||||
let mysqlConn;
|
||||
let pgPool;
|
||||
|
||||
async function connect() {
|
||||
console.log("[migrate] Conectando a MySQL...");
|
||||
mysqlConn = await mysql.createConnection({
|
||||
host: process.env.WOO_MYSQL_HOST,
|
||||
port: parseInt(process.env.WOO_MYSQL_PORT || "3306", 10),
|
||||
user: process.env.WOO_MYSQL_USER,
|
||||
password: process.env.WOO_MYSQL_PASSWORD,
|
||||
database: process.env.WOO_MYSQL_DATABASE,
|
||||
rowsAsArray: false,
|
||||
});
|
||||
console.log("[migrate] MySQL conectado");
|
||||
|
||||
console.log("[migrate] Conectando a PostgreSQL...");
|
||||
pgPool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
max: 5,
|
||||
});
|
||||
await pgPool.query("SELECT 1");
|
||||
console.log("[migrate] PostgreSQL conectado");
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (mysqlConn) await mysqlConn.end();
|
||||
if (pgPool) await pgPool.end();
|
||||
}
|
||||
|
||||
// --- Query principal de pedidos (HPOS) ---
|
||||
function buildOrdersQuery() {
|
||||
return `
|
||||
SELECT
|
||||
o.id as order_id,
|
||||
o.status,
|
||||
o.currency,
|
||||
o.total_amount as total,
|
||||
o.date_created_gmt as date_created,
|
||||
o.date_paid_gmt as date_paid,
|
||||
o.payment_method,
|
||||
o.payment_method_title,
|
||||
|
||||
-- Billing
|
||||
ba.first_name as billing_first_name,
|
||||
ba.last_name as billing_last_name,
|
||||
ba.address_1 as billing_address_1,
|
||||
ba.city as billing_city,
|
||||
ba.state as billing_state,
|
||||
ba.postcode as billing_postcode,
|
||||
ba.phone as billing_phone,
|
||||
ba.email as billing_email,
|
||||
|
||||
-- Shipping
|
||||
sa.first_name as shipping_first_name,
|
||||
sa.last_name as shipping_last_name,
|
||||
sa.address_1 as shipping_address_1,
|
||||
sa.address_2 as shipping_address_2,
|
||||
sa.city as shipping_city,
|
||||
sa.state as shipping_state,
|
||||
sa.postcode as shipping_postcode
|
||||
|
||||
FROM ${TABLE_PREFIX}wc_orders o
|
||||
LEFT JOIN ${TABLE_PREFIX}wc_order_addresses ba
|
||||
ON ba.order_id = o.id AND ba.address_type = 'billing'
|
||||
LEFT JOIN ${TABLE_PREFIX}wc_order_addresses sa
|
||||
ON sa.order_id = o.id AND sa.address_type = 'shipping'
|
||||
WHERE o.type = 'shop_order'
|
||||
ORDER BY o.id ASC
|
||||
`;
|
||||
}
|
||||
|
||||
// --- Query de items por pedido ---
|
||||
async function getOrderItems(orderId) {
|
||||
const [items] = await mysqlConn.query(`
|
||||
SELECT
|
||||
oi.order_item_id,
|
||||
oi.order_item_name as product_name,
|
||||
MAX(CASE WHEN oim.meta_key = '_product_id' THEN oim.meta_value END) as product_id,
|
||||
MAX(CASE WHEN oim.meta_key = '_variation_id' THEN oim.meta_value END) as variation_id,
|
||||
MAX(CASE WHEN oim.meta_key = '_qty' THEN oim.meta_value END) as quantity,
|
||||
MAX(CASE WHEN oim.meta_key = '_line_total' THEN oim.meta_value END) as line_total,
|
||||
MAX(CASE WHEN oim.meta_key = '_line_subtotal' THEN oim.meta_value END) as line_subtotal,
|
||||
MAX(CASE WHEN oim.meta_key = 'unit' THEN oim.meta_value END) as unit,
|
||||
MAX(CASE WHEN oim.meta_key = 'weight_g' THEN oim.meta_value END) as weight_g
|
||||
FROM ${TABLE_PREFIX}woocommerce_order_items oi
|
||||
LEFT JOIN ${TABLE_PREFIX}woocommerce_order_itemmeta oim ON oim.order_item_id = oi.order_item_id
|
||||
WHERE oi.order_id = ? AND oi.order_item_type = 'line_item'
|
||||
GROUP BY oi.order_item_id, oi.order_item_name
|
||||
`, [orderId]);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- Query de metadata por pedido (source, shipping_method, etc) ---
|
||||
async function getOrderMeta(orderId) {
|
||||
const [rows] = await mysqlConn.query(`
|
||||
SELECT meta_key, meta_value
|
||||
FROM ${TABLE_PREFIX}wc_orders_meta
|
||||
WHERE order_id = ?
|
||||
AND meta_key IN ('source', 'shipping_method', 'payment_method_wa', 'run_id')
|
||||
`, [orderId]);
|
||||
|
||||
const meta = {};
|
||||
for (const row of rows) {
|
||||
meta[row.meta_key] = row.meta_value;
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
// --- Detectar source y flags ---
|
||||
function detectOrderFlags(order, meta) {
|
||||
// Source
|
||||
const source = meta.source || "web";
|
||||
|
||||
// isDelivery
|
||||
const shippingMethod = meta.shipping_method || "";
|
||||
const isDelivery = shippingMethod === "delivery" ||
|
||||
(!shippingMethod.toLowerCase().includes("retiro") &&
|
||||
!shippingMethod.toLowerCase().includes("pickup") &&
|
||||
!shippingMethod.toLowerCase().includes("local") &&
|
||||
order.shipping_address_1); // Si tiene dirección de envío
|
||||
|
||||
// isCash
|
||||
const metaPayment = meta.payment_method_wa || "";
|
||||
const isCash = metaPayment === "cash" ||
|
||||
order.payment_method === "cod" ||
|
||||
(order.payment_method_title || "").toLowerCase().includes("efectivo");
|
||||
|
||||
return { source, isDelivery, isCash };
|
||||
}
|
||||
|
||||
// --- Detectar sell_unit del item ---
|
||||
function detectSellUnit(item) {
|
||||
if (item.unit === "g" || item.unit === "kg") return "kg";
|
||||
if (item.unit === "unit") return "unit";
|
||||
if (item.weight_g) return "kg";
|
||||
|
||||
const name = (item.product_name || "").toLowerCase();
|
||||
if (name.includes(" kg") || name.includes("kilo")) return "kg";
|
||||
|
||||
return "unit";
|
||||
}
|
||||
|
||||
// --- Insert en PostgreSQL (batch con transacción) ---
|
||||
async function insertOrderBatch(orders) {
|
||||
if (DRY_RUN || orders.length === 0) return;
|
||||
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
for (const order of orders) {
|
||||
// Upsert pedido
|
||||
await client.query(`
|
||||
INSERT INTO woo_orders_cache (
|
||||
tenant_id, woo_order_id, status, total, currency,
|
||||
date_created, date_paid, source, is_delivery, is_cash,
|
||||
customer_name, customer_phone, customer_email,
|
||||
shipping_address_1, shipping_address_2, shipping_city,
|
||||
shipping_state, shipping_postcode, shipping_country,
|
||||
billing_address_1, billing_city, billing_state, billing_postcode,
|
||||
raw, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8, $9, $10,
|
||||
$11, $12, $13,
|
||||
$14, $15, $16,
|
||||
$17, $18, $19,
|
||||
$20, $21, $22, $23,
|
||||
$24, NOW()
|
||||
)
|
||||
ON CONFLICT (tenant_id, woo_order_id)
|
||||
DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
total = EXCLUDED.total,
|
||||
date_paid = EXCLUDED.date_paid,
|
||||
source = EXCLUDED.source,
|
||||
is_delivery = EXCLUDED.is_delivery,
|
||||
is_cash = EXCLUDED.is_cash,
|
||||
updated_at = NOW()
|
||||
`, [
|
||||
TENANT_ID,
|
||||
order.order_id,
|
||||
order.status?.replace("wc-", "") || "pending",
|
||||
parseFloat(order.total) || 0,
|
||||
order.currency || "ARS",
|
||||
order.date_created,
|
||||
order.date_paid,
|
||||
order.source,
|
||||
order.isDelivery,
|
||||
order.isCash,
|
||||
`${order.billing_first_name || ""} ${order.billing_last_name || ""}`.trim(),
|
||||
order.billing_phone,
|
||||
order.billing_email,
|
||||
order.shipping_address_1,
|
||||
order.shipping_address_2,
|
||||
order.shipping_city,
|
||||
order.shipping_state,
|
||||
order.shipping_postcode,
|
||||
"AR",
|
||||
order.billing_address_1,
|
||||
order.billing_city,
|
||||
order.billing_state,
|
||||
order.billing_postcode,
|
||||
JSON.stringify({}), // raw simplificado para ahorrar espacio
|
||||
]);
|
||||
|
||||
// Delete + insert items
|
||||
await client.query(
|
||||
`DELETE FROM woo_order_items WHERE tenant_id = $1 AND woo_order_id = $2`,
|
||||
[TENANT_ID, order.order_id]
|
||||
);
|
||||
|
||||
if (order.items && order.items.length > 0) {
|
||||
const itemValues = order.items.map(it => [
|
||||
TENANT_ID,
|
||||
order.order_id,
|
||||
it.product_id || it.variation_id,
|
||||
it.product_name,
|
||||
null, // sku
|
||||
parseFloat(it.quantity) || 0,
|
||||
it.line_subtotal ? parseFloat(it.line_subtotal) / (parseFloat(it.quantity) || 1) : null,
|
||||
parseFloat(it.line_total) || 0,
|
||||
detectSellUnit(it),
|
||||
]);
|
||||
|
||||
for (const vals of itemValues) {
|
||||
await client.query(`
|
||||
INSERT INTO woo_order_items (
|
||||
tenant_id, woo_order_id, woo_product_id,
|
||||
product_name, sku, quantity, unit_price, line_total, sell_unit
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
`, vals);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK");
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
async function main() {
|
||||
console.log("=".repeat(60));
|
||||
console.log("[migrate] Migración WooCommerce (MySQL) -> PostgreSQL");
|
||||
console.log(`[migrate] Tenant: ${TENANT_ID}`);
|
||||
console.log(`[migrate] Batch size: ${BATCH_SIZE}`);
|
||||
console.log(`[migrate] Table prefix: ${TABLE_PREFIX}`);
|
||||
console.log(`[migrate] Dry run: ${DRY_RUN}`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
await connect();
|
||||
|
||||
// Contar total de pedidos
|
||||
const [[{ total }]] = await mysqlConn.query(`
|
||||
SELECT COUNT(*) as total
|
||||
FROM ${TABLE_PREFIX}wc_orders
|
||||
WHERE type = 'shop_order'
|
||||
`);
|
||||
console.log(`[migrate] Total pedidos en WooCommerce: ${total}`);
|
||||
|
||||
// Limpiar cache existente si no es dry run
|
||||
if (!DRY_RUN) {
|
||||
console.log("[migrate] Limpiando cache existente...");
|
||||
await pgPool.query(`DELETE FROM woo_order_items WHERE tenant_id = $1`, [TENANT_ID]);
|
||||
await pgPool.query(`DELETE FROM woo_orders_cache WHERE tenant_id = $1`, [TENANT_ID]);
|
||||
console.log("[migrate] Cache limpiado");
|
||||
}
|
||||
|
||||
// Query de pedidos
|
||||
console.log("[migrate] Iniciando migración...");
|
||||
const [ordersRows] = await mysqlConn.query(buildOrdersQuery());
|
||||
|
||||
let count = 0;
|
||||
let batch = [];
|
||||
const startTime = Date.now();
|
||||
|
||||
for (const row of ordersRows) {
|
||||
// Obtener items y metadata
|
||||
const [items, meta] = await Promise.all([
|
||||
getOrderItems(row.order_id),
|
||||
getOrderMeta(row.order_id),
|
||||
]);
|
||||
|
||||
const flags = detectOrderFlags(row, meta);
|
||||
|
||||
batch.push({
|
||||
...row,
|
||||
...flags,
|
||||
items,
|
||||
});
|
||||
|
||||
count++;
|
||||
|
||||
// Insert batch
|
||||
if (batch.length >= BATCH_SIZE) {
|
||||
await insertOrderBatch(batch);
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
const rate = (count / elapsed).toFixed(0);
|
||||
const pct = ((count / total) * 100).toFixed(1);
|
||||
console.log(`[migrate] Progreso: ${count}/${total} (${pct}%) - ${rate} pedidos/s`);
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Último batch
|
||||
if (batch.length > 0) {
|
||||
await insertOrderBatch(batch);
|
||||
}
|
||||
|
||||
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log("=".repeat(60));
|
||||
console.log(`[migrate] COMPLETADO`);
|
||||
console.log(`[migrate] Pedidos migrados: ${count}`);
|
||||
console.log(`[migrate] Tiempo total: ${totalTime}s`);
|
||||
console.log(`[migrate] Velocidad promedio: ${(count / totalTime).toFixed(0)} pedidos/s`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
await disconnect();
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error("[migrate] ERROR:", err);
|
||||
disconnect().finally(() => process.exit(1));
|
||||
});
|
||||
Reference in New Issue
Block a user