This commit is contained in:
2026-01-14 11:29:24 +03:30
parent 1075dbc44f
commit 4ab9de447a
26 changed files with 1130 additions and 1710 deletions
@@ -5,13 +5,10 @@ import { NotificationPreferenceService } from '../services/notification-preferen
import { AuthGuard } from '../../auth/guards/auth.guard';
import { UserId } from '../../../common/decorators/user-id.decorator';
import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard';
import { } from '../../../common/decorators/rest-id.decorator';
import { UpdatePreferenceDto } from '../dto/update-preference.dto';
import { CreatePreferenceDto } from '../dto/create-preference.dto';
import { AdminId } from 'src/common/decorators/admin-id.decorator';
import { API_HEADER_SLUG } from 'src/common/constants/index';
import { NotificationMessage } from 'src/common/enums/message.enum';
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { Permission } from 'src/common/enums/permission.enum';
@@ -23,174 +20,156 @@ export class NotificationsController {
private readonly preferenceService: NotificationPreferenceService,
) { }
@UseGuards(AuthGuard)
@ApiBearerAuth()
@Get('public/notifications')
@ApiOperation({ summary: 'Get user restaurant notifications' })
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @Get('public/notifications')
// @ApiOperation({ summary: 'Get user restaurant notifications' })
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items to return (default: 50)' })
@ApiQuery({
name: 'cursor',
required: false,
type: String,
description: 'Cursor for pagination (ID of the last item from previous page)',
})
@ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
async getUserNotifications(
@UserId() userId: string,
,
@Query('limit') limit?: number,
@Query('cursor') cursor?: string,
@Query('status') status?: 'seen' | 'unseen',
) {
return await this.notificationService.findByUserAndRestaurant(
userId,
,
limit ? parseInt(limit.toString(), 10) : 50,
cursor,
status,
);
}
// @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items to return (default: 50)' })
// @ApiQuery({
// name: 'cursor',
// required: false,
// type: String,
// description: 'Cursor for pagination (ID of the last item from previous page)',
// })
// @ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
// async getUserNotifications(
// @UserId() userId: string,
// @Query('limit') limit?: number,
// @Query('cursor') cursor?: string,
// @Query('status') status?: 'seen' | 'unseen',
// ) {
// return await this.notificationService.findByUserAndRestaurant(
// userId,
// limit ? parseInt(limit.toString(), 10) : 50,
// cursor,
// status,
// );
// }
@UseGuards(AuthGuard)
@ApiBearerAuth()
@Get('public/notifications/unseen-count')
@ApiOperation({ summary: 'Get unseen notifications count for user' })
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @Get('public/notifications/unseen-count')
// @ApiOperation({ summary: 'Get unseen notifications count for user' })
async getUserUnseenCount(@UserId() userId: string,) {
const count = await this.notificationService.countUnseenByUserAndRestaurant(userId,);
return { count };
}
// async getUserUnseenCount(@UserId() userId: string,) {
// const count = await this.notificationService.countUnseenByUserAndRestaurant(userId,);
// return { count };
// }
@UseGuards(AuthGuard)
@ApiBearerAuth()
@Put('public/notifications/:id')
@ApiOperation({ summary: 'Read a notification ' })
@ApiParam({ name: 'id', description: 'Notification ID' })
async readNotificationUser(, @Param('id') id: string, @UserId() userId: string) {
await this.notificationService.readNotificationAsUser(id, userId,);
return { message: NotificationMessage.READ_SUCCESS };
}
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @Put('public/notifications/:id')
// @ApiOperation({ summary: 'Read a notification ' })
// @ApiParam({ name: 'id', description: 'Notification ID' })
// async readNotificationUser(, @Param('id') id: string, @UserId() userId: string) {
// await this.notificationService.readNotificationAsUser(id, userId,);
// return { message: NotificationMessage.READ_SUCCESS };
// }
@UseGuards(AuthGuard)
@ApiBearerAuth()
@Put('public/notifications/read/all')
@ApiOperation({ summary: 'Read all notification ' })
async readAllNotificationUser(, @UserId() userId: string) {
await this.notificationService.readAllNotifsAsUser(userId,);
return { message: NotificationMessage.READ_SUCCESS };
}
/* ***************** Admin Endpoints ***************** */
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @Put('public/notifications/read/all')
// @ApiOperation({ summary: 'Read all notification ' })
// async readAllNotificationUser(, @UserId() userId: string) {
// await this.notificationService.readAllNotifsAsUser(userId,);
// return { message: NotificationMessage.READ_SUCCESS };
// }
// /* ***************** Admin Endpoints ***************** */
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Get('admin/notifications')
@ApiOperation({ summary: 'Get Admin notifications' })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({
name: 'cursor',
required: false,
type: String,
description: 'Cursor for pagination (ID of the last item from previous page)',
})
@ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
async getAdminNotifications(
@AdminId() adminId: string,
,
@Query('limit') limit?: number,
@Query('cursor') cursor?: string,
@Query('status') status?: 'seen' | 'unseen',
) {
const parsedLimit = limit ? parseInt(limit.toString(), 10) : 50;
return await this.notificationService.findByRestaurant(, adminId, parsedLimit, cursor, status);
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Get('admin/notifications')
// @ApiOperation({ summary: 'Get Admin notifications' })
// @ApiQuery({ name: 'limit', required: false, type: Number })
// @ApiQuery({
// name: 'cursor',
// required: false,
// type: String,
// description: 'Cursor for pagination (ID of the last item from previous page)',
// })
// @ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
// async getAdminNotifications(
// @AdminId() adminId: string,
// ,
// @Query('limit') limit?: number,
// @Query('cursor') cursor?: string,
// @Query('status') status?: 'seen' | 'unseen',
// ) {
// const parsedLimit = limit ? parseInt(limit.toString(), 10) : 50;
// return await this.notificationService.findByRestaurant( adminId, parsedLimit, cursor, status);
// }
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Get('admin/notifications/unseen-count')
@ApiOperation({ summary: 'Get unseen notifications count for admin' })
async getAdminUnseenCount(@AdminId() adminId: string,) {
const count = await this.notificationService.countUnseenByRestaurant(adminId,);
return { count };
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Get('admin/notifications/unseen-count')
// @ApiOperation({ summary: 'Get unseen notifications count for admin' })
// async getAdminUnseenCount(@AdminId() adminId: string,) {
// const count = await this.notificationService.countUnseenByRestaurant(adminId,);
// return { count };
// }
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Put('admin/notifications/:id')
@ApiOperation({ summary: 'Read a notification ' })
@ApiParam({ name: 'id', description: 'Notification ID' })
async readNotificationAdmin(, @AdminId() adminId: string, @Param('id') id: string) {
await this.notificationService.readNotificationAdmin(id, adminId,);
return { message: NotificationMessage.READ_SUCCESS };
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Put('admin/notifications/:id')
// @ApiOperation({ summary: 'Read a notification ' })
// @ApiParam({ name: 'id', description: 'Notification ID' })
// async readNotificationAdmin(, @AdminId() adminId: string, @Param('id') id: string) {
// await this.notificationService.readNotificationAdmin(id, adminId,);
// return { message: NotificationMessage.READ_SUCCESS };
// }
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Put('admin/notifications/read/all')
@ApiOperation({ summary: 'Read all notification ' })
async readAllNotificationAdmin(, @AdminId() adminId: string, @Param('id') id: string) {
await this.notificationService.readAllNotifsAsAdmin(adminId,);
return { message: NotificationMessage.READ_SUCCESS };
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Put('admin/notifications/read/all')
// @ApiOperation({ summary: 'Read all notification ' })
// async readAllNotificationAdmin(, @AdminId() adminId: string, @Param('id') id: string) {
// await this.notificationService.readAllNotifsAsAdmin(adminId,);
// return { message: NotificationMessage.READ_SUCCESS };
// }
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_SETTINGS)
@Get('admin/notification-preferences')
@ApiOperation({ summary: 'Get all notification preferences for a restaurant' })
async getPreferences() {
return this.preferenceService.findByRestaurant();
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Permissions(Permission.MANAGE_SETTINGS)
// @Get('admin/notification-preferences')
// @ApiOperation({ summary: 'Get all notification preferences for a restaurant' })
// async getPreferences() {
// return this.preferenceService.findByRestaurant();
// }
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_SETTINGS)
@Patch('admin/notification-preferences/:id')
@ApiOperation({ summary: 'Update notification channels' })
@ApiParam({ name: 'id', description: 'Notification preference ID' })
async updatePreference(
,
@Param('id') preferenceId: string,
@Body() dto: UpdatePreferenceDto,
) {
return this.preferenceService.updatePreference(preferenceId, , dto);
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Permissions(Permission.MANAGE_SETTINGS)
// @Patch('admin/notification-preferences/:id')
// @ApiOperation({ summary: 'Update notification channels' })
// @ApiParam({ name: 'id', description: 'Notification preference ID' })
// async updatePreference(
// ,
// @Param('id') preferenceId: string,
// @Body() dto: UpdatePreferenceDto,
// ) {
// return this.preferenceService.updatePreference(preferenceId, dto);
// }
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_SETTINGS)
@Post('admin/notification-preferences')
@ApiOperation({ summary: 'Create notification preference' })
async createPreference(
,
@Body() dto: CreatePreferenceDto,
) {
return this.preferenceService.create(, dto);
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Permissions(Permission.MANAGE_SETTINGS)
// @Post('admin/notification-preferences')
// @ApiOperation({ summary: 'Create notification preference' })
// async createPreference(
// ,
// @Body() dto: CreatePreferenceDto,
// ) {
// return this.preferenceService.create(dto);
// }
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Get('admin/notifications/sms-usage')
@ApiOperation({ summary: 'Get SMS usage for my restaurant' })
async getRestaurantSmsUsage() {
const smsCount = await this.notificationService.getSmsCountBy();
return { smsCount };
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Get('admin/notifications/sms-usage')
// @ApiOperation({ summary: 'Get SMS usage for my restaurant' })
// async getRestaurantSmsUsage() {
// const smsCount = await this.notificationService.getSmsCountBy();
// return { smsCount };
// }
// super admin endpoints
@UseGuards(SuperAdminAuthGuard)
@ApiBearerAuth()
@Get('super-admin/notifications/sms-count')
@ApiOperation({ summary: 'Get SMS count for each restaurant with pagination' })
@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)', minimum: 1 })
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default: 10)', minimum: 1 })
async getSmsCount(
@Query('page') page?: number,
@Query('limit') limit?: number,
) {
const parsedPage = page ? parseInt(page.toString(), 10) : 1;
const parsedLimit = limit ? parseInt(limit.toString(), 10) : 10;
return await this.notificationService.getSmsCountByRestaurant(parsedPage, parsedLimit);
}
}
@@ -1,26 +0,0 @@
import type { ExecutionContext } from '@nestjs/common';
import { createParamDecorator } from '@nestjs/common';
import type { AuthenticatedSocket } from '../guards/ws-admin-auth.guard';
/**
* Extract restaurant ID from authenticated WebSocket client
* Use this decorator in WebSocket handlers to get the from the authenticated admin
*
* @example
* ```typescript
* @SubscribeMessage('get:notifications')
* handleGetNotifications(@Ws() : string) {
* // is automatically extracted from authenticated admin
* }
* ```
*/
export const Ws = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
const client = ctx.switchToWs().getClient<AuthenticatedSocket>();
const = client.;
if (!) {
throw new Error('Restaurant ID not found. Ensure WsAdminAuthGuard is applied.');
}
return;
});
@@ -1,14 +1,10 @@
import { Entity, Property, ManyToOne, Unique } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { NotifTitleEnum } from '../interfaces/notification.interface';
import { NotifChannelEnum } from '../interfaces/notification.interface';
@Entity({ tableName: 'notification_preferences' })
@Unique({ properties: ['restaurant', 'title'] })
export class NotificationPreference extends BaseEntity {
@ManyToOne(() => Restaurant)
restaurant!: Restaurant;
@Property()
title!: NotifTitleEnum;
@@ -1,14 +1,10 @@
import { Entity, Property, ManyToOne, PrimaryKey } from '@mikro-orm/core';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { Entity, Property, PrimaryKey } from '@mikro-orm/core';
@Entity({ tableName: 'sms_logs' })
export class SmsLog {
@PrimaryKey()
id!: number;
@ManyToOne(() => Restaurant)
restaurant!: Restaurant;
@Property()
phone!: string;
@@ -7,7 +7,6 @@ import { IAdminTokenPayload } from '../../auth/interfaces/IToken-payload';
export interface AuthenticatedSocket extends Socket {
adminId?: string;
?: string;
}
@Injectable()
@@ -41,16 +40,15 @@ export class WsAdminAuthGuard implements CanActivate {
secret,
});
if (!payload.adminId || !payload.) {
if (!payload.adminId) {
this.logger.error('Invalid token payload structure', payload);
throw new WsException('Invalid token payload');
}
// Attach admin info to socket
(client as AuthenticatedSocket).adminId = payload.adminId;
(client as AuthenticatedSocket). = payload.;
this.logger.log(`Admin authenticated via WebSocket: ${payload.adminId}, : ${payload.}`);
this.logger.log(`Admin authenticated via WebSocket: ${payload.adminId}`);
return true;
} catch (error) {
@@ -3,7 +3,6 @@ import type { NotifTitleEnum, recipientType } from './notification.interface';
export interface SmsNotificationQueueJob {
recipient: recipientType;
templateId: string;
: string;
parameters?: Record<string, string>;
}
export interface PushNotificationQueueJob {
@@ -18,7 +17,7 @@ export interface PushNotificationQueueJob {
pushTokens?: string[]; // Multiple FCM tokens for bulk push notifications
}
export interface InAppNotificationQueueJob {
recipient: { adminId: string; : string };
recipient: { adminId: string; };
subject: NotifTitleEnum;
body: string;
notificationId: string;
@@ -40,7 +40,6 @@ export interface NotifRequest {
// timestamp: Date;
// notifType: NotifTypeEnum;
// channels: NotifChannelEnum[];
: string;
recipients: recipientType[];
message: NotifRequestMessage;
metadata: {
@@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { SmsSentEvent } from '../events/sms.events';
import { RestRepository } from '../../restaurants/repositories/rest.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { SmsLog } from '../entities/smsLogs.entity';
@@ -10,7 +9,6 @@ export class SmsListeners {
private readonly logger = new Logger(SmsListeners.name);
constructor(
private readonly restRepository: RestRepository,
private readonly em: EntityManager,
) {
}
@@ -19,22 +17,11 @@ export class SmsListeners {
async handleSmsSent(event: SmsSentEvent) {
try {
this.logger.log(
`SMS sent event received: phone ${event.phoneNumber} for restaurant: ${event.}`,
`SMS sent event received: phone ${event.phoneNumber} `,
);
// Get the restaurant entity
const restaurant = await this.restRepository.findOne({ id: event. });
if (!restaurant) {
this.logger.warn(
`Restaurant not found for SMS log: ${event.}`,
);
return;
}
// Create SMS log record
const smsLog = this.em.create(SmsLog, {
restaurant: restaurant,
phone: event.phoneNumber,
createdAt: new Date(),
});
@@ -46,7 +33,7 @@ export class SmsListeners {
);
} catch (error) {
this.logger.error(
`Failed to create SMS log for event: ${event.}`,
`Failed to create SMS `,
error instanceof Error ? error.stack : String(error),
);
}
@@ -4,8 +4,6 @@ import {
// SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
// MessageBody,
// ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger, Inject, forwardRef } from '@nestjs/common';
@@ -70,8 +68,8 @@ export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisco
// }
}
sendInAppNotification(repipient: { adminId: string; : string }, payload: IInAppNotificationPayload) {
const room = this.getRoom(repipient.adminId, repipient.);
sendInAppNotification(repipient: { adminId: string; }, payload: IInAppNotificationPayload) {
const room = this.getRoom(repipient.adminId,);
this.logger.log(`Sending in app notification to admin: ${repipient.adminId}`);
this.server.to(room).emit('notifications', payload, async () => {
@@ -81,19 +79,19 @@ export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisco
this.logger.log(`In app notification sent to admin: ${repipient.adminId}`);
}
private getRoom(adminId: string, : string) {
return `restaurant:${}-admin:${adminId}`;
private getRoom(adminId: string,) {
return `admin:${adminId}`;
}
handleLeaveRoom(client: AuthenticatedSocket) {
const room = this.getRoom(client.adminId!, client.!);
const room = this.getRoom(client.adminId!,);
void client.leave(room);
this.logger.log(`Admin ${client.adminId} (Client ${client.id}) left room: ${room}`);
void client.emit('left', { room, message: 'Successfully Admin left room' });
}
handleJoinRoom(client: AuthenticatedSocket) {
const room = this.getRoom(client.adminId!, client.!);
const room = this.getRoom(client.adminId!,);
void client.join(room);
this.logger.log(`Admin ${client.adminId} (Client ${client.id}) joined room: ${room}`);
void client.emit('joined', { room, message: 'Successfully Admin joined room' });
@@ -9,69 +9,5 @@ export class SmsLogRepository extends EntityRepository<SmsLog> {
super(em, SmsLog);
}
async getSmsCountByRestaurant(
page: number = 1,
limit: number = 10,
): Promise<PaginatedResult<{ : string; restaurantName: string; smsCount: number
}>> {
const offset = (page - 1) * limit;
// Get total count of unique restaurants with SMS logs
const totalResult = await this.em.execute(
`
SELECT COUNT(DISTINCT sl.restaurant_id) as total
FROM sms_logs sl
WHERE sl.restaurant_id IS NOT NULL
`,
);
const total = parseInt(totalResult[0]?.total || '0', 10);
// Get paginated results with SMS counts grouped by restaurant
const results = await this.em.execute(
`
SELECT
r.id as "",
r.name as "restaurantName",
COUNT(sl.id)::int as "smsCount"
FROM sms_logs sl
INNER JOIN restaurants r ON sl.restaurant_id = r.id
GROUP BY r.id, r.name
ORDER BY "smsCount" DESC, r.name ASC
LIMIT ? OFFSET ?
`,
[limit, offset],
);
const data = results.map((row: any) => ({
: row.,
restaurantName: row.restaurantName || 'Unknown',
smsCount: parseInt(row.smsCount || '0', 10),
}));
const totalPages = Math.ceil(total / limit);
return {
data,
meta: {
total,
page,
limit,
totalPages,
},
};
}
async getSmsCountBy(: string): Promise < number > {
const result = await this.em.execute(
`
SELECT COUNT(sl.id)::int as "smsCount"
FROM sms_logs sl
WHERE sl.restaurant_id = ?
`,
[],
);
return parseInt(result[0]?.smsCount || '0', 10);
}
}
@@ -1,7 +1,6 @@
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';
@@ -10,34 +9,34 @@ import { NotifTitleEnum } from '../interfaces/notification.interface';
export class NotificationPreferenceService {
constructor(private readonly em: EntityManager) { }
async create(: string, dto: CreatePreferenceDto): Promise<NotificationPreference> {
const restaurant = await this.em.findOne(Restaurant, { id: });
if (!restaurant) {
throw new NotFoundException('Restaurant not found');
}
// async create( dto: CreatePreferenceDto): Promise<NotificationPreference> {
// const restaurant = await this.em.findOne(Restaurant, { id: });
// if (!restaurant) {
// throw new NotFoundException('Restaurant not found');
// }
const preference = this.em.create(NotificationPreference, {
restaurant,
channels: dto.channels,
title: dto.title,
});
// const preference = this.em.create(NotificationPreference, {
// restaurant,
// channels: dto.channels,
// title: dto.title,
// });
await this.em.persistAndFlush(preference);
return preference;
}
// await this.em.persistAndFlush(preference);
// return preference;
// }
async findByRestaurant(: string): Promise<NotificationPreference[]> {
return this.em.find(NotificationPreference, {
restaurant: { id: },
});
}
// async findByRestaurant(: string): Promise<NotificationPreference[]> {
// return this.em.find(NotificationPreference, {
// restaurant: { id: },
// });
// }
async findByRestaurantAndType(: string, title: NotifTitleEnum): Promise<NotificationPreference | null> {
return this.em.findOne(NotificationPreference, {
restaurant: { id: },
title,
});
}
// async findByRestaurantAndType(: string, title: NotifTitleEnum): Promise<NotificationPreference | null> {
// return this.em.findOne(NotificationPreference, {
// restaurant: { id: },
// title,
// });
// }
// async updateEnabled(
// : string,
@@ -54,32 +53,32 @@ export class NotificationPreferenceService {
// return preference;
// }
async updatePreference(
preferenceId: string,
: string,
dto: UpdatePreferenceDto,
): Promise<NotificationPreference> {
const preference = await this.em.findOne(NotificationPreference, {
id: preferenceId,
restaurant: { id: },
});
if (!preference) {
throw new NotFoundException('Notification preference not found');
}
// async updatePreference(
// preferenceId: string,
// : string,
// dto: UpdatePreferenceDto,
// ): Promise<NotificationPreference> {
// const preference = await this.em.findOne(NotificationPreference, {
// id: preferenceId,
// restaurant: { id: },
// });
// if (!preference) {
// throw new NotFoundException('Notification preference not found');
// }
this.em.assign(preference, dto);
await this.em.persistAndFlush(preference);
return preference;
}
// this.em.assign(preference, dto);
// await this.em.persistAndFlush(preference);
// return preference;
// }
async delete(: string, preferenceId: string): Promise<void> {
const preference = await this.em.findOne(NotificationPreference, {
restaurant: { id: },
id: preferenceId,
});
if (!preference) {
throw new NotFoundException('Notification preference not found');
}
await this.em.removeAndFlush(preference);
}
// async delete(: string, preferenceId: string): Promise<void> {
// const preference = await this.em.findOne(NotificationPreference, {
// restaurant: { id: },
// id: preferenceId,
// });
// if (!preference) {
// throw new NotFoundException('Notification preference not found');
// }
// await this.em.removeAndFlush(preference);
// }
}
@@ -21,259 +21,259 @@ export class NotificationService {
private readonly smsLogRepository: SmsLogRepository,
) { }
async sendNotification(params: NotifRequest): Promise<Notification[]> {
const { recipients, message, metadata, } = params;
// async sendNotification(params: NotifRequest): Promise<Notification[]> {
// const { recipients, message, metadata, } = params;
// create Database notifications
const notifications = await this.createAdminBulkNotifications(
recipients.map(recipient => ({
title: message.title,
content: message.content,
adminId: 'adminId' in recipient ? recipient.adminId : null,
userId: 'userId' in recipient ? recipient.userId : null,
})),
);
// // create Database notifications
// const notifications = await this.createAdminBulkNotifications(
// recipients.map(recipient => ({
// 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(, message.title);
// // get admin prefrences
// const preference = await this.preferenceService.findByRestaurantAndType(, message.title);
if(preference?.channels?.length === 0) {
this.logger.warn(`Notification type is NONE for restaurant ${}, title ${message.title}`);
return notifications;
}
// if(preference?.channels?.length === 0) {
// this.logger.warn(`Notification type is NONE for restaurant ${}, 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 || '', },
subject: message.title,
body: message.content,
notificationId: notification.id,
})),
);
}
// // send in app notification
// if (preference?.channels?.includes(NotifChannelEnum.IN_APP)) {
// await this.queueService.addBulkInAppNotifications(
// notifications.map(notification => ({
// recipient: { adminId: notification.admin?.id || '', },
// 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,
})),
);
}
// // 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,
// })),
// );
// }
this.logger.log(`Queued notification for restaurant ${}, title ${message.title}`);
// this.logger.log(`Queued notification for restaurant ${}, title ${message.title}`);
return notifications;
}
// return notifications;
// }
async createAdminBulkNotifications(
params: {
// async createAdminBulkNotifications(
// params: {
title: NotifTitleEnum;
content: string;
adminId: string | null;
userId: string | null;
}[],
): Promise < Notification[] > {
const notifications = params.map(param => {
return this.em.create(Notification, {
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(
: string,
adminId: string,
limit = 50,
cursor ?: string,
status ?: 'seen' | 'unseen',
): Promise < { data: Notification[]; nextCursor: string | null } > {
const where: FilterQuery<Notification> = {
restaurant: { id: },
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,
: string,
limit = 50,
cursor ?: string,
status ?: 'seen' | 'unseen',
): Promise < { data: Notification[]; nextCursor: string | null } > {
const where: FilterQuery<Notification> = {
user: { id: userId },
restaurant: { id: },
};
// 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, : string): Promise < void> {
const notification = await this.em.findOne(Notification, {
id,
admin: { id: adminId },
restaurant: { id: },
});
if(!notification) {
throw new NotFoundException('Notification not found');
}
notification.seenAt = new Date();
await this.em.persistAndFlush(notification);
}
async readNotificationAsUser(id: string, userId: string, : string): Promise < void> {
const notification = await this.em.findOne(Notification, {
id,
user: { id: userId },
restaurant: { id: },
});
if(!notification) {
throw new NotFoundException('Notification not found');
}
notification.seenAt = new Date();
await this.em.persistAndFlush(notification);
}
async findByRestaurantAndType(: string, title: NotifTitleEnum, limit = 50): Promise < Notification[] > {
return this.em.find(
Notification,
{
restaurant: { id: },
title,
},
{
orderBy: { createdAt: 'DESC' },
limit,
populate: ['user'],
},
);
}
async findByAdminAndRestaurant(adminId: string, : string, limit = 50): Promise < Notification[] > {
return this.em.find(
Notification,
{ admin: { id: adminId }, restaurant: { id: } },
{
orderBy: { createdAt: 'DESC' },
limit,
},
);
}
async countUnseenByUserAndRestaurant(userId: string, : string): Promise < number > {
const where: FilterQuery<Notification> = {
user: { id: userId },
restaurant: { id: },
seenAt: null,
};
return this.em.count(Notification, where);
}
async countUnseenByRestaurant(adminId: string, : string): Promise < number > {
const where: FilterQuery<Notification> = {
admin: { id: adminId },
restaurant: { id: },
seenAt: null,
};
return this.em.count(Notification, where);
}
readAllNotifsAsUser(userId: string, : string): Promise < number > {
const where: FilterQuery<Notification> = {
user: { id: userId },
restaurant: { id: },
seenAt: null,
};
return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
}
readAllNotifsAsAdmin(adminId: string, : string): Promise < number > {
const where: FilterQuery<Notification> = {
admin: { id: adminId },
restaurant: { id: },
seenAt: null,
};
return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
}
async getSmsCountByRestaurant(
page: number = 1,
limit: number = 10,
): Promise < PaginatedResult < { : string; restaurantName: string; smsCount: number } >> {
return this.smsLogRepository.getSmsCountByRestaurant(page, limit);
}
async getSmsCountBy(: string): Promise < number > {
return this.smsLogRepository.getSmsCountBy();
}
// title: NotifTitleEnum;
// content: string;
// adminId: string | null;
// userId: string | null;
// }[],
// ): Promise < Notification[] > {
// const notifications = params.map(param => {
// return this.em.create(Notification, {
// 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(
// : string,
// adminId: string,
// limit = 50,
// cursor ?: string,
// status ?: 'seen' | 'unseen',
// ): Promise < { data: Notification[]; nextCursor: string | null } > {
// const where: FilterQuery<Notification> = {
// restaurant: { id: },
// 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,
// : string,
// limit = 50,
// cursor ?: string,
// status ?: 'seen' | 'unseen',
// ): Promise < { data: Notification[]; nextCursor: string | null } > {
// const where: FilterQuery<Notification> = {
// user: { id: userId },
// restaurant: { id: },
// };
// // 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, : string): Promise < void> {
// const notification = await this.em.findOne(Notification, {
// id,
// admin: { id: adminId },
// restaurant: { id: },
// });
// if(!notification) {
// throw new NotFoundException('Notification not found');
// }
// notification.seenAt = new Date();
// await this.em.persistAndFlush(notification);
// }
// async readNotificationAsUser(id: string, userId: string, : string): Promise < void> {
// const notification = await this.em.findOne(Notification, {
// id,
// user: { id: userId },
// restaurant: { id: },
// });
// if(!notification) {
// throw new NotFoundException('Notification not found');
// }
// notification.seenAt = new Date();
// await this.em.persistAndFlush(notification);
// }
// async findByRestaurantAndType(: string, title: NotifTitleEnum, limit = 50): Promise < Notification[] > {
// return this.em.find(
// Notification,
// {
// restaurant: { id: },
// title,
// },
// {
// orderBy: { createdAt: 'DESC' },
// limit,
// populate: ['user'],
// },
// );
// }
// async findByAdminAndRestaurant(adminId: string, : string, limit = 50): Promise < Notification[] > {
// return this.em.find(
// Notification,
// { admin: { id: adminId }, restaurant: { id: } },
// {
// orderBy: { createdAt: 'DESC' },
// limit,
// },
// );
// }
// async countUnseenByUserAndRestaurant(userId: string, : string): Promise < number > {
// const where: FilterQuery<Notification> = {
// user: { id: userId },
// restaurant: { id: },
// seenAt: null,
// };
// return this.em.count(Notification, where);
// }
// async countUnseenByRestaurant(adminId: string, : string): Promise < number > {
// const where: FilterQuery<Notification> = {
// admin: { id: adminId },
// restaurant: { id: },
// seenAt: null,
// };
// return this.em.count(Notification, where);
// }
// readAllNotifsAsUser(userId: string, : string): Promise < number > {
// const where: FilterQuery<Notification> = {
// user: { id: userId },
// restaurant: { id: },
// seenAt: null,
// };
// return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
// }
// readAllNotifsAsAdmin(adminId: string, : string): Promise < number > {
// const where: FilterQuery<Notification> = {
// admin: { id: adminId },
// restaurant: { id: },
// seenAt: null,
// };
// return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
// }
// async getSmsCountByRestaurant(
// page: number = 1,
// limit: number = 10,
// ): Promise < PaginatedResult < { : string; restaurantName: string; smsCount: number } >> {
// return this.smsLogRepository.getSmsCountByRestaurant(page, limit);
// }
// async getSmsCountBy(: string): Promise < number > {
// return this.smsLogRepository.getSmsCountBy();
// }
}