diff --git a/apps/api/src/app/integrations/dtos/credentials.dto.ts b/apps/api/src/app/integrations/dtos/credentials.dto.ts
index 85dab4d0dad..3af99ef9414 100644
--- a/apps/api/src/app/integrations/dtos/credentials.dto.ts
+++ b/apps/api/src/app/integrations/dtos/credentials.dto.ts
@@ -236,4 +236,9 @@ export class CredentialsDto implements ICredentials {
@IsOptional()
@IsString()
AppIOBaseUrl?: string;
+
+ @ApiPropertyOptional()
+ @IsString()
+ @IsOptional()
+ accessSecret?: string;
}
diff --git a/apps/dashboard/public/images/providers/light/square/maqsam-whatsapp.svg b/apps/dashboard/public/images/providers/light/square/maqsam-whatsapp.svg
new file mode 100644
index 00000000000..4c3a53a2b19
--- /dev/null
+++ b/apps/dashboard/public/images/providers/light/square/maqsam-whatsapp.svg
@@ -0,0 +1,10 @@
+
diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts
index 46b958797f8..97da403965f 100644
--- a/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts
+++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts
@@ -297,15 +297,23 @@ export class SendMessageChat extends SendMessageBase {
Object.values(ChatProviderIdEnum).includes(chan.providerId as ChatProviderIdEnum)
) || [];
- // Add WhatsApp Business if subscriber has phone
+ // Add WhatsApp APIs if subscriber has phone
if (subscriber.phone) {
- // @ts-expect-error - Adding WhatsApp channel without _integrationId
+ // @ts-expect-error - Adding WhatsApp Business channel without _integrationId
chatChannels.push({
providerId: ChatProviderIdEnum.WhatsAppBusiness,
credentials: {
phoneNumber: subscriber.phone,
},
});
+
+ // @ts-expect-error - Adding Maqsam WhatsApp channel without _integrationId
+ chatChannels.push({
+ providerId: ChatProviderIdEnum.MaqsamWhatsApp,
+ credentials: {
+ phoneNumber: subscriber.phone,
+ },
+ })
}
return chatChannels;
diff --git a/libs/application-generic/src/factories/chat/chat.factory.ts b/libs/application-generic/src/factories/chat/chat.factory.ts
index 0de0fac8e39..0f6588e0ff9 100644
--- a/libs/application-generic/src/factories/chat/chat.factory.ts
+++ b/libs/application-generic/src/factories/chat/chat.factory.ts
@@ -3,6 +3,7 @@ import { ChatWebhookHandler } from './handlers/chat-webhook.handler';
import { DiscordHandler } from './handlers/discord.handler';
import { GetstreamChatHandler } from './handlers/getstream.handler';
import { GrafanaOnCallHandler } from './handlers/grafana-on-call.handler';
+import { MaqsamWhatsAppHandler } from './handlers/maqsam.handler';
import { MattermostHandler } from './handlers/mattermost.handler';
import { MSTeamsHandler } from './handlers/msteams.handler';
import { NovuSlackHandler } from './handlers/novu-slack.handler';
@@ -27,6 +28,7 @@ export class ChatFactory implements IChatFactory {
new GetstreamChatHandler(),
new RocketChatHandler(),
new WhatsAppBusinessHandler(),
+ new MaqsamWhatsAppHandler(),
];
getHandler(integration: Pick) {
diff --git a/libs/application-generic/src/factories/chat/handlers/maqsam.handler.ts b/libs/application-generic/src/factories/chat/handlers/maqsam.handler.ts
new file mode 100644
index 00000000000..4ea7de17c13
--- /dev/null
+++ b/libs/application-generic/src/factories/chat/handlers/maqsam.handler.ts
@@ -0,0 +1,15 @@
+import { MaqsamWhatsAppProvider } from '@novu/providers';
+import { ChannelTypeEnum, ChatProviderIdEnum, ICredentials } from '@novu/shared';
+import { BaseChatHandler } from './base.handler';
+
+export class MaqsamWhatsAppHandler extends BaseChatHandler {
+ constructor() {
+ super(ChatProviderIdEnum.MaqsamWhatsApp, ChannelTypeEnum.CHAT);
+ }
+
+ buildProvider(credentials: ICredentials) {
+ const config: { accessKey: string, accessSecret: string } = { accessKey: credentials.accessKey, accessSecret: credentials.accessSecret };
+
+ this.provider = new MaqsamWhatsAppProvider(config);
+ }
+}
diff --git a/libs/dal/src/repositories/integration/integration.schema.ts b/libs/dal/src/repositories/integration/integration.schema.ts
index 43541996a20..c5b389ed793 100644
--- a/libs/dal/src/repositories/integration/integration.schema.ts
+++ b/libs/dal/src/repositories/integration/integration.schema.ts
@@ -70,6 +70,7 @@ const integrationSchema = new Schema(
AppIOSubscriptionId: Schema.Types.String,
AppIOBearerToken: Schema.Types.String,
AppIOOriginalSignature: Schema.Types.String,
+ accessSecret: Schema.Types.String,
},
configurations: {
inboundWebhookEnabled: Schema.Types.Boolean,
diff --git a/libs/internal-sdk/src/models/components/providersidenum.ts b/libs/internal-sdk/src/models/components/providersidenum.ts
index 79f4817777f..bb292fdf9cf 100644
--- a/libs/internal-sdk/src/models/components/providersidenum.ts
+++ b/libs/internal-sdk/src/models/components/providersidenum.ts
@@ -87,6 +87,7 @@ export const ProvidersIdEnum = {
WhatsappBusiness: "whatsapp-business",
ChatWebhook: "chat-webhook",
NovuSlack: "novu-slack",
+ MaqsamWhatsApp: 'maqsam-whatsapp',
} as const;
/**
* Provider ID of the job
diff --git a/packages/framework/src/schemas/providers/chat/index.ts b/packages/framework/src/schemas/providers/chat/index.ts
index 9d341f1868a..308c03e011c 100644
--- a/packages/framework/src/schemas/providers/chat/index.ts
+++ b/packages/framework/src/schemas/providers/chat/index.ts
@@ -15,4 +15,5 @@ export const chatProviderSchemas = {
slack: slackProviderSchemas,
'whatsapp-business': genericProviderSchemas,
zulip: genericProviderSchemas,
+ 'maqsam-whatsapp': genericProviderSchemas,
} as const satisfies Record;
diff --git a/packages/framework/src/shared.ts b/packages/framework/src/shared.ts
index 0d0294fd149..7444c36aca6 100644
--- a/packages/framework/src/shared.ts
+++ b/packages/framework/src/shared.ts
@@ -175,6 +175,7 @@ export enum ChatProviderIdEnum {
RocketChat = 'rocket-chat',
WhatsAppBusiness = 'whatsapp-business',
ChatWebhook = 'chat-webhook',
+ MaqsamWhatsApp = 'maqsam-whatsapp',
}
export enum PushProviderIdEnum {
diff --git a/packages/providers/src/lib/chat/index.ts b/packages/providers/src/lib/chat/index.ts
index cd5072c0e69..08130b1e750 100644
--- a/packages/providers/src/lib/chat/index.ts
+++ b/packages/providers/src/lib/chat/index.ts
@@ -2,6 +2,7 @@ export * from './chat-webhook/chat-webhook.provider';
export * from './discord/discord.provider';
export * from './getstream/getstream.provider';
export * from './grafana-on-call/grafana-on-call.provider';
+export * from './maqsam-whatsapp/maqsam-whatsapp.provider';
export * from './mattermost/mattermost.provider';
export * from './msTeams/msTeams.provider';
export * from './rocket-chat/rocket-chat.provider';
diff --git a/packages/providers/src/lib/chat/maqsam-whatsapp/maqsam-whatsapp.provider.spec.ts b/packages/providers/src/lib/chat/maqsam-whatsapp/maqsam-whatsapp.provider.spec.ts
new file mode 100644
index 00000000000..ff438b71822
--- /dev/null
+++ b/packages/providers/src/lib/chat/maqsam-whatsapp/maqsam-whatsapp.provider.spec.ts
@@ -0,0 +1,140 @@
+import { IChatOptions } from '@novu/stateless';
+import { nanoid } from 'nanoid';
+import { expect, test } from 'vitest';
+import { axiosSpy } from '../../../utils/test/spy-axios';
+import { MaqsamWhatsAppProvider } from './maqsam-whatsapp.provider';
+
+const mockProviderConfig = {
+ accessKey: 'my-access-key',
+ accessSecret: 'my-access-secret',
+};
+
+const buildResponse = (messageId: string) => {
+ return {
+ data: {
+ message_id: messageId,
+ message_status: 'submitted',
+ },
+ };
+};
+
+test('should trigger maqsam library correctly with template message', async () => {
+ const messageId = nanoid();
+
+ const { mockPost, axiosMockSpy } = axiosSpy(buildResponse(messageId));
+
+ const provider = new MaqsamWhatsAppProvider(mockProviderConfig);
+
+ const options: IChatOptions = {
+ phoneNumber: '+111111111',
+ content: 'Template message',
+ customData: {
+ templateId: '123',
+ templateVariables: {
+ name: 'John',
+ company: 'Acme',
+ },
+ },
+ };
+
+ const res = await provider.sendMessage(options);
+
+ expect(mockPost).toHaveBeenCalled();
+ expect(mockPost).toHaveBeenCalledWith('https://api.maqsam.com/v2/whatsapp/messages/send_message', {
+ RecipientPhone: options.phoneNumber,
+ TemplateId: options.customData?.templateId,
+ TemplateVariables: options.customData?.templateVariables,
+ });
+
+ const token = Buffer.from(`${mockProviderConfig.accessKey}:${mockProviderConfig.accessSecret}`).toString('base64');
+
+ expect(axiosMockSpy).toHaveBeenCalledWith({
+ headers: {
+ Authorization: `Basic ${token}`,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+
+ expect(res.id).toBe(messageId);
+});
+
+test('should trigger maqsam library correctly with template message with _passthrough', async () => {
+ const messageId = nanoid();
+
+ const { mockPost, axiosMockSpy } = axiosSpy(buildResponse(messageId));
+
+ const provider = new MaqsamWhatsAppProvider(mockProviderConfig);
+
+ const options: IChatOptions = {
+ phoneNumber: '+111111111',
+ content: 'Template message',
+ customData: {
+ templateId: '123',
+ templateVariables: {
+ name: 'John',
+ company: 'Acme',
+ },
+ },
+ };
+
+ const res = await provider.sendMessage(options, {
+ _passthrough: {
+ query: {
+ RecipientPhone: '+111111111',
+ TemplateId: '123',
+ TemplateVariables: JSON.stringify({
+ name: 'John',
+ company: 'Acme',
+ }),
+ },
+ },
+ });
+
+ expect(mockPost).toHaveBeenCalled();
+ expect(mockPost).toHaveBeenCalledWith('https://api.maqsam.com/v2/whatsapp/messages/send_message', {
+ RecipientPhone: '+111111111',
+ TemplateId: '123',
+ TemplateVariables: {
+ name: 'John',
+ company: 'Acme',
+ },
+ });
+
+ const token = Buffer.from(`${mockProviderConfig.accessKey}:${mockProviderConfig.accessSecret}`).toString('base64');
+
+ expect(axiosMockSpy).toHaveBeenCalledWith({
+ headers: {
+ Authorization: `Basic ${token}`,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+
+ expect(res.id).toBe(messageId);
+});
+
+test('should throw error when message status is not submitted', async () => {
+ const messageId = nanoid();
+
+ axiosSpy({
+ data: {
+ message_id: messageId,
+ message_status: 'failed',
+ },
+ });
+
+ const provider = new MaqsamWhatsAppProvider(mockProviderConfig);
+
+ const options: IChatOptions = {
+ phoneNumber: '+111111111',
+ content: 'Template message',
+ customData: {
+ templateId: '123',
+ templateVariables: {
+ name: 'John',
+ company: 'Acme',
+ },
+ },
+ };
+
+ await expect(provider.sendMessage(options)).rejects.toThrow('Maqsam Chat failed:');
+});
diff --git a/packages/providers/src/lib/chat/maqsam-whatsapp/maqsam-whatsapp.provider.ts b/packages/providers/src/lib/chat/maqsam-whatsapp/maqsam-whatsapp.provider.ts
new file mode 100644
index 00000000000..e9b8f758b2f
--- /dev/null
+++ b/packages/providers/src/lib/chat/maqsam-whatsapp/maqsam-whatsapp.provider.ts
@@ -0,0 +1,64 @@
+import {
+ ChannelTypeEnum,
+ IChatOptions,
+ IChatProvider,
+ ISendMessageSuccessResponse,
+ PhoneData,
+} from "@novu/stateless";
+import Axios, { AxiosInstance } from 'axios';
+import { BaseProvider, CasingEnum } from "../../../base.provider";
+import { WithPassthrough } from "../../../utils/types";
+import { ISendMessageFailureRes, ISendMessageRes } from "./types/maqsam-whatsapp.types";
+
+export class MaqsamWhatsAppProvider extends BaseProvider implements IChatProvider {
+ id = "maqsam-whatsapp";
+ channelType = ChannelTypeEnum.CHAT as ChannelTypeEnum.CHAT;
+ protected casing: CasingEnum = CasingEnum.CAMEL_CASE;
+
+ private readonly axiosClient: AxiosInstance;
+ private readonly baseUrl = 'https://api.maqsam.com/v2/whatsapp/messages/send_message';
+
+ constructor(private config: {
+ accessKey: string;
+ accessSecret: string;
+ }) {
+ super();
+
+ const token = Buffer.from(`${this.config.accessKey}:${this.config.accessSecret}`).toString('base64');
+
+ this.axiosClient = Axios.create({
+ headers: {
+ Authorization: `Basic ${token}`,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+ }
+
+ private defineMessagePayload(options: IChatOptions) {
+ return {
+ RecipientPhone: (options.channelData as PhoneData).endpoint.phoneNumber,
+ TemplateId: options.customData?.templateId,
+ TemplateVariables: options.customData?.templateVariables,
+ }
+ }
+
+ async sendMessage(
+ options: IChatOptions,
+ bridgeProviderData: WithPassthrough> = {},
+ ): Promise {
+ const payload = this.transform(bridgeProviderData, this.defineMessagePayload(options));
+
+ const { data } = await this.axiosClient.post(this.baseUrl,
+ payload.body,
+ );
+
+ if ('conversationId' in data && data.conversationId) {
+ return {
+ id: data.conversationId,
+ date: new Date().toISOString(),
+ };
+ }
+
+ throw new Error(`Maqsam Chat failed: ${JSON.stringify(data || {})}`);
+ }
+}
diff --git a/packages/providers/src/lib/chat/maqsam-whatsapp/types/maqsam-whatsapp.types.ts b/packages/providers/src/lib/chat/maqsam-whatsapp/types/maqsam-whatsapp.types.ts
new file mode 100644
index 00000000000..4227d36f9e0
--- /dev/null
+++ b/packages/providers/src/lib/chat/maqsam-whatsapp/types/maqsam-whatsapp.types.ts
@@ -0,0 +1,10 @@
+export interface ISendMessageRes {
+ conversationId: string;
+ messageStatus: string;
+ result: string;
+}
+
+export interface ISendMessageFailureRes {
+ error: string
+ message: string
+}
diff --git a/packages/shared/src/consts/providers/channels/chat.ts b/packages/shared/src/consts/providers/channels/chat.ts
index 649a981dd94..5cc4ad6c721 100644
--- a/packages/shared/src/consts/providers/channels/chat.ts
+++ b/packages/shared/src/consts/providers/channels/chat.ts
@@ -4,6 +4,7 @@ import {
chatWebhookConfig,
getstreamConfig,
grafanaOnCallConfig,
+ maqsamWhatsAppChatConfig,
msTeamsConfig,
rocketChatConfig,
slackConfigLegacy,
@@ -109,4 +110,13 @@ export const chatProviders: IProviderConfig[] = [
logoFileName: { light: 'chat-webhook.svg', dark: 'chat-webhook.svg' },
betaVersion: true,
},
+ {
+ id: ChatProviderIdEnum.MaqsamWhatsApp,
+ displayName: 'Maqsam WhatsApp',
+ channel: ChannelTypeEnum.CHAT,
+ credentials: maqsamWhatsAppChatConfig,
+ docReference: 'https://portal.maqsam.com/docs/v2/whatsapp',
+ logoFileName: { light: 'maqsam-whatsapp.svg', dark: 'maqsam-whatsapp.svg' },
+ betaVersion: true,
+ }
];
diff --git a/packages/shared/src/consts/providers/credentials/provider-credentials.ts b/packages/shared/src/consts/providers/credentials/provider-credentials.ts
index d8cda74f68b..123b7e63c05 100644
--- a/packages/shared/src/consts/providers/credentials/provider-credentials.ts
+++ b/packages/shared/src/consts/providers/credentials/provider-credentials.ts
@@ -926,6 +926,21 @@ export const maqsamConfig: IConfigCredential[] = [
...smsConfigBase,
];
+export const maqsamWhatsAppChatConfig: IConfigCredential[] = [
+ {
+ key: CredentialsKeyEnum.AccessKey,
+ displayName: 'Access Key',
+ type: 'string',
+ required: true,
+ },
+ {
+ key: CredentialsKeyEnum.AccessSecret,
+ displayName: 'Access Secret',
+ type: 'string',
+ required: true,
+ }
+];
+
export const smsCentralConfig: IConfigCredential[] = [
{
key: CredentialsKeyEnum.User,
diff --git a/packages/shared/src/consts/providers/credentials/secure-credentials.ts b/packages/shared/src/consts/providers/credentials/secure-credentials.ts
index ea9eceae8c2..b0b3ae38469 100644
--- a/packages/shared/src/consts/providers/credentials/secure-credentials.ts
+++ b/packages/shared/src/consts/providers/credentials/secure-credentials.ts
@@ -7,4 +7,5 @@ export const secureCredentials: CredentialsKeyEnum[] = [
CredentialsKeyEnum.Token,
CredentialsKeyEnum.Password,
CredentialsKeyEnum.ServiceAccount,
+ CredentialsKeyEnum.AccessSecret
];
diff --git a/packages/shared/src/entities/integration/credential.interface.ts b/packages/shared/src/entities/integration/credential.interface.ts
index d19a1d85d11..0ce5f1a5cb3 100644
--- a/packages/shared/src/entities/integration/credential.interface.ts
+++ b/packages/shared/src/entities/integration/credential.interface.ts
@@ -53,4 +53,5 @@ export interface ICredentials {
AppIOOriginalSignature?: string;
servicePlanId?: string;
tenantId?: string;
+ accessSecret?: string;
}
diff --git a/packages/shared/src/types/providers.ts b/packages/shared/src/types/providers.ts
index 7516c6159d0..c82cc8ef001 100644
--- a/packages/shared/src/types/providers.ts
+++ b/packages/shared/src/types/providers.ts
@@ -52,6 +52,7 @@ export enum CredentialsKeyEnum {
AppIOBaseUrl = 'AppIOBaseUrl',
ServicePlanId = 'servicePlanId',
TenantId = 'tenantId',
+ AccessSecret = 'accessSecret'
}
export type ConfigurationKey = keyof IConfigurations;
@@ -135,6 +136,7 @@ export enum ChatProviderIdEnum {
WhatsAppBusiness = 'whatsapp-business',
ChatWebhook = 'chat-webhook',
Novu = 'novu-slack',
+ MaqsamWhatsApp = 'maqsam-whatsapp',
}
export enum PushProviderIdEnum {