audit and sync

This commit is contained in:
Lucas Tettamanti
2026-01-18 19:00:49 -03:00
parent 3b39e706af
commit 23c3d44490
9 changed files with 404 additions and 19 deletions

View File

@@ -1,4 +1,4 @@
import { handleSearchProducts, handleListProducts, handleGetProduct, handleSyncProducts, handleUpdateProductUnit, handleBulkUpdateProductUnit, handleUpdateProduct } from "../handlers/products.js";
import { handleSearchProducts, handleListProducts, handleGetProduct, handleSyncProducts, handleSyncFromWoo, handleUpdateProductUnit, handleBulkUpdateProductUnit, handleUpdateProduct } from "../handlers/products.js";
export const makeSearchProducts = (tenantIdOrFn) => async (req, res) => {
try {
@@ -54,6 +54,20 @@ export const makeSyncProducts = (tenantIdOrFn) => async (req, res) => {
}
};
/**
* Sync manual de emergencia: reimporta todos los productos de WooCommerce
*/
export const makeSyncFromWoo = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
const result = await handleSyncFromWoo({ tenantId });
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "internal_error", message: err.message });
}
};
export const makeUpdateProductUnit = (tenantIdOrFn) => async (req, res) => {
try {
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;

View File

@@ -624,3 +624,63 @@ export async function countQtyRulesByProduct({ tenantId }) {
const { rows } = await pool.query(sql, [tenantId]);
return rows;
}
// ─────────────────────────────────────────────────────────────
// Audit Log
// ─────────────────────────────────────────────────────────────
/**
* Inserta un registro en el audit log
* @param {Object} params
* @param {string} params.tenantId - UUID del tenant
* @param {string} params.entityType - 'product' | 'user' | 'order'
* @param {string} params.entityId - ID de la entidad (woo_product_id, wa_chat_id, etc)
* @param {string} params.action - 'create' | 'update' | 'delete' | 'sync_from_woo' | 'push_to_woo'
* @param {Object} params.changes - { field: { old, new } }
* @param {string} params.actor - 'system' | 'webhook' | 'ui'
*/
export async function insertAuditLog({ tenantId, entityType, entityId, action, changes, actor = 'system' }) {
const sql = `
insert into audit_log (tenant_id, entity_type, entity_id, action, changes, actor)
values ($1, $2, $3, $4, $5::jsonb, $6)
returning id, created_at
`;
const { rows } = await pool.query(sql, [
tenantId,
entityType,
String(entityId),
action,
changes ? JSON.stringify(changes) : null,
actor
]);
return rows[0] || null;
}
/**
* Obtiene el historial de cambios de una entidad
*/
export async function getAuditLog({ tenantId, entityType, entityId, limit = 50 }) {
const sql = `
select id, entity_type, entity_id, action, changes, actor, created_at
from audit_log
where tenant_id = $1
and ($2::text is null or entity_type = $2)
and ($3::text is null or entity_id = $3)
order by created_at desc
limit $4
`;
const { rows } = await pool.query(sql, [tenantId, entityType || null, entityId || null, limit]);
return rows;
}
/**
* Obtiene el historial de cambios recientes de productos
*/
export async function getProductAuditLog({ tenantId, wooProductId, limit = 20 }) {
return getAuditLog({
tenantId,
entityType: 'product',
entityId: wooProductId ? String(wooProductId) : null,
limit
});
}

View File

@@ -1,5 +1,5 @@
import { searchSnapshotItems } from "../../shared/wooSnapshot.js";
import { listProducts, getProductByWooId, updateProductSellUnit, bulkUpdateProductSellUnit, updateProduct } from "../db/repo.js";
import { searchSnapshotItems, syncFromWoo, pushProductToWoo } from "../../shared/wooSnapshot.js";
import { listProducts, getProductByWooId, updateProductSellUnit, bulkUpdateProductSellUnit, updateProduct, insertAuditLog } from "../db/repo.js";
export async function handleSearchProducts({ tenantId, q = "", limit = "10", forceWoo = "0" }) {
const { items, source } = await searchSnapshotItems({
@@ -20,9 +20,34 @@ export async function handleGetProduct({ tenantId, wooProductId }) {
}
export async function handleSyncProducts({ tenantId }) {
// This is a placeholder - actual sync would fetch from Woo API
// For now, just return success
return { ok: true, message: "Sync triggered (use import script for full sync)" };
// Placeholder legacy - redirigir a syncFromWoo
return handleSyncFromWoo({ tenantId });
}
/**
* Sincroniza todos los productos desde WooCommerce (solo para emergencias)
*/
export async function handleSyncFromWoo({ tenantId }) {
console.log("[products] handleSyncFromWoo starting...");
try {
const result = await syncFromWoo({ tenantId });
// Registrar en audit_log
await insertAuditLog({
tenantId,
entityType: 'product',
entityId: 'all',
action: 'sync_from_woo',
changes: { synced_count: result.synced, ms: result.ms },
actor: 'ui'
});
return { ok: true, synced: result.synced, ms: result.ms };
} catch (err) {
console.error("[products] handleSyncFromWoo error:", err);
return { ok: false, error: err.message };
}
}
export async function handleUpdateProductUnit({ tenantId, wooProductId, sell_unit }) {
@@ -35,8 +60,43 @@ export async function handleBulkUpdateProductUnit({ tenantId, wooProductIds, sel
return { ok: true, updated_count: wooProductIds.length, sell_unit };
}
/**
* Actualiza un producto localmente y automáticamente lo pushea a WooCommerce
*/
export async function handleUpdateProduct({ tenantId, wooProductId, sell_unit, categories }) {
// 1. Guardar cambios localmente
await updateProduct({ tenantId, wooProductId, sell_unit, categories });
return { ok: true, woo_product_id: wooProductId, sell_unit, categories };
// 2. Auto-push a WooCommerce (best effort - no falla si Woo no responde)
let wooPushResult = null;
try {
wooPushResult = await pushProductToWoo({ tenantId, wooProductId, categories, sellUnit: sell_unit });
console.log("[products] handleUpdateProduct: pushed to Woo", { wooProductId, updated: wooPushResult?.updated });
} catch (err) {
console.error("[products] handleUpdateProduct: Woo push failed (continuing anyway)", err.message);
// No lanzamos el error - el guardado local fue exitoso
}
// 3. Registrar en audit_log
await insertAuditLog({
tenantId,
entityType: 'product',
entityId: String(wooProductId),
action: 'update',
changes: {
sell_unit: { new: sell_unit },
categories: { new: categories },
woo_synced: wooPushResult?.updated ?? false
},
actor: 'ui'
});
return {
ok: true,
woo_product_id: wooProductId,
sell_unit,
categories,
woo_synced: wooPushResult?.updated ?? false
};
}