import { NextResponse } from 'next/server'; import { dataService } from '@/lib/data-service'; import { parseSupplierMessage, detectCategory } from '@/lib/gemini-parser'; import { performMarketIntelligenceSearch } from '@/lib/market-intelligence'; import { getProductSocialProof } from '@/lib/social-proof'; import { generateAttractiveProductCopy } from '@/lib/attractive-copy'; import { sanitizePublicationTitle, sanitizePublicationText } from '@/lib/sanitizer'; import { getSupplierPostsFromAppwrite, saveSupplierPostToAppwrite, updateSupplierPostStatusInAppwrite, saveProductToAppwrite, getProductsFromAppwrite, } from '@/lib/server-appwrite'; 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') || '3000', 10); const maxPages = parseInt(searchParams.get('pages') || '30', 10); const filterJid = searchParams.get('groupJid'); const statusFilter = (searchParams.get('status') || 'pending').toLowerCase(); // 1. Obtener posts y productos existentes en Appwrite para control estricto de IDs de mensajes de WhatsApp const [savedPosts, existingProducts] = await Promise.all([ getSupplierPostsFromAppwrite(), getProductsFromAppwrite(), ]); // Índices de WhatsApp message IDs ya procesados const publishedMsgIds = new Set(); const discardedMsgIds = new Set(); const appwritePostMap = new Map(); savedPosts.forEach((p) => { appwritePostMap.set(p.id, p); const msgId = p.whatsappMessageId || p.id.replace('post-', ''); if (msgId) { if (p.status === 'published' || p.status === 'published_store' || p.status === 'published_facebook') { publishedMsgIds.add(msgId); publishedMsgIds.add(`post-${msgId}`); publishedMsgIds.add(p.id); } else if (p.status === 'discarded' || p.status === 'ignored') { discardedMsgIds.add(msgId); discardedMsgIds.add(`post-${msgId}`); discardedMsgIds.add(p.id); } } }); existingProducts.forEach((prod) => { if (prod.whatsappMessageId) { publishedMsgIds.add(prod.whatsappMessageId); publishedMsgIds.add(`post-${prod.whatsappMessageId}`); } }); // 2. Obtener grupos locales y descubrir grupos en vivo desde Evolution API const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups(); const groupsMap = new Map(); 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); } // 3. Determinar JIDs objetivo a consultar (excluyendo grupos de los que salimos o inactivos) const leftJids = new Set( localGroups.filter((g) => g.hasLeft || g.isStillMember === false || !g.isMonitored).map((g) => g.jid) ); let targetJids: string[] = []; if (filterJid && filterJid !== 'all') { targetJids = [filterJid]; } else { const monitored = localGroups.filter((g) => g.isMonitored && !g.hasLeft && g.isStillMember !== false).map((g) => g.jid); if (monitored.length > 0) { targetJids = monitored; } else if (liveGroups.length > 0) { targetJids = liveGroups .map((g: any) => g.remoteJid || g.id) .filter((jid: string) => !leftJids.has(jid)); } else { targetJids = Array.from(groupsMap.keys()).filter((jid: string) => !leftJids.has(jid)); } } if (!fetchLive || targetJids.length === 0) { const filteredSaved = savedPosts .filter((p) => { // Si el mensaje es de un grupo del que salimos y estamos en pendientes, no mostrarlo if (p.channelJid && leftJids.has(p.channelJid) && statusFilter === 'pending') return false; if (statusFilter === 'pending') return p.status === 'pending_review'; if (statusFilter === 'published') return p.status === 'published' || p.status === 'published_store'; if (statusFilter === 'discarded') return p.status === 'discarded'; return true; }) .map((p) => { // Si tiene thumbnail de baja resolución pero cuenta con ID de WhatsApp, suministrar la URL en Alta Resolución Full HD if (p.whatsappMessageId && Array.isArray(p.rawImages) && (p.rawImages.length === 0 || p.rawImages[0].startsWith('data:image'))) { return { ...p, rawImages: [`/api/whatsapp-media?id=${encodeURIComponent(p.whatsappMessageId)}&remoteJid=${encodeURIComponent(p.channelJid || '')}`], }; } return p; }); return NextResponse.json({ success: true, isLive: false, totalFound: filteredSaved.length, counts: { pending: savedPosts.filter((p) => p.status === 'pending_review').length, published: publishedMsgIds.size, discarded: discardedMsgIds.size, all: savedPosts.length, }, posts: filteredSaved, groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })), }); } // 4. Consultar mensajes de los grupos objetivo recorriendo todas las páginas en paralelo const groupFetchPromises = targetJids.map(async (jid) => { const allGroupRecords: any[] = []; const pagesToFetch = Array.from({ length: maxPages }, (_, i) => i + 1); // Lotes concurrentes de 10 páginas para máxima velocidad y profundidad const BATCH_SIZE = 10; for (let i = 0; i < pagesToFetch.length; i += BATCH_SIZE) { const batch = pagesToFetch.slice(i, i + BATCH_SIZE); const batchResults = await Promise.allSettled( batch.map(async (page) => { const msgRes = await fetch( `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=${page}&limit=50`, { 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 : []); }) ); let hitEnd = false; for (const res of batchResults) { if (res.status === 'fulfilled' && Array.isArray(res.value)) { allGroupRecords.push(...res.value); if (res.value.length === 0) { hitEnd = true; } } } if (hitEnd) break; } return allGroupRecords; }); 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); } } // Ordenar cronológicamente (más recientes primero) allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0)); // 5. Procesar mensajes con control estricto de identificador de WhatsApp const processedPosts: SupplierPost[] = []; const seenMessageIds = new Set(); for (const record of allRecords) { const remoteJid = record.key?.remoteJid || ''; const rawMsgId = (record.key?.id || '').trim(); const cleanMsgId = rawMsgId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 36); const postId = `post-${cleanMsgId || Date.now()}`; if (seenMessageIds.has(postId) || (rawMsgId && seenMessageIds.has(rawMsgId))) { continue; } seenMessageIds.add(postId); if (rawMsgId) seenMessageIds.add(rawMsgId); 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); if (!text && !hasImage) continue; const existingInAppwrite = appwritePostMap.get(postId) || savedPosts.find( (p) => p.id === postId || p.whatsappMessageId === rawMsgId || p.whatsappMessageId === cleanMsgId || (p.rawMessage === text && (p as any).channelJid === remoteJid) ); const isPublished = existingInAppwrite?.status === 'published' || existingInAppwrite?.status === 'published_store' || existingInAppwrite?.status === 'published_facebook' || publishedMsgIds.has(rawMsgId) || publishedMsgIds.has(cleanMsgId) || publishedMsgIds.has(postId); const isDiscarded = existingInAppwrite?.status === 'discarded' || existingInAppwrite?.status === 'ignored' || discardedMsgIds.has(rawMsgId) || discardedMsgIds.has(cleanMsgId) || discardedMsgIds.has(postId); const effectiveStatus: SupplierPost['status'] = isPublished ? 'published' : isDiscarded ? 'discarded' : 'pending_review'; if (statusFilter === 'pending') { if (isPublished || isDiscarded) continue; } else if (statusFilter === 'published') { if (!isPublished) continue; } else if (statusFilter === 'discarded') { if (!isDiscarded) continue; } let imageUrls: string[] = []; const imgMsg = record.message?.imageMessage; if (imgMsg) { if (rawMsgId) { // URL de streaming en Alta Resolución (Full HD) desde Evolution API imageUrls.push( `/api/whatsapp-media?id=${encodeURIComponent(rawMsgId)}&remoteJid=${encodeURIComponent(remoteJid)}` ); } else if (imgMsg.jpegThumbnail) { imageUrls.push(`data:image/jpeg;base64,${imgMsg.jpegThumbnail}`); } else if (imgMsg.url) { imageUrls.push(imgMsg.url); } } if (imageUrls.length === 0) { imageUrls.push( 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80' ); } let extracted = existingInAppwrite?.extractedProduct; if (!extracted) { const cleanInputText = text || ''; extracted = await parseSupplierMessage(cleanInputText, undefined, [groupName]); } else if (extracted) { if (extracted.name) { extracted.name = sanitizePublicationTitle(extracted.name, [groupName], extracted.brand); } if (!extracted.category || extracted.category === 'Calzado / Sneakers' || extracted.category === 'Sneakers / Colección') { extracted.category = detectCategory(`${extracted.name || ''} ${text}`).category; } } 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: postId, whatsappMessageId: rawMsgId || cleanMsgId, channelName: groupName, channelJid: remoteJid, rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`, rawImages: imageUrls, extractedProduct: extracted, status: effectiveStatus, receivedAt: timestamp, supplierName: groupName, supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877', isRead: Boolean(record.status === 'READ' || record.key?.fromMe), }; processedPosts.push(post); saveSupplierPostToAppwrite(post).catch(() => {}); if (processedPosts.length >= limit) break; } return NextResponse.json({ success: true, isLive: true, statusFilter, totalFound: processedPosts.length, counts: { pending: processedPosts.filter((p) => p.status === 'pending_review').length, published: publishedMsgIds.size, discarded: discardedMsgIds.size, all: processedPosts.length + publishedMsgIds.size + discardedMsgIds.size, }, posts: processedPosts, 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, customBrand, customColorName, customSizes, customCategory, marketIntelligence, } = body; if (action === 'publish' && post) { const retailPrice = customRetailPrice || post.extractedProduct?.suggestedRetailPrice || (post.extractedProduct?.supplierPrice ? post.extractedProduct.supplierPrice + 35000 : 125000); const rawProdName = customName || post.extractedProduct?.name || 'Artículo Urbano'; const prodBrand = customBrand || post.extractedProduct?.brand || 'Urbano / Variado'; const forbiddenContext = [post.channelName, post.supplierName]; const prodName = sanitizePublicationTitle(rawProdName, forbiddenContext, prodBrand); const prodCategory = customCategory || post.extractedProduct?.category || 'Zapatillas'; const proof = getProductSocialProof({ name: prodName, brand: prodBrand }); const prodDescription = generateAttractiveProductCopy({ name: prodName, brand: prodBrand, category: prodCategory, retailPrice, supplierPrice: post.extractedProduct?.supplierPrice || 75000, forbiddenTerms: forbiddenContext, }); const whatsappMsgId = post.whatsappMessageId || post.id.replace('post-', ''); 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), marketPriceCOP: post.extractedProduct?.marketPriceCOP || marketIntelligence?.marketPriceCOP, marketPriceSource: post.extractedProduct?.marketPriceSource || marketIntelligence?.marketPriceSource, marketPriceSourceUrl: post.extractedProduct?.marketPriceSourceUrl || marketIntelligence?.marketPriceSourceUrl, marketComparisonSummary: post.extractedProduct?.marketComparisonSummary || marketIntelligence?.marketComparisonSummary, sizes: customSizes || post.extractedProduct?.sizes || ['37', '38', '39', '40', '41', '42', '43'], images: Array.isArray(post.rawImages) && post.rawImages.length > 0 ? post.rawImages.map((img: string) => { if (img.startsWith('data:image') && whatsappMsgId) { return `/api/whatsapp-media?id=${encodeURIComponent(whatsappMsgId)}&remoteJid=${encodeURIComponent(post.channelJid || '')}`; } return img; }) : ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'], description: prodDescription, category: prodCategory, 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', whatsappMessageId: whatsappMsgId, }; const added = dataService.addProduct(newProduct as any); await saveProductToAppwrite(added); await updateSupplierPostStatusInAppwrite(post.id, 'published'); dataService.updateSupplierPostStatus(post.id, 'published'); return NextResponse.json({ success: true, message: `¡${prodName} publicado!`, product: added, }); } 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 rawProdName = p.extractedProduct?.name || 'Artículo Urbano'; const prodBrand = p.extractedProduct?.brand || 'Urbano / Variado'; const forbiddenContext = [p.channelName, p.supplierName]; const prodName = sanitizePublicationTitle(rawProdName, forbiddenContext, prodBrand); const prodCategory = p.extractedProduct?.category || 'Zapatillas'; const proof = getProductSocialProof({ name: prodName, brand: prodBrand }); const prodDescription = generateAttractiveProductCopy({ name: prodName, brand: prodBrand, category: prodCategory, retailPrice, supplierPrice: p.extractedProduct?.supplierPrice || 75000, forbiddenTerms: forbiddenContext, }); const whatsappMsgId = p.whatsappMessageId || p.id.replace('post-', ''); 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), marketPriceCOP: p.extractedProduct?.marketPriceCOP, marketPriceSource: p.extractedProduct?.marketPriceSource, marketPriceSourceUrl: p.extractedProduct?.marketPriceSourceUrl, marketComparisonSummary: p.extractedProduct?.marketComparisonSummary, sizes: p.extractedProduct?.sizes || ['37', '38', '39', '40', '41', '42', '43'], images: Array.isArray(p.rawImages) && p.rawImages.length > 0 ? p.rawImages.map((img: string) => { if (img.startsWith('data:image') && whatsappMsgId) { return `/api/whatsapp-media?id=${encodeURIComponent(whatsappMsgId)}&remoteJid=${encodeURIComponent(p.channelJid || '')}`; } return img; }) : ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'], description: prodDescription, category: prodCategory, 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', whatsappMessageId: whatsappMsgId, }; const added = dataService.addProduct(newProd as any); await saveProductToAppwrite(added); await updateSupplierPostStatusInAppwrite(p.id, 'published'); dataService.updateSupplierPostStatus(p.id, 'published'); createdProducts.push(added); } return NextResponse.json({ success: true, message: `¡Se publicaron ${createdProducts.length} productos!`, products: createdProducts, }); } if (action === 'discard' && post?.id) { await updateSupplierPostStatusInAppwrite(post.id, 'discarded'); dataService.updateSupplierPostStatus(post.id, 'discarded'); return NextResponse.json({ success: true, message: `Mensaje [${post.whatsappMessageId || post.id}] descartado y archivado.`, }); } if (action === 'bulk-discard' && Array.isArray(posts)) { const BATCH_SIZE = 10; for (let i = 0; i < posts.length; i += BATCH_SIZE) { const batch = posts.slice(i, i + BATCH_SIZE); await Promise.all( batch.map(async (p) => { await updateSupplierPostStatusInAppwrite(p.id, 'discarded'); dataService.updateSupplierPostStatus(p.id, 'discarded'); }) ); } return NextResponse.json({ success: true, message: `¡${posts.length} mensajes descartados y archivados con éxito!`, }); } if (action === 'discard-group' && (body.groupJid || body.groupName)) { const { groupJid, groupName } = body; const allSaved = await getSupplierPostsFromAppwrite(); const matchingPosts = allSaved.filter( (p: SupplierPost) => (groupJid && p.channelJid === groupJid) || (groupName && (p.channelName === groupName || p.supplierName === groupName)) ); const BATCH_SIZE = 10; for (let i = 0; i < matchingPosts.length; i += BATCH_SIZE) { const batch = matchingPosts.slice(i, i + BATCH_SIZE); await Promise.all( batch.map(async (p: SupplierPost) => { await updateSupplierPostStatusInAppwrite(p.id, 'discarded'); dataService.updateSupplierPostStatus(p.id, 'discarded'); }) ); } if (groupJid) { dataService.toggleGroupMonitoring(groupJid, false); } return NextResponse.json({ success: true, message: `¡Se han descartado y archivado ${matchingPosts.length} mensajes del grupo!`, discardedCount: matchingPosts.length, }); } if (action === 'market-research' && post) { const img = post.rawImages && post.rawImages[0]; const res = await performMarketIntelligenceSearch(img, post.rawMessage, post.extractedProduct?.supplierPrice); post.extractedProduct = { ...post.extractedProduct, ...res, }; post.marketResearchDone = true; await saveSupplierPostToAppwrite(post); return NextResponse.json({ success: true, message: '¡Búsqueda visual e inteligencia de precios completada con éxito!', extractedProduct: post.extractedProduct, }); } 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 }); } }