remove unused modules

This commit is contained in:
2026-01-07 12:24:55 +03:30
parent f2284c103d
commit 560b2983f3
150 changed files with 1853 additions and 96521 deletions
@@ -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}`;
}
}