notification
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Notification, NotificationStatus, NotificationChannel } 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';
|
||||
|
||||
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 }>;
|
||||
};
|
||||
idempotencyKey?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService {
|
||||
private readonly logger = new Logger(NotificationService.name);
|
||||
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly preferenceService: NotificationPreferenceService,
|
||||
private readonly queueService: NotificationQueueService,
|
||||
) {}
|
||||
|
||||
async sendNotification(params: SendNotificationParams): Promise<Notification[]> {
|
||||
const { restaurantId, userId, notificationType, payload, idempotencyKey } = params;
|
||||
|
||||
// Verify restaurant exists
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException('Restaurant not found');
|
||||
}
|
||||
|
||||
// Get notification preferences
|
||||
const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, notificationType);
|
||||
|
||||
if (!preference || !preference.enabled) {
|
||||
this.logger.warn(
|
||||
`Notification preference not found or disabled for restaurant ${restaurantId}, type ${notificationType}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Determine which channels to use
|
||||
const channels: NotificationChannel[] = [];
|
||||
if (preference.channels.sms && payload.phoneNumber) {
|
||||
channels.push(NotificationChannel.SMS);
|
||||
}
|
||||
if (preference.channels.push && payload.pushToken) {
|
||||
channels.push(NotificationChannel.PUSH);
|
||||
}
|
||||
|
||||
if (channels.length === 0) {
|
||||
this.logger.warn(`No enabled channels for notification type ${notificationType}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Notification> {
|
||||
const notification = await this.em.findOne(Notification, { id }, { populate: ['restaurant', 'user'] });
|
||||
if (!notification) {
|
||||
throw new NotFoundException('Notification not found');
|
||||
}
|
||||
return notification;
|
||||
}
|
||||
|
||||
async findByRestaurant(restaurantId: string, limit = 50): Promise<Notification[]> {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{ restaurant: { id: restaurantId } },
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
populate: ['user'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async findByUserId(userId: string, limit = 50): Promise<Notification[]> {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{ user: { id: userId } },
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
populate: ['restaurant'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async findByRestaurantAndType(restaurantId: string, notificationType: string, limit = 50): Promise<Notification[]> {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{
|
||||
restaurant: { id: restaurantId },
|
||||
notificationType,
|
||||
},
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
populate: ['user'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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