feat(notifications): add multi-channel notification module with Email, FCM, templates, and event listeners

- Domain: NotificationLog/NotificationPreference entities, repositories, channel value object
- Infrastructure: EmailService (nodemailer/SMTP), FcmService (firebase-admin), TemplateService (Handlebars)
- Application: SendNotification CQRS command, UserRegistered + AgentVerified event listeners
- Presentation: NotificationsController with history, preferences, and templates endpoints
- Prisma: NotificationLog and NotificationPreference models with proper indexes
- Templates: Vietnamese notification templates for user.registered, agent.verified, listing.approved, inquiry.received, password.reset

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 01:42:17 +07:00
parent 9301f44119
commit 0b29fac35e
42 changed files with 1720 additions and 6 deletions

View File

@@ -0,0 +1,70 @@
import {
Controller,
Get,
Put,
Body,
Query,
UseGuards,
Inject,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { CurrentUser } from '@modules/auth/presentation/decorators';
import { type JwtPayload } from '@modules/auth';
import {
NOTIFICATION_REPOSITORY,
type INotificationRepository,
NOTIFICATION_PREFERENCE_REPOSITORY,
type INotificationPreferenceRepository,
} from '../../domain';
import { TemplateService } from '../../infrastructure/services/template.service';
import { IsBoolean, IsEnum, IsString } from 'class-validator';
import { NotificationChannel as PrismaChannel } from '@prisma/client';
class UpdatePreferenceDto {
@IsEnum(PrismaChannel)
channel!: PrismaChannel;
@IsString()
eventType!: string;
@IsBoolean()
enabled!: boolean;
}
@Controller('notifications')
@UseGuards(AuthGuard('jwt'))
export class NotificationsController {
constructor(
@Inject(NOTIFICATION_REPOSITORY)
private readonly notificationRepo: INotificationRepository,
@Inject(NOTIFICATION_PREFERENCE_REPOSITORY)
private readonly preferenceRepo: INotificationPreferenceRepository,
private readonly templateService: TemplateService,
) {}
@Get('history')
async getHistory(
@CurrentUser() user: JwtPayload,
@Query('limit') limit?: number,
) {
return this.notificationRepo.findByUserId(user.sub, limit ?? 50);
}
@Get('preferences')
async getPreferences(@CurrentUser() user: JwtPayload) {
return this.preferenceRepo.findByUserId(user.sub);
}
@Put('preferences')
async updatePreference(
@CurrentUser() user: JwtPayload,
@Body() dto: UpdatePreferenceDto,
) {
return this.preferenceRepo.upsert(user.sub, dto.channel, dto.eventType, dto.enabled);
}
@Get('templates')
async getTemplates() {
return { templates: this.templateService.getTemplateKeys() };
}
}