🔗 Integración de Ares con GMC
La integración con Google Merchant Center (GMC) permite publicar productos de Ares hacia un Google Spreadsheet, el cual es consumido diariamente por GMC siguiendo las reglas con su documentación oficial.
⚙️ Flujo de Integración
| Etapa | Descripción |
|---|---|
| Ejecución | Se dispara 1 vez al día mediante un cron job en AWS EventBridge. |
| Extracción | Se consultan en MongoDB (Ares) los productos con filtros: completed = true, comercializable = 1, visibility = CatalogSearch. |
| Transformación | Los productos se convierten al formato GMC usando un map template y reglas adicionales (afterTransform). |
| Validaciones | Se descartan productos sin imagen y donde sale price > price. |
| Publicación | Se limpian y cargan los datos en un Google Spreadsheet (sheet: Automatizado). |
| Consumo GMC | GMC sincroniza automáticamente 1 vez al día leyendo el spreadsheet. |
↔️ Mapeo de datos
Plantilla
import { transform } from "lodash";
export const map = {
template: {
id: {
path: "sku",
transform: (value, transformedObject, originalObject) =>
value ?? originalObject?._id,
},
title: {
path: "name",
},
description: {
path: "description",
},
availability: {
path: "salable",
transform: (salable, transformedObject, originalObject) => {
if (salable) {
return originalObject?.availability > 0 ? "in stock" : "backorder";
} else {
return "out of stock";
}
},
},
link: {
path: "product_link",
transform: (val) => val,
},
'availability date': {
path: "sku",
transform: (val, transformedObject, originalObject) => {
if ("backorder" === transformedObject?.availability) {
const days = originalObject.tiempo_entrega_max_sin_stock;
return addBusinessDays(new Date(), 20);
}
},
},
'image link': {
path: "image",
transform: (value, transformedObject) => {
return replaceImageHost(value);
},
},
price: {
path: "price",
transform: (val, transformedObject, originalObject) => {
if (val) {
return `${val} MXN`
} else if (originalObject?.sale_price) {
return `${originalObject.sale_price} MXN`
}
},
},
'sale price': {
path: "sale_price",
transform: (val, transformedObject, originalObject) => {
if (val) {
return `${val} MXN`
} else if (originalObject?.price) {
return `${originalObject.price} MXN`
}
},
},
'identifier exists': {
path: "name",
transform: (value, transformedObject, originalObject) => 'no',
},
gtin: {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
mpn: {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
brand: {
path: "name",
transform: (value, transformedObject, originalObject) => 'GAIA',
},
'product highlight': {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
'product detail': {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
'additional image link': {
path: "images",
transform: (images, transformedObject, originalObject) => {
const options = [];
if (Array.isArray(images)) {
images.forEach(img => {
if (img.includes("_miniambiente") || img.includes("_infografico")) {
options.push(img);
}
});
}
if (originalObject?.image) {
options.push(originalObject.image);
}
return replaceImageHost(options[0]);
},
},
condition: {
path: "name",
transform: (value, transformedObject, originalObject) => 'new',
},
adult: {
path: "name",
transform: (value, transformedObject, originalObject) => 'no',
},
color: {
path: "color_principal",
},
material: {
path: "material",
transform: (value) => {
if (value && Array.isArray(value)) {
return value[0];
}
if (value) {
return value;
}
return undefined;
},
},
size: {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
gender: {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
pattern: {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
'age group': {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
multipack: {
path: "name",
transform: (value, transformedObject, originalObject) => {
return undefined;
},
},
'is bundle': {
path: "product_type",
transform: (value, transformedObject, originalObject) => {
return 'gaiagrouped' == value ? 'yes' : 'no';
},
},
'unit pricing measure': {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
'unit pricing base measure': {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
'energy efficiency class': {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
'min energy efficiency class': {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
'min energy efficiency class': {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
'item group id': {
path: "sku",
transform: (val, transformedObject, originalObject) => originalObject._id.split('_')[0]
},
'sell on google quantity': {
path: "name",
transform: (value, transformedObject, originalObject) => undefined,
},
'custom label 0': {
path: "sku",
transform: (value, transformedObject, originalObject) => {
const cat2 = originalObject.category_2;
return `${cat2}`;
},
},
'custom label 1': {
path: "name",
transform: (value, transformedObject, originalObject) => {
const cat2 = originalObject.category_3;
return `${cat2}`;
},
},
'custom label 2': {
path: "sku",
transform: (value, transformedObject, originalObject) => {
return Array.isArray(originalObject?.landing_pages) ? originalObject.landing_pages.join(',') : '';
},
},
},
afterTransform: null,
};
const getColorFilter = (originalObject, colorAttr, colorsMap) => {
const colorInfo = colorsMap.find(
(e) => e.key == originalObject[colorAttr]
) ?? { color: { hex: "#FFFFFF" }, translate: "" };
return {
name: colorInfo.translate,
color: colorInfo.color.hex,
refinement: `${colorInfo.translate};${colorInfo.color.hex}`,
};
};
const isExpressDelivery = (product) => {
let isExpressDelivery = false;
const isAvailable = product.salable;
const maxDeliveryTime = product.tiempo_entrega_max || 0;
const maxDeliveryTimeWithoutStock =
product.tiempo_entrega_max_sin_stock || 0;
const quantity = product.availability;
if (maxDeliveryTime && isAvailable) {
isExpressDelivery = quantity > 0 || (quantity == 0 && isAvailable);
const deliveryDays =
isExpressDelivery && quantity > 0
? maxDeliveryTime
: maxDeliveryTimeWithoutStock;
isExpressDelivery = deliveryDays <= 8; // ahora esta hardcodeado pero se seteaba en M2
}
return isExpressDelivery;
};
const replaceImageHost = (url) => {
if (url) {
const filename = url.split('/').pop();
return `https://cdnpim.gaia.design/${filename}`;
} else {
return url;
}
};
const addBusinessDays = (startDate, businessDaysToAdd) => {
const result = new Date(startDate);
let addedDays = 0;
while (addedDays < businessDaysToAdd) {
result.setDate(result.getDate() + 1);
const day = result.getDay();
if (day !== 0 && day !== 6) { // 0 = domingo, 6 = sábado
addedDays++;
}
}
const offset = -6 * 60; // -0600 en minutos
const pad = (n) => String(n).padStart(2, '0');
const formatted = `${result.getUTCFullYear()}-${pad(result.getUTCMonth() + 1)}-${pad(result.getUTCDate())}T${pad(result.getUTCHours())}:${pad(result.getUTCMinutes())}${offset >= 0 ? '+' : '-'}${pad(Math.abs(offset / 60))}00`;
return formatted;
}
Esta plantilla es procesada usando la libreria json-map-transform