update notif

This commit is contained in:
2025-12-12 21:28:46 +03:30
parent ecb68de7b3
commit 249e7f7581
10 changed files with 216 additions and 127 deletions
+2
View File
@@ -3,6 +3,7 @@
* All permission names used in the system should be defined here * All permission names used in the system should be defined here
*/ */
export enum Permission { export enum Permission {
MANAGE_PAGER = 'manage_pager',
// Food Management // Food Management
MANAGE_FOODS = 'manage_foods', MANAGE_FOODS = 'manage_foods',
MANAGE_CATEGORIES = 'manage_categories', MANAGE_CATEGORIES = 'manage_categories',
@@ -34,6 +35,7 @@ export enum Permission {
* Maps permission names to their Farsi titles * Maps permission names to their Farsi titles
*/ */
export const PermissionTitles: Record<Permission, string> = { export const PermissionTitles: Record<Permission, string> = {
[Permission.MANAGE_PAGER]: 'مدیریت پیجر',
[Permission.MANAGE_FOODS]: 'مدیریت غذاها', [Permission.MANAGE_FOODS]: 'مدیریت غذاها',
[Permission.MANAGE_CATEGORIES]: 'مدیریت دسته‌بندی‌ها', [Permission.MANAGE_CATEGORIES]: 'مدیریت دسته‌بندی‌ها',
[Permission.MANAGE_ORDERS]: 'مدیریت سفارشات', [Permission.MANAGE_ORDERS]: 'مدیریت سفارشات',
@@ -174,4 +174,5 @@ export class AdminService {
} }
return this.em.removeAndFlush(adminRole); return this.em.removeAndFlush(adminRole);
} }
} }
@@ -47,4 +47,12 @@ export class AdminRepository extends EntityRepository<Admin> {
return adminRole.admin; return adminRole.admin;
} }
async findAdminsWithPermission(restaurantId: string, permission: string): Promise<Admin[]> {
const admins = await this.em.find(
Admin,
{ roles: { restaurant: { id: restaurantId }, role: { permissions: { name: permission } } } },
{ populate: ['roles', 'roles.role', 'roles.role.permissions'] },
);
return admins;
}
} }
@@ -9,19 +9,16 @@ export enum NotifTypeEnum {
SYSTEM = 'system', SYSTEM = 'system',
} }
export enum NotifTitleEnum { export enum NotifTitleEnum {
PAGER_CREATED = 'pager.created',
ORDER_CREATED = 'order.created', ORDER_CREATED = 'order.created',
PAYMENT_SUCCESS = 'payment.success', PAYMENT_SUCCESS = 'payment.success',
REVIEW_CREATED = 'review.created', REVIEW_CREATED = 'review.created',
ORDER_STATUS_CHANGED = 'order.status.changed', ORDER_STATUS_CHANGED = 'order.status.changed',
} }
export type recipientType = export type recipientType = { userId: string; restaurantId: string } | { adminId: string; restaurantId: string };
| { phone: string }
| { email: string }
| { fcmToken: string }
| { userId: string; restaurantId: string };
export interface NotifRequestMessage { export interface NotifRequestMessage {
subject: string; subject: NotifTitleEnum;
body: string; body: string;
smsText: string; smsText: string;
pushNotif: { pushNotif: {
@@ -36,11 +33,11 @@ export interface NotifRequestMessage {
} }
export interface NotifRequest { export interface NotifRequest {
requestId: string; // requestId: string;
timestamp: Date; // timestamp: Date;
notifType: NotifTypeEnum; // notifType: NotifTypeEnum;
channels: NotifChannelEnum[]; // channels: NotifChannelEnum[];
recipient: recipientType; recipients: recipientType[];
message: NotifRequestMessage; message: NotifRequestMessage;
metadata: { metadata: {
priority: number; priority: number;
@@ -15,78 +15,78 @@ export class NotificationListeners {
constructor(private readonly notificationService: NotificationService) {} constructor(private readonly notificationService: NotificationService) {}
@OnEvent(OrderCreatedEvent.name) // @OnEvent(OrderCreatedEvent.name)
async handleOrderCreated(event: OrderCreatedEvent) { // async handleOrderCreated(event: OrderCreatedEvent) {
try { // try {
this.logger.log(`Order created event received: ${event.orderNumber}`); // this.logger.log(`Order created event received: ${event.orderNumber}`);
console.log('Order created event received: ', event); // console.log('Order created event received: ', event);
await this.notificationService.sendNotification({ // await this.notificationService.sendNotification({
restaurantId: event.restaurantId, // restaurantId: event.restaurantId,
userId: event.userId, // userId: event.userId,
title: NotifTitleEnum.ORDER_CREATED, // title: NotifTitleEnum.ORDER_CREATED,
content: `Your order #${event.orderNumber} has created successfully`, // content: `Your order #${event.orderNumber} has created successfully`,
}); // });
} catch (error) { // } catch (error) {
this.logger.error( // this.logger.error(
`Failed to send notification for order.created event: ${event.orderNumber}`, // `Failed to send notification for order.created event: ${event.orderNumber}`,
error instanceof Error ? error.stack : String(error), // error instanceof Error ? error.stack : String(error),
); // );
} // }
} // }
@OnEvent(OrderPaymentSuccessEvent.name) // @OnEvent(OrderPaymentSuccessEvent.name)
async handleOrderPaymentSuccess(event: OrderPaymentSuccessEvent) { // async handleOrderPaymentSuccess(event: OrderPaymentSuccessEvent) {
try { // try {
this.logger.log(`Order payment success event received: ${event.orderNumber}`); // this.logger.log(`Order payment success event received: ${event.orderNumber}`);
await this.notificationService.sendNotification({ // await this.notificationService.sendNotification({
restaurantId: event.restaurantId, // restaurantId: event.restaurantId,
userId: event.userId, // userId: event.userId,
title: NotifTitleEnum.ORDER_CREATED, // title: NotifTitleEnum.ORDER_CREATED,
content: `Your Payement for order #${event.orderNumber} has been successful Paid ${event.totalAmount}`, // content: `Your Payement for order #${event.orderNumber} has been successful Paid ${event.totalAmount}`,
}); // });
} catch (error) { // } catch (error) {
this.logger.error( // this.logger.error(
`Failed to send notification for order.created event: ${event.orderNumber}`, // `Failed to send notification for order.created event: ${event.orderNumber}`,
error instanceof Error ? error.stack : String(error), // error instanceof Error ? error.stack : String(error),
); // );
} // }
} // }
@OnEvent(ReviewCreatedEvent.name) // @OnEvent(ReviewCreatedEvent.name)
async handleReviewCreated(event: ReviewCreatedEvent) { // async handleReviewCreated(event: ReviewCreatedEvent) {
try { // try {
this.logger.log(`Review created event received: ${event.reviewId}`); // this.logger.log(`Review created event received: ${event.reviewId}`);
await this.notificationService.sendNotification({ // await this.notificationService.sendNotification({
restaurantId: event.restaurantId, // restaurantId: event.restaurantId,
userId: event.userId, // userId: event.userId,
title: NotifTitleEnum.REVIEW_CREATED, // title: NotifTitleEnum.REVIEW_CREATED,
content: 'Thank you for your review!', // content: 'Thank you for your review!',
}); // });
} catch (error) { // } catch (error) {
this.logger.error( // this.logger.error(
`Failed to send notification for review.created event: ${event.reviewId}`, // `Failed to send notification for review.created event: ${event.reviewId}`,
error instanceof Error ? error.stack : String(error), // error instanceof Error ? error.stack : String(error),
); // );
} // }
} // }
@OnEvent(OrderStatusChangedEvent.name) // @OnEvent(OrderStatusChangedEvent.name)
async handleOrderStatusChanged(event: OrderStatusChangedEvent) { // async handleOrderStatusChanged(event: OrderStatusChangedEvent) {
try { // try {
this.logger.log(`Order status changed: ${event.orderId} from ${event.oldStatus} to ${event.newStatus}`); // this.logger.log(`Order status changed: ${event.orderId} from ${event.oldStatus} to ${event.newStatus}`);
await this.notificationService.sendNotification({ // await this.notificationService.sendNotification({
restaurantId: event.restaurantId, // restaurantId: event.restaurantId,
userId: event.userId, // userId: event.userId,
title: NotifTitleEnum.ORDER_STATUS_CHANGED, // title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `Your order #${event.orderId} is now ${event.newStatus}`, // content: `Your order #${event.orderId} is now ${event.newStatus}`,
}); // });
} catch (error) { // } catch (error) {
this.logger.error( // this.logger.error(
`Failed to send notification for order.status.changed event: ${event.orderId}`, // `Failed to send notification for order.status.changed event: ${event.orderId}`,
error instanceof Error ? error.stack : String(error), // error instanceof Error ? error.stack : String(error),
); // );
} // }
} // }
} }
@@ -6,7 +6,7 @@ import { NotificationQueueService } from './notification-queue.service';
import { NotificationQueueJob } from '../interfaces/notification-queue.interface'; import { NotificationQueueJob } from '../interfaces/notification-queue.interface';
import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { User } from '../../users/entities/user.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 { export interface SendNotificationParams {
restaurantId: string; restaurantId: string;
@@ -26,52 +26,46 @@ export class NotificationService {
private readonly queueService: NotificationQueueService, private readonly queueService: NotificationQueueService,
) {} ) {}
async sendNotification(params: SendNotificationParams): Promise<Notification> { async sendNotification(params: NotifRequest): Promise<Notification[]> {
const { restaurantId, userId, title, content } = params; const { recipients, message, metadata } = params;
// Verify restaurant exists // create Database notifications
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); const notifications = await this.createAdminBulkNotifications(
if (!restaurant) { recipients.map(recipient => ({
throw new NotFoundException('Restaurant not found'); 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 job: NotificationQueueJob = {
const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, title); // restaurantId,
// userId,
// title,
// content,
// // idempotencyKey: finalIdempotencyKey,
// notificationId: notification.id,
// channels: preference.channels,
// };
if (!preference) { // preference.channels.forEach(channel => {
this.logger.warn(`Notification preference not found or disabled for restaurant ${restaurantId}, type ${title}`); // if (channel === NotifChannelEnum.SMS) {
throw new NotFoundException(`Notification preference not found or disabled for type ${title}`); // await this.queueService.addSmsNotification(job);
} // } else if (channel === NotifChannelEnum.PUSH) {
// await this.queueService.addPushNotification(job);
// 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;
}
// Add to queue // Add to queue
const job: NotificationQueueJob = {
restaurantId,
userId,
title,
content,
// idempotencyKey: finalIdempotencyKey,
notificationId: notification.id,
channels: preference.channels,
};
// if (preference.notificationType === NotificationType.SMS) { // if (preference.notificationType === NotificationType.SMS) {
// await this.queueService.addSmsNotification(job); // await this.queueService.addSmsNotification(job);
// } else if (preference.notificationType === NotificationType.PUSH) { // } else if (preference.notificationType === NotificationType.PUSH) {
@@ -81,9 +75,30 @@ export class NotificationService {
// await this.queueService.addPushNotification(job); // 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<Notification[]> {
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<Notification> { async findOne(id: string): Promise<Notification> {
+3
View File
@@ -0,0 +1,3 @@
export class PagerCreatedEvent {
constructor(public readonly restaurantId: string) {}
}
@@ -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),
);
}
}
}
+6 -3
View File
@@ -7,6 +7,8 @@ import { Pager } from '../entities/pager.entity';
import { PagerStatus } from '../interface/pager'; import { PagerStatus } from '../interface/pager';
import { ulid } from 'ulid'; import { ulid } from 'ulid';
import { Admin } from '../../admin/entities/admin.entity'; import { Admin } from '../../admin/entities/admin.entity';
import { PagerCreatedEvent } from '../events/pager.events';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable() @Injectable()
export class PagerService { export class PagerService {
@@ -14,6 +16,7 @@ export class PagerService {
private readonly em: EntityManager, private readonly em: EntityManager,
// @Inject(forwardRef(() => PagerGateway)) // @Inject(forwardRef(() => PagerGateway))
// private readonly pagerGateway: PagerGateway, // private readonly pagerGateway: PagerGateway,
private readonly eventEmitter: EventEmitter2,
) {} ) {}
async create( async create(
@@ -65,10 +68,10 @@ export class PagerService {
await this.em.persistAndFlush(pager); await this.em.persistAndFlush(pager);
// Populate relations before emitting // 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 // Emit socket event for new pager
// this.pagerGateway.emitPagerCreated(pager); this.eventEmitter.emit(PagerCreatedEvent.name, new PagerCreatedEvent(pager.restaurant.id));
return pager; return pager;
} }
@@ -6,6 +6,10 @@ export interface NotificationPreferenceData {
} }
export const notificationPreferencesData: NotificationPreferenceData[] = [ export const notificationPreferencesData: NotificationPreferenceData[] = [
{
title: NotifTitleEnum.PAGER_CREATED,
channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP],
},
{ {
title: NotifTitleEnum.ORDER_CREATED, title: NotifTitleEnum.ORDER_CREATED,
channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP],