Skip to content

fixes: backend/room-booking#250

Merged
harshitap1305 merged 2 commits intoOpenLake:mainfrom
harshitap1305:room-booking
Apr 11, 2026
Merged

fixes: backend/room-booking#250
harshitap1305 merged 2 commits intoOpenLake:mainfrom
harshitap1305:room-booking

Conversation

@harshitap1305
Copy link
Copy Markdown
Member

@harshitap1305 harshitap1305 commented Apr 11, 2026

Pull request overview

This PR updates the room booking controller to make booking creation and booking review (approve/reject) more robust by reintroducing transaction/session usage and improving how overlapping events are queried.

Changes:

  • Made findOverlappingEvent() apply a Mongoose session only when one is provided.
  • Refactored bookRoom to perform clash checks + booking creation inside a transaction and return richer 409 conflict responses.
  • Refactored updateBookingStatus to perform approval validation and status updates inside a transaction (including overlap checks).

Copilot AI review requested due to automatic review settings April 11, 2026 18:12
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 11, 2026

@harshitap1305 is attempting to deploy a commit to the openlake's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

Walkthrough

The PR updates roomBookingController.js with comprehensive validation, MongoDB transaction-backed booking operations, and conflict detection. Changes add request validation, atomic clash checking against bookings and events, detailed error responses for 409 conflicts, and enforce same-day calendar constraints in both bookRoom and updateBookingStatus functions.

Changes

Cohort / File(s) Summary
Room Booking Operations
backend/controllers/roomBookingController.js
bookRoom replaced with request validation, date/time parsing, room/role verification, and transactional overlap detection against existing bookings and calendar events; updateBookingStatus replaced with transactional approval logic enforcing Pending status, overlap checks, and conflict details in 409 responses; findOverlappingEvent conditional session handling added.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Controller as RoomBooking<br/>Controller
    participant DB as MongoDB<br/>Session
    participant Booking as Booking<br/>Collection
    participant Room as Room<br/>Collection
    participant Event as Event<br/>Collection

    Client->>Controller: POST /bookRoom (request data)
    Controller->>Controller: Validate fields, parse times, check startTime < endTime
    Controller->>Room: Verify room exists & active
    Controller->>Controller: Authorize role
    Controller->>DB: startSession()
    DB->>DB: withTransaction() start
    
    DB->>Booking: Find overlapping<br/>pending/approved bookings
    Booking-->>DB: Conflicting booking?
    
    alt Conflict Found
        DB->>DB: Rollback
        DB-->>Controller: Throw conflict error
        Controller-->>Client: 409 + conflictingBooking
    else No Conflict
        DB->>Event: Find overlapping events<br/>(via findOverlappingEvent)
        Event-->>DB: Conflicting event?
        
        alt Event Conflict Found
            DB->>DB: Rollback
            DB-->>Controller: Throw conflict error
            Controller-->>Client: 409 + conflictingEvent
        else No Conflict
            DB->>Booking: Save new booking
            Booking-->>DB: Created
            DB->>DB: Commit transaction
            DB-->>Controller: Success
            Controller-->>Client: 201 + booking data
        end
    end
Loading
sequenceDiagram
    actor Client
    participant Controller as RoomBooking<br/>Controller
    participant DB as MongoDB<br/>Session
    participant Booking as Booking<br/>Collection
    participant Room as Room<br/>Collection
    participant Event as Event<br/>Collection

    Client->>Controller: PATCH /booking/:id (status)
    Controller->>Controller: Validate booking ID & target status
    Controller->>Booking: Fetch booking
    Booking-->>Controller: Booking data
    
    alt Status not Pending
        Controller-->>Client: 409 + error
    else Status is Pending
        Controller->>DB: startSession()
        DB->>DB: withTransaction() start
        
        DB->>Booking: Find other approved<br/>bookings in time range
        Booking-->>DB: Conflict?
        
        alt Conflict Found
            DB->>DB: Rollback
            DB-->>Controller: Throw error
            Controller-->>Client: 409 + conflictingBooking
        else No Conflict
            DB->>Event: Find overlapping events
            Event-->>DB: Conflict?
            
            alt Event Conflict
                DB->>DB: Rollback
                DB-->>Controller: Throw error
                Controller-->>Client: 409 + conflictingEvent
            else No Conflict
                DB->>Booking: Update status, reviewedBy, timestamp
                Booking-->>DB: Updated
                DB->>Room: Update room timestamp
                Room-->>DB: Updated
                DB->>DB: Commit transaction
                DB-->>Controller: Success
                Controller-->>Client: 200 + booking data
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #235: Directly updates bookRoom and updateBookingStatus implementations with validation and transactional conflict detection logic in the same controller file.
  • PR #233: Modifies room booking handlers and overlap checking logic with session management patterns that are foundational to these transactional changes.

Poem

🐰 A booking system, tried and true,
Transactions flow like morning dew,
With overlaps caught, conflicts clear,
No double-bookings we need fear!
MongoDB keeps our promises tight,
Room bookings done just right! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR description lacks critical details from template: no issue reference, incomplete change sections, no testing validation, and missing deployment/documentation details. Complete all required sections: add issue #, specify Added/Fixed/Updated/Removed details, document testing performed, confirm documentation updates, and address deployment notes.
Title check ❓ Inconclusive The title 'fixes: backend/room-booking' is overly vague and generic, using a non-descriptive format that doesn't convey meaningful information about the specific changes made to the codebase. Provide a more descriptive title that highlights the main change, such as 'Add validation and transaction support to room booking endpoints' or 'Implement atomic booking operations with conflict detection'.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the room booking controller to make booking creation and booking review (approve/reject) more robust by reintroducing transaction/session usage and improving how overlapping events are queried.

Changes:

  • Made findOverlappingEvent() apply a Mongoose session only when one is provided.
  • Refactored bookRoom to perform clash checks + booking creation inside a transaction and return richer 409 conflict responses.
  • Refactored updateBookingStatus to perform approval validation and status updates inside a transaction (including overlap checks).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const bounds = getDayBounds(referenceDate);
if (!bounds) return false;
// Check if the time falls exactly within that local day's 24-hour IST window
return testDate >= bounds.dayStart && testDate <= bounds.dayEnd;
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSameCalendarDay treats dayEnd as inclusive (<=). Since getDayBounds() defines dayEnd as exactly dayStart + 24h, this will classify timestamps exactly at the next day’s 00:00 IST boundary as being on the previous day. Consider making dayEnd exclusive (testDate < bounds.dayEnd) or setting dayEnd to the last millisecond of the day if you want an inclusive comparison.

Suggested change
return testDate >= bounds.dayStart && testDate <= bounds.dayEnd;
return testDate >= bounds.dayStart && testDate < bounds.dayEnd;

Copilot uses AI. Check for mistakes.
Comment on lines +568 to +577
const roomForBooking = await Room.findById(booking.room)
.session(session)
.select("name room_id location")
.lean();

if (!roomForBooking) {
const missingRoomError = new Error("Room not found or inactive.");
missingRoomError.statusCode = 404;
throw missingRoomError;
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When approving a booking, the code treats a missing room as "not found or inactive", but Room.findById(...).select("name room_id location") does not check is_active. This can allow approvals for rooms that have been deactivated. Filter by is_active: true (or select is_active and validate it) to match the error message and booking creation behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +409 to +455
await session.withTransaction(async () => {
const clash = await RoomBooking.findOne({
room: roomObjectId,
status: { $in: ["Pending", "Approved"] },
date: { $gte: dayBounds.dayStart, $lt: dayBounds.dayEnd },
startTime: { $lt: parsedEndTime },
endTime: { $gt: parsedStartTime },
})
.session(session)
.select("_id room event startTime endTime status")
.lean();

if (clash) {
const conflictError = new Error("Room clash detected");
conflictError.statusCode = 409;
conflictError.conflictingBooking = clash;
throw conflictError;
}

const overlappingEvent = await findOverlappingEvent({
session,
venueCandidates,
start: parsedStartTime,
end: parsedEndTime,
excludeEventId: eventId,
});

if (overlappingEvent) {
const eventConflictError = new Error("Room clash detected");
eventConflictError.statusCode = 409;
eventConflictError.conflictingEvent = overlappingEvent;
throw eventConflictError;
}

const newBooking = new RoomBooking({
room: roomObjectId,
event: eventId || undefined,
date: dayBounds.dayStart,
startTime: parsedStartTime,
endTime: parsedEndTime,
purpose,
bookedBy: req.user._id,
});

await newBooking.save({ session });
createdBookingId = newBooking._id;

Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transaction here does not actually prevent two concurrent requests from both passing the overlap checks and inserting overlapping bookings, because each transaction only inserts a new RoomBooking document (no shared document is written that would cause a write conflict). If double-booking under concurrency must be prevented, introduce a per-(room, day) lock document (or another mechanism) that both transactions update within the transaction before re-checking overlaps, so MongoDB can detect a write conflict and force a retry.

Copilot uses AI. Check for mistakes.
Comment on lines +581 to +625
const dayBounds = getDayBounds(booking.date);
const overlappingApprovedBooking = await RoomBooking.findOne({
_id: { $ne: booking._id },
room: booking.room,
status: "Approved",
date: { $gte: dayBounds.dayStart, $lt: dayBounds.dayEnd },
startTime: { $lt: booking.endTime },
endTime: { $gt: booking.startTime },
})
.session(session)
.select("_id room event startTime endTime")
.lean();

if (overlappingApprovedBooking) {
const conflictError = new Error(
"Cannot approve booking due to overlap with another approved booking.",
);
conflictError.statusCode = 409;
conflictError.conflictingBooking = overlappingApprovedBooking;
throw conflictError;
}

const overlappingEvent = await findOverlappingEvent({
session,
venueCandidates,
start: booking.startTime,
end: booking.endTime,
excludeEventId: booking.event,
});

if (overlappingEvent) {
const eventConflictError = new Error(
"Cannot approve booking due to overlap with another event.",
);
eventConflictError.statusCode = 409;
eventConflictError.conflictingEvent = overlappingEvent;
throw eventConflictError;
}
}

booking.status = status;
booking.reviewedBy = req.user._id;
booking.updated_at = new Date();
await booking.save({ session });
reviewedBookingId = booking._id;
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concurrency issue on approval: two reviewers (or retries) can concurrently approve overlapping pending bookings since the transaction only updates the specific booking document and reads approved bookings; there’s no shared lock/write that would create a conflict. If exclusive approval is required, use a per-(room, day) lock/update inside the transaction (or another conflict-enforcing strategy) before checking overlaps and setting status to Approved.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/controllers/roomBookingController.js`:
- Around line 568-577: The Room lookup currently ignores the room's is_active
flag so disabled rooms can still be approved; update the query that populates
roomForBooking to only return active rooms (e.g., replace
Room.findById(booking.room) with a query that includes { _id: booking.room,
is_active: true } or add .where({ is_active: true })), keep the same
.session(session).select("name room_id location").lean() chain, and leave the
existing not-found Error throw intact so inactive rooms trigger the 404 path.
- Around line 363-369: The current same-day check allows an end time exactly at
dayBounds.dayEnd (next day's 00:00); update the validation in the block using
parsedStartTime, parsedEndTime, dayBounds.dayStart and dayBounds.dayEnd so that
bookings ending exactly at dayBounds.dayEnd are rejected. Concretely, keep the
existing isSameCalendarDay checks but add a guard that also fails when
parsedEndTime.getTime() === dayBounds.dayEnd.getTime() (or otherwise ensure
parsedEndTime is strictly before dayBounds.dayEnd) so next-day midnight end
times are treated as cross-day and return the 400 error.
- Around line 559-589: The approval path only checks startTime < endTime but
doesn't prevent approving bookings that span midnight; before approving (in the
block using getDayBounds and RoomBooking.findOne) reapply the same-day
invariant: use getDayBounds(booking.date) to compute the day's bounds and ensure
the booking's start and end datetimes fall inside that single day (e.g., combine
booking.date with booking.startTime/booking.endTime and assert start < end and
end <= dayBounds.dayEnd); if the check fails throw a 400 error (same pattern as
the existing invalidTimeError). This prevents approving cross-day pending rows
that would evade the overlap query and keeps RoomBooking.findOne safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d032e943-b716-4975-8809-de1268ec05bf

📥 Commits

Reviewing files that changed from the base of the PR and between b8c2b52 and 594a46e.

📒 Files selected for processing (1)
  • backend/controllers/roomBookingController.js

Comment on lines +363 to +369
if (
!isSameCalendarDay(parsedStartTime, dayBounds.dayStart) ||
!isSameCalendarDay(parsedEndTime, dayBounds.dayStart)
) {
return res
.status(400)
.json({ message: "Booking must start and end on the same date." });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject next-day midnight end times here.

This check still lets a booking ending exactly at the next day's 00:00 through, because isSameCalendarDay() treats dayEnd as inclusive. That creates a cross-day booking while date is still bucketed under the previous day, so the spillover can be missed by next-day availability/conflict queries.

Proposed fix
-    if (
-      !isSameCalendarDay(parsedStartTime, dayBounds.dayStart) ||
-      !isSameCalendarDay(parsedEndTime, dayBounds.dayStart)
-    ) {
+    if (
+      parsedStartTime < dayBounds.dayStart ||
+      parsedStartTime >= dayBounds.dayEnd ||
+      parsedEndTime <= dayBounds.dayStart ||
+      parsedEndTime >= dayBounds.dayEnd
+    ) {
       return res
         .status(400)
         .json({ message: "Booking must start and end on the same date." });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/roomBookingController.js` around lines 363 - 369, The
current same-day check allows an end time exactly at dayBounds.dayEnd (next
day's 00:00); update the validation in the block using parsedStartTime,
parsedEndTime, dayBounds.dayStart and dayBounds.dayEnd so that bookings ending
exactly at dayBounds.dayEnd are rejected. Concretely, keep the existing
isSameCalendarDay checks but add a guard that also fails when
parsedEndTime.getTime() === dayBounds.dayEnd.getTime() (or otherwise ensure
parsedEndTime is strictly before dayBounds.dayEnd) so next-day midnight end
times are treated as cross-day and return the 400 error.

Comment on lines +559 to +589
if (status === "Approved") {
if (!(booking.startTime < booking.endTime)) {
const invalidTimeError = new Error(
"Invalid booking time range for approval.",
);
invalidTimeError.statusCode = 400;
throw invalidTimeError;
}

const roomForBooking = await Room.findById(booking.room)
.session(session)
.select("name room_id location")
.lean();

if (!roomForBooking) {
const missingRoomError = new Error("Room not found or inactive.");
missingRoomError.statusCode = 404;
throw missingRoomError;
}

const venueCandidates = getRoomVenueCandidates(roomForBooking);

const dayBounds = getDayBounds(booking.date);
const overlappingApprovedBooking = await RoomBooking.findOne({
_id: { $ne: booking._id },
room: booking.room,
status: "Approved",
date: { $gte: dayBounds.dayStart, $lt: dayBounds.dayEnd },
startTime: { $lt: booking.endTime },
endTime: { $gt: booking.startTime },
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reapply the same-day invariant during approval.

bookRoom now blocks cross-day ranges, but approval only checks startTime < endTime. Any preexisting pending row that spans midnight can still be approved, and this overlap query only scans booking.date's day bucket, so conflicts on the following day are missed.

Proposed fix
         if (status === "Approved") {
           if (!(booking.startTime < booking.endTime)) {
             const invalidTimeError = new Error(
               "Invalid booking time range for approval.",
             );
             invalidTimeError.statusCode = 400;
             throw invalidTimeError;
           }
+
+          const approvalDayBounds = getDayBounds(booking.date);
+          if (
+            !approvalDayBounds ||
+            booking.startTime < approvalDayBounds.dayStart ||
+            booking.startTime >= approvalDayBounds.dayEnd ||
+            booking.endTime <= approvalDayBounds.dayStart ||
+            booking.endTime >= approvalDayBounds.dayEnd
+          ) {
+            const invalidDayError = new Error(
+              "Booking must start and end on the same date.",
+            );
+            invalidDayError.statusCode = 400;
+            throw invalidDayError;
+          }
 
           const roomForBooking = await Room.findById(booking.room)
             .session(session)
             .select("name room_id location")
             .lean();
 
@@
-          const dayBounds = getDayBounds(booking.date);
+          const dayBounds = approvalDayBounds;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/roomBookingController.js` around lines 559 - 589, The
approval path only checks startTime < endTime but doesn't prevent approving
bookings that span midnight; before approving (in the block using getDayBounds
and RoomBooking.findOne) reapply the same-day invariant: use
getDayBounds(booking.date) to compute the day's bounds and ensure the booking's
start and end datetimes fall inside that single day (e.g., combine booking.date
with booking.startTime/booking.endTime and assert start < end and end <=
dayBounds.dayEnd); if the check fails throw a 400 error (same pattern as the
existing invalidTimeError). This prevents approving cross-day pending rows that
would evade the overlap query and keeps RoomBooking.findOne safe.

Comment on lines +568 to +577
const roomForBooking = await Room.findById(booking.room)
.session(session)
.select("name room_id location")
.lean();

if (!roomForBooking) {
const missingRoomError = new Error("Room not found or inactive.");
missingRoomError.statusCode = 404;
throw missingRoomError;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inactive rooms can still be approved.

This lookup never reads is_active, so a room disabled after the booking was created is still treated as approvable. The current error message already implies inactive rooms should be rejected.

Proposed fix
           const roomForBooking = await Room.findById(booking.room)
             .session(session)
-            .select("name room_id location")
+            .select("name room_id location is_active")
             .lean();
 
-          if (!roomForBooking) {
+          if (!roomForBooking || !roomForBooking.is_active) {
             const missingRoomError = new Error("Room not found or inactive.");
             missingRoomError.statusCode = 404;
             throw missingRoomError;
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const roomForBooking = await Room.findById(booking.room)
.session(session)
.select("name room_id location")
.lean();
if (!roomForBooking) {
const missingRoomError = new Error("Room not found or inactive.");
missingRoomError.statusCode = 404;
throw missingRoomError;
}
const roomForBooking = await Room.findById(booking.room)
.session(session)
.select("name room_id location is_active")
.lean();
if (!roomForBooking || !roomForBooking.is_active) {
const missingRoomError = new Error("Room not found or inactive.");
missingRoomError.statusCode = 404;
throw missingRoomError;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/controllers/roomBookingController.js` around lines 568 - 577, The
Room lookup currently ignores the room's is_active flag so disabled rooms can
still be approved; update the query that populates roomForBooking to only return
active rooms (e.g., replace Room.findById(booking.room) with a query that
includes { _id: booking.room, is_active: true } or add .where({ is_active: true
})), keep the same .session(session).select("name room_id location").lean()
chain, and leave the existing not-found Error throw intact so inactive rooms
trigger the 404 path.

@harshitap1305 harshitap1305 changed the title fixed fixes: backend/room-booking Apr 11, 2026
@harshitap1305 harshitap1305 merged commit ad4069a into OpenLake:main Apr 11, 2026
7 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants