user module

This commit is contained in:
2026-01-13 19:59:06 +03:30
parent 6522acff5f
commit d630cb844a
62 changed files with 1137 additions and 3040 deletions
@@ -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();
}
}