remove unused modules
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { NotificationPreference } from '../entities/notification-preference.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { CreatePreferenceDto } from '../dto/create-preference.dto';
|
||||
import { UpdatePreferenceDto } from '../dto/update-preference.dto';
|
||||
import { NotifTitleEnum } from '../interfaces/notification.interface';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationPreferenceService {
|
||||
constructor(private readonly em: EntityManager) { }
|
||||
|
||||
async create(restaurantId: string, dto: CreatePreferenceDto): Promise<NotificationPreference> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException('Restaurant not found');
|
||||
}
|
||||
|
||||
const preference = this.em.create(NotificationPreference, {
|
||||
restaurant,
|
||||
channels: dto.channels,
|
||||
title: dto.title,
|
||||
});
|
||||
|
||||
await this.em.persistAndFlush(preference);
|
||||
return preference;
|
||||
}
|
||||
|
||||
async findByRestaurant(restaurantId: string): Promise<NotificationPreference[]> {
|
||||
return this.em.find(NotificationPreference, {
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
}
|
||||
|
||||
async findByRestaurantAndType(restaurantId: string, title: NotifTitleEnum): Promise<NotificationPreference | null> {
|
||||
return this.em.findOne(NotificationPreference, {
|
||||
restaurant: { id: restaurantId },
|
||||
title,
|
||||
});
|
||||
}
|
||||
|
||||
// async updateEnabled(
|
||||
// restaurantId: string,
|
||||
// notificationType: string,
|
||||
// enabled: boolean,
|
||||
// ): Promise<NotificationPreference> {
|
||||
// const preference = await this.findByRestaurantAndType(restaurantId, notificationType);
|
||||
// if (!preference) {
|
||||
// throw new NotFoundException('Notification preference not found');
|
||||
// }
|
||||
|
||||
// preference.enabled = enabled;
|
||||
// await this.em.persistAndFlush(preference);
|
||||
// return preference;
|
||||
// }
|
||||
|
||||
async updatePreference(
|
||||
preferenceId: string,
|
||||
restaurantId: string,
|
||||
dto: UpdatePreferenceDto,
|
||||
): Promise<NotificationPreference> {
|
||||
const preference = await this.em.findOne(NotificationPreference, {
|
||||
id: preferenceId,
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
if (!preference) {
|
||||
throw new NotFoundException('Notification preference not found');
|
||||
}
|
||||
|
||||
this.em.assign(preference, dto);
|
||||
await this.em.persistAndFlush(preference);
|
||||
return preference;
|
||||
}
|
||||
|
||||
async delete(restaurantId: string, preferenceId: string): Promise<void> {
|
||||
const preference = await this.em.findOne(NotificationPreference, {
|
||||
restaurant: { id: restaurantId },
|
||||
id: preferenceId,
|
||||
});
|
||||
if (!preference) {
|
||||
throw new NotFoundException('Notification preference not found');
|
||||
}
|
||||
await this.em.removeAndFlush(preference);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Queue } from 'bullmq';
|
||||
import { NotificationQueueNameEnum } from '../constants/queue';
|
||||
import {
|
||||
InAppNotificationQueueJob,
|
||||
PushNotificationQueueJob,
|
||||
SmsNotificationQueueJob,
|
||||
} from '../interfaces/jobs-queue.interface';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationQueueService {
|
||||
private readonly logger = new Logger(NotificationQueueService.name);
|
||||
|
||||
constructor(
|
||||
@InjectQueue(NotificationQueueNameEnum.SMS) private readonly smsQueue: Queue,
|
||||
@InjectQueue(NotificationQueueNameEnum.PUSH) private readonly pushQueue: Queue,
|
||||
@InjectQueue(NotificationQueueNameEnum.IN_APP) private readonly inAppQueue: Queue,
|
||||
) {}
|
||||
|
||||
async addSmsNotification(job: SmsNotificationQueueJob): Promise<void> {
|
||||
try {
|
||||
const jobId = `${job.templateId}-${this.generateRandomInt()}-${Date.now()}`;
|
||||
|
||||
await this.smsQueue.add('sms-notification', job, {
|
||||
jobId,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 2000,
|
||||
},
|
||||
removeOnComplete: {
|
||||
age: 24 * 3600, // Keep completed jobs for 24 hours
|
||||
count: 1000,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 7 * 24 * 3600, // Keep failed jobs for 7 days
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`SMS notification job added to queue: ${jobId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to add SMS notification to queue:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async addPushNotification(job: PushNotificationQueueJob): Promise<void> {
|
||||
try {
|
||||
const jobId = `${job.title}-${this.generateRandomInt()}-${Date.now()}`;
|
||||
|
||||
await this.pushQueue.add('push-notification', job, {
|
||||
jobId,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 2000,
|
||||
},
|
||||
removeOnComplete: {
|
||||
age: 24 * 3600, // Keep completed jobs for 24 hours
|
||||
count: 1000,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 7 * 24 * 3600, // Keep failed jobs for 7 days
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`Push notification job added to queue: ${jobId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to add push notification to queue:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async addBulkSmsNotifications(jobs: SmsNotificationQueueJob[]): Promise<void> {
|
||||
try {
|
||||
const queueJobs = jobs.map(job => ({
|
||||
name: 'sms-notification',
|
||||
data: job,
|
||||
opts: {
|
||||
jobId: `${job.templateId}-${this.generateRandomInt()}`,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 2000,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await this.smsQueue.addBulk(queueJobs);
|
||||
this.logger.log(`Added ${jobs.length} SMS notification jobs to queue`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to add bulk notifications to queue:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async addBulkPushNotifications(jobs: PushNotificationQueueJob[]): Promise<void> {
|
||||
try {
|
||||
const queueJobs = jobs.map(job => ({
|
||||
name: 'push-notification',
|
||||
data: job,
|
||||
opts: {
|
||||
jobId: `${job.title}-${this.generateRandomInt()}-${Date.now()}`,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 2000,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await this.pushQueue.addBulk(queueJobs);
|
||||
this.logger.log(`Added ${jobs.length} Push notification jobs to queue`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to add bulk notifications to queue:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async addBulkInAppNotifications(jobs: InAppNotificationQueueJob[]): Promise<void> {
|
||||
try {
|
||||
const queueJobs = jobs.map(job => ({
|
||||
name: 'in-app-notification',
|
||||
data: job,
|
||||
opts: {
|
||||
jobId: `${job.subject}-${job.notificationId}-${Date.now()}`,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 2000,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await this.inAppQueue.addBulk(queueJobs);
|
||||
this.logger.log(`Added ${jobs.length} InApp notification jobs to queue`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to add bulk notifications to queue:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
private generateRandomInt(): string {
|
||||
return Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { EntityManager, FilterQuery } from '@mikro-orm/postgresql';
|
||||
import { Notification } from '../entities/notification.entity';
|
||||
import { NotificationPreferenceService } from './notification-preference.service';
|
||||
import { NotificationQueueService } from './notification-queue.service';
|
||||
import { NotificationsGateway } from '../notifications.gateway';
|
||||
import { NotifChannelEnum, NotifRequest, NotifTitleEnum } from '../interfaces/notification.interface';
|
||||
import { SmsLog } from '../entities/smsLogs.entity';
|
||||
import { SmsLogRepository } from '../repositories/sms-log.repository';
|
||||
import { PaginatedResult } from '../../../common/interfaces/pagination.interface';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService {
|
||||
private readonly logger = new Logger(NotificationService.name);
|
||||
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly preferenceService: NotificationPreferenceService,
|
||||
private readonly queueService: NotificationQueueService,
|
||||
private readonly notificationGateway: NotificationsGateway,
|
||||
private readonly smsLogRepository: SmsLogRepository,
|
||||
) { }
|
||||
|
||||
async sendNotification(params: NotifRequest): Promise<Notification[]> {
|
||||
const { recipients, message, metadata, restaurantId } = params;
|
||||
|
||||
// create Database notifications
|
||||
const notifications = await this.createAdminBulkNotifications(
|
||||
recipients.map(recipient => ({
|
||||
restaurantId,
|
||||
title: message.title,
|
||||
content: message.content,
|
||||
adminId: 'adminId' in recipient ? recipient.adminId : null,
|
||||
userId: 'userId' in recipient ? recipient.userId : null,
|
||||
})),
|
||||
);
|
||||
|
||||
// get admin prefrences
|
||||
const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, message.title);
|
||||
|
||||
if (preference?.channels?.length === 0) {
|
||||
this.logger.warn(`Notification type is NONE for restaurant ${restaurantId}, title ${message.title}`);
|
||||
return notifications;
|
||||
}
|
||||
|
||||
// send in app notification
|
||||
if (preference?.channels?.includes(NotifChannelEnum.IN_APP)) {
|
||||
await this.queueService.addBulkInAppNotifications(
|
||||
notifications.map(notification => ({
|
||||
recipient: { adminId: notification.admin?.id || '', restaurantId },
|
||||
subject: message.title,
|
||||
body: message.content,
|
||||
notificationId: notification.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// add sms notifications to queue
|
||||
if (preference?.channels?.includes(NotifChannelEnum.SMS)) {
|
||||
await this.queueService.addBulkSmsNotifications(
|
||||
recipients.map(recipient => ({
|
||||
recipient,
|
||||
templateId: message.sms.templateId,
|
||||
parameters: message.sms.parameters,
|
||||
restaurantId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(`Queued notification for restaurant ${restaurantId}, title ${message.title}`);
|
||||
|
||||
return notifications;
|
||||
}
|
||||
|
||||
async createAdminBulkNotifications(
|
||||
params: {
|
||||
restaurantId: string;
|
||||
title: NotifTitleEnum;
|
||||
content: string;
|
||||
adminId: string | null;
|
||||
userId: string | null;
|
||||
}[],
|
||||
): Promise<Notification[]> {
|
||||
const notifications = params.map(param => {
|
||||
return this.em.create(Notification, {
|
||||
restaurant: param.restaurantId,
|
||||
admin: param.adminId,
|
||||
user: param.userId && param.userId.trim() !== '' ? param.userId : null,
|
||||
title: param.title,
|
||||
content: param.content,
|
||||
});
|
||||
});
|
||||
await this.em.persistAndFlush(notifications);
|
||||
return notifications;
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Notification> {
|
||||
const notification = await this.em.findOne(Notification, { id }, { populate: ['restaurant', 'user'] });
|
||||
if (!notification) {
|
||||
throw new NotFoundException('Notification not found');
|
||||
}
|
||||
return notification;
|
||||
}
|
||||
|
||||
async findByRestaurant(
|
||||
restaurantId: string,
|
||||
adminId: string,
|
||||
limit = 50,
|
||||
cursor?: string,
|
||||
status?: 'seen' | 'unseen',
|
||||
): Promise<{ data: Notification[]; nextCursor: string | null }> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
restaurant: { id: restaurantId },
|
||||
admin: { id: adminId },
|
||||
};
|
||||
|
||||
// Filter by status (seen/unseen)
|
||||
if (status === 'seen') {
|
||||
where.seenAt = { $ne: null };
|
||||
} else if (status === 'unseen') {
|
||||
where.seenAt = null;
|
||||
}
|
||||
|
||||
// Cursor-based pagination: if cursor is provided, get items with id < cursor
|
||||
if (cursor) {
|
||||
where.id = { $lt: cursor };
|
||||
}
|
||||
|
||||
const notifications = await this.em.find(Notification, where, {
|
||||
orderBy: { createdAt: 'DESC', id: 'DESC' },
|
||||
limit: limit + 1, // fetch one extra to determine next page
|
||||
populate: ['user'],
|
||||
});
|
||||
|
||||
const hasNextPage = notifications.length > limit;
|
||||
const data = hasNextPage ? notifications.slice(0, limit) : notifications;
|
||||
const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null;
|
||||
|
||||
return {
|
||||
data,
|
||||
nextCursor: nextCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async findByUserAndRestaurant(
|
||||
userId: string,
|
||||
restaurantId: string,
|
||||
limit = 50,
|
||||
cursor?: string,
|
||||
status?: 'seen' | 'unseen',
|
||||
): Promise<{ data: Notification[]; nextCursor: string | null }> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
};
|
||||
|
||||
// Filter by status (seen/unseen)
|
||||
if (status === 'seen') {
|
||||
where.seenAt = { $ne: null };
|
||||
} else if (status === 'unseen') {
|
||||
where.seenAt = null;
|
||||
}
|
||||
|
||||
// Cursor-based pagination: if cursor is provided, get items with id < cursor (since ULIDs are time-ordered)
|
||||
if (cursor) {
|
||||
where.id = { $lt: cursor };
|
||||
}
|
||||
|
||||
const notifications = await this.em.find(Notification, where, {
|
||||
orderBy: { createdAt: 'DESC', id: 'DESC' },
|
||||
limit: limit + 1, // Fetch one extra to determine if there's a next page
|
||||
});
|
||||
|
||||
// Check if there's a next page
|
||||
const hasNextPage = notifications.length > limit;
|
||||
const data = hasNextPage ? notifications.slice(0, limit) : notifications;
|
||||
const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null;
|
||||
|
||||
return {
|
||||
data,
|
||||
nextCursor: nextCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async readNotificationAdmin(id: string, adminId: string, restaurantId: string): Promise<void> {
|
||||
const notification = await this.em.findOne(Notification, {
|
||||
id,
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
if (!notification) {
|
||||
throw new NotFoundException('Notification not found');
|
||||
}
|
||||
notification.seenAt = new Date();
|
||||
await this.em.persistAndFlush(notification);
|
||||
}
|
||||
async readNotificationAsUser(id: string, userId: string, restaurantId: string): Promise<void> {
|
||||
const notification = await this.em.findOne(Notification, {
|
||||
id,
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
if (!notification) {
|
||||
throw new NotFoundException('Notification not found');
|
||||
}
|
||||
notification.seenAt = new Date();
|
||||
await this.em.persistAndFlush(notification);
|
||||
}
|
||||
|
||||
async findByRestaurantAndType(restaurantId: string, title: NotifTitleEnum, limit = 50): Promise<Notification[]> {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{
|
||||
restaurant: { id: restaurantId },
|
||||
title,
|
||||
},
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
populate: ['user'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async findByAdminAndRestaurant(adminId: string, restaurantId: string, limit = 50): Promise<Notification[]> {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{ admin: { id: adminId }, restaurant: { id: restaurantId } },
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async countUnseenByUserAndRestaurant(userId: string, restaurantId: string): Promise<number> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
seenAt: null,
|
||||
};
|
||||
return this.em.count(Notification, where);
|
||||
}
|
||||
|
||||
async countUnseenByRestaurant(adminId: string, restaurantId: string): Promise<number> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: restaurantId },
|
||||
seenAt: null,
|
||||
};
|
||||
return this.em.count(Notification, where);
|
||||
}
|
||||
|
||||
readAllNotifsAsUser(userId: string, restaurantId: string): Promise<number> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
seenAt: null,
|
||||
};
|
||||
return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
|
||||
}
|
||||
|
||||
readAllNotifsAsAdmin(adminId: string, restaurantId: string): Promise<number> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: restaurantId },
|
||||
seenAt: null,
|
||||
};
|
||||
return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
|
||||
}
|
||||
|
||||
async getSmsCountByRestaurant(
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
): Promise<PaginatedResult<{ restaurantId: string; restaurantName: string; smsCount: number }>> {
|
||||
return this.smsLogRepository.getSmsCountByRestaurant(page, limit);
|
||||
}
|
||||
|
||||
async getSmsCountByRestaurantId(restaurantId: string): Promise<number> {
|
||||
return this.smsLogRepository.getSmsCountByRestaurantId(restaurantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
export interface PushNotificationPayload {
|
||||
title: string;
|
||||
body: string;
|
||||
data?: Record<string, any>;
|
||||
token?: string;
|
||||
tokens?: string[];
|
||||
}
|
||||
|
||||
export interface PushNotificationResponse {
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PushNotificationService {
|
||||
private readonly logger = new Logger(PushNotificationService.name);
|
||||
private firebaseApp: admin.app.App | null = null;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.initializeFirebase();
|
||||
}
|
||||
|
||||
private initializeFirebase() {
|
||||
try {
|
||||
const firebaseConfig = this.configService.get<string>('FIREBASE_SERVICE_ACCOUNT');
|
||||
|
||||
if (!firebaseConfig) {
|
||||
this.logger.warn('Firebase service account not configured. Push notifications will be disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the service account JSON
|
||||
const serviceAccount = JSON.parse(firebaseConfig);
|
||||
|
||||
if (!this.firebaseApp) {
|
||||
this.firebaseApp = admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
});
|
||||
this.logger.log('Firebase initialized successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize Firebase:', error);
|
||||
this.logger.warn('Push notifications will be disabled');
|
||||
}
|
||||
}
|
||||
|
||||
async sendToToken(payload: PushNotificationPayload): Promise<PushNotificationResponse> {
|
||||
if (!this.firebaseApp || !payload.token) {
|
||||
throw new HttpException(
|
||||
'Push notification service not configured or token missing',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const message: admin.messaging.Message = {
|
||||
notification: {
|
||||
title: payload.title,
|
||||
body: payload.body,
|
||||
},
|
||||
data: this.convertDataToString(payload.data || {}),
|
||||
token: payload.token,
|
||||
android: {
|
||||
priority: 'high',
|
||||
},
|
||||
apns: {
|
||||
headers: {
|
||||
'apns-priority': '10',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await admin.messaging().send(message);
|
||||
this.logger.log(`Push notification sent successfully: ${response}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
messageId: response,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send push notification: ${error}`);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async sendToMultipleTokens(payload: PushNotificationPayload): Promise<PushNotificationResponse> {
|
||||
if (!this.firebaseApp || !payload.tokens || payload.tokens.length === 0) {
|
||||
throw new HttpException(
|
||||
'Push notification service not configured or tokens missing',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const message: admin.messaging.MulticastMessage = {
|
||||
notification: {
|
||||
title: payload.title,
|
||||
body: payload.body,
|
||||
},
|
||||
data: this.convertDataToString(payload.data || {}),
|
||||
tokens: payload.tokens,
|
||||
android: {
|
||||
priority: 'high',
|
||||
},
|
||||
apns: {
|
||||
headers: {
|
||||
'apns-priority': '10',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await admin.messaging().sendEachForMulticast(message);
|
||||
this.logger.log(`Push notifications sent: ${response.successCount} successful, ${response.failureCount} failed`);
|
||||
|
||||
return {
|
||||
success: response.successCount > 0,
|
||||
messageId: response.responses[0]?.messageId,
|
||||
error: response.failureCount > 0 ? `${response.failureCount} notifications failed` : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send push notifications: ${error}`);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private convertDataToString(data: Record<string, any>): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
result[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return this.firebaseApp !== null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
import { ISmsParams, ISmsResponse, ISmsBodyParameters } from '../interfaces/sms';
|
||||
|
||||
@Injectable()
|
||||
export class SmsService {
|
||||
private readonly baseURL: string;
|
||||
private readonly OTP: string;
|
||||
private readonly apiKey: string;
|
||||
|
||||
constructor(
|
||||
// private readonly httpService: HttpService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.baseURL = this.configService.getOrThrow<string>('SMS_BASE_URL');
|
||||
this.apiKey = this.configService.getOrThrow<string>('SMS_API_KEY');
|
||||
this.OTP = this.configService.getOrThrow<string>('SMS_PATTERN_OTP');
|
||||
}
|
||||
|
||||
sendotp(phone: string, otp: string) {
|
||||
return this.sendSms({
|
||||
phone,
|
||||
parameters: { otp },
|
||||
templateId: this.OTP,
|
||||
});
|
||||
}
|
||||
|
||||
async sendSms(params: ISmsParams) {
|
||||
const { parameters, phone, templateId } = params;
|
||||
const parametersArray = Object.entries(parameters ?? {}).map(([name, value]) => ({
|
||||
name,
|
||||
value: value.toString(),
|
||||
}));
|
||||
const cleanedPhone = this.formatPhone(phone);
|
||||
|
||||
const smsData: ISmsBodyParameters = {
|
||||
Parameters: parametersArray,
|
||||
Mobile: cleanedPhone,
|
||||
TemplateId: templateId,
|
||||
};
|
||||
|
||||
const url = `${this.baseURL}/send/verify`;
|
||||
|
||||
try {
|
||||
const { data } = await axios.post<ISmsResponse>(url, smsData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': this.apiKey,
|
||||
},
|
||||
});
|
||||
if (data.status !== 1) {
|
||||
throw new HttpException('The SMS was not sent. Please try again later.', HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('SMS sending failed:', JSON.stringify(error));
|
||||
throw new HttpException('The SMS was not sent. Please try again later.', HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
}
|
||||
|
||||
formatPhone(phone: string): string {
|
||||
// remove spaces, dashes, parentheses
|
||||
phone = phone.replace(/[^\d+]/g, '');
|
||||
|
||||
if (phone.startsWith('+98')) {
|
||||
return phone;
|
||||
}
|
||||
|
||||
if (phone.startsWith('98')) {
|
||||
return `+${phone}`;
|
||||
}
|
||||
|
||||
if (phone.startsWith('0')) {
|
||||
return `+98${phone.slice(1)}`;
|
||||
}
|
||||
|
||||
return `+98${phone}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user