template-saas-backend-v2/src/modules/notifications/adapters/channel-adapters.service.ts
Adrian Flores Cortes b49a051d85 [SYNC] feat: Add pending entities, tests and notification adapters
- audit/unified-log.entity.ts
- billing/__tests__/billing-usage.service.spec.ts
- feature-flags/flag-override.entity.ts
- notifications/adapters/
- webhooks/__tests__/webhook-retry.spec.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 18:12:16 -06:00

331 lines
8.2 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { BaseNotificationAdapter, NotificationDeliveryResult } from './channel-adapter.interface';
/**
* Email Channel Adapter
* Handles email notifications
*/
@Injectable()
export class EmailChannelAdapter extends BaseNotificationAdapter {
readonly channelType = 'email';
constructor(private readonly emailService: any) {
super('email');
}
async isAvailable(): Promise<boolean> {
try {
// Check email service configuration
return !!(this.emailService && await this.emailService.isConfigured?.());
} catch {
return false;
}
}
getConfigSchema(): Record<string, any> {
return {
type: 'object',
properties: {
smtp: {
type: 'object',
properties: {
host: { type: 'string' },
port: { type: 'number' },
secure: { type: 'boolean' },
auth: {
type: 'object',
properties: {
user: { type: 'string' },
pass: { type: 'string' },
},
},
},
},
},
};
}
protected transform(notification: any): any {
return {
to: notification.recipient,
subject: notification.title,
text: notification.message,
html: notification.html || notification.message,
attachments: notification.attachments || [],
};
}
async send(notification: any, preferences?: any): Promise<NotificationDeliveryResult> {
try {
if (!this.validate(notification)) {
return {
success: false,
error: 'Invalid notification data',
};
}
const emailData = this.transform(notification);
const result = await this.emailService.sendMail(emailData);
const deliveryResult: NotificationDeliveryResult = {
success: true,
messageId: result.messageId,
metadata: {
provider: 'smtp',
...result,
},
};
this.logAttempt(notification, deliveryResult);
return deliveryResult;
} catch (error) {
const deliveryResult: NotificationDeliveryResult = {
success: false,
error: error.message,
};
this.logAttempt(notification, deliveryResult);
return deliveryResult;
}
}
}
/**
* Push Notification Channel Adapter
* Handles mobile/web push notifications
*/
@Injectable()
export class PushChannelAdapter extends BaseNotificationAdapter {
readonly channelType = 'push';
constructor(private readonly pushService: any) {
super('push');
}
async isAvailable(): Promise<boolean> {
try {
return !!(this.pushService && await this.pushService.isConfigured?.());
} catch {
return false;
}
}
getConfigSchema(): Record<string, any> {
return {
type: 'object',
properties: {
fcm: {
type: 'object',
properties: {
serverKey: { type: 'string' },
},
},
apns: {
type: 'object',
properties: {
keyId: { type: 'string' },
teamId: { type: 'string' },
bundleId: { type: 'string' },
},
},
},
};
}
protected transform(notification: any): any {
return {
title: notification.title,
body: notification.message,
icon: notification.icon,
image: notification.image,
data: notification.data || {},
actions: notification.actions || [],
badge: notification.badge,
sound: notification.sound || 'default',
};
}
async send(notification: any, preferences?: any): Promise<NotificationDeliveryResult> {
try {
if (!notification.deviceToken) {
return {
success: false,
error: 'Device token required for push notifications',
};
}
const pushData = this.transform(notification);
const result = await this.pushService.send(notification.deviceToken, pushData);
const deliveryResult: NotificationDeliveryResult = {
success: true,
messageId: result.messageId,
metadata: {
provider: 'fcm/apns',
deviceToken: notification.deviceToken,
...result,
},
};
this.logAttempt(notification, deliveryResult);
return deliveryResult;
} catch (error) {
const deliveryResult: NotificationDeliveryResult = {
success: false,
error: error.message,
};
this.logAttempt(notification, deliveryResult);
return deliveryResult;
}
}
}
/**
* WhatsApp Channel Adapter
* Handles WhatsApp Business API notifications
*/
@Injectable()
export class WhatsAppChannelAdapter extends BaseNotificationAdapter {
readonly channelType = 'whatsapp';
constructor(private readonly whatsappService: any) {
super('whatsapp');
}
async isAvailable(): Promise<boolean> {
try {
return !!(this.whatsappService && await this.whatsappService.isConfigured?.());
} catch {
return false;
}
}
getConfigSchema(): Record<string, any> {
return {
type: 'object',
properties: {
accessToken: { type: 'string' },
phoneNumberId: { type: 'string' },
version: { type: 'string' },
webhookVerifyToken: { type: 'string' },
},
};
}
protected transform(notification: any): any {
return {
to: notification.recipient,
type: notification.template ? 'template' : 'text',
text: { body: notification.message },
template: notification.template,
};
}
async send(notification: any, preferences?: any): Promise<NotificationDeliveryResult> {
try {
if (!notification.recipient) {
return {
success: false,
error: 'Recipient required for WhatsApp notifications',
};
}
const whatsappData = this.transform(notification);
const result = await this.whatsappService.sendMessage(whatsappData);
const deliveryResult: NotificationDeliveryResult = {
success: true,
messageId: result.messageId,
metadata: {
provider: 'whatsapp',
recipient: notification.recipient,
...result,
},
};
this.logAttempt(notification, deliveryResult);
return deliveryResult;
} catch (error) {
const deliveryResult: NotificationDeliveryResult = {
success: false,
error: error.message,
};
this.logAttempt(notification, deliveryResult);
return deliveryResult;
}
}
}
/**
* In-App Channel Adapter
* Handles in-app notifications (stored in database)
*/
@Injectable()
export class InAppChannelAdapter extends BaseNotificationAdapter {
readonly channelType = 'in_app';
constructor(private readonly notificationRepository: any) {
super('in_app');
}
async isAvailable(): Promise<boolean> {
return true; // In-app is always available if DB is accessible
}
getConfigSchema(): Record<string, any> {
return {
type: 'object',
properties: {
retention: {
type: 'object',
properties: {
days: { type: 'number', default: 30 },
},
},
},
};
}
protected transform(notification: any): any {
return {
user_id: notification.userId,
tenant_id: notification.tenantId,
type: notification.type || 'info',
title: notification.title,
message: notification.message,
data: notification.data || null,
action_url: notification.actionUrl || null,
delivery_status: 'delivered',
};
}
async send(notification: any, preferences?: any): Promise<NotificationDeliveryResult> {
try {
const notificationData = this.transform(notification);
const saved = await this.notificationRepository.save(notificationData);
const deliveryResult: NotificationDeliveryResult = {
success: true,
messageId: saved.id,
metadata: {
provider: 'database',
notificationId: saved.id,
},
};
this.logAttempt(notification, deliveryResult);
return deliveryResult;
} catch (error) {
const deliveryResult: NotificationDeliveryResult = {
success: false,
error: error.message,
};
this.logAttempt(notification, deliveryResult);
return deliveryResult;
}
}
}