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>
307 lines
15 KiB
JavaScript
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
|