Skip to content
1 change: 1 addition & 0 deletions src/notifications/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { NotificationsController } from './notifications.controller';
88 changes: 88 additions & 0 deletions src/notifications/controllers/notifications.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
Controller,
Get,
Put,
Delete,
Param,
Body,
Query,
UseGuards,
HttpStatus,
HttpCode,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/jwt-auth.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { NotificationsService } from '../services/notifications.service';
import { NotificationPreferencesService } from '../services/notification-preferences.service';
import { ListNotificationsDto, UpdatePreferencesDto } from '../dto';
import { Notification } from '../entities/notification.entity';

@Controller('notifications')
@UseGuards(JwtAuthGuard)
export class NotificationsController {
constructor(
private readonly notificationsService: NotificationsService,
private readonly preferencesService: NotificationPreferencesService,
) {}

@Get()
async listNotifications(
@CurrentUser('id') userId: string,
@Query() filters: ListNotificationsDto,
) {
return this.notificationsService.listNotifications(userId, filters);
}

@Get('unread')
async getUnreadCount(@CurrentUser('id') userId: string) {
const count = await this.notificationsService.getUnreadCount(userId);
return { unreadCount: count };
}

@Get('history')
async getDeliveryHistory(
@CurrentUser('id') userId: string,
@Query() filters: ListNotificationsDto,
) {
return this.notificationsService.getDeliveryHistory(userId, filters);
}

@Put(':id/read')
@HttpCode(HttpStatus.OK)
async markAsRead(
@CurrentUser('id') userId: string,
@Param('id') notificationId: string,
) {
await this.notificationsService.markAsRead(userId, notificationId);
return { success: true };
}

@Put('mark-all-read')
@HttpCode(HttpStatus.OK)
async markAllAsRead(@CurrentUser('id') userId: string) {
await this.notificationsService.markAllAsRead(userId);
return { success: true };
}

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async deleteNotification(
@CurrentUser('id') userId: string,
@Param('id') notificationId: string,
) {
await this.notificationsService.deleteNotification(userId, notificationId);
}

@Get('preferences')
async getPreferences(@CurrentUser('id') userId: string) {
return this.preferencesService.getUserPreferences(userId);
}

@Put('preferences')
async updatePreferences(
@CurrentUser('id') userId: string,
@Body() preferences: UpdatePreferencesDto,
) {
return this.preferencesService.updateUserPreferences(userId, preferences);
}
}
2 changes: 2 additions & 0 deletions src/notifications/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ListNotificationsDto } from './list-notifications.dto';
export { UpdatePreferencesDto } from './update-preferences.dto';
39 changes: 39 additions & 0 deletions src/notifications/dto/list-notifications.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IsOptional, IsString, IsBoolean, IsInt, Min, Max, IsEnum } from 'class-validator';
import { Type } from 'class-transformer';
import { NotificationCategory, NotificationPriority } from '../interfaces/notification.types';

export class ListNotificationsDto {
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
page?: number = 1;

@IsOptional()
@IsInt()
@Min(1)
@Max(100)
@Type(() => Number)
limit?: number = 20;

@IsOptional()
@IsBoolean()
@Type(() => Boolean)
unreadOnly?: boolean;

@IsOptional()
@IsEnum(NotificationCategory)
category?: NotificationCategory;

@IsOptional()
@IsEnum(NotificationPriority)
priority?: NotificationPriority;

@IsOptional()
@IsString()
fromDate?: string;

@IsOptional()
@IsString()
toDate?: string;
}
48 changes: 48 additions & 0 deletions src/notifications/dto/update-preferences.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IsOptional, IsBoolean, IsArray, IsEnum, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { DeliveryChannel, NotificationCategory } from '../interfaces/notification.types';

export class EmailPreferencesDto {
@IsOptional()
@IsBoolean()
digestEnabled?: boolean;

@IsOptional()
@IsEnum(['daily', 'weekly', 'never'])
digestFrequency?: 'daily' | 'weekly' | 'never';

@IsOptional()
@IsString()
emailAddress?: string;
}

export class UpdatePreferencesDto {
@IsOptional()
@IsArray()
@IsEnum(DeliveryChannel, { each: true })
enabledChannels?: DeliveryChannel[];

@IsOptional()
@ValidateNested()
@Type(() => EmailPreferencesDto)
emailPreferences?: EmailPreferencesDto;

@IsOptional()
@IsBoolean()
governanceAlerts?: boolean;

@IsOptional()
@IsBoolean()
stakingAlerts?: boolean;

@IsOptional()
@IsBoolean()
rewardNotifications?: boolean;

@IsOptional()
@IsBoolean()
securityAlerts?: boolean;

@IsOptional()
categorySettings?: Record<NotificationCategory, boolean>;
}
46 changes: 46 additions & 0 deletions src/notifications/entities/delivery-history.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn } from 'typeorm';
import { Notification } from './notification.entity';
import { DeliveryChannel, DeliveryStatus } from '../interfaces/notification.types';

@Entity('delivery_history')
export class DeliveryHistory {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
notificationId: string;

@ManyToOne(() => Notification, { onDelete: 'CASCADE' })
notification: Notification;

@Column({
type: 'varchar',
length: 20,
})
channel: DeliveryChannel;

@Column({
type: 'varchar',
length: 20,
default: 'pending',
})
status: DeliveryStatus;

@Column({ type: 'int', default: 0 })
retryAttempts: number;

@Column({ nullable: true })
lastRetryAt: Date;

@Column({ nullable: true })
deliveredAt: Date;

@Column({ nullable: true })
failureReason: string;

@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;

@CreateDateColumn()
createdAt: Date;
}
3 changes: 3 additions & 0 deletions src/notifications/entities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Notification } from './notification.entity';
export { NotificationPreference } from './notification-preference.entity';
export { DeliveryHistory } from './delivery-history.entity';
25 changes: 25 additions & 0 deletions src/notifications/entities/notification-preference.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';
import { User } from '../../entities/user.entity';
import { UserPreferenceSettings } from '../interfaces/notification.types';

@Entity('notification_preferences')
export class NotificationPreference {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
userId: string;

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

@Column({ type: 'jsonb' })
settings: UserPreferenceSettings;

@Column({ default: () => 'now()' })
createdAt: Date;

@Column({ default: () => 'now()' })
updatedAt: Date;
}
41 changes: 41 additions & 0 deletions src/notifications/entities/notification.entity.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { User } from '../../entities/user.entity';
import { NotificationCategory, NotificationPriority } from '../interfaces/notification.types';

@Entity('notifications')
import {
Entity,
PrimaryGeneratedColumn,
Expand Down Expand Up @@ -30,6 +35,41 @@ export class Notification {
userId: string;

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

@Column()
title: string;

@Column('text')
message: string;

@Column({
type: 'varchar',
length: 50,
})
category: NotificationCategory;

@Column({
type: 'varchar',
length: 20,
default: 'medium',
})
priority: NotificationPriority;

@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;

@Column({ type: 'jsonb', nullable: true })
sourceEvent: Record<string, any>;

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

@Column({ nullable: true })
readAt: Date;

@Column({ nullable: true })
scheduledFor: Date;
@JoinColumn({ name: 'userId' })
user: User;

Expand Down Expand Up @@ -75,3 +115,4 @@ export class Notification {
@UpdateDateColumn()
updatedAt: Date;
}
}
Loading
Loading