almeira-web/almeira-store-app/src/app/api/webhook/whatsapp/route.ts

164 lines
5.8 KiB
TypeScript

import { NextResponse } from 'next/server';
import { dataService } from '@/lib/data-service';
import { parseSupplierMessage } from '@/lib/gemini-parser';
import { SupplierPost, Product } from '@/types';
import { calculateRetailPrice } from '@/lib/pricing-engine';
export async function GET() {
const posts = dataService.getSupplierPosts();
const settings = dataService.getSettings();
return NextResponse.json({
success: true,
autoPublishEnabled: settings.autoPublishSupplierPosts ?? true,
postsCount: posts.length,
posts,
});
}
export async function POST(request: Request) {
try {
const payload = await request.json();
console.log('📥 [WhatsApp Webhook] Mensaje recibido:', JSON.stringify(payload).slice(0, 300));
const data = payload.data || payload;
const settings = dataService.getSettings();
// 1. Extraer texto del mensaje de WhatsApp
const messageContent =
data.message?.conversation ||
data.message?.extendedTextMessage?.text ||
data.message?.imageMessage?.caption ||
data.text ||
'';
// 2. Extraer nombre del canal o grupo
const senderPhone = data.key?.remoteJid?.replace(/\D/g, '') || '';
const isGroup = data.key?.remoteJid?.includes('g.us');
const channelName =
data.groupInfo?.subject ||
data.pushName ||
(isGroup ? 'Grupo Mayorista WhatsApp' : 'Chat Proveedor Directo');
if (!messageContent || messageContent.trim().length < 6) {
return NextResponse.json({
success: true,
ignored: 'Mensaje sin texto suficiente para extraer calzado.',
});
}
// 3. Buscar proveedor asociado por teléfono o grupo
const suppliers = dataService.getSuppliers();
const matchedSupplier = suppliers.find(
(s) =>
s.phone.includes(senderPhone) ||
(channelName && s.name.toLowerCase().includes(channelName.toLowerCase())) ||
(channelName && channelName.toLowerCase().includes(s.name.toLowerCase()))
);
const supplierName = matchedSupplier?.name || channelName;
const supplierPhone = matchedSupplier?.phone || senderPhone || '573167455877';
// 4. Procesar y extraer información con Gemini AI
const extracted = await parseSupplierMessage(messageContent);
// Recalcular precio con el margen y redondeo al $5.000 oficial
const finalRetailPrice = calculateRetailPrice(
extracted.supplierPrice,
settings.defaultProfitMargin || 35
);
// Extraer imágenes (si Evolution API envió imagen adjunta o usar fotos de alta resolución)
const images: string[] = [];
if (data.message?.imageMessage?.url) {
images.push(data.message.imageMessage.url);
} else {
images.push(
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
);
}
// 5. Verificar Anti-Duplicados en el Catálogo
const existingProducts = dataService.getProducts();
const duplicateProduct = existingProducts.find(
(p) => p.name.toLowerCase() === extracted.name.toLowerCase()
);
let registeredProduct: Product | null = null;
const autoPublish = settings.autoPublishSupplierPosts ?? true;
if (autoPublish) {
if (duplicateProduct) {
// Actualizar stock y tallas existentes sin duplicar
const mergedSizes = Array.from(new Set([...duplicateProduct.sizes, ...extracted.sizes]));
registeredProduct = dataService.updateProduct(duplicateProduct.id, {
supplierPrice: extracted.supplierPrice,
retailPrice: finalRetailPrice,
sizes: mergedSizes,
inStock: true,
isActive: true,
supplierName,
supplierPhone,
});
console.log(`🔄 [Auto-Sync] Zapatilla "${extracted.name}" actualizada en catálogo (Anti-Duplicados).`);
} else {
// Registrar y publicar automáticamente
registeredProduct = dataService.addProduct({
name: extracted.name,
brand: extracted.brand,
supplierPrice: extracted.supplierPrice,
retailPrice: finalRetailPrice,
originalPrice: Math.round(finalRetailPrice * 1.35),
sizes: extracted.sizes,
images,
description: `Zapatillas ${extracted.brand} ${extracted.name} garantizadas. Calidad seleccionada con horma nacional y suela antideslizante.`,
category: extracted.category || 'Sneakers / Urbano',
inStock: true,
isActive: true,
isFlashDeal: true,
rating: 4.9,
reviewsCount: 15,
salesCount: 30,
originBadge: 'Bodega Mayorista',
supplierId: matchedSupplier?.id || 'sup-auto',
supplierName,
supplierPhone,
});
console.log(`✨ [Auto-Publish] Nueva zapatilla "${extracted.name}" publicada automáticamente en tienda.`);
}
}
// 6. Registrar en el historial de publicaciones recibidas
const newPost: SupplierPost = {
id: `post-${Date.now()}`,
channelName,
supplierName,
supplierPhone,
rawMessage: messageContent,
rawImages: images,
extractedProduct: {
...extracted,
suggestedRetailPrice: finalRetailPrice,
supplierName,
supplierPhone,
},
status: autoPublish ? 'published_store' : 'pending_review',
receivedAt: 'Justo ahora',
};
dataService.addSupplierPost(newPost);
return NextResponse.json({
success: true,
autoPublished: autoPublish,
product: registeredProduct,
post: newPost,
message: autoPublish
? `🔥 ¡Zapatilla "${extracted.name}" procesada y publicada AUTOMÁTICAMENTE en la tienda!`
: `Mensaje ingresado al feed para aprobación manual.`,
});
} catch (error: any) {
console.error('Error procesando webhook de WhatsApp:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}