dashboard

This commit is contained in:
Lucas Tettamanti
2026-01-27 02:41:39 -03:00
parent 493f26af17
commit df9420b954
19 changed files with 2105 additions and 111 deletions

View 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));
});