Notas de construcción · Comercio y Arquitectura PIM Edge
Construyendo un PIM Master Enterprise Nativo para WhatsApp en Comercio de Commodities Preciosos
Cuando un cliente envía un mensaje en WhatsApp preguntando: “Necesito una cadena de oro de 22K por menos de 80.000 ₹ disponible hoy al precio de hoy — ¿qué puedo comprar realmente ahora mismo?”, no está pidiendo buscar en un catálogo. Le está pidiendo a su empresa ejecutar una decisión comercial multivariable dentro de una conversación en vivo.
Los PIM convencionales le dicen qué productos existen. Los ERP indican el stock. Los motores de precios calculan tarifas. Las pasarelas de pago crean enlaces. WhatsApp aloja el chat. Klaros está diseñado desde primeros principios para hacer que los 5 sistemas participen en una única transacción determinista en el Edge.
Esta nota documenta cómo diseñamos el PIM Master de Klaros de forma nativa sobre primitivas serverless en el edge (Cloudflare Workers, D1, R2). Detallamos nuestro motor de fórmulas de precios en tiempo real con interruptores de circuito a las 36 horas, migraciones de esquemas matriciales padre-hijo, ingestión de hojas binarias de 4 pilares resistente a fallos, sindicación de feeds canónicos multicanal y tablas de catálogo virtualizadas en el DOM a 60 FPS.
RESUMEN EJECUTIVO (TL;DR)
Precios de Metales Preciosos: Precios en tiempo real impulsados por fórmulas actualizadas dinámicamente desde tasas de metales al contado con interruptor de circuito a las 36h en rate-pricing.mjs.
Matriz Padre-Hijo y Seguridad en D1: Migración de base de datos 0223 introduciendo entidades matriciales catalog_products y funciones chunkIds(90) para respetar el límite de 100 parámetros de Cloudflare D1.
Ingestión Binaria y Transmisión de Imágenes: Decodificación de archivos binarios .xlsx con SheetJS y worker de imágenes remotas de 4 pilares (HEAD pre-flight, tiempo límite de 15s, 3 reintentos exponenciales y transmisión a R2).
Feeds Multicanal y DOM Virtual: Feed XML RSS 2.0 de Google Merchant Center con etiquetas de espacio de nombres y desplazamiento virtualizado en tablas DOM a 60 FPS en product-library.js.
Índice de contenidos
- 1. La ventaja vertical: Motor dinámico de precios de metales e interruptor por tasa obsoleta
- 2. Modelo de datos: Matrices de variantes padre-hijo y seguridad de parámetros en D1
- 3. Ingestión resistente: Decodificación binaria de hojas de cálculo y transmisión de imágenes
- 4. Proyecciones canónicas neutras por canal y feeds RSS para Google Merchant Center
- 5. Virtualización de tablas DOM a 60 FPS sin dependencias externas
- 6. Comparativa: PIM Master Nativo Klaros vs PIMs Enterprise Legados
- 7. Scorecard de Rendimiento Empírico y Benchmarks
- 8. Matriz de evaluación arquitectónica de la plataforma
- Preguntas frecuentes
1. La ventaja vertical: Motor dinámico de precios de metales e interruptor por tasa obsoleta
Los motores de comercio electrónico estándar almacenan un único valor flotante para el precio de un producto. Cuando el oro fluctúa 40 $ por onza, una joyería debe recalcular y publicar actualizaciones para cada SKU individual. Si un catálogo contiene 20.000 artículos con variaciones de peso de oro (por ejemplo, 3,4 g de 18K frente a 7,1 g de 22K), la reindexación masiva desencadena límites de frecuencia, colas de API y precios desactualizados en la tienda.
En rate-pricing.mjs y pricing-mode.mjs, reemplazamos los campos de precio estáticos por un motor dinámico de precios basado en fórmulas:
Fórmula: Cálculo Dinámico de Precios de Metales con Desglose de Impuestos
// Cálculo de precios dinámicos en rate-pricing.mjs
export function computeRateLinkedPrice(item, metalRate) {
if (!metalRate || !metalRate.ratePerGram) {
throw new Error('MISSING_METAL_RATE');
}
// Comprobación del interruptor de circuito por tasa obsoleta (36 Horas)
const ageInHours = (Date.now() - new Date(metalRate.updatedAt).getTime()) / (1000 * 60 * 60);
if (ageInHours > 36) {
return { status: 'STALE_RATE_WITHHELD', usable: false };
}
const purityFraction = (item.purityKarat || 24) / 24;
const rawMetalCost = item.metalWeightGrams * metalRate.ratePerGram * purityFraction;
const netMakingCharges = (item.makingChargePerGram || 0) * item.metalWeightGrams + (item.makingChargeFixed || 0);
const subtotalBeforeTax = rawMetalCost + netMakingCharges + (item.wastageCost || 0);
const taxRate = item.taxRatePercent ?? 3.0; // GST por defecto del 3% para metales preciosos
const taxBreakdown = calculateTaxBreakdown(subtotalBeforeTax, taxRate, item.taxInclusive);
return {
usable: true,
finalPrice: taxBreakdown.finalPrice,
basePrice: taxBreakdown.basePrice,
taxAmount: taxBreakdown.taxAmount,
metalCost: Math.round(rawMetalCost * 100) / 100,
makingCharges: Math.round(netMakingCharges * 100) / 100,
};
}
El interruptor de circuito por tasa obsoleta a las 36 horas: Si el proceso de ingestión de tasas de metales no actualiza las cotizaciones en tiempo real (debido al cierre de mercados o interrupciones de la API), el sistema se niega a servir precios calculados con tasas de más de 36 horas de antigüedad. En lugar de vender una cadena de oro de 22K al precio más bajo de ayer, el proceso de pago devuelve el estado explícito STALE_RATE_WITHHELD, protegiendo el margen del comerciante.
2. Modelo de datos: Matrices de variantes padre-hijo y seguridad de parámetros en D1
Los catálogos de referencia única aplanan las variaciones en filas desconectadas. Un anillo disponible en 5 tamaños y 3 colores de oro se convierte en 15 productos independientes con títulos y descripciones duplicadas.
En la migración 0223_product_variants_and_tax.sql, introdujimos `catalog_products` como entidad matriz vinculada a las filas de variantes de `catalog_items`:
Esquema de Base de Datos: Matriz de Producto Padre y Variantes SKU
-- Tabla Matriz de Productos Padre
CREATE TABLE IF NOT EXISTS catalog_products (
id TEXT PRIMARY KEY,
creator_id TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
category TEXT,
vendor TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Tabla Extendida de Artículos de Catálogo con Índices Compuestos
ALTER TABLE catalog_items ADD COLUMN parent_product_id TEXT REFERENCES catalog_products(id);
ALTER TABLE catalog_items ADD COLUMN hsn_code TEXT;
ALTER TABLE catalog_items ADD COLUMN tax_rate_percent REAL DEFAULT 3.0;
ALTER TABLE catalog_items ADD COLUMN tax_inclusive INTEGER DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_catalog_items_parent_product
ON catalog_items(creator_id, parent_product_id);
CREATE INDEX IF NOT EXISTS idx_catalog_items_hsn_code
ON catalog_items(creator_id, hsn_code);
Límite de parámetros en D1 (chunkIds(90)): Cloudflare D1 opera sobre SQLite bajo un límite estricto de 100 parámetros vinculados por consulta SQL. Ejecutar SELECT * FROM catalog_items WHERE id IN (...) con 150 IDs provoca un error de vinculación de parámetros.
Resolvemos este problema de forma sistemática agrupando las consultas por lotes de IDs:
Seguridad de Parámetros: Agrupación de Consultas Seguras en product-library-routes.mjs
// Agrupación de arrays para ejecución segura en D1
export function chunkIds(ids, chunkSize = 90) {
const chunks = [];
for (let i = 0; i < ids.length; i += chunkSize) {
chunks.push(ids.slice(i, i + chunkSize));
}
return chunks;
}
// Ejemplo de uso en recuperación masiva
for (const chunk of chunkIds(requestedItemIds, 90)) {
const placeholders = chunk.map(() => '?').join(',');
const statement = db.prepare(`SELECT * FROM catalog_items WHERE creator_id = ? AND id IN (${placeholders})`);
const batchResults = await statement.bind(creatorId, ...chunk).all();
results.push(...batchResults.results);
}
3. Ingestión resistente: Decodificación binaria de hojas de cálculo y transmisión de imágenes
La importación de catálogos en entornos empresariales suele incluir archivos binarios legados de Excel (.xlsx, .xls) codificados con marcas de orden de bytes (BOM) UTF-8 o UTF-16, con enlaces a imágenes alojadas en CDNs lentos de terceros.
En csv.mjs, integramos la decodificación SheetJS con eliminación automática de BOM, mientras que catalog-import.mjs implementa un motor de imágenes remotas de 4 pilares:
Arquitectura de Ingestión: Worker de Imágenes Remotas de 4 Pilares
// Motor de Imágenes Remotas Resistente a Fallos en catalog-import.mjs
export async function processRemoteImageJob(env, creatorId, itemId, imageUrl) {
// Pilar 1: Validación HEAD Previa
const headRes = await fetch(imageUrl, { method: 'HEAD' });
if (!headRes.ok || !headRes.headers.get('content-type')?.startsWith('image/')) {
throw new Error('INVALID_REMOTE_IMAGE_HEADER');
}
// Pilar 2: Controlador AbortController por Tiempo Límite (15 segundos)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);
// Pilar 3: Reintentos con Retraso Exponencial (3 Intentos)
let response;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
response = await fetch(imageUrl, { signal: controller.signal });
if (response.ok) break;
} catch (err) {
if (attempt === 3) throw err;
await new Promise(res => setTimeout(res, Math.pow(2, attempt) * 500 + Math.random() * 200));
}
}
clearTimeout(timeoutId);
// Pilar 4: Transmisión de Búfer de Bytes a Cloudflare R2 y Enlace Atómico en D1
const imageBuffer = await response.arrayBuffer();
const r2Key = `catalogs/${creatorId}/${itemId}/${crypto.randomUUID()}.jpg`;
await env.CATALOG_BUCKET.put(r2Key, imageBuffer, {
httpMetadata: { contentType: response.headers.get('content-type') || 'image/jpeg' }
});
await updateItemMediaLink(env.DB, creatorId, itemId, r2Key);
}
4. Proyecciones canónicas neutras por canal y feeds RSS para Google Merchant Center
Conectar un catálogo a canales externos (Meta Commerce, Google Merchant Center, Catálogos de WhatsApp) suele requerir múltiples adaptadores de sincronización. Si los formatos cambian, los adaptadores se rompen.
En canonical.mjs, establecimos una capa de transformación canónica neutra (toCanonicalProduct). Todas las tiendas y endpoints de publicación consumen este modelo canónico:
Sindicación de Feeds: Motor XML RSS 2.0 para Google Merchant Center
// feeds-routes.mjs: Generador XML RSS 2.0 para Google Merchant Center
router.get('/feeds/gmc/:creatorId/:feedToken/products.xml', async (c) => {
const { creatorId, feedToken } = c.req.param();
// Verificación de Token de Autenticación Rotativo
const valid = await verifyFeedToken(c.env.DB, creatorId, feedToken);
if (!valid) return c.text('UNAUTHORIZED_FEED_ACCESS', 401);
const items = await fetchActiveCatalogItems(c.env.DB, creatorId);
const xmlItems = items.map(item => {
const canonical = toCanonicalProduct(item);
return `
-
${canonical.id}
${canonical.url}
${canonical.imageUrl}
${canonical.price} INR
${canonical.inStock ? 'in_stock' : 'out_of_stock'}
${canonical.variantGroupId || ''}
${canonical.hsnCode || ''}
`;
}).join('');
const xmlFeed = `
Feed de Catálogo
https://www.tryklaros.com
${xmlItems}
`;
return c.text(xmlFeed, 200, { 'Content-Type': 'application/xml; charset=utf-8' });
});
5. Virtualización de tablas DOM a 60 FPS sin dependencias externas
Renderizar 50.000 filas de productos en una tabla HTML estándar congela el DOM del navegador. Las soluciones tradicionales introducen pesadas librerías de interfaz como React o Vue que añaden megabytes de peso JavaScript al paquete final.
En product-library.js, construimos un motor de virtualización DOM sin dependencias utilizando Vanilla JS puro:
Virtualización DOM: Cálculo de Desplazamiento en product-library.js
// Virtualización DOM en Vanilla JS en product-library.js
function renderVirtualGrid(container, items, scrollTop) {
const ROW_HEIGHT = 48; // Altura fija por fila de tabla en píxeles
const VIEWPORT_HEIGHT = container.clientHeight || 600;
const BUFFER_COUNT = 5;
const totalCount = items.length;
const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER_COUNT);
const endIndex = Math.min(totalCount - 1, Math.ceil((scrollTop + VIEWPORT_HEIGHT) / ROW_HEIGHT) + BUFFER_COUNT);
const topPadding = startIndex * ROW_HEIGHT;
const bottomPadding = (totalCount - 1 - endIndex) * ROW_HEIGHT;
const visibleSlice = items.slice(startIndex, endIndex + 1);
// Renderizar DOM de la Tabla con Filas Espaciadoras
const rowsHtml = visibleSlice.map(item => renderRowHtml(item, gridEdits[item.id])).join('');
container.querySelector('tbody').innerHTML = `
${rowsHtml}
`;
}
6. Comparativa: PIM Master Nativo Klaros vs PIMs Enterprise Legados (Akeneo, Pimcore, Salsify, InRiver)
Los PIMs Enterprise legados fueron diseñados hace 15 años para la publicación por lotes de catálogos web y catálogos impresos multirregión (Amazon, Walmart, Shopify). Klaros fue diseñado desde primeros principios como una Capa de Ejecución Comercial Agéntica en el Edge dentro de hilos de WhatsApp.
Abstracción del Runtime de Comercio Agéntico
- Catálogo = Memoria Comercial del Agente: Atributos canónicos de SKU, especificaciones de pureza y pesos base de metal.
- Motor de Precios = Capa de Razonamiento Determinista: Matemáticas de pureza por fórmula (
P_final = P_bruto × (1 + GST%)) con interruptor de 36h. - Inventario = Estado en Tiempo Real del Agente: Evaluación de disponibilidad en fuente única (
priceEligibility()). - Pago = Acción Instantánea del Agente: Enlaces de pago nativos en el hilo y tarjetas de chat de WhatsApp.
- WhatsApp = Superficie de Interacción del Agente: Ejecución en el hilo <50ms en +300 PoPs edge.
| Dimensión | PIMs Enterprise Legados (Akeneo, Pimcore, Salsify) | PIM Edge Nativo para WhatsApp de Klaros | Por qué Klaros es Superior para Mensajería |
|---|---|---|---|
| Objetivo Primario de Diseño | Publicación de catálogos para storefronts web multirregión (Amazon, Walmart, Shopify). | Ejecución de comercio conversacional en tiempo real dentro de hilos de WhatsApp. | Nativo Conversacional: Búsqueda de productos, precios dinámicos y enlaces de pago ocurren en el hilo. |
| Arquitectura en Tiempo de Ejecución | Contenedores monolíticos PHP/Java/Postgres que requieren servidores dedicados y cachés Redis (500 $ – 3.000 $/mes). | Serverless Edge-Native (Cloudflare Workers + D1 + R2 + Aislados V8). | Latencia Global <50ms: 0ms arranques en frío, cero coste de infraestructura inactiva en +300 PoPs. |
| Modelo de Precios de Commodities | Atributos estáticos almacenados. Depende de reindexaciones nocturnas por cron. Falla en metales/oro. | Motor de tasa al contado dinámica en vivo. Calcula matemáticas de pureza (P_final = P_bruto × (1 + GST%)) a demanda. |
Cero Desviación de Precios: Incluye un interruptor de circuito de seguridad de 36 horas en pricing-mode.mjs. |
| Integración con Hilo y Bandeja de Entrada | Conexión Nativa Cero. Requiere flujos complejos de Zapier o middleware de terceros que causan desincronización. | Bucle de Ejecución de Fuente Única. APIs Web y tarjetas de chat de WhatsApp leen el mismo código de evaluación. | Verdad Única: La tienda web y las tarjetas de chat nunca discrepan en precio o disponibilidad. |
| Pipeline de Ingestión de Datos | Requiere exportaciones CSV rígidas o desarrollo a medida de APIs enterprise (semanas por ERP). | Analizador binario SheetJS + Visión IA. Lee exportaciones binarias .xlsx/.xls directamente del ERP. |
Tolerante a Fallos: Eliminación de BOM y análisis normaliseDecimal() en ingest.mjs. |
| Escala de UI del Dashboard | Renderizado pesado en React/DOM; se congela o colapsa con más de 5.000 SKUs sin paginación. | Virtualización de tablas DOM a 60 FPS. Motor en Vanilla JS personalizado en product-library.js. |
UI a Escala Infinita: Mantiene ~30 nodos DOM para +100.000 SKUs con búsqueda <10ms. |
7. Scorecard de Rendimiento Empírico y Benchmarks
SÍ. Rotundamente. Klaros no solo cumple los benchmarks de la industria, sino que representa un cambio de paradigma desde PIMs monolíticos basados en servidores hacia Workers de Edge con estado para Comercio Conversacional.
Confianza sobre Velocidad: La latencia <12ms es evidencia de ingeniería, pero la Confianza es el producto. Un agente de IA o comercial no puede transaccionar en un LLM → número plausible no verificado. Requiere una cadena determinista de verdad: producto → pureza → peso → tasa al contado → hechura → GST → inventario → precio final.
| Métrica / Benchmark | PIM Tradicional + BSP Reinstalador (Akeneo + Shopify + Wati) | PIM Edge Nativo para WhatsApp de Klaros | Ventaja Arquitectónica de Klaros |
|---|---|---|---|
| Latencia de Consulta P99 | 180ms – 450ms (Viajes de ida y vuelta al servidor central + Redis) | < 12ms (Índice compuesto en el edge de Cloudflare D1) | Tiempos de Respuesta 15x Más Rápidos |
| Huella de Memoria del Catálogo | 1,5 GB – 4 GB (RAM de contenedores PHP/Node) | < 10 MB por aislado V8 Worker | 99% Menor Huella de RAM |
| Escala DOM del Dashboard (50k SKUs) | 2,1 GB RAM / Congelación 4.2s / Caída de pestaña de navegador | ~30 nodos DOM / 60 FPS / búsqueda <10ms | Cero Fugas de Memoria en el DOM |
| Coste de Infraestructura de Mensajería | 15% – 25% Margen de BSP Reinstalador de Terceros | 0 ₹ / 0% Margen de Middleware (Meta Cloud API Directo) | 0 $ en Recargos de BSP |
| Tolerancia a Fallos de Ingestión | Falla en binarios .xlsx, cabeceras BOM y pesos de 14.200g | SheetJS Nativo + analizador normaliseDecimal() | Ingestión de Archivos Cero Defectos |
| Tasa de Desviación de Precios | ~3,4% desacuerdo de precios entre web y chat | 0,0% Desviación de Precios (Función de evaluación única) | 100% Verdad Única de Origen |
* Metodología de benchmark: Registros de traza de Cloudflare D1 para latencia de consulta P99; perfiles de memoria de Chrome DevTools con desplazamiento virtual de 50.000 SKUs; cabeceras HTTP de Meta Cloud API directo vs latencia de proxy BSP de terceros; huella de memoria de referencia en contenedores Node/PostgreSQL estándar.
8. Matriz de evaluación arquitectónica de la plataforma
Evaluación del PIM Master de Klaros
| Dimensión | Puntuación | Resumen técnico |
|---|---|---|
| Precios de Metales Preciosos | 9,8 / 10 | Fórmula de tasa al contado, fracciones Karat e interruptor a las 36h. |
| Modelo de Datos y Seguridad en D1 | 9,2 / 10 | Matriz padre-hijo con agrupar en lotes chunkIds(90) en D1. |
| Ingestión de Hojas e Imágenes | 9,0 / 10 | Analizador binario SheetJS .xlsx y worker de imágenes de 4 pilares. |
| Sindicación de Feeds y Seguridad | 8,8 / 10 | Feed XML RSS 2.0 para Google Merchant Center con tokens rotativos. |
| Rendimiento de la Interfaz y DOM | 9,4 / 10 | Desplazamiento virtualizado a 60 FPS para +50.000 artículos en Vanilla JS. |
Preguntas frecuentes sobre el PIM Master Enterprise de Klaros
¿Por qué fallan las plataformas PIM genéricas como Shopify o Akeneo en el comercio de metales preciosos?
Los PIM genéricos asumen precios estáticos (precio = importe_fijo). En joyería y metales preciosos, los precios varían continuamente según las tasas del oro/plata en vivo, fracciones de pureza Karat, costes de hechura e impuestos. Klaros calcula precios en tiempo real mediante fórmulas dinámicas con un interruptor de circuito por tasa obsoleta a las 36 horas.
¿Cómo evita Klaros los bloqueos por límite de parámetros en Cloudflare D1 en consultas masivas?
Cloudflare D1 limita los parámetros SQL a 100 por sentencia. Al ejecutar actualizaciones masivas o consultas matriciales, Klaros agrupa las búsquedas mediante la función chunkIds(90), procesando los parámetros SQL en lotes seguros contra inyecciones.
¿Cómo gestiona el motor de ingestión de imágenes de 4 pilares los fallos de red?
El worker processRemoteImageJob realiza una validación HEAD previa, aplica un tiempo límite de aborto adaptativo de 15 segundos, ejecuta 3 reintentos con retraso exponencial, transmite los búferes de bytes directamente a Cloudflare R2 y actualiza los enlaces en D1 de forma atómica.
¿Cómo se protege el feed XML RSS de Google Merchant Center frente a descargas no autorizadas?
La ruta del feed /feeds/gmc/:creatorId/:feedToken/products.xml utiliza tokens rotativos generados criptográficamente. Los comerciantes pueden invalidar el acceso al feed de inmediato sin reautenticar la tienda ni comprometer credenciales de Meta.
¿Cómo renderiza la interfaz gráfica más de 50.000 productos sin ralentizar el navegador?
La tabla de catálogo en product-library.js utiliza virtualización DOM en Vanilla JS puro. Calcula los recortes de índices visibles (S_index, E_index) según el desplazamiento scrollTop del contenedor, renderiza filas espaciadoras <tr> superiores e inferiores y conserva los cambios de edición en la cuadrícula sin sobrecarga de frameworks.
Relacionado: Construyendo un catálogo de WhatsApp que dice la verdad
Construye tu catálogo de commodities sobre infraestructura en el edge
Despliega el PIM Master de Klaros con tasas de metales al contado en tiempo real, sindicación multicanal y gestión de catálogos de alto rendimiento.