From 89f57446d0995ddc4260104aa3e79859386fb796 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 00:57:59 +0000 Subject: [PATCH 1/3] Optimize booking retrieval in cron job to solve N+1 query problem - Introduced `getBookings` helper in `helpers/booking.helper.js` for batch fetching of bookings by type and IDs. - Refactored `getUnpaidOnlineBookingsAndTransactions` in `cron.js` to use the new batch fetching logic. - Added unit tests in `tests/optimize_booking_retrieval.test.js` verifying the optimization. - Resolved N+1 problem by replacing individual queries inside a loop with a single query per booking type. Co-authored-by: VitraagVigyaan <211280194+VitraagVigyaan@users.noreply.github.com> --- cron.js | 16 +++-- helpers/booking.helper.js | 47 +++++++++++++++ tests/optimize_booking_retrieval.test.js | 75 ++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 tests/optimize_booking_retrieval.test.js diff --git a/cron.js b/cron.js index 2ca8250a..206c5156 100644 --- a/cron.js +++ b/cron.js @@ -26,6 +26,7 @@ import UtsavDb from './models/utsav_db.model.js'; import { sendCancellationEmail, sendOpenBookingEmail } from './helpers/mailer.helper.js'; import { getBooking, + getBookings, getBookingType, getBookingTypeFromBooking } from './helpers/booking.helper.js'; @@ -109,19 +110,26 @@ async function getUnpaidOnlineBookingsAndTransactions(bookings, transactions) { .subtract(MAX_APP_PAYMENT_DURATION, 'minutes'); const pendingTransactions = await getPendingTransactions(cancelTimeFilter); + const bookingsByType = {}; + for (const transaction of pendingTransactions) { const bookingType = getBookingType(transaction); - // TODO: optimize, get all bookings at once // Food bookings are handled in a special way if (bookingType != TYPE_FOOD) { - const booking = await getBooking(bookingType, transaction.bookingid); - if (booking) { - bookings.push(booking); + if (!bookingsByType[bookingType]) { + bookingsByType[bookingType] = []; } + bookingsByType[bookingType].push(transaction.bookingid); } transactions.push(transaction); } + + for (const bookingType in bookingsByType) { + const bookingIds = bookingsByType[bookingType]; + const fetchedBookings = await getBookings(bookingType, bookingIds); + bookings.push(...fetchedBookings); + } } async function cancelBookings(systemUser, bookings, userBookingIds, openBookings, t) { diff --git a/helpers/booking.helper.js b/helpers/booking.helper.js index e6f501ea..ee730fbc 100644 --- a/helpers/booking.helper.js +++ b/helpers/booking.helper.js @@ -84,6 +84,53 @@ export async function getBooking(bookingType, bookingid) { return booking; } +export async function getBookings(bookingType, bookingids) { + var bookings = []; + + switch (bookingType) { + case TYPE_ROOM: + bookings = await RoomBooking.findAll({ + where: { bookingid: bookingids } + }); + break; + + case TYPE_FLAT: + bookings = await FlatBooking.findAll({ + where: { bookingid: bookingids } + }); + break; + + case TYPE_ADHYAYAN: + bookings = await ShibirBookingDb.findAll({ + where: { bookingid: bookingids } + }); + break; + + case TYPE_FOOD: + bookings = await FoodDb.findAll({ + where: { id: bookingids } + }); + break; + + case TYPE_TRAVEL: + bookings = await TravelDb.findAll({ + where: { bookingid: bookingids } + }); + break; + + case TYPE_UTSAV: + bookings = await UtsavBooking.findAll({ + where: { bookingid: bookingids } + }); + break; + + default: + throw new ApiError(400, `${ERR_INVALID_BOOKING_TYPE}: ${bookingType}`); + } + + return bookings; +} + export function ifMigrated(transaction) { let bookingid = transaction.bookingid; let description = transaction.description; diff --git a/tests/optimize_booking_retrieval.test.js b/tests/optimize_booking_retrieval.test.js new file mode 100644 index 00000000..65dcc92a --- /dev/null +++ b/tests/optimize_booking_retrieval.test.js @@ -0,0 +1,75 @@ +import { expect, it, describe, mock } from "bun:test"; +import { + TYPE_ROOM, + TYPE_FLAT, + TYPE_ADHYAYAN, + TYPE_FOOD, + TYPE_TRAVEL, + TYPE_UTSAV +} from '../config/constants.js'; + +mock.module("../models/associations.js", () => ({ + RoomBooking: { findAll: async (q) => q.where.bookingid.map(id => ({ bookingid: id })) }, + FlatBooking: { findAll: async (q) => q.where.bookingid.map(id => ({ bookingid: id })) }, + ShibirBookingDb: { findAll: async (q) => q.where.bookingid.map(id => ({ bookingid: id })) }, + FoodDb: { findAll: async (q) => q.where.id.map(id => ({ id })) }, + TravelDb: { findAll: async (q) => q.where.bookingid.map(id => ({ bookingid: id })) }, + UtsavBooking: { findAll: async (q) => q.where.bookingid.map(id => ({ bookingid: id })) }, + CardDb: {}, + Transactions: {}, + ShibirDb: {}, + Departments: {}, + MaintenanceDb: {}, + GateRecord: {}, + CentreDb: {}, + BulkFoodBooking: {}, + FoodPhysicalPlate: {}, + RoomDb: {}, + FlatDb: {}, + UtsavDb: {}, + UtsavPackagesDb: {}, + AdminRoles: {}, + AdminUsers: {}, + Roles: {}, + Menu: {}, + Cities: {}, + States: {}, + Countries: {}, + GuestDb: {}, + GuestRelationship: {}, + RazorpayWebhook: {}, + RazorpaySettlement: {}, + SupportTickets: {}, + BlockDates: {}, + Updates: {}, + AdhyayanFeedback: {}, + RazorpaySettlementRecon: {}, + ShibirAttendanceDb: {} +})); + +const { getBookings } = await import('../helpers/booking.helper.js'); + +describe('getBookings', () => { + it('should call RoomBooking.findAll for TYPE_ROOM', async () => { + const ids = ['room1']; + const result = await getBookings(TYPE_ROOM, ids); + expect(result).toHaveLength(1); + expect(result[0].bookingid).toBe('room1'); + }); + + it('should call FoodDb.findAll for TYPE_FOOD', async () => { + const ids = [1]; + const result = await getBookings(TYPE_FOOD, ids); + expect(result).toHaveLength(1); + expect(result[0].id).toBe(1); + }); + + it('should throw error for invalid booking type', async () => { + try { + await getBookings('INVALID', ['id1']); + expect(true).toBe(false); // Should not reach here + } catch (e) { + expect(e.message).toContain('Invalid booking type'); + } + }); +}); From 342f960d856965c2dc5775c9109a9c929d480eaf Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 01:21:33 +0000 Subject: [PATCH 2/3] Optimize booking retrieval in cron job and add unit tests - Implemented batch fetching for bookings in `helpers/booking.helper.js` to solve N+1 query problem. - Refactored cron job in `cron.js` to use the new batch retrieval logic. - Added comprehensive unit tests in `tests/optimize_booking_retrieval.test.js` using Bun. - Verified logic via automated tests. Co-authored-by: VitraagVigyaan <211280194+VitraagVigyaan@users.noreply.github.com> From ad3ecad31e7c3baf4d586dd680a516b46e6e789b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 01:39:44 +0000 Subject: [PATCH 3/3] Refine booking retrieval optimization in cron job - Switched to using `Set` for unique booking IDs to prevent redundant queries. - Updated `getBookings` to use `let`/`const` and added robustness check for empty IDs. - Used `Object.entries()` for cleaner iteration over booking groups. - Updated unit tests with additional cases. Co-authored-by: VitraagVigyaan <211280194+VitraagVigyaan@users.noreply.github.com> --- cron.js | 8 ++++---- helpers/booking.helper.js | 6 +++++- tests/optimize_booking_retrieval.test.js | 5 +++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cron.js b/cron.js index 206c5156..48f3c278 100644 --- a/cron.js +++ b/cron.js @@ -118,15 +118,15 @@ async function getUnpaidOnlineBookingsAndTransactions(bookings, transactions) { // Food bookings are handled in a special way if (bookingType != TYPE_FOOD) { if (!bookingsByType[bookingType]) { - bookingsByType[bookingType] = []; + bookingsByType[bookingType] = new Set(); } - bookingsByType[bookingType].push(transaction.bookingid); + bookingsByType[bookingType].add(transaction.bookingid); } transactions.push(transaction); } - for (const bookingType in bookingsByType) { - const bookingIds = bookingsByType[bookingType]; + for (const [bookingType, bookingIdsSet] of Object.entries(bookingsByType)) { + const bookingIds = Array.from(bookingIdsSet); const fetchedBookings = await getBookings(bookingType, bookingIds); bookings.push(...fetchedBookings); } diff --git a/helpers/booking.helper.js b/helpers/booking.helper.js index ee730fbc..24fa42e9 100644 --- a/helpers/booking.helper.js +++ b/helpers/booking.helper.js @@ -85,7 +85,11 @@ export async function getBooking(bookingType, bookingid) { } export async function getBookings(bookingType, bookingids) { - var bookings = []; + if (!bookingids || bookingids.length === 0) { + return []; + } + + let bookings = []; switch (bookingType) { case TYPE_ROOM: diff --git a/tests/optimize_booking_retrieval.test.js b/tests/optimize_booking_retrieval.test.js index 65dcc92a..d352d84d 100644 --- a/tests/optimize_booking_retrieval.test.js +++ b/tests/optimize_booking_retrieval.test.js @@ -50,6 +50,11 @@ mock.module("../models/associations.js", () => ({ const { getBookings } = await import('../helpers/booking.helper.js'); describe('getBookings', () => { + it('should return empty array for empty bookingids', async () => { + const result = await getBookings(TYPE_ROOM, []); + expect(result).toEqual([]); + }); + it('should call RoomBooking.findAll for TYPE_ROOM', async () => { const ids = ['room1']; const result = await getBookings(TYPE_ROOM, ids);