From 40bc65396f7b730cfe7270c4eb91ee91af2fc051 Mon Sep 17 00:00:00 2001 From: Horacio Daniel Ros Date: Sat, 19 Sep 2026 10:50:56 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20agregar=20soporte=20para=20el=20env?= =?UTF-8?q?=C3=ADo=20de=20plantillas=20de=20WhatsApp=20a=20trav=C3=A9s=20d?= =?UTF-8?q?e=20Meta,=20incluyendo=20nueva=20funcionalidad=20y=20adaptadore?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Models/Jobs/ChannelDispatchers.ts | 34 ++++++++++ .../src/Models/Jobs/JobProcessor.ts | 10 ++- .../Jobs/NotificationJob.Adapter.Mongoose.ts | 4 +- .../NotificationJobs.Interface.ts | 2 +- ...nyNotificationOverride.Adapter.Mongoose.ts | 4 +- ...otificationPreferences.Adapter.Mongoose.ts | 4 +- ...panyNotificationPolicy.Adapter.Mongoose.ts | 4 +- .../NotificationJobService.ts | 4 ++ .../NotificationPolicies/PolicyResolver.ts | 1 + .../Notifications.Adapter.Meta.ts | 48 ++++++++++++++ .../Notifications/Notifications.Interface.ts | 10 +++ .../src/Models/Notifications/Notifications.ts | 8 +++ .../Notifications/Notifications.Controller.ts | 65 ++++++++++++++----- .../Models/NotificationPreferences.model.ts | 2 +- .../client/[clientId]/notifications/page.tsx | 3 +- .../org/profile/[id]/notifications/page.tsx | 3 +- .../notifications-preferences/page.tsx | 3 +- 17 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 server/src/Models/Notifications/Notifications.Adapter.Meta.ts diff --git a/notification-sender/src/Models/Jobs/ChannelDispatchers.ts b/notification-sender/src/Models/Jobs/ChannelDispatchers.ts index ffbdda3..0aa1cb2 100644 --- a/notification-sender/src/Models/Jobs/ChannelDispatchers.ts +++ b/notification-sender/src/Models/Jobs/ChannelDispatchers.ts @@ -190,3 +190,37 @@ export async function dispatchSystem(params: { return { success: false, error: msg }; } } + +export async function dispatchMetaWap(params: { + phoneNumber: string; + templateName: string; + languageCode: string; + templateVariables?: string[]; +}): Promise { + 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 }; + } +} diff --git a/notification-sender/src/Models/Jobs/JobProcessor.ts b/notification-sender/src/Models/Jobs/JobProcessor.ts index 4ed6934..df8c6b8 100644 --- a/notification-sender/src/Models/Jobs/JobProcessor.ts +++ b/notification-sender/src/Models/Jobs/JobProcessor.ts @@ -1,7 +1,7 @@ import { logDone, logError, logIntent } from "../../config/logger.js"; import { NotificationJobModel, INotificationJob } from "./NotificationJob.Adapter.Mongoose.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"; const POLL_INTERVAL_MS = 5000; @@ -217,6 +217,14 @@ export class JobProcessor { code: String(job.appointmentId), }); 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: result = { success: false, error: `Unknown channel: ${job.channel}` }; } diff --git a/notification-sender/src/Models/Jobs/NotificationJob.Adapter.Mongoose.ts b/notification-sender/src/Models/Jobs/NotificationJob.Adapter.Mongoose.ts index 01690de..4346be0 100644 --- a/notification-sender/src/Models/Jobs/NotificationJob.Adapter.Mongoose.ts +++ b/notification-sender/src/Models/Jobs/NotificationJob.Adapter.Mongoose.ts @@ -1,6 +1,6 @@ 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 NotificationJobType = "creation" | "reminder" | "update" | "cancellation"; @@ -69,7 +69,7 @@ export class NotificationJobModel { channel: { type: String, required: true, - enum: ["whatsapp", "email", "system"], + enum: ["whatsapp", "meta_wap", "email", "system"], }, type: { type: String, diff --git a/server/src/Models/NotificationJobs/NotificationJobs.Interface.ts b/server/src/Models/NotificationJobs/NotificationJobs.Interface.ts index 251aa1d..083b896 100644 --- a/server/src/Models/NotificationJobs/NotificationJobs.Interface.ts +++ b/server/src/Models/NotificationJobs/NotificationJobs.Interface.ts @@ -6,7 +6,7 @@ export enum NotificationJobStatus { CANCELLED = "cancelled", } -export type NotificationChannel = "whatsapp" | "email" | "system"; +export type NotificationChannel = "whatsapp" | "meta_wap" | "email" | "system"; export type NotificationJobType = | "creation" diff --git a/server/src/Models/NotificationPolicies/ClientCompanyNotificationOverride.Adapter.Mongoose.ts b/server/src/Models/NotificationPolicies/ClientCompanyNotificationOverride.Adapter.Mongoose.ts index 87f27df..751e737 100644 --- a/server/src/Models/NotificationPolicies/ClientCompanyNotificationOverride.Adapter.Mongoose.ts +++ b/server/src/Models/NotificationPolicies/ClientCompanyNotificationOverride.Adapter.Mongoose.ts @@ -32,13 +32,13 @@ export class ClientCompanyNotificationOverrideAdapterMongoose type: [String], required: true, default: ["whatsapp", "email", "system"], - enum: ["whatsapp", "email", "system"], + enum: ["whatsapp", "meta_wap", "email", "system"], }, mutedChannels: { type: [String], required: true, default: [], - enum: ["whatsapp", "email", "system"], + enum: ["whatsapp", "meta_wap", "email", "system"], }, }, { diff --git a/server/src/Models/NotificationPolicies/ClientNotificationPreferences.Adapter.Mongoose.ts b/server/src/Models/NotificationPolicies/ClientNotificationPreferences.Adapter.Mongoose.ts index c10fd0d..c8b13c6 100644 --- a/server/src/Models/NotificationPolicies/ClientNotificationPreferences.Adapter.Mongoose.ts +++ b/server/src/Models/NotificationPolicies/ClientNotificationPreferences.Adapter.Mongoose.ts @@ -28,13 +28,13 @@ export class ClientNotificationPreferencesAdapterMongoose type: [String], required: true, default: ["whatsapp", "email", "system"], - enum: ["whatsapp", "email", "system"], + enum: ["whatsapp", "meta_wap", "email", "system"], }, mutedChannels: { type: [String], required: true, default: [], - enum: ["whatsapp", "email", "system"], + enum: ["whatsapp", "meta_wap", "email", "system"], }, }, { diff --git a/server/src/Models/NotificationPolicies/CompanyNotificationPolicy.Adapter.Mongoose.ts b/server/src/Models/NotificationPolicies/CompanyNotificationPolicy.Adapter.Mongoose.ts index 6676802..b3e2497 100644 --- a/server/src/Models/NotificationPolicies/CompanyNotificationPolicy.Adapter.Mongoose.ts +++ b/server/src/Models/NotificationPolicies/CompanyNotificationPolicy.Adapter.Mongoose.ts @@ -28,13 +28,13 @@ export class CompanyNotificationPolicyAdapterMongoose type: [String], required: true, default: ["whatsapp", "email", "system"], - enum: ["whatsapp", "email", "system"], + enum: ["whatsapp", "meta_wap", "email", "system"], }, mutedChannels: { type: [String], required: false, default: [], - enum: ["whatsapp", "email", "system"], + enum: ["whatsapp", "meta_wap", "email", "system"], }, timezone: { type: String, diff --git a/server/src/Models/NotificationPolicies/NotificationJobService.ts b/server/src/Models/NotificationPolicies/NotificationJobService.ts index 5d01f5e..83a4c8b 100644 --- a/server/src/Models/NotificationPolicies/NotificationJobService.ts +++ b/server/src/Models/NotificationPolicies/NotificationJobService.ts @@ -248,6 +248,10 @@ export class NotificationJobService { if (payload?.phoneNumber) { channels.push("whatsapp"); + // meta_wap requires templateName in payload to be useful + if (payload?.templateName) { + channels.push("meta_wap"); + } } if (payload?.userId) { diff --git a/server/src/Models/NotificationPolicies/PolicyResolver.ts b/server/src/Models/NotificationPolicies/PolicyResolver.ts index ba6aa3a..9af84ae 100644 --- a/server/src/Models/NotificationPolicies/PolicyResolver.ts +++ b/server/src/Models/NotificationPolicies/PolicyResolver.ts @@ -14,6 +14,7 @@ const SYSTEM_DEFAULT_CHANNELS: NotificationChannel[] = ["whatsapp", "email", "sy const PLAN_CHANNEL_MAP: Record = { whatsapp: PlanFeatures.WAP_NOTIFICATIONS, + meta_wap: PlanFeatures.WAP_NOTIFICATIONS, email: PlanFeatures.MAIL_NOTIFICATIONS, }; diff --git a/server/src/Models/Notifications/Notifications.Adapter.Meta.ts b/server/src/Models/Notifications/Notifications.Adapter.Meta.ts new file mode 100644 index 0000000..67ca123 --- /dev/null +++ b/server/src/Models/Notifications/Notifications.Adapter.Meta.ts @@ -0,0 +1,48 @@ +import axios from "axios"; +import { INotificationsAdapter, NotificationDataMeta } from "./Notifications.Interface"; + +export class NotificationsMetaAdapter implements INotificationsAdapter { + // ── 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 { + const templateBody: Record = { + 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).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; + } +} diff --git a/server/src/Models/Notifications/Notifications.Interface.ts b/server/src/Models/Notifications/Notifications.Interface.ts index 08d2bf2..472329c 100644 --- a/server/src/Models/Notifications/Notifications.Interface.ts +++ b/server/src/Models/Notifications/Notifications.Interface.ts @@ -1,9 +1,19 @@ export enum NOTIFICATION_TYPES { EMAIL = "EMAIL", WAP = "WAP", + META_WAP = "META_WAP", 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 = { email: string; subject?: string; diff --git a/server/src/Models/Notifications/Notifications.ts b/server/src/Models/Notifications/Notifications.ts index 87225e5..c8e7b60 100644 --- a/server/src/Models/Notifications/Notifications.ts +++ b/server/src/Models/Notifications/Notifications.ts @@ -1,10 +1,12 @@ import { NotificationDataEmail, + NotificationDataMeta, NotificationDataSystem, NotificationDataWap, } from "./Notifications.Interface"; import { NotificationsWhatsappBotAdapter } from "./Notifications.Adapter.Bot"; +import { NotificationsMetaAdapter } from "./Notifications.Adapter.Meta"; import { NotificationsSystemAdapter } from "./Notifications.Adapter.System"; import { NotificationsDonWebAdapter } from "./Notifications.Adapter.donweb"; @@ -12,11 +14,13 @@ class NotificationsMngr { //notificationsNodeMailer: NotificationsNodemailerAdapter; notificationsDonWeb: NotificationsDonWebAdapter; notificationsBot: NotificationsWhatsappBotAdapter; + notificationsMeta: NotificationsMetaAdapter; notificationsSystem: NotificationsSystemAdapter; constructor() { //this.notificationsNodeMailer = new NotificationsNodemailerAdapter(); this.notificationsDonWeb = new NotificationsDonWebAdapter(); this.notificationsBot = new NotificationsWhatsappBotAdapter(); + this.notificationsMeta = new NotificationsMetaAdapter(); this.notificationsSystem = new NotificationsSystemAdapter(); } @@ -29,6 +33,10 @@ class NotificationsMngr { await this.notificationsBot.send(data); } + async sendWhatsAppTemplate(data: NotificationDataMeta): Promise { + await this.notificationsMeta.send(data); + } + async sendSystemNotification(data: NotificationDataSystem): Promise { await this.notificationsSystem.send(data); } diff --git a/server/src/api/Notifications/Notifications.Controller.ts b/server/src/api/Notifications/Notifications.Controller.ts index 19a3f8c..64380be 100644 --- a/server/src/api/Notifications/Notifications.Controller.ts +++ b/server/src/api/Notifications/Notifications.Controller.ts @@ -21,6 +21,14 @@ interface SendWapByContentParams { sessionUser: string; } +interface SendWhatsAppTemplateParams { + systemToken: string; + phoneNumber: string; + templateName: string; + languageCode: string; + templateVariables?: string[]; +} + @Route("notifications/send-email") @Middlewares([rateLimiter, systemTokenMiddleware]) export class SendEmailByContentController extends Controller { @@ -43,23 +51,44 @@ export class SendEmailByContentController extends Controller { } } -@Route("notifications/send-wap") -@Middlewares([rateLimiter, systemTokenMiddleware]) -export class SendWapByContentController extends Controller { - @SuccessResponse(200, "Done") - @Post() - public async sendWapByContent(@Body() requestBody: SendWapByContentParams): Promise { - await NotificationsManager.sendWap({ - phoneNumber: requestBody.phoneNumber, - message: requestBody.message, - companyId: requestBody.companyId, - sessionUser: requestBody.sessionUser, - }); + @Route("notifications/send-wap") + @Middlewares([rateLimiter, systemTokenMiddleware]) + export class SendWapByContentController extends Controller { + @SuccessResponse(200, "Done") + @Post() + public async sendWapByContent(@Body() requestBody: SendWapByContentParams): Promise { + await NotificationsManager.sendWap({ + phoneNumber: requestBody.phoneNumber, + message: requestBody.message, + companyId: requestBody.companyId, + sessionUser: requestBody.sessionUser, + }); - this.setStatus(200); - return { - message: "Done", - success: true, - }; + this.setStatus(200); + return { + message: "Done", + 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 { + await NotificationsManager.sendWhatsAppTemplate({ + phoneNumber: requestBody.phoneNumber, + templateName: requestBody.templateName, + languageCode: requestBody.languageCode, + templateVariables: requestBody.templateVariables, + }); + + this.setStatus(200); + return { + message: "Done", + success: true, + }; + } } -} diff --git a/txclient/src/Models/NotificationPreferences.model.ts b/txclient/src/Models/NotificationPreferences.model.ts index 5bcc06c..c122b5d 100644 --- a/txclient/src/Models/NotificationPreferences.model.ts +++ b/txclient/src/Models/NotificationPreferences.model.ts @@ -1,4 +1,4 @@ -export type NotificationChannel = "whatsapp" | "email" | "system"; +export type NotificationChannel = "whatsapp" | "meta_wap" | "email" | "system"; export interface QuietHours { from: string; // "HH:mm" diff --git a/txclient/src/app/admin/(client-profile)/org/[oid]/client/[clientId]/notifications/page.tsx b/txclient/src/app/admin/(client-profile)/org/[oid]/client/[clientId]/notifications/page.tsx index 1f209fd..97430bb 100644 --- a/txclient/src/app/admin/(client-profile)/org/[oid]/client/[clientId]/notifications/page.tsx +++ b/txclient/src/app/admin/(client-profile)/org/[oid]/client/[clientId]/notifications/page.tsx @@ -18,7 +18,8 @@ type UrlData = { }; 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: "system", label: "Sistema" } ]; diff --git a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/notifications/page.tsx b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/notifications/page.tsx index 048166a..a7c4b01 100644 --- a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/notifications/page.tsx +++ b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/notifications/page.tsx @@ -19,7 +19,8 @@ type UrlData = { }; 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: "system", label: "Sistema" } ]; diff --git a/txclient/src/app/user/profile/notifications-preferences/page.tsx b/txclient/src/app/user/profile/notifications-preferences/page.tsx index 16f1729..15fffa3 100644 --- a/txclient/src/app/user/profile/notifications-preferences/page.tsx +++ b/txclient/src/app/user/profile/notifications-preferences/page.tsx @@ -15,7 +15,8 @@ import MaterialAlert from "@mui/material/Alert"; import styles from "./NotificationsPreferencesPage.module.css"; 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: "system", label: "Sistema" } ];