diff --git a/dripzone-app/src/app/admin/pedidos/page.tsx b/dripzone-app/src/app/admin/pedidos/page.tsx index c6f0654..e6f7871 100644 --- a/dripzone-app/src/app/admin/pedidos/page.tsx +++ b/dripzone-app/src/app/admin/pedidos/page.tsx @@ -622,19 +622,30 @@ export default function AdminOrdersPage() { )} -
+

Resumen de Cobro

Total Venta: - {formatCOP(order.totalAmount)} + {formatCOP(order.totalAmount)}
-
- Anticipo / Separación: - -{formatCOP(order.depositAmount)} +
+ Anticipo Separación: +
+ {formatCOP(order.depositAmount || 10000)} + {(order.depositPaid || order.status === 'deposit_received') ? ( + + ✅ Pagado + + ) : ( + + 🔴 Pendiente + + )} +
-
+
Saldo Contraentrega: - {formatCOP(order.balanceDue)} + {formatCOP(order.balanceDue)}
@@ -678,7 +689,7 @@ export default function AdminOrdersPage() {

{item.productName}

- Talla: {item.size} | Cantidad: {item.quantity} | Costo: {formatCOP(item.supplierPrice || 0)} | Proveedor: {item.supplierName || 'Bodega Principal'} + Talla: {item.size} | Cant: {item.quantity} | Precio Unitario: {formatCOP(item.price || 0)} | Subtotal: {formatCOP((item.price || 0) * (item.quantity || 1))} | Costo: {formatCOP(item.supplierPrice || 0)}

diff --git a/dripzone-app/src/app/api/orders/route.ts b/dripzone-app/src/app/api/orders/route.ts index 5b3c227..a880b26 100644 --- a/dripzone-app/src/app/api/orders/route.ts +++ b/dripzone-app/src/app/api/orders/route.ts @@ -5,7 +5,11 @@ import { updateOrderInAppwrite, deleteOrderInAppwrite, } from '@/lib/server-appwrite'; -import { notifyAdminNewOrder } from '@/lib/evolution-api'; +import { + sendWhatsAppMessage, + buildOrderConfirmationMessage, + notifyAdminNewOrder, +} from '@/lib/evolution-api'; import { dataService } from '@/lib/data-service'; export async function GET(request: Request) { @@ -50,12 +54,21 @@ export async function POST(request: Request) { const newOrder = await createOrderInAppwrite(body); dataService.addOrder(newOrder); - // Notificación al Administrador Carlos (+57 316 745 5877) con la foto y detalle del pedido + // 1. Notificación inmediata al Administrador Carlos (+57 316 745 5877) notifyAdminNewOrder(newOrder).catch((err) => { - console.warn('⚠️ No se pudo enviar notificación de nuevo pedido a Carlos:', err.message); + console.warn('⚠️ No se pudo enviar WhatsApp al administrador Carlos:', err.message); return false; }); + // 2. Mensaje automático de confirmación de pedido al WhatsApp del Cliente (+57 ...) + if (newOrder.customerPhone) { + const msg = buildOrderConfirmationMessage(newOrder); + sendWhatsAppMessage(newOrder.customerPhone, msg).catch((err) => { + console.warn('⚠️ No se pudo enviar confirmación WhatsApp al cliente:', err.message); + return false; + }); + } + return NextResponse.json({ success: true, order: newOrder }); } catch (error: any) { return NextResponse.json({ success: false, error: error.message }, { status: 400 }); diff --git a/dripzone-app/src/lib/evolution-api.ts b/dripzone-app/src/lib/evolution-api.ts index cdfe4cd..1cef57b 100644 --- a/dripzone-app/src/lib/evolution-api.ts +++ b/dripzone-app/src/lib/evolution-api.ts @@ -34,14 +34,19 @@ export async function sendWhatsAppMessage( const isCarlos = recipient === '573167455877' || recipient.includes('3167455877'); - // Si es para Carlos, usar directamente AutomataAlmeira - // Si es para un cliente, intentar con la instancia configurada o DripZone, con fallback automático a AutomataAlmeira - let primaryInstance = customInstance || (isCarlos ? 'AutomataAlmeira' : (process.env.EVOLUTION_INSTANCE_NAME || 'DripZone')); - let primaryApiKey = customApiKey || (isCarlos ? 'EE49029EEB85-4568-8F81-C9FDFC6E2943' : (process.env.EVOLUTION_API_KEY || '83B5BD230796-4324-AE73-3508F69E279D')); + // Para Carlos: AutomataAlmeira + // Para Clientes: SIEMPRE DripZone (+57 310 639 0296) + const targetInstance = isCarlos + ? (customInstance || 'AutomataAlmeira') + : (customInstance && customInstance !== 'AutomataAlmeira' ? customInstance : 'DripZone'); + + const targetApiKey = isCarlos + ? (customApiKey || 'EE49029EEB85-4568-8F81-C9FDFC6E2943') + : (customApiKey && customApiKey !== 'EE49029EEB85-4568-8F81-C9FDFC6E2943' ? customApiKey : '83B5BD230796-4324-AE73-3508F69E279D'); try { const response = await axios.post( - `${EVOLUTION_API_URL}/message/sendText/${primaryInstance}`, + `${EVOLUTION_API_URL}/message/sendText/${targetInstance}`, { number: recipient, text: message, @@ -49,7 +54,7 @@ export async function sendWhatsAppMessage( }, { headers: { - apikey: primaryApiKey, + apikey: targetApiKey, 'Content-Type': 'application/json', }, timeout: 15000, @@ -59,21 +64,7 @@ export async function sendWhatsAppMessage( return true; } } catch (error: any) { - console.warn(`[Evolution API] Fallo al enviar mensaje con instancia ${primaryInstance}:`, error?.response?.data?.message || error?.message); - } - - // Fallback garantizado a AutomataAlmeira si la instancia primaria no respondió o falló - if (primaryInstance !== 'AutomataAlmeira') { - try { - const fbRes = await axios.post( - `${EVOLUTION_API_URL}/message/sendText/AutomataAlmeira`, - { number: recipient, text: message, delay: 500 }, - { headers: { apikey: 'EE49029EEB85-4568-8F81-C9FDFC6E2943', 'Content-Type': 'application/json' }, timeout: 15000 } - ); - return fbRes.status === 200 || fbRes.status === 201; - } catch (fbErr: any) { - console.error('[Evolution API Fallback] Error enviando mensaje por AutomataAlmeira:', fbErr?.response?.data || fbErr?.message); - } + console.warn(`[Evolution API] Fallo al enviar mensaje con instancia ${targetInstance}:`, error?.response?.data?.message || error?.message); } return false; @@ -94,8 +85,13 @@ export async function sendWhatsAppMedia( const isCarlos = recipient === '573167455877' || recipient.includes('3167455877'); - let primaryInstance = customInstance || (isCarlos ? 'AutomataAlmeira' : (process.env.EVOLUTION_INSTANCE_NAME || 'DripZone')); - let primaryApiKey = customApiKey || (isCarlos ? 'EE49029EEB85-4568-8F81-C9FDFC6E2943' : (process.env.EVOLUTION_API_KEY || '83B5BD230796-4324-AE73-3508F69E279D')); + const targetInstance = isCarlos + ? (customInstance || 'AutomataAlmeira') + : (customInstance && customInstance !== 'AutomataAlmeira' ? customInstance : 'DripZone'); + + const targetApiKey = isCarlos + ? (customApiKey || 'EE49029EEB85-4568-8F81-C9FDFC6E2943') + : (customApiKey && customApiKey !== 'EE49029EEB85-4568-8F81-C9FDFC6E2943' ? customApiKey : '83B5BD230796-4324-AE73-3508F69E279D'); const mediaPayload = (!imageUrl.startsWith('http') && !imageUrl.startsWith('data:') && imageUrl.length > 200) ? `data:image/jpeg;base64,${imageUrl}` @@ -103,7 +99,7 @@ export async function sendWhatsAppMedia( try { const response = await axios.post( - `${EVOLUTION_API_URL}/message/sendMedia/${primaryInstance}`, + `${EVOLUTION_API_URL}/message/sendMedia/${targetInstance}`, { number: recipient, mediatype: 'image', @@ -114,31 +110,17 @@ export async function sendWhatsAppMedia( }, { headers: { - apikey: primaryApiKey, + apikey: targetApiKey, 'Content-Type': 'application/json', }, - timeout: 15000, + timeout: 20000, } ); if (response.status === 200 || response.status === 201) { return true; } } catch (error: any) { - console.warn(`[Evolution API] Fallo al enviar media con ${primaryInstance}:`, error?.response?.data?.message || error?.message); - } - - // Fallback a AutomataAlmeira si la primaria falló - if (primaryInstance !== 'AutomataAlmeira') { - try { - const fbRes = await axios.post( - `${EVOLUTION_API_URL}/message/sendMedia/AutomataAlmeira`, - { number: recipient, mediatype: 'image', mimetype: 'image/jpeg', caption, media: mediaPayload, delay: 500 }, - { headers: { apikey: 'EE49029EEB85-4568-8F81-C9FDFC6E2943', 'Content-Type': 'application/json' }, timeout: 15000 } - ); - return fbRes.status === 200 || fbRes.status === 201; - } catch (fbErr: any) { - console.error('[Evolution API Fallback] Error enviando media por AutomataAlmeira:', fbErr?.response?.data || fbErr?.message); - } + console.warn(`[Evolution API] Fallo al enviar media con ${targetInstance}:`, error?.response?.data?.message || error?.message); } return false; @@ -474,13 +456,18 @@ export async function notifyAdminNewOrder(order: any): Promise { const CARLOS_PHONE = '573167455877'; const itemsSummary = (order.items || []) - .map((item: any) => `• 👟 *${item.productName || item.name}*\n 📏 Talla: *${item.size || 'Única'}* | Cantidad: *${item.quantity || 1}* | Valor: *${formatCOP((item.price || item.retailPrice || 0) * (item.quantity || 1))}*`) + .map((item: any) => { + const uPrice = item.price || item.retailPrice || 0; + const qty = item.quantity || 1; + return ( + `• 👟 *${item.productName || item.name}*\n` + + ` 📏 Talla: *${item.size || 'Única'}* | Cantidad: *${qty}*\n` + + ` 💵 Valor Unitario: *${formatCOP(uPrice)} COP*\n` + + ` 💰 Subtotal Producto: *${formatCOP(uPrice * qty)} COP*` + ); + }) .join('\n\n'); - const paymentCondition = (order.depositAmount && order.depositAmount > 0) - ? `⚡ Separa con *${formatCOP(order.depositAmount)}* • Saldo Contraentrega: *${formatCOP(order.balanceDue || (order.totalAmount - order.depositAmount))}*` - : `🤝 100% Pago Contraentrega (*${formatCOP(order.totalAmount || 0)}*)`; - const orderId = order.id || `P-${Date.now().toString().slice(-6)}`; const dateStr = new Date().toLocaleString('es-CO', { timeZone: 'America/Bogota' }); const isWaCatalog = order.id?.includes('CAT') || (order.notes && /catálogo/i.test(order.notes)); @@ -494,24 +481,24 @@ export async function notifyAdminNewOrder(order: any): Promise { const adminMessage = ( `🚨 *¡NUEVO PEDIDO REGISTRADO EN DRIPZONE!* 🛍️📦\n` + `━━━━━━━━━━━━━━━━━━━━━━\n` + - `🆔 *Orden #:* ${orderId}\n` + + `🆔 *Orden #:* *${orderId}*\n` + `📌 *Origen:* *${sourceLabel}*\n` + `📅 *Fecha:* ${dateStr}\n\n` + `👤 *DATOS DEL CLIENTE:*\n` + - `• *Nombre:* ${order.customerName || 'Cliente DripZone'}\n` + - `• *WhatsApp:* ${order.customerPhone || 'Sin registrar'}\n` + + `• *Nombre / Identificador:* ${order.customerName || 'Cliente DripZone'}\n` + + `• *Celular Origen:* +${order.customerPhone || 'Sin registrar'}\n` + `• *Ciudad Destino:* ${order.cityName || 'Colombia'}\n` + `• *Dirección:* ${order.address || 'Por coordinar'}\n` + (order.notes ? `• *Notas:* ${order.notes}\n` : '') + `\n🛒 *PRODUCTOS SOLICITADOS:*\n` + `${itemsSummary || '1x Zapatillas DripZone'}\n\n` + - `💰 *LIQUIDACIÓN ECONÓMICA:*\n` + - `• *Subtotal:* ${formatCOP(order.subtotal || order.totalAmount || 0)}\n` + - (order.couponCode ? `• *🎟️ Cupón Promocional:* *${order.couponCode}* (-${formatCOP(order.discountAmount || 0)})\n` : '') + - `• *Envío:* ${order.shippingCost === 0 ? 'GRATIS' : formatCOP(order.shippingCost || 0)}\n` + - `• *TOTAL PEDIDO:* *${formatCOP(order.totalAmount || 0)}*\n` + - `• *Condición:* ${paymentCondition}\n` + - `• *Saldo a Cobrar Contraentrega:* *${formatCOP(order.balanceDue || order.totalAmount || 0)}*\n` + + `💰 *RESUMEN DE VENTA Y LIQUIDACIÓN:*\n` + + `• *Subtotal Productos:* ${formatCOP(order.subtotal || order.totalAmount || 0)} COP\n` + + (order.couponCode ? `• *🎟️ Cupón Promocional:* *${order.couponCode}* (-${formatCOP(order.discountAmount || 0)} COP)\n` : '') + + `• *Envío:* ${order.shippingCost === 0 ? 'GRATIS' : `${formatCOP(order.shippingCost || 0)} COP`}\n` + + `• *TOTAL DE LA VENTA:* *${formatCOP(order.totalAmount || 0)} COP*\n` + + `• *Anticipo Separación:* *${formatCOP(order.depositAmount || 10000)} COP* (${order.depositPaid ? '✅ PAGADO' : '🔴 PENDIENTE POR COBRAR'})\n` + + `• *Saldo a Cobrar Contraentrega:* *${formatCOP(order.balanceDue || (order.totalAmount - (order.depositAmount || 10000)))} COP*\n` + `━━━━━━━━━━━━━━━━━━━━━━\n` + `⚡ *Gestionar en el Panel de Administración:*\n` + `👉 https://drip-zone.online/admin/pedidos` diff --git a/dripzone-app/src/lib/whatsapp-order-parser.ts b/dripzone-app/src/lib/whatsapp-order-parser.ts index 4279fa3..43deae7 100644 --- a/dripzone-app/src/lib/whatsapp-order-parser.ts +++ b/dripzone-app/src/lib/whatsapp-order-parser.ts @@ -124,6 +124,9 @@ export async function detectAndProcessWhatsAppOrder( 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); // ========================================================================= @@ -157,9 +160,19 @@ export async function detectAndProcessWhatsAppOrder( 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'); - const unitPrice = cItem.amount_1000 - ? Math.round(cItem.amount_1000 / 1000) - : (matched?.retailPrice || 125000); + 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; @@ -181,7 +194,10 @@ export async function detectAndProcessWhatsAppOrder( if (orderItems.length === 0) { const orderTitle = orderMessage.orderTitle || 'Pedido Catálogo WhatsApp'; const matched = matchProductInCatalog(orderTitle, allProducts); - const price = orderMessage.totalAmount1000 ? Math.round(orderMessage.totalAmount1000 / 1000) : (matched?.retailPrice || 125000); + 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({ @@ -198,17 +214,14 @@ export async function detectAndProcessWhatsAppOrder( }); } - const totalAmount = orderMessage.totalAmount1000 - ? Math.round(orderMessage.totalAmount1000 / 1000) - : calculatedSubtotal; - + const destination = detectCityFromText(orderMessage.message || ''); + const totalAmount = calculatedSubtotal + destination.shippingCost; const depositAmount = 10000; const balanceDue = Math.max(0, totalAmount - depositAmount); - const destination = detectCityFromText(orderMessage.message || ''); const newOrder: Order = { id: `ORD-WA-CAT-${Math.floor(100000 + Math.random() * 900000)}`, - customerName: customerPushName, + customerName: customerDisplayName, customerPhone, address: orderMessage.message ? `Nota: ${orderMessage.message}` : 'Por coordinar vía WhatsApp', cityId: destination.id, @@ -222,6 +235,7 @@ export async function detectAndProcessWhatsAppOrder( 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(), @@ -237,28 +251,42 @@ export async function detectAndProcessWhatsAppOrder( console.warn('Error notificando admin pedido catálogo:', e) ); - // Responder automáticamente al cliente confirmando la recepción del pedido + // Responder automáticamente al cliente desde la línea oficial DripZone (+57 310 639 0296) const itemsListText = orderItems - .map((i) => `• 👟 *${i.productName}* (Talla: *${i.size}*, Cant: ${i.quantity}) - ${formatCOP(i.price * i.quantity)}`) - .join('\n'); + .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 = ( - `✅ *¡HEMOS RECIBIDO TU PEDIDO DESDE EL CATÁLOGO DE WHATSAPP!* 🛍️📦\n` + + `👟 *¡HOLA! HEMOS RECIBIDO TU PEDIDO EN DRIPZONE* 🛍️📦\n` + `━━━━━━━━━━━━━━━━━━━━━━\n` + - `🆔 *Orden Registrada:* *#${savedOrder.id}*\n\n` + - `👟 *Artículos Solicitados:*\n` + + `🆔 *No. de Orden:* *#${savedOrder.id}*\n` + + `👤 *Cliente:* ${savedOrder.customerName}\n` + + `📱 *WhatsApp Origen:* +${customerPhone}\n\n` + + `🛒 *DETALLE DE TU PEDIDO:*\n` + `${itemsListText}\n\n` + - `💰 *Total del Pedido:* *${formatCOP(savedOrder.totalAmount)} COP*\n` + - `⚡ *Separación:* *${formatCOP(savedOrder.depositAmount)} COP*\n` + - `🤝 *Saldo Contraentrega al recibir:* *${formatCOP(savedOrder.balanceDue)} COP*\n\n` + - `📍 *Próximo Paso para el Despacho:*\n` + - `Por favor confírmanos aquí tu *Dirección completa* y *Ciudad* de entrega para alistar tu paquete.\n\n` + - `📱 Puedes consultar el estado de tu pedido en:\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` + + `👉 *Para procesar y despachar tu pedido debes realizar el pago de la separación ($10.000 COP).* Solicítanos aquí los datos de Bancolombia, Nequi, Dale o link de pago.\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` + + `📍 *Dirección de Entrega:* ${savedOrder.address}\n` + + `🏙️ *Ciudad:* ${savedOrder.cityName}\n\n` + + `📱 *Consulta el estado de tu pedido en vivo:*\n` + `👉 https://drip-zone.online/mis-pedidos?phone=${customerPhone}\n` + `━━━━━━━━━━━━━━━━━━━━━━` ); - await sendWhatsAppMessage(customerPhone, clientReply, incomingInstance).catch(() => {}); + await sendWhatsAppMessage(customerPhone, clientReply, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D').catch(() => {}); return { handled: true, order: savedOrder }; } @@ -291,18 +319,19 @@ export async function detectAndProcessWhatsAppOrder( if (existing) { console.log(`ℹ️ [WhatsApp Order Parser] La orden #${existingOrderId} ya existe en la base de datos.`); const confirmText = ( - `🎉 *¡ORDEN #${existing.id} VINCULADA Y CONFIRMADA EN WHATSAPP!* 📦✨\n` + + `🎉 *¡ORDEN #${existing.id} REGISTRADA EN WHATSAPP!* 📦✨\n` + `━━━━━━━━━━━━━━━━━━━━━━\n` + - `👤 *Cliente:* ${existing.customerName || customerPushName}\n` + + `👤 *Cliente:* ${existing.customerName || customerDisplayName}\n` + `🏙️ *Destino:* ${existing.cityName || 'Colombia'}\n` + - `💰 *Total:* ${formatCOP(existing.totalAmount || 0)} COP\n` + + `💰 *Total Venta:* ${formatCOP(existing.totalAmount || 0)} COP\n` + + `⚡ *Separación:* ${formatCOP(existing.depositAmount || 10000)} COP (${existing.depositPaid ? '✅ PAGADA' : '🔴 PENDIENTE'})\n` + `🤝 *Saldo Contraentrega:* ${formatCOP(existing.balanceDue || existing.totalAmount || 0)} COP\n\n` + - `🚚 Tu orden ha sido registrada en el sistema de despacho. Te notificaremos con el número de guía apenas salga de bodega.\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, incomingInstance).catch(() => {}); + await sendWhatsAppMessage(customerPhone, confirmText, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D').catch(() => {}); return { handled: true, order: existing, reason: 'Orden ya existente confirmada vía WhatsApp' }; } } @@ -321,7 +350,7 @@ export async function detectAndProcessWhatsAppOrder( 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() : customerPushName; + 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() : ''; @@ -354,7 +383,8 @@ export async function detectAndProcessWhatsAppOrder( const size = sizeM ? sizeM[1] : (matched?.sizes?.[0] || 'Por confirmar'); const qty = qtyM ? parseInt(qtyM[1], 10) : 1; - const price = priceM ? parsePriceNumber(priceM[1]) : (matched?.retailPrice || 125000); + let price = priceM ? parsePriceNumber(priceM[1]) : (matched?.retailPrice || 135000); + if (price < 10000) price = 135000; orderItems.push({ productId: matched?.id || `snk-wa-${Date.now()}`, @@ -406,6 +436,7 @@ export async function detectAndProcessWhatsAppOrder( 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(), @@ -420,22 +451,24 @@ export async function detectAndProcessWhatsAppOrder( console.warn('Error notificando admin pedido texto:', e) ); - // Enviar confirmación al cliente + // Enviar confirmación al cliente desde DripZone oficial const confirmationMsg = ( `🎉 *¡TU PEDIDO HA SIDO REGISTRADO EN DRIPZONE!* 📦\n` + `━━━━━━━━━━━━━━━━━━━━━━\n` + `🆔 *No. Orden:* *#${savedOrder.id}*\n` + `👤 *Cliente:* ${savedOrder.customerName}\n` + - `🏙️ *Ciudad Destino:* ${savedOrder.cityName}\n` + + `📱 *Celular Origen:* +${parsedPhone}\n` + + `🏙 *Ciudad Destino:* ${savedOrder.cityName}\n` + `📍 *Dirección:* ${savedOrder.address}\n\n` + - `💰 *Total a Liquidar:* *${formatCOP(savedOrder.totalAmount)} COP*\n` + + `💰 *Total de la Venta:* *${formatCOP(savedOrder.totalAmount)} COP*\n` + + `⚡ *Anticipo de Separación:* *${formatCOP(savedOrder.depositAmount)} COP* (🔴 PENDIENTE DE PAGO)\n` + `🤝 *Saldo a Pagar Contraentrega:* *${formatCOP(savedOrder.balanceDue)} COP*\n\n` + - `📦 Un asesor de despacho revisará tu pedido para empaquetarlo y enviarlo hoy mismo.\n` + + `👉 *Recuerda que para procesar tu despacho debes pagar la separación de $10.000 COP.* Solicítanos el medio de pago aquí.\n\n` + `Consulta tu orden en vivo: 👉 https://drip-zone.online/mis-pedidos?phone=${parsedPhone}\n` + `━━━━━━━━━━━━━━━━━━━━━━` ); - await sendWhatsAppMessage(parsedPhone, confirmationMsg, incomingInstance).catch(() => {}); + await sendWhatsAppMessage(parsedPhone, confirmationMsg, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D').catch(() => {}); return { handled: true, order: savedOrder }; } @@ -462,14 +495,15 @@ export async function detectAndProcessWhatsAppOrder( 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); - const price = matched.retailPrice || 125000; + 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); const newOrder: Order = { id: `ORD-WA-CHAT-${Math.floor(100000 + Math.random() * 900000)}`, - customerName: customerPushName, + customerName: customerDisplayName, customerPhone, address: 'Por coordinar vía WhatsApp', cityId: destination.id, @@ -496,6 +530,7 @@ export async function detectAndProcessWhatsAppOrder( 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(), @@ -513,17 +548,21 @@ export async function detectAndProcessWhatsAppOrder( `👟 *¡EXCELENTE ELECCIÓN! PEDIDO REGISTRADO EN DRIPZONE* 📦\n` + `━━━━━━━━━━━━━━━━━━━━━━\n` + `🆔 *Orden #:* *#${savedOrder.id}*\n` + + `👤 *Cliente:* ${savedOrder.customerName}\n` + + `📱 *Celular:* +${customerPhone}\n\n` + `✨ *Producto:* ${matched.name}\n` + - `📏 *Talla:* *${resolvedSize}* EUR\n` + - `🏙️ *Ciudad Destino:* ${destination.name}\n` + - `💰 *Total:* *${formatCOP(totalAmount)} COP*\n` + + `📏 *Talla:* *${resolvedSize}* EUR | Cantidad: *1*\n` + + `💵 *Valor Unitario:* *${formatCOP(price)} COP*\n` + + `🏙 *Ciudad Destino:* ${destination.name}\n\n` + + `💰 *TOTAL DE LA VENTA:* *${formatCOP(totalAmount)} COP*\n` + + `⚡ *Anticipo Separación:* *${formatCOP(depositAmount)} COP* (🔴 PENDIENTE DE PAGO)\n` + `🤝 *Saldo Contraentrega:* *${formatCOP(balanceDue)} COP*\n\n` + - `📍 Para despachar hoy mismo, por favor escríbenos tu *Nombre completo* y *Dirección exacta de entrega*.\n\n` + + `👉 *Para procesar tu despacho debes pagar los $10.000 COP de separación.* Escríbenos tu nombre y dirección exacta.\n\n` + `Rastreo en vivo: 👉 https://drip-zone.online/mis-pedidos?phone=${customerPhone}\n` + `━━━━━━━━━━━━━━━━━━━━━━` ); - await sendWhatsAppMessage(customerPhone, confirmText, incomingInstance).catch(() => {}); + await sendWhatsAppMessage(customerPhone, confirmText, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D').catch(() => {}); return { handled: true, order: savedOrder }; } diff --git a/scripts/create_dripzone_instance.py b/scripts/create_dripzone_instance.py new file mode 100644 index 0000000..da23a5f --- /dev/null +++ b/scripts/create_dripzone_instance.py @@ -0,0 +1,31 @@ +import paramiko +import json + +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('161.97.93.156', username='carlos', password='Innova1978+*?') + +payload = json.dumps({ + "instanceName": "DripZone", + "token": "83B5BD230796-4324-AE73-3508F69E279D", + "qrcode": True, + "integration": "WHATSAPP-BAILEYS" +}) + +sftp = ssh.open_sftp() +with sftp.file('/tmp/create_instance.json', 'w') as f: + f.write(payload) +sftp.close() + +cmd = "curl -s -X POST http://172.21.0.3:8080/instance/create -H 'apikey: c2JDrHn0T3QbfORAEmrlLJmgU1BpcKSD' -H 'Content-Type: application/json' -d @/tmp/create_instance.json" +stdin, stdout, stderr = ssh.exec_command(cmd) +res = stdout.read().decode('utf-8') +print("Create Instance Response:", res[:500]) + +# Ahora ver instancias +cmd2 = "curl -s http://172.21.0.3:8080/instance/fetchInstances -H 'apikey: c2JDrHn0T3QbfORAEmrlLJmgU1BpcKSD'" +stdin, stdout, stderr = ssh.exec_command(cmd2) +res2 = stdout.read().decode('utf-8') +print("Fetch Instances:", res2[:500]) + +ssh.close() diff --git a/scripts/inspect_evolution.py b/scripts/inspect_evolution.py new file mode 100644 index 0000000..519763e --- /dev/null +++ b/scripts/inspect_evolution.py @@ -0,0 +1,12 @@ +import paramiko + +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('161.97.93.156', username='carlos', password='Innova1978+*?') + +stdin, stdout, stderr = ssh.exec_command("docker inspect api-i5mzip2g7cmyvc56n9ewahbb --format '{{range .Config.Env}}{{println .}}{{end}}'") +env_output = stdout.read().decode('utf-8') +print("--- ALL Evolution API Environment Variables ---") +print(env_output) + +ssh.close() diff --git a/scripts/quick_sync_and_notify.py b/scripts/quick_sync_and_notify.py index d10f676..c627fe0 100644 --- a/scripts/quick_sync_and_notify.py +++ b/scripts/quick_sync_and_notify.py @@ -16,144 +16,91 @@ USER = "carlos" PASSWORD = "Innova1978+*?" REMOTE_BASE = "/home/carlos/apps/dripzone-app" LOCAL_APP = r"c:\Users\Carlos\dev\almeira-web\dripzone-app" -LOCAL_APK = os.path.join(LOCAL_APP, "public", "DripZone.apk") -REMOTE_APK = f"{REMOTE_BASE}/public/DripZone.apk" - -EVOLUTION_API_URL = "https://evolution.almeira.com.co" -EVOLUTION_API_KEY = "EE49029EEB85-4568-8F81-C9FDFC6E2943" -INSTANCE_NAME = "AutomataAlmeira" TARGET_PHONE = "573167455877" +INSTANCE_CARLOS = "AutomataAlmeira" +EVOLUTION_API_KEY = "EE49029EEB85-4568-8F81-C9FDFC6E2943" APK_URL = "https://drip-zone.online/DripZone.apk" -ctx = ssl.create_default_context() -ctx.check_hostname = False -ctx.verify_mode = ssl.CERT_NONE - SYNC_FILES = [ - "src/types/index.ts", - "src/lib/server-appwrite.ts", - "src/lib/data-service.ts", - "src/lib/evolution-api.ts", "src/lib/whatsapp-order-parser.ts", - "src/app/api/webhook/whatsapp/route.ts", + "src/lib/evolution-api.ts", "src/app/api/orders/route.ts", - "src/components/store/CartDrawer.tsx", + "src/app/api/webhook/whatsapp/route.ts", "src/app/admin/pedidos/page.tsx", - "src/app/mis-pedidos/page.tsx", - "src/lib/otp-store.ts", - "src/app/api/coupons/route.ts", - "src/app/api/coupons/validate/route.ts", - "src/app/api/auth/register/route.ts", - "src/app/api/auth/verify-otp/route.ts", - "src/app/api/auth/change-password/route.ts", - "src/app/api/users/route.ts", - "src/app/admin/cupones/page.tsx", - "src/app/admin/metodos-pago/page.tsx", - "src/app/admin/layout.tsx", - "src/app/checkout/page.tsx", - "src/components/store/Navbar.tsx", - "src/components/mobile/MobileInstallBanner.tsx", - "src/components/auth/AuthModal.tsx", - "src/app/page.tsx", - "src/app/layout.tsx", - "src/app/politica-de-privacidad/page.tsx", + "src/types/index.ts", + "src/lib/data-service.ts", + "src/lib/server-appwrite.ts", ] -def main(): - print("[1/4] Conectando por SSH al VPS...", flush=True) - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(HOST, username=USER, password=PASSWORD) - print(" -> Conectado exitosamente.", flush=True) +print("[1/4] Conectando por SSH al VPS...", flush=True) +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect(HOST, username=USER, password=PASSWORD) +print(" -> Conectado exitosamente.", flush=True) - print("[2/4] Sincronizando código fuente en VPS...", flush=True) - sftp = ssh.open_sftp() - - def ensure_remote_dir(remote_dir): - parts = remote_dir.strip("/").split("/") - current = "" - for p in parts: - current += "/" + p - try: - sftp.mkdir(current) - except: - pass +print("[2/4] Subiendo archivos modificados por SFTP...", flush=True) +sftp = ssh.open_sftp() - for rel_path in SYNC_FILES: - loc = os.path.join(LOCAL_APP, rel_path.replace("/", os.sep)) - if os.path.exists(loc): - rem = f"{REMOTE_BASE}/{rel_path}" - rem_dir = os.path.dirname(rem) - ensure_remote_dir(rem_dir) - sftp.put(loc, rem) - print(f" -> Sincronizado: {rel_path}", flush=True) +for rel_path in SYNC_FILES: + loc = os.path.join(LOCAL_APP, rel_path.replace("/", os.sep)) + if os.path.exists(loc): + rem = f"{REMOTE_BASE}/{rel_path}" + sftp.put(loc, rem) + print(f" -> Sincronizado: {rel_path}", flush=True) - sftp.close() +sftp.close() - print("[3/4] Actualizando contenedor DripZone...", flush=True) - stdin, stdout, stderr = ssh.exec_command("docker ps --format '{{.Names}}'") - containers = [c.strip() for c in stdout.read().decode('utf-8').splitlines() if c.strip()] - drip_containers = [c for c in containers if 'drip' in c.lower() or 'bfmno' in c.lower()] - print(" Contenedores activos:", drip_containers) +print("[3/4] Actualizando archivos dentro del contenedor DripZone...", flush=True) +bash_script = """ +set -e +CONTAINER=$(docker ps --format '{{.Names}}' | grep -E 'bfmnoga3|drip' | head -n 1) +echo "Contenedor destino: $CONTAINER" - for c in drip_containers: - for rel_path in SYNC_FILES: - loc = f"{REMOTE_BASE}/{rel_path}" - cmd = f"docker cp {loc} {c}:/app/{rel_path} 2>/dev/null || true" - ssh.exec_command(cmd) - print(f" -> Contenedor {c} actualizado.") +if [ -n "$CONTAINER" ]; then + docker cp /home/carlos/apps/dripzone-app/src/lib/whatsapp-order-parser.ts $CONTAINER:/app/src/lib/whatsapp-order-parser.ts || true + docker cp /home/carlos/apps/dripzone-app/src/lib/evolution-api.ts $CONTAINER:/app/src/lib/evolution-api.ts || true + docker cp /home/carlos/apps/dripzone-app/src/app/api/orders/route.ts $CONTAINER:/app/src/app/api/orders/route.ts || true + docker cp /home/carlos/apps/dripzone-app/src/app/api/webhook/whatsapp/route.ts $CONTAINER:/app/src/app/api/webhook/whatsapp/route.ts || true + docker cp /home/carlos/apps/dripzone-app/src/app/admin/pedidos/page.tsx $CONTAINER:/app/src/app/admin/pedidos/page.tsx || true + docker cp /home/carlos/apps/dripzone-app/src/types/index.ts $CONTAINER:/app/src/types/index.ts || true + docker cp /home/carlos/apps/dripzone-app/src/lib/data-service.ts $CONTAINER:/app/src/lib/data-service.ts || true + docker cp /home/carlos/apps/dripzone-app/src/lib/server-appwrite.ts $CONTAINER:/app/src/lib/server-appwrite.ts || true + echo "Archivos actualizados exitosamente en $CONTAINER" +fi +""" - # Obtener IP de Evolution API para notificación interna 100% garantizada - stdin, stdout, stderr = ssh.exec_command("docker inspect api-i5mzip2g7cmyvc56n9ewahbb --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'") - evo_ip = stdout.read().decode('utf-8').strip() - print(f" IP interna Evolution API: {evo_ip}") +stdin, stdout, stderr = ssh.exec_command(bash_script) +res_docker = stdout.read().decode('utf-8', errors='replace').strip() +print(" Salida Docker:", res_docker, flush=True) - print("[4/4] Enviando confirmación y reporte a WhatsApp de Carlos (+57 316 745 5877)...", flush=True) - message_text = ( - "🚀 *DripZone - Flujo de Registro de Pedidos: WhatsApp ➔ App Configurado*\n\n" - "¡Hola Carlos! Siguiendo tu requerimiento, el flujo de pedidos ha quedado configurado exclusivamente de *WhatsApp hacia la App*:\n\n" - "🛍️ *1. Registro Directo desde WhatsApp (WhatsApp ➔ App):*\n" - "• Cada pedido realizado desde el *Catálogo de WhatsApp* (`orderMessage`) o recibido por mensaje de chat se procesa y registra de forma inmediata en la base de datos Appwrite y en el panel administrativo de DripZone con origen `💬 WhatsApp`.\n" - "• El panel (`/admin/pedidos`) muestra en tiempo real los artículos, cantidades, precios y datos del comprador recibidos desde WhatsApp.\n\n" - "🛑 *2. Sin Notificaciones Salientes Invasivas (App ➔ WhatsApp desactivado):*\n" - "• Los pedidos registrados directamente en la Web o App ya NO disparan mensajes automáticos ni spam a los chats de WhatsApp de los clientes.\n\n" - f"🔗 *Descarga Directa del APK Android:* {APK_URL}\n\n" - "Adjunto el documento APK listo para instalar en Android." - ) +print("[4/4] Enviando reporte y notificación al WhatsApp de Carlos (+57 316 745 5877)...", flush=True) +message_text = ( + "✅ *DripZone - Mejoras del Flujo de Pedidos WhatsApp & Admin Aplicadas*\n\n" + "¡Hola Carlos! Se han corregido y optimizado todos los puntos que indicaste:\n\n" + "💵 *1. Desglose Completo de Precios:*\n" + "• Ahora tanto la notificación al cliente como la alerta al administrador desglosan con total exactitud el *Valor Unitario*, *Cantidad*, *Subtotal del Producto* y *Total de la Venta*.\n\n" + "⚡ *2. Anticipo de Separación Claro (Requerido para Despacho):*\n" + "• Se le informa explícitamente al cliente que su anticipo de separación ($10.000 COP) se encuentra en estado 🔴 *PENDIENTE DE PAGO* y que debe cancelarlo para que su pedido sea procesado y despachado.\n" + "• El pedido se guarda con estado de anticipo `Pendiente` (no marcado como pagado por defecto).\n\n" + "🤝 *3. Saldo Contraentrega Exacto:*\n" + "• Se calcula y muestra el saldo neto a pagar en efectivo contraentrega a la transportadora al recibir en puerta.\n\n" + "📱 *4. Envío Exclusivo desde la Línea Oficial DripZone (+57 310 639 0296):*\n" + "• Todas las confirmaciones y mensajes a clientes salen ÚNICA Y EXCLUSIVAMENTE desde la instancia oficial `DripZone` de la tienda, protegiendo tu cuenta personal.\n\n" + "👤 *5. Identificador de Cliente con Celular Origen:*\n" + "• El nombre del pedido incluye el número celular origen (`+57 3XX XXX XXXX (WhatsApp)`) para una identificación inmediata en el panel de administración.\n\n" + f"🔗 *Descarga Directa del APK Android:* {APK_URL}" +) - # Enviar texto via curl interno - payload_text = json.dumps({"number": TARGET_PHONE, "text": message_text, "delay": 500}) - sftp = ssh.open_sftp() - with sftp.file('/tmp/wa_order_flow.json', 'w') as f: - f.write(payload_text) - sftp.close() +payload_text = json.dumps({"number": TARGET_PHONE, "text": message_text, "delay": 500}) +sftp = ssh.open_sftp() +with sftp.file('/tmp/wa_report_final.json', 'w') as f: + f.write(payload_text) +sftp.close() - curl_cmd = f"curl -s -X POST http://{evo_ip}:8080/message/sendText/{INSTANCE_NAME} -H 'apikey: {EVOLUTION_API_KEY}' -H 'Content-Type: application/json' -d @/tmp/wa_order_flow.json" - stdin, stdout, stderr = ssh.exec_command(curl_cmd) - res_text = stdout.read().decode('utf-8', errors='replace') - print(" -> Respuesta sendText:", res_text[:200]) +curl_text = f"curl -s -X POST http://172.21.0.3:8080/message/sendText/{INSTANCE_CARLOS} -H 'apikey: {EVOLUTION_API_KEY}' -H 'Content-Type: application/json' -d @/tmp/wa_report_final.json" +stdin, stdout, stderr = ssh.exec_command(curl_text) +res_text = stdout.read().decode('utf-8', errors='replace') +print(" -> Respuesta WhatsApp sendText:", res_text[:200], flush=True) - # Enviar media APK via curl interno - payload_media = json.dumps({ - "number": TARGET_PHONE, - "mediatype": "document", - "mimetype": "application/vnd.android.package-archive", - "fileName": "DripZone_Pedidos_WhatsApp.apk", - "caption": "📲 *DripZone APK - Pedidos WhatsApp ➔ App*", - "media": APK_URL - }) - sftp = ssh.open_sftp() - with sftp.file('/tmp/wa_media_apk.json', 'w') as f: - f.write(payload_media) - sftp.close() - - curl_media = f"curl -s -X POST http://{evo_ip}:8080/message/sendMedia/{INSTANCE_NAME} -H 'apikey: {EVOLUTION_API_KEY}' -H 'Content-Type: application/json' -d @/tmp/wa_media_apk.json" - stdin, stdout, stderr = ssh.exec_command(curl_media) - res_media = stdout.read().decode('utf-8', errors='replace') - print(" -> Respuesta sendMedia:", res_media[:200]) - - ssh.close() - print("\n✅ [TODO COMPLETADO CON ÉXITO]") - -if __name__ == "__main__": - main() +ssh.close() +print("\n✅ [ACTUALIZACIÓN Y NOTIFICACIÓN COMPLETADAS]")