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
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CurrencyHubModule } from './currency-hub/currency-hub.module';
import { LessonQuizResultModule } from './lesson-quiz-result/lesson-quiz-result.module';
import { ProgressModule } from './progress/progress.module';
import { RecommendationModule } from './recommendation/recommendation.module';
import { NotificationPreferenceModule } from './notification-preference/notification-preference.module';

@Module({
imports: [
Expand Down Expand Up @@ -47,6 +48,7 @@ import { RecommendationModule } from './recommendation/recommendation.module';
LessonQuizResultModule,
ProgressModule,
RecommendationModule,
NotificationPreferenceModule,
],
controllers: [AppController],
providers: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Test, TestingModule } from "@nestjs/testing"
import { NotificationPreferenceController } from "./notification-preference.controller"
import { NotificationPreferenceService } from "../providers/notification-preference.service"
import { UserRole } from "src/roles/roles.enum"
import { UpdateNotificationPreferenceDto } from "../dto/update-notification-preference.dto"

describe("NotificationPreferenceController", () => {
let controller: NotificationPreferenceController
let service: NotificationPreferenceService

const mockUser = {
id: "user-id",
role: UserRole.STUDENT,
}

const mockPreference = {
id: "pref-id",
userId: "user-id",
role: UserRole.STUDENT,
lessonUpdates: true,
viaEmail: true,
viaInApp: true,
}

const mockNotificationPreferenceService = {
findByUser: jest.fn().mockResolvedValue(mockPreference),
update: jest.fn().mockResolvedValue(mockPreference),
resetToDefaults: jest.fn().mockResolvedValue(mockPreference),
remove: jest.fn().mockResolvedValue(undefined),
}

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [NotificationPreferenceController],
providers: [
{
provide: NotificationPreferenceService,
useValue: mockNotificationPreferenceService,
},
],
}).compile()

controller = module.get<NotificationPreferenceController>(NotificationPreferenceController)
service = module.get<NotificationPreferenceService>(NotificationPreferenceService)
})

it("should be defined", () => {
expect(controller).toBeDefined()
})

describe("getPreferences", () => {
it("should return user preferences", async () => {
const req = { user: mockUser } as any

const result = await controller.getPreferences(req)

expect(service.findByUser).toHaveBeenCalledWith(mockUser.id, mockUser.role)
expect(result).toEqual(mockPreference)
})
})

describe("updatePreferences", () => {
it("should update user preferences", async () => {
const req = { user: mockUser } as any
const updateDto: UpdateNotificationPreferenceDto = {
lessonUpdates: false,
viaEmail: false,
}

const result = await controller.updatePreferences(req, updateDto)

expect(service.update).toHaveBeenCalledWith(mockUser.id, mockUser.role, updateDto)
expect(result).toEqual(mockPreference)
})
})

describe("resetPreferences", () => {
it("should reset preferences to defaults", async () => {
const req = { user: mockUser } as any

const result = await controller.resetPreferences(req)

expect(service.resetToDefaults).toHaveBeenCalledWith(mockUser.id, mockUser.role)
expect(result).toEqual(mockPreference)
})
})

describe("deletePreferences", () => {
it("should delete user preferences", async () => {
const req = { user: mockUser } as any

await controller.deletePreferences(req)

expect(service.remove).toHaveBeenCalledWith(mockUser.id, mockUser.role)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Controller, Get, Put, Body, UseGuards, Request, Post, Delete, HttpCode, HttpStatus } from "@nestjs/common"
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from "@nestjs/swagger"
import { UserRole } from "src/roles/roles.enum"
import { RolesGuard } from "src/roles/roles.guard"
import { NotificationPreferenceService } from "../providers/notification-preference.service"
import { UpdateNotificationPreferenceDto } from "../dto/update-notification-preference.dto"

interface RequestUser {
id: string
role: UserRole
}

interface RequestWithUser extends Request {
user: RequestUser
}

@ApiTags("notification-preferences")
@Controller("notification-preferences")
@UseGuards(RolesGuard)
@ApiBearerAuth()
export class NotificationPreferenceController {
constructor(private readonly notificationPreferenceService: NotificationPreferenceService) {}

@Get()
@ApiOperation({ summary: 'Get current user notification preferences' })
@ApiResponse({
status: 200,
description: 'User notification preferences retrieved successfully',
})
async getPreferences(@Request() req: any) {
return await this.notificationPreferenceService.findByUser(
req.user.id,
req.user.role,
);
}

@Put()
@ApiOperation({ summary: "Update current user notification preferences" })
@ApiResponse({
status: 200,
description: "Notification preferences updated successfully",
})
@ApiResponse({ status: 400, description: "Invalid input data" })
async updatePreferences(@Request() req: any, @Body() updateDto: UpdateNotificationPreferenceDto) {
return await this.notificationPreferenceService.update(req.user.id, req.user.role, updateDto)
}

@Post('reset')
@ApiOperation({ summary: 'Reset notification preferences to defaults' })
@ApiResponse({
status: 200,
description: 'Notification preferences reset to defaults',
})
async resetPreferences(@Request() req: any) {
return await this.notificationPreferenceService.resetToDefaults(
req.user.id,
req.user.role,
);
}

@Delete()
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete current user notification preferences' })
@ApiResponse({
status: 204,
description: 'Notification preferences deleted successfully',
})
@ApiResponse({ status: 404, description: 'Preferences not found' })
async deletePreferences(@Request() req: any) {
await this.notificationPreferenceService.remove(
req.user.id,
req.user.role,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { IsBoolean, IsEnum, IsOptional, IsString, IsUUID, Matches } from "class-validator"
import { UserRole } from "../../roles/roles.enum"

export class CreateNotificationPreferenceDto {
@IsUUID()
userId: string

@IsEnum(UserRole)
role: UserRole

// Notification Categories
@IsOptional()
@IsBoolean()
courseEnrollment?: boolean

@IsOptional()
@IsBoolean()
courseCompletion?: boolean

@IsOptional()
@IsBoolean()
lessonUpdates?: boolean

@IsOptional()
@IsBoolean()
lessonCompletion?: boolean

@IsOptional()
@IsBoolean()
quizResults?: boolean

@IsOptional()
@IsBoolean()
quizReminders?: boolean

@IsOptional()
@IsBoolean()
daoUpdates?: boolean

@IsOptional()
@IsBoolean()
daoProposals?: boolean

@IsOptional()
@IsBoolean()
daoVoting?: boolean

@IsOptional()
@IsBoolean()
systemAnnouncements?: boolean

@IsOptional()
@IsBoolean()
maintenance?: boolean

@IsOptional()
@IsBoolean()
profileUpdates?: boolean

@IsOptional()
@IsBoolean()
passwordChanges?: boolean

// Delivery Channels
@IsOptional()
@IsBoolean()
viaEmail?: boolean

@IsOptional()
@IsBoolean()
viaInApp?: boolean

@IsOptional()
@IsBoolean()
viaSms?: boolean

@IsOptional()
@IsBoolean()
viaPush?: boolean

// Timing Preferences
@IsOptional()
@IsBoolean()
instantDelivery?: boolean

@IsOptional()
@IsBoolean()
dailyDigest?: boolean

@IsOptional()
@IsBoolean()
weeklyDigest?: boolean

@IsOptional()
@IsString()
@Matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, {
message: "quietHoursStart must be in HH:MM format",
})
quietHoursStart?: string

@IsOptional()
@IsString()
@Matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, {
message: "quietHoursEnd must be in HH:MM format",
})
quietHoursEnd?: string

@IsOptional()
@IsString()
timezone?: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IsOptional, IsEnum } from "class-validator"
import { UserRole } from "../../roles/roles.enum"

export class QueryNotificationPreferenceDto {
@IsOptional()
@IsEnum(UserRole)
role?: UserRole
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { PartialType, OmitType } from "@nestjs/mapped-types"
import { CreateNotificationPreferenceDto } from "./create-notification-preference.dto"

export class UpdateNotificationPreferenceDto extends PartialType(
OmitType(CreateNotificationPreferenceDto, ["userId", "role"] as const),
) {}
Loading
Loading