From 8170d1359fe7de82a435bea7e770f0cafa378821 Mon Sep 17 00:00:00 2001 From: Hussein Saad Date: Sun, 20 Apr 2025 18:10:43 +0200 Subject: [PATCH] Revert "Revert "Feat/notifcation"" --- src/app.module.ts | 2 + src/notifications/notifications.controller.ts | 80 +++++++++ src/notifications/notifications.module.ts | 20 +++ src/notifications/notifications.service.ts | 152 ++++++++++++++++++ .../schemas/notification.schema.ts | 32 ++++ .../service-bookings.module.ts | 4 +- .../service-bookings.service.ts | 66 +++++++- 7 files changed, 352 insertions(+), 4 deletions(-) create mode 100644 src/notifications/notifications.controller.ts create mode 100644 src/notifications/notifications.module.ts create mode 100644 src/notifications/notifications.service.ts create mode 100644 src/notifications/schemas/notification.schema.ts diff --git a/src/app.module.ts b/src/app.module.ts index 544c7bf..816948c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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: [ @@ -45,6 +46,7 @@ import { CategoriesModule } from './categories/categories.module'; FeedbackModule, AdviceModule, ChatModule, + NotificationsModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/notifications/notifications.controller.ts b/src/notifications/notifications.controller.ts new file mode 100644 index 0000000..13ebd51 --- /dev/null +++ b/src/notifications/notifications.controller.ts @@ -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 { + return this.notificationService.getUserNotifications(user.user_id); + } + + @Get('unread') + async getUnreadNotifications( + @CurrentUser() user: any, + ): Promise { + return this.notificationService.getUserUnreadNotifications(user.user_id); + } + + @Post(':id/read') + async markAsRead( + @Param('id') id: string, + @CurrentUser() user: any, + ): Promise { + 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'); + } + } +} diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts new file mode 100644 index 0000000..70ef09f --- /dev/null +++ b/src/notifications/notifications.module.ts @@ -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 {} diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts new file mode 100644 index 0000000..dc3a9e8 --- /dev/null +++ b/src/notifications/notifications.service.ts @@ -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, + ) {} + + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } + } +} diff --git a/src/notifications/schemas/notification.schema.ts b/src/notifications/schemas/notification.schema.ts new file mode 100644 index 0000000..79182cc --- /dev/null +++ b/src/notifications/schemas/notification.schema.ts @@ -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); diff --git a/src/service-bookings/service-bookings.module.ts b/src/service-bookings/service-bookings.module.ts index 2496a84..c46d120 100644 --- a/src/service-bookings/service-bookings.module.ts +++ b/src/service-bookings/service-bookings.module.ts @@ -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: [ @@ -19,7 +20,8 @@ import { UserModule } from 'src/users/user.module'; ]), AuthModule, forwardRef(() => ServicesModule), - forwardRef(() => UserModule) + forwardRef(() => UserModule), + NotificationsModule, ], controllers: [BookingsController], providers: [BookingsService], diff --git a/src/service-bookings/service-bookings.service.ts b/src/service-bookings/service-bookings.service.ts index ff8aac5..3b411bf 100644 --- a/src/service-bookings/service-bookings.service.ts +++ b/src/service-bookings/service-bookings.service.ts @@ -9,6 +9,8 @@ import { Model, Types } from 'mongoose'; import { ServiceBookings } from './schemas/service-booking.schema'; import { ServicesService } from '../services/services.service'; import { UsersService } from './../users/users.service'; +import { NotificationService } from '../notifications/notifications.service'; + @Injectable() export class BookingsService { constructor( @@ -16,6 +18,7 @@ export class BookingsService { private bookingModel: Model, private readonly servicesService: ServicesService, private readonly UsersService: UsersService, + private readonly notificationService: NotificationService, ) {} async createBooking( @@ -35,13 +38,17 @@ export class BookingsService { throw new NotFoundException('Service not found'); } - const provider = await this.UsersService.findById( - service.service_provider?._id.toString(), - ); + const providerId = service.service_provider?._id.toString(); + const provider = await this.UsersService.findById(providerId); if (!provider) { throw new NotFoundException('Service provider not available'); } + const customer = await this.UsersService.findById(customerId); + if (!customer) { + throw new NotFoundException('Customer not found'); + } + const booking = new this.bookingModel({ customer: customerId, service: serviceId, @@ -49,6 +56,16 @@ export class BookingsService { }); const savedBooking = await booking.save(); + + await this.notificationService.createNotification({ + title: 'New Service Booking', + message: `${customer.full_name} has booked your service "${service.service_name}".`, + recipient: providerId, + type: 'booking', + booking_id: savedBooking._id, + service_id: serviceId, + }); + return savedBooking; } catch (error) { if ( @@ -121,9 +138,52 @@ export class BookingsService { ); } + if (booking.status === status) { + return booking; + } + booking.status = status; const updatedBooking = await booking.save(); + const service = await this.servicesService.getServiceById( + booking.service.toString(), + ); + const customer = await this.UsersService.findById( + booking.customer.toString(), + ); + const providerId = service.service_provider?._id.toString(); + const customerMessages = { + pending: `Your booking for "${service.service_name}" is pending confirmation.`, + confirmed: `Your booking for "${service.service_name}" has been confirmed.`, + completed: `Your booking for "${service.service_name}" has been marked as completed.`, + cancelled: `Your booking for "${service.service_name}" has been cancelled.`, + }; + + const providerMessages = { + pending: `Booking for "${service.service_name}" by ${customer.full_name} is pending.`, + confirmed: `You've confirmed the booking for "${service.service_name}" by ${customer.full_name}.`, + completed: `You've marked the booking for "${service.service_name}" by ${customer.full_name} as completed.`, + cancelled: `The booking for "${service.service_name}" by ${customer.full_name} has been cancelled.`, + }; + + await this.notificationService.createNotification({ + title: `Booking Status Update: ${status}`, + message: customerMessages[status], + recipient: booking.customer.toString(), + type: 'status_change', + booking_id: bookingId, + service_id: booking.service.toString(), + }); + + await this.notificationService.createNotification({ + title: `Booking Status Update: ${status}`, + message: providerMessages[status], + recipient: providerId, + type: 'status_change', + booking_id: bookingId, + service_id: booking.service.toString(), + }); + return updatedBooking; } catch (error) { if (