import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as sgMail from '@sendgrid/mail'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; export interface NotificationChannel { send(recipient: string, subject: string, body: string, context?: Record): Promise; } @Injectable() export class EmailAdapter implements NotificationChannel { private readonly logger = new Logger(EmailAdapter.name); constructor(private readonly config: ConfigService) { const apiKey = this.config.get('SENDGRID_API_KEY'); if (apiKey) { sgMail.setApiKey(apiKey); this.logger.log('SendGrid Email adapter initialized'); } else { this.logger.warn('SENDGRID_API_KEY not configured - emails will be logged only'); } } async send( recipient: string, subject: string, body: string, context?: Record, ): Promise { const apiKey = this.config.get('SENDGRID_API_KEY'); const fromEmail = this.config.get('SENDGRID_FROM_EMAIL') || 'noreply@edr-platform.com'; if (!apiKey) { this.logger.log(`[EMAIL MOCK] To: ${recipient} | Subject: ${subject} | Body: ${body.substring(0, 100)}`); return true; } try { const msg: sgMail.MailDataRequired = { to: recipient, from: fromEmail, subject, text: body, html: this.formatHtml(body, context), }; await sgMail.send(msg); this.logger.log(`Email sent successfully to ${recipient}`); return true; } catch (err) { const message = err instanceof Error ? err.message : String(err); this.logger.error(`Failed to send email to ${recipient}: ${message}`); return false; } } private formatHtml(body: string, context?: Record): string { const contextHtml = context ? `
${JSON.stringify(context, null, 2)}
` : ''; return `

Ethio-Djibouti Railway

${body.replace(/\n/g, '
')} ${contextHtml}
`; } } @Injectable() export class SmsAdapter implements NotificationChannel { private readonly logger = new Logger(SmsAdapter.name); constructor( private readonly config: ConfigService, private readonly http: HttpService, ) { const provider = this.config.get('SMS_PROVIDER'); this.logger.log(`SMS adapter initialized with provider: ${provider || 'MOCK'}`); } async send( recipient: string, subject: string, body: string, _context?: Record, ): Promise { const provider = this.config.get('SMS_PROVIDER'); const apiKey = this.config.get('SMS_API_KEY'); if (!provider || !apiKey) { this.logger.log(`[SMS MOCK] To: ${recipient} | Message: ${body.substring(0, 100)}`); return true; } try { switch (provider.toLowerCase()) { case 'twilio': return await this.sendViaTwilio(recipient, body); case 'africastalking': return await this.sendViaAfricasTalking(recipient, body); default: this.logger.warn(`Unknown SMS provider: ${provider}`); return false; } } catch (err) { const message = err instanceof Error ? err.message : String(err); this.logger.error(`Failed to send SMS to ${recipient}: ${message}`); return false; } } private async sendViaTwilio(to: string, body: string): Promise { const accountSid = this.config.get('TWILIO_ACCOUNT_SID'); const authToken = this.config.get('TWILIO_AUTH_TOKEN'); const fromNumber = this.config.get('TWILIO_FROM_NUMBER'); const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`; const auth = Buffer.from(`${accountSid}:${authToken}`).toString('base64'); const response = await firstValueFrom( this.http.post( url, new URLSearchParams({ To: to, From: fromNumber || '', Body: body, }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Basic ${auth}`, }, }, ), ); return response.status === 201; } private async sendViaAfricasTalking(to: string, body: string): Promise { const apiKey = this.config.get('SMS_API_KEY'); const username = this.config.get('AFRICASTALKING_USERNAME'); const from = this.config.get('AFRICASTALKING_FROM'); const url = 'https://api.africastalking.com/version1/messaging'; const response = await firstValueFrom( this.http.post( url, new URLSearchParams({ username: username || '', to, message: body, from: from || '', }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'apiKey': apiKey || '', }, }, ), ); return response.status === 201; } } @Injectable() export class PushAdapter implements NotificationChannel { private readonly logger = new Logger(PushAdapter.name); constructor(private readonly config: ConfigService) { this.logger.log('Push notification adapter initialized'); } async send( recipient: string, subject: string, body: string, context?: Record, ): Promise { // Push notifications would typically use FCM/APNS // For now, just log this.logger.log(`[PUSH MOCK] To: ${recipient} | Title: ${subject} | Body: ${body.substring(0, 100)}`); return true; } }