- Pricing page: enhanced with checkout modal integration, plan comparison table, and subscription funnel - Payment return page: new VNPay/MoMo callback handler - Subscription components: new checkout-modal with payment method selection (VNPay, MoMo, ZaloPay) - API modules: type-safe PII encryption, improved error handling in MFA/auth/payments/analytics/search/notifications modules - Audit docs: comprehensive Wave 13 platform assessment, pricing audit, production readiness checklist - Updated PROJECT_TRACKER with Wave 13 status Co-Authored-By: Paperclip <noreply@paperclip.ing>
128 lines
4.4 KiB
TypeScript
128 lines
4.4 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Put,
|
|
Patch,
|
|
Body,
|
|
Param,
|
|
Query,
|
|
UseGuards,
|
|
Inject,
|
|
} from '@nestjs/common';
|
|
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, type JwtPayload } from '@modules/auth';
|
|
import {
|
|
NOTIFICATION_REPOSITORY,
|
|
type INotificationRepository,
|
|
NOTIFICATION_PREFERENCE_REPOSITORY,
|
|
type INotificationPreferenceRepository,
|
|
} from '../../domain';
|
|
import { TemplateService } from '../../infrastructure/services/template.service';
|
|
|
|
class UpdatePreferenceDto {
|
|
@ApiProperty({ enum: PrismaChannel, description: 'Notification channel' })
|
|
@IsEnum(PrismaChannel)
|
|
channel!: PrismaChannel;
|
|
|
|
@ApiProperty({ description: 'Event type identifier' })
|
|
@IsString()
|
|
eventType!: string;
|
|
|
|
@ApiProperty({ description: 'Whether the preference is enabled' })
|
|
@IsBoolean()
|
|
enabled!: boolean;
|
|
}
|
|
|
|
@ApiTags('notifications')
|
|
@ApiBearerAuth('JWT')
|
|
@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')
|
|
@ApiOperation({ summary: 'Get notification history' })
|
|
@ApiResponse({ status: 200, description: 'Notification history retrieved' })
|
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
@ApiQuery({ name: 'limit', required: false, type: Number })
|
|
async getHistory(
|
|
@CurrentUser() user: JwtPayload,
|
|
@Query('limit') limit?: number,
|
|
) {
|
|
return this.notificationRepo.findByUserId(user.sub, limit ?? 50);
|
|
}
|
|
|
|
@Get('preferences')
|
|
@ApiOperation({ summary: 'Get notification preferences' })
|
|
@ApiResponse({ status: 200, description: 'Preferences retrieved' })
|
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
async getPreferences(@CurrentUser() user: JwtPayload) {
|
|
return this.preferenceRepo.findByUserId(user.sub);
|
|
}
|
|
|
|
@Put('preferences')
|
|
@ApiOperation({ summary: 'Update a notification preference' })
|
|
@ApiResponse({ status: 200, description: 'Preference updated' })
|
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
async updatePreference(
|
|
@CurrentUser() user: JwtPayload,
|
|
@Body() dto: UpdatePreferenceDto,
|
|
) {
|
|
return this.preferenceRepo.upsert(user.sub, dto.channel, dto.eventType, dto.enabled);
|
|
}
|
|
|
|
@Get('unread')
|
|
@ApiOperation({ summary: 'Get unread notifications' })
|
|
@ApiResponse({ status: 200, description: 'Unread notifications retrieved' })
|
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
@ApiQuery({ name: 'limit', required: false, type: Number })
|
|
async getUnread(
|
|
@CurrentUser() user: JwtPayload,
|
|
@Query('limit') limit?: number,
|
|
) {
|
|
const [notifications, count] = await Promise.all([
|
|
this.notificationRepo.findUnreadByUserId(user.sub, limit ?? 50),
|
|
this.notificationRepo.countUnreadByUserId(user.sub),
|
|
]);
|
|
return { notifications, unreadCount: count };
|
|
}
|
|
|
|
@Patch(':id/read')
|
|
@ApiOperation({ summary: 'Mark a notification as read' })
|
|
@ApiResponse({ status: 200, description: 'Notification marked as read' })
|
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
async markAsRead(
|
|
@CurrentUser() user: JwtPayload,
|
|
@Param('id') id: string,
|
|
) {
|
|
await this.notificationRepo.markAsRead(id, user.sub);
|
|
return { success: true };
|
|
}
|
|
|
|
@Patch('read-all')
|
|
@ApiOperation({ summary: 'Mark all notifications as read' })
|
|
@ApiResponse({ status: 200, description: 'All notifications marked as read' })
|
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
async markAllAsRead(@CurrentUser() user: JwtPayload) {
|
|
const count = await this.notificationRepo.markAllAsRead(user.sub);
|
|
return { markedCount: count };
|
|
}
|
|
|
|
@Get('templates')
|
|
@ApiOperation({ summary: 'Get available notification templates' })
|
|
@ApiResponse({ status: 200, description: 'Templates retrieved' })
|
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
|
async getTemplates() {
|
|
return { templates: this.templateService.getTemplateKeys() };
|
|
}
|
|
}
|