notification queue

This commit is contained in:
2025-12-13 23:26:07 +03:30
parent 835f2a3853
commit befa311e98
20 changed files with 176 additions and 405 deletions
@@ -22,9 +22,6 @@ export class Notification extends BaseEntity {
@Property() @Property()
content!: string; content!: string;
// @Property({ nullable: true, unique: true })
// idempotencyKey?: string;
@Property({ nullable: true }) @Property({ nullable: true })
sentAt?: Date; sentAt?: Date;
} }
@@ -1,36 +0,0 @@
export class OrderCreatedEvent {
constructor(
public readonly restaurantId: string,
public readonly userId: string,
public readonly orderNumber: string,
public readonly totalAmount: number,
) {}
}
export class OrderPaymentSuccessEvent {
constructor(
public readonly restaurantId: string,
public readonly userId: string,
public readonly orderNumber: string,
public readonly totalAmount: number,
) {}
}
export class ReviewCreatedEvent {
constructor(
public readonly restaurantId: string,
public readonly userId: string,
public readonly reviewId: string,
public readonly reviewData: any,
) {}
}
export class OrderStatusChangedEvent {
constructor(
public readonly restaurantId: string,
public readonly userId: string,
public readonly orderId: string,
public readonly oldStatus: string,
public readonly newStatus: string,
) {}
}
@@ -0,0 +1,25 @@
import type { recipientType } from './notification.interface';
export interface SmsNotificationQueueJob {
recipient: recipientType;
templateId: string;
parameters?: Record<string, string>;
}
export interface PushNotificationQueueJob {
title: string;
content: string;
icon: string;
action: {
type: string; //view order
url: string;
};
pushToken?: string; // FCM token for push notifications
pushTokens?: string[]; // Multiple FCM tokens for bulk push notifications
}
export interface SmsNotificationQueueJobResult {
success: boolean;
notificationId: string;
providerResponse?: Record<string, any>;
error?: string;
}
@@ -1,21 +0,0 @@
import type { NotifChannelEnum } from './notification.interface';
import type { NotifTitleEnum } from './notification.interface';
export interface NotificationQueueJob {
restaurantId: string;
userId?: string;
title: NotifTitleEnum;
content: string;
idempotencyKey?: string;
notificationId?: string; // For retries
channels: NotifChannelEnum[];
pushToken?: string; // FCM token for push notifications
pushTokens?: string[]; // Multiple FCM tokens for bulk push notifications
}
export interface NotificationQueueJobResult {
success: boolean;
notificationId: string;
providerResponse?: Record<string, any>;
error?: string;
}
@@ -20,7 +20,10 @@ export type recipientType = { userId: string } | { adminId: string };
export interface NotifRequestMessage { export interface NotifRequestMessage {
title: NotifTitleEnum; title: NotifTitleEnum;
content: string; content: string;
smsText: string; sms: {
templateId: string;
parameters?: Record<string, string>;
};
pushNotif: { pushNotif: {
title: string; title: string;
content: string; content: string;
@@ -42,7 +45,7 @@ export interface NotifRequest {
message: NotifRequestMessage; message: NotifRequestMessage;
metadata: { metadata: {
priority: number; priority: number;
retries: number; // retries: number;
}; };
} }
//************************************************ */ //************************************************ */
@@ -54,6 +57,7 @@ interface INotifySms {
subject: NotifTitleEnum; subject: NotifTitleEnum;
} }
export interface IInAppNotificationPayload { export interface IInAppNotificationPayload {
notificationId: string;
subject: NotifTitleEnum; subject: NotifTitleEnum;
body: string; body: string;
} }
@@ -1,92 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { NotificationService } from '../services/notification.service';
import { NotifTitleEnum } from '../interfaces/notification.interface';
import {
OrderCreatedEvent,
ReviewCreatedEvent,
OrderStatusChangedEvent,
OrderPaymentSuccessEvent,
} from '../events/notification.events';
@Injectable()
export class NotificationListeners {
private readonly logger = new Logger(NotificationListeners.name);
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}`);
// 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}`);
// 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}`);
// 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),
// );
// }
// }
}
@@ -8,7 +8,7 @@ import {
// ConnectedSocket, // ConnectedSocket,
} from '@nestjs/websockets'; } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io'; import { Server, Socket } from 'socket.io';
import { Logger, UseGuards, Inject, forwardRef } from '@nestjs/common'; import { Logger, Inject, forwardRef } from '@nestjs/common';
import { NotificationService } from './services/notification.service'; import { NotificationService } from './services/notification.service';
import { WsAdminAuthGuard, type AuthenticatedSocket } from './guards/ws-admin-auth.guard'; import { WsAdminAuthGuard, type AuthenticatedSocket } from './guards/ws-admin-auth.guard';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
@@ -8,13 +8,11 @@ import { NotificationService } from './services/notification.service';
import { NotificationPreferenceService } from './services/notification-preference.service'; import { NotificationPreferenceService } from './services/notification-preference.service';
import { NotificationQueueService } from './services/notification-queue.service'; import { NotificationQueueService } from './services/notification-queue.service';
import { PushNotificationService } from './services/push-notification.service'; import { PushNotificationService } from './services/push-notification.service';
import { SmsAdaptorService } from './services/sms.adaptor';
import { SmsProcessor } from './processors/sms.processor'; import { SmsProcessor } from './processors/sms.processor';
import { PushProcessor } from './processors/push.processor'; import { PushProcessor } from './processors/push.processor';
import { NotificationsController } from './controllers/notifications.controller'; import { NotificationsController } from './controllers/notifications.controller';
import { User } from '../users/entities/user.entity'; import { User } from '../users/entities/user.entity';
import { Restaurant } from '../restaurants/entities/restaurant.entity'; import { Restaurant } from '../restaurants/entities/restaurant.entity';
import { NotificationListeners } from './listeners/notification.listeners';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { NotificationQueueNameEnum } from './constants/queue'; import { NotificationQueueNameEnum } from './constants/queue';
@@ -46,19 +44,11 @@ import { WsAdminAuthGuard } from './guards/ws-admin-auth.guard';
NotificationPreferenceService, NotificationPreferenceService,
NotificationQueueService, NotificationQueueService,
PushNotificationService, PushNotificationService,
SmsAdaptorService,
PushProcessor, PushProcessor,
SmsProcessor, SmsProcessor,
NotificationListeners,
NotificationsGateway, NotificationsGateway,
WsAdminAuthGuard, WsAdminAuthGuard,
], ],
exports: [ exports: [NotificationService, NotificationPreferenceService, NotificationQueueService, PushNotificationService],
NotificationService,
NotificationPreferenceService,
NotificationQueueService,
PushNotificationService,
SmsAdaptorService,
],
}) })
export class NotificationsModule {} export class NotificationsModule {}
@@ -4,7 +4,7 @@ import { Job } from 'bullmq';
import { InjectRepository } from '@mikro-orm/nestjs'; import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository, EntityManager } from '@mikro-orm/postgresql'; import { EntityRepository, EntityManager } from '@mikro-orm/postgresql';
import { Notification } from '../entities/notification.entity'; import { Notification } from '../entities/notification.entity';
import { NotificationQueueJob, NotificationQueueJobResult } from '../interfaces/notification-queue.interface'; import { PushNotificationQueueJob } from '../interfaces/jobs-queue.interface';
import { NotificationQueueNameEnum } from '../constants/queue'; import { NotificationQueueNameEnum } from '../constants/queue';
import { PushNotificationService } from '../services/push-notification.service'; import { PushNotificationService } from '../services/push-notification.service';
@@ -21,70 +21,18 @@ export class PushProcessor extends WorkerHost {
super(); super();
} }
async process(job: Job<NotificationQueueJob>): Promise<NotificationQueueJobResult> { async process(job: Job<PushNotificationQueueJob>) {
const { restaurantId, userId, title, content, idempotencyKey, notificationId } = job.data; const { title, content, action, pushToken, pushTokens } = job.data;
this.logger.log(`Processing push notification job: ${job.id} - Title: ${title}, Restaurant: ${restaurantId}`); this.logger.log(`Processing push notification job: ${job.id} - Title: ${title},`);
try { try {
// Find existing notification record (should already exist from NotificationService.sendNotification)
let notification: Notification | null = null;
if (notificationId) {
notification = await this.notificationRepository.findOne(
{ id: notificationId },
{ populate: ['user', 'restaurant'] },
);
if (!notification) {
throw new Error(`Notification ${notificationId} not found`);
}
} else if (idempotencyKey) {
// Fallback: find by idempotency key if notificationId not provided
notification = await this.notificationRepository.findOne(
{
id: 'sdfpsdfpsdf',
},
// { idempotencyKey },
// { populate: ['user', 'restaurant'] },
);
// if (notification && notification.sentAt) {
// this.logger.warn(`Duplicate notification skipped: ${idempotencyKey}`);
// return {
// success: true,
// notificationId: notification.id,
// };
// }
}
if (!notification) {
throw new Error(`Notification not found for job ${job.id}. notificationId or idempotencyKey required.`);
}
// Check if push notification service is configured
if (!this.pushNotificationService.isConfigured()) {
this.logger.warn(`Push notification service not configured. Skipping notification ${notification.id}`);
// Mark as sent even though we didn't send it (service unavailable)
notification.sentAt = new Date();
await this.em.persistAndFlush(notification);
return {
success: false,
notificationId: notification.id,
error: 'Push notification service not configured',
};
}
// For push notifications, we need a push token
if (!userId || !notification.user) {
this.logger.warn(`No user found for push notification ${notification.id}`);
throw new Error('User is required for push notifications');
}
// Get push token from job data // Get push token from job data
const pushToken = job.data.pushToken; const pushToken = job.data.pushToken;
const pushTokens = job.data.pushTokens; const pushTokens = job.data.pushTokens;
if (!pushToken && (!pushTokens || pushTokens.length === 0)) { if (!pushToken && (!pushTokens || pushTokens.length === 0)) {
this.logger.warn(`Push token(s) not provided for notification ${notification.id}`); this.logger.warn(`Push token(s) not provided for notification `);
throw new Error('Push token or pushTokens array is required for push notifications'); throw new Error('Push token or pushTokens array is required for push notifications');
} }
@@ -102,10 +50,7 @@ export class PushProcessor extends WorkerHost {
body: content, body: content,
tokens: pushTokens, tokens: pushTokens,
data: { data: {
notificationId: notification.id, action,
restaurantId,
userId,
title: String(title),
}, },
}); });
} else if (pushToken) { } else if (pushToken) {
@@ -115,10 +60,7 @@ export class PushProcessor extends WorkerHost {
body: content, body: content,
token: pushToken, token: pushToken,
data: { data: {
notificationId: notification.id, action,
restaurantId,
userId,
title: String(title),
}, },
}); });
} else { } else {
@@ -129,15 +71,11 @@ export class PushProcessor extends WorkerHost {
throw new Error(pushResult.error || 'Failed to send push notification'); throw new Error(pushResult.error || 'Failed to send push notification');
} }
// Mark notification as sent this.logger.log(`Push notification sent successfully`);
notification.sentAt = new Date();
await this.em.persistAndFlush(notification);
this.logger.log(`Push notification ${notification.id} sent successfully`);
return { return {
success: true, success: true,
notificationId: notification.id,
providerResponse: { messageId: pushResult.messageId }, providerResponse: { messageId: pushResult.messageId },
}; };
} catch (error) { } catch (error) {
@@ -2,14 +2,13 @@ import { Processor, WorkerHost, OnWorkerEvent } from '@nestjs/bullmq';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Job } from 'bullmq'; import { Job } from 'bullmq';
import { InjectRepository } from '@mikro-orm/nestjs'; import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository, EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
import { Notification } from '../entities/notification.entity'; import { Notification } from '../entities/notification.entity';
import { NotificationQueueJob, NotificationQueueJobResult } from '../interfaces/notification-queue.interface'; import { SmsNotificationQueueJob } from '../interfaces/jobs-queue.interface';
import { User } from '../../users/entities/user.entity'; import { User } from '../../users/entities/user.entity';
import { NotificationQueueNameEnum } from '../constants/queue'; import { NotificationQueueNameEnum } from '../constants/queue';
import { SmsAdaptorService } from '../services/sms.adaptor'; import { SmsService } from 'src/modules/utils/sms.service';
import { NotifTitleEnum } from '../interfaces/notification.interface'; import { Admin } from 'src/modules/admin/entities/admin.entity';
import { ISmsResponse } from '../../utils/interface/sms';
@Processor(NotificationQueueNameEnum.SMS) @Processor(NotificationQueueNameEnum.SMS)
export class SmsProcessor extends WorkerHost { export class SmsProcessor extends WorkerHost {
@@ -17,65 +16,50 @@ export class SmsProcessor extends WorkerHost {
constructor( constructor(
@InjectRepository(Notification) @InjectRepository(Notification)
private readonly notificationRepository: EntityRepository<Notification>,
private readonly em: EntityManager, private readonly em: EntityManager,
private readonly smsAdaptorService: SmsAdaptorService, private readonly smsService: SmsService,
) { ) {
super(); super();
} }
async process(job: Job<NotificationQueueJob>): Promise<NotificationQueueJobResult> { async process(job: Job<SmsNotificationQueueJob>) {
const { restaurantId, userId, title } = job.data; const { recipient, templateId, parameters } = job.data;
this.logger.log(`Processing SMS notification job: ${job.id} - Title: ${title}, this.logger.log(`Processing SMS notification - Recipient: ${JSON.stringify(recipient)}, templateID: ${templateId}`);
Restaurant: ${restaurantId}`);
try { try {
const user = await this.em.findOne(User, { id: userId }); let phone!: string;
if ('userId' in recipient) {
const user = await this.em.findOne(User, { id: recipient.userId });
if (!user || !user.phone) { if (!user || !user.phone) {
this.logger.warn(`User ${userId} not found or has no phone number`); this.logger.warn(`User ${recipient.userId} not found or has no phone number`);
throw new Error('User phone number is required for SMS notifications'); throw new Error('User phone number is required for SMS notifications');
} }
phone = user.phone;
// Check if the notification type is supported for SMS }
if (title !== NotifTitleEnum.ORDER_CREATED && title !== NotifTitleEnum.PAYMENT_SUCCESS) { if ('adminId' in recipient) {
throw new Error(`SMS notification type ${title} is not supported`); const admin = await this.em.findOne(Admin, { id: recipient.adminId });
if (!admin || !admin.phone) {
this.logger.warn(`Admin ${recipient.adminId} not found or has no phone number`);
throw new Error('Admin phone number is required for SMS notifications');
}
phone = admin.phone;
}
if (!phone) {
this.logger.warn(`Phone number not found for recipient ${JSON.stringify(recipient)}`);
throw new Error('Phone number is required for SMS notifications');
} }
// Send SMS notification // Send SMS notification
let smsResult: ISmsResponse; await this.smsService.sendSms({
// if (title === NotifTitleEnum.ORDER_CREATED) { phone,
// smsResult = await this.smsAdaptorService.sendNotifySms({ templateId,
// phone: user.phone, parameters,
// subject: NotifTitleEnum.ORDER_CREATED, });
// payload: {
// orderNumber: '1234567890',
// orderAmount: 100000,
// orderDate: new Date(),
// },
// });
// } else {
// smsResult = await this.smsAdaptorService.sendNotifySms({
// phone: user.phone,
// subject: NotifTitleEnum.PAYMENT_SUCCESS,
// payload: {
// amount: 100000,
// date: new Date(),
// },
// });
// }
// if (smsResult.status !== 1) { this.logger.log(`SMS notification sent successfully to ${phone}`);
// throw new Error(smsResult.message ?? 'Failed to send SMS notification');
// }
// Mark notification as sent
this.logger.log(`SMS notification sent successfully to ${user.phone}`);
return { return {
success: true, success: true,
notificationId: 'sdfpsdfpsdf',
}; };
} catch (error) { } catch (error) {
this.logger.error(`Error processing SMS notification job ${job.id}:`, error); this.logger.error(`Error processing SMS notification job ${job.id}:`, error);
@@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq'; import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { NotificationQueueNameEnum } from '../constants/queue'; import { NotificationQueueNameEnum } from '../constants/queue';
import { NotificationQueueJob } from '../interfaces/notification-queue.interface'; import { PushNotificationQueueJob, SmsNotificationQueueJob } from '../interfaces/jobs-queue.interface';
@Injectable() @Injectable()
export class NotificationQueueService { export class NotificationQueueService {
@@ -13,9 +13,9 @@ export class NotificationQueueService {
@InjectQueue(NotificationQueueNameEnum.PUSH) private readonly pushQueue: Queue, @InjectQueue(NotificationQueueNameEnum.PUSH) private readonly pushQueue: Queue,
) {} ) {}
async addSmsNotification(job: NotificationQueueJob): Promise<void> { async addSmsNotification(job: SmsNotificationQueueJob): Promise<void> {
try { try {
const jobId = job.idempotencyKey || `${job.restaurantId}-${job.title}-${Date.now()}`; const jobId = `${job.templateId}-${JSON.stringify(job.parameters)}-${Date.now()}`;
await this.smsQueue.add('sms-notification', job, { await this.smsQueue.add('sms-notification', job, {
jobId, jobId,
@@ -40,9 +40,9 @@ export class NotificationQueueService {
} }
} }
async addPushNotification(job: NotificationQueueJob): Promise<void> { async addPushNotification(job: PushNotificationQueueJob): Promise<void> {
try { try {
const jobId = job.idempotencyKey || `${job.restaurantId}-${job.title}-${Date.now()}`; const jobId = `${job.title}-${JSON.stringify(job.action)}-${Date.now()}`;
await this.pushQueue.add('push-notification', job, { await this.pushQueue.add('push-notification', job, {
jobId, jobId,
@@ -67,26 +67,49 @@ export class NotificationQueueService {
} }
} }
// async addBulkNotifications(jobs: NotificationQueueJob[]): Promise<void> { async addBulkSmsNotifications(jobs: SmsNotificationQueueJob[]): Promise<void> {
// try { try {
// const queueJobs = jobs.map(job => ({ const queueJobs = jobs.map(job => ({
// name: 'send-notification', name: 'sms-notification',
// data: job, data: job,
// opts: { opts: {
// jobId: job.idempotencyKey || `${job.restaurantId}-${job.title}-${Date.now()}`, jobId: `${job.templateId}-${JSON.stringify(job.parameters)}-${Date.now()}`,
// attempts: 3, attempts: 3,
// backoff: { backoff: {
// type: 'exponential', type: 'exponential',
// delay: 2000, delay: 2000,
// }, },
// }, },
// })); }));
// await this.notificationQueue.addBulk(queueJobs); await this.smsQueue.addBulk(queueJobs);
// this.logger.log(`Added ${jobs.length} notification jobs to queue`); this.logger.log(`Added ${jobs.length} SMS notification jobs to queue`);
// } catch (error) { } catch (error) {
// this.logger.error(`Failed to add bulk notifications to queue:`, error); this.logger.error(`Failed to add bulk notifications to queue:`, error);
// throw error; throw error;
// } }
// } }
async addBulkPushNotifications(jobs: PushNotificationQueueJob[]): Promise<void> {
try {
const queueJobs = jobs.map(job => ({
name: 'push-notification',
data: job,
opts: {
jobId: `${job.title}-${JSON.stringify(job.action)}-${Date.now()}`,
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
},
}));
await this.pushQueue.addBulk(queueJobs);
this.logger.log(`Added ${jobs.length} Push notification jobs to queue`);
} catch (error) {
this.logger.error(`Failed to add bulk notifications to queue:`, error);
throw error;
}
}
} }
@@ -4,9 +4,6 @@ import { Notification } from '../entities/notification.entity';
import { NotificationPreferenceService } from './notification-preference.service'; import { NotificationPreferenceService } from './notification-preference.service';
import { NotificationQueueService } from './notification-queue.service'; import { NotificationQueueService } from './notification-queue.service';
import { NotificationsGateway } from '../notifications.gateway'; import { NotificationsGateway } from '../notifications.gateway';
import { NotificationQueueJob } from '../interfaces/notification-queue.interface';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { User } from '../../users/entities/user.entity';
import { NotifChannelEnum, NotifRequest, NotifTitleEnum } from '../interfaces/notification.interface'; import { NotifChannelEnum, NotifRequest, NotifTitleEnum } from '../interfaces/notification.interface';
@Injectable() @Injectable()
@@ -44,11 +41,12 @@ export class NotificationService {
// send in app notification // send in app notification
if (preference?.channels?.includes(NotifChannelEnum.IN_APP)) { if (preference?.channels?.includes(NotifChannelEnum.IN_APP)) {
recipients.forEach(recipient => { notifications.forEach(notif => {
if ('adminId' in recipient) { if (notif.admin) {
this.notificationGateway.sendInAppNotification( this.notificationGateway.sendInAppNotification(
{ adminId: recipient.adminId, restaurantId }, { adminId: notif.admin.id, restaurantId },
{ {
notificationId: notif.id,
subject: message.title, subject: message.title,
body: message.content, body: message.content,
}, },
@@ -56,32 +54,16 @@ export class NotificationService {
} }
}); });
} }
// const job: NotificationQueueJob = { // add sms notifications to queue
// restaurantId, if (preference?.channels?.includes(NotifChannelEnum.SMS)) {
// userId, await this.queueService.addBulkSmsNotifications(
// title, recipients.map(recipient => ({
// content, recipient,
// // idempotencyKey: finalIdempotencyKey, templateId: message.sms.templateId,
// notificationId: notification.id, parameters: message.sms.parameters,
// channels: preference.channels, })),
// }; );
}
// 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
// if (preference.notificationType === NotificationType.SMS) {
// await this.queueService.addSmsNotification(job);
// } else if (preference.notificationType === NotificationType.PUSH) {
// await this.queueService.addPushNotification(job);
// } else if (preference.notificationType === NotificationType.Both) {
// await this.queueService.addSmsNotification(job);
// await this.queueService.addPushNotification(job);
// }
this.logger.log(`Queued notification for restaurant ${restaurantId}, title ${message.title}`); this.logger.log(`Queued notification for restaurant ${restaurantId}, title ${message.title}`);
@@ -1,36 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SmsService } from 'src/modules/utils/sms.service';
import { ISmsNotifyPayload } from '../interfaces/notification.interface';
import { NotifTitleEnum } from '../interfaces/notification.interface';
@Injectable()
export class SmsAdaptorService {
private readonly smsPatternOrderCreated: string;
private readonly smsPatternPaymentSuccess: string;
constructor(
private readonly configService: ConfigService,
private readonly smsService: SmsService,
) {
this.smsPatternOrderCreated = this.configService.get<string>('SMS_PATTERN_ORDER_CREATED') ?? '1';
this.smsPatternPaymentSuccess = this.configService.get<string>('SMS_PATTERN_PAYMENT_SUCCESS') ?? '2';
}
async sendNotifySms(params: ISmsNotifyPayload) {
const { phone, subject, payload } = params;
let templateId!: string;
switch (subject) {
// case NotifTitleEnum.ORDER_CREATED:
// templateId = this.smsPatternOrderCreated;
// break;
case NotifTitleEnum.PAYMENT_SUCCESS:
templateId = this.smsPatternPaymentSuccess;
break;
}
return this.smsService.sendSms({
phone,
parameters: payload,
templateId,
});
}
}
@@ -31,7 +31,12 @@ export class OrderListeners {
message: { message: {
title: NotifTitleEnum.ORDER_CREATED, title: NotifTitleEnum.ORDER_CREATED,
content: `Order ${event.orderNumber} has been created successfully`, content: `Order ${event.orderNumber} has been created successfully`,
smsText: `Order ${event.orderNumber} has been created successfully`, sms: {
templateId: '1234567890',
parameters: {
orderNumber: event.orderNumber,
},
},
pushNotif: { pushNotif: {
title: `Order ${event.orderNumber} has been created successfully`, title: `Order ${event.orderNumber} has been created successfully`,
content: `Order ${event.orderNumber} has been created successfully`, content: `Order ${event.orderNumber} has been created successfully`,
@@ -45,7 +50,6 @@ export class OrderListeners {
recipients, recipients,
metadata: { metadata: {
priority: 1, priority: 1,
retries: 3,
}, },
}); });
} catch (error) { } catch (error) {
@@ -20,7 +20,6 @@ import { OrderRepository } from '../repositories/order.repository';
import { FindOrdersDto } from '../dto/find-orders.dto'; import { FindOrdersDto } from '../dto/find-orders.dto';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { OrderCreatedEvent, OrderPaymentSuccessEvent } from '../../notifications/events/notification.events';
@Injectable() @Injectable()
export class OrdersService { export class OrdersService {
@@ -49,14 +48,14 @@ export class OrdersService {
await this.cartService.clearCart(userId, restaurantId); await this.cartService.clearCart(userId, restaurantId);
this.eventEmitter2.emit( // this.eventEmitter2.emit(
OrderCreatedEvent.name, // OrderCreatedEvent.name,
new OrderCreatedEvent(restaurantId, order.user.id, order.id, order.total), // new OrderCreatedEvent(restaurantId, order.user.id, order.id, order.total),
); // );
this.eventEmitter2.emit( // this.eventEmitter2.emit(
OrderPaymentSuccessEvent.name, // OrderPaymentSuccessEvent.name,
new OrderPaymentSuccessEvent(restaurantId, order.user.id, order.id, order.total), // new OrderPaymentSuccessEvent(restaurantId, order.user.id, order.id, order.total),
); // );
console.log('Order created event emitted 111'); console.log('Order created event emitted 111');
return { order, paymentUrl }; return { order, paymentUrl };
} }
+4 -1
View File
@@ -1,3 +1,6 @@
export class PagerCreatedEvent { export class PagerCreatedEvent {
constructor(public readonly restaurantId: string) {} constructor(
public readonly restaurantId: string,
public readonly tableNumber: string,
) {}
} }
@@ -28,22 +28,26 @@ export class PagerListeners {
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
message: { message: {
title: NotifTitleEnum.PAGER_CREATED, title: NotifTitleEnum.PAGER_CREATED,
content: `Your pager has created successfully`, content: `میز شماره ${event.tableNumber} پیج جدیدی انجام داد`,
smsText: `Your pager has created successfully`, sms: {
templateId: '1234567890',
parameters: {
tableNumber: event.tableNumber.toString(),
},
},
pushNotif: { pushNotif: {
title: `Your pager has created successfully`, title: ` پیج جدید`,
content: `Your pager has created successfully`, content: `میز شماره ${event.tableNumber} پیج جدیدی انجام داد`,
icon: `Your pager has created successfully`, icon: `/assets/images/logo.png`,
action: { action: {
type: `Your pager has created successfully`, type: NotifTitleEnum.PAGER_CREATED,
url: `Your pager has created successfully`, url: `/restaurants/${event.restaurantId}/pagers`,
}, },
}, },
}, },
recipients, recipients,
metadata: { metadata: {
priority: 1, priority: 1,
retries: 3,
}, },
}); });
} catch (error) { } catch (error) {
+1 -1
View File
@@ -70,7 +70,7 @@ export class PagerService {
// Populate relations before emitting // Populate relations before emitting
await this.em.populate(pager, ['restaurant']); await this.em.populate(pager, ['restaurant']);
// Emit socket event for new pager // Emit socket event for new pager
this.eventEmitter.emit(PagerCreatedEvent.name, new PagerCreatedEvent(pager.restaurant.id)); this.eventEmitter.emit(PagerCreatedEvent.name, new PagerCreatedEvent(pager.restaurant.id, pager.tableNumber));
return pager; return pager;
} }
+1 -1
View File
@@ -5,7 +5,7 @@ export interface ISmsResponse {
export interface ISmsParams { export interface ISmsParams {
phone: string; phone: string;
parameters: Record<string, string | number | Date>; parameters?: Record<string, string | number | Date>;
templateId: string; templateId: string;
} }
+4 -1
View File
@@ -28,7 +28,10 @@ export class SmsService {
async sendSms(params: ISmsParams) { async sendSms(params: ISmsParams) {
const { parameters, phone, templateId } = params; const { parameters, phone, templateId } = params;
const parametersArray = Object.entries(parameters).map(([name, value]) => ({ name, value: value.toString() })); const parametersArray = Object.entries(parameters ?? {}).map(([name, value]) => ({
name,
value: value.toString(),
}));
const smsData: ISmsBodyParameters = { const smsData: ISmsBodyParameters = {
Parameters: parametersArray, Parameters: parametersArray,