feat: Add Meta WAP Credentials management
- Introduced Meta WAP Credentials model and interface for managing WhatsApp Business credentials. - Implemented Mongoose adapter for Meta WAP Credentials with upsert and find functionalities. - Created service layer for handling Meta WAP Credentials operations including save, find, test, and disable. - Added API controllers for Meta WAP Credentials with endpoints for saving, finding, testing, and disabling credentials. - Updated Notifications Manager to utilize Meta WAP Credentials for sending WhatsApp messages. - Enhanced the front-end to manage Meta WAP Credentials with forms for input and validation. - Implemented UI components for displaying and interacting with Meta WAP Credentials.
This commit is contained in:
@@ -74,3 +74,4 @@ ARCA_WSAA_URL=https://wsaahomo.afip.gov.ar/ws/services/LoginCms
|
|||||||
ARCA_WSFE_URL=https://wswhomo.afip.gov.ar/wsfev1/service.asmx
|
ARCA_WSFE_URL=https://wswhomo.afip.gov.ar/wsfev1/service.asmx
|
||||||
|
|
||||||
ARCA_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
ARCA_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
||||||
|
META_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
||||||
@@ -72,3 +72,4 @@ ARCA_WSAA_URL=https://wsaa.afip.gov.ar/ws/services/LoginCms
|
|||||||
ARCA_WSFE_URL=https://servicios1.afip.gov.ar/wsfev1/service.asmx
|
ARCA_WSFE_URL=https://servicios1.afip.gov.ar/wsfev1/service.asmx
|
||||||
|
|
||||||
ARCA_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
ARCA_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
||||||
|
META_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Document, FilterQuery, Model, Schema, model } from "mongoose";
|
||||||
|
import {
|
||||||
|
IMetaWapCredential,
|
||||||
|
IMetaWapCredentialAdapter,
|
||||||
|
META_WAP_CREDENTIAL_STATUS,
|
||||||
|
} from "./MetaWapCredentials.Interface";
|
||||||
|
|
||||||
|
export interface IMetaWapCredentialDocument extends Omit<IMetaWapCredential, "id">, Document {}
|
||||||
|
|
||||||
|
export class MetaWapCredentialsAdapterMongoose implements IMetaWapCredentialAdapter {
|
||||||
|
schema: Schema;
|
||||||
|
metaWapCredentialList: Model<IMetaWapCredentialDocument>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.schema = new Schema({
|
||||||
|
companyId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
required: true,
|
||||||
|
ref: "Company",
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
phoneNumberId: { type: String, required: true },
|
||||||
|
wabaId: { type: String, required: true },
|
||||||
|
encryptedAccessToken: { type: String, required: true },
|
||||||
|
accessTokenIv: { type: String, required: true },
|
||||||
|
accessTokenAuthTag: { type: String, required: true },
|
||||||
|
encryptionKeyVersion: { type: String, required: false, default: "v1" },
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
enum: Object.values(META_WAP_CREDENTIAL_STATUS),
|
||||||
|
default: META_WAP_CREDENTIAL_STATUS.CONFIGURED,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
lastTestedAt: { type: Date, required: false },
|
||||||
|
lastTestError: { type: String, required: false },
|
||||||
|
createdAt: { type: Date, required: true, default: Date.now },
|
||||||
|
updatedAt: { type: Date, required: true, default: Date.now },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.metaWapCredentialList = model<IMetaWapCredentialDocument>("MetaWapCredential", this.schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSearchCriteria(filters: { companyId?: string }): FilterQuery<IMetaWapCredential> {
|
||||||
|
const searchCriteria: FilterQuery<IMetaWapCredential> = {};
|
||||||
|
|
||||||
|
if (filters.companyId) searchCriteria.companyId = filters.companyId;
|
||||||
|
|
||||||
|
return searchCriteria;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findOne(filters: { companyId?: string }): Promise<IMetaWapCredentialDocument | null> {
|
||||||
|
const searchCriteria = this.buildSearchCriteria(filters);
|
||||||
|
return this.metaWapCredentialList.findOne(searchCriteria).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async upsert(data: IMetaWapCredential): Promise<IMetaWapCredential> {
|
||||||
|
const { companyId, ...updateData } = data;
|
||||||
|
|
||||||
|
return this.metaWapCredentialList
|
||||||
|
.findOneAndUpdate({ companyId }, { ...updateData, updatedAt: Date.now() }, { new: true, upsert: true })
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { IMetaWapCredentialDocument } from "./MetaWapCredentials.Adapter.Mongoose";
|
||||||
|
|
||||||
|
export enum META_WAP_CREDENTIAL_STATUS {
|
||||||
|
CONFIGURED = "CONFIGURED",
|
||||||
|
TESTED = "TESTED",
|
||||||
|
INVALID = "INVALID",
|
||||||
|
DISABLED = "DISABLED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SaveMetaWapCredentialParams = {
|
||||||
|
companyId: string;
|
||||||
|
phoneNumberId: string;
|
||||||
|
wabaId: string;
|
||||||
|
accessToken: string;
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FindMetaWapCredentialParams = {
|
||||||
|
companyId: string;
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TestMetaWapCredentialParams = {
|
||||||
|
companyId: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
templateName: string;
|
||||||
|
languageCode: string;
|
||||||
|
templateVariables?: string[];
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DisableMetaWapCredentialParams = {
|
||||||
|
companyId: string;
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IMetaWapCredential {
|
||||||
|
id?: string;
|
||||||
|
companyId: string;
|
||||||
|
phoneNumberId: string;
|
||||||
|
wabaId: string;
|
||||||
|
encryptedAccessToken: string;
|
||||||
|
accessTokenIv: string;
|
||||||
|
accessTokenAuthTag: string;
|
||||||
|
encryptionKeyVersion?: string;
|
||||||
|
status: META_WAP_CREDENTIAL_STATUS;
|
||||||
|
lastTestedAt?: Date;
|
||||||
|
lastTestError?: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MetaWapCredentialView = Omit<
|
||||||
|
IMetaWapCredential,
|
||||||
|
"encryptedAccessToken" | "accessTokenIv" | "accessTokenAuthTag"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TestMetaWapCredentialResult = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
testedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IMetaWapCredentialAdapter {
|
||||||
|
findOne(filters: { companyId?: string }): Promise<IMetaWapCredentialDocument | null>;
|
||||||
|
upsert(data: IMetaWapCredential): Promise<IMetaWapCredential>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMetaWapCredentialManager {
|
||||||
|
metaWapCredentials: IMetaWapCredentialAdapter;
|
||||||
|
save(data: SaveMetaWapCredentialParams): Promise<MetaWapCredentialView>;
|
||||||
|
find(data: FindMetaWapCredentialParams): Promise<MetaWapCredentialView>;
|
||||||
|
test(data: TestMetaWapCredentialParams): Promise<TestMetaWapCredentialResult>;
|
||||||
|
disable(data: DisableMetaWapCredentialParams): Promise<MetaWapCredentialView>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
|
||||||
|
import { NotificationsManager } from "../Notifications/Notifications";
|
||||||
|
import { MetaWapCredentialsAdapterMongoose } from "./MetaWapCredentials.Adapter.Mongoose";
|
||||||
|
import {
|
||||||
|
DisableMetaWapCredentialParams,
|
||||||
|
FindMetaWapCredentialParams,
|
||||||
|
IMetaWapCredential,
|
||||||
|
IMetaWapCredentialManager,
|
||||||
|
META_WAP_CREDENTIAL_STATUS,
|
||||||
|
MetaWapCredentialView,
|
||||||
|
SaveMetaWapCredentialParams,
|
||||||
|
TestMetaWapCredentialParams,
|
||||||
|
TestMetaWapCredentialResult,
|
||||||
|
} from "./MetaWapCredentials.Interface";
|
||||||
|
|
||||||
|
export type MetaWapConnectionCredentials = {
|
||||||
|
phoneNumberId: string;
|
||||||
|
authToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MetaWapCredentialsManager implements IMetaWapCredentialManager {
|
||||||
|
metaWapCredentials: MetaWapCredentialsAdapterMongoose;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.metaWapCredentials = new MetaWapCredentialsAdapterMongoose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMasterKey(): Buffer {
|
||||||
|
const configuredKey = process.env.META_CREDENTIALS_MASTER_KEY;
|
||||||
|
|
||||||
|
if (!configuredKey) {
|
||||||
|
throw new Error("META_CREDENTIALS_MASTER_KEY no está configurada");
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = /^[a-fA-F0-9]{64}$/.test(configuredKey)
|
||||||
|
? Buffer.from(configuredKey, "hex")
|
||||||
|
: Buffer.from(configuredKey, "base64");
|
||||||
|
|
||||||
|
if (key.length !== 32) {
|
||||||
|
throw new Error("META_CREDENTIALS_MASTER_KEY debe tener 32 bytes en base64 o 64 caracteres hexadecimales");
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private encryptAccessToken(accessToken: string): {
|
||||||
|
encryptedAccessToken: string;
|
||||||
|
accessTokenIv: string;
|
||||||
|
accessTokenAuthTag: string;
|
||||||
|
} {
|
||||||
|
const iv = crypto.randomBytes(12);
|
||||||
|
const cipher = crypto.createCipheriv(
|
||||||
|
"aes-256-gcm",
|
||||||
|
crypto.createSecretKey(new Uint8Array(this.getMasterKey())),
|
||||||
|
new Uint8Array(iv)
|
||||||
|
);
|
||||||
|
const encryptedAccessToken = cipher.update(accessToken, "utf8", "base64") + cipher.final("base64");
|
||||||
|
|
||||||
|
return {
|
||||||
|
encryptedAccessToken,
|
||||||
|
accessTokenIv: iv.toString("base64"),
|
||||||
|
accessTokenAuthTag: cipher.getAuthTag().toString("base64"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private decryptAccessToken(credential: IMetaWapCredential): string {
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
"aes-256-gcm",
|
||||||
|
crypto.createSecretKey(new Uint8Array(this.getMasterKey())),
|
||||||
|
new Uint8Array(Buffer.from(credential.accessTokenIv, "base64"))
|
||||||
|
);
|
||||||
|
decipher.setAuthTag(new Uint8Array(Buffer.from(credential.accessTokenAuthTag, "base64")));
|
||||||
|
|
||||||
|
return decipher.update(credential.encryptedAccessToken, "base64", "utf8") + decipher.final("utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeCredential(credential: IMetaWapCredential): MetaWapCredentialView {
|
||||||
|
const rawCredential = typeof (credential as any).toObject === "function"
|
||||||
|
? (credential as any).toObject()
|
||||||
|
: credential;
|
||||||
|
const { encryptedAccessToken, accessTokenIv, accessTokenAuthTag, ...safeCredential } = rawCredential;
|
||||||
|
return safeCredential;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateCompanyAdmin(companyId: string, sessionUser: string): Promise<void> {
|
||||||
|
await validateSessionUser({ sessionUser });
|
||||||
|
|
||||||
|
if (!companyId || !companyId.trim()) {
|
||||||
|
throw new Error("Se requiere una organización para operar credenciales de Meta");
|
||||||
|
}
|
||||||
|
|
||||||
|
await validatePermissionsByCompany({ companyId, sessionUser });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requireConfiguredCredentials(companyId: string): Promise<IMetaWapCredential> {
|
||||||
|
const credential = await this.metaWapCredentials.findOne({ companyId });
|
||||||
|
|
||||||
|
if (!credential) {
|
||||||
|
throw new Error("La organización no tiene credenciales de Meta configuradas");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential.status === META_WAP_CREDENTIAL_STATUS.DISABLED) {
|
||||||
|
throw new Error("Las credenciales de Meta están desactivadas");
|
||||||
|
}
|
||||||
|
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async save(data: SaveMetaWapCredentialParams): Promise<MetaWapCredentialView> {
|
||||||
|
await this.validateCompanyAdmin(data.companyId, data.sessionUser);
|
||||||
|
|
||||||
|
if (!data.phoneNumberId || !data.phoneNumberId.trim()) {
|
||||||
|
throw new Error("phoneNumberId es obligatorio");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.wabaId || !data.wabaId.trim()) {
|
||||||
|
throw new Error("wabaId es obligatorio");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.accessToken || !data.accessToken.trim()) {
|
||||||
|
throw new Error("accessToken es obligatorio");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { encryptedAccessToken, accessTokenIv, accessTokenAuthTag } = this.encryptAccessToken(data.accessToken);
|
||||||
|
|
||||||
|
const credential = await this.metaWapCredentials.upsert({
|
||||||
|
companyId: data.companyId,
|
||||||
|
phoneNumberId: data.phoneNumberId,
|
||||||
|
wabaId: data.wabaId,
|
||||||
|
encryptedAccessToken,
|
||||||
|
accessTokenIv,
|
||||||
|
accessTokenAuthTag,
|
||||||
|
encryptionKeyVersion: process.env.META_CREDENTIALS_KEY_VERSION || "v1",
|
||||||
|
status: META_WAP_CREDENTIAL_STATUS.CONFIGURED,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.sanitizeCredential(credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async find(data: FindMetaWapCredentialParams): Promise<MetaWapCredentialView> {
|
||||||
|
await this.validateCompanyAdmin(data.companyId, data.sessionUser);
|
||||||
|
|
||||||
|
const credential = await this.metaWapCredentials.findOne({ companyId: data.companyId });
|
||||||
|
|
||||||
|
if (!credential) {
|
||||||
|
throw new Error("La organización no tiene credenciales de Meta configuradas");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sanitizeCredential(credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async test(data: TestMetaWapCredentialParams): Promise<TestMetaWapCredentialResult> {
|
||||||
|
await this.validateCompanyAdmin(data.companyId, data.sessionUser);
|
||||||
|
|
||||||
|
const credential = await this.requireConfiguredCredentials(data.companyId);
|
||||||
|
|
||||||
|
const authToken = this.decryptAccessToken(credential);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await NotificationsManager.sendWhatsAppTemplate(
|
||||||
|
{
|
||||||
|
companyId: data.companyId,
|
||||||
|
phoneNumber: data.phoneNumber,
|
||||||
|
templateName: data.templateName,
|
||||||
|
languageCode: data.languageCode,
|
||||||
|
templateVariables: data.templateVariables,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
phoneNumberId: credential.phoneNumberId,
|
||||||
|
authToken,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.metaWapCredentials.upsert({
|
||||||
|
companyId: data.companyId,
|
||||||
|
phoneNumberId: credential.phoneNumberId,
|
||||||
|
wabaId: credential.wabaId,
|
||||||
|
encryptedAccessToken: credential.encryptedAccessToken,
|
||||||
|
accessTokenIv: credential.accessTokenIv,
|
||||||
|
accessTokenAuthTag: credential.accessTokenAuthTag,
|
||||||
|
encryptionKeyVersion: credential.encryptionKeyVersion,
|
||||||
|
status: META_WAP_CREDENTIAL_STATUS.TESTED,
|
||||||
|
lastTestedAt: new Date(),
|
||||||
|
lastTestError: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Test sent successfully",
|
||||||
|
testedAt: new Date(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
await this.metaWapCredentials.upsert({
|
||||||
|
companyId: data.companyId,
|
||||||
|
phoneNumberId: credential.phoneNumberId,
|
||||||
|
wabaId: credential.wabaId,
|
||||||
|
encryptedAccessToken: credential.encryptedAccessToken,
|
||||||
|
accessTokenIv: credential.accessTokenIv,
|
||||||
|
accessTokenAuthTag: credential.accessTokenAuthTag,
|
||||||
|
encryptionKeyVersion: credential.encryptionKeyVersion,
|
||||||
|
status: META_WAP_CREDENTIAL_STATUS.INVALID,
|
||||||
|
lastTestedAt: new Date(),
|
||||||
|
lastTestError: errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error(`Test failed: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disable(data: DisableMetaWapCredentialParams): Promise<MetaWapCredentialView> {
|
||||||
|
await this.validateCompanyAdmin(data.companyId, data.sessionUser);
|
||||||
|
|
||||||
|
const credential = await this.requireConfiguredCredentials(data.companyId);
|
||||||
|
|
||||||
|
const updated = await this.metaWapCredentials.upsert({
|
||||||
|
companyId: data.companyId,
|
||||||
|
phoneNumberId: credential.phoneNumberId,
|
||||||
|
wabaId: credential.wabaId,
|
||||||
|
encryptedAccessToken: credential.encryptedAccessToken,
|
||||||
|
accessTokenIv: credential.accessTokenIv,
|
||||||
|
accessTokenAuthTag: credential.accessTokenAuthTag,
|
||||||
|
encryptionKeyVersion: credential.encryptionKeyVersion,
|
||||||
|
status: META_WAP_CREDENTIAL_STATUS.DISABLED,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.sanitizeCredential(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getCredentialsForSending(companyId: string): Promise<MetaWapConnectionCredentials> {
|
||||||
|
const credential = await this.requireConfiguredCredentials(companyId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
phoneNumberId: credential.phoneNumberId,
|
||||||
|
authToken: this.decryptAccessToken(credential),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new MetaWapCredentialsManager();
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { INotificationsAdapter, NotificationDataMeta } from "./Notifications.Interface";
|
import { INotificationsAdapter, NotificationDataMeta } from "./Notifications.Interface";
|
||||||
|
|
||||||
|
export type MetaConnectionCredentials = {
|
||||||
|
phoneNumberId: string;
|
||||||
|
authToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class NotificationsMetaAdapter implements INotificationsAdapter<NotificationDataMeta> {
|
export class NotificationsMetaAdapter implements INotificationsAdapter<NotificationDataMeta> {
|
||||||
// ── Datos hardcodeados para pruebas (etapa 1) ──
|
private readonly credentials: MetaConnectionCredentials;
|
||||||
private readonly phoneNumberId = "1379557278565544"; // ej: "1379557278565544"
|
|
||||||
private readonly authToken =
|
|
||||||
"EAAeZCf8kXotYBSuVCLO7smu3jGxv7qqndFn3OZB6E55QZBRKB2ZCPmOkylVfseI8PuZBzGBIrGa3QO0hUJZCGq7GwwJBgaxhu4A07LJZCOna82ZAQZAYZBljlQVApTurK1Hj0YHVZA4cnSdQa5xKmoYBa8YTuzyqrC1ZC2s9qbIvWHImUkVUgG2ps7F25ZAZCMISympdL1c34iO2IDLW2NGndy3o4E11K0c1FA3RFTCQJJXPmuddK360KLz5Tpj8BOTZC12FztytNQkwqpHZBBdpCpJfrOyQy546"; // tu token de Meta
|
|
||||||
private readonly apiVersion = "v25.0";
|
private readonly apiVersion = "v25.0";
|
||||||
|
|
||||||
|
constructor(credentials: MetaConnectionCredentials) {
|
||||||
|
this.credentials = credentials;
|
||||||
|
}
|
||||||
|
|
||||||
get baseUrl(): string {
|
get baseUrl(): string {
|
||||||
return `https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`;
|
return `https://graph.facebook.com/${this.apiVersion}/${this.credentials.phoneNumberId}/messages`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(data: NotificationDataMeta): Promise<void> {
|
async send(data: NotificationDataMeta): Promise<void> {
|
||||||
@@ -38,7 +44,7 @@ export class NotificationsMetaAdapter implements INotificationsAdapter<Notificat
|
|||||||
|
|
||||||
const response = await axios.post(this.baseUrl, templateBody, {
|
const response = await axios.post(this.baseUrl, templateBody, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.authToken}`,
|
Authorization: `Bearer ${this.credentials.authToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export enum NOTIFICATION_TYPES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type NotificationDataMeta = {
|
export type NotificationDataMeta = {
|
||||||
|
companyId: string;
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
templateName: string;
|
templateName: string;
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} 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 { MetaConnectionCredentials, 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";
|
||||||
|
|
||||||
@@ -14,13 +14,11 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +31,9 @@ class NotificationsMngr {
|
|||||||
await this.notificationsBot.send(data);
|
await this.notificationsBot.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendWhatsAppTemplate(data: NotificationDataMeta): Promise<void> {
|
async sendWhatsAppTemplate(data: NotificationDataMeta, credentials: MetaConnectionCredentials): Promise<void> {
|
||||||
await this.notificationsMeta.send(data);
|
const notificationsMeta = new NotificationsMetaAdapter(credentials);
|
||||||
|
await notificationsMeta.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendSystemNotification(data: NotificationDataSystem): Promise<void> {
|
async sendSystemNotification(data: NotificationDataSystem): Promise<void> {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { Body, Controller, Middlewares, Post, Response, Route, SuccessResponse } from "tsoa";
|
||||||
|
import { authenticateMiddleware } from "../../middleware/authentication";
|
||||||
|
import { rateLimiter } from "../../middleware/ratelimiter";
|
||||||
|
import { ApiValidationError } from "../../Models/Server.Error.model";
|
||||||
|
import {
|
||||||
|
DisableMetaWapCredentialParams,
|
||||||
|
FindMetaWapCredentialParams,
|
||||||
|
MetaWapCredentialView,
|
||||||
|
SaveMetaWapCredentialParams,
|
||||||
|
TestMetaWapCredentialParams,
|
||||||
|
TestMetaWapCredentialResult,
|
||||||
|
} from "../../Models/MetaWapCredentials/MetaWapCredentials.Interface";
|
||||||
|
import { MetaWapCredentialsService } from "./MetaWapCredentials.Service";
|
||||||
|
|
||||||
|
@Route("meta-credentials/save")
|
||||||
|
@Middlewares(authenticateMiddleware)
|
||||||
|
export class MetaWapCredentialsSaveController extends Controller {
|
||||||
|
@SuccessResponse(200, "Done")
|
||||||
|
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||||
|
@Post()
|
||||||
|
public async save(
|
||||||
|
@Body() requestBody: SaveMetaWapCredentialParams
|
||||||
|
): Promise<MetaWapCredentialView | ApiValidationError> {
|
||||||
|
try {
|
||||||
|
const credential = await new MetaWapCredentialsService().save(requestBody);
|
||||||
|
this.setStatus(200);
|
||||||
|
return credential;
|
||||||
|
} catch (e) {
|
||||||
|
const errorOccurred: Error = e as Error;
|
||||||
|
this.setStatus(500);
|
||||||
|
return new ApiValidationError(500, errorOccurred.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route("meta-credentials/find")
|
||||||
|
@Middlewares(authenticateMiddleware)
|
||||||
|
export class MetaWapCredentialsFindController extends Controller {
|
||||||
|
@SuccessResponse(200, "Done")
|
||||||
|
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||||
|
@Post()
|
||||||
|
public async find(
|
||||||
|
@Body() requestBody: FindMetaWapCredentialParams
|
||||||
|
): Promise<MetaWapCredentialView | ApiValidationError> {
|
||||||
|
try {
|
||||||
|
const credential = await new MetaWapCredentialsService().find(requestBody);
|
||||||
|
this.setStatus(200);
|
||||||
|
return credential;
|
||||||
|
} catch (e) {
|
||||||
|
const errorOccurred: Error = e as Error;
|
||||||
|
this.setStatus(500);
|
||||||
|
return new ApiValidationError(500, errorOccurred.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route("meta-credentials/test")
|
||||||
|
@Middlewares([rateLimiter, authenticateMiddleware])
|
||||||
|
export class MetaWapCredentialsTestController extends Controller {
|
||||||
|
@SuccessResponse(200, "Done")
|
||||||
|
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||||
|
@Post()
|
||||||
|
public async test(
|
||||||
|
@Body() requestBody: TestMetaWapCredentialParams
|
||||||
|
): Promise<TestMetaWapCredentialResult | ApiValidationError> {
|
||||||
|
try {
|
||||||
|
const result = await new MetaWapCredentialsService().test(requestBody);
|
||||||
|
this.setStatus(200);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
const errorOccurred: Error = e as Error;
|
||||||
|
this.setStatus(500);
|
||||||
|
return new ApiValidationError(500, errorOccurred.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route("meta-credentials/disable")
|
||||||
|
@Middlewares(authenticateMiddleware)
|
||||||
|
export class MetaWapCredentialsDisableController extends Controller {
|
||||||
|
@SuccessResponse(200, "Done")
|
||||||
|
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||||
|
@Post()
|
||||||
|
public async disable(
|
||||||
|
@Body() requestBody: DisableMetaWapCredentialParams
|
||||||
|
): Promise<MetaWapCredentialView | ApiValidationError> {
|
||||||
|
try {
|
||||||
|
const credential = await new MetaWapCredentialsService().disable(requestBody);
|
||||||
|
this.setStatus(200);
|
||||||
|
return credential;
|
||||||
|
} catch (e) {
|
||||||
|
const errorOccurred: Error = e as Error;
|
||||||
|
this.setStatus(500);
|
||||||
|
return new ApiValidationError(500, errorOccurred.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { connect } from "mongoose";
|
||||||
|
import MetaWapCredentials from "../../Models/MetaWapCredentials/MetaWapCredentials";
|
||||||
|
import {
|
||||||
|
DisableMetaWapCredentialParams,
|
||||||
|
FindMetaWapCredentialParams,
|
||||||
|
MetaWapCredentialView,
|
||||||
|
SaveMetaWapCredentialParams,
|
||||||
|
TestMetaWapCredentialParams,
|
||||||
|
TestMetaWapCredentialResult,
|
||||||
|
} from "../../Models/MetaWapCredentials/MetaWapCredentials.Interface";
|
||||||
|
|
||||||
|
export class MetaWapCredentialsService {
|
||||||
|
public async save(data: SaveMetaWapCredentialParams): Promise<MetaWapCredentialView> {
|
||||||
|
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||||
|
return await MetaWapCredentials.save(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async find(data: FindMetaWapCredentialParams): Promise<MetaWapCredentialView> {
|
||||||
|
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||||
|
return await MetaWapCredentials.find(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async test(data: TestMetaWapCredentialParams): Promise<TestMetaWapCredentialResult> {
|
||||||
|
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||||
|
return await MetaWapCredentials.test(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disable(data: DisableMetaWapCredentialParams): Promise<MetaWapCredentialView> {
|
||||||
|
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||||
|
return await MetaWapCredentials.disable(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Body, Controller, Middlewares, Post, Route, SuccessResponse } from "tso
|
|||||||
import { rateLimiter } from "../../middleware/ratelimiter";
|
import { rateLimiter } from "../../middleware/ratelimiter";
|
||||||
import { systemTokenMiddleware } from "../../middleware/systemToken";
|
import { systemTokenMiddleware } from "../../middleware/systemToken";
|
||||||
import { NotificationsManager } from "../../Models/Notifications/Notifications";
|
import { NotificationsManager } from "../../Models/Notifications/Notifications";
|
||||||
|
import MetaWapCredentialsManager from "../../Models/MetaWapCredentials/MetaWapCredentials";
|
||||||
import { ApiVoidResult } from "../../Models/Api.VoidResult.type";
|
import { ApiVoidResult } from "../../Models/Api.VoidResult.type";
|
||||||
|
|
||||||
interface SendEmailByContentParams {
|
interface SendEmailByContentParams {
|
||||||
@@ -23,6 +24,7 @@ interface SendWapByContentParams {
|
|||||||
|
|
||||||
interface SendWhatsAppTemplateParams {
|
interface SendWhatsAppTemplateParams {
|
||||||
systemToken: string;
|
systemToken: string;
|
||||||
|
companyId: string;
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
templateName: string;
|
templateName: string;
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
@@ -72,45 +74,24 @@ export class SendWapByContentController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TEMPORARY TEST ENDPOINT — remove after testing meta_wap integration
|
|
||||||
* No auth required. Just for curl testing sendWhatsAppTemplate flow.
|
|
||||||
*/
|
|
||||||
@Route("notifications/test-meta-send")
|
|
||||||
export class TestMetaSendController extends Controller {
|
|
||||||
@SuccessResponse(200, "Done")
|
|
||||||
@Post()
|
|
||||||
public async testMetaSend(@Body() requestBody: SendWhatsAppTemplateParams): Promise<ApiVoidResult> {
|
|
||||||
try {
|
|
||||||
await NotificationsManager.sendWhatsAppTemplate({
|
|
||||||
phoneNumber: requestBody.phoneNumber,
|
|
||||||
templateName: requestBody.templateName,
|
|
||||||
languageCode: requestBody.languageCode,
|
|
||||||
templateVariables: requestBody.templateVariables,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setStatus(200);
|
|
||||||
return { message: "Test sent successfully", success: true };
|
|
||||||
} catch (error) {
|
|
||||||
const msg = error instanceof Error ? error.message : String(error);
|
|
||||||
this.setStatus(500);
|
|
||||||
return { message: `Test failed: ${msg}`, success: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Route("notifications/send-whatsapp-template")
|
@Route("notifications/send-whatsapp-template")
|
||||||
@Middlewares([rateLimiter, systemTokenMiddleware])
|
@Middlewares([rateLimiter, systemTokenMiddleware])
|
||||||
export class SendWhatsAppTemplateController extends Controller {
|
export class SendWhatsAppTemplateController extends Controller {
|
||||||
@SuccessResponse(200, "Done")
|
@SuccessResponse(200, "Done")
|
||||||
@Post()
|
@Post()
|
||||||
public async sendWhatsAppTemplate(@Body() requestBody: SendWhatsAppTemplateParams): Promise<ApiVoidResult> {
|
public async sendWhatsAppTemplate(@Body() requestBody: SendWhatsAppTemplateParams): Promise<ApiVoidResult> {
|
||||||
await NotificationsManager.sendWhatsAppTemplate({
|
const credentials = await MetaWapCredentialsManager.getCredentialsForSending(requestBody.companyId);
|
||||||
|
|
||||||
|
await NotificationsManager.sendWhatsAppTemplate(
|
||||||
|
{
|
||||||
|
companyId: requestBody.companyId,
|
||||||
phoneNumber: requestBody.phoneNumber,
|
phoneNumber: requestBody.phoneNumber,
|
||||||
templateName: requestBody.templateName,
|
templateName: requestBody.templateName,
|
||||||
languageCode: requestBody.languageCode,
|
languageCode: requestBody.languageCode,
|
||||||
templateVariables: requestBody.templateVariables,
|
templateVariables: requestBody.templateVariables,
|
||||||
});
|
},
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
|
||||||
this.setStatus(200);
|
this.setStatus(200);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
export enum META_WAP_CREDENTIAL_STATUS {
|
||||||
|
CONFIGURED = "CONFIGURED",
|
||||||
|
TESTED = "TESTED",
|
||||||
|
INVALID = "INVALID",
|
||||||
|
DISABLED = "DISABLED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SaveMetaWapCredentialParams = {
|
||||||
|
companyId: string;
|
||||||
|
phoneNumberId: string;
|
||||||
|
wabaId: string;
|
||||||
|
accessToken: string;
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FindMetaWapCredentialParams = {
|
||||||
|
companyId: string;
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TestMetaWapCredentialParams = {
|
||||||
|
companyId: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
templateName: string;
|
||||||
|
languageCode: string;
|
||||||
|
templateVariables?: string[];
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DisableMetaWapCredentialParams = {
|
||||||
|
companyId: string;
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IMetaWapCredential {
|
||||||
|
id?: string;
|
||||||
|
companyId: string;
|
||||||
|
phoneNumberId: string;
|
||||||
|
wabaId: string;
|
||||||
|
encryptionKeyVersion?: string;
|
||||||
|
status: META_WAP_CREDENTIAL_STATUS;
|
||||||
|
lastTestedAt?: Date;
|
||||||
|
lastTestError?: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MetaWapCredentialView = IMetaWapCredential;
|
||||||
|
|
||||||
|
export type TestMetaWapCredentialResult = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
testedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const META_WAP_CREDENTIAL_NOT_CONFIGURED_MESSAGE =
|
||||||
|
"La organización no tiene credenciales de Meta configuradas";
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
import ApiRequest from "@services/Api.Service";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { ApiError } from "@models/Server.Error.model";
|
||||||
|
import {
|
||||||
|
DisableMetaWapCredentialParams,
|
||||||
|
FindMetaWapCredentialParams,
|
||||||
|
MetaWapCredentialView,
|
||||||
|
SaveMetaWapCredentialParams,
|
||||||
|
TestMetaWapCredentialParams,
|
||||||
|
TestMetaWapCredentialResult,
|
||||||
|
} from "@models/MetaWapCredential.model";
|
||||||
|
|
||||||
|
const baseFields = {
|
||||||
|
companyId: Yup.string().required("No se ha especificado una organización."),
|
||||||
|
sessionUser: Yup.string().required(
|
||||||
|
"No se ha podido determinar el usuario que intenta realizar esta acción"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSchema = Yup.object().shape({
|
||||||
|
...baseFields,
|
||||||
|
phoneNumberId: Yup.string().required("Debe introducir el Phone Number ID."),
|
||||||
|
wabaId: Yup.string().required("Debe introducir el WABA ID."),
|
||||||
|
accessToken: Yup.string().required("Debe introducir el Access Token."),
|
||||||
|
});
|
||||||
|
|
||||||
|
const findSchema = Yup.object().shape({
|
||||||
|
...baseFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
const testSchema = Yup.object().shape({
|
||||||
|
...baseFields,
|
||||||
|
phoneNumber: Yup.string().required("Debe introducir el número de teléfono de destino."),
|
||||||
|
templateName: Yup.string().required("Debe introducir el nombre del template."),
|
||||||
|
languageCode: Yup.string().required("Debe introducir el código de idioma."),
|
||||||
|
});
|
||||||
|
|
||||||
|
const disableSchema = Yup.object().shape({
|
||||||
|
...baseFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateOrThrow = async (
|
||||||
|
validationSchema: Yup.ObjectSchema<Yup.AnyObject>,
|
||||||
|
data: unknown
|
||||||
|
): Promise<void> => {
|
||||||
|
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await validationSchema.validate(data, { abortEarly: true });
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Yup.ValidationError) {
|
||||||
|
yupErrors = error as Yup.ValidationError;
|
||||||
|
}
|
||||||
|
throw new ApiError(400, yupErrors.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const metaWapCredentialSave = async (
|
||||||
|
data: SaveMetaWapCredentialParams
|
||||||
|
): Promise<MetaWapCredentialView> => {
|
||||||
|
await validateOrThrow(saveSchema, data);
|
||||||
|
|
||||||
|
return ApiRequest.post<MetaWapCredentialView>("meta-credentials/save", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const metaWapCredentialFind = async (
|
||||||
|
data: FindMetaWapCredentialParams
|
||||||
|
): Promise<MetaWapCredentialView> => {
|
||||||
|
await validateOrThrow(findSchema, data);
|
||||||
|
|
||||||
|
return ApiRequest.post<MetaWapCredentialView>("meta-credentials/find", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const metaWapCredentialTest = async (
|
||||||
|
data: TestMetaWapCredentialParams
|
||||||
|
): Promise<TestMetaWapCredentialResult> => {
|
||||||
|
await validateOrThrow(testSchema, data);
|
||||||
|
|
||||||
|
return ApiRequest.post<TestMetaWapCredentialResult>("meta-credentials/test", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const metaWapCredentialDisable = async (
|
||||||
|
data: DisableMetaWapCredentialParams
|
||||||
|
): Promise<MetaWapCredentialView> => {
|
||||||
|
await validateOrThrow(disableSchema, data);
|
||||||
|
|
||||||
|
return ApiRequest.post<MetaWapCredentialView>("meta-credentials/disable", data);
|
||||||
|
};
|
||||||
+361
@@ -0,0 +1,361 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import MaterialAlert from "@mui/material/Alert";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import Button from "@core/app/components/Button/Button";
|
||||||
|
import Textbox from "@components/Textbox/Textbox";
|
||||||
|
import { useConfirmStore } from "@core/Store/Confirm.Store";
|
||||||
|
import { useSessionStore } from "@store/Sesion.Store";
|
||||||
|
import { useAlert } from "@store/Alert.Store";
|
||||||
|
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
|
||||||
|
import { ApiError } from "@models/Server.Error.model";
|
||||||
|
import {
|
||||||
|
META_WAP_CREDENTIAL_NOT_CONFIGURED_MESSAGE,
|
||||||
|
META_WAP_CREDENTIAL_STATUS,
|
||||||
|
MetaWapCredentialView,
|
||||||
|
} from "@models/MetaWapCredential.model";
|
||||||
|
import {
|
||||||
|
metaWapCredentialDisable,
|
||||||
|
metaWapCredentialFind,
|
||||||
|
metaWapCredentialSave,
|
||||||
|
metaWapCredentialTest,
|
||||||
|
} from "./MetaWapCredentials.Service";
|
||||||
|
|
||||||
|
export default function MetaWapCredentials({ companyId }: { companyId: string }) {
|
||||||
|
const SessionInfo = useSessionStore();
|
||||||
|
const eventHandler = useEventHandlerStore();
|
||||||
|
const alert = useAlert();
|
||||||
|
const confirm = useConfirmStore();
|
||||||
|
|
||||||
|
const [phoneNumberId, setPhoneNumberId] = useState<string>("");
|
||||||
|
const [wabaId, setWabaId] = useState<string>("");
|
||||||
|
const [accessToken, setAccessToken] = useState<string>("");
|
||||||
|
const [templateName, setTemplateName] = useState<string>("");
|
||||||
|
const [phoneNumber, setPhoneNumber] = useState<string>("");
|
||||||
|
const [languageCode, setLanguageCode] = useState<string>("es");
|
||||||
|
const [templateVariables, setTemplateVariables] = useState<string>("");
|
||||||
|
const [credential, setCredential] = useState<MetaWapCredentialView | undefined>(undefined);
|
||||||
|
|
||||||
|
const formatError = (error: unknown): string => {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
return error.format();
|
||||||
|
}
|
||||||
|
return String(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNotConfiguredError = (error: unknown): boolean => {
|
||||||
|
return (
|
||||||
|
error instanceof ApiError &&
|
||||||
|
error.desc.includes(META_WAP_CREDENTIAL_NOT_CONFIGURED_MESSAGE)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusLabel = (status: META_WAP_CREDENTIAL_STATUS): string => {
|
||||||
|
switch (status) {
|
||||||
|
case META_WAP_CREDENTIAL_STATUS.CONFIGURED:
|
||||||
|
return "Configuradas";
|
||||||
|
case META_WAP_CREDENTIAL_STATUS.TESTED:
|
||||||
|
return "Probadas";
|
||||||
|
case META_WAP_CREDENTIAL_STATUS.INVALID:
|
||||||
|
return "Inválidas";
|
||||||
|
case META_WAP_CREDENTIAL_STATUS.DISABLED:
|
||||||
|
return "Desactivadas";
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusColor = (status: META_WAP_CREDENTIAL_STATUS): string => {
|
||||||
|
if (status === META_WAP_CREDENTIAL_STATUS.TESTED) {
|
||||||
|
return "var(--green-darkestX2)";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
status === META_WAP_CREDENTIAL_STATUS.INVALID ||
|
||||||
|
status === META_WAP_CREDENTIAL_STATUS.DISABLED
|
||||||
|
) {
|
||||||
|
return "var(--error)";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshCredential = (): void => {
|
||||||
|
if (!SessionInfo.userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
metaWapCredentialFind({
|
||||||
|
companyId: companyId,
|
||||||
|
sessionUser: SessionInfo.userId,
|
||||||
|
})
|
||||||
|
.then((view: MetaWapCredentialView) => {
|
||||||
|
setCredential(view);
|
||||||
|
setPhoneNumberId(view.phoneNumberId ?? "");
|
||||||
|
setWabaId(view.wabaId ?? "");
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (isNotConfiguredError(error)) {
|
||||||
|
setCredential(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert.showError(formatError(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!SessionInfo.userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||||
|
metaWapCredentialFind({
|
||||||
|
companyId: companyId,
|
||||||
|
sessionUser: SessionInfo.userId,
|
||||||
|
})
|
||||||
|
.then((view: MetaWapCredentialView) => {
|
||||||
|
setCredential(view);
|
||||||
|
setPhoneNumberId(view.phoneNumberId ?? "");
|
||||||
|
setWabaId(view.wabaId ?? "");
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (isNotConfiguredError(error)) {
|
||||||
|
setCredential(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert.showError(formatError(error));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||||
|
});
|
||||||
|
}, [SessionInfo.userId]);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!SessionInfo.userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||||
|
metaWapCredentialSave({
|
||||||
|
companyId: companyId,
|
||||||
|
phoneNumberId: phoneNumberId,
|
||||||
|
wabaId: wabaId,
|
||||||
|
accessToken: accessToken,
|
||||||
|
sessionUser: SessionInfo.userId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setAccessToken("");
|
||||||
|
alert.showSuccess("Credenciales guardadas correctamente");
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
alert.showError(formatError(error));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
refreshCredential();
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTest = () => {
|
||||||
|
if (!SessionInfo.userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!credential || credential.status === META_WAP_CREDENTIAL_STATUS.DISABLED) {
|
||||||
|
alert.showWarning(
|
||||||
|
"Primero debe configurar las credenciales de Meta para poder enviar un mensaje de prueba."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||||
|
metaWapCredentialTest({
|
||||||
|
companyId: companyId,
|
||||||
|
phoneNumber: phoneNumber,
|
||||||
|
templateName: templateName,
|
||||||
|
languageCode: languageCode,
|
||||||
|
templateVariables: templateVariables
|
||||||
|
? templateVariables
|
||||||
|
.split(",")
|
||||||
|
.map((variable) => variable.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: undefined,
|
||||||
|
sessionUser: SessionInfo.userId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
alert.showSuccess("Mensaje de prueba enviado correctamente");
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
alert.showError(formatError(error));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
refreshCredential();
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisable = () => {
|
||||||
|
confirm.show(
|
||||||
|
"¿Está seguro de desactivar las credenciales de Meta de esta organización?",
|
||||||
|
() => {
|
||||||
|
if (!SessionInfo.userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||||
|
metaWapCredentialDisable({
|
||||||
|
companyId: companyId,
|
||||||
|
sessionUser: SessionInfo.userId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
alert.showSuccess("Credenciales desactivadas correctamente");
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
alert.showError(formatError(error));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
refreshCredential();
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section style={{ marginTop: "20px", padding: "0 16px" }}>
|
||||||
|
<MaterialAlert severity="info" sx={{ marginBottom: "20px" }}>
|
||||||
|
Configure las credenciales de WhatsApp Business (Meta) para poder enviar
|
||||||
|
notificaciones mediante este método.
|
||||||
|
</MaterialAlert>
|
||||||
|
|
||||||
|
<Textbox
|
||||||
|
placeholder="Phone Number ID"
|
||||||
|
type="text"
|
||||||
|
value={phoneNumberId}
|
||||||
|
width="100%"
|
||||||
|
onChange={(e) => {
|
||||||
|
setPhoneNumberId(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Textbox
|
||||||
|
placeholder="WABA ID"
|
||||||
|
type="text"
|
||||||
|
value={wabaId}
|
||||||
|
width="100%"
|
||||||
|
onChange={(e) => {
|
||||||
|
setWabaId(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Textbox
|
||||||
|
placeholder="Access Token"
|
||||||
|
type="password"
|
||||||
|
value={accessToken}
|
||||||
|
width="100%"
|
||||||
|
onChange={(e) => {
|
||||||
|
setAccessToken(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text="Guardar credenciales"
|
||||||
|
width="100%"
|
||||||
|
color="primary"
|
||||||
|
style={{ height: "42px", marginTop: "10px" }}
|
||||||
|
onClick={handleSave}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{credential && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: "20px",
|
||||||
|
padding: "12px 16px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: "1px solid var(--border-color)",
|
||||||
|
backgroundColor: "var(--bg-primary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<b style={{ color: statusColor(credential.status) }}>
|
||||||
|
Estado: {statusLabel(credential.status)}
|
||||||
|
</b>
|
||||||
|
<div>Phone Number ID: {credential.phoneNumberId}</div>
|
||||||
|
<div>WABA ID: {credential.wabaId}</div>
|
||||||
|
{credential.lastTestedAt && (
|
||||||
|
<div>
|
||||||
|
Última prueba:{" "}
|
||||||
|
{dayjs(credential.lastTestedAt).format("DD/MM/YYYY HH:mm")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{credential.lastTestError && (
|
||||||
|
<div style={{ color: "var(--error)" }}>
|
||||||
|
Último error: {credential.lastTestError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{credential.status === META_WAP_CREDENTIAL_STATUS.DISABLED && (
|
||||||
|
<MaterialAlert severity="warning" sx={{ marginTop: "10px" }}>
|
||||||
|
Las credenciales están desactivadas. Guarde nuevamente para
|
||||||
|
reactivarlas.
|
||||||
|
</MaterialAlert>
|
||||||
|
)}
|
||||||
|
{credential.status !== META_WAP_CREDENTIAL_STATUS.DISABLED && (
|
||||||
|
<div style={{ marginTop: "10px" }}>
|
||||||
|
<Button
|
||||||
|
text="Desactivar credenciales"
|
||||||
|
width="100%"
|
||||||
|
color="error"
|
||||||
|
style={{ height: "42px", marginTop: "10px" }}
|
||||||
|
onClick={handleDisable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginTop: "30px" }}>
|
||||||
|
<MaterialAlert severity="info" sx={{ marginBottom: "20px" }}>
|
||||||
|
Envía un mensaje de prueba con las credenciales guardadas.
|
||||||
|
</MaterialAlert>
|
||||||
|
<Textbox
|
||||||
|
placeholder="Número de destino (con código de país, ej 54911...)"
|
||||||
|
type="text"
|
||||||
|
value={phoneNumber}
|
||||||
|
width="100%"
|
||||||
|
onChange={(e) => {
|
||||||
|
setPhoneNumber(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Textbox
|
||||||
|
placeholder="Template (ej: hello_world)"
|
||||||
|
type="text"
|
||||||
|
value={templateName}
|
||||||
|
width="100%"
|
||||||
|
onChange={(e) => {
|
||||||
|
setTemplateName(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Textbox
|
||||||
|
placeholder="Código de idioma (ej: es)"
|
||||||
|
type="text"
|
||||||
|
value={languageCode}
|
||||||
|
width="100%"
|
||||||
|
onChange={(e) => {
|
||||||
|
setLanguageCode(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Textbox
|
||||||
|
placeholder="Variables separadas por coma (opcional)"
|
||||||
|
type="text"
|
||||||
|
value={templateVariables}
|
||||||
|
width="100%"
|
||||||
|
onChange={(e) => {
|
||||||
|
setTemplateVariables(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text="Enviar mensaje de prueba"
|
||||||
|
width="100%"
|
||||||
|
color="success"
|
||||||
|
style={{ height: "42px", marginTop: "10px" }}
|
||||||
|
onClick={handleTest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,11 @@ import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
|
|||||||
import OrgConfigIcon from "@mui/icons-material/SettingsOutlined";
|
import OrgConfigIcon from "@mui/icons-material/SettingsOutlined";
|
||||||
import MaterialAlert from "@mui/material/Alert";
|
import MaterialAlert from "@mui/material/Alert";
|
||||||
import Button from "@core/app/components/Button/Button";
|
import Button from "@core/app/components/Button/Button";
|
||||||
|
import { ThemeProvider } from "@emotion/react";
|
||||||
|
import turnosXpressTheme from "@core/app/theme/turnosXpress";
|
||||||
|
import { ButtonGroup } from "@mui/material";
|
||||||
|
import ButtonMaterial from "@mui/material/Button";
|
||||||
|
import MetaWapCredentials from "./MetaWapCredentials";
|
||||||
import {
|
import {
|
||||||
wapServerAssign,
|
wapServerAssign,
|
||||||
wapServerBotCreate,
|
wapServerBotCreate,
|
||||||
@@ -42,6 +47,7 @@ export default function OrganizationEdit() {
|
|||||||
|
|
||||||
const [wapView, setWapView] = useState<BotView | undefined>(undefined);
|
const [wapView, setWapView] = useState<BotView | undefined>(undefined);
|
||||||
const [qr, setQr] = useState<string | undefined>(undefined);
|
const [qr, setQr] = useState<string | undefined>(undefined);
|
||||||
|
const [activeTab, setActiveTab] = useState<"wap" | "meta">("wap");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
location.clear();
|
location.clear();
|
||||||
@@ -501,7 +507,32 @@ export default function OrganizationEdit() {
|
|||||||
<>
|
<>
|
||||||
<Location />
|
<Location />
|
||||||
|
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", marginTop: "15px" }}>
|
||||||
|
<ThemeProvider theme={turnosXpressTheme}>
|
||||||
|
<ButtonGroup variant="outlined" aria-label="Basic button group">
|
||||||
|
<ButtonMaterial
|
||||||
|
variant={activeTab === "wap" ? "contained" : "outlined"}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab("wap");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
WAP
|
||||||
|
</ButtonMaterial>
|
||||||
|
<ButtonMaterial
|
||||||
|
variant={activeTab === "meta" ? "contained" : "outlined"}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab("meta");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
META
|
||||||
|
</ButtonMaterial>
|
||||||
|
</ButtonGroup>
|
||||||
|
</ThemeProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SecureByPlan content={SECURE_CONTENTS.WAP} companyId={id}>
|
<SecureByPlan content={SECURE_CONTENTS.WAP} companyId={id}>
|
||||||
|
{activeTab === "wap" ? (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: "20px",
|
marginTop: "20px",
|
||||||
@@ -544,6 +575,10 @@ export default function OrganizationEdit() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ marginBottom: "40px" }}> </div>
|
<div style={{ marginBottom: "40px" }}> </div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<MetaWapCredentials companyId={id} />
|
||||||
|
)}
|
||||||
</SecureByPlan>
|
</SecureByPlan>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user