FRE-4495: Set up notification infrastructure (email, push, SMS)
- Created shared-notifications package with multi-channel support
- Implemented EmailService with Resend integration
- Implemented PushService with FCM/APNs support
- Implemented SMSService with Twilio integration
- Added NotificationService to orchestrate all channels
- Created notification types, configuration, and routes
- Added rate limiting and delivery tracking support
- Configured notification preferences management
Files:
- packages/shared-notifications/src/{types,config,services}/*.ts
- packages/shared-notifications/package.json
- apps/api/src/routes/notifications.routes.ts
- apps/api/package.json (updated dependencies)
This commit is contained in:
170
packages/shared-notifications/src/services/email.service.ts
Normal file
170
packages/shared-notifications/src/services/email.service.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { Resend } from 'resend';
|
||||
import { ResendConfig } from '../config/notification.config';
|
||||
import {
|
||||
EmailNotification,
|
||||
NotificationStatus,
|
||||
NotificationPriority,
|
||||
NotificationRecipient,
|
||||
NotificationChannel,
|
||||
} from '../types/notification.types';
|
||||
|
||||
export class EmailService {
|
||||
private resend: Resend;
|
||||
private config: ResendConfig;
|
||||
|
||||
constructor(config: ResendConfig) {
|
||||
this.config = config;
|
||||
this.resend = new Resend(config.apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a transactional email
|
||||
*/
|
||||
async sendEmail(
|
||||
recipient: NotificationRecipient,
|
||||
subject: string,
|
||||
htmlBody: string,
|
||||
textBody?: string,
|
||||
attachments?: Array<{
|
||||
filename: string;
|
||||
content: Buffer | string;
|
||||
mimeType?: string;
|
||||
}>
|
||||
): Promise<EmailNotification> {
|
||||
const notification: EmailNotification = {
|
||||
id: `email_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
userId: recipient.userId,
|
||||
channel: NotificationChannel.EMAIL,
|
||||
templateId: 'custom', // Can be updated to use actual template
|
||||
priority: NotificationPriority.NORMAL,
|
||||
status: NotificationStatus.PENDING,
|
||||
to: recipient.email!,
|
||||
subject,
|
||||
htmlBody,
|
||||
textBody,
|
||||
attachments,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, error } = await this.resend.emails.send({
|
||||
from: `${this.config.fromName} <${this.config.fromEmail}>`,
|
||||
to: [recipient.email!],
|
||||
subject,
|
||||
html: htmlBody,
|
||||
text: textBody,
|
||||
attachments: attachments?.map((att) => ({
|
||||
filename: att.filename,
|
||||
content: typeof att.content === 'string' ? Buffer.from(att.content) : att.content,
|
||||
mimeType: att.mimeType,
|
||||
})),
|
||||
metadata: {
|
||||
userId: recipient.userId,
|
||||
notificationId: notification.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
notification.status = NotificationStatus.FAILED;
|
||||
notification.failedAt = new Date();
|
||||
notification.errorMessage = error.message;
|
||||
} else {
|
||||
notification.status = NotificationStatus.SENT;
|
||||
notification.sentAt = new Date();
|
||||
}
|
||||
|
||||
return notification;
|
||||
} catch (error) {
|
||||
notification.status = NotificationStatus.FAILED;
|
||||
notification.failedAt = new Date();
|
||||
notification.errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email with retry logic
|
||||
*/
|
||||
async sendEmailWithRetry(
|
||||
recipient: NotificationRecipient,
|
||||
subject: string,
|
||||
htmlBody: string,
|
||||
textBody?: string,
|
||||
maxRetries: number = 3
|
||||
): Promise<EmailNotification> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
const result = await this.sendEmail(recipient, subject, htmlBody, textBody);
|
||||
|
||||
if (result.status === NotificationStatus.SENT) {
|
||||
return result;
|
||||
}
|
||||
|
||||
lastError = new Error(result.errorMessage);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
}
|
||||
|
||||
// Wait before retry (exponential backoff)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
|
||||
}
|
||||
|
||||
return {
|
||||
id: `email_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
userId: recipient.userId,
|
||||
channel: NotificationChannel.EMAIL,
|
||||
templateId: 'custom',
|
||||
priority: NotificationPriority.NORMAL,
|
||||
status: NotificationStatus.FAILED,
|
||||
to: recipient.email!,
|
||||
subject,
|
||||
htmlBody,
|
||||
textBody,
|
||||
createdAt: new Date(),
|
||||
failedAt: new Date(),
|
||||
errorMessage: lastError?.message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check email delivery status
|
||||
*/
|
||||
async getDeliveryStatus(notificationId: string): Promise<NotificationStatus> {
|
||||
try {
|
||||
const emailId = notificationId.replace('email_', '');
|
||||
const { data } = await this.resend.emails.get(emailId);
|
||||
|
||||
if (data.status === 'sent') {
|
||||
return NotificationStatus.SENT;
|
||||
} else if (data.status === 'delivered') {
|
||||
return NotificationStatus.DELIVERED;
|
||||
} else if (data.status === 'failed') {
|
||||
return NotificationStatus.FAILED;
|
||||
}
|
||||
|
||||
return NotificationStatus.PENDING;
|
||||
} catch {
|
||||
return NotificationStatus.PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch send emails
|
||||
*/
|
||||
async batchSendEmails(
|
||||
recipients: Array<{
|
||||
recipient: NotificationRecipient;
|
||||
subject: string;
|
||||
htmlBody: string;
|
||||
textBody?: string;
|
||||
}>
|
||||
): Promise<EmailNotification[]> {
|
||||
const promises = recipients.map(async ({ recipient, subject, htmlBody, textBody }) => {
|
||||
return this.sendEmail(recipient, subject, htmlBody, textBody);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user