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 src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { FeedbackModule } from './feedback/feedback.module';
import { AdviceModule } from './advice/advice.module';
import { ChatModule } from './chat/chat.module';
import { CategoriesModule } from './categories/categories.module';
import { NotificationsModule } from './notifications/notifications.module';

@Module({
imports: [
Expand Down Expand Up @@ -45,6 +46,7 @@ import { CategoriesModule } from './categories/categories.module';
FeedbackModule,
AdviceModule,
ChatModule,
NotificationsModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
80 changes: 80 additions & 0 deletions src/notifications/notifications.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
Controller,
Get,
Post,
Delete,
Param,
UseGuards,
NotFoundException,
} from '@nestjs/common';
import { NotificationService } from './notifications.service';
import { BlacklistedJwtAuthGuard } from '../auth/guards/blacklisted-jwt-auth.guard';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import { Notification } from './schemas/notification.schema';

@Controller('notifications')
@UseGuards(BlacklistedJwtAuthGuard)
export class NotificationsController {
constructor(private readonly notificationService: NotificationService) {}

@Get()
async getUserNotifications(
@CurrentUser() user: any,
): Promise<Notification[]> {
return this.notificationService.getUserNotifications(user.user_id);
}

@Get('unread')
async getUnreadNotifications(
@CurrentUser() user: any,
): Promise<Notification[]> {
return this.notificationService.getUserUnreadNotifications(user.user_id);
}

@Post(':id/read')
async markAsRead(
@Param('id') id: string,
@CurrentUser() user: any,
): Promise<Notification> {
try {
const notification =
await this.notificationService.markNotificationAsRead(id);

if (notification.recipient.toString() !== user.user_id) {
throw new NotFoundException('Notification not found');
}

return notification;
} catch (error) {
throw new NotFoundException('Notification not found');
}
}

@Post('read-all')
async markAllAsRead(@CurrentUser() user: any): Promise<{ message: string }> {
await this.notificationService.markAllNotificationsAsRead(user.user_id);
return { message: 'All notifications marked as read' };
}

@Delete(':id')
async deleteNotification(
@Param('id') id: string,
@CurrentUser() user: any,
): Promise<{ message: string }> {
try {
const notifications = await this.notificationService.getUserNotifications(
user.user_id,
);
const notification = notifications.find((n) => n._id.toString() === id);

if (!notification) {
throw new NotFoundException('Notification not found');
}

await this.notificationService.deleteNotification(id);
return { message: 'Notification deleted successfully' };
} catch (error) {
throw new NotFoundException('Notification not found');
}
}
}
20 changes: 20 additions & 0 deletions src/notifications/notifications.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { NotificationService } from './notifications.service';
import { NotificationsController } from './notifications.controller';
import {
Notification,
NotificationSchema,
} from './schemas/notification.schema';

@Module({
imports: [
MongooseModule.forFeature([
{ name: Notification.name, schema: NotificationSchema },
]),
],
controllers: [NotificationsController],
providers: [NotificationService],
exports: [NotificationService],
})
export class NotificationsModule {}
152 changes: 152 additions & 0 deletions src/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { Notification } from './schemas/notification.schema';

@Injectable()
export class NotificationService {
private readonly logger = new Logger(NotificationService.name);

constructor(
@InjectModel(Notification.name)
private notificationModel: Model<Notification>,
) {}

async createNotification(notificationData: {
title: string;
message: string;
recipient: string | Types.ObjectId;
type: 'booking' | 'status_change' | 'feedback' | 'system';
booking_id?: string | Types.ObjectId;
service_id?: string | Types.ObjectId;
}): Promise<Notification> {
try {
const recipientId =
typeof notificationData.recipient === 'string'
? new Types.ObjectId(notificationData.recipient)
: notificationData.recipient;

const bookingId = notificationData.booking_id
? typeof notificationData.booking_id === 'string'
? new Types.ObjectId(notificationData.booking_id)
: notificationData.booking_id
: undefined;

const serviceId = notificationData.service_id
? typeof notificationData.service_id === 'string'
? new Types.ObjectId(notificationData.service_id)
: notificationData.service_id
: undefined;

const notification = new this.notificationModel({
title: notificationData.title,
message: notificationData.message,
recipient: recipientId,
type: notificationData.type,
booking_id: bookingId,
service_id: serviceId,
read: false,
});

const savedNotification = await notification.save();
this.logger.log(
`Created notification ${savedNotification._id} for user ${recipientId}`,
);
return savedNotification;
} catch (error) {
this.logger.error(
`Failed to create notification: ${error.message}`,
error.stack,
);
throw error;
}
}

async getUserNotifications(userId: string): Promise<Notification[]> {
try {
const recipientId = new Types.ObjectId(userId);
this.logger.log(`Fetching notifications for user: ${userId}`);

return this.notificationModel
.find({ recipient: recipientId })
.sort({ createdAt: -1 })
.exec();
} catch (error) {
this.logger.error(
`Failed to get notifications for user ${userId}: ${error.message}`,
error.stack,
);
throw error;
}
}

async getUserUnreadNotifications(userId: string): Promise<Notification[]> {
try {
const recipientId = new Types.ObjectId(userId);

return this.notificationModel
.find({ recipient: recipientId, read: false })
.sort({ createdAt: -1 })
.exec();
} catch (error) {
this.logger.error(
`Failed to get unread notifications for user ${userId}: ${error.message}`,
error.stack,
);
throw error;
}
}

async markNotificationAsRead(notificationId: string): Promise<Notification> {
try {
const notification = await this.notificationModel
.findByIdAndUpdate(notificationId, { read: true }, { new: true })
.exec();

if (!notification) {
throw new Error(`Notification with ID ${notificationId} not found`);
}

return notification;
} catch (error) {
this.logger.error(
`Failed to mark notification ${notificationId} as read: ${error.message}`,
error.stack,
);
throw error;
}
}

async markAllNotificationsAsRead(userId: string): Promise<void> {
try {
const recipientId = new Types.ObjectId(userId);

await this.notificationModel
.updateMany({ recipient: recipientId, read: false }, { read: true })
.exec();
} catch (error) {
this.logger.error(
`Failed to mark all notifications as read for user ${userId}: ${error.message}`,
error.stack,
);
throw error;
}
}

async deleteNotification(notificationId: string): Promise<void> {
try {
const result = await this.notificationModel
.findByIdAndDelete(notificationId)
.exec();
if (!result) {
throw new Error(`Notification with ID ${notificationId} not found`);
}
} catch (error) {
this.logger.error(
`Failed to delete notification ${notificationId}: ${error.message}`,
error.stack,
);
throw error;
}
}
}
32 changes: 32 additions & 0 deletions src/notifications/schemas/notification.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';

@Schema({ timestamps: true })
export class Notification extends Document {
@Prop({ required: true })
title: string;

@Prop({ required: true })
message: string;

@Prop({ type: Types.ObjectId, ref: 'User', required: true, index: true })
recipient: Types.ObjectId;

@Prop({
type: String,
enum: ['booking', 'status_change', 'feedback', 'system'],
default: 'system',
})
type: string;

@Prop({ type: Types.ObjectId, ref: 'ServiceBookings', index: true })
booking_id?: Types.ObjectId;

@Prop({ type: Types.ObjectId, ref: 'Service', index: true })
service_id?: Types.ObjectId;

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

export const NotificationSchema = SchemaFactory.createForClass(Notification);
4 changes: 3 additions & 1 deletion src/service-bookings/service-bookings.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ServicesModule } from '../services/services.module';
import { BookingsController } from './service-bookings.controller';
import { BookingsService } from './service-bookings.service';
import { UserModule } from 'src/users/user.module';
import { NotificationsModule } from '../notifications/notifications.module';

@Module({
imports: [
Expand All @@ -19,7 +20,8 @@ import { UserModule } from 'src/users/user.module';
]),
AuthModule,
forwardRef(() => ServicesModule),
forwardRef(() => UserModule)
forwardRef(() => UserModule),
NotificationsModule,
],
controllers: [BookingsController],
providers: [BookingsService],
Expand Down
Loading
Loading