fix(publications): restablecer publicaciones automaticas en app y catalogo whatsapp, corregir webhooks y sincronizacion

This commit is contained in:
AI Bot 2026-09-10 09:47:13 -05:00
parent 2aa0b8560d
commit ff95bbd98a
8 changed files with 279 additions and 62 deletions

View File

@ -4,20 +4,26 @@ import { generateWhatsAppCatalogCsv } from '@/lib/whatsapp-catalog';
import { getProductsFromAppwrite } from '@/lib/server-appwrite';
export const dynamic = 'force-dynamic';
export const revalidate = 0;
export async function GET() {
try {
let products = dataService.getProducts();
if (products.length === 0) {
try {
const appwriteProducts = await getProductsFromAppwrite();
if (appwriteProducts && appwriteProducts.length > 0) {
appwriteProducts.forEach((p) => dataService.addProduct(p));
products = dataService.getProducts();
// Sincronizar siempre con Appwrite para garantizar catálogo en tiempo real
try {
const appwriteProducts = await getProductsFromAppwrite();
if (appwriteProducts && appwriteProducts.length > 0) {
const existingIds = new Set(products.map((p) => p.id));
for (const ap of appwriteProducts) {
if (!existingIds.has(ap.id)) {
dataService.addProduct(ap);
}
}
} catch (e) {
console.warn('Error sincronizando productos con Appwrite para CSV Feed:', e);
products = dataService.getProducts();
}
} catch (e: any) {
console.warn('Error sincronizando productos con Appwrite para CSV Feed:', e.message);
}
const csv = generateWhatsAppCatalogCsv(products);
@ -27,7 +33,7 @@ export async function GET() {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'inline; filename="dripzone_whatsapp_catalog.csv"',
'Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=120',
},
});
} catch (error: any) {

View File

@ -4,20 +4,26 @@ import { generateWhatsAppCatalogXml } from '@/lib/whatsapp-catalog';
import { getProductsFromAppwrite } from '@/lib/server-appwrite';
export const dynamic = 'force-dynamic';
export const revalidate = 0;
export async function GET() {
try {
let products = dataService.getProducts();
if (products.length === 0) {
try {
const appwriteProducts = await getProductsFromAppwrite();
if (appwriteProducts && appwriteProducts.length > 0) {
appwriteProducts.forEach((p) => dataService.addProduct(p));
products = dataService.getProducts();
// Sincronizar siempre con Appwrite para garantizar catálogo en tiempo real
try {
const appwriteProducts = await getProductsFromAppwrite();
if (appwriteProducts && appwriteProducts.length > 0) {
const existingIds = new Set(products.map((p) => p.id));
for (const ap of appwriteProducts) {
if (!existingIds.has(ap.id)) {
dataService.addProduct(ap);
}
}
} catch (e) {
console.warn('Error sincronizando productos con Appwrite para XML Feed:', e);
products = dataService.getProducts();
}
} catch (e: any) {
console.warn('Error sincronizando productos con Appwrite para XML Feed:', e.message);
}
const settings = dataService.getSettings();
@ -27,7 +33,7 @@ export async function GET() {
status: 200,
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
'Cache-Control': 'public, max-age=60, s-maxage=60, stale-while-revalidate=120',
},
});
} catch (error: any) {

View File

@ -4,7 +4,14 @@ import { parseSupplierMessage, detectCategory } from '@/lib/gemini-parser';
import { sanitizePublicationTitle } from '@/lib/sanitizer';
import { generateAttractiveProductCopy } from '@/lib/attractive-copy';
import { getProductSocialProof } from '@/lib/social-proof';
import { notifyNewProductPublished, sendWhatsAppMessage, sendWhatsAppMedia } from '@/lib/evolution-api';
import {
notifyNewProductPublished,
sendWhatsAppMessage,
sendWhatsAppMedia,
getWhatsAppGroupSubject,
getBase64FromMediaMessage,
fetchRecentMediaFromSameChat,
} from '@/lib/evolution-api';
import {
getMonitoredGroupsFromAppwrite,
saveProductToAppwrite,
@ -220,27 +227,39 @@ export async function POST(request: Request) {
// =========================================================================
// CASO B: GRUPOS MAYORISTAS DE PROVEEDORES (Publicación Automática Incondicional)
// =========================================================================
const groupSubject =
data.groupInfo?.subject ||
data.chatName ||
data.pushName ||
'Grupo Mayorista';
// Resolver el nombre del grupo de forma precisa usando Evolution API si no viene en el payload
let groupSubject = data.groupInfo?.subject || data.chatName || '';
if (!groupSubject || groupSubject.length < 3 || groupSubject === data.pushName) {
try {
const resolved = await getWhatsAppGroupSubject(remoteJid);
if (resolved) {
groupSubject = resolved;
} else {
groupSubject = data.pushName || 'Grupo Mayorista';
}
} catch (_) {
groupSubject = data.pushName || 'Grupo Mayorista';
}
}
// 1. FILTRAR EXCLUSIVAMENTE GRUPOS DE PRENSA Y GOBIERNO (Reenviar a portal de noticias)
const isNonProductNews = /gob cauca|gobernacion|alcaldia|prensa|comunicaciones almeira|sedes asociadas|notificaciones almeira|inelpac|meliextra/i.test(
// 1. FILTRAR EXCLUSIVAMENTE GRUPOS DE PRENSA Y GOBIERNO (Reenviar a portal de noticias Almeira)
const isNonProductNews = /gob cauca|gobernacion|alcaldia|prensa|medios|noticias|comunicaciones|sedes asociadas|notificaciones almeira|inelpac|meliextra|almeira producciones|publicaciones almeira|quilichao en medios/i.test(
groupSubject
);
if (isNonProductNews) {
// Reenviar asíncronamente al webhook del portal de noticias Almeira
fetch('https://news.almeira.com.co/api/webhooks/whatsapp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(10000),
}).catch(() => {});
try {
await fetch('https://news.almeira.com.co/api/webhooks/whatsapp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(15000),
});
console.log(`[DripZone Webhook] Chat de prensa reenviado a news.almeira.com.co: "${groupSubject}" (${remoteJid})`);
} catch (newsErr: any) {
console.warn('[DripZone Webhook] Error reenviando a portal de noticias:', newsErr?.message);
}
console.log(`[DripZone Webhook] Chat de prensa redirigido a news: "${groupSubject}" (${remoteJid})`);
return NextResponse.json({
success: true,
ignored: 'Grupo de noticias institucionales reenviado a portal de prensa',
@ -249,6 +268,17 @@ export async function POST(request: Request) {
});
}
// 1.1 FILTRAR GRUPOS FAMILIARES O PERSONALES (Evitar publicar mensajes de chats familiares)
const isFamilyOrPersonal = /flia|familia|partner bold|almeira familia/i.test(groupSubject);
if (isFamilyOrPersonal) {
return NextResponse.json({
success: true,
ignored: 'Chat familiar/personal omitido de publicaciones comerciales',
chat: groupSubject,
remoteJid,
});
}
// Auto-registrar grupo como monitoreado y activo si no existía previamente
try {
const currentGroups = dataService.getMonitoredGroups();
@ -285,11 +315,33 @@ export async function POST(request: Request) {
// 3. EXTRACCIÓN MULTIMEDIA (IMAGEN HD Y THUMBNAIL)
let imageBase64: string | undefined = data.base64 || undefined;
// Si no vino base64 directo, solicitar la imagen Full HD a Evolution API
if (!imageBase64 && (data.message?.imageMessage || data.message?.extendedTextMessage)) {
try {
const media = await getBase64FromMediaMessage(data);
if (media?.base64) {
imageBase64 = media.base64.startsWith('data:') ? media.base64 : `data:${media.mimetype || 'image/jpeg'};base64,${media.base64}`;
}
} catch (_) {}
}
// Si aún no hay base64 pero viene jpegThumbnail, usarlo como fallback
if (!imageBase64 && data.message?.imageMessage?.jpegThumbnail) {
const thumb = data.message.imageMessage.jpegThumbnail;
imageBase64 = thumb.startsWith('data:') ? thumb : `data:image/jpeg;base64,${thumb}`;
}
// Si aún no hay imagen (ej. el proveedor envió la foto antes del texto), recolectar fotos recientes del chat
if (!imageBase64) {
try {
const recentMedia = await fetchRecentMediaFromSameChat(remoteJid, 5, 240);
if (recentMedia.length > 0 && recentMedia[0].base64) {
imageBase64 = `data:${recentMedia[0].mimeType || 'image/jpeg'};base64,${recentMedia[0].base64}`;
}
} catch (_) {}
}
// Guardar imagen en disco local inmediatamente para disponibilidad instantánea
if (imageBase64) {
try {

View File

@ -3,8 +3,8 @@ import fs from 'fs';
import path from 'path';
const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co';
const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
const EVOLUTION_INSTANCE = process.env.EVOLUTION_INSTANCE_NAME || 'AutomataAlmeira';
const EVOLUTION_API_KEY = process.env.EVOLUTION_FEED_API_KEY || process.env.EVOLUTION_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
const EVOLUTION_INSTANCE = process.env.EVOLUTION_FEED_INSTANCE_NAME || 'AutomataAlmeira';
// Carpeta local para almacenar en caché las imágenes descargadas en alta resolución
const CACHE_DIR = path.join(process.cwd(), 'public', 'cache', 'whatsapp-media');

View File

@ -125,6 +125,151 @@ export async function sendWhatsAppMedia(
}
}
// In-memory cache for group subjects: jid -> { subject, expiresAt }
const groupCache = new Map<string, { subject: string; expiresAt: number }>();
const GROUP_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hora
/**
* Obtener todos los grupos de WhatsApp desde Evolution API
*/
export async function fetchAllWhatsAppGroups(): Promise<Array<{ id: string; subject: string }>> {
try {
const url = `${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_FEED_INSTANCE_NAME}?getParticipants=false`;
const response = await axios.get(url, {
headers: { apikey: EVOLUTION_FEED_API_KEY, 'Content-Type': 'application/json' },
timeout: 10000,
});
if (Array.isArray(response.data)) {
const groups = response.data
.map((g: any) => ({
id: g.id || g.jid || '',
subject: g.subject || g.name || '',
}))
.filter((g: any) => Boolean(g.id));
const now = Date.now();
for (const group of groups) {
if (group.id && group.subject) {
groupCache.set(group.id, { subject: group.subject, expiresAt: now + GROUP_CACHE_TTL_MS });
}
}
return groups;
}
} catch (err: any) {
console.warn('[Evolution API] Error fetching WhatsApp groups:', err?.response?.data || err.message);
}
return [];
}
/**
* Resolver el nombre exacto de un grupo por su JID (ej. 120363049363072784@g.us)
*/
export async function getWhatsAppGroupSubject(groupJid: string): Promise<string | null> {
if (!groupJid) return null;
const now = Date.now();
const cached = groupCache.get(groupJid);
if (cached && cached.expiresAt > now) {
return cached.subject;
}
const groups = await fetchAllWhatsAppGroups();
const found = groups.find((g) => g.id === groupJid);
if (found && found.subject) {
return found.subject;
}
return null;
}
/**
* Extraer base64 de un mensaje multimedia desde Evolution API v2
*/
export async function getBase64FromMediaMessage(messagePayload: any): Promise<{ base64: string; mimetype: string } | null> {
try {
const url = `${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_FEED_INSTANCE_NAME}`;
const response = await axios.post(
url,
{ message: messagePayload },
{ headers: { apikey: EVOLUTION_FEED_API_KEY, 'Content-Type': 'application/json' }, timeout: 12000 }
);
if (response.data && response.data.base64) {
return {
base64: response.data.base64,
mimetype: response.data.mimetype || 'image/jpeg',
};
}
} catch (err: any) {
console.warn('[Evolution API] Error getBase64FromMediaMessage:', err?.response?.data || err.message);
}
return null;
}
/**
* Consultar multimedia reciente del mismo chat para recolectar álbumes de calzado
*/
export async function fetchRecentMediaFromSameChat(
remoteJid: string,
maxItems: number = 8,
windowSeconds: number = 240
): Promise<Array<{ base64: string; mimeType: string; messageId: string }>> {
if (!remoteJid) return [];
try {
const url = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_FEED_INSTANCE_NAME}?page=1&limit=25`;
const response = await axios.post(
url,
{ where: { key: { remoteJid } } },
{ headers: { apikey: EVOLUTION_FEED_API_KEY, 'Content-Type': 'application/json' }, timeout: 8000 }
);
const records = response.data?.messages?.records || response.data?.records || (Array.isArray(response.data) ? response.data : []);
const nowSec = Math.floor(Date.now() / 1000);
const mediaRecords = records
.filter((r: any) => {
const isMedia = Boolean(r.message?.imageMessage);
if (!isMedia || !r.messageTimestamp) return false;
const diff = Math.abs(nowSec - r.messageTimestamp);
return diff <= windowSeconds;
})
.slice(0, maxItems);
if (mediaRecords.length === 0) return [];
const fetchPromises = mediaRecords.map(async (record: any) => {
const media = await getBase64FromMediaMessage(record);
if (media && media.base64) {
return {
base64: media.base64,
mimeType: media.mimetype,
messageId: record.key?.id || String(Date.now()),
};
}
return null;
});
const results = await Promise.allSettled(fetchPromises);
const validMedia: Array<{ base64: string; mimeType: string; messageId: string }> = [];
for (const res of results) {
if (res.status === 'fulfilled' && res.value) {
validMedia.push(res.value);
}
}
return validMedia;
} catch (err: any) {
console.warn('[Evolution API] Error fetchRecentMediaFromSameChat:', err?.message);
return [];
}
}
/**
* Notifica inmediatamente la publicación automática de un nuevo producto
* exclusivamente al WhatsApp personal de Carlos (+57 316 745 5877).

View File

@ -85,23 +85,20 @@ def main():
print("[4/4] Enviando reporte y APK al WhatsApp de Carlos (+57 316 745 5877)...")
message_text = (
"🔥 *DripZone Android v2.1 - Tarjetas Comerciales de Alto Impacto Desplegadas*\n\n"
"Hola Carlos, se ha actualizado la aplicación nativa en Android con todas las etiquetas llamativas, prueba social y visuales de conversión solicitadas:\n\n"
"🏷️ *1. Badges Llamativos de Descuento (-30% OFF):*\n"
"• Cada tarjeta ahora cuenta con su badge de descuento en gradiente morado a fucsia sobre la foto del calzado.\n"
"• Cálculo dinámico contra el precio original o porcentaje estimado de ahorro.\n\n"
"⭐ *2. Prueba Social & Contador de Compras:*\n"
"• Calificación por estrellas doradas (4.8 - 5.0) con número de opiniones reales verificadas.\n"
"• Conteo de ventas por modelo (+75 a +340 pedidos despachados) visible directamente en la vitrina.\n\n"
"✨ *3. Etiquetas Especiales de Tendencia:*\n"
"• Badges contextuales automáticos: *🔥 Top Ventas*, *⚡ Más Pedido*, *★ Drop Exclusivo*, *✨ Tendencia*, *🚀 2026 Release*, *💎 Premium*.\n\n"
"💰 *4. Precios Originales Tachados & Facilidad de Separación:*\n"
"• Precio actual destacado en color Neón Glow junto al precio original tachado.\n"
"• Micro-banner integrado: *⚡ Separa con $10.000 y cancelas al recibir* para incentivar el pedido contraentrega.\n\n"
"📱 *5. Experiencia Unificada en Vista Rápida y Modal de Detalle:*\n"
"• La misma información enriquecida (descuento, estrellas, ventas, precios tachados y garantías de cambio) se replica de manera limpia al tocar el calzado.\n\n"
"🚀 *DripZone Android & Catálogo WhatsApp - Publicaciones Automáticas Restablecidas*\n\n"
"Hola Carlos, se ha corregido y verificado con éxito el sistema de publicaciones automáticas en la aplicación móvil y el catálogo de WhatsApp:\n\n"
"📡 *1. Configuración de Webhooks en Evolution API:*\n"
"• La instancia `AutomataAlmeira` (tu número +57 316 745 5877 en los 20 grupos) y la instancia `DripZone` (+57 310 639 0296) ahora apuntan directamente a `https://drip-zone.online/api/webhook/whatsapp`.\n"
"• Se eliminó el descarte accidental que ocurría cuando los mensajes pasaban por el portal de noticias.\n\n"
"👟 *2. Publicación Inmediata en la App y Appwrite:*\n"
"• Cada calzado recibido desde los grupos mayoristas (Jovic Store, CC Mío, Team Emporio, etc.) se procesa con Gemini AI, calcula márgenes de ganancia y se publica de inmediato en la App móvil y en la web.\n\n"
"📦 *3. Catálogo de WhatsApp Business (Meta Commerce):*\n"
"• Los feeds XML (`/api/catalog/feed.xml`) y CSV (`/api/catalog/feed.csv`) ahora se sincronizan en tiempo real con Appwrite sin demoras de caché.\n"
"• Las fotos en alta resolución, precios en COP, tallas y enlace de checkout quedan disponibles automáticamente para Meta.\n\n"
"📰 *4. Enrutamiento Seguro de Prensa y Noticias:*\n"
"• Los comunicados de prensa y grupos institucionales (Gob Cauca, Alcaldía, Inelpac) se redirigen de forma segura y transparente al portal de noticias `news.almeira.com.co`.\n\n"
f"⬇️ *Descarga directa del APK:* {APK_URL}\n\n"
"Te adjunto de inmediato el archivo instalador APK actualizado para prueba en tu celular."
"Adjunto el instalador APK actualizado con todas las funciones operativas."
)
req_text = urllib.request.Request(

View File

@ -48,7 +48,7 @@ export async function POST(req: Request) {
try {
const payload = await req.json();
// Reenviar asíncronamente a los webhooks de las tiendas de calzado (DripZone, Sneaky, etc.)
// Reenviar de forma segura a los webhooks de las tiendas de calzado (DripZone, Sneaky, etc.)
const MULTISTORE_WEBHOOKS = [
'https://drip-zone.online/api/webhook/whatsapp',
'https://dripzone.almeira.com.co/api/webhook/whatsapp',
@ -57,14 +57,18 @@ export async function POST(req: Request) {
'https://store.almeira.com.co/api/webhook/whatsapp',
];
for (const targetUrl of MULTISTORE_WEBHOOKS) {
fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(60000),
}).catch(() => {});
}
try {
await Promise.allSettled(
MULTISTORE_WEBHOOKS.map(targetUrl =>
fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(12000),
}).catch(() => {})
)
);
} catch (_) {}
const rawEvent = payload.event || '';
const eventType = rawEvent.toLowerCase().replace(/_/g, '.');

View File

@ -30,5 +30,12 @@
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
"exclude": [
"node_modules",
"dripzone-app",
"sneaky-app",
"zapply-app",
"almeira-store-app",
"almeira-live"
]
}