notifcation
This commit is contained in:
@@ -1,26 +1,18 @@
|
||||
import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Notification, NotificationStatus, NotificationChannel } from '../entities/notification.entity';
|
||||
import { Notification, NotificationTitle } from '../entities/notification.entity';
|
||||
import { NotificationPreferenceService } from './notification-preference.service';
|
||||
import { NotificationQueueService } from './notification-queue.service';
|
||||
import { NotificationQueueJob } from '../interfaces/notification-queue.interface';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
import { NotificationType } from '../interfaces/notification.interface';
|
||||
|
||||
export interface SendNotificationParams {
|
||||
restaurantId: string;
|
||||
userId?: string;
|
||||
notificationType: string;
|
||||
payload: {
|
||||
title?: string;
|
||||
message: string;
|
||||
body?: string;
|
||||
phoneNumber?: string;
|
||||
pushToken?: string;
|
||||
data?: Record<string, any>;
|
||||
templateId?: string;
|
||||
parameters?: Array<{ name: string; value: string }>;
|
||||
};
|
||||
title: NotificationTitle;
|
||||
content: string;
|
||||
idempotencyKey?: string;
|
||||
}
|
||||
|
||||
@@ -34,8 +26,8 @@ export class NotificationService {
|
||||
private readonly queueService: NotificationQueueService,
|
||||
) {}
|
||||
|
||||
async sendNotification(params: SendNotificationParams): Promise<Notification[]> {
|
||||
const { restaurantId, userId, notificationType, payload, idempotencyKey } = params;
|
||||
async sendNotification(params: SendNotificationParams): Promise<Notification> {
|
||||
const { restaurantId, userId, title, content, idempotencyKey } = params;
|
||||
|
||||
// Verify restaurant exists
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||
@@ -44,78 +36,54 @@ export class NotificationService {
|
||||
}
|
||||
|
||||
// Get notification preferences
|
||||
const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, notificationType);
|
||||
const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, title);
|
||||
|
||||
if (!preference || !preference.enabled) {
|
||||
this.logger.warn(
|
||||
`Notification preference not found or disabled for restaurant ${restaurantId}, type ${notificationType}`,
|
||||
);
|
||||
return [];
|
||||
if (!preference) {
|
||||
this.logger.warn(`Notification preference not found or disabled for restaurant ${restaurantId}, type ${title}`);
|
||||
throw new NotFoundException(`Notification preference not found or disabled for type ${title}`);
|
||||
}
|
||||
|
||||
// Determine which channels to use
|
||||
const channels: NotificationChannel[] = [];
|
||||
if (preference.channels.sms && payload.phoneNumber) {
|
||||
channels.push(NotificationChannel.SMS);
|
||||
// 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.notificationType === NotificationType.NONE) {
|
||||
this.logger.warn(`Notification type is NONE for restaurant ${restaurantId}, title ${title}`);
|
||||
return notification;
|
||||
}
|
||||
if (preference.channels.push && payload.pushToken) {
|
||||
channels.push(NotificationChannel.PUSH);
|
||||
// Add to queue
|
||||
const job: NotificationQueueJob = {
|
||||
restaurantId,
|
||||
userId,
|
||||
title,
|
||||
content,
|
||||
idempotencyKey: finalIdempotencyKey,
|
||||
notificationId: notification.id,
|
||||
notificationType: preference.notificationType,
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
if (channels.length === 0) {
|
||||
this.logger.warn(`No enabled channels for notification type ${notificationType}`);
|
||||
return [];
|
||||
}
|
||||
this.logger.log(`Queued notification for restaurant ${restaurantId}, title ${title}`);
|
||||
|
||||
// Create notification records and queue jobs
|
||||
const notifications: Notification[] = [];
|
||||
const jobs: NotificationQueueJob[] = [];
|
||||
|
||||
for (const channel of channels) {
|
||||
const channelIdempotencyKey = idempotencyKey
|
||||
? `${idempotencyKey}-${channel}`
|
||||
: `${restaurantId}-${notificationType}-${channel}-${Date.now()}`;
|
||||
|
||||
// Create notification record
|
||||
const user = userId ? await this.em.findOne(User, { id: userId }) : undefined;
|
||||
const notification = this.em.create(Notification, {
|
||||
restaurant,
|
||||
user,
|
||||
notificationType,
|
||||
channel,
|
||||
payload,
|
||||
status: NotificationStatus.PENDING,
|
||||
idempotencyKey: channelIdempotencyKey,
|
||||
attemptCount: 0,
|
||||
});
|
||||
|
||||
await this.em.persistAndFlush(notification);
|
||||
notifications.push(notification);
|
||||
|
||||
// Add to queue
|
||||
jobs.push({
|
||||
restaurantId,
|
||||
userId,
|
||||
notificationType,
|
||||
channel,
|
||||
payload,
|
||||
idempotencyKey: channelIdempotencyKey,
|
||||
notificationId: notification.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Add all jobs to queue
|
||||
if (jobs.length === 1) {
|
||||
await this.queueService.addNotification(jobs[0]);
|
||||
} else {
|
||||
await this.queueService.addBulkNotifications(jobs);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Queued ${notifications.length} notification(s) for restaurant ${restaurantId}, type ${notificationType}`,
|
||||
);
|
||||
|
||||
return notifications;
|
||||
return notification;
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Notification> {
|
||||
@@ -138,24 +106,31 @@ export class NotificationService {
|
||||
);
|
||||
}
|
||||
|
||||
async findByUserId(userId: string, limit = 50): Promise<Notification[]> {
|
||||
async findByUserAndRestaurant(userId: string, restaurantId: string, limit = 50): Promise<Notification[]> {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{ user: { id: userId } },
|
||||
{ user: { id: userId }, restaurant: { id: restaurantId } },
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
populate: ['restaurant'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async findByRestaurantAndType(restaurantId: string, notificationType: string, limit = 50): Promise<Notification[]> {
|
||||
async removeNotification(id: string, restaurantId: string): Promise<void> {
|
||||
const notification = await this.em.findOne(Notification, { id, restaurant: { id: restaurantId } });
|
||||
if (!notification) {
|
||||
throw new NotFoundException('Notification not found');
|
||||
}
|
||||
await this.em.removeAndFlush(notification as any);
|
||||
}
|
||||
|
||||
async findByRestaurantAndType(restaurantId: string, title: NotificationTitle, limit = 50): Promise<Notification[]> {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{
|
||||
restaurant: { id: restaurantId },
|
||||
notificationType,
|
||||
title,
|
||||
},
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
@@ -164,27 +139,4 @@ export class NotificationService {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async retryFailedNotification(notificationId: string): Promise<void> {
|
||||
const notification = await this.findOne(notificationId);
|
||||
|
||||
if (notification.status !== NotificationStatus.FAILED) {
|
||||
throw new BadRequestException('Only failed notifications can be retried');
|
||||
}
|
||||
|
||||
// Reset status and add to queue
|
||||
notification.status = NotificationStatus.PENDING;
|
||||
await this.em.persistAndFlush(notification);
|
||||
|
||||
await this.queueService.addNotification({
|
||||
restaurantId: notification.restaurant.id,
|
||||
notificationType: notification.notificationType,
|
||||
channel: notification.channel,
|
||||
payload: notification.payload,
|
||||
idempotencyKey: notification.idempotencyKey,
|
||||
notificationId: notification.id,
|
||||
});
|
||||
|
||||
this.logger.log(`Retry queued for notification ${notificationId}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user