feat(api): add price history, Stringee SMS, Zalo OA, WebSocket notifications, and feature-listing command

- Add PriceHistory model + migration, price-changed domain event, and event handler
- Add GetPriceHistory query handler and controller endpoint
- Implement StringeeSmsService and ZaloOaService with unit tests
- Add Zalo ZNS templates for Vietnamese notification messages
- Add WebSocket notification gateway for real-time push
- Add FeatureListingCommand for promoted listings
- Apply remaining consistent-type-imports lint fixes across API modules

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 05:15:04 +07:00
parent c920934fb6
commit d4e100a00c
48 changed files with 1766 additions and 225 deletions

View File

@@ -13,14 +13,15 @@ import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiProperty } from '@nestjs/swagger';
import { NotificationChannel as PrismaChannel } from '@prisma/client';
import { IsBoolean, IsEnum, IsString } from 'class-validator';
import { CurrentUser, JwtPayload } from '@modules/auth';
import { CurrentUser, type JwtPayload } from '@modules/auth';
import {
NOTIFICATION_REPOSITORY,
INotificationRepository,
type INotificationRepository,
NOTIFICATION_PREFERENCE_REPOSITORY,
INotificationPreferenceRepository,
type INotificationPreferenceRepository,
} from '../../domain';
import { TemplateService } from '../../infrastructure/services/template.service';
import { type TemplateService } from '../../infrastructure/services/template.service';
import { type NotificationsGateway } from '../gateways/notifications.gateway';
class UpdatePreferenceDto {
@ApiProperty({ enum: PrismaChannel, description: 'Notification channel' })
@@ -47,6 +48,7 @@ export class NotificationsController {
@Inject(NOTIFICATION_PREFERENCE_REPOSITORY)
private readonly preferenceRepo: INotificationPreferenceRepository,
private readonly templateService: TemplateService,
private readonly notificationsGateway: NotificationsGateway,
) {}
@Get('history')
@@ -80,6 +82,15 @@ export class NotificationsController {
return this.preferenceRepo.upsert(user.sub, dto.channel, dto.eventType, dto.enabled);
}
@Get('unread-count')
@ApiOperation({ summary: 'Get unread notification count (Redis-cached)' })
@ApiResponse({ status: 200, description: 'Unread count retrieved' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getUnreadCount(@CurrentUser() user: JwtPayload) {
const count = await this.notificationRepo.countUnreadByUserId(user.sub);
return { unreadCount: count };
}
@Get('unread')
@ApiOperation({ summary: 'Get unread notifications' })
@ApiResponse({ status: 200, description: 'Unread notifications retrieved' })
@@ -105,6 +116,9 @@ export class NotificationsController {
@Param('id') id: string,
) {
await this.notificationRepo.markAsRead(id, user.sub);
// Invalidate cached count and push updated count via WebSocket
await this.notificationsGateway.invalidateUnreadCount(user.sub);
await this.notificationsGateway.emitUnreadCount(user.sub);
return { success: true };
}
@@ -114,6 +128,9 @@ export class NotificationsController {
@ApiResponse({ status: 401, description: 'Unauthorized' })
async markAllAsRead(@CurrentUser() user: JwtPayload) {
const count = await this.notificationRepo.markAllAsRead(user.sub);
// Invalidate cached count and push updated count via WebSocket
await this.notificationsGateway.invalidateUnreadCount(user.sub);
await this.notificationsGateway.emitUnreadCount(user.sub);
return { markedCount: count };
}