diff --git a/src/app.module.ts b/src/app.module.ts index 2f29cc3..c0dd93c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,8 +8,6 @@ import { UploaderModule } from './modules/uploader/uploader.module'; import { AdminModule } from './modules/admin/admin.module'; import { ThrottlerModule } from '@nestjs/throttler'; import { ScheduleModule } from '@nestjs/schedule'; - - import { NotificationsModule } from './modules/notifications/notifications.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { CacheModule } from '@nestjs/cache-manager'; import { cacheConfig } from './config/cache.config'; @@ -32,8 +30,6 @@ import { CatalogueModule } from './modules/catalogue/catalogue.module'; }, ]), ScheduleModule.forRoot(), - - NotificationsModule, EventEmitterModule.forRoot(), BusinessModule, CatalogueModule, diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 72eac2c..44c8440 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -9,7 +9,6 @@ import { TokensService } from './services/tokens.service'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { AdminAuthGuard } from './guards/adminAuth.guard'; import { RefreshToken } from './entities/refresh-token.entity'; -import { NotificationsModule } from '../notifications/notifications.module'; import { BusinessModule } from '../business/business.module'; @Module({ @@ -32,7 +31,6 @@ import { BusinessModule } from '../business/business.module'; forwardRef(() => AdminModule), forwardRef(() => BusinessModule), MikroOrmModule.forFeature([RefreshToken]), - forwardRef(() => NotificationsModule), BusinessModule ], controllers: [AuthController], diff --git a/src/modules/auth/controllers/auth.controller.ts b/src/modules/auth/controllers/auth.controller.ts index ed0c673..7fc95ee 100644 --- a/src/modules/auth/controllers/auth.controller.ts +++ b/src/modules/auth/controllers/auth.controller.ts @@ -1,12 +1,7 @@ import { Controller, Post, Body, UseGuards } from '@nestjs/common'; import { AuthService } from '../services/auth.service'; -import { RequestOtpDto } from '../dto/request-otp.dto'; import { DirectLoginDto } from '../dto/direct-login.dto'; import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger'; -import { VerifyOtpDto } from '../dto/verify-otp.dto'; -import { Throttle } from '@nestjs/throttler'; -import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator'; -import { RefreshTokenDto } from '../dto/refresh-token.dto'; import { SuperAdminAuthGuard } from '../guards/superAdminAuth.guard'; @ApiTags('auth') diff --git a/src/modules/auth/entities/refresh-token.entity.ts b/src/modules/auth/entities/refresh-token.entity.ts index 6ff4db1..75ad2e7 100644 --- a/src/modules/auth/entities/refresh-token.entity.ts +++ b/src/modules/auth/entities/refresh-token.entity.ts @@ -2,13 +2,9 @@ import { Entity, Enum, Index, Property } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; -export enum RefreshTokenType { - USER = 'user', - ADMIN = 'admin', -} - + @Entity({ tableName: 'refreshtokens' }) -@Index({ properties: ['ownerId', 'restId', 'type'] }) +@Index({ properties: ['adminId', 'businessId'] }) @Index({ properties: ['hashedToken'] }) @Index({ properties: ['expiresAt'] }) export class RefreshToken extends BaseEntity { @@ -16,13 +12,10 @@ export class RefreshToken extends BaseEntity { hashedToken!: string; @Property() - ownerId!: string; + adminId!: string; @Property() - restId!: string; - - @Enum(() => RefreshTokenType) - type!: RefreshTokenType; + businessId!: string; @Property({ type: 'timestamptz' }) expiresAt!: Date; diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index f4ff78e..9911b80 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,28 +1,14 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; -import { RequestOtpDto } from '../dto/request-otp.dto'; -import { CacheService } from '../../utils/cache.service'; -import { SmsService } from '../../notifications/services/sms.service'; -import { randomInt } from 'crypto'; +import { Injectable } from '@nestjs/common'; import { TokensService } from './tokens.service'; -import { AuthMessage, RestMessage } from 'src/common/enums/message.enum'; -import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; -import { ConfigService } from '@nestjs/config'; -import { normalizePhone } from '../../utils/phone.util'; import { BusinessService } from 'src/modules/business/business.service'; @Injectable() export class AuthService { - readonly ADMIN_PERMISSIONS_KEY = 'admin-perms'; - private OTP_EXPIRATION_TIME: number; + constructor( - private readonly cacheService: CacheService, - private readonly smsService: SmsService, private readonly tokensService: TokensService, - private readonly adminRepository: AdminRepository, - private readonly configService: ConfigService, private readonly businessService: BusinessService, ) { - this.OTP_EXPIRATION_TIME = this.configService.get('OTP_EXPIRATION_TIME') ?? 240; } // private userOtpKey(restaurantSlug: string, phone: string) { @@ -33,8 +19,11 @@ export class AuthService { // return `otp-admin:${restaurantSlug}:${phone}`; // } - directLogin(danakSubId: string) { - const business=this.businessService.findBySubIdOrFail(danakSubId) + async directLogin(danakSubId: string) { + const business = await this.businessService.findBySubIdOrFail(danakSubId) + const admin = business.admin + const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, business.id); + return tokens } // async requestOtp(dto: RequestOtpDto, isAdmin: boolean) { diff --git a/src/modules/auth/services/tokens.service.ts b/src/modules/auth/services/tokens.service.ts index 43fd0d9..fa25659 100755 --- a/src/modules/auth/services/tokens.service.ts +++ b/src/modules/auth/services/tokens.service.ts @@ -3,13 +3,12 @@ import { EntityManager } from '@mikro-orm/postgresql'; import { LockMode } from '@mikro-orm/core'; import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { AuthMessage } from '../../../common/enums/message.enum'; +import { RefreshToken } from '../entities/refresh-token.entity'; +import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload'; import { JwtService } from '@nestjs/jwt'; import dayjs from 'dayjs'; -import { AuthMessage } from '../../../common/enums/message.enum'; -import { RefreshToken, RefreshTokenType } from '../entities/refresh-token.entity'; -import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload'; - @Injectable() export class TokensService { private readonly logger = new Logger(TokensService.name); @@ -17,28 +16,23 @@ export class TokensService { private readonly configService: ConfigService, private readonly jwtService: JwtService, private readonly em: EntityManager, - ) {} + ) { } async generateAccessAndRefreshToken( - ownerId: string, - restaurantId: string, - isAdmin: boolean, - slug: string, + adminId: string, + businessId: string, em?: EntityManager, ) { const refreshExpire = this.configService.getOrThrow('REFRESH_TOKEN_EXPIRE'); const accessExpire = this.configService.getOrThrow('JWT_EXPIRATION_TIME'); - const payload: ITokenPayload | IAdminTokenPayload = isAdmin - ? { adminId: ownerId, restId: restaurantId } - : { userId: ownerId, restId: restaurantId, slug }; + const payload: IAdminTokenPayload = { adminId: adminId, restId: businessId } const accessToken = await this.generateAccessToken(payload, accessExpire); const refreshToken = this.generateRefreshToken(); - const type = isAdmin ? RefreshTokenType.ADMIN : RefreshTokenType.USER; // Only pass em if it's a transaction manager (not this.em), otherwise pass undefined to trigger flush - await this.storeRefreshToken(ownerId, restaurantId, refreshToken, type, em); + await this.storeRefreshToken(adminId, businessId, refreshToken, em); return { accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'seconds').valueOf() }, @@ -57,7 +51,6 @@ export class TokensService { ownerId: string, restId: string, refreshToken: string, - type: RefreshTokenType, em?: EntityManager, ) { const entityManager = em || this.em; @@ -68,9 +61,8 @@ export class TokensService { const token = entityManager.create(RefreshToken, { hashedToken, - ownerId, - restId, - type, + adminId: ownerId, + businessId: restId, expiresAt, }); @@ -104,20 +96,13 @@ export class TokensService { } // Store token data before removal - const ownerId = token.ownerId; - const restId = token.restId; - const isAdmin = token.type === RefreshTokenType.ADMIN; - - // Verify restaurant still exists - // const restaurant = await em.findOne(Restaurant, { id: restId }); - // if (!restaurant) { - // throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); - // } + const adminId = token.adminId; + const businessId = token.businessId; try { // Generate new tokens first (before removing old token) // Pass the transaction EntityManager to ensure operations are within the transaction - const tokens = await this.generateAccessAndRefreshToken(ownerId, "restaurant.id", isAdmin, "restaurant.slug", em); + const tokens = await this.generateAccessAndRefreshToken(adminId, businessId, em); // Remove old token only after new token is created em.remove(token); diff --git a/src/modules/business/business.service.ts b/src/modules/business/business.service.ts index ee6d46c..b1e6997 100644 --- a/src/modules/business/business.service.ts +++ b/src/modules/business/business.service.ts @@ -22,7 +22,7 @@ export class BusinessService { name, slug }) - + const admin = em.create(Admin, { firstName, lastName, @@ -43,7 +43,7 @@ export class BusinessService { } async findBySubIdOrFail(danakSubscriptionId: string) { - const business = await this.businessRepository.findOne({ danakSubscriptionId }) + const business = await this.businessRepository.findOne({ danakSubscriptionId }, { populate: ['admin'] }) if (!business) { throw new NotFoundException("Business not found") } diff --git a/src/modules/notifications/constants/queue.ts b/src/modules/notifications/constants/queue.ts deleted file mode 100644 index 644e72f..0000000 --- a/src/modules/notifications/constants/queue.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum NotificationQueueNameEnum { - SMS = 'sms', - PUSH = 'push', - IN_APP = 'in-app', -} diff --git a/src/modules/notifications/dto/create-notification.dto.ts b/src/modules/notifications/dto/create-notification.dto.ts deleted file mode 100644 index e42d4b5..0000000 --- a/src/modules/notifications/dto/create-notification.dto.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { IsString, IsNotEmpty, IsOptional, IsArray, IsObject } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -export class CreateNotificationDto { - @ApiProperty({ description: 'Notification title' }) - @IsString() - @IsNotEmpty() - title: string; - - @ApiProperty({ description: 'Notification message' }) - @IsString() - @IsNotEmpty() - message: string; - - @ApiProperty({ description: 'Notification type' }) - @IsString() - @IsNotEmpty() - type: string; - - @ApiPropertyOptional({ description: 'User ID' }) - @IsString() - @IsOptional() - userId?: string; - - @ApiPropertyOptional({ description: 'Phone number for SMS' }) - @IsString() - @IsOptional() - phoneNumber?: string; - - @ApiPropertyOptional({ description: 'Push notification tokens', type: [String] }) - @IsArray() - @IsString({ each: true }) - @IsOptional() - pushTokens?: string[]; - - @ApiPropertyOptional({ description: 'Additional data' }) - @IsObject() - @IsOptional() - data?: Record; -} diff --git a/src/modules/notifications/dto/create-preference.dto.ts b/src/modules/notifications/dto/create-preference.dto.ts deleted file mode 100644 index 63583fc..0000000 --- a/src/modules/notifications/dto/create-preference.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IsNotEmpty, IsEnum, IsArray } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { NotifTitleEnum } 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: NotifTitleEnum, - example: NotifTitleEnum.ORDER_CREATED, - }) - @IsEnum(NotifTitleEnum) - @IsNotEmpty() - title!: NotifTitleEnum; - - @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/notifications/dto/send-notification.dto.ts b/src/modules/notifications/dto/send-notification.dto.ts deleted file mode 100644 index c306319..0000000 --- a/src/modules/notifications/dto/send-notification.dto.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IsString, IsNotEmpty, IsOptional, IsObject } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -export class SendNotificationDto { - @ApiProperty({ description: 'Restaurant ID (ULID)' }) - @IsString() - @IsNotEmpty() - restaurantId: string; - - @ApiPropertyOptional({ description: 'User ID (ULID)' }) - @IsString() - @IsOptional() - userId?: string; - - @ApiProperty({ description: 'Notification type (e.g., ORDER_CONFIRMED, PAYMENT_FAILED)' }) - @IsString() - @IsNotEmpty() - notificationType: string; - - @ApiProperty({ description: 'Notification payload' }) - @IsObject() - @IsNotEmpty() - payload: { - title?: string; - message: string; - body?: string; - phoneNumber?: string; - pushToken?: string; - data?: Record; - templateId?: string; - parameters?: Array<{ name: string; value: string }>; - }; - - @ApiPropertyOptional({ description: 'Idempotency key to prevent duplicates' }) - @IsString() - @IsOptional() - idempotencyKey?: string; -} diff --git a/src/modules/notifications/dto/update-preference.dto.ts b/src/modules/notifications/dto/update-preference.dto.ts deleted file mode 100644 index 19af347..0000000 --- a/src/modules/notifications/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/notifications/interfaces/jobs-queue.interface.ts b/src/modules/notifications/interfaces/jobs-queue.interface.ts deleted file mode 100644 index 470a72c..0000000 --- a/src/modules/notifications/interfaces/jobs-queue.interface.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { NotifTitleEnum, recipientType } from './notification.interface'; - -export interface SmsNotificationQueueJob { - recipient: recipientType; - templateId: string; - restaurantId: string; - parameters?: Record; -} -export interface PushNotificationQueueJob { - title: string; - content: string; - icon: string; - action: { - type: string; //view order - url: string; - }; - pushToken?: string; // FCM token for push notifications - pushTokens?: string[]; // Multiple FCM tokens for bulk push notifications -} -export interface InAppNotificationQueueJob { - recipient: { adminId: string; restaurantId: string }; - subject: NotifTitleEnum; - body: string; - notificationId: string; -} - -export interface SmsNotificationQueueJobResult { - success: boolean; - notificationId: string; - providerResponse?: Record; - error?: string; -} diff --git a/src/modules/notifications/interfaces/notification.interface.ts b/src/modules/notifications/interfaces/notification.interface.ts deleted file mode 100644 index c97ca61..0000000 --- a/src/modules/notifications/interfaces/notification.interface.ts +++ /dev/null @@ -1,76 +0,0 @@ -export enum NotifChannelEnum { - IN_APP = 'in-app', - SMS = 'sms', - PUSH = 'push', -} -export enum NotifTypeEnum { - TRANSACTIONAL = 'transactional', - PROMOTIONAL = 'promotional', - SYSTEM = 'system', -} -export enum NotifTitleEnum { - PAGER_CREATED = 'pager.created', - ORDER_CREATED = 'order.created', - PAYMENT_SUCCESS = 'payment.success', - REVIEW_CREATED = 'review.created', - ORDER_STATUS_CHANGED = 'order.status.changed', -} -export type recipientType = { userId: string } | { adminId: string }; - -export interface NotifRequestMessage { - title: NotifTitleEnum; - content: string; - sms: { - 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[]; - restaurantId: string; - recipients: recipientType[]; - message: NotifRequestMessage; - metadata: { - priority: number; - // retries: number; - }; -} -//************************************************ */ -interface INotifySmsPayload { - [key: string]: string | number | Date; -} -interface INotifySms { - phone: string; - subject: NotifTitleEnum; -} -export interface IInAppNotificationPayload { - notificationId: string; - subject: NotifTitleEnum; - body: string; -} - -// 2. Payment Success -interface IPaymentSuccessSmsNotifyPayload extends INotifySmsPayload { - amount: number; - date: Date; -} - -interface IPaymentSuccessSmsNotify extends INotifySms { - subject: NotifTitleEnum.PAYMENT_SUCCESS; - payload: IPaymentSuccessSmsNotifyPayload; -} - -export type ISmsNotifyPayload = IPaymentSuccessSmsNotify; diff --git a/src/modules/notifications/interfaces/sms.ts b/src/modules/notifications/interfaces/sms.ts deleted file mode 100644 index fd0a746..0000000 --- a/src/modules/notifications/interfaces/sms.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface ISmsResponse { - status: number; - message: string; -} - -export interface ISmsParams { - phone: string; - parameters?: Record; - templateId: string; -} - -export interface ISmsBodyParameters { - Mobile: string; - TemplateId: string; - Parameters: { name: string; value: string }[]; -} diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts deleted file mode 100644 index a568490..0000000 --- a/src/modules/notifications/notifications.module.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Module, forwardRef } from '@nestjs/common'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { BullModule } from '@nestjs/bullmq'; -import { JwtModule } from '@nestjs/jwt'; -import { NotificationQueueService } from './services/notification-queue.service'; -import { SmsProcessor } from './processors/sms.processor'; -import { AuthModule } from '../auth/auth.module'; -import { ConfigService } from '@nestjs/config'; -import { NotificationQueueNameEnum } from './constants/queue'; -import { UtilsModule } from '../utils/utils.module'; -import { AdminModule } from '../admin/admin.module'; -import { SmsService } from './services/sms.service'; - - -@Module({ - imports: [ - forwardRef(() => AuthModule), - JwtModule, - BullModule.registerQueue( - { name: NotificationQueueNameEnum.SMS }, - { name: NotificationQueueNameEnum.PUSH }, - { name: NotificationQueueNameEnum.IN_APP }, - ), - BullModule.forRootAsync({ - inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - connection: { - host: config.get('REDIS_HOST') || 'captain.dev.danakcorp.com', - port: config.get('REDIS_PORT') || 50601, - password: config.get('REDIS_PASSWORD'), // Pull from .env - }, - }), - }), - AdminModule, - UtilsModule, - ], - controllers: [], - providers: [ - NotificationQueueService, - SmsProcessor, - SmsService, - ], - exports: [ - NotificationQueueService, SmsService], -}) -export class NotificationsModule { } diff --git a/src/modules/notifications/processors/sms.processor.ts b/src/modules/notifications/processors/sms.processor.ts deleted file mode 100644 index d6f2f5c..0000000 --- a/src/modules/notifications/processors/sms.processor.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Processor, WorkerHost, OnWorkerEvent } from '@nestjs/bullmq'; -import { Logger } from '@nestjs/common'; -import { Job } from 'bullmq'; - import { SmsNotificationQueueJob } from '../interfaces/jobs-queue.interface'; -import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; -import { NotificationQueueNameEnum } from '../constants/queue'; -import { SmsService } from '../services/sms.service'; -import { EventEmitter2 } from '@nestjs/event-emitter'; - -@Processor(NotificationQueueNameEnum.SMS) -export class SmsProcessor extends WorkerHost { - private readonly logger = new Logger(SmsProcessor.name); - - constructor( - private readonly adminRepository: AdminRepository, - private readonly smsService: SmsService, - private readonly eventEmitter: EventEmitter2, - ) { - super(); - } - - async process(job: Job) { - const { recipient, templateId, parameters, restaurantId } = job.data; - - this.logger.log(`Processing SMS notification - Recipient: ${JSON.stringify(recipient)}, templateID: ${templateId}`); - - try { - let phone!: string; - - if ('adminId' in recipient) { - const admin = await this.adminRepository.findOne({ id: recipient.adminId }); - if (!admin || !admin.phone) { - this.logger.warn(`Admin ${recipient.adminId} not found or has no phone number`); - throw new Error('Admin phone number is required for SMS notifications'); - } - phone = admin.phone; - } - if (!phone) { - this.logger.warn(`Phone number not found for recipient ${JSON.stringify(recipient)}`); - throw new Error('Phone number is required for SMS notifications'); - } - // Send SMS notification - await this.smsService.sendSms({ - phone, - templateId, - parameters, - }); - - this.logger.log(`SMS notification sent successfully to ${phone}`); - - - return { - success: true, - }; - } catch (error) { - this.logger.error(`Error processing SMS notification job ${job.id}:`, error); - throw error; - } - } - - @OnWorkerEvent('completed') - onCompleted(job: Job) { - this.logger.log(`Notification job ${job.id} completed`); - } - - @OnWorkerEvent('failed') - onFailed(job: Job, error: Error) { - this.logger.error(`Notification job ${job.id} failed:`, error); - } -} diff --git a/src/modules/notifications/services/notification-queue.service.ts b/src/modules/notifications/services/notification-queue.service.ts deleted file mode 100644 index d556c51..0000000 --- a/src/modules/notifications/services/notification-queue.service.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectQueue } from '@nestjs/bullmq'; -import { Queue } from 'bullmq'; -import { NotificationQueueNameEnum } from '../constants/queue'; -import { - InAppNotificationQueueJob, - PushNotificationQueueJob, - SmsNotificationQueueJob, -} from '../interfaces/jobs-queue.interface'; - -@Injectable() -export class NotificationQueueService { - private readonly logger = new Logger(NotificationQueueService.name); - - constructor( - @InjectQueue(NotificationQueueNameEnum.SMS) private readonly smsQueue: Queue, - @InjectQueue(NotificationQueueNameEnum.PUSH) private readonly pushQueue: Queue, - @InjectQueue(NotificationQueueNameEnum.IN_APP) private readonly inAppQueue: Queue, - ) {} - - async addSmsNotification(job: SmsNotificationQueueJob): Promise { - try { - const jobId = `${job.templateId}-${this.generateRandomInt()}-${Date.now()}`; - - await this.smsQueue.add('sms-notification', job, { - jobId, - attempts: 3, - backoff: { - type: 'exponential', - delay: 2000, - }, - removeOnComplete: { - age: 24 * 3600, // Keep completed jobs for 24 hours - count: 1000, - }, - removeOnFail: { - age: 7 * 24 * 3600, // Keep failed jobs for 7 days - }, - }); - - this.logger.log(`SMS notification job added to queue: ${jobId}`); - } catch (error) { - this.logger.error(`Failed to add SMS notification to queue:`, error); - throw error; - } - } - - async addPushNotification(job: PushNotificationQueueJob): Promise { - try { - const jobId = `${job.title}-${this.generateRandomInt()}-${Date.now()}`; - - await this.pushQueue.add('push-notification', job, { - jobId, - attempts: 3, - backoff: { - type: 'exponential', - delay: 2000, - }, - removeOnComplete: { - age: 24 * 3600, // Keep completed jobs for 24 hours - count: 1000, - }, - removeOnFail: { - age: 7 * 24 * 3600, // Keep failed jobs for 7 days - }, - }); - - this.logger.log(`Push notification job added to queue: ${jobId}`); - } catch (error) { - this.logger.error(`Failed to add push notification to queue:`, error); - throw error; - } - } - - async addBulkSmsNotifications(jobs: SmsNotificationQueueJob[]): Promise { - try { - const queueJobs = jobs.map(job => ({ - name: 'sms-notification', - data: job, - opts: { - jobId: `${job.templateId}-${this.generateRandomInt()}`, - attempts: 3, - backoff: { - type: 'exponential', - delay: 2000, - }, - }, - })); - - await this.smsQueue.addBulk(queueJobs); - this.logger.log(`Added ${jobs.length} SMS notification jobs to queue`); - } catch (error) { - this.logger.error(`Failed to add bulk notifications to queue:`, error); - throw error; - } - } - - async addBulkPushNotifications(jobs: PushNotificationQueueJob[]): Promise { - try { - const queueJobs = jobs.map(job => ({ - name: 'push-notification', - data: job, - opts: { - jobId: `${job.title}-${this.generateRandomInt()}-${Date.now()}`, - attempts: 3, - backoff: { - type: 'exponential', - delay: 2000, - }, - }, - })); - - await this.pushQueue.addBulk(queueJobs); - this.logger.log(`Added ${jobs.length} Push notification jobs to queue`); - } catch (error) { - this.logger.error(`Failed to add bulk notifications to queue:`, error); - throw error; - } - } - async addBulkInAppNotifications(jobs: InAppNotificationQueueJob[]): Promise { - try { - const queueJobs = jobs.map(job => ({ - name: 'in-app-notification', - data: job, - opts: { - jobId: `${job.subject}-${job.notificationId}-${Date.now()}`, - attempts: 3, - backoff: { - type: 'exponential', - delay: 2000, - }, - }, - })); - - await this.inAppQueue.addBulk(queueJobs); - this.logger.log(`Added ${jobs.length} InApp notification jobs to queue`); - } catch (error) { - this.logger.error(`Failed to add bulk notifications to queue:`, error); - throw error; - } - } - private generateRandomInt(): string { - return Math.random().toString(36).substring(2, 15); - } -} diff --git a/src/modules/notifications/services/sms.service.ts b/src/modules/notifications/services/sms.service.ts deleted file mode 100644 index 996664b..0000000 --- a/src/modules/notifications/services/sms.service.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import axios from 'axios'; -import { ISmsParams, ISmsResponse, ISmsBodyParameters } from '../interfaces/sms'; - -@Injectable() -export class SmsService { - private readonly baseURL: string; - private readonly OTP: string; - private readonly apiKey: string; - - constructor( - // private readonly httpService: HttpService, - private readonly configService: ConfigService, - ) { - this.baseURL = this.configService.getOrThrow('SMS_BASE_URL'); - this.apiKey = this.configService.getOrThrow('SMS_API_KEY'); - this.OTP = this.configService.getOrThrow('SMS_PATTERN_OTP'); - } - - sendotp(phone: string, otp: string) { - return this.sendSms({ - phone, - parameters: { otp }, - templateId: this.OTP, - }); - } - - async sendSms(params: ISmsParams) { - const { parameters, phone, templateId } = params; - const parametersArray = Object.entries(parameters ?? {}).map(([name, value]) => ({ - name, - value: value.toString(), - })); - const cleanedPhone = this.formatPhone(phone); - - const smsData: ISmsBodyParameters = { - Parameters: parametersArray, - Mobile: cleanedPhone, - TemplateId: templateId, - }; - - const url = `${this.baseURL}/send/verify`; - - try { - const { data } = await axios.post(url, smsData, { - headers: { - 'Content-Type': 'application/json', - 'X-API-KEY': this.apiKey, - }, - }); - if (data.status !== 1) { - throw new HttpException('The SMS was not sent. Please try again later.', HttpStatus.BAD_GATEWAY); - } - return data; - } catch (error) { - console.error('SMS sending failed:', JSON.stringify(error)); - throw new HttpException('The SMS was not sent. Please try again later.', HttpStatus.BAD_GATEWAY); - } - } - - formatPhone(phone: string): string { - // remove spaces, dashes, parentheses - phone = phone.replace(/[^\d+]/g, ''); - - if (phone.startsWith('+98')) { - return phone; - } - - if (phone.startsWith('98')) { - return `+${phone}`; - } - - if (phone.startsWith('0')) { - return `+98${phone.slice(1)}`; - } - - return `+98${phone}`; - } -}