almeira-web/dripzone-app/src/lib/whatsapp-order-parser.ts

754 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Order, OrderItem, Product, User } from '@/types';
import { dataService } from '@/lib/data-service';
import { createOrderInAppwrite, getOrdersFromAppwrite } from '@/lib/server-appwrite';
import { notifyAdminNewOrder, sendWhatsAppMessage } from '@/lib/evolution-api';
import { formatCOP } from '@/lib/pricing-engine';
import { createBoldPayment } from '@/lib/bold-service';
// Cache para evitar procesar la misma orden múltiples veces si Evolution API reenvía el webhook
const processedOrdersCache = new Set<string>();
/**
* Normaliza textos para comparaciones seguras sin tildes ni caracteres especiales
*/
function normalizeText(text: string): string {
return (text || '')
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim();
}
/**
* Extrae números limpios de precios (ej: "$135.000 COP" -> 135000)
*/
function parsePriceNumber(raw: string): number {
if (!raw) return 0;
const digits = raw.replace(/[^\d]/g, '');
const num = parseInt(digits, 10);
return isNaN(num) ? 0 : num;
}
/**
* Busca una ciudad colombiana dentro de un texto
*/
function detectCityFromText(text: string): { id: string; name: string; shippingCost: number } {
const norm = normalizeText(text);
const cities = dataService.getCities();
for (const c of cities) {
const cNorm = normalizeText(c.name);
if (norm.includes(cNorm)) {
return { id: c.id, name: c.name, shippingCost: c.shippingCost };
}
}
// Comprobar ciudades principales
if (norm.includes('cali') || norm.includes('jamundi') || norm.includes('yumbo')) {
return { id: 'city-cali', name: 'Cali', shippingCost: 0 };
}
if (norm.includes('bogota') || norm.includes('soacha')) {
return { id: 'city-bogota', name: 'Bogotá D.C.', shippingCost: 15000 };
}
if (norm.includes('medellin') || norm.includes('bello') || norm.includes('envigado') || norm.includes('itagui')) {
return { id: 'city-medellin', name: 'Medellín', shippingCost: 15000 };
}
if (norm.includes('barranquilla') || norm.includes('soledad')) {
return { id: 'city-barranquilla', name: 'Barranquilla', shippingCost: 18000 };
}
return { id: 'city-cali', name: 'Cali', shippingCost: 0 };
}
/**
* Intenta hacer match de un texto con un producto del catálogo de la tienda
*/
function matchProductInCatalog(titleOrReference: string, allProducts: Product[]): Product | null {
if (!titleOrReference) return null;
const norm = normalizeText(titleOrReference);
// 1. Por ID directo (ej: snk-...)
const directId = allProducts.find((p) => p.id && norm.includes(p.id.toLowerCase()));
if (directId) return directId;
// 2. Por nombre exacto
const exact = allProducts.find((p) => p.name && normalizeText(p.name) === norm);
if (exact) return exact;
// 3. Por inclusión de nombre
const partial = allProducts.find((p) => {
if (!p.name) return false;
const pNorm = normalizeText(p.name);
return norm.includes(pNorm) || (pNorm.length > 8 && norm.includes(pNorm.slice(0, 15)));
});
if (partial) return partial;
// 4. Por palabras clave significativas (marca + modelo)
const words = norm.split(/\s+/).filter((w) => w.length >= 4);
if (words.length >= 2) {
const scored = allProducts.map((p) => {
const pNorm = normalizeText(p.name + ' ' + (p.modelName || '') + ' ' + (p.brand || ''));
const matches = words.filter((w) => pNorm.includes(w)).length;
return { product: p, score: matches };
}).filter((item) => item.score >= 2);
if (scored.length > 0) {
scored.sort((a, b) => b.score - a.score);
return scored[0].product;
}
}
return null;
}
/**
* Garantiza que el cliente que realiza el pedido quede registrado en la base de datos de la App.
* Si el usuario no existe, lo crea automáticamente con rol 'customer' y datos iniciales.
* Si ya existe, actualiza su conteo de pedidos y última actividad.
*/
function ensureCustomerRegistered(
customerPhone: string,
customerPushName: string,
cityName: string,
address: string
): User {
const cleanPhone = customerPhone.replace(/\D/g, '');
let user = dataService.getUsers().find((u) => {
const uPhone = (u.phone || '').replace(/\D/g, '');
return (uPhone.length >= 7 && (uPhone.endsWith(cleanPhone) || cleanPhone.endsWith(uPhone)));
});
if (!user) {
const cleanName =
customerPushName && customerPushName !== 'Cliente WhatsApp'
? customerPushName.trim()
: `Cliente WhatsApp (+${cleanPhone})`;
const newUser: User = {
id: `usr-wa-${cleanPhone.slice(-6)}-${Date.now().toString().slice(-4)}`,
name: cleanName,
email: `${cleanPhone}@drip-zone.online`,
phone: cleanPhone,
city: cityName || 'Cali',
address: address && address !== 'Por coordinar vía WhatsApp' ? address : '',
role: 'customer',
isActive: true,
phoneVerified: true,
requiresShippingDeposit: true,
depositReason: 'Garantía de separación requerida para primer pedido WhatsApp',
totalOrders: 1,
createdAt: new Date().toISOString().split('T')[0],
lastLogin: 'WhatsApp Order',
};
user = dataService.addUser(newUser);
console.log(`👤 [WhatsApp Order Parser] ¡Nuevo cliente AUTO-REGISTRADO en la App! ID: ${user.id}, Nombre: ${user.name}, Tel: ${user.phone}`);
} else {
const updates: Partial<User> = {
totalOrders: (user.totalOrders || 0) + 1,
lastLogin: 'WhatsApp Order',
};
if (!user.address && address && address !== 'Por coordinar vía WhatsApp') {
updates.address = address;
}
if ((!user.city || user.city === 'Cali') && cityName) {
updates.city = cityName;
}
if ((user.name.startsWith('+') || user.name === 'Cliente WhatsApp') && customerPushName && customerPushName !== 'Cliente WhatsApp') {
updates.name = customerPushName.trim();
}
const updated = dataService.updateUser(user.id, updates);
if (updated) user = updated;
}
return user;
}
/**
* Genera el enlace directo de pago en línea de Bold para el anticipo / separación del pedido.
* Si la API de Bold responde exitosamente, devuelve el checkoutUrl oficial (soporta PSE, Nequi, Tarjetas, Botón Bancolombia, Bre-B).
* En caso de contingencia devuelve el enlace seguro de la pasarela web de DripZone.
*/
async function generateBoldDepositLink(
orderId: string,
depositAmount: number,
customerPhone: string,
customerEmail?: string
): Promise<string> {
try {
const res = await createBoldPayment({
orderId,
amount: depositAmount || 10000,
description: `Separación Pedido #${orderId} - DripZone`,
payerEmail: customerEmail || `${customerPhone}@drip-zone.online`,
redirectionUrl: `https://drip-zone.online/mis-pedidos?phone=${customerPhone}&orderId=${orderId}&paid=true`,
type: 'deposit',
});
if (res.success && res.checkoutUrl) {
console.log(`💳 [WhatsApp Order Parser] Link de Bold generado con éxito para pedido #${orderId}: ${res.checkoutUrl}`);
return res.checkoutUrl;
}
} catch (err: any) {
console.warn(`⚠️ [WhatsApp Order Parser] No se pudo generar enlace directo en Bold para #${orderId}:`, err.message);
}
return `https://drip-zone.online/checkout?orderId=${orderId}&payDeposit=true`;
}
/**
* Motor Principal: Detecta y procesa pedidos recibidos desde WhatsApp:
* 1. Mensajes de Pedido del Catálogo Nativo de WhatsApp (Meta Commerce / orderMessage).
* 2. Mensajes formateados de pedido (order_checkout_wa o carritos enviados por texto).
* 3. Solicitudes directas de compra con producto y talla.
*/
export async function detectAndProcessWhatsAppOrder(
payload: any,
data: any,
remoteJid: string,
incomingInstance: string
): Promise<{ handled: boolean; order?: Order; reason?: string }> {
try {
const rawMessage = data.message || {};
const text = (
rawMessage.conversation ||
rawMessage.extendedTextMessage?.text ||
rawMessage.imageMessage?.caption ||
data.text ||
''
).trim();
const customerPhone = remoteJid.replace(/\D/g, '');
const customerPushName = data.pushName || 'Cliente WhatsApp';
const customerDisplayName = (customerPushName && customerPushName !== 'Cliente WhatsApp')
? `${customerPushName} (+${customerPhone})`
: `+${customerPhone} (WhatsApp)`;
const allProducts = dataService.getProducts().filter((p) => p.isActive !== false);
// =========================================================================
// CASO 1: PEDIDO NATIVO DEL CATÁLOGO DE WHATSAPP (Meta Commerce orderMessage)
// =========================================================================
const orderMessage =
rawMessage.orderMessage ||
rawMessage.extendedTextMessage?.contextInfo?.quotedMessage?.orderMessage ||
data.orderMessage;
if (orderMessage) {
console.log('🛍️ [WhatsApp Order Parser] ¡Detectado pedido nativo del Catálogo de WhatsApp!', JSON.stringify(orderMessage).slice(0, 300));
const rawOrderId = orderMessage.orderId || `WA-CAT-${Date.now().toString().slice(-6)}`;
const cacheKey = `cat-order:${customerPhone}:${rawOrderId}`;
if (processedOrdersCache.has(cacheKey)) {
return { handled: true, reason: 'Pedido de catálogo ya procesado previamente' };
}
processedOrdersCache.add(cacheKey);
const catalogItems = Array.isArray(orderMessage.orderItems) ? orderMessage.orderItems : [];
const orderItems: OrderItem[] = [];
let calculatedSubtotal = 0;
for (const cItem of catalogItems) {
const itemName = cItem.name || 'Zapatilla Catálogo';
const matched = matchProductInCatalog(cItem.retailer_id || itemName, allProducts);
// Extraer talla si está indicada en el nombre o en la nota del cliente
const noteText = (orderMessage.message || '') + ' ' + itemName;
const sizeMatch = noteText.match(/talla\s*:?\s*(\d{2})/i) || noteText.match(/#\s*(\d{2})/) || noteText.match(/\b(3[6-9]|4[0-5])\b/);
const resolvedSize = sizeMatch ? sizeMatch[1] : (matched?.sizes?.[0] || 'Por confirmar');
let unitPrice = 0;
if (cItem.amount_1000) {
unitPrice = cItem.amount_1000 > 1000000 ? Math.round(cItem.amount_1000 / 1000) : cItem.amount_1000;
} else if (cItem.price) {
unitPrice = parsePriceNumber(String(cItem.price));
}
if (unitPrice < 10000 && matched?.retailPrice) {
unitPrice = matched.retailPrice;
}
if (unitPrice < 10000) {
unitPrice = 135000;
}
const qty = cItem.quantity || 1;
calculatedSubtotal += unitPrice * qty;
orderItems.push({
productId: matched?.id || cItem.retailer_id || `snk-cat-${Date.now()}`,
productName: matched?.name || itemName,
brand: matched?.brand || 'Sneakers',
size: resolvedSize,
quantity: qty,
price: unitPrice,
supplierPrice: matched?.supplierPrice || Math.round(unitPrice * 0.7),
supplierName: matched?.supplierName || 'DIFUSIÓN JOVIC STORE',
supplierPhone: matched?.supplierPhone || '573167455877',
image: matched?.images?.[0] || 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80',
});
}
// Si no vinieron items desglosados en orderItems, crear item genérico basado en el título del pedido
if (orderItems.length === 0) {
const orderTitle = orderMessage.orderTitle || 'Pedido Catálogo WhatsApp';
const matched = matchProductInCatalog(orderTitle, allProducts);
let price = orderMessage.totalAmount1000
? (orderMessage.totalAmount1000 > 1000000 ? Math.round(orderMessage.totalAmount1000 / 1000) : orderMessage.totalAmount1000)
: (matched?.retailPrice || 135000);
if (price < 10000) price = 135000;
calculatedSubtotal = price;
orderItems.push({
productId: matched?.id || `snk-cat-${Date.now()}`,
productName: matched?.name || orderTitle,
brand: matched?.brand || 'Sneakers',
size: 'Por confirmar',
quantity: orderMessage.itemCount || 1,
price,
supplierPrice: matched?.supplierPrice || Math.round(price * 0.7),
supplierName: matched?.supplierName || 'DIFUSIÓN JOVIC STORE',
supplierPhone: matched?.supplierPhone || '573167455877',
image: matched?.images?.[0] || 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80',
});
}
const destination = detectCityFromText(orderMessage.message || '');
const totalAmount = calculatedSubtotal + destination.shippingCost;
const depositAmount = 10000;
const balanceDue = Math.max(0, totalAmount - depositAmount);
const deliveryAddress = orderMessage.message ? `Nota: ${orderMessage.message}` : 'Por coordinar vía WhatsApp';
// 1. AUTO-REGISTRAR AL CLIENTE EN LA APP SI NO ESTÁ REGISTRADO
const customerUser = ensureCustomerRegistered(customerPhone, customerPushName, destination.name, deliveryAddress);
const newOrder: Order = {
id: `ORD-WA-CAT-${Math.floor(100000 + Math.random() * 900000)}`,
userId: customerUser.id,
customerName: customerUser.name || customerDisplayName,
customerPhone,
address: deliveryAddress,
cityId: destination.id,
cityName: destination.name,
items: orderItems,
subtotal: calculatedSubtotal,
shippingCost: destination.shippingCost,
totalAmount,
depositAmount,
balanceDue,
paymentMethodId: 'pay-contraentrega',
paymentMethodName: 'Pago Contraentrega (Catálogo WhatsApp)',
status: 'pending',
depositPaid: false,
source: 'whatsapp',
notes: `🛍️ Pedido enviado desde el Catálogo de WhatsApp (Meta Commerce). Orden WA Ref: #${rawOrderId}.`,
createdAt: new Date().toISOString(),
};
// 2. REGISTRAR EL PEDIDO EN APPWRITE Y EN MEMORIA
const savedOrder = await createOrderInAppwrite(newOrder);
dataService.addOrder(savedOrder);
console.log(`✅ [WhatsApp Order Parser] Pedido de Catálogo #${savedOrder.id} REGISTRADO EN LA APP exitosamente.`);
// 3. GENERAR ENLACE DE PAGO EN LÍNEA DE BOLD PARA EL ANTICIPO
const boldPaymentUrl = await generateBoldDepositLink(
savedOrder.id,
savedOrder.depositAmount,
customerPhone,
customerUser.email
);
// 4. NOTIFICAR DE INMEDIATO AL ADMINISTRADOR CARLOS (+57 316 745 5877)
await notifyAdminNewOrder(savedOrder).catch((e) =>
console.warn('Error notificando admin pedido catálogo:', e)
);
// 5. RESPONDER AUTOMÁTICAMENTE AL CLIENTE DESDE LA LÍNEA OFICIAL DRIPZONE (+57 310 639 0296) CON EL ENLACE DE BOLD
const itemsListText = orderItems
.map((i) => (
`• 👟 *${i.productName}*\n` +
` 📏 Talla: *${i.size}* | Cantidad: *${i.quantity}*\n` +
` 💵 Valor Unitario: *${formatCOP(i.price)} COP*\n` +
` 💰 Subtotal Producto: *${formatCOP(i.price * i.quantity)} COP*`
))
.join('\n\n');
const clientReply = (
`👟 *¡HOLA! HEMOS RECIBIDO TU PEDIDO EN DRIPZONE* 🛍️📦\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`🆔 *No. de Orden:* *#${savedOrder.id}*\n` +
`👤 *Cliente:* ${savedOrder.customerName}\n` +
`📱 *WhatsApp Origen:* +${customerPhone}\n` +
`🏙️ *Ciudad Destino:* ${savedOrder.cityName}\n` +
`📍 *Dirección de Entrega:* ${savedOrder.address}\n\n` +
`🛒 *DETALLE DE TU PEDIDO:*\n` +
`${itemsListText}\n\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`💰 *LIQUIDACIÓN ECONÓMICA:*\n` +
`• *Subtotal Productos:* ${formatCOP(savedOrder.subtotal)} COP\n` +
`• *Costo de Envío:* ${savedOrder.shippingCost === 0 ? 'GRATIS' : `${formatCOP(savedOrder.shippingCost)} COP`}\n` +
`• *TOTAL DE LA VENTA:* *${formatCOP(savedOrder.totalAmount)} COP*\n\n` +
`⚡ *ANTICIPO DE SEPARACIÓN (REQUERIDO PARA DESPACHO):*\n` +
`⚠️ *Estado Anticipo:* 🔴 *PENDIENTE DE PAGO* (*${formatCOP(savedOrder.depositAmount)} COP*)\n\n` +
`💳 *ENLACE DE PAGO EN LÍNEA CON BOLD (SEPARACIÓN):*\n` +
`👉 ${boldPaymentUrl}\n` +
`_(Acepta PSE, Nequi, Botón Bancolombia, Tarjetas de Crédito/Débito y QR Bre-B sin comisiones)_\n\n` +
`🤝 *SALDO A PAGAR CONTRAENTREGA AL RECIBIR:*\n` +
`💵 *${formatCOP(savedOrder.balanceDue)} COP* (se cancela en efectivo a la transportadora al recibir en tu puerta).\n\n` +
`👤 *Tu Cuenta DripZone:* Registrada y vinculada automáticamente a tu número (+${customerPhone}).\n` +
`📱 *Consulta y rastrea el estado de tu pedido en vivo:*\n` +
`👉 https://drip-zone.online/mis-pedidos?phone=${customerPhone}\n` +
`━━━━━━━━━━━━━━━━━━━━━━`
);
await sendWhatsAppMessage(customerPhone, clientReply, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D').catch(() => {});
return { handled: true, order: savedOrder };
}
// =========================================================================
// CASO 2: MENSAJE DE TEXTO CON FORMATO DE PEDIDO ESTRUCTURADO (Carrito o Web)
// =========================================================================
const isFormattedOrder =
text.includes('CONFIRMACIÓN DE PEDIDO') ||
text.includes('NUEVO PEDIDO DE CALZADO') ||
text.includes('NUEVO PEDIDO EN') ||
(text.includes('Cliente:') && text.includes('Artículos:') && (text.includes('Total:') || text.includes('Saldo')));
if (isFormattedOrder) {
console.log('📝 [WhatsApp Order Parser] Detectado mensaje de texto con formato de pedido estructurado');
// 1. Extraer ID si ya viene en el mensaje (ej: #ORD-WA-8921 o #WEB-4921)
const idMatch = text.match(/#([A-Za-z0-9_-]{4,20})/);
const existingOrderId = idMatch ? idMatch[1] : null;
// Si la orden ya existía en la app (registrada desde el checkout web o móvil), confirmar recepción y enviar link de Bold si está pendiente
if (existingOrderId) {
let existing = dataService.getOrderById(existingOrderId);
if (!existing) {
try {
const allOrders = await getOrdersFromAppwrite();
existing = allOrders.find((o: any) => o.id === existingOrderId);
} catch (_) {}
}
if (existing) {
console.log(`ℹ️ [WhatsApp Order Parser] La orden #${existingOrderId} ya existe en la base de datos.`);
const boldPaymentUrl = !existing.depositPaid
? await generateBoldDepositLink(existing.id, existing.depositAmount || 10000, customerPhone)
: null;
const boldPaymentBlock = boldPaymentUrl
? `\n💳 *ENLACE DE PAGO EN LÍNEA CON BOLD (SEPARACIÓN):*\n👉 ${boldPaymentUrl}\n_(Acepta PSE, Nequi, Botón Bancolombia, Tarjetas y QR Bre-B)_\n`
: '';
const confirmText = (
`🎉 *¡ORDEN #${existing.id} REGISTRADA EN WHATSAPP!* 📦✨\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`👤 *Cliente:* ${existing.customerName || customerDisplayName}\n` +
`🏙️ *Destino:* ${existing.cityName || 'Colombia'}\n` +
`💰 *Total Venta:* ${formatCOP(existing.totalAmount || 0)} COP\n` +
`⚡ *Separación:* ${formatCOP(existing.depositAmount || 10000)} COP (${existing.depositPaid ? '✅ PAGADA' : '🔴 PENDIENTE'})\n` +
boldPaymentBlock +
`🤝 *Saldo Contraentrega:* ${formatCOP(existing.balanceDue || existing.totalAmount || 0)} COP\n\n` +
`🚚 Para despachar tu paquete recuerda confirmar la separación de $10.000 COP.\n\n` +
`📱 Seguimiento en vivo:\n` +
`👉 https://drip-zone.online/mis-pedidos?phone=${customerPhone}\n` +
`━━━━━━━━━━━━━━━━━━━━━━`
);
await sendWhatsAppMessage(customerPhone, confirmText, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D').catch(() => {});
return { handled: true, order: existing, reason: 'Orden ya existente confirmada vía WhatsApp' };
}
}
const cacheKey = `txt-order:${customerPhone}:${existingOrderId || text.slice(0, 40)}`;
if (processedOrdersCache.has(cacheKey)) {
return { handled: true, reason: 'Pedido de texto ya procesado' };
}
processedOrdersCache.add(cacheKey);
// Extraer datos del cliente mediante regex
const nameMatch = text.match(/\*?Cliente:\*?\s*([^\n\r]+)/i);
const phoneMatch = text.match(/\*?Teléfono:\*?\s*([^\n\r]+)/i);
const addressMatch = text.match(/\*?Dirección:\*?\s*([^\n\r]+)/i);
const cityMatch = text.match(/\*?Ciudad:\*?\s*([^\n\r]+)/i);
const totalMatch = text.match(/\*?Total:\*?\s*([^\n\r]+)/i);
const balanceMatch = text.match(/\*?Saldo[^\n\r]*:\*?\s*([^\n\r]+)/i);
const parsedCustomerName = nameMatch ? nameMatch[1].replace(/[*_]/g, '').trim() : customerDisplayName;
const parsedPhone = phoneMatch ? phoneMatch[1].replace(/\D/g, '').trim() : customerPhone;
const parsedAddress = addressMatch ? addressMatch[1].replace(/[*_]/g, '').trim() : 'Por coordinar vía WhatsApp';
const parsedCityRaw = cityMatch ? cityMatch[1].replace(/[*_]/g, '').trim() : '';
const destination = detectCityFromText(parsedCityRaw || text);
const totalAmount = totalMatch ? parsePriceNumber(totalMatch[1]) : 135000;
const balanceDue = balanceMatch ? parsePriceNumber(balanceMatch[1]) : Math.max(0, totalAmount - 10000);
const depositAmount = totalAmount > balanceDue ? totalAmount - balanceDue : 10000;
// 1. AUTO-REGISTRAR AL CLIENTE EN LA APP
const customerUser = ensureCustomerRegistered(parsedPhone, parsedCustomerName, destination.name, parsedAddress);
// Parsear artículos
const orderItems: OrderItem[] = [];
const lines = text.split('\n');
let inItemsSection = false;
for (const line of lines) {
if (/art[íi]culos:/i.test(line)) {
inItemsSection = true;
continue;
}
if (inItemsSection && (/total:/i.test(line) || /condici[oó]n/i.test(line) || /m[eé]todo/i.test(line))) {
inItemsSection = false;
break;
}
if (inItemsSection && line.trim().length > 3) {
const cleanLine = line.replace(/^[•*\-\s]+/, '').replace(/[*_]/g, '').trim();
const matched = matchProductInCatalog(cleanLine, allProducts);
const sizeM = cleanLine.match(/talla\s*:?\s*(\d{2})/i) || cleanLine.match(/#\s*(\d{2})/) || cleanLine.match(/\b(3[6-9]|4[0-5])\b/);
const qtyM = cleanLine.match(/cant\s*:?\s*(\d+)/i) || cleanLine.match(/x(\d+)/i);
const priceM = cleanLine.match(/\$?([0-9.,]+)\s*(?:cop)?$/i);
const size = sizeM ? sizeM[1] : (matched?.sizes?.[0] || 'Por confirmar');
const qty = qtyM ? parseInt(qtyM[1], 10) : 1;
let price = priceM ? parsePriceNumber(priceM[1]) : (matched?.retailPrice || 135000);
if (price < 10000) price = 135000;
orderItems.push({
productId: matched?.id || `snk-wa-${Date.now()}`,
productName: matched?.name || cleanLine.slice(0, 40),
brand: matched?.brand || 'Sneakers',
size,
quantity: qty,
price,
supplierPrice: matched?.supplierPrice || Math.round(price * 0.7),
supplierName: matched?.supplierName || 'DIFUSIÓN JOVIC STORE',
supplierPhone: matched?.supplierPhone || '573167455877',
image: matched?.images?.[0] || 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80',
});
}
}
// Si no se extrajeron items específicos, crear un item con el catálogo
if (orderItems.length === 0) {
const matched = matchProductInCatalog(text, allProducts);
orderItems.push({
productId: matched?.id || `snk-wa-${Date.now()}`,
productName: matched?.name || 'Zapatilla DripZone',
brand: matched?.brand || 'Sneakers',
size: 'Por confirmar',
quantity: 1,
price: totalAmount,
supplierPrice: matched?.supplierPrice || Math.round(totalAmount * 0.7),
supplierName: matched?.supplierName || 'DIFUSIÓN JOVIC STORE',
supplierPhone: matched?.supplierPhone || '573167455877',
image: matched?.images?.[0] || 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80',
});
}
const generatedId = existingOrderId || `ORD-WA-${Math.floor(100000 + Math.random() * 900000)}`;
const newOrder: Order = {
id: generatedId,
userId: customerUser.id,
customerName: customerUser.name || parsedCustomerName,
customerPhone: parsedPhone,
address: parsedAddress,
cityId: destination.id,
cityName: destination.name,
items: orderItems,
subtotal: totalAmount,
shippingCost: destination.shippingCost,
totalAmount,
depositAmount,
balanceDue,
paymentMethodId: 'pay-contraentrega',
paymentMethodName: 'Pago Contraentrega (WhatsApp)',
status: 'pending',
depositPaid: false,
source: 'whatsapp',
notes: `💬 Pedido recibido vía mensaje formateado de WhatsApp.`,
createdAt: new Date().toISOString(),
};
// 2. REGISTRAR EL PEDIDO EN APPWRITE Y EN MEMORIA
const savedOrder = await createOrderInAppwrite(newOrder);
dataService.addOrder(savedOrder);
console.log(`✅ [WhatsApp Order Parser] Pedido de texto #${savedOrder.id} REGISTRADO EN LA APP con éxito.`);
// 3. GENERAR ENLACE DE PAGO BOLD
const boldPaymentUrl = await generateBoldDepositLink(
savedOrder.id,
savedOrder.depositAmount,
parsedPhone,
customerUser.email
);
// 4. NOTIFICAR AL ADMINISTRADOR CARLOS (+57 316 745 5877)
await notifyAdminNewOrder(savedOrder).catch((e) =>
console.warn('Error notificando admin pedido texto:', e)
);
// 5. ENVIAR CONFIRMACIÓN AL CLIENTE DESDE DRIPZONE OFICIAL CON EL ENLACE DE BOLD
const itemsListText = orderItems
.map((i) => (
`• 👟 *${i.productName}*\n` +
` 📏 Talla: *${i.size}* | Cantidad: *${i.quantity}*\n` +
` 💵 Valor Unitario: *${formatCOP(i.price)} COP*\n` +
` 💰 Subtotal Producto: *${formatCOP(i.price * i.quantity)} COP*`
))
.join('\n\n');
const confirmationMsg = (
`🎉 *¡TU PEDIDO HA SIDO REGISTRADO EN DRIPZONE!* 📦\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`🆔 *No. Orden:* *#${savedOrder.id}*\n` +
`👤 *Cliente:* ${savedOrder.customerName}\n` +
`📱 *Celular Origen:* +${parsedPhone}\n` +
`🏙 *Ciudad Destino:* ${savedOrder.cityName}\n` +
`📍 *Dirección:* ${savedOrder.address}\n\n` +
`🛒 *DETALLE DE ARTÍCULOS:*\n` +
`${itemsListText}\n\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`💰 *LIQUIDACIÓN ECONÓMICA:*\n` +
`• *Subtotal Productos:* ${formatCOP(savedOrder.subtotal)} COP\n` +
`• *Envío:* ${savedOrder.shippingCost === 0 ? 'GRATIS' : `${formatCOP(savedOrder.shippingCost)} COP`}\n` +
`• *TOTAL DE LA VENTA:* *${formatCOP(savedOrder.totalAmount)} COP*\n\n` +
`⚡ *ANTICIPO DE SEPARACIÓN (REQUERIDO PARA DESPACHO):*\n` +
`⚠️ *Estado Anticipo:* 🔴 *PENDIENTE DE PAGO* (*${formatCOP(savedOrder.depositAmount)} COP*)\n\n` +
`💳 *ENLACE DE PAGO EN LÍNEA CON BOLD (SEPARACIÓN):*\n` +
`👉 ${boldPaymentUrl}\n` +
`_(Paga al instante con PSE, Nequi, Botón Bancolombia, Tarjetas de Crédito/Débito y QR Bre-B)_\n\n` +
`🤝 *SALDO A PAGAR CONTRAENTREGA AL RECIBIR:*\n` +
`💵 *${formatCOP(savedOrder.balanceDue)} COP* (se cancela en efectivo a la transportadora al recibir en tu puerta).\n\n` +
`👤 *Cuenta DripZone:* Registrada y vinculada automáticamente a tu número (+${parsedPhone}).\n` +
`📱 *Consulta tu orden en vivo:*\n` +
`👉 https://drip-zone.online/mis-pedidos?phone=${parsedPhone}\n` +
`━━━━━━━━━━━━━━━━━━━━━━`
);
await sendWhatsAppMessage(parsedPhone, confirmationMsg, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D').catch(() => {});
return { handled: true, order: savedOrder };
}
// =========================================================================
// CASO 3: PEDIDO EN LENGUAJE NATURAL (Ej: "Quiero pedir las Jordan 4 talla 41 para Cali")
// =========================================================================
const lower = text.toLowerCase();
const hasOrderIntent =
/quiero pedir|hacer un pedido|hacer el pedido|para pedir|me interesa pedir|quiero comprar|deseo comprar|encargar/i.test(lower);
const hasSize = /talla\s*:?\s*(\d{2})|#\s*(\d{2})|\b(3[6-9]|4[0-5])\b/i.test(lower);
if (hasOrderIntent && hasSize) {
const matched = matchProductInCatalog(text, allProducts);
if (matched) {
console.log(`💡 [WhatsApp Order Parser] Detectado pedido en lenguaje natural para: "${matched.name}"`);
const cacheKey = `nat-order:${customerPhone}:${matched.id}`;
if (processedOrdersCache.has(cacheKey)) {
return { handled: true, reason: 'Pedido natural reciente ya procesado' };
}
processedOrdersCache.add(cacheKey);
const sizeMatch = text.match(/talla\s*:?\s*(\d{2})/i) || text.match(/#\s*(\d{2})/) || text.match(/\b(3[6-9]|4[0-5])\b/);
const resolvedSize = sizeMatch ? sizeMatch[1] : (matched.sizes?.[0] || 'Por confirmar');
const destination = detectCityFromText(text);
let price = matched.retailPrice || 135000;
if (price < 10000) price = 135000;
const totalAmount = price + destination.shippingCost;
const depositAmount = 10000;
const balanceDue = Math.max(0, totalAmount - depositAmount);
// 1. AUTO-REGISTRAR AL CLIENTE EN LA APP
const customerUser = ensureCustomerRegistered(customerPhone, customerPushName, destination.name, 'Por coordinar vía WhatsApp');
const newOrder: Order = {
id: `ORD-WA-CHAT-${Math.floor(100000 + Math.random() * 900000)}`,
userId: customerUser.id,
customerName: customerUser.name || customerDisplayName,
customerPhone,
address: 'Por coordinar vía WhatsApp',
cityId: destination.id,
cityName: destination.name,
items: [
{
productId: matched.id,
productName: matched.name,
brand: matched.brand || 'Sneakers',
size: resolvedSize,
quantity: 1,
price,
supplierPrice: matched.supplierPrice || Math.round(price * 0.7),
supplierName: matched.supplierName || 'DIFUSIÓN JOVIC STORE',
supplierPhone: matched.supplierPhone || '573167455877',
image: matched.images[0] || '',
},
],
subtotal: price,
shippingCost: destination.shippingCost,
totalAmount,
depositAmount,
balanceDue,
paymentMethodId: 'pay-contraentrega',
paymentMethodName: 'Pago Contraentrega (WhatsApp)',
status: 'pending',
depositPaid: false,
source: 'whatsapp',
notes: `💬 Pedido pre-registrado desde chat de WhatsApp: "${text.slice(0, 100)}"`,
createdAt: new Date().toISOString(),
};
// 2. REGISTRAR EL PEDIDO EN APPWRITE Y EN MEMORIA
const savedOrder = await createOrderInAppwrite(newOrder);
dataService.addOrder(savedOrder);
console.log(`✅ [WhatsApp Order Parser] Pedido de chat #${savedOrder.id} REGISTRADO EN LA APP exitosamente.`);
// 3. GENERAR ENLACE DE PAGO BOLD
const boldPaymentUrl = await generateBoldDepositLink(
savedOrder.id,
savedOrder.depositAmount,
customerPhone,
customerUser.email
);
// 4. NOTIFICAR AL ADMINISTRADOR CARLOS (+57 316 745 5877)
await notifyAdminNewOrder(savedOrder).catch((e) =>
console.warn('Error notificando admin pedido chat:', e)
);
// 5. ENVIAR CONFIRMACIÓN AL CLIENTE DESDE DRIPZONE OFICIAL CON ENLACE DE BOLD
const confirmText = (
`👟 *¡EXCELENTE ELECCIÓN! PEDIDO REGISTRADO EN DRIPZONE* 📦\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`🆔 *Orden #:* *#${savedOrder.id}*\n` +
`👤 *Cliente:* ${savedOrder.customerName}\n` +
`📱 *Celular:* +${customerPhone}\n` +
`🏙 *Ciudad Destino:* ${destination.name}\n\n` +
`🛒 *DETALLE DEL PRODUCTO:*\n` +
`• 👟 *${matched.name}*\n` +
` 📏 Talla: *${resolvedSize}* EUR | Cantidad: *1*\n` +
` 💵 Valor Unitario: *${formatCOP(price)} COP*\n` +
` 💰 Subtotal: *${formatCOP(price)} COP*\n\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`💰 *TOTAL DE LA VENTA:* *${formatCOP(totalAmount)} COP*\n` +
`⚡ *Anticipo Separación:* *${formatCOP(depositAmount)} COP* (🔴 PENDIENTE DE PAGO)\n\n` +
`💳 *ENLACE DE PAGO EN LÍNEA CON BOLD (SEPARACIÓN):*\n` +
`👉 ${boldPaymentUrl}\n` +
`_(Acepta PSE, Nequi, Botón Bancolombia, Tarjetas de Crédito/Débito y QR Bre-B)_\n\n` +
`🤝 *Saldo Contraentrega al Recibir:* *${formatCOP(balanceDue)} COP*\n\n` +
`👤 *Tu Cuenta DripZone:* Registrada y vinculada automáticamente a tu número (+${customerPhone}).\n` +
`📱 *Rastreo en vivo:* 👉 https://drip-zone.online/mis-pedidos?phone=${customerPhone}\n` +
`━━━━━━━━━━━━━━━━━━━━━━`
);
await sendWhatsAppMessage(customerPhone, confirmText, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D').catch(() => {});
return { handled: true, order: savedOrder };
}
}
return { handled: false };
} catch (error: any) {
console.error('❌ Error en detectAndProcessWhatsAppOrder:', error);
return { handled: false, reason: error.message };
}
}