🔗 Fuentes de Información de Ares
El microservicio Ares depende de varias fuentes de información que le envían datos de productos a través de RabbitMQ. Cada una de estas fuentes es responsable de un aspecto específico de la información del producto.
Todas las fuentes publican las actualizaciones de productos con el mismo mecanismo explicado en el proceso de Publicación de Productos. Naturalmente cada sistema publica el mensaje con diferentes atributos, por ejemplo pricing solo comunica 'msrp' y 'price'.
PIM
PIM es el gestor de productos utilizado por el equipo de Comercial para crear y actualizar productos.
Cada vez que un producto es modificado en PIM (por ejemplo, cambio de nombre, imágenes, color o estado activo/inactivo), se dispara un mensaje a RabbitMQ, el cual será consumido posteriormente por Ares.
Pricing
Pricing es la plataforma interna para la gestión de precios y campañas comerciales.
Permite definir precios basados en fechas o periodos de campañas.
Cuando llega el momento de aplicar una campaña que actualiza los precios de ciertos productos, Pricing envía un mensaje a RabbitMQ para que Ares reciba la actualización.
Williams
Williams es el microservicio encargado de gestionar el inventario disponible.
Controla el stock de cada SKU y envía actualizaciones cuando, se resta stock por asignación a un pedido, se descuenta inventario debido a una pieza dañada, llega nuevo producto a CEDIS y se libera para su venta, etc.
Cada modificación en el inventario dispara un mensaje en RabbitMQ que es consumido por Ares para mantener actualizado el stock en los canales.
↔️ Mapeo de datos
Se utiliza la siguiente plantilla personalizada que transforma la informacion de un producto almacenado en Ares para que sea correctamente interpretado por Typesense.
Plantilla
module.exports = {
currency: {
default: "MXN",
},
brand: {
default: "GAIA",
},
condition: {
default: "new",
},
metric_unit: {
default: "cm",
},
mass_unit: {
default: "kg",
},
country: {
default: "MX",
},
language: {
default: "ES",
},
channel: {
default: "online",
},
productSource: {
default: "GAIA",
},
sku: {
path: "sku",
},
name: {
path: "name",
},
visibility: {
path: "visibility",
transform: (visibility) => (visibility ? +visibility : undefined),
},
product_weight: {
path: "weight",
transform: (weight) => (weight ? +weight : undefined),
},
status: {
path: "status",
transform: (status) =>
status !== undefined ? getStatus(status) : undefined,
},
product_height: {
path: "alto",
transform: (value, transformedObject, originalObject) =>
value ? value : originalObject.dimension_alto,
},
product_width: {
path: "ancho",
transform: (value, transformedObject, originalObject) =>
value ? value : originalObject.dimension_ancho,
},
product_length: {
path: "largo",
transform: (value, transformedObject, originalObject) =>
value ? value : originalObject.dimension_largo,
},
gaia_grouped_skus: {
path: "gaia_grouped_skus",
},
product_type: {
path: "type",
},
custom_labels: {
transform: (value, transformedObject, originalObject) =>
getCustomLabels(originalObject),
},
description: {
path: "description",
transform: (value, transformedObject, originalObject) =>
value ? value : originalObject.short_description,
},
color: {
path: "color_principal",
transform: (value, transformedObject, originalObject) =>
value
? valueToArray(value)
: originalObject.color
? valueToArray(originalObject.color)
: undefined,
"api_import": {
validator: (value) => ['0','blanco', 'negro'].includes(value)
}
},
color_color: {
path: "color",
},
color_principal: {
path: "color_principal",
},
color_family: {
path: "color_family",
},
material: {
path: "material_principal",
transform: (value, transformedObject, originalObject) =>
value
? [value]
: originalObject.material
? valueToArray(originalObject.material)
: undefined,
},
link: {
path: "link",
},
image_link: {
path: "image_link",
},
additional_image_link: {
path: "additional_image_link",
transform: (value) => (value ? valueToArray(value) : undefined),
},
image: {
path: "image",
transform: (value) => typeof value === "string" ? value.replace(/[;+]/g, "") : undefined
},
images: {
path: "images",
transform: (value) => (Array.isArray(value) ? value : undefined),
},
category_1: {
path: "category_1",
},
category_2: {
path: "category_2",
},
category_3: {
path: "category_3",
},
categories: {
path: "categories",
transform: (val) => {
if (typeof val === "string") {
return val.split(";;").map((category) => category.trim());
}
return undefined;
},
},
google_product_category: {
path: "google_product_category",
transform: (value, transformedObject, originalObject) =>
value
? value
: getCategoriesConcatedForGPC(
originalObject.category_1,
originalObject.category_2,
originalObject.category_3
),
},
cbu: {
path: "comportamiento_bajo_umbral",
transform: (value) => (value ? +value : undefined),
},
product_line: {
path: "product_line",
},
linea: {
path: "linea",
},
marketplace: {
transform: (value, transformedObject, originalObject) =>
getMarketplace(originalObject.marketplace),
},
ean: {
transform: (value, transformedObject, originalObject) =>
getEan(originalObject),
},
simples_skus: {
path: "simples_skus",
transform: (simples_skus) => {
if (typeof simples_skus === "string") {
return simples_skus.split(",").map((sku) => sku.trim());
}
return undefined;
},
},
comercializable: {
path: "comercializable",
},
extra_days_code: {
path: "extra_days_code",
},
extend_zone: {
path: "extend_zone",
transform: (val) => {
if (val !== undefined) {
return val === '1';
}
}
},
tiempo_entrega_max: {
path: "tiempo_entrega_max",
transform: (val) => {
if (val || val === 0) {
const number = parseInt(val);
return isNaN(number) ? null : number;
}
},
},
tiempo_entrega_max_sin_stock: {
path: "tiempo_entrega_max_sin_stock",
transform: (val) => {
if (val || val === 0) {
const number = parseInt(val);
return isNaN(number) ? null : number;
}
},
},
is_best_seller: {
path: "is_best_seller",
},
reviews_io: {
path: "reviews_io",
},
label_discount: {
path: "label_discount",
transform: (val) => {
if (val !== undefined) {
return val ?? '';
}
}
},
potencial: {
path: "potencial",
},
master_scores: {
path: "master_scores",
},
recommended_items: {
path: "recommended_items",
transform: (val) => {
if (typeof val === "string") {
return val.split(",").map((e) => e.trim());
}
return undefined;
},
"api_import": {
"indexName": "recommended"
}
},
tags_descuento: {
path: "tags_descuento",
},
extra_tags: {
path: "extra_tags",
transform: (val) => {
if (val !== undefined) {
return Array.isArray(val) ? val : [];
}
}
},
landing_pages: {
path: "landing_pages",
},
has_cashback: {
path: "has_cashback",
},
additional_services: {
path: "additional_services",
transform: (val) => {
if (val !== undefined) {
return Array.isArray(val) ? val : [];
}
}
},
enhanced_attributes: {
path: "enhanced_attributes",
},
facetable_attributes: {
path: "facetable_attributes",
},
availability: {
"path": "new_stock",
"transform": (new_stock) => new_stock !== undefined || new_stock !== null ? new_stock : undefined,
"api_import": false,
},
stock: {
"path": "stock",
"api_import": false,
"api_export": false,
},
price: {
"path": "msrp",
"transform": (msrp) => msrp ? +(Number(msrp).toFixed(2)) : undefined,
"api_import": false,
},
sale_price: {
"path": "price",
"transform": (price) => price ? +(Number(price).toFixed(2)) : undefined,
"api_import": false,
},
};
const valueToArray = (value) => {
if (value === undefined || value === null) return undefined;
if (Array.isArray(value)) return value;
return [value];
};
const getStatus = (status) => {
if (status === null) return undefined;
return +status === 1;
};
const getCategoriesConcatedForGPC = (category_1, category_2, category_3) => {
if (category_1 && category_2) {
return category_3
? `${category_1}>${category_2}>${category_3}`
: `${category_1}>${category_2}`;
}
return undefined;
};
const getEan = (product) => {
return product.ean ? product.ean : undefined;
};
const getMarketplace = (marketplace) => {
return Array.isArray(marketplace) ? marketplace : undefined;
};
const getCustomLabels = (data) => {
if (
!data.promotion &&
!data.label_discount &&
(data.promotion === null ||
data.label_discount === null ||
data.promotion === "" ||
data.label_discount === "")
) {
return [];
}
if (data.promotion) {
return [valueToArray(data.promotion).join(",")];
}
if (data.label_discount) {
return [valueToArray(data.label_discount).join(",")];
}
return undefined;
};
Esta plantilla es procesada usando la libreria json-map-transform
Atributos especiales
Se calculan y almacenan algunos atributos especiales junto con la información del producto que funcionan para manejar el flujo de actualizacion y publicación. No se deben enviar en comunicaciones de RabbitMQ ya que Ares los crea y actualiza.
- link: Se crea automaticamente la ruta de un producto en formato URL, se usa el prefijo 'https://www.gaiadesign.com.mx/p/' despues tomando el nombre del producto, eliminando guiones, espacios, mayúsculas y acentos, y agregando su identificador sku al final. Ej.
https://www.gaiadesign.com.mx/p/producto-prueba/11_1.html - elements: Cuando el producto es gaiagroped se almacena un objeto que guarda información de los productos que lo componen, solo se registran los atributos que permiten definir la visibilidad (salable, tiempo_entrega_max_sin_stock, tiempo_entrega_max, extend_zone, cbu, stock, units).
- completed: Almacena un valor boolean para indicar si el producto tiene los atributos minimos necesarios para ser publicado en los canales. Los atributos son name, description, link, availability, price, sale_price, product_type, status, visibility, cbu, google_product_category, category_1 y category_2.
- salable: Este campo boolean define si el producto puede ser vendido, las reglas son las siguientes según el tipo de producto:
- Cuando el producto es simple entonces debe cumplirse lo siguiente
-
const isActive = product?.comercializable === 1 && product?.status;
const salableByStock =
product?.cbu === CBU.NDPC && product?.availability > 0;
const salableByCbu = product?.cbu === CBU.DM;
salable = isActive && (salableByStock || salableByCbu);
-
- Cuando el producto es gaiagrouped entonces debe cumplirse lo siguiente
-
const allElementsSalable = Object.values(product.elements).every(e => e.salable === true);
const allElementsDM = Object.values(product.elements).every(e => e?.cbu === CBU.DM);
const setEnabled = product?.comercializable == 1 && product?.status;
const setWithStock = product?.availability > 0;
salable = setEnabled
&& allElementsSalable
&& (setWithStock || allElementsDM);
-
- Cuando el producto es simple entonces debe cumplirse lo siguiente
- channels: Este campo lo genera y actualiza Ares para llevar el control de los canales donde se ha publicado el producto.