From 4dafba955ee97f0944f29624f54f01b16fd0a3af Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 22 Dec 2025 23:27:53 +0330 Subject: [PATCH] sms logs --- src/modules/auth/auth.module.ts | 3 + src/modules/auth/services/auth.service.ts | 2 +- .../notifications/entities/smsLogs.entity.ts | 17 ++++++ .../notifications/events/sms.events.ts | 7 +++ .../interfaces/jobs-queue.interface.ts | 1 + .../interfaces}/sms.ts | 0 .../notifications/listeners/sms.listeners.ts | 55 +++++++++++++++++++ .../notifications/notifications.module.ts | 24 +++++--- .../notifications/processors/sms.processor.ts | 9 ++- .../repositories/sms-log.repository.ts | 11 ++++ .../services/notification.service.ts | 1 + .../services}/sms.service.ts | 3 +- .../orders/listeners/order.listeners.ts | 11 +--- src/modules/utils/utils.module.ts | 7 +-- 14 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 src/modules/notifications/entities/smsLogs.entity.ts create mode 100644 src/modules/notifications/events/sms.events.ts rename src/modules/{utils/interface => notifications/interfaces}/sms.ts (100%) create mode 100644 src/modules/notifications/listeners/sms.listeners.ts create mode 100644 src/modules/notifications/repositories/sms-log.repository.ts rename src/modules/{utils => notifications/services}/sms.service.ts (96%) diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index a7519cc..ebb6d27 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -12,6 +12,7 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { User } from '../users/entities/user.entity'; import { AdminAuthGuard } from './guards/adminAuth.guard'; import { RefreshToken } from './entities/refresh-token.entity'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ imports: [ @@ -34,6 +35,8 @@ import { RefreshToken } from './entities/refresh-token.entity'; forwardRef(() => AdminModule), forwardRef(() => RestaurantsModule), MikroOrmModule.forFeature([User, RefreshToken]), + forwardRef(() => NotificationsModule), + ], controllers: [AuthController], providers: [AuthService, TokensService, AdminAuthGuard], diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 10e382e..227c599 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,7 +1,7 @@ import { Injectable, BadRequestException } from '@nestjs/common'; import { RequestOtpDto } from '../dto/request-otp.dto'; import { CacheService } from '../../utils/cache.service'; -import { SmsService } from '../../utils/sms.service'; +import { SmsService } from '../../notifications/services/sms.service'; import { UserService } from '../../users/providers/user.service'; import { randomInt } from 'crypto'; import { TokensService } from './tokens.service'; diff --git a/src/modules/notifications/entities/smsLogs.entity.ts b/src/modules/notifications/entities/smsLogs.entity.ts new file mode 100644 index 0000000..edeb103 --- /dev/null +++ b/src/modules/notifications/entities/smsLogs.entity.ts @@ -0,0 +1,17 @@ +import { Entity, Property, ManyToOne, PrimaryKey } from '@mikro-orm/core'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; + +@Entity({ tableName: 'sms_logs' }) +export class SmsLog { + @PrimaryKey() + id!: number; + + @ManyToOne(() => Restaurant) + restaurant!: Restaurant; + + @Property() + phone!: string; + + @Property() + createdAt: Date = new Date(); +} \ No newline at end of file diff --git a/src/modules/notifications/events/sms.events.ts b/src/modules/notifications/events/sms.events.ts new file mode 100644 index 0000000..303b366 --- /dev/null +++ b/src/modules/notifications/events/sms.events.ts @@ -0,0 +1,7 @@ +export class SmsSentEvent { + constructor( + public readonly phoneNumber: string, + public readonly restaurantId: string, + ) {} +} + diff --git a/src/modules/notifications/interfaces/jobs-queue.interface.ts b/src/modules/notifications/interfaces/jobs-queue.interface.ts index 0cdd80f..470a72c 100644 --- a/src/modules/notifications/interfaces/jobs-queue.interface.ts +++ b/src/modules/notifications/interfaces/jobs-queue.interface.ts @@ -3,6 +3,7 @@ import type { NotifTitleEnum, recipientType } from './notification.interface'; export interface SmsNotificationQueueJob { recipient: recipientType; templateId: string; + restaurantId: string; parameters?: Record; } export interface PushNotificationQueueJob { diff --git a/src/modules/utils/interface/sms.ts b/src/modules/notifications/interfaces/sms.ts similarity index 100% rename from src/modules/utils/interface/sms.ts rename to src/modules/notifications/interfaces/sms.ts diff --git a/src/modules/notifications/listeners/sms.listeners.ts b/src/modules/notifications/listeners/sms.listeners.ts new file mode 100644 index 0000000..8c2a842 --- /dev/null +++ b/src/modules/notifications/listeners/sms.listeners.ts @@ -0,0 +1,55 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { SmsSentEvent } from '../events/sms.events'; +import { RestRepository } from '../../restaurants/repositories/rest.repository'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { SmsLog } from '../entities/smsLogs.entity'; + +@Injectable() +export class SmsListeners { + private readonly logger = new Logger(SmsListeners.name); + + constructor( + private readonly restRepository: RestRepository, + private readonly em: EntityManager, + ) { + } + + @OnEvent(SmsSentEvent.name) + async handleSmsSent(event: SmsSentEvent) { + try { + this.logger.log( + `SMS sent event received: phone ${event.phoneNumber} for restaurant: ${event.restaurantId}`, + ); + + // Get the restaurant entity + const restaurant = await this.restRepository.findOne({ id: event.restaurantId }); + + if (!restaurant) { + this.logger.warn( + `Restaurant not found for SMS log: ${event.restaurantId}`, + ); + return; + } + + // Create SMS log record + const smsLog = this.em.create(SmsLog, { + restaurant: restaurant, + phone: event.phoneNumber, + createdAt: new Date(), + }); + + await this.em.flush(); + + this.logger.log( + `SMS log created successfully: ${smsLog.id} for phone ${event.phoneNumber}`, + ); + } catch (error) { + this.logger.error( + `Failed to create SMS log for event: ${event.restaurantId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } + +} \ No newline at end of file diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts index 291938c..c99a1ae 100644 --- a/src/modules/notifications/notifications.module.ts +++ b/src/modules/notifications/notifications.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { BullModule } from '@nestjs/bullmq'; import { JwtModule } from '@nestjs/jwt'; @@ -23,13 +23,17 @@ import { InAppProcessor } from './processors/in-app.processor'; import { AdminModule } from '../admin/admin.module'; import { UserModule } from '../users/user.module'; import { NotificationCrone } from './crone/notification.crone'; +import { SmsService } from './services/sms.service'; +import { SmsLog } from './entities/smsLogs.entity'; +import { SmsLogRepository } from './repositories/sms-log.repository'; +import { RestaurantsModule } from '../restaurants/restaurants.module'; +import { SmsListeners } from './listeners/sms.listeners'; @Module({ imports: [ - AuthModule, - UtilsModule, + forwardRef(() => AuthModule), JwtModule, - MikroOrmModule.forFeature([Notification, NotificationPreference, User, Restaurant]), + MikroOrmModule.forFeature([Notification, NotificationPreference, User, Restaurant, SmsLog]), BullModule.registerQueue( { name: NotificationQueueNameEnum.SMS }, { name: NotificationQueueNameEnum.PUSH }, @@ -46,7 +50,9 @@ import { NotificationCrone } from './crone/notification.crone'; }), }), AdminModule, - UserModule, + forwardRef(() => UserModule), + UtilsModule, + forwardRef(() => RestaurantsModule), ], controllers: [NotificationsController], providers: [ @@ -60,7 +66,11 @@ import { NotificationCrone } from './crone/notification.crone'; NotificationsGateway, WsAdminAuthGuard, NotificationCrone, + SmsService, + SmsLogRepository, + SmsListeners, ], - exports: [NotificationService, NotificationPreferenceService, NotificationQueueService, PushNotificationService], + exports: [NotificationService, NotificationPreferenceService, + NotificationQueueService, PushNotificationService, SmsService], }) -export class NotificationsModule {} +export class NotificationsModule { } diff --git a/src/modules/notifications/processors/sms.processor.ts b/src/modules/notifications/processors/sms.processor.ts index be46c39..0846e2b 100644 --- a/src/modules/notifications/processors/sms.processor.ts +++ b/src/modules/notifications/processors/sms.processor.ts @@ -5,7 +5,9 @@ import { UserRepository } from 'src/modules/users/repositories/user.repository'; import { SmsNotificationQueueJob } from '../interfaces/jobs-queue.interface'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; import { NotificationQueueNameEnum } from '../constants/queue'; -import { SmsService } from 'src/modules/utils/sms.service'; +import { SmsService } from '../services/sms.service'; +import { SmsSentEvent } from '../events/sms.events'; +import { EventEmitter2 } from '@nestjs/event-emitter'; @Processor(NotificationQueueNameEnum.SMS) export class SmsProcessor extends WorkerHost { @@ -15,12 +17,13 @@ export class SmsProcessor extends WorkerHost { private readonly adminRepository: AdminRepository, private readonly userRepository: UserRepository, private readonly smsService: SmsService, + private readonly eventEmitter: EventEmitter2, ) { super(); } async process(job: Job) { - const { recipient, templateId, parameters } = job.data; + const { recipient, templateId, parameters, restaurantId } = job.data; this.logger.log(`Processing SMS notification - Recipient: ${JSON.stringify(recipient)}, templateID: ${templateId}`); @@ -55,6 +58,8 @@ export class SmsProcessor extends WorkerHost { this.logger.log(`SMS notification sent successfully to ${phone}`); + this.eventEmitter.emit(SmsSentEvent.name, new SmsSentEvent(phone, restaurantId)); + return { success: true, }; diff --git a/src/modules/notifications/repositories/sms-log.repository.ts b/src/modules/notifications/repositories/sms-log.repository.ts new file mode 100644 index 0000000..2653094 --- /dev/null +++ b/src/modules/notifications/repositories/sms-log.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { SmsLog } from '../entities/smsLogs.entity'; + +@Injectable() +export class SmsLogRepository extends EntityRepository { + constructor(readonly em: EntityManager) { + super(em, SmsLog); + } +} + diff --git a/src/modules/notifications/services/notification.service.ts b/src/modules/notifications/services/notification.service.ts index 66993c1..b35dca5 100644 --- a/src/modules/notifications/services/notification.service.ts +++ b/src/modules/notifications/services/notification.service.ts @@ -58,6 +58,7 @@ export class NotificationService { recipient, templateId: message.sms.templateId, parameters: message.sms.parameters, + restaurantId, })), ); } diff --git a/src/modules/utils/sms.service.ts b/src/modules/notifications/services/sms.service.ts similarity index 96% rename from src/modules/utils/sms.service.ts rename to src/modules/notifications/services/sms.service.ts index 9756ced..996664b 100644 --- a/src/modules/utils/sms.service.ts +++ b/src/modules/notifications/services/sms.service.ts @@ -1,7 +1,7 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; -import { ISmsParams, ISmsResponse, ISmsBodyParameters } from './interface/sms'; +import { ISmsParams, ISmsResponse, ISmsBodyParameters } from '../interfaces/sms'; @Injectable() export class SmsService { @@ -58,6 +58,7 @@ export class SmsService { 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, ''); diff --git a/src/modules/orders/listeners/order.listeners.ts b/src/modules/orders/listeners/order.listeners.ts index 9801754..e654b7d 100644 --- a/src/modules/orders/listeners/order.listeners.ts +++ b/src/modules/orders/listeners/order.listeners.ts @@ -28,7 +28,7 @@ export class OrderListeners { const statusMap: Record = { [OrderStatus.PENDING_PAYMENT]: 'در انتظار پرداخت', [OrderStatus.PAID]: 'پرداخت شده', - [OrderStatus.PREPARING]: 'در حال آماده‌سازی', + [OrderStatus.PREPARING]: 'در حال آماده‌سازی', [OrderStatus.DELIVERED_TO_RECEPTIONIST]: 'تحویل به پذیرش', [OrderStatus.DELIVERED_TO_WAITER]: 'تحویل به گارسون', [OrderStatus.SHIPPED]: 'ارسال شده', @@ -91,15 +91,6 @@ export class OrderListeners { `Order status changed event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, ); - // const order = await this.OrderRepository.findOneById(event.orderId, { populate: ['user'] }); - // if (!order) { - // this.logger.warn(`Order not found: ${event.orderId}`); - // return; - // } - // if (order.user && order.user.phoneNumber) { - // this.logger.warn(`Order not found: ${event.orderId}`); - // } - const recipients = [ { userId: event.userId, diff --git a/src/modules/utils/utils.module.ts b/src/modules/utils/utils.module.ts index 79132d8..2f7cef5 100644 --- a/src/modules/utils/utils.module.ts +++ b/src/modules/utils/utils.module.ts @@ -1,12 +1,11 @@ import { Module } from '@nestjs/common'; import { CacheService } from './cache.service'; -import { SmsService } from './sms.service'; - + @Module({ imports: [], controllers: [], - providers: [CacheService, SmsService], - exports: [CacheService, SmsService], + providers: [CacheService, ], + exports: [CacheService], }) export class UtilsModule {}