notification
This commit is contained in:
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<Admin | null> {
|
||||
@@ -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 });
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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[] = [];
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
};
|
||||
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 {
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -9,5 +9,48 @@ export class SmsLogRepository extends EntityRepository<SmsLog> {
|
||||
super(em, SmsLog);
|
||||
}
|
||||
|
||||
async getSmsCountByPhone(
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
): Promise<PaginatedResult<{ phone: string; smsCount: number }>> {
|
||||
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<string, unknown>) => ({
|
||||
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<number> {
|
||||
const result = await this.em
|
||||
.createQueryBuilder(SmsLog, 's')
|
||||
.select('count(*)', true)
|
||||
.execute('get');
|
||||
return Number(result ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<NotificationPreference> {
|
||||
// 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<NotificationPreference[]> {
|
||||
// return this.em.find(NotificationPreference, {
|
||||
// restaurant: { id: },
|
||||
// });
|
||||
// }
|
||||
|
||||
// async findByRestaurantAndType(: string, title: NotifTitleEnum): Promise<NotificationPreference | null> {
|
||||
// return this.em.findOne(NotificationPreference, {
|
||||
// restaurant: { id: },
|
||||
// title,
|
||||
// });
|
||||
// }
|
||||
|
||||
// async updateEnabled(
|
||||
// : string,
|
||||
// notificationType: string,
|
||||
// enabled: boolean,
|
||||
// ): Promise<NotificationPreference> {
|
||||
// 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<NotificationPreference> {
|
||||
// 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<void> {
|
||||
// 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);
|
||||
// }
|
||||
}
|
||||
@@ -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<Notification[]> {
|
||||
// const { recipients, message, metadata, } = params;
|
||||
async sendNotification(params: NotifRequest): Promise<Notification[]> {
|
||||
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<Notification[]> {
|
||||
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<Notification> = {
|
||||
admin: { id: adminId },
|
||||
};
|
||||
|
||||
// async findByRestaurant(
|
||||
// : 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 },
|
||||
// };
|
||||
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<Notification> = {
|
||||
// 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<Notification> = {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<Notification[]> {
|
||||
// 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<Notification[]> {
|
||||
// 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<Notification> = {
|
||||
// user: { id: userId },
|
||||
// restaurant: { id: },
|
||||
// seenAt: null,
|
||||
// };
|
||||
// return this.em.count(Notification, where);
|
||||
// }
|
||||
|
||||
// async countUnseenByRestaurant(adminId: string, : string): Promise < number > {
|
||||
// const where: FilterQuery<Notification> = {
|
||||
// admin: { id: adminId },
|
||||
// restaurant: { id: },
|
||||
// seenAt: null,
|
||||
// };
|
||||
// return this.em.count(Notification, where);
|
||||
// }
|
||||
async countUnseenByUser(userId: string): Promise<number> {
|
||||
return this.em.count(Notification, {
|
||||
user: { id: userId },
|
||||
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() });
|
||||
// }
|
||||
|
||||
// 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() });
|
||||
// }
|
||||
async countUnseenByAdmin(adminId: string): Promise<number> {
|
||||
return this.em.count(Notification, {
|
||||
admin: { id: adminId },
|
||||
seenAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async readAllNotifsAsUser(userId: string): Promise<number> {
|
||||
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<number> {
|
||||
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();
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as admin from 'firebase-admin';
|
||||
export interface PushNotificationPayload {
|
||||
title: string;
|
||||
body: string;
|
||||
data?: Record<string, any>;
|
||||
data?: Record<string, unknown>;
|
||||
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<string, any>): Record<string, string> {
|
||||
private convertDataToString(data: Record<string, unknown>): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
result[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
|
||||
Reference in New Issue
Block a user