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>
248 lines
10 KiB
JavaScript
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
|