feat: agregar soporte para el envío de plantillas de WhatsApp a través de Meta, incluyendo nueva funcionalidad y adaptadores

This commit is contained in:
2026-09-19 10:50:56 -03:00
parent 728d0a711c
commit 40bc65396f
17 changed files with 177 additions and 32 deletions
@@ -190,3 +190,37 @@ export async function dispatchSystem(params: {
return { success: false, error: msg }; return { success: false, error: msg };
} }
} }
export async function dispatchMetaWap(params: {
phoneNumber: string;
templateName: string;
languageCode: string;
templateVariables?: string[];
}): Promise<DispatchResult> {
try {
if (!params.phoneNumber.trim() || !params.templateName.trim() || !params.languageCode.trim()) {
return { success: false, error: "Meta WAP dispatch requires phoneNumber, templateName, and languageCode" };
}
const axios = (await import("axios")).default;
const response = await axios.post(
buildApiUrl("/notifications/send-whatsapp-template"),
{
phoneNumber: params.phoneNumber,
templateName: params.templateName,
languageCode: params.languageCode,
templateVariables: params.templateVariables,
},
{
headers: { "Content-Type": "application/json" },
}
);
logDone(`Meta WAP dispatched for phone ${params.phoneNumber}`);
return { success: true, providerResponse: JSON.stringify(response.data) };
} catch (error) {
const msg = formatDispatchError(error);
logError(`Meta WAP dispatch failed for ${params.phoneNumber}:`, msg);
return { success: false, error: msg };
}
}
@@ -1,7 +1,7 @@
import { logDone, logError, logIntent } from "../../config/logger.js"; import { logDone, logError, logIntent } from "../../config/logger.js";
import { NotificationJobModel, INotificationJob } from "./NotificationJob.Adapter.Mongoose.js"; import { NotificationJobModel, INotificationJob } from "./NotificationJob.Adapter.Mongoose.js";
import { WhatsAppThrottleClass } from "./WhatsAppThrottle.js"; import { WhatsAppThrottleClass } from "./WhatsAppThrottle.js";
import { dispatchWhatsApp, dispatchEmail, dispatchSystem } from "./ChannelDispatchers.js"; import { dispatchWhatsApp, dispatchEmail, dispatchSystem, dispatchMetaWap } from "./ChannelDispatchers.js";
import { resolveNotificationContent } from "./NotificationContentResolver.js"; import { resolveNotificationContent } from "./NotificationContentResolver.js";
const POLL_INTERVAL_MS = 5000; const POLL_INTERVAL_MS = 5000;
@@ -217,6 +217,14 @@ export class JobProcessor {
code: String(job.appointmentId), code: String(job.appointmentId),
}); });
break; break;
case "meta_wap":
result = await dispatchMetaWap({
phoneNumber: content.phoneNumber || "",
templateName: (job.payload as any)?.templateName || "",
languageCode: (job.payload as any)?.languageCode || "en_US",
templateVariables: (job.payload as any)?.templateVariables,
});
break;
default: default:
result = { success: false, error: `Unknown channel: ${job.channel}` }; result = { success: false, error: `Unknown channel: ${job.channel}` };
} }
@@ -1,6 +1,6 @@
import mongoose, { Document, Model, Schema, model } from "mongoose"; import mongoose, { Document, Model, Schema, model } from "mongoose";
export type NotificationChannel = "whatsapp" | "email" | "system"; export type NotificationChannel = "whatsapp" | "meta_wap" | "email" | "system";
export type NotificationJobStatus = "pending" | "processing" | "sent" | "failed" | "cancelled"; export type NotificationJobStatus = "pending" | "processing" | "sent" | "failed" | "cancelled";
export type NotificationJobType = "creation" | "reminder" | "update" | "cancellation"; export type NotificationJobType = "creation" | "reminder" | "update" | "cancellation";
@@ -69,7 +69,7 @@ export class NotificationJobModel {
channel: { channel: {
type: String, type: String,
required: true, required: true,
enum: ["whatsapp", "email", "system"], enum: ["whatsapp", "meta_wap", "email", "system"],
}, },
type: { type: {
type: String, type: String,
@@ -6,7 +6,7 @@ export enum NotificationJobStatus {
CANCELLED = "cancelled", CANCELLED = "cancelled",
} }
export type NotificationChannel = "whatsapp" | "email" | "system"; export type NotificationChannel = "whatsapp" | "meta_wap" | "email" | "system";
export type NotificationJobType = export type NotificationJobType =
| "creation" | "creation"
@@ -32,13 +32,13 @@ export class ClientCompanyNotificationOverrideAdapterMongoose
type: [String], type: [String],
required: true, required: true,
default: ["whatsapp", "email", "system"], default: ["whatsapp", "email", "system"],
enum: ["whatsapp", "email", "system"], enum: ["whatsapp", "meta_wap", "email", "system"],
}, },
mutedChannels: { mutedChannels: {
type: [String], type: [String],
required: true, required: true,
default: [], default: [],
enum: ["whatsapp", "email", "system"], enum: ["whatsapp", "meta_wap", "email", "system"],
}, },
}, },
{ {
@@ -28,13 +28,13 @@ export class ClientNotificationPreferencesAdapterMongoose
type: [String], type: [String],
required: true, required: true,
default: ["whatsapp", "email", "system"], default: ["whatsapp", "email", "system"],
enum: ["whatsapp", "email", "system"], enum: ["whatsapp", "meta_wap", "email", "system"],
}, },
mutedChannels: { mutedChannels: {
type: [String], type: [String],
required: true, required: true,
default: [], default: [],
enum: ["whatsapp", "email", "system"], enum: ["whatsapp", "meta_wap", "email", "system"],
}, },
}, },
{ {
@@ -28,13 +28,13 @@ export class CompanyNotificationPolicyAdapterMongoose
type: [String], type: [String],
required: true, required: true,
default: ["whatsapp", "email", "system"], default: ["whatsapp", "email", "system"],
enum: ["whatsapp", "email", "system"], enum: ["whatsapp", "meta_wap", "email", "system"],
}, },
mutedChannels: { mutedChannels: {
type: [String], type: [String],
required: false, required: false,
default: [], default: [],
enum: ["whatsapp", "email", "system"], enum: ["whatsapp", "meta_wap", "email", "system"],
}, },
timezone: { timezone: {
type: String, type: String,
@@ -248,6 +248,10 @@ export class NotificationJobService {
if (payload?.phoneNumber) { if (payload?.phoneNumber) {
channels.push("whatsapp"); channels.push("whatsapp");
// meta_wap requires templateName in payload to be useful
if (payload?.templateName) {
channels.push("meta_wap");
}
} }
if (payload?.userId) { if (payload?.userId) {
@@ -14,6 +14,7 @@ const SYSTEM_DEFAULT_CHANNELS: NotificationChannel[] = ["whatsapp", "email", "sy
const PLAN_CHANNEL_MAP: Record<string, PlanFeatures> = { const PLAN_CHANNEL_MAP: Record<string, PlanFeatures> = {
whatsapp: PlanFeatures.WAP_NOTIFICATIONS, whatsapp: PlanFeatures.WAP_NOTIFICATIONS,
meta_wap: PlanFeatures.WAP_NOTIFICATIONS,
email: PlanFeatures.MAIL_NOTIFICATIONS, email: PlanFeatures.MAIL_NOTIFICATIONS,
}; };
@@ -0,0 +1,48 @@
import axios from "axios";
import { INotificationsAdapter, NotificationDataMeta } from "./Notifications.Interface";
export class NotificationsMetaAdapter implements INotificationsAdapter<NotificationDataMeta> {
// ── Datos hardcodeados para pruebas (etapa 1) ──
private readonly phoneNumberId = "1379557278565544"; // ej: "1379557278565544"
private readonly authToken =
"EAAeZCf8kXotYBSlhC68tvuv6fPhFgfbqwdoF8Ab6r50cAQXJLOPl9qRCYtKYgz4r8ZBYzPIOVg3zZBU8JRcTmgoWU9DAphsWB3xQHQuZCMYZAMs3YHukOK567GUQeZCxOw8oZBnavnaqc0KukT0GzweRB2VC4CD5a6Vj5EsiJnZCjYuZCrffoha5WCkRRyUryook9syNTQ2cexsObVyhMVpRRc7X74BxbCWuzLZAvbZA6ZAb6zIZCHadhUWKX30dpYd1elqZAWuygnVC8pZBQyFG0YET498DAfs"; // tu token de Meta
private readonly apiVersion = "v25.0";
get baseUrl(): string {
return `https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`;
}
async send(data: NotificationDataMeta): Promise<void> {
const templateBody: Record<string, unknown> = {
messaging_product: "whatsapp",
to: data.phoneNumber,
type: "template",
template: {
name: data.templateName,
language: { code: data.languageCode },
},
};
// Si hay variables, convertirlas al formato components que exige Meta
if (data.templateVariables && data.templateVariables.length > 0) {
(templateBody.template as Record<string, unknown>).components = [
{
type: "body",
parameters: data.templateVariables.map((value) => ({
type: "text",
text: value,
})),
},
];
}
const response = await axios.post(this.baseUrl, templateBody, {
headers: {
Authorization: `Bearer ${this.authToken}`,
"Content-Type": "application/json",
},
});
return response.data;
}
}
@@ -1,9 +1,19 @@
export enum NOTIFICATION_TYPES { export enum NOTIFICATION_TYPES {
EMAIL = "EMAIL", EMAIL = "EMAIL",
WAP = "WAP", WAP = "WAP",
META_WAP = "META_WAP",
SYSTEM = "SYSTEM", SYSTEM = "SYSTEM",
} }
export type NotificationDataMeta = {
phoneNumber: string;
templateName: string;
languageCode: string;
// Si el template tiene variables, este array es obligatorio.
// Cada elemento mapea a {{1}}, {{2}}, {{3}}, etc.
templateVariables?: string[];
};
export type NotificationDataEmail = { export type NotificationDataEmail = {
email: string; email: string;
subject?: string; subject?: string;
@@ -1,10 +1,12 @@
import { import {
NotificationDataEmail, NotificationDataEmail,
NotificationDataMeta,
NotificationDataSystem, NotificationDataSystem,
NotificationDataWap, NotificationDataWap,
} from "./Notifications.Interface"; } from "./Notifications.Interface";
import { NotificationsWhatsappBotAdapter } from "./Notifications.Adapter.Bot"; import { NotificationsWhatsappBotAdapter } from "./Notifications.Adapter.Bot";
import { NotificationsMetaAdapter } from "./Notifications.Adapter.Meta";
import { NotificationsSystemAdapter } from "./Notifications.Adapter.System"; import { NotificationsSystemAdapter } from "./Notifications.Adapter.System";
import { NotificationsDonWebAdapter } from "./Notifications.Adapter.donweb"; import { NotificationsDonWebAdapter } from "./Notifications.Adapter.donweb";
@@ -12,11 +14,13 @@ class NotificationsMngr {
//notificationsNodeMailer: NotificationsNodemailerAdapter; //notificationsNodeMailer: NotificationsNodemailerAdapter;
notificationsDonWeb: NotificationsDonWebAdapter; notificationsDonWeb: NotificationsDonWebAdapter;
notificationsBot: NotificationsWhatsappBotAdapter; notificationsBot: NotificationsWhatsappBotAdapter;
notificationsMeta: NotificationsMetaAdapter;
notificationsSystem: NotificationsSystemAdapter; notificationsSystem: NotificationsSystemAdapter;
constructor() { constructor() {
//this.notificationsNodeMailer = new NotificationsNodemailerAdapter(); //this.notificationsNodeMailer = new NotificationsNodemailerAdapter();
this.notificationsDonWeb = new NotificationsDonWebAdapter(); this.notificationsDonWeb = new NotificationsDonWebAdapter();
this.notificationsBot = new NotificationsWhatsappBotAdapter(); this.notificationsBot = new NotificationsWhatsappBotAdapter();
this.notificationsMeta = new NotificationsMetaAdapter();
this.notificationsSystem = new NotificationsSystemAdapter(); this.notificationsSystem = new NotificationsSystemAdapter();
} }
@@ -29,6 +33,10 @@ class NotificationsMngr {
await this.notificationsBot.send(data); await this.notificationsBot.send(data);
} }
async sendWhatsAppTemplate(data: NotificationDataMeta): Promise<void> {
await this.notificationsMeta.send(data);
}
async sendSystemNotification(data: NotificationDataSystem): Promise<void> { async sendSystemNotification(data: NotificationDataSystem): Promise<void> {
await this.notificationsSystem.send(data); await this.notificationsSystem.send(data);
} }
@@ -21,6 +21,14 @@ interface SendWapByContentParams {
sessionUser: string; sessionUser: string;
} }
interface SendWhatsAppTemplateParams {
systemToken: string;
phoneNumber: string;
templateName: string;
languageCode: string;
templateVariables?: string[];
}
@Route("notifications/send-email") @Route("notifications/send-email")
@Middlewares([rateLimiter, systemTokenMiddleware]) @Middlewares([rateLimiter, systemTokenMiddleware])
export class SendEmailByContentController extends Controller { export class SendEmailByContentController extends Controller {
@@ -43,23 +51,44 @@ export class SendEmailByContentController extends Controller {
} }
} }
@Route("notifications/send-wap") @Route("notifications/send-wap")
@Middlewares([rateLimiter, systemTokenMiddleware]) @Middlewares([rateLimiter, systemTokenMiddleware])
export class SendWapByContentController extends Controller { export class SendWapByContentController extends Controller {
@SuccessResponse(200, "Done") @SuccessResponse(200, "Done")
@Post() @Post()
public async sendWapByContent(@Body() requestBody: SendWapByContentParams): Promise<ApiVoidResult> { public async sendWapByContent(@Body() requestBody: SendWapByContentParams): Promise<ApiVoidResult> {
await NotificationsManager.sendWap({ await NotificationsManager.sendWap({
phoneNumber: requestBody.phoneNumber, phoneNumber: requestBody.phoneNumber,
message: requestBody.message, message: requestBody.message,
companyId: requestBody.companyId, companyId: requestBody.companyId,
sessionUser: requestBody.sessionUser, sessionUser: requestBody.sessionUser,
}); });
this.setStatus(200); this.setStatus(200);
return { return {
message: "Done", message: "Done",
success: true, success: true,
}; };
}
}
@Route("notifications/send-whatsapp-template")
@Middlewares([rateLimiter, systemTokenMiddleware])
export class SendWhatsAppTemplateController extends Controller {
@SuccessResponse(200, "Done")
@Post()
public async sendWhatsAppTemplate(@Body() requestBody: SendWhatsAppTemplateParams): Promise<ApiVoidResult> {
await NotificationsManager.sendWhatsAppTemplate({
phoneNumber: requestBody.phoneNumber,
templateName: requestBody.templateName,
languageCode: requestBody.languageCode,
templateVariables: requestBody.templateVariables,
});
this.setStatus(200);
return {
message: "Done",
success: true,
};
}
} }
}
@@ -1,4 +1,4 @@
export type NotificationChannel = "whatsapp" | "email" | "system"; export type NotificationChannel = "whatsapp" | "meta_wap" | "email" | "system";
export interface QuietHours { export interface QuietHours {
from: string; // "HH:mm" from: string; // "HH:mm"
@@ -18,7 +18,8 @@ type UrlData = {
}; };
const CHANNELS: { key: NotificationChannel; label: string }[] = [ const CHANNELS: { key: NotificationChannel; label: string }[] = [
{ key: "whatsapp", label: "WhatsApp" }, { key: "whatsapp", label: "WhatsApp (Bot)" },
{ key: "meta_wap", label: "WhatsApp (Meta)" },
{ key: "email", label: "Email" }, { key: "email", label: "Email" },
{ key: "system", label: "Sistema" } { key: "system", label: "Sistema" }
]; ];
@@ -19,7 +19,8 @@ type UrlData = {
}; };
const CHANNELS: { key: NotificationChannel; label: string }[] = [ const CHANNELS: { key: NotificationChannel; label: string }[] = [
{ key: "whatsapp", label: "WhatsApp" }, { key: "whatsapp", label: "WhatsApp (Bot)" },
{ key: "meta_wap", label: "WhatsApp (Meta)" },
{ key: "email", label: "Email" }, { key: "email", label: "Email" },
{ key: "system", label: "Sistema" } { key: "system", label: "Sistema" }
]; ];
@@ -15,7 +15,8 @@ import MaterialAlert from "@mui/material/Alert";
import styles from "./NotificationsPreferencesPage.module.css"; import styles from "./NotificationsPreferencesPage.module.css";
const CHANNELS: { key: NotificationChannel; label: string }[] = [ const CHANNELS: { key: NotificationChannel; label: string }[] = [
{ key: "whatsapp", label: "WhatsApp" }, { key: "whatsapp", label: "WhatsApp (Bot)" },
{ key: "meta_wap", label: "WhatsApp (Meta)" },
{ key: "email", label: "Email" }, { key: "email", label: "Email" },
{ key: "system", label: "Sistema" } { key: "system", label: "Sistema" }
]; ];