754 lines
34 KiB
TypeScript
754 lines
34 KiB
TypeScript
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 };
|
||
}
|
||
}
|