notification

This commit is contained in:
2025-12-12 16:45:53 +03:30
parent f57145edc2
commit 3c3e5797e5
13 changed files with 241 additions and 172 deletions
@@ -1,6 +1,6 @@
import { IsNotEmpty, IsEnum } from 'class-validator'; import { IsNotEmpty, IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { NotificationTitleEnum } from '../interfaces/notification.interface'; import { NotifTitleEnum } from '../interfaces/notification.interface';
import { NotificationType } from '../interfaces/notification.interface'; import { NotificationType } from '../interfaces/notification.interface';
export class CreatePreferenceDto { export class CreatePreferenceDto {
@@ -15,10 +15,10 @@ export class CreatePreferenceDto {
@ApiProperty({ @ApiProperty({
description: 'Notification title/type (e.g., order.created, review.created)', description: 'Notification title/type (e.g., order.created, review.created)',
enum: NotificationTitleEnum, enum: NotifTitleEnum,
example: NotificationTitleEnum.ORDER_CREATED, example: NotifTitleEnum.ORDER_CREATED,
}) })
@IsEnum(NotificationTitleEnum) @IsEnum(NotifTitleEnum)
@IsNotEmpty() @IsNotEmpty()
title!: NotificationTitleEnum; title!: NotifTitleEnum;
} }
@@ -1,8 +1,9 @@
import { Entity, Property, ManyToOne, Unique, Enum } from '@mikro-orm/core'; import { Entity, Property, ManyToOne, Unique } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity'; import { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { NotificationTitleEnum } from '../interfaces/notification.interface'; import { NotifTitleEnum } from '../interfaces/notification.interface';
import { NotificationType } from '../interfaces/notification.interface'; import { Admin } from 'src/modules/admin/entities/admin.entity';
import { NotifChannelEnum } from '../interfaces/notification.interface';
@Entity({ tableName: 'notification_preferences' }) @Entity({ tableName: 'notification_preferences' })
@Unique({ properties: ['restaurant', 'title'] }) @Unique({ properties: ['restaurant', 'title'] })
@@ -10,9 +11,12 @@ export class NotificationPreference extends BaseEntity {
@ManyToOne(() => Restaurant) @ManyToOne(() => Restaurant)
restaurant!: Restaurant; restaurant!: Restaurant;
@Property() @ManyToOne(() => Admin, { nullable: true })
title!: NotificationTitleEnum; admin?: Admin;
@Enum(() => NotificationType) @Property()
notificationType!: NotificationType; //sms, push, email title!: NotifTitleEnum;
@Property({ type: 'json' })
channels: NotifChannelEnum[] = [];
} }
@@ -2,7 +2,8 @@ import { Entity, Property, ManyToOne, Enum } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity'; import { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { User } from '../../users/entities/user.entity'; import { User } from '../../users/entities/user.entity';
import { NotificationTitleEnum } from '../interfaces/notification.interface'; import { NotifTitleEnum } from '../interfaces/notification.interface';
import { Admin } from 'src/modules/admin/entities/admin.entity';
@Entity({ tableName: 'notifications' }) @Entity({ tableName: 'notifications' })
export class Notification extends BaseEntity { export class Notification extends BaseEntity {
@@ -12,8 +13,11 @@ export class Notification extends BaseEntity {
@ManyToOne(() => User, { nullable: true }) @ManyToOne(() => User, { nullable: true })
user?: User; user?: User;
@Enum(() => NotificationTitleEnum) @ManyToOne(() => Admin, { nullable: true })
title!: NotificationTitleEnum; admin?: Admin;
@Enum(() => NotifTitleEnum)
title!: NotifTitleEnum;
@Property() @Property()
content!: string; content!: string;
@@ -101,4 +101,25 @@ export class WsAdminAuthGuard implements CanActivate {
return undefined; return undefined;
} }
// private extractToken(client: Socket): string | undefined {
// const tryGet = (...values: any[]) => values.find(v => typeof v === 'string' && v.length > 0);
// let token = tryGet(
// client.handshake.auth?.token,
// client.handshake.auth?.authorization,
// client.handshake.query?.token,
// client.handshake.query?.authorization,
// client.handshake.headers.authorization,
// client.handshake.headers['authorization'],
// );
// if (!token) return undefined;
// if (token.startsWith('Bearer ')) {
// token = token.slice(7);
// }
// return token;
// }
} }
@@ -1,10 +1,10 @@
import type { NotificationType } from './notification.interface'; import type { NotificationType } from './notification.interface';
import type { NotificationTitleEnum } from './notification.interface'; import type { NotifTitleEnum } from './notification.interface';
export interface NotificationQueueJob { export interface NotificationQueueJob {
restaurantId: string; restaurantId: string;
userId?: string; userId?: string;
title: NotificationTitleEnum; title: NotifTitleEnum;
content: string; content: string;
idempotencyKey?: string; idempotencyKey?: string;
notificationId?: string; // For retries notificationId?: string; // For retries
@@ -1,22 +1,59 @@
export enum NotificationTitleEnum { export enum NotifChannelEnum {
IN_APP = 'in-app',
SMS = 'sms',
PUSH = 'push',
}
export enum NotifTypeEnum {
TRANSACTIONAL = 'transactional',
PROMOTIONAL = 'promotional',
SYSTEM = 'system',
}
export enum NotifTitleEnum {
ORDER_CREATED = 'order.created', ORDER_CREATED = 'order.created',
PAYMENT_SUCCESS = 'payment.success', PAYMENT_SUCCESS = 'payment.success',
REVIEW_CREATED = 'review.created', REVIEW_CREATED = 'review.created',
ORDER_STATUS_CHANGED = 'order.status.changed', ORDER_STATUS_CHANGED = 'order.status.changed',
} }
export type recipientType =
| { phone: string }
| { email: string }
| { fcmToken: string }
| { userId: string; restaurantId: string };
export enum NotificationType { export interface NotifRequestMessage {
NONE = 'none', subject: string;
SMS = 'sms', body: string;
PUSH = 'push', smsText: string;
Both = 'both', pushNotif: {
title: string;
body: string;
icon: string;
action: {
type: string; //view order
url: string;
};
};
} }
export interface NotifRequest {
requestId: string;
timestamp: Date;
notifType: NotifTypeEnum;
channels: NotifChannelEnum[];
recipient: recipientType;
message: NotifRequestMessage;
metadata: {
priority: number;
retries: number;
};
}
//************************************************ */
interface INotifySmsPayload { interface INotifySmsPayload {
[key: string]: string | number | Date; [key: string]: string | number | Date;
} }
interface INotifySms { interface INotifySms {
phone: string; phone: string;
subject: NotificationTitleEnum; subject: NotifTitleEnum;
} }
// Sub intefaces // Sub intefaces
// 1. Order Created Sms Notify Payload // 1. Order Created Sms Notify Payload
@@ -27,7 +64,7 @@ interface IOrderCreatedSmsNotifyPayload extends INotifySmsPayload {
} }
interface IOrderCreatedSmsNotify extends INotifySms { interface IOrderCreatedSmsNotify extends INotifySms {
subject: NotificationTitleEnum.ORDER_CREATED; subject: NotifTitleEnum.ORDER_CREATED;
payload: IOrderCreatedSmsNotifyPayload; payload: IOrderCreatedSmsNotifyPayload;
} }
// 2. Payment Success // 2. Payment Success
@@ -37,7 +74,7 @@ interface IPaymentSuccessSmsNotifyPayload extends INotifySmsPayload {
} }
interface IPaymentSuccessSmsNotify extends INotifySms { interface IPaymentSuccessSmsNotify extends INotifySms {
subject: NotificationTitleEnum.PAYMENT_SUCCESS; subject: NotifTitleEnum.PAYMENT_SUCCESS;
payload: IPaymentSuccessSmsNotifyPayload; payload: IPaymentSuccessSmsNotifyPayload;
} }
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { NotificationService } from '../services/notification.service'; import { NotificationService } from '../services/notification.service';
import { NotificationTitleEnum } from '../interfaces/notification.interface'; import { NotifTitleEnum } from '../interfaces/notification.interface';
import { import {
OrderCreatedEvent, OrderCreatedEvent,
ReviewCreatedEvent, ReviewCreatedEvent,
@@ -23,7 +23,7 @@ export class NotificationListeners {
await this.notificationService.sendNotification({ await this.notificationService.sendNotification({
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
userId: event.userId, userId: event.userId,
title: NotificationTitleEnum.ORDER_CREATED, title: NotifTitleEnum.ORDER_CREATED,
content: `Your order #${event.orderNumber} has created successfully`, content: `Your order #${event.orderNumber} has created successfully`,
}); });
} catch (error) { } catch (error) {
@@ -41,7 +41,7 @@ export class NotificationListeners {
await this.notificationService.sendNotification({ await this.notificationService.sendNotification({
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
userId: event.userId, userId: event.userId,
title: NotificationTitleEnum.ORDER_CREATED, title: NotifTitleEnum.ORDER_CREATED,
content: `Your Payement for order #${event.orderNumber} has been successful Paid ${event.totalAmount}`, content: `Your Payement for order #${event.orderNumber} has been successful Paid ${event.totalAmount}`,
}); });
} catch (error) { } catch (error) {
@@ -60,7 +60,7 @@ export class NotificationListeners {
await this.notificationService.sendNotification({ await this.notificationService.sendNotification({
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
userId: event.userId, userId: event.userId,
title: NotificationTitleEnum.REVIEW_CREATED, title: NotifTitleEnum.REVIEW_CREATED,
content: 'Thank you for your review!', content: 'Thank you for your review!',
}); });
} catch (error) { } catch (error) {
@@ -79,7 +79,7 @@ export class NotificationListeners {
await this.notificationService.sendNotification({ await this.notificationService.sendNotification({
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
userId: event.userId, userId: event.userId,
title: NotificationTitleEnum.ORDER_STATUS_CHANGED, title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `Your order #${event.orderId} is now ${event.newStatus}`, content: `Your order #${event.orderId} is now ${event.newStatus}`,
}); });
} catch (error) { } catch (error) {
+123 -113
View File
@@ -1,15 +1,15 @@
import { import {
WebSocketGateway, WebSocketGateway,
WebSocketServer, WebSocketServer,
SubscribeMessage, // SubscribeMessage,
OnGatewayConnection, OnGatewayConnection,
OnGatewayDisconnect, OnGatewayDisconnect,
MessageBody, // MessageBody,
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, UseGuards, Inject, forwardRef } from '@nestjs/common';
import { WsRestId } from './decorators/ws-rest-id.decorator'; // import { WsRestId } from './decorators/ws-rest-id.decorator';
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';
@@ -21,6 +21,7 @@ import { WsAdminAuthGuard, type AuthenticatedSocket } from './guards/ws-admin-au
connectionGuard: WsAdminAuthGuard, connectionGuard: WsAdminAuthGuard,
}, },
namespace: '/notifications', namespace: '/notifications',
transports: ['websocket', 'polling'],
}) })
export class NotificationGateway implements OnGatewayConnection, OnGatewayDisconnect { export class NotificationGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() @WebSocketServer()
@@ -33,7 +34,8 @@ export class NotificationGateway implements OnGatewayConnection, OnGatewayDiscon
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
) {} ) {}
handleConnection(client: Socket) { handleConnection(client: AuthenticatedSocket) {
console.log(client);
this.logger.log(`Client connected: ${client.id}`); this.logger.log(`Client connected: ${client.id}`);
} }
@@ -41,147 +43,155 @@ export class NotificationGateway implements OnGatewayConnection, OnGatewayDiscon
this.logger.log(`Client disconnected: ${client.id}`); this.logger.log(`Client disconnected: ${client.id}`);
} }
sendAdminNotification(client: AuthenticatedSocket, event: string, data: any) {
const room = this.getRoom(client);
this.server.to(room).emit(event, data);
}
private getRoom(client: AuthenticatedSocket) {
return `restaurant:${client.restId}-admin:${client.adminId}`;
}
/** /**
* Join a room for a specific restaurant (admin/staff) * Join a room for a specific restaurant (admin/staff)
* Requires admin authentication via JWT token * Requires admin authentication via JWT token
* Restaurant ID is extracted from the authenticated admin's token * Restaurant ID is extracted from the authenticated admin's token
* Room format: `restaurant:${restId}` * Room format: `restaurant:${restId}`
*/ */
@UseGuards(WsAdminAuthGuard) // @UseGuards(WsAdminAuthGuard)
@SubscribeMessage('join:restaurant') // @SubscribeMessage('join:restaurant')
handleJoinRestaurant(@ConnectedSocket() client: AuthenticatedSocket, @WsRestId() restId: string) { // handleJoinRestaurant(@ConnectedSocket() client: AuthenticatedSocket, @WsRestId() restId: string) {
const room = `restaurant:${restId}`; // const room = `restaurant:${restId}`;
void client.join(room); // void client.join(room);
this.logger.log(`Admin ${client.adminId} (Client ${client.id}) joined room: ${room}`); // this.logger.log(`Admin ${client.adminId} (Client ${client.id}) joined room: ${room}`);
void client.emit('joined', { room, message: 'Successfully joined restaurant room' }); // void client.emit('joined', { room, message: 'Successfully joined restaurant room' });
} // }
/** /**
* Join a room for a specific cookie/session (public user) * Join a room for a specific cookie/session (public user)
* Room format: `cookie:${cookieId}` * Room format: `cookie:${cookieId}`
*/ */
@SubscribeMessage('join:session') // @SubscribeMessage('join:session')
handleJoinSession(@ConnectedSocket() client: Socket, @MessageBody() data: { cookieId: string }) { // handleJoinSession(@ConnectedSocket() client: Socket, @MessageBody() data: { cookieId: string }) {
if (!data.cookieId) { // if (!data.cookieId) {
void client.emit('error', { message: 'Cookie ID is required' }); // void client.emit('error', { message: 'Cookie ID is required' });
return; // return;
} // }
const room = `cookie:${data.cookieId}`; // const room = `cookie:${data.cookieId}`;
void client.join(room); // void client.join(room);
this.logger.log(`Client ${client.id} joined room: ${room}`); // this.logger.log(`Client ${client.id} joined room: ${room}`);
void client.emit('joined', { room, message: 'Successfully joined session room' }); // void client.emit('joined', { room, message: 'Successfully joined session room' });
} // }
/** /**
* Leave a room * Leave a room
*/ */
@SubscribeMessage('leave:room') // @SubscribeMessage('leave:room')
handleLeaveRoom(@ConnectedSocket() client: Socket, @MessageBody() data: { room: string }) { // handleLeaveRoom(@ConnectedSocket() client: Socket, @MessageBody() data: { room: string }) {
if (data.room) { // if (data.room) {
void client.leave(data.room); // void client.leave(data.room);
this.logger.log(`Client ${client.id} left room: ${data.room}`); // this.logger.log(`Client ${client.id} left room: ${data.room}`);
void client.emit('left', { room: data.room, message: 'Successfully left room' }); // void client.emit('left', { room: data.room, message: 'Successfully left room' });
} // }
} // }
/** /**
* Get list of pagers for the restaurant (admin only) * Get list of pagers for the restaurant (admin only)
* Requires admin authentication via JWT token * Requires admin authentication via JWT token
* Restaurant ID is extracted from the authenticated admin's token * Restaurant ID is extracted from the authenticated admin's token
*/ */
@UseGuards(WsAdminAuthGuard) // @UseGuards(WsAdminAuthGuard)
@SubscribeMessage('get:pagers') // @SubscribeMessage('get:pagers')
async handleGetPagers(@ConnectedSocket() client: AuthenticatedSocket, @WsRestId() restId: string) { // async handleGetPagers(@ConnectedSocket() client: AuthenticatedSocket, @WsRestId() restId: string) {
try { // try {
const pagers = await this.pagerService.findAllByRestaurantId(restId); // const pagers = await this.pagerService.findAllByRestaurantId(restId);
const pagersData = pagers.map(pager => ({ // const pagersData = pagers.map(pager => ({
id: pager.id, // id: pager.id,
tableNumber: pager.tableNumber, // tableNumber: pager.tableNumber,
message: pager.message, // message: pager.message,
status: pager.status, // status: pager.status,
cookieId: pager.cookieId, // cookieId: pager.cookieId,
createdAt: pager.createdAt, // createdAt: pager.createdAt,
updatedAt: pager.updatedAt, // updatedAt: pager.updatedAt,
user: pager.user // user: pager.user
? { // ? {
id: pager.user.id, // id: pager.user.id,
firstName: pager.user.firstName, // firstName: pager.user.firstName,
lastName: pager.user.lastName, // lastName: pager.user.lastName,
} // }
: null, // : null,
staff: pager.staff // staff: pager.staff
? { // ? {
id: pager.staff.id, // id: pager.staff.id,
firstName: pager.staff.firstName, // firstName: pager.staff.firstName,
lastName: pager.staff.lastName, // lastName: pager.staff.lastName,
} // }
: null, // : null,
})); // }));
void client.emit('pagers:list', { pagers: pagersData }); // void client.emit('pagers:list', { pagers: pagersData });
this.logger.log(`Admin ${client.adminId} requested pagers list for restaurant ${restId}`); // this.logger.log(`Admin ${client.adminId} requested pagers list for restaurant ${restId}`);
} catch (error) { // } catch (error) {
this.logger.error(`Error getting pagers list: ${error instanceof Error ? error.message : 'Unknown error'}`); // this.logger.error(`Error getting pagers list: ${error instanceof Error ? error.message : 'Unknown error'}`);
void client.emit('error', { // void client.emit('error', {
message: 'Failed to get pagers list', // message: 'Failed to get pagers list',
code: 'GET_PAGERS_ERROR', // code: 'GET_PAGERS_ERROR',
details: error instanceof Error ? error.message : 'Unknown error', // details: error instanceof Error ? error.message : 'Unknown error',
}); // });
} // }
} // }
/** /**
* Update pager status (admin only) * Update pager status (admin only)
* Requires admin authentication via JWT token * Requires admin authentication via JWT token
* Restaurant ID and Admin ID are extracted from the authenticated admin's token * Restaurant ID and Admin ID are extracted from the authenticated admin's token
*/ */
@UseGuards(WsAdminAuthGuard) // @UseGuards(WsAdminAuthGuard)
@SubscribeMessage('update:pager:status') // @SubscribeMessage('update:pager:status')
async handleUpdatePagerStatus( // async handleUpdatePagerStatus(
@ConnectedSocket() client: AuthenticatedSocket, // @ConnectedSocket() client: AuthenticatedSocket,
@WsRestId() restId: string, // @WsRestId() restId: string,
@WsAdminId() adminId: string, // @WsAdminId() adminId: string,
@MessageBody() data: { pagerId: string; status: PagerStatus }, // @MessageBody() data: { pagerId: string; status: PagerStatus },
) { // ) {
try { // try {
if (!data.pagerId || !data.status) { // if (!data.pagerId || !data.status) {
void client.emit('error', { // void client.emit('error', {
message: 'Pager ID and status are required', // message: 'Pager ID and status are required',
code: 'INVALID_PAYLOAD', // code: 'INVALID_PAYLOAD',
}); // });
return; // return;
} // }
const pager = await this.pagerService.updateStatus(data.pagerId, restId, adminId, data.status); // const pager = await this.pagerService.updateStatus(data.pagerId, restId, adminId, data.status);
const pagerData = { // const pagerData = {
id: pager.id, // id: pager.id,
tableNumber: pager.tableNumber, // tableNumber: pager.tableNumber,
message: pager.message, // message: pager.message,
status: pager.status, // status: pager.status,
updatedAt: pager.updatedAt, // updatedAt: pager.updatedAt,
staff: pager.staff // staff: pager.staff
? { // ? {
id: pager.staff.id, // id: pager.staff.id,
firstName: pager.staff.firstName, // firstName: pager.staff.firstName,
lastName: pager.staff.lastName, // lastName: pager.staff.lastName,
} // }
: null, // : null,
}; // };
void client.emit('pager:status:updated:response', { pager: pagerData }); // void client.emit('pager:status:updated:response', { pager: pagerData });
this.logger.log(`Admin ${adminId} updated pager ${data.pagerId} status to ${data.status}`); // this.logger.log(`Admin ${adminId} updated pager ${data.pagerId} status to ${data.status}`);
} catch (error) { // } catch (error) {
this.logger.error(`Error updating pager status: ${error instanceof Error ? error.message : 'Unknown error'}`); // this.logger.error(`Error updating pager status: ${error instanceof Error ? error.message : 'Unknown error'}`);
void client.emit('error', { // void client.emit('error', {
message: error instanceof Error ? error.message : 'Failed to update pager status', // message: error instanceof Error ? error.message : 'Failed to update pager status',
code: 'UPDATE_PAGER_STATUS_ERROR', // code: 'UPDATE_PAGER_STATUS_ERROR',
details: error instanceof Error ? error.message : 'Unknown error', // details: error instanceof Error ? error.message : 'Unknown error',
}); // });
} // }
} // }
// /** // /**
// * Emit pager created event to restaurant room // * Emit pager created event to restaurant room
@@ -8,7 +8,7 @@ import { NotificationQueueJob, NotificationQueueJobResult } from '../interfaces/
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 { SmsAdaptorService } from '../services/sms.adaptor';
import { NotificationTitleEnum } from '../interfaces/notification.interface'; import { NotifTitleEnum } from '../interfaces/notification.interface';
import { ISmsResponse } from '../../utils/interface/sms'; import { ISmsResponse } from '../../utils/interface/sms';
@Processor(NotificationQueueNameEnum.SMS) @Processor(NotificationQueueNameEnum.SMS)
@@ -38,16 +38,16 @@ export class SmsProcessor extends WorkerHost {
} }
// Check if the notification type is supported for SMS // Check if the notification type is supported for SMS
if (title !== NotificationTitleEnum.ORDER_CREATED && title !== NotificationTitleEnum.PAYMENT_SUCCESS) { if (title !== NotifTitleEnum.ORDER_CREATED && title !== NotifTitleEnum.PAYMENT_SUCCESS) {
throw new Error(`SMS notification type ${title} is not supported`); throw new Error(`SMS notification type ${title} is not supported`);
} }
// Send SMS notification // Send SMS notification
let smsResult: ISmsResponse; let smsResult: ISmsResponse;
if (title === NotificationTitleEnum.ORDER_CREATED) { if (title === NotifTitleEnum.ORDER_CREATED) {
smsResult = await this.smsAdaptorService.sendNotifySms({ smsResult = await this.smsAdaptorService.sendNotifySms({
phone: user.phone, phone: user.phone,
subject: NotificationTitleEnum.ORDER_CREATED, subject: NotifTitleEnum.ORDER_CREATED,
payload: { payload: {
orderNumber: '1234567890', orderNumber: '1234567890',
orderAmount: 100000, orderAmount: 100000,
@@ -57,7 +57,7 @@ export class SmsProcessor extends WorkerHost {
} else { } else {
smsResult = await this.smsAdaptorService.sendNotifySms({ smsResult = await this.smsAdaptorService.sendNotifySms({
phone: user.phone, phone: user.phone,
subject: NotificationTitleEnum.PAYMENT_SUCCESS, subject: NotifTitleEnum.PAYMENT_SUCCESS,
payload: { payload: {
amount: 100000, amount: 100000,
date: new Date(), date: new Date(),
@@ -4,7 +4,7 @@ import { NotificationPreference } from '../entities/notification-preference.enti
import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { CreatePreferenceDto } from '../dto/create-preference.dto'; import { CreatePreferenceDto } from '../dto/create-preference.dto';
import { UpdatePreferenceDto } from '../dto/update-preference.dto'; import { UpdatePreferenceDto } from '../dto/update-preference.dto';
import { NotificationTitleEnum } from '../interfaces/notification.interface'; import { NotifTitleEnum } from '../interfaces/notification.interface';
@Injectable() @Injectable()
export class NotificationPreferenceService { export class NotificationPreferenceService {
@@ -32,10 +32,7 @@ export class NotificationPreferenceService {
}); });
} }
async findByRestaurantAndType( async findByRestaurantAndType(restaurantId: string, title: NotifTitleEnum): Promise<NotificationPreference | null> {
restaurantId: string,
title: NotificationTitleEnum,
): Promise<NotificationPreference | null> {
return this.em.findOne(NotificationPreference, { return this.em.findOne(NotificationPreference, {
restaurant: { id: restaurantId }, restaurant: { id: restaurantId },
title, title,
@@ -7,12 +7,12 @@ import { NotificationQueueJob } from '../interfaces/notification-queue.interface
import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { User } from '../../users/entities/user.entity'; import { User } from '../../users/entities/user.entity';
import { NotificationType } from '../interfaces/notification.interface'; import { NotificationType } from '../interfaces/notification.interface';
import { NotificationTitleEnum } from '../interfaces/notification.interface'; import { NotifTitleEnum } from '../interfaces/notification.interface';
export interface SendNotificationParams { export interface SendNotificationParams {
restaurantId: string; restaurantId: string;
userId?: string; userId?: string;
title: NotificationTitleEnum; title: NotifTitleEnum;
content: string; content: string;
idempotencyKey?: string; idempotencyKey?: string;
} }
@@ -45,7 +45,7 @@ export class NotificationService {
} }
// Generate idempotency key if not provided // Generate idempotency key if not provided
const finalIdempotencyKey = idempotencyKey || `${restaurantId}-${title}-${Date.now()}`; // const finalIdempotencyKey = idempotencyKey || `${restaurantId}-${title}-${Date.now()}`;
// Create notification record // Create notification record
const user = userId ? await this.em.findOne(User, { id: userId }) : undefined; const user = userId ? await this.em.findOne(User, { id: userId }) : undefined;
@@ -69,7 +69,7 @@ export class NotificationService {
userId, userId,
title, title,
content, content,
idempotencyKey: finalIdempotencyKey, // idempotencyKey: finalIdempotencyKey,
notificationId: notification.id, notificationId: notification.id,
notificationType: preference.notificationType, notificationType: preference.notificationType,
}; };
@@ -126,11 +126,7 @@ export class NotificationService {
await this.em.removeAndFlush(notification as any); await this.em.removeAndFlush(notification as any);
} }
async findByRestaurantAndType( async findByRestaurantAndType(restaurantId: string, title: NotifTitleEnum, limit = 50): Promise<Notification[]> {
restaurantId: string,
title: NotificationTitleEnum,
limit = 50,
): Promise<Notification[]> {
return this.em.find( return this.em.find(
Notification, Notification,
{ {
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { SmsService } from 'src/modules/utils/sms.service'; import { SmsService } from 'src/modules/utils/sms.service';
import { ISmsNotifyPayload } from '../interfaces/notification.interface'; import { ISmsNotifyPayload } from '../interfaces/notification.interface';
import { NotificationTitleEnum } from '../interfaces/notification.interface'; import { NotifTitleEnum } from '../interfaces/notification.interface';
@Injectable() @Injectable()
export class SmsAdaptorService { export class SmsAdaptorService {
@@ -20,10 +20,10 @@ export class SmsAdaptorService {
const { phone, subject, payload } = params; const { phone, subject, payload } = params;
let templateId!: string; let templateId!: string;
switch (subject) { switch (subject) {
case NotificationTitleEnum.ORDER_CREATED: case NotifTitleEnum.ORDER_CREATED:
templateId = this.smsPatternOrderCreated; templateId = this.smsPatternOrderCreated;
break; break;
case NotificationTitleEnum.PAYMENT_SUCCESS: case NotifTitleEnum.PAYMENT_SUCCESS:
templateId = this.smsPatternPaymentSuccess; templateId = this.smsPatternPaymentSuccess;
break; break;
} }
@@ -1,26 +1,26 @@
import { NotificationTitleEnum } from '../../modules/notifications/interfaces/notification.interface'; import { NotifTitleEnum } from '../../modules/notifications/interfaces/notification.interface';
import { NotificationType } from '../../modules/notifications/interfaces/notification.interface'; import { NotificationType } from '../../modules/notifications/interfaces/notification.interface';
export interface NotificationPreferenceData { export interface NotificationPreferenceData {
title: NotificationTitleEnum; title: NotifTitleEnum;
notificationType: NotificationType; notificationType: NotificationType;
} }
export const notificationPreferencesData: NotificationPreferenceData[] = [ export const notificationPreferencesData: NotificationPreferenceData[] = [
{ {
title: NotificationTitleEnum.ORDER_CREATED, title: NotifTitleEnum.ORDER_CREATED,
notificationType: NotificationType.SMS, notificationType: NotificationType.SMS,
}, },
{ {
title: NotificationTitleEnum.PAYMENT_SUCCESS, title: NotifTitleEnum.PAYMENT_SUCCESS,
notificationType: NotificationType.SMS, notificationType: NotificationType.SMS,
}, },
{ {
title: NotificationTitleEnum.REVIEW_CREATED, title: NotifTitleEnum.REVIEW_CREATED,
notificationType: NotificationType.PUSH, notificationType: NotificationType.PUSH,
}, },
{ {
title: NotificationTitleEnum.ORDER_STATUS_CHANGED, title: NotifTitleEnum.ORDER_STATUS_CHANGED,
notificationType: NotificationType.Both, notificationType: NotificationType.Both,
}, },
]; ];