This commit is contained in:
2025-12-22 23:27:53 +03:30
parent ce2987d67b
commit 4dafba955e
14 changed files with 126 additions and 25 deletions
@@ -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();
}
@@ -0,0 +1,7 @@
export class SmsSentEvent {
constructor(
public readonly phoneNumber: string,
public readonly restaurantId: string,
) {}
}
@@ -3,6 +3,7 @@ import type { NotifTitleEnum, recipientType } from './notification.interface';
export interface SmsNotificationQueueJob {
recipient: recipientType;
templateId: string;
restaurantId: string;
parameters?: Record<string, string>;
}
export interface PushNotificationQueueJob {
@@ -0,0 +1,16 @@
export interface ISmsResponse {
status: number;
message: string;
}
export interface ISmsParams {
phone: string;
parameters?: Record<string, string | number | Date>;
templateId: string;
}
export interface ISmsBodyParameters {
Mobile: string;
TemplateId: string;
Parameters: { name: string; value: string }[];
}
@@ -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),
);
}
}
}
@@ -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 { }
@@ -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<SmsNotificationQueueJob>) {
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,
};
@@ -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<SmsLog> {
constructor(readonly em: EntityManager) {
super(em, SmsLog);
}
}
@@ -58,6 +58,7 @@ export class NotificationService {
recipient,
templateId: message.sms.templateId,
parameters: message.sms.parameters,
restaurantId,
})),
);
}
@@ -0,0 +1,80 @@
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<string>('SMS_BASE_URL');
this.apiKey = this.configService.getOrThrow<string>('SMS_API_KEY');
this.OTP = this.configService.getOrThrow<string>('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<ISmsResponse>(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}`;
}
}