sms logs
This commit is contained in:
@@ -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}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user