Update email and sms services

This commit is contained in:
Roba Boru
2026-06-16 14:21:56 +03:00
parent e9f06a52aa
commit 1a71f654b1
8 changed files with 171 additions and 19 deletions

View File

@@ -19,7 +19,6 @@
"prisma:backfill": "ts-node prisma/backfill-fields.ts", "prisma:backfill": "ts-node prisma/backfill-fields.ts",
"prisma:verify": "ts-node prisma/verify-backfill.ts" "prisma:verify": "ts-node prisma/verify-backfill.ts"
}, },
"dependencies": { "dependencies": {
"@edr/types": "workspace:*", "@edr/types": "workspace:*",
"@golevelup/nestjs-rabbitmq": "^5.5.0", "@golevelup/nestjs-rabbitmq": "^5.5.0",
@@ -29,6 +28,7 @@
"@nestjs/core": "^11.1.19", "@nestjs/core": "^11.1.19",
"@nestjs/event-emitter": "^2.0.4", "@nestjs/event-emitter": "^2.0.4",
"@nestjs/jwt": "^10.2.0", "@nestjs/jwt": "^10.2.0",
"@nestjs/microservices": "^11.1.24",
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^11.1.19", "@nestjs/platform-express": "^11.1.19",
"@nestjs/schedule": "^6.1.3", "@nestjs/schedule": "^6.1.3",
@@ -63,13 +63,13 @@
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/uuid": "^9.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"prisma": "^6.19.3", "prisma": "^6.19.3",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.3.3", "typescript": "^5.3.3"
"@types/uuid": "^9.0.0"
}, },
"prisma": { "prisma": {
"schema": "prisma/schema.prisma" "schema": "prisma/schema.prisma"

View File

@@ -0,0 +1,8 @@
export class SendEmail {
to: string;
subject: string;
body: string;
html?: string;
templateKey?: string;
context?: Record<string, unknown>;
}

View File

@@ -0,0 +1,9 @@
export class SendMessage {
to: string;
message: string;
from?: string;
}
export class BulkMessagesDto {
messages: SendMessage[];
}

View File

@@ -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 {};
}
}

View File

@@ -1,16 +1,24 @@
import { Controller, Get, Param, Patch, Post, Body, UseGuards } from '@nestjs/common'; 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 { NotificationsService } from './notifications.service';
import { JwtGuard } from '../../common/jwt.guard'; import { JwtGuard } from '../../common/jwt.guard';
import { IamGuard, IamRoles } from '../../common/iam-adapter'; import { IamGuard, IamRoles } from '../../common/iam-adapter';
import { TestNotificationDto } from './notifications.dto'; 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') @ApiTags('Notifications')
@Controller('notifications') @Controller('notifications')
@UseGuards(JwtGuard) @UseGuards(JwtGuard)
@ApiBearerAuth('JWT-auth') @ApiBearerAuth('JWT-auth')
export class NotificationsController { export class NotificationsController {
constructor(private service: NotificationsService) {} constructor(
private service: NotificationsService,
private emailClient: EmailClientService,
private smsClient: SmsClientService,
) {}
@Get(':passengerId') @Get(':passengerId')
@ApiOperation({ summary: 'Get notifications for passenger' }) @ApiOperation({ summary: 'Get notifications for passenger' })
@@ -30,6 +38,24 @@ export class NotificationsController {
return this.service.markAllRead(id); 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') @Post('test')
@UseGuards(IamGuard) @UseGuards(IamGuard)
@IamRoles('ADMIN', 'STAFF') @IamRoles('ADMIN', 'STAFF')

View File

@@ -1,13 +1,56 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { NotificationsController } from './notifications.controller'; import { NotificationsController } from './notifications.controller';
import { NotificationsService } from './notifications.service'; import { NotificationsService } from './notifications.service';
import { EmailAdapter, SmsAdapter, PushAdapter } from './notification.adapters'; import { EmailAdapter, SmsAdapter, PushAdapter } from './notification.adapters';
import { EmailClientService } from './email-client.service';
import { SmsClientService } from './sms-client.service';
@Module({ @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<string>('RABBITMQ_URL') ?? 'amqp://localhost:5672'],
queue: config.get<string>('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<string>('RABBITMQ_URL') ?? 'amqp://localhost:5672'],
queue: config.get<string>('SMS_QUEUE') ?? 'sms_queue',
queueOptions: { durable: true },
noAck: true,
},
}),
},
]),
],
controllers: [NotificationsController], controllers: [NotificationsController],
providers: [NotificationsService, EmailAdapter, SmsAdapter, PushAdapter], providers: [
exports: [NotificationsService], NotificationsService,
EmailAdapter,
SmsAdapter,
PushAdapter,
EmailClientService,
SmsClientService,
],
exports: [NotificationsService, EmailClientService, SmsClientService],
}) })
export class NotificationsModule {} export class NotificationsModule {}

View File

@@ -1,8 +1,10 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { PrismaService } from '../../common/prisma.service'; import { PrismaService } from '../../common/prisma.service';
import { SendNotificationDto, NotificationCategoryEnum } from './notifications.dto'; 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'; export type NotificationChannelType = 'EMAIL' | 'SMS' | 'PUSH' | 'IN_APP';
@@ -13,13 +15,13 @@ export class NotificationsService {
constructor( constructor(
private prisma: PrismaService, private prisma: PrismaService,
private emailAdapter: EmailAdapter, private emailClient: EmailClientService,
private smsAdapter: SmsAdapter, private smsClient: SmsClientService,
private pushAdapter: PushAdapter, private pushAdapter: PushAdapter,
) { ) {
this.channels = new Map<NotificationChannelType, NotificationChannel>([ this.channels = new Map<NotificationChannelType, NotificationChannel>([
['EMAIL', this.emailAdapter as NotificationChannel], ['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, body }).then(() => true) }],
['SMS', this.smsAdapter as NotificationChannel], ['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, message: body }).then(() => true) }],
['PUSH', this.pushAdapter as NotificationChannel], ['PUSH', this.pushAdapter as NotificationChannel],
]); ]);
} }
@@ -102,11 +104,11 @@ export class NotificationsService {
}); });
if (passenger?.user) { if (passenger?.user) {
await this.emailAdapter.send( await this.emailClient.sendEmail({
passenger.user.email, to: passenger.user.email,
this.sanitize(dto.title), subject: this.sanitize(dto.title),
this.sanitize(dto.body), body: this.sanitize(dto.body),
); });
} }
return notification; return notification;

View File

@@ -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 {};
}
}