diff --git a/packages/core/types/src/notification/providers/index.ts b/packages/core/types/src/notification/providers/index.ts index e0069c9d0d0ca..bd4c5f88b1623 100644 --- a/packages/core/types/src/notification/providers/index.ts +++ b/packages/core/types/src/notification/providers/index.ts @@ -1,2 +1,3 @@ export * from "./logger" export * from "./sendgrid" +export * from "./twilio" diff --git a/packages/core/types/src/notification/providers/twilio.ts b/packages/core/types/src/notification/providers/twilio.ts new file mode 100644 index 0000000000000..a418fe1855191 --- /dev/null +++ b/packages/core/types/src/notification/providers/twilio.ts @@ -0,0 +1,6 @@ +export interface TwilioNotificationServiceOptions { + account_sid: string + auth_token: string + from?: string + messaging_service_sid?: string +} diff --git a/packages/medusa/src/modules/notification-twilio.ts b/packages/medusa/src/modules/notification-twilio.ts new file mode 100644 index 0000000000000..537445f5191f7 --- /dev/null +++ b/packages/medusa/src/modules/notification-twilio.ts @@ -0,0 +1,6 @@ +import TwilioNotificationProvider from "@medusajs/notification-twilio" + +export * from "@medusajs/notification-twilio" + +export default TwilioNotificationProvider +export const discoveryPath = require.resolve("@medusajs/notification-twilio") diff --git a/packages/modules/providers/notification-sendgrid/src/services/sendgrid.ts b/packages/modules/providers/notification-sendgrid/src/services/sendgrid.ts index 23b80d299481b..f1ad38cf0afa5 100644 --- a/packages/modules/providers/notification-sendgrid/src/services/sendgrid.ts +++ b/packages/modules/providers/notification-sendgrid/src/services/sendgrid.ts @@ -75,7 +75,7 @@ export class SendgridNotificationService extends AbstractNotificationProviderSer } else { // we can't mix html and templates for sendgrid mailContent = { - templateId: notification.template, + templateId: notification.template || "", } } diff --git a/packages/modules/providers/notification-twilio/.gitignore b/packages/modules/providers/notification-twilio/.gitignore new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/modules/providers/notification-twilio/jest.config.js b/packages/modules/providers/notification-twilio/jest.config.js new file mode 100644 index 0000000000000..818699559a62f --- /dev/null +++ b/packages/modules/providers/notification-twilio/jest.config.js @@ -0,0 +1,10 @@ +const defineJestConfig = require("../../../../define_jest_config") +module.exports = defineJestConfig({ + moduleNameMapper: { + "^@models": "/src/models", + "^@services": "/src/services", + "^@repositories": "/src/repositories", + "^@types": "/src/types", + "^@utils": "/src/utils", + }, +}) diff --git a/packages/modules/providers/notification-twilio/package.json b/packages/modules/providers/notification-twilio/package.json new file mode 100644 index 0000000000000..6e41c648aadc3 --- /dev/null +++ b/packages/modules/providers/notification-twilio/package.json @@ -0,0 +1,46 @@ +{ + "name": "@medusajs/notification-twilio", + "version": "0.0.1", + "description": "Twilio notification provider for Medusa", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/modules/providers/notification-twilio" + }, + "files": [ + "dist", + "!dist/**/__tests__", + "!dist/**/__mocks__", + "!dist/**/__fixtures__" + ], + "engines": { + "node": ">=20" + }, + "author": "Medusa", + "license": "MIT", + "scripts": { + "test": "jest --passWithNoTests src", + "test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.spec.ts", + "build": "rimraf dist && tsc --build ./tsconfig.json", + "watch": "tsc --watch" + }, + "devDependencies": { + "@medusajs/framework": "^0.0.1", + "@swc/core": "^1.7.28", + "@swc/jest": "^0.2.36", + "jest": "^29.7.0", + "rimraf": "^5.0.1", + "typescript": "^5.6.2" + }, + "dependencies": { + "twilio": "^5.3.3" + }, + "peerDependencies": { + "@medusajs/framework": "^0.0.1" + }, + "keywords": [ + "medusa-provider", + "medusa-provider-twilio" + ] +} diff --git a/packages/modules/providers/notification-twilio/src/index.ts b/packages/modules/providers/notification-twilio/src/index.ts new file mode 100644 index 0000000000000..16187404f35fe --- /dev/null +++ b/packages/modules/providers/notification-twilio/src/index.ts @@ -0,0 +1,8 @@ +import { ModuleProvider, Modules } from "@medusajs/framework/utils" +import { TwilioNotificationService } from "./services/twilio" + +const services = [TwilioNotificationService] + +export default ModuleProvider(Modules.NOTIFICATION, { + services, +}) diff --git a/packages/modules/providers/notification-twilio/src/services/twilio.ts b/packages/modules/providers/notification-twilio/src/services/twilio.ts new file mode 100644 index 0000000000000..f7ce224589e17 --- /dev/null +++ b/packages/modules/providers/notification-twilio/src/services/twilio.ts @@ -0,0 +1,124 @@ +import { + Logger, + NotificationTypes, + TwilioNotificationServiceOptions, +} from "@medusajs/framework/types" +import { + AbstractNotificationProviderService, + MedusaError, +} from "@medusajs/framework/utils" +import Twilio from "twilio" + +type InjectedDependencies = { + logger: Logger +} + +type SmsContent = Required< + Omit +> + +interface TwilioServiceConfig { + accountSid: string + authToken: string + from?: string + messagingServiceSid?: string +} + +export class TwilioNotificationService extends AbstractNotificationProviderService { + static identifier = "notification-twilio" + protected config_: TwilioServiceConfig + protected logger_: Logger + protected client_: Twilio.Twilio + + constructor( + { logger }: InjectedDependencies, + options: TwilioNotificationServiceOptions + ) { + super() + + this.config_ = { + accountSid: options.account_sid, + authToken: options.auth_token, + from: options.from, + messagingServiceSid: options.messaging_service_sid, + } + this.logger_ = logger + this.client_ = Twilio(this.config_.accountSid, this.config_.authToken) + } + /** + * Sends a notification via Twilio SMS (or MMS if mediaUrls are provided) + * @param notification the notification data + * @returns a promise that resolves when the message is sent + */ + + async send( + notification: NotificationTypes.ProviderSendNotificationDTO + ): Promise { + if (!notification) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "No notification information provided" + ) + } + const content = notification.content as SmsContent + + if (!content?.text) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Message body (content.text) is required for SMS` + ) + } + + const mediaUrls = notification.data?.mediaUrls as string[] | undefined + const fromNumber = notification.from?.trim() || this.config_.from + const messagingService = + (notification.data?.messagingServiceSid as string) || + this.config_.messagingServiceSid + + if (!notification.to) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Recipient to (#) is required" + ) + } + + if (!fromNumber && !messagingService) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Either 'from' number or 'messagingServiceSid' must be provided` + ) + } + const smsData = { + to: notification.to, + body: content.text, + from: fromNumber || undefined, + messagingServiceSid: messagingService || undefined, + mediaUrl: mediaUrls && mediaUrls.length > 0 ? mediaUrls : undefined, + } + + try { + const message = await this.client_.messages.create(smsData) + const messageBody = + typeof message.body === "string" + ? JSON.parse(message.body) + : message.body + if (messageBody.error_code) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + `Failed to send SMS: ${messageBody.error_code} - ${messageBody.error_message}` + ) + } + + return { id: message.sid } + } catch (error: any) { + const errorCode = error.code + const responseError = error.response?.body?.errors?.[0] + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + `Failed to send SMS: ${errorCode} - ${ + responseError?.message ?? "unknown error" + }` + ) + } + } +} diff --git a/packages/modules/providers/notification-twilio/tsconfig.json b/packages/modules/providers/notification-twilio/tsconfig.json new file mode 100644 index 0000000000000..90f3a70b383ef --- /dev/null +++ b/packages/modules/providers/notification-twilio/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../_tsconfig.base.json", + "compilerOptions": { + "paths": { + "@models": ["./src/models"], + "@services": ["./src/services"], + "@repositories": ["./src/repositories"], + "@types": ["./src/types"], + "@utils": ["./src/utils"] + } + } +}