almeira-web/dripzone-app/src/lib/evolution-api.ts

306 lines
12 KiB
TypeScript

import axios from 'axios';
import { formatCOP } from './pricing-engine';
const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co';
// Instancia oficial exclusiva de DripZone (Para pedidos, confirmaciones, OTP, cobros Bold y atención a clientes)
const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || '83B5BD230796-4324-AE73-3508F69E279D';
const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'DripZone';
// Instancia personal de Carlos (Para scraping de publicaciones y grupos de WhatsApp)
export const EVOLUTION_FEED_API_KEY = process.env.EVOLUTION_FEED_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
export const EVOLUTION_FEED_INSTANCE_NAME = process.env.EVOLUTION_FEED_INSTANCE_NAME || 'AutomataAlmeira';
function resolveRecipient(target: string): string {
const trimmed = target.trim();
if (trimmed.includes('@')) {
return trimmed;
}
const cleanDigits = trimmed.replace(/\D/g, '');
return cleanDigits.startsWith('57') ? cleanDigits : `57${cleanDigits}`;
}
/**
* Enviar mensaje de texto por WhatsApp a través de Evolution API v2
* Usa por defecto la línea oficial DripZone
*/
export async function sendWhatsAppMessage(
toPhone: string,
message: string,
customInstance?: string,
customApiKey?: string
): Promise<boolean> {
const recipient = resolveRecipient(toPhone);
if (!recipient) return false;
const instance = customInstance || EVOLUTION_INSTANCE_NAME;
const apiKey = customApiKey || EVOLUTION_API_KEY;
try {
const response = await axios.post(
`${EVOLUTION_API_URL}/message/sendText/${instance}`,
{
number: recipient,
text: message,
delay: 1200,
},
{
headers: {
apikey: apiKey,
'Content-Type': 'application/json',
},
timeout: 20000,
}
);
return response.status === 200 || response.status === 201;
} catch (error: any) {
console.error(`Error enviando mensaje de WhatsApp (${instance}):`, error?.response?.data || error?.message);
return false;
}
}
/**
* Enviar mensaje con foto/catálogo a través de Evolution API v2
* Usa por defecto la línea oficial DripZone
*/
export async function sendWhatsAppMedia(
toPhone: string,
imageUrl: string,
caption: string,
customInstance?: string,
customApiKey?: string
): Promise<boolean> {
const recipient = resolveRecipient(toPhone);
if (!recipient) return false;
const instance = customInstance || EVOLUTION_INSTANCE_NAME;
const apiKey = customApiKey || EVOLUTION_API_KEY;
try {
const response = await axios.post(
`${EVOLUTION_API_URL}/message/sendMedia/${instance}`,
{
number: recipient,
mediatype: 'image',
mimetype: 'image/jpeg',
caption: caption,
media: imageUrl,
delay: 1500,
},
{
headers: {
apikey: apiKey,
'Content-Type': 'application/json',
},
timeout: 25000,
}
);
return response.status === 200 || response.status === 201;
} catch (error: any) {
console.error(`Error enviando media por WhatsApp (${instance}):`, error?.response?.data || error?.message);
return false;
}
}
/**
* Notifica inmediatamente la publicación automática de un nuevo producto
* tanto al WhatsApp personal de Carlos (+57 316 745 5877) como al canal
* de Notificaciones Almeira (120363428012905103@g.us).
*/
export async function notifyNewProductPublished(
product: any,
sourceGroup: string,
mediaUrl?: string
): Promise<{ success: boolean; dispatchedTo: string[] }> {
const NOTIFICATION_TARGETS = [
'573167455877', // Carlos (WhatsApp personal)
'120363428012905103@g.us', // Notificaciones Almeira
];
const retailFormatted = formatCOP(product.retailPrice || 125000);
const supplierFormatted = product.supplierPrice ? formatCOP(product.supplierPrice) : 'No especificado';
const sizesText = Array.isArray(product.sizes) && product.sizes.length > 0
? product.sizes.join(', ')
: '37, 38, 39, 40, 41, 42, 43';
const notificationCaption = (
`🚀 *¡NUEVO PRODUCTO PUBLICADO EN LA APP!* 🚀\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`👟 *${product.name}*\n` +
`🏷️ *Marca:* ${product.brand || 'Sneakers'} • *Categoría:* ${product.category || 'Zapatillas'}\n` +
`💵 *Precio de Venta en la App:* *${retailFormatted}*\n` +
`🏭 *Costo Proveedor:* ${supplierFormatted}\n` +
`📏 *Tallas Disponibles:* ${sizesText}\n` +
`📍 *Grupo Proveedor:* ${sourceGroup || 'Canal Mayorista'}\n\n` +
`🌐 *Ver en la Tienda Web:*\n` +
`👉 https://drip-zone.online\n\n` +
`📱 *Disponible de inmediato en la aplicación móvil DripZone para todos los clientes.*\n` +
`━━━━━━━━━━━━━━━━━━━━━━`
);
// Resolver URL absoluta de la imagen para garantizar que Evolution API la descargue con éxito
let resolvedMediaUrl = mediaUrl;
if (!resolvedMediaUrl && product?.images && product.images.length > 0) {
const rawImg = product.images[0];
if (rawImg.startsWith('http://') || rawImg.startsWith('https://')) {
resolvedMediaUrl = rawImg;
} else if (rawImg.startsWith('/')) {
resolvedMediaUrl = `https://drip-zone.online${rawImg}`;
} else {
resolvedMediaUrl = `https://drip-zone.online/${rawImg}`;
}
} else if (resolvedMediaUrl && !resolvedMediaUrl.startsWith('http')) {
resolvedMediaUrl = `https://drip-zone.online${resolvedMediaUrl.startsWith('/') ? '' : '/'}${resolvedMediaUrl}`;
}
const dispatchedTo: string[] = [];
for (const target of NOTIFICATION_TARGETS) {
try {
let sent = false;
// 1. Enviar prioritariamente con la foto del calzado en alta definición
if (resolvedMediaUrl && (resolvedMediaUrl.startsWith('http://') || resolvedMediaUrl.startsWith('https://'))) {
sent = await sendWhatsAppMedia(target, resolvedMediaUrl, notificationCaption);
}
// 2. Si no hay imagen o falló el envío multimedia, enviar como texto formateado
if (!sent) {
sent = await sendWhatsAppMessage(target, notificationCaption);
}
if (sent) {
dispatchedTo.push(target);
}
} catch (err: any) {
console.warn(`[notifyNewProductPublished] Error despachando a ${target}:`, err.message);
}
}
return {
success: dispatchedTo.length > 0,
dispatchedTo,
};
}
import { dataService } from './data-service';
/**
* Genera el mensaje de confirmación de pedido para el cliente y el administrador
* utilizando el sistema dinámico de plantillas configurables.
*/
export function buildOrderConfirmationMessage(order: any): string {
const itemsText = (order.items || [])
.map((item: any) => `👟 *${item.productName}*\n 📏 Talla: *${item.size}* | Cantidad: ${item.quantity} | Valor: ${formatCOP(item.price)}`)
.join('\n\n');
const settings = dataService.getSettings();
const paymentCondition = order.depositAmount > 0
? `⚡ *Separación / Anticipo:* ${formatCOP(order.depositAmount)}\n🤝 *Saldo a Pagar Contraentrega:* ${formatCOP(order.balanceDue)}`
: '🤝 *Pago 100% Contraentrega al recibir en tu casa.*';
return dataService.renderMessageTemplate('order_confirmation_customer', {
tienda_nombre: settings?.storeName || 'DripZone Luxury',
orden_id: order.id,
cliente_nombre: order.customerName || 'Cliente',
cliente_telefono: order.customerPhone || '',
cliente_direccion: order.address || '',
cliente_ciudad: order.cityName || 'Cali',
articulos_resumen: itemsText,
subtotal: formatCOP(order.subtotal || 0),
costo_envio: order.shippingCost === 0 ? 'GRATIS' : formatCOP(order.shippingCost),
total: formatCOP(order.totalAmount || 0),
condicion_pago_resumen: paymentCondition,
metodo_pago: order.paymentMethodName || 'Contraentrega',
saldo_contraentrega: formatCOP(order.balanceDue || 0),
anticipo: formatCOP(order.depositAmount || 0),
});
}
/**
* Notifica inmediatamente al administrador Carlos (+57 316 745 5877)
* y al canal de notificaciones cuando entra un nuevo pedido en la plataforma (web o app móvil),
* INCLUYENDO la fotografía del calzado en la notificación oficial.
*/
export async function notifyAdminNewOrder(order: any): Promise<boolean> {
const CARLOS_PHONE = '573167455877';
const NOTIFICATION_GROUP = '120363428012905103@g.us';
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))}*`)
.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 adminMessage = (
`🚨 *¡NUEVO PEDIDO REGISTRADO EN DRIPZONE!* 🛍️📦\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`🆔 *Orden #:* ${orderId}\n` +
`📅 *Fecha:* ${dateStr}\n\n` +
`👤 *DATOS DEL CLIENTE:*\n` +
`• *Nombre:* ${order.customerName || 'Cliente DripZone'}\n` +
`• *WhatsApp:* ${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` +
`• *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` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`⚡ *Gestionar en el Panel de Administración:*\n` +
`👉 https://drip-zone.online/admin/pedidos`
);
// Extraer y resolver la URL absoluta de la fotografía del calzado
const firstItem = order.items?.[0];
const rawImage = firstItem?.image || (Array.isArray(firstItem?.images) ? firstItem.images[0] : null);
let resolvedImageUrl: string | null = null;
if (rawImage && typeof rawImage === 'string') {
if (rawImage.startsWith('http://') || rawImage.startsWith('https://')) {
resolvedImageUrl = rawImage;
} else if (rawImage.startsWith('/')) {
resolvedImageUrl = `https://drip-zone.online${rawImage}`;
} else {
resolvedImageUrl = `https://drip-zone.online/${rawImage}`;
}
}
let success = false;
try {
// 1. Notificación principal al WhatsApp personal de Carlos CON LA FOTO DEL PRODUCTO
let sentCarlos = false;
if (resolvedImageUrl) {
sentCarlos = await sendWhatsAppMedia(CARLOS_PHONE, resolvedImageUrl, adminMessage);
}
if (!sentCarlos) {
sentCarlos = await sendWhatsAppMessage(CARLOS_PHONE, adminMessage);
}
if (sentCarlos) success = true;
// 2. Notificación al canal de alertas (grupo) CON LA FOTO DEL PRODUCTO
let sentGroup = false;
if (resolvedImageUrl) {
sentGroup = await sendWhatsAppMedia(NOTIFICATION_GROUP, resolvedImageUrl, adminMessage).catch(() => false);
}
if (!sentGroup) {
await sendWhatsAppMessage(NOTIFICATION_GROUP, adminMessage).catch(() => {});
}
} catch (err: any) {
console.error('Error enviando notificación de pedido a Carlos:', err.message);
}
return success;
}