310 lines
12 KiB
TypeScript
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 });
|
|
}
|
|
}
|