user module
This commit is contained in:
@@ -5,7 +5,7 @@ import { NotificationPreferenceService } from '../services/notification-preferen
|
||||
import { AuthGuard } from '../../auth/guards/auth.guard';
|
||||
import { UserId } from '../../../common/decorators/user-id.decorator';
|
||||
import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard';
|
||||
import { RestId } from '../../../common/decorators/rest-id.decorator';
|
||||
import { } from '../../../common/decorators/rest-id.decorator';
|
||||
import { UpdatePreferenceDto } from '../dto/update-preference.dto';
|
||||
import { CreatePreferenceDto } from '../dto/create-preference.dto';
|
||||
import { AdminId } from 'src/common/decorators/admin-id.decorator';
|
||||
@@ -38,14 +38,14 @@ export class NotificationsController {
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
|
||||
async getUserNotifications(
|
||||
@UserId() userId: string,
|
||||
@RestId() restaurantId: string,
|
||||
,
|
||||
@Query('limit') limit?: number,
|
||||
@Query('cursor') cursor?: string,
|
||||
@Query('status') status?: 'seen' | 'unseen',
|
||||
) {
|
||||
return await this.notificationService.findByUserAndRestaurant(
|
||||
userId,
|
||||
restaurantId,
|
||||
,
|
||||
limit ? parseInt(limit.toString(), 10) : 50,
|
||||
cursor,
|
||||
status,
|
||||
@@ -57,8 +57,8 @@ export class NotificationsController {
|
||||
@Get('public/notifications/unseen-count')
|
||||
@ApiOperation({ summary: 'Get unseen notifications count for user' })
|
||||
|
||||
async getUserUnseenCount(@UserId() userId: string, @RestId() restaurantId: string) {
|
||||
const count = await this.notificationService.countUnseenByUserAndRestaurant(userId, restaurantId);
|
||||
async getUserUnseenCount(@UserId() userId: string,) {
|
||||
const count = await this.notificationService.countUnseenByUserAndRestaurant(userId,);
|
||||
return { count };
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ export class NotificationsController {
|
||||
@Put('public/notifications/:id')
|
||||
@ApiOperation({ summary: 'Read a notification ' })
|
||||
@ApiParam({ name: 'id', description: 'Notification ID' })
|
||||
async readNotificationUser(@RestId() restaurantId: string, @Param('id') id: string, @UserId() userId: string) {
|
||||
await this.notificationService.readNotificationAsUser(id, userId, restaurantId);
|
||||
async readNotificationUser(, @Param('id') id: string, @UserId() userId: string) {
|
||||
await this.notificationService.readNotificationAsUser(id, userId,);
|
||||
return { message: NotificationMessage.READ_SUCCESS };
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ export class NotificationsController {
|
||||
@ApiBearerAuth()
|
||||
@Put('public/notifications/read/all')
|
||||
@ApiOperation({ summary: 'Read all notification ' })
|
||||
async readAllNotificationUser(@RestId() restaurantId: string, @UserId() userId: string) {
|
||||
await this.notificationService.readAllNotifsAsUser(userId, restaurantId);
|
||||
async readAllNotificationUser(, @UserId() userId: string) {
|
||||
await this.notificationService.readAllNotifsAsUser(userId,);
|
||||
return { message: NotificationMessage.READ_SUCCESS };
|
||||
}
|
||||
/* ***************** Admin Endpoints ***************** */
|
||||
@@ -96,21 +96,21 @@ export class NotificationsController {
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
|
||||
async getAdminNotifications(
|
||||
@AdminId() adminId: string,
|
||||
@RestId() restaurantId: string,
|
||||
,
|
||||
@Query('limit') limit?: number,
|
||||
@Query('cursor') cursor?: string,
|
||||
@Query('status') status?: 'seen' | 'unseen',
|
||||
) {
|
||||
const parsedLimit = limit ? parseInt(limit.toString(), 10) : 50;
|
||||
return await this.notificationService.findByRestaurant(restaurantId, adminId, parsedLimit, cursor, status);
|
||||
return await this.notificationService.findByRestaurant(, adminId, parsedLimit, cursor, status);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('admin/notifications/unseen-count')
|
||||
@ApiOperation({ summary: 'Get unseen notifications count for admin' })
|
||||
async getAdminUnseenCount(@AdminId() adminId: string, @RestId() restaurantId: string) {
|
||||
const count = await this.notificationService.countUnseenByRestaurant(adminId, restaurantId);
|
||||
async getAdminUnseenCount(@AdminId() adminId: string,) {
|
||||
const count = await this.notificationService.countUnseenByRestaurant(adminId,);
|
||||
return { count };
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ export class NotificationsController {
|
||||
@Put('admin/notifications/:id')
|
||||
@ApiOperation({ summary: 'Read a notification ' })
|
||||
@ApiParam({ name: 'id', description: 'Notification ID' })
|
||||
async readNotificationAdmin(@RestId() restaurantId: string, @AdminId() adminId: string, @Param('id') id: string) {
|
||||
await this.notificationService.readNotificationAdmin(id, adminId, restaurantId);
|
||||
async readNotificationAdmin(, @AdminId() adminId: string, @Param('id') id: string) {
|
||||
await this.notificationService.readNotificationAdmin(id, adminId,);
|
||||
return { message: NotificationMessage.READ_SUCCESS };
|
||||
}
|
||||
|
||||
@@ -128,8 +128,8 @@ export class NotificationsController {
|
||||
@ApiBearerAuth()
|
||||
@Put('admin/notifications/read/all')
|
||||
@ApiOperation({ summary: 'Read all notification ' })
|
||||
async readAllNotificationAdmin(@RestId() restaurantId: string, @AdminId() adminId: string, @Param('id') id: string) {
|
||||
await this.notificationService.readAllNotifsAsAdmin(adminId, restaurantId);
|
||||
async readAllNotificationAdmin(, @AdminId() adminId: string, @Param('id') id: string) {
|
||||
await this.notificationService.readAllNotifsAsAdmin(adminId,);
|
||||
return { message: NotificationMessage.READ_SUCCESS };
|
||||
}
|
||||
|
||||
@@ -138,8 +138,8 @@ export class NotificationsController {
|
||||
@Permissions(Permission.MANAGE_SETTINGS)
|
||||
@Get('admin/notification-preferences')
|
||||
@ApiOperation({ summary: 'Get all notification preferences for a restaurant' })
|
||||
async getPreferences(@RestId() restaurantId: string) {
|
||||
return this.preferenceService.findByRestaurant(restaurantId);
|
||||
async getPreferences() {
|
||||
return this.preferenceService.findByRestaurant();
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@@ -149,11 +149,11 @@ export class NotificationsController {
|
||||
@ApiOperation({ summary: 'Update notification channels' })
|
||||
@ApiParam({ name: 'id', description: 'Notification preference ID' })
|
||||
async updatePreference(
|
||||
@RestId() restaurantId: string,
|
||||
,
|
||||
@Param('id') preferenceId: string,
|
||||
@Body() dto: UpdatePreferenceDto,
|
||||
) {
|
||||
return this.preferenceService.updatePreference(preferenceId, restaurantId, dto);
|
||||
return this.preferenceService.updatePreference(preferenceId, , dto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@@ -162,18 +162,18 @@ export class NotificationsController {
|
||||
@Post('admin/notification-preferences')
|
||||
@ApiOperation({ summary: 'Create notification preference' })
|
||||
async createPreference(
|
||||
@RestId() restaurantId: string,
|
||||
,
|
||||
@Body() dto: CreatePreferenceDto,
|
||||
) {
|
||||
return this.preferenceService.create(restaurantId, dto);
|
||||
return this.preferenceService.create(, dto);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('admin/notifications/sms-usage')
|
||||
@ApiOperation({ summary: 'Get SMS usage for my restaurant' })
|
||||
async getRestaurantSmsUsage(@RestId() restaurantId: string) {
|
||||
const smsCount = await this.notificationService.getSmsCountByRestaurantId(restaurantId);
|
||||
async getRestaurantSmsUsage() {
|
||||
const smsCount = await this.notificationService.getSmsCountBy();
|
||||
return { smsCount };
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,23 @@ import type { AuthenticatedSocket } from '../guards/ws-admin-auth.guard';
|
||||
|
||||
/**
|
||||
* Extract restaurant ID from authenticated WebSocket client
|
||||
* Use this decorator in WebSocket handlers to get the restId from the authenticated admin
|
||||
* Use this decorator in WebSocket handlers to get the from the authenticated admin
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @SubscribeMessage('get:notifications')
|
||||
* handleGetNotifications(@WsRestId() restId: string) {
|
||||
* // restId is automatically extracted from authenticated admin
|
||||
* handleGetNotifications(@Ws() : string) {
|
||||
* // is automatically extracted from authenticated admin
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const WsRestId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
export const Ws = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const client = ctx.switchToWs().getClient<AuthenticatedSocket>();
|
||||
const restId = client.restId;
|
||||
const = client.;
|
||||
|
||||
if (!restId) {
|
||||
if (!) {
|
||||
throw new Error('Restaurant ID not found. Ensure WsAdminAuthGuard is applied.');
|
||||
}
|
||||
|
||||
return restId;
|
||||
return;
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ export class SendNotificationDto {
|
||||
@ApiProperty({ description: 'Restaurant ID (ULID)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
restaurantId: string;
|
||||
: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'User ID (ULID)' })
|
||||
@IsString()
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Entity, Property, ManyToOne, Enum } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { NotifTitleEnum } from '../interfaces/notification.interface';
|
||||
import { Admin } from 'src/modules/admin/entities/admin.entity';
|
||||
|
||||
@Entity({ tableName: 'notifications' })
|
||||
export class Notification extends BaseEntity {
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
|
||||
@ManyToOne(() => User, { nullable: true })
|
||||
user?: User;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export class SmsSentEvent {
|
||||
constructor(
|
||||
public readonly phoneNumber: string,
|
||||
public readonly restaurantId: string,
|
||||
) {}
|
||||
) { }
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IAdminTokenPayload } from '../../auth/interfaces/IToken-payload';
|
||||
|
||||
export interface AuthenticatedSocket extends Socket {
|
||||
adminId?: string;
|
||||
restId?: string;
|
||||
?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -19,7 +19,7 @@ export class WsAdminAuthGuard implements CanActivate {
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const client: Socket = context.switchToWs().getClient<Socket>();
|
||||
@@ -41,16 +41,16 @@ export class WsAdminAuthGuard implements CanActivate {
|
||||
secret,
|
||||
});
|
||||
|
||||
if (!payload.adminId || !payload.restId) {
|
||||
if (!payload.adminId || !payload.) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new WsException('Invalid token payload');
|
||||
}
|
||||
|
||||
// Attach admin info to socket
|
||||
(client as AuthenticatedSocket).adminId = payload.adminId;
|
||||
(client as AuthenticatedSocket).restId = payload.restId;
|
||||
(client as AuthenticatedSocket). = payload.;
|
||||
|
||||
this.logger.log(`Admin authenticated via WebSocket: ${payload.adminId}, restId: ${payload.restId}`);
|
||||
this.logger.log(`Admin authenticated via WebSocket: ${payload.adminId}, : ${payload.}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { NotifTitleEnum, recipientType } from './notification.interface';
|
||||
export interface SmsNotificationQueueJob {
|
||||
recipient: recipientType;
|
||||
templateId: string;
|
||||
restaurantId: string;
|
||||
: string;
|
||||
parameters?: Record<string, string>;
|
||||
}
|
||||
export interface PushNotificationQueueJob {
|
||||
@@ -18,7 +18,7 @@ export interface PushNotificationQueueJob {
|
||||
pushTokens?: string[]; // Multiple FCM tokens for bulk push notifications
|
||||
}
|
||||
export interface InAppNotificationQueueJob {
|
||||
recipient: { adminId: string; restaurantId: string };
|
||||
recipient: { adminId: string; : string };
|
||||
subject: NotifTitleEnum;
|
||||
body: string;
|
||||
notificationId: string;
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface NotifRequest {
|
||||
// timestamp: Date;
|
||||
// notifType: NotifTypeEnum;
|
||||
// channels: NotifChannelEnum[];
|
||||
restaurantId: string;
|
||||
: string;
|
||||
recipients: recipientType[];
|
||||
message: NotifRequestMessage;
|
||||
metadata: {
|
||||
|
||||
@@ -19,15 +19,15 @@ export class SmsListeners {
|
||||
async handleSmsSent(event: SmsSentEvent) {
|
||||
try {
|
||||
this.logger.log(
|
||||
`SMS sent event received: phone ${event.phoneNumber} for restaurant: ${event.restaurantId}`,
|
||||
`SMS sent event received: phone ${event.phoneNumber} for restaurant: ${event.}`,
|
||||
);
|
||||
|
||||
// Get the restaurant entity
|
||||
const restaurant = await this.restRepository.findOne({ id: event.restaurantId });
|
||||
const restaurant = await this.restRepository.findOne({ id: event. });
|
||||
|
||||
if (!restaurant) {
|
||||
this.logger.warn(
|
||||
`Restaurant not found for SMS log: ${event.restaurantId}`,
|
||||
`Restaurant not found for SMS log: ${event.}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export class SmsListeners {
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to create SMS log for event: ${event.restaurantId}`,
|
||||
`Failed to create SMS log for event: ${event.}`,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisco
|
||||
@Inject(forwardRef(() => NotificationService))
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly moduleRef: ModuleRef,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async handleConnection(client: Socket) {
|
||||
try {
|
||||
@@ -70,8 +70,8 @@ export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisco
|
||||
// }
|
||||
}
|
||||
|
||||
sendInAppNotification(repipient: { adminId: string; restaurantId: string }, payload: IInAppNotificationPayload) {
|
||||
const room = this.getRoom(repipient.adminId, repipient.restaurantId);
|
||||
sendInAppNotification(repipient: { adminId: string; : string }, payload: IInAppNotificationPayload) {
|
||||
const room = this.getRoom(repipient.adminId, repipient.);
|
||||
|
||||
this.logger.log(`Sending in app notification to admin: ${repipient.adminId}`);
|
||||
this.server.to(room).emit('notifications', payload, async () => {
|
||||
@@ -81,19 +81,19 @@ export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisco
|
||||
this.logger.log(`In app notification sent to admin: ${repipient.adminId}`);
|
||||
}
|
||||
|
||||
private getRoom(adminId: string, restaurantId: string) {
|
||||
return `restaurant:${restaurantId}-admin:${adminId}`;
|
||||
private getRoom(adminId: string, : string) {
|
||||
return `restaurant:${}-admin:${adminId}`;
|
||||
}
|
||||
|
||||
handleLeaveRoom(client: AuthenticatedSocket) {
|
||||
const room = this.getRoom(client.adminId!, client.restId!);
|
||||
const room = this.getRoom(client.adminId!, client.!);
|
||||
void client.leave(room);
|
||||
this.logger.log(`Admin ${client.adminId} (Client ${client.id}) left room: ${room}`);
|
||||
void client.emit('left', { room, message: 'Successfully Admin left room' });
|
||||
}
|
||||
|
||||
handleJoinRoom(client: AuthenticatedSocket) {
|
||||
const room = this.getRoom(client.adminId!, client.restId!);
|
||||
const room = this.getRoom(client.adminId!, client.!);
|
||||
void client.join(room);
|
||||
this.logger.log(`Admin ${client.adminId} (Client ${client.id}) joined room: ${room}`);
|
||||
void client.emit('joined', { room, message: 'Successfully Admin joined room' });
|
||||
|
||||
@@ -27,7 +27,7 @@ export class InAppProcessor extends WorkerHost {
|
||||
|
||||
try {
|
||||
this.notificationsGateway.sendInAppNotification(
|
||||
{ adminId: recipient.adminId, restaurantId: recipient.restaurantId },
|
||||
{ adminId: recipient.adminId },
|
||||
{ subject, body, notificationId },
|
||||
);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export class SmsProcessor extends WorkerHost {
|
||||
}
|
||||
|
||||
async process(job: Job<SmsNotificationQueueJob>) {
|
||||
const { recipient, templateId, parameters, restaurantId } = job.data;
|
||||
const { recipient, templateId, parameters, } = job.data;
|
||||
|
||||
this.logger.log(`Processing SMS notification - Recipient: ${JSON.stringify(recipient)}, templateID: ${templateId}`);
|
||||
|
||||
@@ -58,7 +58,7 @@ export class SmsProcessor extends WorkerHost {
|
||||
|
||||
this.logger.log(`SMS notification sent successfully to ${phone}`);
|
||||
|
||||
this.eventEmitter.emit(SmsSentEvent.name, new SmsSentEvent(phone, restaurantId));
|
||||
this.eventEmitter.emit(SmsSentEvent.name, new SmsSentEvent(phone));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -12,24 +12,25 @@ export class SmsLogRepository extends EntityRepository<SmsLog> {
|
||||
async getSmsCountByRestaurant(
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
): Promise<PaginatedResult<{ restaurantId: string; restaurantName: string; smsCount: number }>> {
|
||||
const offset = (page - 1) * limit;
|
||||
): Promise<PaginatedResult<{ : string; restaurantName: string; smsCount: number
|
||||
}>> {
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Get total count of unique restaurants with SMS logs
|
||||
const totalResult = await this.em.execute(
|
||||
`
|
||||
// Get total count of unique restaurants with SMS logs
|
||||
const totalResult = await this.em.execute(
|
||||
`
|
||||
SELECT COUNT(DISTINCT sl.restaurant_id) as total
|
||||
FROM sms_logs sl
|
||||
WHERE sl.restaurant_id IS NOT NULL
|
||||
`,
|
||||
);
|
||||
const total = parseInt(totalResult[0]?.total || '0', 10);
|
||||
);
|
||||
const total = parseInt(totalResult[0]?.total || '0', 10);
|
||||
|
||||
// Get paginated results with SMS counts grouped by restaurant
|
||||
const results = await this.em.execute(
|
||||
`
|
||||
// Get paginated results with SMS counts grouped by restaurant
|
||||
const results = await this.em.execute(
|
||||
`
|
||||
SELECT
|
||||
r.id as "restaurantId",
|
||||
r.id as "",
|
||||
r.name as "restaurantName",
|
||||
COUNT(sl.id)::int as "smsCount"
|
||||
FROM sms_logs sl
|
||||
@@ -38,39 +39,39 @@ export class SmsLogRepository extends EntityRepository<SmsLog> {
|
||||
ORDER BY "smsCount" DESC, r.name ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`,
|
||||
[limit, offset],
|
||||
);
|
||||
[limit, offset],
|
||||
);
|
||||
|
||||
const data = results.map((row: any) => ({
|
||||
restaurantId: row.restaurantId,
|
||||
restaurantName: row.restaurantName || 'Unknown',
|
||||
smsCount: parseInt(row.smsCount || '0', 10),
|
||||
}));
|
||||
const data = results.map((row: any) => ({
|
||||
: row.,
|
||||
restaurantName: row.restaurantName || 'Unknown',
|
||||
smsCount: parseInt(row.smsCount || '0', 10),
|
||||
}));
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getSmsCountByRestaurantId(restaurantId: string): Promise<number> {
|
||||
const result = await this.em.execute(
|
||||
`
|
||||
async getSmsCountBy(: string): Promise < number > {
|
||||
const result = await this.em.execute(
|
||||
`
|
||||
SELECT COUNT(sl.id)::int as "smsCount"
|
||||
FROM sms_logs sl
|
||||
WHERE sl.restaurant_id = ?
|
||||
`,
|
||||
[restaurantId],
|
||||
);
|
||||
[],
|
||||
);
|
||||
|
||||
return parseInt(result[0]?.smsCount || '0', 10);
|
||||
}
|
||||
return parseInt(result[0]?.smsCount || '0', 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import { NotifTitleEnum } from '../interfaces/notification.interface';
|
||||
export class NotificationPreferenceService {
|
||||
constructor(private readonly em: EntityManager) { }
|
||||
|
||||
async create(restaurantId: string, dto: CreatePreferenceDto): Promise<NotificationPreference> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||
async create(: string, dto: CreatePreferenceDto): Promise<NotificationPreference> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException('Restaurant not found');
|
||||
}
|
||||
@@ -26,25 +26,25 @@ export class NotificationPreferenceService {
|
||||
return preference;
|
||||
}
|
||||
|
||||
async findByRestaurant(restaurantId: string): Promise<NotificationPreference[]> {
|
||||
async findByRestaurant(: string): Promise<NotificationPreference[]> {
|
||||
return this.em.find(NotificationPreference, {
|
||||
restaurant: { id: restaurantId },
|
||||
restaurant: { id: },
|
||||
});
|
||||
}
|
||||
|
||||
async findByRestaurantAndType(restaurantId: string, title: NotifTitleEnum): Promise<NotificationPreference | null> {
|
||||
async findByRestaurantAndType(: string, title: NotifTitleEnum): Promise<NotificationPreference | null> {
|
||||
return this.em.findOne(NotificationPreference, {
|
||||
restaurant: { id: restaurantId },
|
||||
restaurant: { id: },
|
||||
title,
|
||||
});
|
||||
}
|
||||
|
||||
// async updateEnabled(
|
||||
// restaurantId: string,
|
||||
// : string,
|
||||
// notificationType: string,
|
||||
// enabled: boolean,
|
||||
// ): Promise<NotificationPreference> {
|
||||
// const preference = await this.findByRestaurantAndType(restaurantId, notificationType);
|
||||
// const preference = await this.findByRestaurantAndType(, notificationType);
|
||||
// if (!preference) {
|
||||
// throw new NotFoundException('Notification preference not found');
|
||||
// }
|
||||
@@ -56,12 +56,12 @@ export class NotificationPreferenceService {
|
||||
|
||||
async updatePreference(
|
||||
preferenceId: string,
|
||||
restaurantId: string,
|
||||
: string,
|
||||
dto: UpdatePreferenceDto,
|
||||
): Promise<NotificationPreference> {
|
||||
const preference = await this.em.findOne(NotificationPreference, {
|
||||
id: preferenceId,
|
||||
restaurant: { id: restaurantId },
|
||||
restaurant: { id: },
|
||||
});
|
||||
if (!preference) {
|
||||
throw new NotFoundException('Notification preference not found');
|
||||
@@ -72,9 +72,9 @@ export class NotificationPreferenceService {
|
||||
return preference;
|
||||
}
|
||||
|
||||
async delete(restaurantId: string, preferenceId: string): Promise<void> {
|
||||
async delete(: string, preferenceId: string): Promise<void> {
|
||||
const preference = await this.em.findOne(NotificationPreference, {
|
||||
restaurant: { id: restaurantId },
|
||||
restaurant: { id: },
|
||||
id: preferenceId,
|
||||
});
|
||||
if (!preference) {
|
||||
|
||||
@@ -22,261 +22,258 @@ export class NotificationService {
|
||||
) { }
|
||||
|
||||
async sendNotification(params: NotifRequest): Promise<Notification[]> {
|
||||
const { recipients, message, metadata, restaurantId } = params;
|
||||
const { recipients, message, metadata, } = params;
|
||||
|
||||
// create Database notifications
|
||||
const notifications = await this.createAdminBulkNotifications(
|
||||
recipients.map(recipient => ({
|
||||
restaurantId,
|
||||
title: message.title,
|
||||
title: message.title,
|
||||
content: message.content,
|
||||
adminId: 'adminId' in recipient ? recipient.adminId : null,
|
||||
userId: 'userId' in recipient ? recipient.userId : null,
|
||||
})),
|
||||
);
|
||||
|
||||
// get admin prefrences
|
||||
const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, message.title);
|
||||
// get admin prefrences
|
||||
const preference = await this.preferenceService.findByRestaurantAndType(, message.title);
|
||||
|
||||
if (preference?.channels?.length === 0) {
|
||||
this.logger.warn(`Notification type is NONE for restaurant ${restaurantId}, title ${message.title}`);
|
||||
return notifications;
|
||||
}
|
||||
if(preference?.channels?.length === 0) {
|
||||
this.logger.warn(`Notification type is NONE for restaurant ${}, title ${message.title}`);
|
||||
return notifications;
|
||||
}
|
||||
|
||||
// send in app notification
|
||||
if (preference?.channels?.includes(NotifChannelEnum.IN_APP)) {
|
||||
await this.queueService.addBulkInAppNotifications(
|
||||
notifications.map(notification => ({
|
||||
recipient: { adminId: notification.admin?.id || '', restaurantId },
|
||||
subject: message.title,
|
||||
body: message.content,
|
||||
notificationId: notification.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
// send in app notification
|
||||
if (preference?.channels?.includes(NotifChannelEnum.IN_APP)) {
|
||||
await this.queueService.addBulkInAppNotifications(
|
||||
notifications.map(notification => ({
|
||||
recipient: { adminId: notification.admin?.id || '', },
|
||||
subject: message.title,
|
||||
body: message.content,
|
||||
notificationId: notification.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// add sms notifications to queue
|
||||
if (preference?.channels?.includes(NotifChannelEnum.SMS)) {
|
||||
await this.queueService.addBulkSmsNotifications(
|
||||
recipients.map(recipient => ({
|
||||
recipient,
|
||||
templateId: message.sms.templateId,
|
||||
parameters: message.sms.parameters,
|
||||
restaurantId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
// add sms notifications to queue
|
||||
if (preference?.channels?.includes(NotifChannelEnum.SMS)) {
|
||||
await this.queueService.addBulkSmsNotifications(
|
||||
recipients.map(recipient => ({
|
||||
recipient,
|
||||
templateId: message.sms.templateId,
|
||||
parameters: message.sms.parameters,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(`Queued notification for restaurant ${restaurantId}, title ${message.title}`);
|
||||
this.logger.log(`Queued notification for restaurant ${}, title ${message.title}`);
|
||||
|
||||
return notifications;
|
||||
return notifications;
|
||||
}
|
||||
|
||||
async createAdminBulkNotifications(
|
||||
params: {
|
||||
restaurantId: string;
|
||||
title: NotifTitleEnum;
|
||||
content: string;
|
||||
adminId: string | null;
|
||||
userId: string | null;
|
||||
}[],
|
||||
): Promise<Notification[]> {
|
||||
const notifications = params.map(param => {
|
||||
return this.em.create(Notification, {
|
||||
restaurant: param.restaurantId,
|
||||
admin: param.adminId,
|
||||
user: param.userId && param.userId.trim() !== '' ? param.userId : null,
|
||||
title: param.title,
|
||||
content: param.content,
|
||||
});
|
||||
params: {
|
||||
|
||||
title: NotifTitleEnum;
|
||||
content: string;
|
||||
adminId: string | null;
|
||||
userId: string | null;
|
||||
}[],
|
||||
): Promise < Notification[] > {
|
||||
const notifications = params.map(param => {
|
||||
return this.em.create(Notification, {
|
||||
admin: param.adminId,
|
||||
user: param.userId && param.userId.trim() !== '' ? param.userId : null,
|
||||
title: param.title,
|
||||
content: param.content,
|
||||
});
|
||||
await this.em.persistAndFlush(notifications);
|
||||
return notifications;
|
||||
}
|
||||
});
|
||||
await this.em.persistAndFlush(notifications);
|
||||
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 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,
|
||||
adminId: string,
|
||||
limit = 50,
|
||||
cursor?: string,
|
||||
status?: 'seen' | 'unseen',
|
||||
): Promise<{ data: Notification[]; nextCursor: string | null }> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
restaurant: { id: restaurantId },
|
||||
admin: { id: adminId },
|
||||
};
|
||||
: string,
|
||||
adminId: string,
|
||||
limit = 50,
|
||||
cursor ?: string,
|
||||
status ?: 'seen' | 'unseen',
|
||||
): Promise < { data: Notification[]; nextCursor: string | null } > {
|
||||
const where: FilterQuery<Notification> = {
|
||||
restaurant: { id: },
|
||||
admin: { id: adminId },
|
||||
};
|
||||
|
||||
// Filter by status (seen/unseen)
|
||||
if (status === 'seen') {
|
||||
where.seenAt = { $ne: null };
|
||||
} else if (status === 'unseen') {
|
||||
where.seenAt = null;
|
||||
}
|
||||
// Filter by status (seen/unseen)
|
||||
if (status === 'seen') {
|
||||
where.seenAt = { $ne: null };
|
||||
} else if (status === 'unseen') {
|
||||
where.seenAt = null;
|
||||
}
|
||||
|
||||
// Cursor-based pagination: if cursor is provided, get items with id < cursor
|
||||
if (cursor) {
|
||||
where.id = { $lt: cursor };
|
||||
}
|
||||
// Cursor-based pagination: if cursor is provided, get items with id < cursor
|
||||
if (cursor) {
|
||||
where.id = { $lt: cursor };
|
||||
}
|
||||
|
||||
const notifications = await this.em.find(Notification, where, {
|
||||
orderBy: { createdAt: 'DESC', id: 'DESC' },
|
||||
limit: limit + 1, // fetch one extra to determine next page
|
||||
populate: ['user'],
|
||||
});
|
||||
const notifications = await this.em.find(Notification, where, {
|
||||
orderBy: { createdAt: 'DESC', id: 'DESC' },
|
||||
limit: limit + 1, // fetch one extra to determine next page
|
||||
populate: ['user'],
|
||||
});
|
||||
|
||||
const hasNextPage = notifications.length > limit;
|
||||
const data = hasNextPage ? notifications.slice(0, limit) : notifications;
|
||||
const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null;
|
||||
const hasNextPage = notifications.length > limit;
|
||||
const data = hasNextPage ? notifications.slice(0, limit) : notifications;
|
||||
const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null;
|
||||
|
||||
return {
|
||||
data,
|
||||
nextCursor: nextCursor ?? null,
|
||||
};
|
||||
return {
|
||||
data,
|
||||
nextCursor: nextCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async findByUserAndRestaurant(
|
||||
userId: string,
|
||||
restaurantId: string,
|
||||
limit = 50,
|
||||
cursor?: string,
|
||||
status?: 'seen' | 'unseen',
|
||||
): Promise<{ data: Notification[]; nextCursor: string | null }> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
};
|
||||
userId: string,
|
||||
: string,
|
||||
limit = 50,
|
||||
cursor ?: string,
|
||||
status ?: 'seen' | 'unseen',
|
||||
): Promise < { data: Notification[]; nextCursor: string | null } > {
|
||||
const where: FilterQuery<Notification> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: },
|
||||
};
|
||||
|
||||
// Filter by status (seen/unseen)
|
||||
if (status === 'seen') {
|
||||
where.seenAt = { $ne: null };
|
||||
} else if (status === 'unseen') {
|
||||
where.seenAt = null;
|
||||
}
|
||||
// Filter by status (seen/unseen)
|
||||
if (status === 'seen') {
|
||||
where.seenAt = { $ne: null };
|
||||
} else if (status === 'unseen') {
|
||||
where.seenAt = null;
|
||||
}
|
||||
|
||||
// Cursor-based pagination: if cursor is provided, get items with id < cursor (since ULIDs are time-ordered)
|
||||
if (cursor) {
|
||||
where.id = { $lt: cursor };
|
||||
}
|
||||
// Cursor-based pagination: if cursor is provided, get items with id < cursor (since ULIDs are time-ordered)
|
||||
if (cursor) {
|
||||
where.id = { $lt: cursor };
|
||||
}
|
||||
|
||||
const notifications = await this.em.find(Notification, where, {
|
||||
orderBy: { createdAt: 'DESC', id: 'DESC' },
|
||||
limit: limit + 1, // Fetch one extra to determine if there's a next page
|
||||
});
|
||||
const notifications = await this.em.find(Notification, where, {
|
||||
orderBy: { createdAt: 'DESC', id: 'DESC' },
|
||||
limit: limit + 1, // Fetch one extra to determine if there's a next page
|
||||
});
|
||||
|
||||
// Check if there's a next page
|
||||
const hasNextPage = notifications.length > limit;
|
||||
const data = hasNextPage ? notifications.slice(0, limit) : notifications;
|
||||
const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null;
|
||||
// Check if there's a next page
|
||||
const hasNextPage = notifications.length > limit;
|
||||
const data = hasNextPage ? notifications.slice(0, limit) : notifications;
|
||||
const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null;
|
||||
|
||||
return {
|
||||
data,
|
||||
nextCursor: nextCursor ?? null,
|
||||
};
|
||||
return {
|
||||
data,
|
||||
nextCursor: nextCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async readNotificationAdmin(id: string, adminId: string, restaurantId: string): Promise<void> {
|
||||
const notification = await this.em.findOne(Notification, {
|
||||
id,
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
if (!notification) {
|
||||
throw new NotFoundException('Notification not found');
|
||||
}
|
||||
async readNotificationAdmin(id: string, adminId: string, : string): Promise < void> {
|
||||
const notification = await this.em.findOne(Notification, {
|
||||
id,
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: },
|
||||
});
|
||||
if(!notification) {
|
||||
throw new NotFoundException('Notification not found');
|
||||
}
|
||||
notification.seenAt = new Date();
|
||||
await this.em.persistAndFlush(notification);
|
||||
await this.em.persistAndFlush(notification);
|
||||
}
|
||||
async readNotificationAsUser(id: string, userId: string, : string): Promise < void> {
|
||||
const notification = await this.em.findOne(Notification, {
|
||||
id,
|
||||
user: { id: userId },
|
||||
restaurant: { id: },
|
||||
});
|
||||
if(!notification) {
|
||||
throw new NotFoundException('Notification not found');
|
||||
}
|
||||
async readNotificationAsUser(id: string, userId: string, restaurantId: string): Promise<void> {
|
||||
const notification = await this.em.findOne(Notification, {
|
||||
id,
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
if (!notification) {
|
||||
throw new NotFoundException('Notification not found');
|
||||
}
|
||||
notification.seenAt = new Date();
|
||||
await this.em.persistAndFlush(notification);
|
||||
}
|
||||
await this.em.persistAndFlush(notification);
|
||||
}
|
||||
|
||||
async findByRestaurantAndType(restaurantId: string, title: NotifTitleEnum, limit = 50): Promise<Notification[]> {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{
|
||||
restaurant: { id: restaurantId },
|
||||
title,
|
||||
},
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
populate: ['user'],
|
||||
},
|
||||
);
|
||||
}
|
||||
async findByRestaurantAndType(: string, title: NotifTitleEnum, limit = 50): Promise < Notification[] > {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{
|
||||
restaurant: { id: },
|
||||
title,
|
||||
},
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
populate: ['user'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async findByAdminAndRestaurant(adminId: string, restaurantId: string, limit = 50): Promise<Notification[]> {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{ admin: { id: adminId }, restaurant: { id: restaurantId } },
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
},
|
||||
);
|
||||
}
|
||||
async findByAdminAndRestaurant(adminId: string, : string, limit = 50): Promise < Notification[] > {
|
||||
return this.em.find(
|
||||
Notification,
|
||||
{ admin: { id: adminId }, restaurant: { id: } },
|
||||
{
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
limit,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async countUnseenByUserAndRestaurant(userId: string, restaurantId: string): Promise<number> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
seenAt: null,
|
||||
async countUnseenByUserAndRestaurant(userId: string, : string): Promise < number > {
|
||||
const where: FilterQuery<Notification> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: },
|
||||
seenAt: null,
|
||||
};
|
||||
return this.em.count(Notification, where);
|
||||
return this.em.count(Notification, where);
|
||||
}
|
||||
|
||||
async countUnseenByRestaurant(adminId: string, restaurantId: string): Promise<number> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: restaurantId },
|
||||
seenAt: null,
|
||||
async countUnseenByRestaurant(adminId: string, : string): Promise < number > {
|
||||
const where: FilterQuery<Notification> = {
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: },
|
||||
seenAt: null,
|
||||
};
|
||||
return this.em.count(Notification, where);
|
||||
return this.em.count(Notification, where);
|
||||
}
|
||||
|
||||
readAllNotifsAsUser(userId: string, restaurantId: string): Promise<number> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: restaurantId },
|
||||
seenAt: null,
|
||||
readAllNotifsAsUser(userId: string, : string): Promise < number > {
|
||||
const where: FilterQuery<Notification> = {
|
||||
user: { id: userId },
|
||||
restaurant: { id: },
|
||||
seenAt: null,
|
||||
};
|
||||
return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
|
||||
return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
|
||||
}
|
||||
|
||||
readAllNotifsAsAdmin(adminId: string, restaurantId: string): Promise<number> {
|
||||
const where: FilterQuery<Notification> = {
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: restaurantId },
|
||||
seenAt: null,
|
||||
readAllNotifsAsAdmin(adminId: string, : string): Promise < number > {
|
||||
const where: FilterQuery<Notification> = {
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: },
|
||||
seenAt: null,
|
||||
};
|
||||
return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
|
||||
return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
|
||||
}
|
||||
|
||||
async getSmsCountByRestaurant(
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
): Promise<PaginatedResult<{ restaurantId: string; restaurantName: string; smsCount: number }>> {
|
||||
return this.smsLogRepository.getSmsCountByRestaurant(page, limit);
|
||||
}
|
||||
|
||||
async getSmsCountByRestaurantId(restaurantId: string): Promise<number> {
|
||||
return this.smsLogRepository.getSmsCountByRestaurantId(restaurantId);
|
||||
}
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
): Promise < PaginatedResult < { : string; restaurantName: string; smsCount: number } >> {
|
||||
return this.smsLogRepository.getSmsCountByRestaurant(page, limit);
|
||||
}
|
||||
|
||||
async getSmsCountBy(: string): Promise < number > {
|
||||
return this.smsLogRepository.getSmsCountBy();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user