update notif
This commit is contained in:
@@ -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> {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user