medusa-twilio-sms
Version:
Twilio SMS notification provider for Medusa v2
120 lines (119 loc) • 5.68 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TwilioSmsService = void 0;
const utils_1 = require("@medusajs/framework/utils");
const twilio_1 = __importDefault(require("twilio"));
class TwilioSmsService extends utils_1.AbstractNotificationProviderService {
static identifier = "twilio-sms";
logger_;
twilioClient_;
fromNumber_;
messagingServiceSid_;
maxRetries_;
retryDelay_;
constructor({ logger }, options) {
super();
this.logger_ = logger;
if (!options.account_sid || !options.auth_token) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Twilio SMS plugin requires account_sid and auth_token in options");
}
if (!options.from_number && !options.messaging_service_sid) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Twilio SMS plugin requires either from_number or messaging_service_sid in options");
}
this.twilioClient_ = (0, twilio_1.default)(options.account_sid, options.auth_token);
this.fromNumber_ = options.from_number;
this.messagingServiceSid_ = options.messaging_service_sid;
this.maxRetries_ = options.max_retries || 3;
this.retryDelay_ = options.retry_delay || 1000;
this.logger_.info("Twilio SMS provider initialized successfully");
}
async send(notification) {
if (!notification) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "No notification information provided");
}
const { to, content, data, channel } = notification;
this.logger_.debug(`Attempting to send SMS to ${to}`);
if (!to || typeof to !== "string") {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Invalid or missing recipient phone number");
}
// Validate phone number format (basic E.164 validation)
const phoneRegex = /^\+[1-9]\d{1,14}$/;
if (!phoneRegex.test(to)) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Invalid phone number format: ${to}. Must be in E.164 format (e.g., +1234567890)`);
}
if (!content?.text) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "SMS notification content must contain text");
}
if (channel && channel !== "sms") {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Unsupported channel type: ${channel}. Only 'sms' is supported.`);
}
// Build message options
const messageOptions = {
body: content.text,
to,
};
// Use messaging service SID if available, otherwise use from number
if (this.messagingServiceSid_) {
messageOptions.messagingServiceSid = this.messagingServiceSid_;
}
else if (this.fromNumber_) {
messageOptions.from = this.fromNumber_;
}
// Handle additional data
if (data && typeof data === "object") {
const smsData = data;
if (smsData.mediaUrl && Array.isArray(smsData.mediaUrl)) {
// Validate media URLs
const validatedUrls = smsData.mediaUrl.filter(url => {
try {
new URL(url);
return true;
}
catch {
this.logger_.warn(`Invalid media URL provided: ${url}`);
return false;
}
});
if (validatedUrls.length > 0) {
messageOptions.mediaUrl = validatedUrls;
}
}
}
// Send with retry logic
return await this.sendWithRetry(messageOptions);
}
async sendWithRetry(messageOptions) {
let lastError = null;
for (let attempt = 1; attempt <= this.maxRetries_; attempt++) {
try {
this.logger_.debug(`Sending SMS attempt ${attempt}/${this.maxRetries_}`);
const message = await this.twilioClient_.messages.create(messageOptions);
this.logger_.info(`SMS sent successfully. Message SID: ${message.sid}, Status: ${message.status}`);
return {
id: message.sid,
status: message.status,
dateCreated: message.dateCreated,
dateSent: message.dateSent,
};
}
catch (error) {
lastError = error;
this.logger_.error(`Twilio SMS send error (attempt ${attempt}/${this.maxRetries_}): ${error.message}`);
// Don't retry on validation errors
if (error.code >= 20000 && error.code < 21000) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Twilio validation error: ${error.message}`);
}
// Wait before retrying (except on last attempt)
if (attempt < this.maxRetries_) {
await new Promise(resolve => setTimeout(resolve, this.retryDelay_ * attempt));
}
}
}
// All retries failed
throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, `Failed to send SMS via Twilio after ${this.maxRetries_} attempts: ${lastError?.message}`);
}
}
exports.TwilioSmsService = TwilioSmsService;