From 1a71f654b15f159f84a93c4a7d0c35c52974ce13 Mon Sep 17 00:00:00 2001 From: Roba Boru Date: Tue, 16 Jun 2026 14:21:56 +0300 Subject: [PATCH] Update email and sms services --- apps/edr-passenger-api/package.json | 6 +-- .../modules/notifications/dtos/email.dto.ts | 8 +++ .../src/modules/notifications/dtos/sms.dto.ts | 9 ++++ .../notifications/email-client.service.ts | 28 +++++++++++ .../notifications/notifications.controller.ts | 30 +++++++++++- .../notifications/notifications.module.ts | 49 +++++++++++++++++-- .../notifications/notifications.service.ts | 24 ++++----- .../notifications/sms-client.service.ts | 36 ++++++++++++++ 8 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 apps/edr-passenger-api/src/modules/notifications/dtos/email.dto.ts create mode 100644 apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts create mode 100644 apps/edr-passenger-api/src/modules/notifications/email-client.service.ts create mode 100644 apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index f1ecd5e07..84ea472d4 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -19,7 +19,6 @@ "prisma:backfill": "ts-node prisma/backfill-fields.ts", "prisma:verify": "ts-node prisma/verify-backfill.ts" }, - "dependencies": { "@edr/types": "workspace:*", "@golevelup/nestjs-rabbitmq": "^5.5.0", @@ -29,6 +28,7 @@ "@nestjs/core": "^11.1.19", "@nestjs/event-emitter": "^2.0.4", "@nestjs/jwt": "^10.2.0", + "@nestjs/microservices": "^11.1.24", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^11.1.19", "@nestjs/schedule": "^6.1.3", @@ -63,13 +63,13 @@ "@types/passport-jwt": "^4.0.1", "@types/qrcode": "^1.5.5", "@types/supertest": "^6.0.2", + "@types/uuid": "^9.0.0", "jest": "^29.7.0", "prisma": "^6.19.3", "supertest": "^7.0.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", - "typescript": "^5.3.3", - "@types/uuid": "^9.0.0" + "typescript": "^5.3.3" }, "prisma": { "schema": "prisma/schema.prisma" diff --git a/apps/edr-passenger-api/src/modules/notifications/dtos/email.dto.ts b/apps/edr-passenger-api/src/modules/notifications/dtos/email.dto.ts new file mode 100644 index 000000000..63bbc4a5a --- /dev/null +++ b/apps/edr-passenger-api/src/modules/notifications/dtos/email.dto.ts @@ -0,0 +1,8 @@ +export class SendEmail { + to: string; + subject: string; + body: string; + html?: string; + templateKey?: string; + context?: Record; +} diff --git a/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts b/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts new file mode 100644 index 000000000..1897f7794 --- /dev/null +++ b/apps/edr-passenger-api/src/modules/notifications/dtos/sms.dto.ts @@ -0,0 +1,9 @@ +export class SendMessage { + to: string; + message: string; + from?: string; +} + +export class BulkMessagesDto { + messages: SendMessage[]; +} diff --git a/apps/edr-passenger-api/src/modules/notifications/email-client.service.ts b/apps/edr-passenger-api/src/modules/notifications/email-client.service.ts new file mode 100644 index 000000000..8fcb82394 --- /dev/null +++ b/apps/edr-passenger-api/src/modules/notifications/email-client.service.ts @@ -0,0 +1,28 @@ +import { Inject, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { SendEmail } from './dtos/email.dto'; + +@Injectable() +export class EmailClientService implements OnApplicationBootstrap { + private readonly logger = new Logger(EmailClientService.name); + + constructor( + @Inject('EMAIL_SERVICE') + private readonly emailServiceClient: ClientProxy, + ) {} + + async onApplicationBootstrap() { + this.emailServiceClient + .connect() + .then(() => this.logger.log('Connected to Email service')) + .catch((err) => this.logger.error('Error connecting to Email service', err)); + } + + async sendEmail(dto: SendEmail) { + this.emailServiceClient.emit('send-email', { + ...dto, + appKey: 'EDR-PASSENGER-API', + }); + return {}; + } +} diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts index 9b363c04f..9fd69830c 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.controller.ts @@ -1,16 +1,24 @@ import { Controller, Get, Param, Patch, Post, Body, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger'; import { NotificationsService } from './notifications.service'; import { JwtGuard } from '../../common/jwt.guard'; import { IamGuard, IamRoles } from '../../common/iam-adapter'; import { TestNotificationDto } from './notifications.dto'; +import { EmailClientService } from './email-client.service'; +import { SmsClientService } from './sms-client.service'; +import { SendEmail } from './dtos/email.dto'; +import { SendMessage } from './dtos/sms.dto'; @ApiTags('Notifications') @Controller('notifications') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') export class NotificationsController { - constructor(private service: NotificationsService) {} + constructor( + private service: NotificationsService, + private emailClient: EmailClientService, + private smsClient: SmsClientService, + ) {} @Get(':passengerId') @ApiOperation({ summary: 'Get notifications for passenger' }) @@ -30,6 +38,24 @@ export class NotificationsController { return this.service.markAllRead(id); } + @Post('send/email') + @UseGuards(IamGuard) + @IamRoles('ADMIN', 'STAFF') + @ApiOperation({ summary: 'Send a direct email via the email microservice' }) + @ApiBody({ type: SendEmail }) + sendEmail(@Body() dto: SendEmail) { + return this.emailClient.sendEmail(dto); + } + + @Post('send/sms') + @UseGuards(IamGuard) + @IamRoles('ADMIN', 'STAFF') + @ApiOperation({ summary: 'Send a direct SMS via the SMS microservice' }) + @ApiBody({ type: SendMessage }) + sendSms(@Body() dto: SendMessage) { + return this.smsClient.sendSms(dto); + } + @Post('test') @UseGuards(IamGuard) @IamRoles('ADMIN', 'STAFF') diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.module.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.module.ts index b4c28405f..f209ccf17 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.module.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.module.ts @@ -1,13 +1,56 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ClientsModule, Transport } from '@nestjs/microservices'; import { NotificationsController } from './notifications.controller'; import { NotificationsService } from './notifications.service'; import { EmailAdapter, SmsAdapter, PushAdapter } from './notification.adapters'; +import { EmailClientService } from './email-client.service'; +import { SmsClientService } from './sms-client.service'; @Module({ - imports: [HttpModule.register({ timeout: 10_000 })], + imports: [ + HttpModule.register({ timeout: 10_000 }), + ClientsModule.registerAsync([ + { + name: 'EMAIL_SERVICE', + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + transport: Transport.RMQ, + options: { + urls: [config.get('RABBITMQ_URL') ?? 'amqp://localhost:5672'], + queue: config.get('EMAIL_QUEUE') ?? 'email_queue', + queueOptions: { durable: true }, + noAck: true, + }, + }), + }, + { + name: 'SMS_SERVICE', + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + transport: Transport.RMQ, + options: { + urls: [config.get('RABBITMQ_URL') ?? 'amqp://localhost:5672'], + queue: config.get('SMS_QUEUE') ?? 'sms_queue', + queueOptions: { durable: true }, + noAck: true, + }, + }), + }, + ]), + ], controllers: [NotificationsController], - providers: [NotificationsService, EmailAdapter, SmsAdapter, PushAdapter], - exports: [NotificationsService], + providers: [ + NotificationsService, + EmailAdapter, + SmsAdapter, + PushAdapter, + EmailClientService, + SmsClientService, + ], + exports: [NotificationsService, EmailClientService, SmsClientService], }) export class NotificationsModule {} diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts index e793a1322..68d76572c 100644 --- a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts +++ b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts @@ -1,8 +1,10 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { PrismaService } from '../../common/prisma.service'; import { SendNotificationDto, NotificationCategoryEnum } from './notifications.dto'; -import { EmailAdapter, SmsAdapter, PushAdapter, NotificationChannel } from './notification.adapters'; +import { PushAdapter, NotificationChannel } from './notification.adapters'; +import { EmailClientService } from './email-client.service'; +import { SmsClientService } from './sms-client.service'; export type NotificationChannelType = 'EMAIL' | 'SMS' | 'PUSH' | 'IN_APP'; @@ -13,13 +15,13 @@ export class NotificationsService { constructor( private prisma: PrismaService, - private emailAdapter: EmailAdapter, - private smsAdapter: SmsAdapter, + private emailClient: EmailClientService, + private smsClient: SmsClientService, private pushAdapter: PushAdapter, ) { this.channels = new Map([ - ['EMAIL', this.emailAdapter as NotificationChannel], - ['SMS', this.smsAdapter as NotificationChannel], + ['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, body }).then(() => true) }], + ['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, message: body }).then(() => true) }], ['PUSH', this.pushAdapter as NotificationChannel], ]); } @@ -102,11 +104,11 @@ export class NotificationsService { }); if (passenger?.user) { - await this.emailAdapter.send( - passenger.user.email, - this.sanitize(dto.title), - this.sanitize(dto.body), - ); + await this.emailClient.sendEmail({ + to: passenger.user.email, + subject: this.sanitize(dto.title), + body: this.sanitize(dto.body), + }); } return notification; diff --git a/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts b/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts new file mode 100644 index 000000000..0108e0758 --- /dev/null +++ b/apps/edr-passenger-api/src/modules/notifications/sms-client.service.ts @@ -0,0 +1,36 @@ +import { Inject, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BulkMessagesDto, SendMessage } from './dtos/sms.dto'; + +@Injectable() +export class SmsClientService implements OnApplicationBootstrap { + private readonly logger = new Logger(SmsClientService.name); + + constructor( + @Inject('SMS_SERVICE') + private readonly smsClient: ClientProxy, + ) {} + + async onApplicationBootstrap() { + this.smsClient + .connect() + .then(() => this.logger.log('Connected to SMS service')) + .catch((err) => this.logger.error('Error connecting to SMS service', err)); + } + + async sendSms(dto: SendMessage) { + this.smsClient.emit('send-sms', { + ...dto, + appKey: 'EDR-PASSENGER-API', + }); + return {}; + } + + async sendBulkMessages(dto: BulkMessagesDto) { + this.smsClient.emit('ozeking-bulk-sms', { + ...dto, + appKey: 'EDR-PASSENGER-API', + }); + return {}; + } +}