- 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>
331 lines
8.2 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|