From 8303a4c1089d1ae1ba1fefda74f491054d019c14 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 23 Feb 2026 10:47:23 +0330 Subject: [PATCH] notification --- src/modules/admin/dto/create-admin.dto.ts | 7 +- src/modules/admin/entities/admin.entity.ts | 9 +- src/modules/admin/providers/admin.service.ts | 52 +-- .../controllers/notifications.controller.ts | 4 - .../notification/dto/create-preference.dto.ts | 24 -- .../notification/dto/update-preference.dto.ts | 15 - .../notification-preference.entity.ts | 15 - .../interfaces/notification.interface.ts | 23 +- .../notification/notifications.module.ts | 8 +- .../repositories/sms-log.repository.ts | 43 ++ .../notification-preference.service.ts | 84 ---- .../services/notification.service.ts | 393 ++++++++---------- .../services/push-notification.service.ts | 5 +- .../data/notification-preferences.data.ts | 29 -- 14 files changed, 245 insertions(+), 466 deletions(-) delete mode 100644 src/modules/notification/dto/create-preference.dto.ts delete mode 100644 src/modules/notification/dto/update-preference.dto.ts delete mode 100644 src/modules/notification/entities/notification-preference.entity.ts delete mode 100644 src/modules/notification/services/notification-preference.service.ts delete mode 100644 src/seeders/data/notification-preferences.data.ts diff --git a/src/modules/admin/dto/create-admin.dto.ts b/src/modules/admin/dto/create-admin.dto.ts index 069f510..81430bd 100644 --- a/src/modules/admin/dto/create-admin.dto.ts +++ b/src/modules/admin/dto/create-admin.dto.ts @@ -1,6 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsMobilePhone, IsNotEmpty, IsOptional, IsString } from 'class-validator'; -import { NotifEvent } from 'src/modules/notification/interfaces/notification.interface'; +import { IsMobilePhone, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateAdminDto { @ApiProperty({ description: 'Mobile phone number' }) @@ -23,8 +22,4 @@ export class CreateAdminDto { @IsNotEmpty() @IsString() roleId!: string; - - @ApiProperty({}) - @IsArray() - notificationPrefrences:NotifEvent[] } diff --git a/src/modules/admin/entities/admin.entity.ts b/src/modules/admin/entities/admin.entity.ts index ca9db7f..b22166e 100644 --- a/src/modules/admin/entities/admin.entity.ts +++ b/src/modules/admin/entities/admin.entity.ts @@ -1,9 +1,7 @@ -import { Cascade, Entity, ManyToOne, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core'; +import { Cascade, Entity, ManyToOne, OptionalProps, Property } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { normalizePhone } from '../../util/phone.util'; import { Role } from 'src/modules/roles/entities/role.entity'; -import { ulid } from 'ulid'; -import { NotificationPreference } from 'src/modules/notification/entities/notification-preference.entity'; @Entity({ tableName: 'admins' }) export class Admin extends BaseEntity { @@ -28,9 +26,4 @@ export class Admin extends BaseEntity { @ManyToOne(() => Role) role: Role - - // @OneToOne(() => NotificationPreference, pref => pref.admin, { - - // }) - // notificationPreference!: NotificationPreference; } diff --git a/src/modules/admin/providers/admin.service.ts b/src/modules/admin/providers/admin.service.ts index 5701fa9..0c8ad95 100644 --- a/src/modules/admin/providers/admin.service.ts +++ b/src/modules/admin/providers/admin.service.ts @@ -7,7 +7,6 @@ import { normalizePhone } from '../../util/phone.util'; import { CreateAdminDto } from '../dto/create-admin.dto'; import { AdminRepository } from '../repositories/admin.repository'; import { RoleRepository } from 'src/modules/roles/respository/role.repository'; -import { NotificationPreference } from 'src/modules/notification/entities/notification-preference.entity'; import { PermissionEnum } from 'src/common/enums/permission.enum'; @Injectable() @@ -19,7 +18,7 @@ export class AdminService { ) { } async create(dto: CreateAdminDto) { - const { phone, firstName, lastName, roleId, notificationPrefrences } = dto; + const { phone, firstName, lastName, roleId } = dto; const normalizedPhone = normalizePhone(phone); @@ -31,39 +30,25 @@ export class AdminService { throw new BadRequestException('This phone number is already used'); } - - // retrieve deleted admin if (currentAdmin && currentAdmin.deletedAt) { - currentAdmin.deletedAt = undefined - await this.em.flush() - return currentAdmin + currentAdmin.deletedAt = undefined; + await this.em.flush(); + return currentAdmin; } - const role = await this.roleRepository.findOne({ id: roleId }) + const role = await this.roleRepository.findOne({ id: roleId }); if (!role) { throw new BadRequestException('Role not found'); } - - return this.em.transactional(async em => { - - const admin = em.create(Admin, { - phone: normalizedPhone, - firstName, - lastName, - role - }) - - const notificationPreference = em.create(NotificationPreference, { - admin, - events: notificationPrefrences, - }) - - await this.em.persistAndFlush([admin, notificationPreference]); - - return admin - }) - + const admin = this.em.create(Admin, { + phone: normalizedPhone, + firstName, + lastName, + role, + }); + await this.em.persistAndFlush(admin); + return admin; } async findByPhone(phone: string): Promise { @@ -103,17 +88,6 @@ export class AdminService { admin.lastName = rest.lastName; } - if (rest.notificationPrefrences) { - // - const nf = await this.em.findOne(NotificationPreference, { - admin - }) - if (!nf) { - throw new BadRequestException('Notification prefrences not found!') - } - nf.events = rest.notificationPrefrences - await this.em.flush() - } if (rest.phone !== undefined && rest.phone !== admin.phone) { const normalizedPhone = normalizePhone(rest.phone); const exists = await this.adminRepository.findOne({ phone: normalizedPhone }); diff --git a/src/modules/notification/controllers/notifications.controller.ts b/src/modules/notification/controllers/notifications.controller.ts index 598a79d..78d53e9 100644 --- a/src/modules/notification/controllers/notifications.controller.ts +++ b/src/modules/notification/controllers/notifications.controller.ts @@ -1,12 +1,9 @@ import { Controller, Get, Body, Param, UseGuards, Query, Patch, Put, Post } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiHeader } from '@nestjs/swagger'; import { NotificationService } from '../services/notification.service'; -import { NotificationPreferenceService } from '../services/notification-preference.service'; import { AuthGuard } from '../../auth/guards/auth.guard'; import { UserId } from '../../../common/decorators/user-id.decorator'; import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard'; -import { UpdatePreferenceDto } from '../dto/update-preference.dto'; -import { CreatePreferenceDto } from '../dto/create-preference.dto'; import { AdminId } from 'src/common/decorators/admin-id.decorator'; import { NotificationMessage } from 'src/common/enums/message.enum'; import { Permissions } from 'src/common/decorators/permissions.decorator'; @@ -17,7 +14,6 @@ import { PermissionEnum } from 'src/common/enums/permission.enum'; export class NotificationsController { constructor( private readonly notificationService: NotificationService, - private readonly preferenceService: NotificationPreferenceService, ) { } // @UseGuards(AuthGuard) diff --git a/src/modules/notification/dto/create-preference.dto.ts b/src/modules/notification/dto/create-preference.dto.ts deleted file mode 100644 index 35b7cf2..0000000 --- a/src/modules/notification/dto/create-preference.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IsNotEmpty, IsEnum, IsArray } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { NotifEvent } from '../interfaces/notification.interface'; -import { NotifChannelEnum } from '../interfaces/notification.interface'; - -export class CreatePreferenceDto { - @ApiProperty({ - description: 'Notification title/type (e.g., order.created, review.created)', - enum: NotifEvent, - example: NotifEvent.NEW_ORDER, - }) - @IsEnum(NotifEvent) - @IsNotEmpty() - title!: NotifEvent; - - @ApiProperty({ - description: 'Notification type (SMS, PUSH, BOTH, or NONE)', - enum: NotifChannelEnum, - example: NotifChannelEnum.SMS, - }) - @IsArray() - @IsEnum(NotifChannelEnum, { each: true }) - channels!: NotifChannelEnum[]; -} diff --git a/src/modules/notification/dto/update-preference.dto.ts b/src/modules/notification/dto/update-preference.dto.ts deleted file mode 100644 index 19af347..0000000 --- a/src/modules/notification/dto/update-preference.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsEnum } from 'class-validator'; -import { NotifChannelEnum } from '../interfaces/notification.interface'; - -export class UpdatePreferenceDto { - @ApiProperty({ - description: 'Notification channels (can be empty or contain any enum values)', - enum: NotifChannelEnum, - isArray: true, - example: ['sms', 'push'], - }) - @IsArray() - @IsEnum(NotifChannelEnum, { each: true }) - channels!: NotifChannelEnum[]; -} diff --git a/src/modules/notification/entities/notification-preference.entity.ts b/src/modules/notification/entities/notification-preference.entity.ts deleted file mode 100644 index 9a2a72d..0000000 --- a/src/modules/notification/entities/notification-preference.entity.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Entity, Property, ManyToOne, Unique, PrimaryKey, OneToOne, Cascade, OptionalProps } from '@mikro-orm/core'; -import { BaseEntity } from '../../../common/entities/base.entity'; -import { NotifEvent } from '../interfaces/notification.interface'; -import { Admin } from 'src/modules/admin/entities/admin.entity'; - -@Entity({ tableName: 'notification_preferences' }) -export class NotificationPreference extends BaseEntity { - [OptionalProps]?: 'createdAt' | 'deletedAt' - - @OneToOne(() => Admin, { owner: true, unique: true, cascade: [Cascade.PERSIST, Cascade.REMOVE] }) - admin!: Admin; - - @Property({ type: 'json', nullable: true }) - events: NotifEvent[] = []; -} diff --git a/src/modules/notification/interfaces/notification.interface.ts b/src/modules/notification/interfaces/notification.interface.ts index 6143ba2..4fb959d 100644 --- a/src/modules/notification/interfaces/notification.interface.ts +++ b/src/modules/notification/interfaces/notification.interface.ts @@ -3,11 +3,7 @@ export enum NotifChannelEnum { SMS = 'sms', PUSH = 'push', } -export enum NotifTypeEnum { - TRANSACTIONAL = 'transactional', - PROMOTIONAL = 'promotional', - SYSTEM = 'system', -} + export enum NotifEvent { NEW_REQUEST = 'newRequest', INVOICED = 'invoiced', @@ -25,28 +21,13 @@ export interface NotifRequestMessage { templateId: string; parameters?: Record; }; - pushNotif: { - title: string; - content: string; - icon: string; - action: { - type: string; //view order - url: string; - }; - }; } export interface NotifRequest { - // requestId: string; - // timestamp: Date; // notifType: NotifTypeEnum; - // channels: NotifChannelEnum[]; + channels: NotifChannelEnum[]; recipients: recipientType[]; message: NotifRequestMessage; - metadata: { - priority: number; - // retries: number; - }; } //************************************************ */ interface INotifySmsPayload { diff --git a/src/modules/notification/notifications.module.ts b/src/modules/notification/notifications.module.ts index 96eff14..c99642d 100644 --- a/src/modules/notification/notifications.module.ts +++ b/src/modules/notification/notifications.module.ts @@ -3,9 +3,7 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { BullModule } from '@nestjs/bullmq'; import { JwtModule } from '@nestjs/jwt'; import { Notification } from './entities/notification.entity'; -import { NotificationPreference } from './entities/notification-preference.entity'; import { NotificationService } from './services/notification.service'; -import { NotificationPreferenceService } from './services/notification-preference.service'; import { NotificationQueueService } from './services/notification-queue.service'; import { PushNotificationService } from './services/push-notification.service'; import { SmsProcessor } from './processors/sms.processor'; @@ -31,7 +29,7 @@ import { SmsListeners } from './listeners/sms.listeners'; imports: [ forwardRef(() => AuthModule), JwtModule, - MikroOrmModule.forFeature([Notification, NotificationPreference, User, SmsLog]), + MikroOrmModule.forFeature([Notification, User, SmsLog]), BullModule.registerQueue( { name: NotificationQueueNameEnum.SMS }, { name: NotificationQueueNameEnum.PUSH }, @@ -54,7 +52,6 @@ import { SmsListeners } from './listeners/sms.listeners'; controllers: [NotificationsController], providers: [ NotificationService, - NotificationPreferenceService, NotificationQueueService, PushNotificationService, PushProcessor, @@ -67,7 +64,6 @@ import { SmsListeners } from './listeners/sms.listeners'; SmsLogRepository, SmsListeners, ], - exports: [NotificationService, NotificationPreferenceService, - NotificationQueueService, PushNotificationService, SmsService], + exports: [NotificationService, NotificationQueueService, PushNotificationService, SmsService], }) export class NotificationsModule { } diff --git a/src/modules/notification/repositories/sms-log.repository.ts b/src/modules/notification/repositories/sms-log.repository.ts index 5a6cf7e..d36d9cf 100644 --- a/src/modules/notification/repositories/sms-log.repository.ts +++ b/src/modules/notification/repositories/sms-log.repository.ts @@ -9,5 +9,48 @@ export class SmsLogRepository extends EntityRepository { super(em, SmsLog); } + async getSmsCountByPhone( + page: number = 1, + limit: number = 10, + ): Promise> { + const offset = (page - 1) * limit; + const knex = this.em.getConnection().getKnex(); + + const countResult = await knex('sms_logs') + .countDistinct('phone as total') + .first(); + const total = Number(countResult?.total ?? 0); + + const rows = await knex('sms_logs') + .select('phone') + .count('* as smsCount') + .groupBy('phone') + .orderBy('smsCount', 'desc') + .limit(limit) + .offset(offset); + + const data = rows.map((row: Record) => ({ + phone: String(row.phone ?? ''), + smsCount: Number(row.smsCount ?? row.sms_count ?? 0), + })); + + return { + data, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit) || 1, + }, + }; + } + + async getTotalSmsCount(): Promise { + const result = await this.em + .createQueryBuilder(SmsLog, 's') + .select('count(*)', true) + .execute('get'); + return Number(result ?? 0); + } } diff --git a/src/modules/notification/services/notification-preference.service.ts b/src/modules/notification/services/notification-preference.service.ts deleted file mode 100644 index 6e2c491..0000000 --- a/src/modules/notification/services/notification-preference.service.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { NotificationPreference } from '../entities/notification-preference.entity'; -import { CreatePreferenceDto } from '../dto/create-preference.dto'; -import { UpdatePreferenceDto } from '../dto/update-preference.dto'; -import { NotifEvent } from '../interfaces/notification.interface'; - -@Injectable() -export class NotificationPreferenceService { - constructor(private readonly em: EntityManager) { } - - // async create( dto: CreatePreferenceDto): Promise { - // const restaurant = await this.em.findOne(Restaurant, { id: }); - // if (!restaurant) { - // throw new NotFoundException('Restaurant not found'); - // } - - // const preference = this.em.create(NotificationPreference, { - // restaurant, - // channels: dto.channels, - // title: dto.title, - // }); - - // await this.em.persistAndFlush(preference); - // return preference; - // } - - // async findByRestaurant(: string): Promise { - // return this.em.find(NotificationPreference, { - // restaurant: { id: }, - // }); - // } - - // async findByRestaurantAndType(: string, title: NotifTitleEnum): Promise { - // return this.em.findOne(NotificationPreference, { - // restaurant: { id: }, - // title, - // }); - // } - - // async updateEnabled( - // : string, - // notificationType: string, - // enabled: boolean, - // ): Promise { - // const preference = await this.findByRestaurantAndType(, notificationType); - // if (!preference) { - // throw new NotFoundException('Notification preference not found'); - // } - - // preference.enabled = enabled; - // await this.em.persistAndFlush(preference); - // return preference; - // } - - // async updatePreference( - // preferenceId: string, - // : string, - // dto: UpdatePreferenceDto, - // ): Promise { - // const preference = await this.em.findOne(NotificationPreference, { - // id: preferenceId, - // restaurant: { id: }, - // }); - // if (!preference) { - // throw new NotFoundException('Notification preference not found'); - // } - - // this.em.assign(preference, dto); - // await this.em.persistAndFlush(preference); - // return preference; - // } - - // async delete(: string, preferenceId: string): Promise { - // const preference = await this.em.findOne(NotificationPreference, { - // restaurant: { id: }, - // id: preferenceId, - // }); - // if (!preference) { - // throw new NotFoundException('Notification preference not found'); - // } - // await this.em.removeAndFlush(preference); - // } -} diff --git a/src/modules/notification/services/notification.service.ts b/src/modules/notification/services/notification.service.ts index 6b66122..b3fede9 100644 --- a/src/modules/notification/services/notification.service.ts +++ b/src/modules/notification/services/notification.service.ts @@ -1,13 +1,12 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { EntityManager, FilterQuery } from '@mikro-orm/postgresql'; import { Notification } from '../entities/notification.entity'; -import { NotificationPreferenceService } from './notification-preference.service'; import { NotificationQueueService } from './notification-queue.service'; import { NotificationsGateway } from '../notifications.gateway'; import { NotifChannelEnum, NotifRequest, NotifEvent } from '../interfaces/notification.interface'; -import { SmsLog } from '../entities/smsLogs.entity'; import { SmsLogRepository } from '../repositories/sms-log.repository'; -import { PaginatedResult } from '../../../common/interfaces/pagination.interface'; +import { Admin } from '../../admin/entities/admin.entity'; +import { User } from '../../user/entities/user.entity'; @Injectable() export class NotificationService { @@ -15,214 +14,189 @@ export class NotificationService { constructor( private readonly em: EntityManager, - private readonly preferenceService: NotificationPreferenceService, private readonly queueService: NotificationQueueService, private readonly notificationGateway: NotificationsGateway, private readonly smsLogRepository: SmsLogRepository, ) { } - // async sendNotification(params: NotifRequest): Promise { - // const { recipients, message, metadata, } = params; + async sendNotification(params: NotifRequest): Promise { + const { recipients, message, channels } = params; - // // create Database notifications - // const notifications = await this.createAdminBulkNotifications( - // recipients.map(recipient => ({ - // title: message.title, - // content: message.content, - // adminId: 'adminId' in recipient ? recipient.adminId : null, - // userId: 'userId' in recipient ? recipient.userId : null, - // })), - // ); + // Create database notifications + const notifData = recipients.map(recipient => ({ + 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(, message.title); + const notifications = await this.createAdminBulkNotifications(notifData); - // if(preference?.channels?.length === 0) { - // this.logger.warn(`Notification type is NONE for restaurant ${}, title ${message.title}`); - // return notifications; - // } + if (channels.length === 0) { + this.logger.warn(`No notification channels for 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 || '', }, - // subject: message.title, - // body: message.content, - // notificationId: notification.id, - // })), - // ); - // } + // Send in-app notifications (only for admin recipients) + if (channels.includes(NotifChannelEnum.IN_APP)) { - // // 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, - // })), - // ); - // } + const adminNotifications = notifications + .filter(n => n.admin?.id) + .map(notification => ({ + recipient: { adminId: notification.admin!.id }, + subject: message.title, + body: message.content, + notificationId: notification.id, + })) - // this.logger.log(`Queued notification for restaurant ${}, title ${message.title}`); + if (adminNotifications.length > 0) { + await this.queueService.addBulkInAppNotifications(adminNotifications); + } + } - // return notifications; - // } + // Add SMS notifications to queue + if (channels.includes(NotifChannelEnum.SMS) && message.sms?.templateId) { + await this.queueService.addBulkSmsNotifications( + recipients.map(recipient => ({ + recipient, + templateId: message.sms.templateId, + parameters: message.sms.parameters, + })), + ); + } - // async createAdminBulkNotifications( - // params: { + this.logger.log(`Queued notification for title ${message.title}`); + return notifications; + } - // 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; - // } + async createAdminBulkNotifications( + params: { + title: NotifEvent; + content: string; + adminId: string | null; + userId: string | null; + }[], + ): Promise { + const notifications = params.map(param => { + return this.em.create(Notification, { + admin: param.adminId ? this.em.getReference(Admin, param.adminId) : undefined, + user: param.userId && param.userId.trim() !== '' ? this.em.getReference(User, param.userId) : undefined, + title: param.title, + content: param.content, + }); + }); + 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 findAdminNotifications( + adminId: string, + limit = 50, + cursor?: string, + status?: 'seen' | 'unseen', + ): Promise<{ data: Notification[]; nextCursor: string | null }> { + const where: FilterQuery = { + admin: { id: adminId }, + }; - // async findByRestaurant( - // : string, - // adminId: string, - // limit = 50, - // cursor ?: string, - // status ?: 'seen' | 'unseen', - // ): Promise < { data: Notification[]; nextCursor: string | null } > { - // const where: FilterQuery = { - // restaurant: { id: }, - // admin: { id: adminId }, - // }; + 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; - // } + 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, + 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, - // : string, - // limit = 50, - // cursor ?: string, - // status ?: 'seen' | 'unseen', - // ): Promise < { data: Notification[]; nextCursor: string | null } > { - // const where: FilterQuery = { - // user: { id: userId }, - // restaurant: { id: }, - // }; + async findUserNotifications( + userId: string, + limit = 50, + cursor?: string, + status?: 'seen' | 'unseen', + ): Promise<{ data: Notification[]; nextCursor: string | null }> { + const where: FilterQuery = { + user: { id: userId }, + }; - // // Filter by status (seen/unseen) - // if (status === 'seen') { - // where.seenAt = { $ne: null }; - // } else if (status === 'unseen') { - // where.seenAt = null; - // } + 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 }; - // } + 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, + }); - // // 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; + 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, : 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); - // } - // 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'); - // } - // notification.seenAt = new Date(); - // await this.em.persistAndFlush(notification); - // } - // async findByRestaurantAndType(: string, title: NotifTitleEnum, limit = 50): Promise < Notification[] > { + async readNotificationAdmin(id: string, adminId: string): Promise { + const notification = await this.em.findOne(Notification, { + id, + admin: { id: adminId }, + }); + if (!notification) { + throw new NotFoundException('Notification not found'); + } + notification.seenAt = new Date(); + await this.em.persistAndFlush(notification); + } + + + async readNotificationAsUser(id: string, userId: string): Promise { + const notification = await this.em.findOne(Notification, { + id, + user: { id: userId }, + }); + if (!notification) { + throw new NotFoundException('Notification not found'); + } + notification.seenAt = new Date(); + await this.em.persistAndFlush(notification); + } + + // async findByTitle(title: NotifEvent, limit = 50): Promise { // return this.em.find( // Notification, - // { - // restaurant: { id: }, - // title, - // }, + // { title }, // { // orderBy: { createdAt: 'DESC' }, // limit, - // populate: ['user'], + // populate: ['user', 'admin'], // }, // ); // } - // async findByAdminAndRestaurant(adminId: string, : string, limit = 50): Promise < Notification[] > { + // async findByAdminAndTitle(adminId: string, title: NotifEvent, limit = 50): Promise { // return this.em.find( // Notification, - // { admin: { id: adminId }, restaurant: { id: } }, + // { admin: { id: adminId }, title }, // { // orderBy: { createdAt: 'DESC' }, // limit, @@ -230,50 +204,45 @@ export class NotificationService { // ); // } - // async countUnseenByUserAndRestaurant(userId: string, : string): Promise < number > { - // const where: FilterQuery = { - // user: { id: userId }, - // restaurant: { id: }, - // seenAt: null, - // }; - // return this.em.count(Notification, where); - // } - // async countUnseenByRestaurant(adminId: string, : string): Promise < number > { - // const where: FilterQuery = { - // admin: { id: adminId }, - // restaurant: { id: }, - // seenAt: null, - // }; - // return this.em.count(Notification, where); - // } + async countUnseenByUser(userId: string): Promise { + return this.em.count(Notification, { + user: { id: userId }, + seenAt: null, + }); + } - // readAllNotifsAsUser(userId: string, : string): Promise < number > { - // const where: FilterQuery = { - // user: { id: userId }, - // restaurant: { id: }, - // seenAt: null, - // }; - // return this.em.nativeUpdate(Notification, where, { seenAt: new Date() }); - // } - // readAllNotifsAsAdmin(adminId: string, : string): Promise < number > { - // const where: FilterQuery = { - // admin: { id: adminId }, - // restaurant: { id: }, - // seenAt: null, - // }; - // return this.em.nativeUpdate(Notification, where, { seenAt: new Date() }); - // } + async countUnseenByAdmin(adminId: string): Promise { + return this.em.count(Notification, { + admin: { id: adminId }, + seenAt: null, + }); + } + + + async readAllNotifsAsUser(userId: string): Promise { + const notifications = await this.em.find(Notification, { + user: { id: userId }, + seenAt: null, + }); + const now = new Date(); + notifications.forEach(n => { n.seenAt = now; }); + await this.em.persistAndFlush(notifications); + return notifications.length; + } + + + async readAllNotifsAsAdmin(adminId: string): Promise { + const notifications = await this.em.find(Notification, { + admin: { id: adminId }, + seenAt: null, + }); + const now = new Date(); + notifications.forEach(n => { n.seenAt = now; }); + await this.em.persistAndFlush(notifications); + return notifications.length; + } - // async getSmsCountByRestaurant( - // 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(); - // } } diff --git a/src/modules/notification/services/push-notification.service.ts b/src/modules/notification/services/push-notification.service.ts index e62c16c..7c5fc02 100644 --- a/src/modules/notification/services/push-notification.service.ts +++ b/src/modules/notification/services/push-notification.service.ts @@ -5,7 +5,7 @@ import * as admin from 'firebase-admin'; export interface PushNotificationPayload { title: string; body: string; - data?: Record; + data?: Record; token?: string; tokens?: string[]; } @@ -34,7 +34,6 @@ export class PushNotificationService { return; } - // Parse the service account JSON const serviceAccount = JSON.parse(firebaseConfig); if (!this.firebaseApp) { @@ -134,7 +133,7 @@ export class PushNotificationService { } } - private convertDataToString(data: Record): Record { + private convertDataToString(data: Record): Record { const result: Record = {}; for (const [key, value] of Object.entries(data)) { result[key] = typeof value === 'string' ? value : JSON.stringify(value); diff --git a/src/seeders/data/notification-preferences.data.ts b/src/seeders/data/notification-preferences.data.ts deleted file mode 100644 index d6be6a4..0000000 --- a/src/seeders/data/notification-preferences.data.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NotifChannelEnum, NotifEvent } from '../../modules/notification/interfaces/notification.interface'; - -export interface NotificationPreferenceData { - title: NotifEvent; - channels: NotifChannelEnum[]; -} - -export const notificationPreferencesData: NotificationPreferenceData[] = [ - // { - // title: NotifEvent.PAGER_CREATED, - // channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - // }, - // { - // title: NotifEvent.ORDER_CREATED, - // channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - // }, - // { - // title: NotifEvent.PAYMENT_SUCCESS, - // channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - // }, - // { - // title: NotifEvent.REVIEW_CREATED, - // channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - // }, - // { - // title: NotifEvent.ORDER_STATUS_CHANGED, - // channels: [NotifChannelEnum.SMS, NotifChannelEnum.PUSH, NotifChannelEnum.IN_APP], - // }, -];