Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions drips/notifications/controllers/notification.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
Controller,
Get,
Patch,
Param,
Query,
// UseGuards,
Request,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';
import { NotificationService } from '../services/notification.service';
import {
ListNotificationsQueryDto,
PaginatedNotificationsResponseDto,
NotificationResponseDto,
} from '../dto/notification.dto';
// import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';

@ApiTags('Notifications')
@Controller('notifications')
// @UseGuards(JwtAuthGuard)
@ApiBearerAuth()
export class NotificationController {
constructor(private readonly notificationService: NotificationService) {}

@Get()
@ApiOperation({
summary: 'List user notifications',
description:
'Retrieve paginated list of notifications for the authenticated user with optional filtering',
})
@ApiResponse({
status: 200,
description: 'Notifications retrieved successfully',
type: PaginatedNotificationsResponseDto,
})
@ApiQuery({ name: 'read', required: false, type: Boolean })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({
name: 'type',
required: false,
enum: ['booking_accepted', 'session_upcoming', 'payment_released'],
})
async list(
@Request() req,
@Query() query: ListNotificationsQueryDto,
): Promise<PaginatedNotificationsResponseDto> {
return this.notificationService.list(req.user.id, query);
}

@Get('unread-count')
@ApiOperation({
summary: 'Get unread notification count',
description:
'Get the count of unread notifications for the authenticated user',
})
@ApiResponse({
status: 200,
description: 'Unread count retrieved successfully',
schema: {
type: 'object',
properties: {
count: { type: 'number', example: 5 },
},
},
})
async getUnreadCount(@Request() req): Promise<{ count: number }> {
const count = await this.notificationService.getUnreadCount(req.user.id);
return { count };
}

@Patch(':id/read')
@ApiOperation({
summary: 'Mark notification as read',
description:
'Mark a single notification as read. Only the owner can mark their notifications.',
})
@ApiParam({ name: 'id', description: 'Notification ID' })
@ApiResponse({
status: 200,
description: 'Notification marked as read',
type: NotificationResponseDto,
})
@ApiResponse({ status: 403, description: 'Forbidden - not the owner' })
@ApiResponse({ status: 404, description: 'Notification not found' })
async markAsRead(
@Request() req,
@Param('id') id: string,
): Promise<NotificationResponseDto> {
return this.notificationService.markAsRead(id, req.user.id);
}

@Patch('read-all')
@ApiOperation({
summary: 'Mark all notifications as read',
description:
'Mark all unread notifications for the authenticated user as read',
})
@ApiResponse({
status: 200,
description: 'All notifications marked as read',
schema: {
type: 'object',
properties: {
updated: { type: 'number', example: 12 },
},
},
})
async markAllAsRead(@Request() req): Promise<{ updated: number }> {
return this.notificationService.markAllAsRead(req.user.id);
}
}
103 changes: 103 additions & 0 deletions drips/notifications/dto/notification.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsEnum,
IsOptional,
IsBoolean,
IsInt,
Min,
Max,
} from 'class-validator';
import { Type } from 'class-transformer';
import { NotificationType } from '../entities/notification.entity';

export class NotificationResponseDto {
@ApiProperty({ example: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' })
id: string;

@ApiProperty({ example: 'f47ac10b-58cc-4372-a567-0e02b2c3d480' })
userId: string;

@ApiProperty({
enum: NotificationType,
example: NotificationType.BOOKING_ACCEPTED,
})
type: NotificationType;

@ApiProperty({
type: 'object',
example: {
bookingId: 'booking-123',
mentorName: 'Sarah Johnson',
sessionDate: '2026-02-01T14:00:00Z',
},
})
payload: Record<string, any>;

@ApiProperty({ example: false })
isRead: boolean;

@ApiProperty({ example: '2026-01-26T10:30:00Z' })
createdAt: Date;

@ApiProperty({ example: '2026-01-26T10:30:00Z' })
updatedAt: Date;
}

export class ListNotificationsQueryDto {
@ApiPropertyOptional({ description: 'Filter by read status' })
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
read?: boolean;

@ApiPropertyOptional({ description: 'Page number', minimum: 1, default: 1 })
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
page?: number = 1;

@ApiPropertyOptional({
description: 'Items per page',
minimum: 1,
maximum: 100,
default: 10,
})
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
@Type(() => Number)
limit?: number = 10;

@ApiPropertyOptional({
enum: NotificationType,
description: 'Filter by notification type',
})
@IsOptional()
@IsEnum(NotificationType)
type?: NotificationType;
}

export class PaginatedNotificationsResponseDto {
@ApiProperty({ type: [NotificationResponseDto] })
items: NotificationResponseDto[];

@ApiProperty({ example: 1 })
page: number;

@ApiProperty({ example: 10 })
limit: number;

@ApiProperty({ example: 42 })
total: number;

@ApiProperty({ example: 5 })
totalPages: number;
}

export class CreateNotificationDto {
userId: string;
type: NotificationType;
payload: Record<string, any>;
}
50 changes: 50 additions & 0 deletions drips/notifications/entities/notification.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
import { User } from 'user/user.entity';

export enum NotificationType {
BOOKING_ACCEPTED = 'booking_accepted',
SESSION_UPCOMING = 'session_upcoming',
PAYMENT_RELEASED = 'payment_released',
}

@Entity('notifications')
@Index(['userId', 'createdAt'])
@Index(['userId', 'isRead'])
export class Notification {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column('uuid')
@Index()
userId: string;

@ManyToOne(() => User, { onDelete: 'CASCADE' })
user: User;

@Column({
type: 'enum',
enum: NotificationType,
})
@Index()
type: NotificationType;

@Column('jsonb')
payload: Record<string, any>;

@Column({ default: false })
isRead: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
11 changes: 8 additions & 3 deletions drips/notifications/notifications.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { EmailService } from './services/email.service';
import { TemplateService } from './services/template.service';
import { Notification } from './entities/notification.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NotificationController } from './controllers/notification.controller';
import { NotificationService } from './services/notification.service';

@Module({
imports: [ConfigModule],
providers: [EmailService, TemplateService],
exports: [EmailService],
imports: [TypeOrmModule.forFeature([Notification]), ConfigModule],
controllers: [NotificationController],
providers: [EmailService, TemplateService, NotificationService],
exports: [EmailService, NotificationService],
})
export class NotificationsModule {}
Loading
Loading