notification queue
This commit is contained in:
@@ -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 (!user || !user.phone) {
|
if ('userId' in recipient) {
|
||||||
this.logger.warn(`User ${userId} not found or has no phone number`);
|
const user = await this.em.findOne(User, { id: recipient.userId });
|
||||||
throw new Error('User phone number is required for SMS notifications');
|
if (!user || !user.phone) {
|
||||||
|
this.logger.warn(`User ${recipient.userId} not found or has no phone number`);
|
||||||
|
throw new Error('User phone number is required for SMS notifications');
|
||||||
|
}
|
||||||
|
phone = user.phone;
|
||||||
}
|
}
|
||||||
|
if ('adminId' in recipient) {
|
||||||
// Check if the notification type is supported for SMS
|
const admin = await this.em.findOne(Admin, { id: recipient.adminId });
|
||||||
if (title !== NotifTitleEnum.ORDER_CREATED && title !== NotifTitleEnum.PAYMENT_SUCCESS) {
|
if (!admin || !admin.phone) {
|
||||||
throw new Error(`SMS notification type ${title} is not supported`);
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,23 +28,27 @@ 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) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user