notification
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user