import twilio from 'twilio'; import { loadNotificationConfig } from '../config/notification.config'; import type { SMSNotification, NotificationResult } from '../types/notification.types'; const config = loadNotificationConfig(); const twilioClient = twilio( config.twilio.accountSid, config.twilio.authToken ); export class SMSService { private static instance: SMSService; private sentCount = new Map(); private cleanupInterval: NodeJS.Timeout; private constructor() { this.cleanupInterval = setInterval(() => { const now = Date.now(); for (const [key, timestamp] of this.sentCount.entries()) { if (now - timestamp > 60000) { this.sentCount.delete(key); } } }, 60000); } static getInstance(): SMSService { if (!SMSService.instance) { SMSService.instance = new SMSService(); } return SMSService.instance; } async send(notification: SMSNotification): Promise { const rateLimitKey = `sms:${notification.to}`; const currentCount = this.sentCount.get(rateLimitKey) || 0; if (currentCount >= config.rateLimits.smsPerMinute) { throw new Error(`SMS rate limit exceeded for ${notification.to}`); } try { const message = await twilioClient.messages.create({ body: notification.body, from: notification.from || config.twilio.messagingServiceSid, to: notification.to, metadata: notification.metadata, }); this.sentCount.set(rateLimitKey, currentCount + 1); return { notificationId: `sms-${message.sid}`, channel: 'sms', status: 'sent', externalId: message.sid, deliveredAt: new Date(), }; } catch (error) { return { notificationId: `sms-${Date.now()}`, channel: 'sms', status: 'failed', error: error instanceof Error ? error.message : 'Unknown error', }; } } async sendBatch(notifications: SMSNotification[]): Promise { const results = await Promise.all( notifications.map(n => this.send(n)) ); return results; } getRateLimitStatus(): { remaining: number; limit: number } { return { remaining: config.rateLimits.smsPerMinute - this.sentCount.size, limit: config.rateLimits.smsPerMinute, }; } }