michangarrito/apps/whatsapp-service/dist/webhook/webhook.service.js
rckrdmrd 48dea7a5d0 feat: Initial commit - michangarrito
Marketplace móvil para negocios locales mexicanos.

Estructura inicial:
- apps/backend (NestJS API)
- apps/frontend (React Web)
- apps/mobile (Expo/React Native)
- apps/mcp-server (Claude MCP Server)
- apps/whatsapp-service (WhatsApp Business API)
- database/ (PostgreSQL DDL)
- docs/ (Documentación)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 04:41:02 -06:00

307 lines
15 KiB
JavaScript

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var WebhookService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebhookService = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const whatsapp_service_1 = require("../whatsapp/whatsapp.service");
const llm_service_1 = require("../llm/llm.service");
const credentials_provider_service_1 = require("../common/credentials-provider.service");
let WebhookService = WebhookService_1 = class WebhookService {
constructor(configService, whatsAppService, llmService, credentialsProvider) {
this.configService = configService;
this.whatsAppService = whatsAppService;
this.llmService = llmService;
this.credentialsProvider = credentialsProvider;
this.logger = new common_1.Logger(WebhookService_1.name);
this.conversations = new Map();
}
verifyWebhook(mode, token, challenge) {
const verifyToken = this.configService.get('WHATSAPP_VERIFY_TOKEN');
if (mode === 'subscribe' && token === verifyToken) {
this.logger.log('Webhook verified successfully');
return challenge;
}
this.logger.warn('Webhook verification failed');
return null;
}
async processIncomingMessage(message, contact, phoneNumberId) {
const phoneNumber = message.from;
const customerName = contact.profile.name;
const tenantId = await this.credentialsProvider.resolveTenantFromPhoneNumberId(phoneNumberId);
const source = tenantId ? `tenant:${tenantId}` : 'platform';
this.logger.log(`Processing message from ${customerName} (${phoneNumber}) via ${source}: ${message.type}`);
let context = this.conversations.get(phoneNumber);
if (!context) {
context = {
customerName,
phoneNumber,
lastActivity: new Date(),
cart: [],
tenantId,
businessName: tenantId ? undefined : 'MiChangarrito',
};
this.conversations.set(phoneNumber, context);
}
context.lastActivity = new Date();
context.tenantId = tenantId;
try {
switch (message.type) {
case 'text':
await this.handleTextMessage(phoneNumber, message.text?.body || '', context);
break;
case 'interactive':
await this.handleInteractiveMessage(phoneNumber, message, context);
break;
case 'image':
await this.handleImageMessage(phoneNumber, message, context);
break;
case 'audio':
await this.handleAudioMessage(phoneNumber, message, context);
break;
case 'location':
await this.handleLocationMessage(phoneNumber, message, context);
break;
default:
await this.whatsAppService.sendTextMessage(phoneNumber, 'Lo siento, no puedo procesar ese tipo de mensaje. Puedes escribirme texto o usar los botones.', context.tenantId);
}
}
catch (error) {
this.logger.error(`Error processing message: ${error.message}`);
await this.whatsAppService.sendTextMessage(phoneNumber, 'Disculpa, hubo un problema procesando tu mensaje. Por favor intenta de nuevo.', context.tenantId);
}
}
async handleTextMessage(phoneNumber, text, context) {
const lowerText = text.toLowerCase().trim();
if (this.isGreeting(lowerText)) {
await this.sendWelcomeMessage(phoneNumber, context.customerName, context);
return;
}
if (lowerText.includes('menu') || lowerText.includes('productos')) {
await this.sendMainMenu(phoneNumber, context);
return;
}
if (lowerText.includes('ayuda') || lowerText.includes('help')) {
await this.sendHelpMessage(phoneNumber, context);
return;
}
if (lowerText.includes('pedido') || lowerText.includes('orden')) {
await this.sendOrderStatus(phoneNumber, context);
return;
}
if (lowerText.includes('fiado') || lowerText.includes('cuenta') || lowerText.includes('deuda')) {
await this.sendFiadoInfo(phoneNumber, context);
return;
}
const response = await this.llmService.processMessage(text, context);
if (response.action) {
await this.executeAction(phoneNumber, response.action, response.data, context);
}
else {
await this.whatsAppService.sendTextMessage(phoneNumber, response.message, context.tenantId);
}
}
async handleInteractiveMessage(phoneNumber, message, context) {
const interactive = message.interactive;
if (interactive?.button_reply) {
const buttonId = interactive.button_reply.id;
await this.handleButtonResponse(phoneNumber, buttonId, context);
}
else if (interactive?.list_reply) {
const listId = interactive.list_reply.id;
await this.handleListResponse(phoneNumber, listId, context);
}
}
async handleImageMessage(phoneNumber, message, context) {
await this.whatsAppService.sendTextMessage(phoneNumber, 'Gracias por la imagen. Esta funcionalidad estara disponible pronto.', context.tenantId);
}
async handleAudioMessage(phoneNumber, message, context) {
await this.whatsAppService.sendTextMessage(phoneNumber, 'Gracias por el audio. La transcripcion estara disponible pronto.', context.tenantId);
}
async handleLocationMessage(phoneNumber, message, context) {
const location = message.location;
if (location) {
await this.whatsAppService.sendTextMessage(phoneNumber, `Ubicacion recibida. Coordenadas: ${location.latitude}, ${location.longitude}`, context.tenantId);
}
}
async handleButtonResponse(phoneNumber, buttonId, context) {
this.logger.log(`Button response: ${buttonId}`);
switch (buttonId) {
case 'menu_products':
await this.sendProductCategories(phoneNumber, context);
break;
case 'menu_orders':
await this.sendOrderStatus(phoneNumber, context);
break;
case 'menu_fiado':
await this.sendFiadoInfo(phoneNumber, context);
break;
case 'pay_fiado':
await this.sendPaymentOptions(phoneNumber, context);
break;
case 'check_balance':
await this.sendFiadoBalance(phoneNumber, context);
break;
case 'confirm_order':
await this.confirmOrder(phoneNumber, context);
break;
case 'cancel_order':
await this.cancelOrder(phoneNumber, context);
break;
default:
await this.whatsAppService.sendTextMessage(phoneNumber, 'Opcion no reconocida. Escribe "menu" para ver las opciones disponibles.', context.tenantId);
}
}
async handleListResponse(phoneNumber, listId, context) {
this.logger.log(`List response: ${listId}`);
if (listId.startsWith('cat_')) {
const categoryId = listId.replace('cat_', '');
await this.sendCategoryProducts(phoneNumber, categoryId, context);
}
else if (listId.startsWith('prod_')) {
const productId = listId.replace('prod_', '');
await this.addToCart(phoneNumber, productId, context);
}
}
async sendWelcomeMessage(phoneNumber, customerName, context) {
const businessName = context.businessName || 'MiChangarrito';
const message = `Hola ${customerName}! Bienvenido a ${businessName}.
Soy tu asistente virtual. Puedo ayudarte con:
- Ver productos disponibles
- Hacer pedidos
- Consultar tu cuenta de fiado
- Revisar el estado de tus pedidos
Como puedo ayudarte hoy?`;
await this.whatsAppService.sendInteractiveButtons(phoneNumber, message, [
{ id: 'menu_products', title: 'Ver productos' },
{ id: 'menu_orders', title: 'Mis pedidos' },
{ id: 'menu_fiado', title: 'Mi fiado' },
], undefined, undefined, context.tenantId);
}
async sendMainMenu(phoneNumber, context) {
await this.whatsAppService.sendInteractiveButtons(phoneNumber, 'Que te gustaria hacer?', [
{ id: 'menu_products', title: 'Ver productos' },
{ id: 'menu_orders', title: 'Mis pedidos' },
{ id: 'menu_fiado', title: 'Mi fiado' },
], 'Menu Principal', undefined, context.tenantId);
}
async sendHelpMessage(phoneNumber, context) {
const helpText = `*Comandos disponibles:*
- *menu* - Ver opciones principales
- *productos* - Ver catalogo
- *pedido* - Estado de tu pedido
- *fiado* - Tu cuenta de fiado
- *ayuda* - Este mensaje
Tambien puedes escribirme de forma natural y tratare de entenderte!`;
await this.whatsAppService.sendTextMessage(phoneNumber, helpText, context.tenantId);
}
async sendProductCategories(phoneNumber, context) {
const categories = [
{ id: 'cat_bebidas', title: 'Bebidas', description: 'Refrescos, aguas, jugos' },
{ id: 'cat_botanas', title: 'Botanas', description: 'Papas, cacahuates, dulces' },
{ id: 'cat_abarrotes', title: 'Abarrotes', description: 'Productos basicos' },
{ id: 'cat_lacteos', title: 'Lacteos', description: 'Leche, queso, yogurt' },
];
await this.whatsAppService.sendInteractiveList(phoneNumber, 'Selecciona una categoria para ver los productos disponibles:', 'Ver categorias', [{ title: 'Categorias', rows: categories }], 'Catalogo', undefined, context.tenantId);
}
async sendCategoryProducts(phoneNumber, categoryId, context) {
const products = [
{ id: 'prod_1', title: 'Coca-Cola 600ml', description: '$18.00' },
{ id: 'prod_2', title: 'Pepsi 600ml', description: '$17.00' },
{ id: 'prod_3', title: 'Agua natural 1L', description: '$12.00' },
];
await this.whatsAppService.sendInteractiveList(phoneNumber, 'Selecciona un producto para agregarlo a tu carrito:', 'Ver productos', [{ title: 'Productos', rows: products }], 'Productos disponibles', undefined, context.tenantId);
}
async addToCart(phoneNumber, productId, context) {
if (!context.cart) {
context.cart = [];
}
context.cart.push({
productId,
name: 'Producto de prueba',
quantity: 1,
price: 18.00,
});
const cartTotal = context.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
await this.whatsAppService.sendInteractiveButtons(phoneNumber, `Producto agregado al carrito.\n\nTotal actual: $${cartTotal.toFixed(2)}\nArticulos: ${context.cart.length}`, [
{ id: 'menu_products', title: 'Seguir comprando' },
{ id: 'confirm_order', title: 'Finalizar pedido' },
], undefined, undefined, context.tenantId);
}
async sendOrderStatus(phoneNumber, context) {
await this.whatsAppService.sendTextMessage(phoneNumber, 'No tienes pedidos activos en este momento.\n\nEscribe "menu" para hacer un nuevo pedido.', context.tenantId);
}
async sendFiadoInfo(phoneNumber, context) {
await this.whatsAppService.sendInteractiveButtons(phoneNumber, 'Tu cuenta de fiado:\n\nSaldo pendiente: *$0.00*\nLimite de credito: *$500.00*\nCredito disponible: *$500.00*', [
{ id: 'pay_fiado', title: 'Hacer pago' },
{ id: 'menu_products', title: 'Comprar a fiado' },
], 'Mi Fiado', undefined, context.tenantId);
}
async sendFiadoBalance(phoneNumber, context) {
await this.whatsAppService.sendTextMessage(phoneNumber, '*Detalle de tu cuenta:*\n\nNo hay movimientos pendientes.', context.tenantId);
}
async sendPaymentOptions(phoneNumber, context) {
await this.whatsAppService.sendTextMessage(phoneNumber, '*Opciones de pago:*\n\n1. Efectivo en tienda\n2. Transferencia bancaria\n\nPara transferencias:\nCLABE: XXXX XXXX XXXX XXXX\nBanco: BBVA\n\nEnvia tu comprobante por este medio.', context.tenantId);
}
async confirmOrder(phoneNumber, context) {
if (!context.cart || context.cart.length === 0) {
await this.whatsAppService.sendTextMessage(phoneNumber, 'Tu carrito esta vacio. Escribe "productos" para ver el catalogo.', context.tenantId);
return;
}
const total = context.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
const orderNumber = `MCH-${Date.now().toString(36).toUpperCase()}`;
await this.whatsAppService.sendOrderConfirmation(phoneNumber, orderNumber, context.cart, total, context.tenantId);
context.cart = [];
}
async cancelOrder(phoneNumber, context) {
context.cart = [];
await this.whatsAppService.sendTextMessage(phoneNumber, 'Pedido cancelado. Tu carrito ha sido vaciado.\n\nEscribe "menu" cuando quieras hacer un nuevo pedido.', context.tenantId);
}
async executeAction(phoneNumber, action, data, context) {
switch (action) {
case 'show_menu':
await this.sendMainMenu(phoneNumber, context);
break;
case 'show_products':
await this.sendProductCategories(phoneNumber, context);
break;
case 'show_fiado':
await this.sendFiadoInfo(phoneNumber, context);
break;
default:
this.logger.warn(`Unknown action: ${action}`);
}
}
processStatusUpdate(status) {
this.logger.log(`Message ${status.id} status: ${status.status}`);
if (status.status === 'failed' && status.errors) {
this.logger.error(`Message failed: ${JSON.stringify(status.errors)}`);
}
}
isGreeting(text) {
const greetings = ['hola', 'hi', 'hello', 'buenos dias', 'buenas tardes', 'buenas noches', 'que tal', 'hey'];
return greetings.some((g) => text.includes(g));
}
};
exports.WebhookService = WebhookService;
exports.WebhookService = WebhookService = WebhookService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [config_1.ConfigService,
whatsapp_service_1.WhatsAppService,
llm_service_1.LlmService,
credentials_provider_service_1.CredentialsProviderService])
], WebhookService);
//# sourceMappingURL=webhook.service.js.map