almeira-web/dripzone-app/src/app/api/whatsapp-feed/route.ts

310 lines
12 KiB
TypeScript

import { NextResponse } from 'next/server';
import { dataService } from '@/lib/data-service';
import { parseSupplierMessage } from '@/lib/gemini-parser';
import { getProductSocialProof } from '@/lib/social-proof';
import { generateAttractiveProductCopy } from '@/lib/attractive-copy';
import { SupplierPost, MonitoredWhatsAppGroup } from '@/types';
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_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'AutomataAlmeira';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid');
// 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const groupsMap = new Map<string, string>();
localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
let liveGroups: any[] = [];
try {
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
const allChats = await chatsRes.json();
if (Array.isArray(allChats)) {
liveGroups = allChats.filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
liveGroups.forEach((g: any) => {
const jid = g.remoteJid || g.id || g.jid;
const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
if (jid) groupsMap.set(jid, name);
});
}
} catch (e) {
console.warn('Error resolviendo chats de WhatsApp:', e);
}
// 2. Determinar JIDs objetivo a consultar
let targetJids: string[] = [];
if (filterJid && filterJid !== 'all') {
targetJids = [filterJid];
} else {
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
if (monitored.length > 0) {
targetJids = monitored;
} else if (liveGroups.length > 0) {
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
} else {
targetJids = Array.from(groupsMap.keys());
}
}
if (targetJids.length === 0) {
return NextResponse.json({
success: true,
isLive: true,
totalFound: 0,
posts: dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
}
// 3. Consultar mensajes de cada grupo objetivo en paralelo
const groupFetchPromises = targetJids.map(async (jid) => {
try {
const msgRes = await fetch(
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
{
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
}
);
const msgData = await msgRes.json();
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
} catch (err) {
return [];
}
});
const results = await Promise.allSettled(groupFetchPromises);
const allRecords: any[] = [];
for (const res of results) {
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
allRecords.push(...res.value);
}
}
// 4. Ordenar cronológicamente (más recientes primero)
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
// 5. Procesar cada mensaje y descargar imágenes/IA
const processedPosts: SupplierPost[] = [];
// Limitar a los 60 más relevantes para rendimiento óptimo
const sliceRecords = allRecords.slice(0, 60);
for (const record of sliceRecords) {
const remoteJid = record.key?.remoteJid || '';
const text =
record.message?.conversation ||
record.message?.extendedTextMessage?.text ||
record.message?.imageMessage?.caption ||
record.message?.videoMessage?.caption ||
'';
const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista';
const hasImage = Boolean(record.message?.imageMessage);
// Si no tiene texto ni imagen, omitir
if (!text && !hasImage) continue;
let imageUrls: string[] = [];
// Descargar imagen en Base64 desde Evolution API si está presente
if (hasImage) {
try {
const mediaUrl = `${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_INSTANCE_NAME}`;
const mediaRes = await fetch(mediaUrl, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: record, convertToMp4: false }),
});
const mediaData = await mediaRes.json();
if (mediaData.base64) {
const mime = mediaData.mimetype || 'image/jpeg';
imageUrls.push(`data:${mime};base64,${mediaData.base64}`);
}
} catch (mediaErr) {
console.warn('Error descargando imagen de WhatsApp:', mediaErr);
}
}
if (imageUrls.length === 0) {
imageUrls.push(
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
);
}
// Parsear oferta de calzado con IA
const extracted = await parseSupplierMessage(text || groupName);
const timestamp = record.messageTimestamp
? new Date(record.messageTimestamp * 1000).toLocaleString('es-CO', {
hour: '2-digit',
minute: '2-digit',
day: '2-digit',
month: 'short',
})
: 'Reciente';
const post: SupplierPost = {
id: `post-${record.key?.id || Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
channelName: groupName,
channelJid: remoteJid,
rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`,
rawImages: imageUrls,
extractedProduct: extracted,
status: 'pending_review',
receivedAt: timestamp,
supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
};
processedPosts.push(post);
}
// Guardar en el servicio local los nuevos posts
if (processedPosts.length > 0) {
dataService.saveSupplierPosts(processedPosts);
}
return NextResponse.json({
success: true,
isLive: true,
totalFound: processedPosts.length,
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
} catch (error: any) {
console.error('Error en /api/whatsapp-feed:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const body = await request.json();
const { action, post, posts, customRetailPrice, customName, customColorName, customSizes } = body;
// 1. PUBLICAR UN PRODUCTO A LA TIENDA (APP MÓVIL Y WEB)
if (action === 'publish' && post) {
const retailPrice =
customRetailPrice ||
post.extractedProduct?.suggestedRetailPrice ||
(post.extractedProduct?.supplierPrice ? post.extractedProduct.supplierPrice + 35000 : 125000);
const prodName = customName || post.extractedProduct?.name || 'Zapatillas Urbanas';
const prodBrand = post.extractedProduct?.brand || 'Nike';
const proof = getProductSocialProof({ name: prodName, brand: prodBrand });
const newProduct = {
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
name: prodName,
modelName: prodName,
colorName: customColorName || post.extractedProduct?.colorName,
brand: prodBrand,
supplierPrice: post.extractedProduct?.supplierPrice || 75000,
retailPrice: retailPrice,
originalPrice: Math.round(retailPrice * 1.35),
sizes: customSizes || post.extractedProduct?.sizes || ['37', '38', '39', '40', '41', '42', '43'],
images: Array.isArray(post.rawImages) && post.rawImages.length > 0 ? post.rawImages : ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'],
description: generateAttractiveProductCopy({ name: prodName, brand: prodBrand, retailPrice }),
category: post.extractedProduct?.category || 'Calzado / Sneakers',
inStock: true,
isActive: true,
rating: proof.rating,
reviewsCount: proof.reviewsCount,
salesCount: proof.salesCount,
originBadge: proof.badge,
supplierName: post.supplierName || post.channelName || 'Proveedor Mayorista',
supplierPhone: post.supplierPhone || '573167455877',
};
const added = dataService.addProduct(newProduct as any);
dataService.updateSupplierPostStatus(post.id, 'published');
return NextResponse.json({
success: true,
message: `¡${prodName} publicado y agrupado en el catálogo!`,
product: added,
});
}
// 2. PUBLICACIÓN MASIVA (BULK)
if (action === 'bulk-publish' && Array.isArray(posts)) {
const createdProducts = [];
for (const p of posts) {
const retailPrice =
p.extractedProduct?.suggestedRetailPrice ||
(p.extractedProduct?.supplierPrice ? p.extractedProduct.supplierPrice + 35000 : 125000);
const prodName = p.extractedProduct?.name || 'Zapatillas Urbanas';
const prodBrand = p.extractedProduct?.brand || 'Nike';
const proof = getProductSocialProof({ name: prodName, brand: prodBrand });
const newProd = {
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
name: prodName,
modelName: prodName,
brand: prodBrand,
supplierPrice: p.extractedProduct?.supplierPrice || 75000,
retailPrice,
originalPrice: Math.round(retailPrice * 1.35),
sizes: p.extractedProduct?.sizes || ['37', '38', '39', '40', '41', '42', '43'],
images: p.rawImages || ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'],
description: generateAttractiveProductCopy({ name: prodName, brand: prodBrand, retailPrice }),
category: p.extractedProduct?.category || 'Calzado / Sneakers',
inStock: true,
isActive: true,
rating: proof.rating,
reviewsCount: proof.reviewsCount,
salesCount: proof.salesCount,
originBadge: proof.badge,
supplierName: p.supplierName || p.channelName || 'Proveedor Mayorista',
supplierPhone: p.supplierPhone || '573167455877',
};
const added = dataService.addProduct(newProd as any);
dataService.updateSupplierPostStatus(p.id, 'published');
createdProducts.push(added);
}
return NextResponse.json({
success: true,
message: `¡Se publicaron y agruparon ${createdProducts.length} productos en la App Móvil y Web!`,
products: createdProducts,
});
}
// 3. DESCARTAR POST
if (action === 'discard' && post?.id) {
dataService.updateSupplierPostStatus(post.id, 'discarded');
return NextResponse.json({ success: true, message: 'Mensaje descartado' });
}
return NextResponse.json({ success: false, error: 'Acción no válida' }, { status: 400 });
} catch (err: any) {
console.error('Error en POST /api/whatsapp-feed:', err);
return NextResponse.json({ success: false, error: err.message }, { status: 500 });
}
}