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:
@@ -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"
|
||||||
|
|||||||
+2
-2
@@ -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"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-2
@@ -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"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-2
@@ -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 {
|
||||||
@@ -63,3 +71,24 @@ export class SendWapByContentController extends Controller {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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"
|
||||||
|
|||||||
+2
-1
@@ -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" }
|
||||||
];
|
];
|
||||||
|
|||||||
+2
-1
@@ -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" }
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user