michangarrito/apps/whatsapp-service/dist/whatsapp/whatsapp.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

248 lines
10 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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var WhatsAppService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WhatsAppService = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const axios_1 = __importDefault(require("axios"));
const credentials_provider_service_1 = require("../common/credentials-provider.service");
let WhatsAppService = WhatsAppService_1 = class WhatsAppService {
constructor(configService, credentialsProvider) {
this.configService = configService;
this.credentialsProvider = credentialsProvider;
this.logger = new common_1.Logger(WhatsAppService_1.name);
this.clientCache = new Map();
}
getClient(credentials) {
const cacheKey = credentials.isFromPlatform
? 'platform'
: `tenant:${credentials.tenantId}`;
if (!this.clientCache.has(cacheKey)) {
const client = axios_1.default.create({
baseURL: 'https://graph.facebook.com/v18.0',
headers: {
Authorization: `Bearer ${credentials.accessToken}`,
'Content-Type': 'application/json',
},
});
this.clientCache.set(cacheKey, client);
}
return this.clientCache.get(cacheKey);
}
async getPhoneNumberId(tenantId) {
const credentials = await this.credentialsProvider.getWhatsAppCredentials(tenantId);
return credentials.phoneNumberId;
}
invalidateClientCache(tenantId) {
const cacheKey = tenantId ? `tenant:${tenantId}` : 'platform';
this.clientCache.delete(cacheKey);
if (tenantId) {
this.credentialsProvider.invalidateCache(tenantId);
}
}
async sendTextMessage(to, text, tenantId) {
const message = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: this.formatPhoneNumber(to),
type: 'text',
text: { body: text },
};
return this.sendMessage(message, tenantId);
}
async sendInteractiveButtons(to, bodyText, buttons, headerText, footerText, tenantId) {
const message = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: this.formatPhoneNumber(to),
type: 'interactive',
interactive: {
type: 'button',
header: headerText ? { type: 'text', text: headerText } : undefined,
body: { text: bodyText },
footer: footerText ? { text: footerText } : undefined,
action: {
buttons: buttons.slice(0, 3).map((btn) => ({
type: 'reply',
reply: { id: btn.id, title: btn.title.substring(0, 20) },
})),
},
},
};
return this.sendMessage(message, tenantId);
}
async sendInteractiveList(to, bodyText, buttonText, sections, headerText, footerText, tenantId) {
const message = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: this.formatPhoneNumber(to),
type: 'interactive',
interactive: {
type: 'list',
header: headerText ? { type: 'text', text: headerText } : undefined,
body: { text: bodyText },
footer: footerText ? { text: footerText } : undefined,
action: {
button: buttonText.substring(0, 20),
sections: sections.map((section) => ({
title: section.title?.substring(0, 24),
rows: section.rows.slice(0, 10).map((row) => ({
id: row.id.substring(0, 200),
title: row.title.substring(0, 24),
description: row.description?.substring(0, 72),
})),
})),
},
},
};
return this.sendMessage(message, tenantId);
}
async sendTemplate(to, templateName, languageCode = 'es_MX', components, tenantId) {
const message = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: this.formatPhoneNumber(to),
type: 'template',
template: {
name: templateName,
language: { code: languageCode },
components,
},
};
return this.sendMessage(message, tenantId);
}
async sendImage(to, imageUrl, caption, tenantId) {
const message = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: this.formatPhoneNumber(to),
type: 'image',
image: {
link: imageUrl,
caption,
},
};
return this.sendMessage(message, tenantId);
}
async sendDocument(to, documentUrl, filename, caption, tenantId) {
const message = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: this.formatPhoneNumber(to),
type: 'document',
document: {
link: documentUrl,
filename,
caption,
},
};
return this.sendMessage(message, tenantId);
}
async downloadMedia(mediaId, tenantId) {
try {
const credentials = await this.credentialsProvider.getWhatsAppCredentials(tenantId);
const client = this.getClient(credentials);
const { data: mediaInfo } = await client.get(`/${mediaId}`);
const { data: mediaBuffer } = await axios_1.default.get(mediaInfo.url, {
headers: {
Authorization: `Bearer ${credentials.accessToken}`,
},
responseType: 'arraybuffer',
});
return Buffer.from(mediaBuffer);
}
catch (error) {
this.logger.error(`Error downloading media: ${error.message}`);
throw error;
}
}
async sendMessage(message, tenantId) {
try {
const credentials = await this.credentialsProvider.getWhatsAppCredentials(tenantId);
const client = this.getClient(credentials);
const { data } = await client.post(`/${credentials.phoneNumberId}/messages`, message);
const source = credentials.isFromPlatform ? 'platform' : `tenant:${tenantId}`;
this.logger.log(`Message sent to ${message.to} via ${source}: ${data.messages[0].id}`);
return data.messages[0].id;
}
catch (error) {
this.logger.error(`Error sending message: ${error.response?.data || error.message}`);
throw error;
}
}
formatPhoneNumber(phone) {
let cleaned = phone.replace(/\D/g, '');
if (cleaned.length === 10) {
cleaned = '52' + cleaned;
}
return cleaned;
}
async sendOrderConfirmation(to, orderNumber, items, total, tenantId) {
const itemsList = items
.map((item) => `${item.quantity}x ${item.name} - $${item.price.toFixed(2)}`)
.join('\n');
const message = `*Pedido Confirmado* 🎉
Número: *${orderNumber}*
${itemsList}
*Total: $${total.toFixed(2)}*
Te avisaremos cuando esté listo.`;
return this.sendTextMessage(to, message, tenantId);
}
async sendOrderStatusUpdate(to, orderNumber, status, tenantId) {
const statusMessages = {
preparing: `*Preparando tu pedido* 👨‍🍳\n\nPedido: ${orderNumber}\n\nEstamos trabajando en tu orden.`,
ready: `*¡Pedido listo!* ✅\n\nPedido: ${orderNumber}\n\nTu pedido está listo para recoger.`,
delivered: `*Pedido entregado* 📦\n\nPedido: ${orderNumber}\n\n¡Gracias por tu compra!`,
};
return this.sendTextMessage(to, statusMessages[status], tenantId);
}
async sendFiadoReminder(to, customerName, amount, dueDate, tenantId) {
let message = `Hola ${customerName} 👋
Te recordamos que tienes un saldo pendiente de *$${amount.toFixed(2)}*`;
if (dueDate) {
message += ` con vencimiento el ${dueDate}`;
}
message += `.\n\n¿Deseas abonar o consultar tu estado de cuenta?`;
return this.sendInteractiveButtons(to, message, [
{ id: 'pay_fiado', title: 'Quiero pagar' },
{ id: 'check_balance', title: 'Ver mi cuenta' },
], undefined, undefined, tenantId);
}
async sendLowStockAlert(to, products, tenantId) {
const productList = products
.map((p) => `${p.name}: ${p.stock} unidades`)
.join('\n');
const message = `*⚠️ Alerta de Stock Bajo*
Los siguientes productos necesitan reabastecimiento:
${productList}
¿Deseas hacer un pedido al proveedor?`;
return this.sendTextMessage(to, message, tenantId);
}
};
exports.WhatsAppService = WhatsAppService;
exports.WhatsAppService = WhatsAppService = WhatsAppService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [config_1.ConfigService,
credentials_provider_service_1.CredentialsProviderService])
], WhatsAppService);
//# sourceMappingURL=whatsapp.service.js.map