diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 079aec89..fe2cbb98 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -59,6 +59,41 @@ func GetAllData(ctx *gin.Context, appsession *models.AppSession) []bson.M { return users } +func CheckCoincidingBookings(ctx *gin.Context, appsession *models.AppSession, booking models.Booking) (bool, error) { + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } + + // Check if the booking exists in the database + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") + + filter := bson.M{ + "roomId": booking.RoomID, + "$and": []bson.M{ + {"start": bson.M{"$lt": booking.End}}, // Existing booking starts before new booking ends + {"end": bson.M{"$gt": booking.Start}}, // Existing booking ends after new booking starts + }, + } + + var existingbooking models.Booking + err := collection.FindOne(ctx, filter).Decode(&existingbooking) + if err != nil { + logrus.Error(err) + if err == mongo.ErrNoDocuments { + return false, nil // No conflict found + } + return false, err // Other errors + } + + logrus.Info("Existing booking: ", existingbooking) + logrus.Info("Room id's match: ", existingbooking.RoomID == booking.RoomID) + logrus.Info("Room id to check: ", booking.RoomID) + logrus.Info("Existing room id: ", existingbooking.RoomID) + + return true, nil +} + // attempts to save booking in database func SaveBooking(ctx *gin.Context, appsession *models.AppSession, booking models.Booking) (bool, error) { // check if database is nil diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index b96b62f5..f2f112a8 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -84,6 +84,19 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { return } + // check if no booking has been made that coincides with the start and end time of this booking + coinciding, err := database.CheckCoincidingBookings(ctx, appsession, booking) + if err != nil { + configs.CaptureError(ctx, err) + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to book", constants.InternalServerErrorCode, "Failed to book", nil)) + return + } + + if coinciding { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Booking coincides with another booking", constants.BadRequestCode, "Booking coincides with another booking", nil)) + return + } + // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() booking.OccupiID = utils.GenerateBookingID() diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index ad13efc0..ac68bbd6 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -1366,8 +1366,7 @@ func TestAddOTP(t *testing.T) { mock.ExpectGet(cache.OTPKey(email, otp)).SetVal(string(otpData)) // Verify the otp was added to the Cache - res := Cache.Get(context.Background(), cache.OTPKey(email, otp)) - _, err = res.Bytes() + _ = Cache.Get(context.Background(), cache.OTPKey(email, otp)) //assert.Nil(t, err) //assert.NotNil(t, otpv) @@ -11578,3 +11577,166 @@ func TestGetUsersLocations(t *testing.T) { assert.Nil(t, results, "Expected nil results") }) } + +// TestCheckCoincidingBookings contains all test cases with sub-tests +func TestCheckCoincidingBookings(t *testing.T) { + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + // Set gin run mode + gin.SetMode(configs.GetGinRunMode()) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + + // Test cases + mt.Run("Database Is Nil", func(mt *mtest.T) { + // Database is nil case + appsessionNil := &models.AppSession{DB: nil} + booking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 9, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 14, 0, 0, 0, time.UTC), + } + + result, err := database.CheckCoincidingBookings(ctx, appsessionNil, booking) + + // Assertions + assert.Error(t, err) + assert.Equal(t, "database is nil", err.Error()) + assert.False(t, result) + }) + + mt.Run("NoConflict", func(mt *mtest.T) { + // No overlapping booking in the database + booking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 13, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 14, 0, 0, 0, time.UTC), + } + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch)) + + appsession := &models.AppSession{DB: mt.Client} + + result, err := database.CheckCoincidingBookings(ctx, appsession, booking) + + assert.NoError(t, err) + assert.False(t, result) + }) + + mt.Run("WithConflict_FullyEnclosed", func(mt *mtest.T) { + // Test with an existing booking that fully encloses the new one (10am-12pm, new booking 9am-1pm) + booking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 9, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 13, 0, 0, 0, time.UTC), + } + existingBooking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 10, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 12, 0, 0, 0, time.UTC), + } + firstBatch := mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "roomId", Value: existingBooking.RoomID}, + {Key: "start", Value: existingBooking.Start}, + {Key: "end", Value: existingBooking.End}, + }) + mt.AddMockResponses(firstBatch) + + appsession := &models.AppSession{DB: mt.Client} + + result, err := database.CheckCoincidingBookings(ctx, appsession, booking) + + assert.NoError(t, err) + assert.True(t, result) + }) + + mt.Run("WithConflict_NewFullyEnclosesExisting", func(mt *mtest.T) { + // Test with a new booking that fully encloses an existing one (new 9am-2pm, existing 10am-12pm) + booking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 9, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 14, 0, 0, 0, time.UTC), + } + existingBooking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 10, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 12, 0, 0, 0, time.UTC), + } + firstBatch := mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "roomId", Value: existingBooking.RoomID}, + {Key: "start", Value: existingBooking.Start}, + {Key: "end", Value: existingBooking.End}, + }) + mt.AddMockResponses(firstBatch) + + appsession := &models.AppSession{DB: mt.Client} + + result, err := database.CheckCoincidingBookings(ctx, appsession, booking) + + assert.NoError(t, err) + assert.True(t, result) + }) + + mt.Run("PartialOverlap_StartInsideExistingEndAfter", func(mt *mtest.T) { + // Test with a booking that starts inside and ends after existing booking (existing 10am-12pm, new 11am-1pm) + booking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 11, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 13, 0, 0, 0, time.UTC), + } + existingBooking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 10, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 12, 0, 0, 0, time.UTC), + } + firstBatch := mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch, bson.D{ + {Key: "roomId", Value: existingBooking.RoomID}, + {Key: "start", Value: existingBooking.Start}, + {Key: "end", Value: existingBooking.End}, + }) + mt.AddMockResponses(firstBatch) + + appsession := &models.AppSession{DB: mt.Client} + + result, err := database.CheckCoincidingBookings(ctx, appsession, booking) + + assert.NoError(t, err) + assert.True(t, result) + }) + + mt.Run("NoConflict_NonOverlapping", func(mt *mtest.T) { + // No overlap (existing 8am-9am, new booking 9am-10am) + booking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 9, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 10, 0, 0, 0, time.UTC), + } + mt.AddMockResponses(mtest.CreateCursorResponse(0, configs.GetMongoDBName()+".RoomBooking", mtest.FirstBatch)) + + appsession := &models.AppSession{DB: mt.Client} + + result, err := database.CheckCoincidingBookings(ctx, appsession, booking) + + assert.NoError(t, err) + assert.False(t, result) + }) + + mt.Run("DatabaseError", func(mt *mtest.T) { + // Simulate database error + booking := models.Booking{ + RoomID: "Room1", + Start: time.Date(2024, 10, 20, 9, 0, 0, 0, time.UTC), + End: time.Date(2024, 10, 20, 14, 0, 0, 0, time.UTC), + } + + mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ + Code: 11000, + Message: "mock error", + })) + + appsession := &models.AppSession{DB: mt.Client} + + result, err := database.CheckCoincidingBookings(ctx, appsession, booking) + + assert.Error(t, err) + assert.False(t, result) + }) +}