notifcation

This commit is contained in:
2025-12-09 10:19:28 +03:30
parent 44fd209c92
commit ead1e096a5
16 changed files with 510 additions and 606 deletions
@@ -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}`);
}
}