279 lines
9.0 KiB
TypeScript
279 lines
9.0 KiB
TypeScript
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);
|
|
}
|
|
}
|