From 6fd9b6d5194f62e81255a28ddb4c712eb7942936 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Thu, 25 Jun 2026 07:41:33 +0000 Subject: [PATCH] chore: setup sms rabbit mq --- apps/edr-freight-api/.env.example | 7 ++ .../src/modules/notifications/dtos/sms.dto.ts | 46 +++++++++++++ .../notifications/notifications.module.ts | 21 +++++- .../notifications/sms-client.service.ts | 68 +++++++++++++++++++ .../src/modules/otp/otp.module.ts | 3 + .../src/modules/otp/otp.service.ts | 39 +++-------- 6 files changed, 151 insertions(+), 33 deletions(-) create mode 100644 apps/edr-freight-api/src/modules/notifications/dtos/sms.dto.ts create mode 100644 apps/edr-freight-api/src/modules/notifications/sms-client.service.ts diff --git a/apps/edr-freight-api/.env.example b/apps/edr-freight-api/.env.example index 73312aae3..4ccffb6a4 100644 --- a/apps/edr-freight-api/.env.example +++ b/apps/edr-freight-api/.env.example @@ -52,3 +52,10 @@ MINIO_SECRET_KEY= # Redis REDIS_HOST=localhost REDIS_PORT=6379 + +# --- Notification broker (RabbitMQ) --------------------------------------------- +# SMS OTP / notifications are queued to RabbitMQ (consumed by the shared SMS service). +# Set RABBITMQ_ENABLED=false to skip the broker entirely (dev without a local broker). +RABBITMQ_ENABLED=false +RABBITMQ_URL=amqp://localhost:5672 +SMS_QUEUE=sms_queue diff --git a/apps/edr-freight-api/src/modules/notifications/dtos/sms.dto.ts b/apps/edr-freight-api/src/modules/notifications/dtos/sms.dto.ts new file mode 100644 index 000000000..b263b54f0 --- /dev/null +++ b/apps/edr-freight-api/src/modules/notifications/dtos/sms.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class SendMessage { + @ApiProperty() + @IsNotEmpty() + @IsString() + to!: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() + message!: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + from?: string; +} + +export class SingleMessageDto { + @ApiProperty({ + description: 'Recipient phone number', + example: '+1234567890', + }) + @IsString() + @IsNotEmpty() + to!: string; + + @ApiProperty({ + description: 'Message content', + example: 'Test Single SMS from', + }) + @IsString() + @IsNotEmpty() + message!: string; +} + +export class BulkMessagesDto { + @ApiProperty({ type: [SendMessage] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SendMessage) + messages!: SendMessage[]; +} diff --git a/apps/edr-freight-api/src/modules/notifications/notifications.module.ts b/apps/edr-freight-api/src/modules/notifications/notifications.module.ts index 70e00c9ac..663f931ef 100644 --- a/apps/edr-freight-api/src/modules/notifications/notifications.module.ts +++ b/apps/edr-freight-api/src/modules/notifications/notifications.module.ts @@ -1,14 +1,29 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; +import { ClientsModule, Transport } from "@nestjs/microservices"; import { NotificationsService } from "./notifications.service"; +import { SmsClientService } from "./sms-client.service"; import { EmailNotificationStrategy } from "./strategies/notification.email.strategy"; import { SmsNotificationStrategy } from "./strategies/notification.sms.strategy"; @Module({ - imports: [ConfigModule], + imports: [ + ConfigModule, + ClientsModule.register([ + { + name: "SMS_SERVICE", + transport: Transport.RMQ, + options: { + urls: [process.env.RABBITMQ_URL as string], + queue: process.env.SMS_QUEUE ?? "sms_queue", + queueOptions: { durable: true }, + }, + }, + ]), + ], controllers: [], - providers: [EmailNotificationStrategy, SmsNotificationStrategy, NotificationsService], - exports: [NotificationsService], + providers: [EmailNotificationStrategy, SmsNotificationStrategy, NotificationsService, SmsClientService], + exports: [NotificationsService, SmsClientService], }) export class NotificationsModule {} diff --git a/apps/edr-freight-api/src/modules/notifications/sms-client.service.ts b/apps/edr-freight-api/src/modules/notifications/sms-client.service.ts new file mode 100644 index 000000000..f94c0c20e --- /dev/null +++ b/apps/edr-freight-api/src/modules/notifications/sms-client.service.ts @@ -0,0 +1,68 @@ +import { + Inject, + Injectable, + Logger, + OnApplicationBootstrap, +} from "@nestjs/common"; +import { ClientProxy } from "@nestjs/microservices"; +import { BulkMessagesDto, SingleMessageDto } from "./dtos/sms.dto"; + +@Injectable() +export class SmsClientService implements OnApplicationBootstrap { + private readonly logger = new Logger(SmsClientService.name); + + constructor( + @Inject("SMS_SERVICE") + private smsClient: ClientProxy, + ) {} + + private readonly enabled = process.env.RABBITMQ_ENABLED !== "false"; + + async onApplicationBootstrap() { + if (!this.enabled) return; + this.smsClient + .connect() + .then(() => { + this.logger.log("connected to SMS service"); + }) + .catch((err) => { + console.error("Error happened at SMS service", err); + }); + } + + async sendSms(dto: SingleMessageDto): Promise<{ queued: boolean }> { + if (!this.enabled) { + this.logger.warn(`RABBITMQ disabled — skipped SMS`); + return { queued: false }; + } + this.smsClient.emit("send-sms", { + to: dto.to, + text: dto.message, + appKey: "IFHCRS-LICENSE-MANAGEMENT", + }); + // Fire-and-forget enqueue: confirms hand-off to RabbitMQ, NOT delivery. + this.logger.log( + `SMS queued to RabbitMQ [${process.env.SMS_QUEUE ?? "sms_queue"}] pattern='send-sms'`, + ); + // Recipient + content are PII — debug only. + this.logger.debug(`SMS payload to=${dto.to} text="${dto.message}"`); + return { queued: true }; + } + + async sendBulkMessages(dto: BulkMessagesDto): Promise<{ queued: boolean }> { + if (!this.enabled) { + this.logger.warn(`RABBITMQ disabled — skipped BULK SMS (${dto.messages?.length ?? 0} messages)`); + return { queued: false }; + } + const messages = (dto.messages ?? []).map((m) => ({ to: m.to, text: m.message, from: m.from })); + this.smsClient.emit("ozeking-bulk-sms", { + messages, + appKey: "IFHCRS-LICENSE-MANAGEMENT", + }); + this.logger.log( + `BULK SMS queued to RabbitMQ [${process.env.SMS_QUEUE ?? "sms_queue"}] pattern='ozeking-bulk-sms' count=${messages.length}`, + ); + this.logger.debug(`BULK SMS payload messages=${JSON.stringify(messages)}`); + return { queued: true }; + } +} diff --git a/apps/edr-freight-api/src/modules/otp/otp.module.ts b/apps/edr-freight-api/src/modules/otp/otp.module.ts index 7a6d1faa6..ec1d9f9ed 100644 --- a/apps/edr-freight-api/src/modules/otp/otp.module.ts +++ b/apps/edr-freight-api/src/modules/otp/otp.module.ts @@ -12,11 +12,14 @@ import { OtpService } from "./otp.service"; import { OtpRepository } from "./otp.repository"; +import { NotificationsModule } from "../notifications/notifications.module"; + @Module({ imports: [ TypeOrmModule.forFeature([ OtpVerification, ]), + NotificationsModule, ], controllers: [OtpController], diff --git a/apps/edr-freight-api/src/modules/otp/otp.service.ts b/apps/edr-freight-api/src/modules/otp/otp.service.ts index d70bc0ce8..ffa9c4e68 100644 --- a/apps/edr-freight-api/src/modules/otp/otp.service.ts +++ b/apps/edr-freight-api/src/modules/otp/otp.service.ts @@ -5,14 +5,15 @@ import { Injectable, } from "@nestjs/common"; -import axios from "axios"; - import { OtpRepository } from "./otp.repository"; +import { SmsClientService } from "../notifications/sms-client.service"; + @Injectable() export class OtpService { constructor( - private readonly otpRepository: OtpRepository + private readonly otpRepository: OtpRepository, + private readonly smsClient: SmsClientService ) {} // --------------------------------------------------------------------------- @@ -56,33 +57,11 @@ export class OtpService { ); } - // send sms - await axios.post( - "https://notification-dev.license.aafda.gov.et/api/sms-services/ozeking/sms", - { - to: phone, - - sourceId: "EDR", - - sourceName: - "EDR Freight", - - appKey: - "YOUR_APP_KEY", - - text: `Your verification code is ${otp}`, - - callbackUrl: "", - }, - { - headers: { - accept: "*/*", - - "Content-Type": - "application/json", - }, - } - ); + // send sms (queued to RabbitMQ via the shared SMS service) + await this.smsClient.sendSms({ + to: phone, + message: `Your verification code is ${otp}`, + }); return { success: true,