feat(whatsapp): publicacion incondicional, notificaciones con fotos/costos y catalogo whatsapp sincronizado

This commit is contained in:
AI Bot 2026-09-07 15:55:58 -05:00
parent a2524211f2
commit b65a4b79bd
5 changed files with 158 additions and 67 deletions

View File

@ -91,7 +91,8 @@ def deploy():
docker compose -f docker-compose.prod.yml down --remove-orphans || true
DOCKER_BUILDKIT=1 docker compose -f docker-compose.prod.yml up -d --build
sleep 3
docker exec dripzone-production chown -R nextjs:nodejs /app/data || true
docker exec -u 0 dripzone-production chown -R nextjs:nodejs /app/data /app/public || true
docker exec -u 0 dripzone-production chmod -R 777 /app/data /app/public || true
docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/public/DripZone.apk || true
docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/DripZone.apk || true
docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/public/dripzone.apk || true
@ -103,7 +104,6 @@ def deploy():
print("[4/4] Ejecutando compilacion y levantando contenedor en Produccion...")
stdin, stdout, stderr = client.exec_command(commands)
while not stdout.channel.exit_status_ready():
line = stdout.readline()
if line:
@ -133,14 +133,16 @@ def send_apk_to_whatsapp():
"Content-Type": "application/json"
}
caption = (
"👑 *¡ACCESO DE ADMINISTRADOR RESTAURADO & APK COMPILADO!* 🚀👟\n"
"🚀 *¡PUBLICACIÓN AUTOMÁTICA EN VIVO & CATÁLOGO WHATSAPP SINCRONIZADO!* 👟📲\n"
"━━━━━━━━━━━━━━━━━━━━━━\n"
"✨ *Ajustes implementados y desplegados en Producción:*\n\n"
"1️⃣ 🛡️ *Botón de Admin Permanente en la Web*: Restaurado de forma visible y accesible en la barra superior de navegación de https://drip-zone.online. Ahora está siempre disponible para ingresar y destacado con gradiente morado/fucsia cuando inicias sesión.\n"
"2️⃣ 👑 *Rol de Super Administrador para Carlos*: Tu celular personal *3167455877* (+57 316 745 5877) quedó configurado como Administrador Maestro incondicional en el servicio de datos (`dataService`), sesión web (`AuthContext`), API de usuarios y panel de control (`/admin`), con badge '👑 Super Administrador'.\n"
"3️⃣ 🚀 *Despliegue en Servidor VPS*: Contenedor `dripzone-production` reconstruido e iniciado con éxito en el servidor de producción.\n"
"4️⃣ 📲 *APK Android Actualizado*: Compilado y disponible para descarga directa.\n\n"
"👉 *Panel de Administración:* https://drip-zone.online/admin\n"
"✨ *Sistema Autónomo de Captura y Notificación 100% Activo:*\n\n"
"1️⃣ 🌐 *Webhook Evolution API Reconfigurado y Conectado*: El webhook de la instancia `AutomataAlmeira` quedó apuntando directamente a `https://drip-zone.online/api/webhook/whatsapp` procesando eventos `MESSAGES_UPSERT`, `MESSAGES_UPDATE` y `SEND_MESSAGE` en tiempo real.\n\n"
"2️⃣ ⚡ *Publicación Incondicional de Mensajes*: Se eliminaron filtros restrictivos. Todo mensaje con foto de calzado entrante en grupos mayoristas se procesa, se parsea con IA Gemini y se publica de inmediato en el catálogo de la tienda web y app móvil.\n\n"
"3️⃣ 📢 *Notificación Instantánea con Foto y Costos a tu WhatsApp*: Cada vez que un producto se publica, el sistema te envía automáticamente un mensaje con la *fotografía en alta definición*, el *valor de venta al público*, el *costo de proveedor*, la *ganancia neta estimada*, las *tallas disponibles* y el enlace directo a la web y panel admin.\n\n"
"4️⃣ 🛍️ *Sincronización con Catálogo de WhatsApp (Meta Commerce)*: Cada producto nuevo o modificado queda inmediatamente disponible en el feed XML de Meta Commerce (`/api/catalog/feed.xml`) y sincronizado con el Catálogo de WhatsApp Business.\n\n"
"5️⃣ 📲 *APK Android Actualizado*: Compilado y listo para usar en tu celular.\n\n"
"👉 *Tienda Web:* https://drip-zone.online\n"
"👉 *Panel de Productos:* https://drip-zone.online/admin/productos\n"
"📥 *Descarga directa del APK:* https://drip-zone.online/DripZone.apk"
)
payload = {

View File

@ -46,10 +46,11 @@ ENV HOSTNAME="0.0.0.0"
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data
RUN mkdir -p /app/data /app/public/cache/whatsapp-media && chown -R nextjs:nodejs /app/data
# Copy standalone output & public static files
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
RUN mkdir -p /app/public/cache/whatsapp-media && chown -R nextjs:nodejs /app/public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

View File

@ -218,7 +218,7 @@ export async function POST(request: Request) {
}
// =========================================================================
// CASO B: GRUPOS MAYORISTAS DE PROVEEDORES (Monitoreo y Publicación Automática)
// CASO B: GRUPOS MAYORISTAS DE PROVEEDORES (Publicación Automática Incondicional)
// =========================================================================
const groupSubject =
data.groupInfo?.subject ||
@ -226,38 +226,49 @@ export async function POST(request: Request) {
data.pushName ||
'Grupo Mayorista';
// 1. VALIDAR SI EL GRUPO ESTÁ MONITOREADO
const [appwriteGroups, localGroups] = await Promise.all([
getMonitoredGroupsFromAppwrite().catch(() => []),
Promise.resolve(dataService.getMonitoredGroups()),
]);
const allGroups = [...localGroups, ...appwriteGroups];
const registeredGroup = allGroups.find(
(g) => g.jid === remoteJid || g.name.toLowerCase() === groupSubject.toLowerCase()
);
// Detección inteligente de grupos de proveedores de calzado y streetwear
const isWholesaleSupplier = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas|bodega/i.test(
groupSubject
);
const isNonProductNews = /gob cauca|prensa|comunicaciones|inelpac|meliextra|sedes asociadas|notificaciones almeira/i.test(
// 1. FILTRAR EXCLUSIVAMENTE GRUPOS DE PRENSA Y GOBIERNO (Reenviar a portal de noticias)
const isNonProductNews = /gob cauca|gobernacion|alcaldia|prensa|comunicaciones almeira|sedes asociadas|notificaciones almeira|inelpac|meliextra/i.test(
groupSubject
);
// Está monitoreado si está explícitamente activado en la base de datos o si coincide con proveedores mayoristas reconocidos
const isMonitored = (registeredGroup && registeredGroup.isMonitored) || (isWholesaleSupplier && !isNonProductNews);
if (isNonProductNews) {
// Reenviar asíncronamente al webhook del portal de noticias Almeira
fetch('https://news.almeira.com.co/api/webhooks/whatsapp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(10000),
}).catch(() => {});
if (!isMonitored) {
console.log(`[DripZone Webhook] Chat descartado (no monitoreado para productos): "${groupSubject}" (${remoteJid})`);
console.log(`[DripZone Webhook] Chat de prensa redirigido a news: "${groupSubject}" (${remoteJid})`);
return NextResponse.json({
success: true,
ignored: 'Grupo no monitoreado para publicación automática de calzado',
ignored: 'Grupo de noticias institucionales reenviado a portal de prensa',
chat: groupSubject,
remoteJid,
});
}
// Auto-registrar grupo como monitoreado y activo si no existía previamente
try {
const currentGroups = dataService.getMonitoredGroups();
const existingGrp = currentGroups.find((g) => g.jid === remoteJid);
if (!existingGrp) {
dataService.saveMonitoredGroups([
...currentGroups,
{
id: `grp-${remoteJid.replace(/\D/g, '').slice(-8)}`,
jid: remoteJid,
name: groupSubject,
isMonitored: true,
autoPublish: true,
notes: 'Auto-registrado desde Webhook activo de WhatsApp',
supplierName: groupSubject,
},
]);
}
} catch (_) {}
// 2. VERIFICACIÓN DE DUPLICADOS
if (isDuplicateMessage(whatsappMessageId, messageContent)) {
return NextResponse.json({ success: true, ignored: 'Mensaje duplicado en ráfaga ignorado' });
@ -279,6 +290,23 @@ export async function POST(request: Request) {
imageBase64 = thumb.startsWith('data:') ? thumb : `data:image/jpeg;base64,${thumb}`;
}
// Guardar imagen en disco local inmediatamente para disponibilidad instantánea
if (imageBase64) {
try {
const fsMod = require('fs');
const pathMod = require('path');
const cacheDir = pathMod.join(process.cwd(), 'public', 'cache', 'whatsapp-media');
if (!fsMod.existsSync(cacheDir)) {
fsMod.mkdirSync(cacheDir, { recursive: true });
}
const cleanMsgId = whatsappMessageId.replace(/[^a-zA-Z0-9_-]/g, '');
const b64Payload = imageBase64.replace(/^data:image\/[a-z]+;base64,/, '');
fsMod.writeFileSync(pathMod.join(cacheDir, `${cleanMsgId}.jpg`), Buffer.from(b64Payload, 'base64'));
} catch (cacheErr) {
console.warn('[DripZone Webhook] Advertencia guardando foto en cache local:', cacheErr);
}
}
// Si no hay texto ni foto, descartar
if (!messageContent && !imageBase64) {
return NextResponse.json({ success: true, ignored: 'Mensaje sin texto ni imagen' });
@ -398,7 +426,8 @@ export async function POST(request: Request) {
// 7. ENVÍO DE NOTIFICACIÓN INMEDIATA DEL NUEVO PRODUCTO PUBLICADO
let notificationResult: any = null;
try {
notificationResult = await notifyNewProductPublished(addedProduct, groupSubject, fullMediaUrl);
const mediaToSend = imageBase64 || fullMediaUrl;
notificationResult = await notifyNewProductPublished(addedProduct, groupSubject, mediaToSend);
console.log('📢 [DripZone Webhook] Notificación enviada a WhatsApp:', notificationResult);
} catch (notifErr: any) {
console.error('[DripZone Webhook] Error al enviar notificación de WhatsApp:', notifErr.message);

View File

@ -14,6 +14,7 @@ import {
} from '@/lib/server-appwrite';
import { SupplierPost, MonitoredWhatsAppGroup } from '@/types';
import { registerProductInWhatsAppCatalog } from '@/lib/whatsapp-catalog';
import { notifyNewProductPublished } from '@/lib/evolution-api';
const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co';
const EVOLUTION_API_KEY = process.env.EVOLUTION_FEED_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
@ -432,6 +433,11 @@ export async function POST(request: Request) {
const added = dataService.addProduct(newProduct as any);
await saveProductToAppwrite(added);
await registerProductInWhatsAppCatalog(added).catch(() => {});
notifyNewProductPublished(
added,
post.channelName || post.supplierName || 'WhatsApp Mayorista',
added.images?.[0]
).catch(() => {});
await updateSupplierPostStatusInAppwrite(post.id, 'published');
dataService.updateSupplierPostStatus(post.id, 'published');
@ -504,6 +510,11 @@ export async function POST(request: Request) {
const added = dataService.addProduct(newProd as any);
await saveProductToAppwrite(added);
await registerProductInWhatsAppCatalog(added).catch(() => {});
notifyNewProductPublished(
added,
p.channelName || p.supplierName || 'WhatsApp Mayorista',
added.images?.[0]
).catch(() => {});
await updateSupplierPostStatusInAppwrite(p.id, 'published');
dataService.updateSupplierPostStatus(p.id, 'published');
createdProducts.push(added);

View File

@ -3,11 +3,11 @@ 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 principal conectada de WhatsApp (AutomataAlmeira / Línea activa de Carlos)
const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'AutomataAlmeira';
// Instancia personal de Carlos (Para scraping de publicaciones y grupos de WhatsApp)
// Instancia 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';
@ -22,7 +22,6 @@ function resolveRecipient(target: string): string {
/**
* 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,
@ -55,13 +54,23 @@ export async function sendWhatsAppMessage(
return response.status === 200 || response.status === 201;
} catch (error: any) {
console.error(`Error enviando mensaje de WhatsApp (${instance}):`, error?.response?.data || error?.message);
// Fallback a AutomataAlmeira si se intentó con otra instancia
if (instance !== 'AutomataAlmeira') {
try {
const fbRes = await axios.post(
`${EVOLUTION_API_URL}/message/sendText/AutomataAlmeira`,
{ number: recipient, text: message, delay: 1200 },
{ headers: { apikey: 'EE49029EEB85-4568-8F81-C9FDFC6E2943', 'Content-Type': 'application/json' }, timeout: 20000 }
);
return fbRes.status === 200 || fbRes.status === 201;
} catch (_) {}
}
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,
@ -76,6 +85,10 @@ export async function sendWhatsAppMedia(
const instance = customInstance || EVOLUTION_INSTANCE_NAME;
const apiKey = customApiKey || EVOLUTION_API_KEY;
const mediaPayload = (!imageUrl.startsWith('http') && !imageUrl.startsWith('data:') && imageUrl.length > 200)
? `data:image/jpeg;base64,${imageUrl}`
: imageUrl;
try {
const response = await axios.post(
`${EVOLUTION_API_URL}/message/sendMedia/${instance}`,
@ -84,7 +97,7 @@ export async function sendWhatsAppMedia(
mediatype: 'image',
mimetype: 'image/jpeg',
caption: caption,
media: imageUrl,
media: mediaPayload,
delay: 1500,
},
{
@ -92,12 +105,22 @@ export async function sendWhatsAppMedia(
apikey: apiKey,
'Content-Type': 'application/json',
},
timeout: 25000,
timeout: 30000,
}
);
return response.status === 200 || response.status === 201;
} catch (error: any) {
console.error(`Error enviando media por WhatsApp (${instance}):`, error?.response?.data || error?.message);
if (instance !== 'AutomataAlmeira') {
try {
const fbRes = await axios.post(
`${EVOLUTION_API_URL}/message/sendMedia/AutomataAlmeira`,
{ number: recipient, mediatype: 'image', mimetype: 'image/jpeg', caption, media: mediaPayload, delay: 1500 },
{ headers: { apikey: 'EE49029EEB85-4568-8F81-C9FDFC6E2943', 'Content-Type': 'application/json' }, timeout: 30000 }
);
return fbRes.status === 200 || fbRes.status === 201;
} catch (_) {}
}
return false;
}
}
@ -117,40 +140,57 @@ export async function notifyNewProductPublished(
'120363428012905103@g.us', // Notificaciones Almeira
];
const retailFormatted = formatCOP(product.retailPrice || 125000);
const supplierFormatted = product.supplierPrice ? formatCOP(product.supplierPrice) : 'No especificado';
const retailPrice = product.retailPrice || 125000;
const supplierPrice = product.supplierPrice || 0;
const profit = Math.max(0, retailPrice - supplierPrice);
const profitMarginPercent = supplierPrice > 0 ? Math.round((profit / supplierPrice) * 100) : 35;
const retailFormatted = formatCOP(retailPrice);
const supplierFormatted = supplierPrice > 0 ? formatCOP(supplierPrice) : 'No especificado';
const profitFormatted = formatCOP(profit);
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` +
`👟 *¡NUEVO PRODUCTO PUBLICADO AUTOMÁTICAMENTE!* 🚀\n` +
`━━━━━━━━━━━━━━━━━━━━━━\n` +
`👟 *${product.name}*\n` +
`✨ *Modelo:* *${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` +
`💵 *Valor de Venta (Público):* *${retailFormatted} COP*\n` +
`🏭 *Costo Proveedor:* *${supplierFormatted} COP*\n` +
`💰 *Ganancia Neta Estimada:* *+${profitFormatted} COP* (~${profitMarginPercent}% margen)\n` +
`📏 *Tallas Disponibles:* *${sizesText}*\n` +
`🎨 *Estilo / Color:* ${product.colorName || 'Estilo Principal'}\n` +
`📦 *Grupo Proveedor Origen:* ${sourceGroup || 'Canal Mayorista'}\n\n` +
`🛍️ *Canales de Publicación Activos:*\n` +
`• ✅ App Móvil Android (Catálogo Activo)\n` +
`• ✅ Tienda Web: https://drip-zone.online\n` +
`• ✅ Catálogo de WhatsApp Business (Meta Commerce)\n\n` +
`🛒 *Ver en la Web:* 👉 https://drip-zone.online\n` +
`⚙️ *Panel de Control:* 👉 https://drip-zone.online/admin/productos\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) {
// Resolver medio (URL pública o Base64) para garantizar entrega
let resolvedMedia = mediaUrl;
const isBase64 = Boolean(
resolvedMedia &&
(resolvedMedia.startsWith('data:') || (!resolvedMedia.startsWith('http') && !resolvedMedia.startsWith('/') && resolvedMedia.length > 200))
);
if (!resolvedMedia && product?.images && product.images.length > 0) {
const rawImg = product.images[0];
if (rawImg.startsWith('http://') || rawImg.startsWith('https://')) {
resolvedMediaUrl = rawImg;
if (rawImg.startsWith('http://') || rawImg.startsWith('https://') || rawImg.startsWith('data:')) {
resolvedMedia = rawImg;
} else if (rawImg.startsWith('/')) {
resolvedMediaUrl = `https://drip-zone.online${rawImg}`;
resolvedMedia = `https://drip-zone.online${rawImg}`;
} else {
resolvedMediaUrl = `https://drip-zone.online/${rawImg}`;
resolvedMedia = `https://drip-zone.online/${rawImg}`;
}
} else if (resolvedMediaUrl && !resolvedMediaUrl.startsWith('http')) {
resolvedMediaUrl = `https://drip-zone.online${resolvedMediaUrl.startsWith('/') ? '' : '/'}${resolvedMediaUrl}`;
} else if (resolvedMedia && !resolvedMedia.startsWith('http') && !isBase64) {
resolvedMedia = `https://drip-zone.online${resolvedMedia.startsWith('/') ? '' : '/'}${resolvedMedia}`;
}
const dispatchedTo: string[] = [];
@ -159,14 +199,22 @@ export async function notifyNewProductPublished(
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);
// 1. Enviar con la foto del calzado en alta definición (sea URL o Base64)
if (resolvedMedia && (resolvedMedia.startsWith('http://') || resolvedMedia.startsWith('https://') || isBase64)) {
sent = await sendWhatsAppMedia(target, resolvedMedia, notificationCaption, 'AutomataAlmeira', 'EE49029EEB85-4568-8F81-C9FDFC6E2943');
}
// 2. Si no hay imagen o falló el envío multimedia, enviar como texto formateado
// 2. Si falló con base64 pero tenemos product.images, intentar con la URL de streaming
if (!sent && isBase64 && product?.images?.[0]) {
const fallbackUrl = product.images[0].startsWith('http')
? product.images[0]
: `https://drip-zone.online${product.images[0].startsWith('/') ? '' : '/'}${product.images[0]}`;
sent = await sendWhatsAppMedia(target, fallbackUrl, notificationCaption, 'AutomataAlmeira', 'EE49029EEB85-4568-8F81-C9FDFC6E2943').catch(() => false);
}
// 3. Si no hay imagen o falló el envío multimedia, enviar como texto formateado
if (!sent) {
sent = await sendWhatsAppMessage(target, notificationCaption);
sent = await sendWhatsAppMessage(target, notificationCaption, 'AutomataAlmeira', 'EE49029EEB85-4568-8F81-C9FDFC6E2943');
}
if (sent) {