diff --git a/src/common/enums/permission.enum.ts b/src/common/enums/permission.enum.ts index 5d3c014..cd03754 100644 --- a/src/common/enums/permission.enum.ts +++ b/src/common/enums/permission.enum.ts @@ -3,6 +3,7 @@ * All permission names used in the system should be defined here */ export enum Permission { + MANAGE_PAGER = 'manage_pager', // Food Management MANAGE_FOODS = 'manage_foods', MANAGE_CATEGORIES = 'manage_categories', @@ -34,6 +35,7 @@ export enum Permission { * Maps permission names to their Farsi titles */ export const PermissionTitles: Record = { + [Permission.MANAGE_PAGER]: 'مدیریت پیجر', [Permission.MANAGE_FOODS]: 'مدیریت غذاها', [Permission.MANAGE_CATEGORIES]: 'مدیریت دسته‌بندی‌ها', [Permission.MANAGE_ORDERS]: 'مدیریت سفارشات', diff --git a/src/modules/admin/providers/admin.service.ts b/src/modules/admin/providers/admin.service.ts index 7d9fbd3..e88a3e5 100644 --- a/src/modules/admin/providers/admin.service.ts +++ b/src/modules/admin/providers/admin.service.ts @@ -174,4 +174,5 @@ export class AdminService { } return this.em.removeAndFlush(adminRole); } + } diff --git a/src/modules/admin/repositories/admin.repository.ts b/src/modules/admin/repositories/admin.repository.ts index 5a1db6c..e6cb6ea 100644 --- a/src/modules/admin/repositories/admin.repository.ts +++ b/src/modules/admin/repositories/admin.repository.ts @@ -47,4 +47,12 @@ export class AdminRepository extends EntityRepository { return adminRole.admin; } + async findAdminsWithPermission(restaurantId: string, permission: string): Promise { + const admins = await this.em.find( + Admin, + { roles: { restaurant: { id: restaurantId }, role: { permissions: { name: permission } } } }, + { populate: ['roles', 'roles.role', 'roles.role.permissions'] }, + ); + return admins; + } } diff --git a/src/modules/notifications/interfaces/notification.interface.ts b/src/modules/notifications/interfaces/notification.interface.ts index 7c98329..00c4fe5 100644 --- a/src/modules/notifications/interfaces/notification.interface.ts +++ b/src/modules/notifications/interfaces/notification.interface.ts @@ -9,19 +9,16 @@ export enum NotifTypeEnum { SYSTEM = 'system', } export enum NotifTitleEnum { + PAGER_CREATED = 'pager.created', ORDER_CREATED = 'order.created', PAYMENT_SUCCESS = 'payment.success', REVIEW_CREATED = 'review.created', ORDER_STATUS_CHANGED = 'order.status.changed', } -export type recipientType = - | { phone: string } - | { email: string } - | { fcmToken: string } - | { userId: string; restaurantId: string }; +export type recipientType = { userId: string; restaurantId: string } | { adminId: string; restaurantId: string }; export interface NotifRequestMessage { - subject: string; + subject: NotifTitleEnum; body: string; smsText: string; pushNotif: { @@ -36,11 +33,11 @@ export interface NotifRequestMessage { } export interface NotifRequest { - requestId: string; - timestamp: Date; - notifType: NotifTypeEnum; - channels: NotifChannelEnum[]; - recipient: recipientType; + // requestId: string; + // timestamp: Date; + // notifType: NotifTypeEnum; + // channels: NotifChannelEnum[]; + recipients: recipientType[]; message: NotifRequestMessage; metadata: { priority: number; diff --git a/src/modules/notifications/listeners/notification.listeners.ts b/src/modules/notifications/listeners/notification.listeners.ts index 7016411..1a8203d 100644 --- a/src/modules/notifications/listeners/notification.listeners.ts +++ b/src/modules/notifications/listeners/notification.listeners.ts @@ -15,78 +15,78 @@ export class NotificationListeners { constructor(private readonly notificationService: NotificationService) {} - @OnEvent(OrderCreatedEvent.name) - async handleOrderCreated(event: OrderCreatedEvent) { - try { - this.logger.log(`Order created event received: ${event.orderNumber}`); - console.log('Order created event received: ', event); - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - userId: event.userId, - title: NotifTitleEnum.ORDER_CREATED, - content: `Your order #${event.orderNumber} has created successfully`, - }); - } catch (error) { - this.logger.error( - `Failed to send notification for order.created event: ${event.orderNumber}`, - error instanceof Error ? error.stack : String(error), - ); - } - } - @OnEvent(OrderPaymentSuccessEvent.name) - async handleOrderPaymentSuccess(event: OrderPaymentSuccessEvent) { - try { - this.logger.log(`Order payment success event received: ${event.orderNumber}`); + // @OnEvent(OrderCreatedEvent.name) + // async handleOrderCreated(event: OrderCreatedEvent) { + // try { + // this.logger.log(`Order created event received: ${event.orderNumber}`); + // console.log('Order created event received: ', event); + // await this.notificationService.sendNotification({ + // restaurantId: event.restaurantId, + // userId: event.userId, + // title: NotifTitleEnum.ORDER_CREATED, + // content: `Your order #${event.orderNumber} has created successfully`, + // }); + // } catch (error) { + // this.logger.error( + // `Failed to send notification for order.created event: ${event.orderNumber}`, + // error instanceof Error ? error.stack : String(error), + // ); + // } + // } + // @OnEvent(OrderPaymentSuccessEvent.name) + // async handleOrderPaymentSuccess(event: OrderPaymentSuccessEvent) { + // try { + // this.logger.log(`Order payment success event received: ${event.orderNumber}`); - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - userId: event.userId, - title: NotifTitleEnum.ORDER_CREATED, - content: `Your Payement for order #${event.orderNumber} has been successful Paid ${event.totalAmount}`, - }); - } catch (error) { - this.logger.error( - `Failed to send notification for order.created event: ${event.orderNumber}`, - error instanceof Error ? error.stack : String(error), - ); - } - } + // await this.notificationService.sendNotification({ + // restaurantId: event.restaurantId, + // userId: event.userId, + // title: NotifTitleEnum.ORDER_CREATED, + // content: `Your Payement for order #${event.orderNumber} has been successful Paid ${event.totalAmount}`, + // }); + // } catch (error) { + // this.logger.error( + // `Failed to send notification for order.created event: ${event.orderNumber}`, + // error instanceof Error ? error.stack : String(error), + // ); + // } + // } - @OnEvent(ReviewCreatedEvent.name) - async handleReviewCreated(event: ReviewCreatedEvent) { - try { - this.logger.log(`Review created event received: ${event.reviewId}`); + // @OnEvent(ReviewCreatedEvent.name) + // async handleReviewCreated(event: ReviewCreatedEvent) { + // try { + // this.logger.log(`Review created event received: ${event.reviewId}`); - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - userId: event.userId, - title: NotifTitleEnum.REVIEW_CREATED, - content: 'Thank you for your review!', - }); - } catch (error) { - this.logger.error( - `Failed to send notification for review.created event: ${event.reviewId}`, - error instanceof Error ? error.stack : String(error), - ); - } - } + // await this.notificationService.sendNotification({ + // restaurantId: event.restaurantId, + // userId: event.userId, + // title: NotifTitleEnum.REVIEW_CREATED, + // content: 'Thank you for your review!', + // }); + // } catch (error) { + // this.logger.error( + // `Failed to send notification for review.created event: ${event.reviewId}`, + // error instanceof Error ? error.stack : String(error), + // ); + // } + // } - @OnEvent(OrderStatusChangedEvent.name) - async handleOrderStatusChanged(event: OrderStatusChangedEvent) { - try { - this.logger.log(`Order status changed: ${event.orderId} from ${event.oldStatus} to ${event.newStatus}`); + // @OnEvent(OrderStatusChangedEvent.name) + // async handleOrderStatusChanged(event: OrderStatusChangedEvent) { + // try { + // this.logger.log(`Order status changed: ${event.orderId} from ${event.oldStatus} to ${event.newStatus}`); - await this.notificationService.sendNotification({ - restaurantId: event.restaurantId, - userId: event.userId, - title: NotifTitleEnum.ORDER_STATUS_CHANGED, - content: `Your order #${event.orderId} is now ${event.newStatus}`, - }); - } catch (error) { - this.logger.error( - `Failed to send notification for order.status.changed event: ${event.orderId}`, - error instanceof Error ? error.stack : String(error), - ); - } - } + // await this.notificationService.sendNotification({ + // restaurantId: event.restaurantId, + // userId: event.userId, + // title: NotifTitleEnum.ORDER_STATUS_CHANGED, + // content: `Your order #${event.orderId} is now ${event.newStatus}`, + // }); + // } catch (error) { + // this.logger.error( + // `Failed to send notification for order.status.changed event: ${event.orderId}`, + // error instanceof Error ? error.stack : String(error), + // ); + // } + // } } diff --git a/src/modules/notifications/services/notification.service.ts b/src/modules/notifications/services/notification.service.ts index b374417..d0069bb 100644 --- a/src/modules/notifications/services/notification.service.ts +++ b/src/modules/notifications/services/notification.service.ts @@ -6,7 +6,7 @@ import { NotificationQueueService } from './notification-queue.service'; import { NotificationQueueJob } from '../interfaces/notification-queue.interface'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { User } from '../../users/entities/user.entity'; - import { NotifTitleEnum } from '../interfaces/notification.interface'; +import { NotifChannelEnum, NotifRequest, NotifTitleEnum } from '../interfaces/notification.interface'; export interface SendNotificationParams { restaurantId: string; @@ -26,52 +26,46 @@ export class NotificationService { private readonly queueService: NotificationQueueService, ) {} - async sendNotification(params: SendNotificationParams): Promise { - const { restaurantId, userId, title, content } = params; + async sendNotification(params: NotifRequest): Promise { + const { recipients, message, metadata } = params; - // Verify restaurant exists - const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); - if (!restaurant) { - throw new NotFoundException('Restaurant not found'); + // create Database notifications + const notifications = await this.createAdminBulkNotifications( + recipients.map(recipient => ({ + restaurantId: recipient.restaurantId, + title: message.subject, + content: message.body, + adminId: 'adminId' in recipient ? recipient.adminId : null, + userId: 'userId' in recipient ? recipient.userId : null, + })), + ); + const restaurantId = recipients[0].restaurantId; + // get admin prefrences + const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, message.subject); + + if (preference?.channels?.length === 0) { + this.logger.warn(`Notification type is NONE for restaurant ${restaurantId}, title ${message.subject}`); + return notifications; } - // Get notification preferences - const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, title); + // const job: NotificationQueueJob = { + // restaurantId, + // userId, + // title, + // content, + // // idempotencyKey: finalIdempotencyKey, + // notificationId: notification.id, + // channels: preference.channels, + // }; - if (!preference) { - this.logger.warn(`Notification preference not found or disabled for restaurant ${restaurantId}, type ${title}`); - throw new NotFoundException(`Notification preference not found or disabled for type ${title}`); - } - - // Generate idempotency key if not provided - // const finalIdempotencyKey = idempotencyKey || `${restaurantId}-${title}-${Date.now()}`; - - // Create notification record - const user = userId ? await this.em.findOne(User, { id: userId }) : undefined; - const notification = this.em.create(Notification, { - restaurant, - user, - title, - content, - // idempotencyKey: finalIdempotencyKey, - }); - - await this.em.persistAndFlush(notification); - - if (preference.channels.length === 0) { - this.logger.warn(`Notification type is NONE for restaurant ${restaurantId}, title ${title}`); - return notification; - } + // preference.channels.forEach(channel => { + // if (channel === NotifChannelEnum.SMS) { + // await this.queueService.addSmsNotification(job); + // } else if (channel === NotifChannelEnum.PUSH) { + // await this.queueService.addPushNotification(job); + // } + // }); // Add to queue - const job: NotificationQueueJob = { - restaurantId, - userId, - title, - content, - // idempotencyKey: finalIdempotencyKey, - notificationId: notification.id, - channels: preference.channels, - }; // if (preference.notificationType === NotificationType.SMS) { // await this.queueService.addSmsNotification(job); // } else if (preference.notificationType === NotificationType.PUSH) { @@ -81,9 +75,30 @@ export class NotificationService { // await this.queueService.addPushNotification(job); // } - this.logger.log(`Queued notification for restaurant ${restaurantId}, title ${title}`); + this.logger.log(`Queued notification for restaurant ${restaurantId}, title ${message.subject}`); - return notification; + return notifications; + } + + async createAdminBulkNotifications( + params: { + restaurantId: string; + title: NotifTitleEnum; + content: string; + adminId: string | null; + userId: string | null; + }[], + ): Promise { + const notifications = params.map(param => { + return this.em.create(Notification, { + restaurant: param.restaurantId, + admin: param.adminId, + title: param.title, + content: param.content, + }); + }); + await this.em.persistAndFlush(notifications); + return notifications; } async findOne(id: string): Promise { diff --git a/src/modules/pager/events/pager.events.ts b/src/modules/pager/events/pager.events.ts new file mode 100644 index 0000000..9035efe --- /dev/null +++ b/src/modules/pager/events/pager.events.ts @@ -0,0 +1,3 @@ +export class PagerCreatedEvent { + constructor(public readonly restaurantId: string) {} +} diff --git a/src/modules/pager/listeners/notification.listeners.ts b/src/modules/pager/listeners/notification.listeners.ts new file mode 100644 index 0000000..9b0f276 --- /dev/null +++ b/src/modules/pager/listeners/notification.listeners.ts @@ -0,0 +1,56 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; +import { PagerCreatedEvent } from '../events/pager.events'; +import { Permission } from 'src/common/enums/permission.enum'; +import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notification.interface'; +import { NotificationService } from 'src/modules/notifications/services/notification.service'; + +@Injectable() +export class PagerListeners { + private readonly logger = new Logger(PagerListeners.name); + + constructor( + private readonly adminService: AdminRepository, + private readonly notificationService: NotificationService, + ) {} + + @OnEvent(PagerCreatedEvent.name) + async handlePagerCreated(event: PagerCreatedEvent) { + try { + this.logger.log(`Pager created event received: ${event.restaurantId}`); + // get admnin os restuaraant that have pager permissuins + const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_PAGER); + const recipients = admins.map(admin => ({ + restaurantId: event.restaurantId, + adminId: admin.id, + })); + await this.notificationService.sendNotification({ + message: { + subject: NotifTitleEnum.PAGER_CREATED, + body: `Your pager has created successfully`, + smsText: `Your pager has created successfully`, + pushNotif: { + title: `Your pager has created successfully`, + body: `Your pager has created successfully`, + icon: `Your pager has created successfully`, + action: { + type: `Your pager has created successfully`, + url: `Your pager has created successfully`, + }, + }, + }, + recipients, + metadata: { + priority: 1, + retries: 3, + }, + }); + } catch (error) { + this.logger.error( + `Failed to send notification for pager created event: ${event.restaurantId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } +} diff --git a/src/modules/pager/providers/pager.service.ts b/src/modules/pager/providers/pager.service.ts index a9e18fc..83b2b02 100644 --- a/src/modules/pager/providers/pager.service.ts +++ b/src/modules/pager/providers/pager.service.ts @@ -7,6 +7,8 @@ import { Pager } from '../entities/pager.entity'; import { PagerStatus } from '../interface/pager'; import { ulid } from 'ulid'; import { Admin } from '../../admin/entities/admin.entity'; +import { PagerCreatedEvent } from '../events/pager.events'; +import { EventEmitter2 } from '@nestjs/event-emitter'; @Injectable() export class PagerService { @@ -14,6 +16,7 @@ export class PagerService { private readonly em: EntityManager, // @Inject(forwardRef(() => PagerGateway)) // private readonly pagerGateway: PagerGateway, + private readonly eventEmitter: EventEmitter2, ) {} async create( @@ -65,10 +68,10 @@ export class PagerService { await this.em.persistAndFlush(pager); // Populate relations before emitting - await this.em.populate(pager, ['restaurant', 'user']); - + await this.em.populate(pager, ['restaurant']); + console.log('pager', pager); // Emit socket event for new pager - // this.pagerGateway.emitPagerCreated(pager); + this.eventEmitter.emit(PagerCreatedEvent.name, new PagerCreatedEvent(pager.restaurant.id)); return pager; } diff --git a/src/seeders/data/notification-preferences.data.ts b/src/seeders/data/notification-preferences.data.ts index 3a3f37c..a007b10 100644 --- a/src/seeders/data/notification-preferences.data.ts +++ b/src/seeders/data/notification-preferences.data.ts @@ -6,6 +6,10 @@ export interface NotificationPreferenceData { } export const notificationPreferencesData: NotificationPreferenceData[] = [ + { + title: NotifTitleEnum.PAGER_CREATED, + channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], + }, { title: NotifTitleEnum.ORDER_CREATED, channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP],