From f8e16cffdfaee9be373db25a30e1df8acc279f6a Mon Sep 17 00:00:00 2001 From: zakhaev26 Date: Wed, 28 Aug 2024 09:37:40 +0530 Subject: [PATCH] feat: adds available rooms API --- package.json | 7 +- .../available-rooms/available-rooms.class.ts | 129 ++++++++++++++++++ .../available-rooms/available-rooms.hooks.ts | 37 +++++ .../available-rooms.service.ts | 26 ++++ src/services/bookings/bookings.hooks.ts | 2 +- src/services/index.ts | 2 + .../isValidStatusMove.ts | 13 ++ .../update-booking-status.class.ts | 31 +++-- test/services/available-rooms.test.ts | 8 ++ 9 files changed, 242 insertions(+), 13 deletions(-) create mode 100644 src/services/available-rooms/available-rooms.class.ts create mode 100644 src/services/available-rooms/available-rooms.hooks.ts create mode 100644 src/services/available-rooms/available-rooms.service.ts create mode 100644 src/services/update-booking-status/isValidStatusMove.ts create mode 100644 test/services/available-rooms.test.ts diff --git a/package.json b/package.json index 25bbc60..f1a767f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "dev": "ts-node-dev --no-notify src/", "start": "yarn run compile && node lib/", "mocha": "mocha --require ts-node/register --require source-map-support/register \"test/**/*.ts\" --recursive --exit", - "compile": "shx rm -rf lib/ && tsc" + "compile": "shx rm -rf lib/ && tsc", + "ngrok": "ngrok http --domain=bat-giving-roughy.ngrok-free.app 3030" }, "standard": { "env": [ @@ -84,5 +85,9 @@ "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", "typescript": "^4.7.3" + }, + "volta": { + "node": "20.16.0", + "yarn": "1.22.22" } } diff --git a/src/services/available-rooms/available-rooms.class.ts b/src/services/available-rooms/available-rooms.class.ts new file mode 100644 index 0000000..6147b61 --- /dev/null +++ b/src/services/available-rooms/available-rooms.class.ts @@ -0,0 +1,129 @@ +import { + Id, + NullableId, + Paginated, + Params, + ServiceMethods, +} from "@feathersjs/feathers"; +import { Application } from "../../declarations"; +import { BadRequest } from "@feathersjs/errors"; + +interface Data {} + +interface ServiceOptions {} + +export class AvailableRooms implements ServiceMethods { + app: Application; + options: ServiceOptions; + + constructor(options: ServiceOptions = {}, app: Application) { + this.options = options; + this.app = app; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async find(params?: Params): Promise> { + /* + * @description + * This Endpoint returns all the available rooms (status == Available ) between a startDate & + * endDate. The Handler queries (agg.) the db to search for rooms between startDate and endDate and + * sends the result to client as response. + * */ + + interface QueryInterface { + startDate?: Date; + endDate?: Date; + } + + const { startDate, endDate } = params?.query as QueryInterface; + + if (!startDate || !endDate) + throw new BadRequest("Please provide startDate and endDate"); + + const start = new Date(startDate); + const end = new Date(endDate); + + if (isNaN(start.getTime()) || isNaN(end.getTime())) { + throw new BadRequest("Invalid date format"); + } + + const roomsModel = this.app.service("rooms").Model; + + // rooms that are not booked during the specified dates + const roomsWithOrWithoutBookings = await roomsModel + .aggregate([ + { + $lookup: { + from: "bookings", + localField: "_id", + foreignField: "room", + as: "bookings", + }, + }, + { + $match: { + $or: [ + { bookings: { $size: 0 } }, + { + bookings: { + $not: { + $elemMatch: { + dates: { + $not: { + $elemMatch: { + $lt: end, + $gte: start, + }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + ]) + .exec(); + + const resp = roomsWithOrWithoutBookings.map((room: any) => { + if (room.bookings.length === 0) { + delete room.bookings; + return room; + } + }); + + return resp; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async get(id: Id, params?: Params): Promise { + return { + id, + text: `A new message with ID: ${id}!`, + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async create(data: Data, params?: Params): Promise { + if (Array.isArray(data)) { + return Promise.all(data.map((current) => this.create(current, params))); + } + + return data; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async update(id: NullableId, data: Data, params?: Params): Promise { + return data; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async patch(id: NullableId, data: Data, params?: Params): Promise { + return data; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async remove(id: NullableId, params?: Params): Promise { + return { id }; + } +} diff --git a/src/services/available-rooms/available-rooms.hooks.ts b/src/services/available-rooms/available-rooms.hooks.ts new file mode 100644 index 0000000..4330229 --- /dev/null +++ b/src/services/available-rooms/available-rooms.hooks.ts @@ -0,0 +1,37 @@ +import { HooksObject } from '@feathersjs/feathers'; +import * as authentication from '@feathersjs/authentication'; +// Don't remove this comment. It's needed to format import lines nicely. + +const { authenticate } = authentication.hooks; + +export default { + before: { + all: [ authenticate('jwt') ], + find: [], + get: [], + create: [], + update: [], + patch: [], + remove: [] + }, + + after: { + all: [], + find: [], + get: [], + create: [], + update: [], + patch: [], + remove: [] + }, + + error: { + all: [], + find: [], + get: [], + create: [], + update: [], + patch: [], + remove: [] + } +}; diff --git a/src/services/available-rooms/available-rooms.service.ts b/src/services/available-rooms/available-rooms.service.ts new file mode 100644 index 0000000..7c89914 --- /dev/null +++ b/src/services/available-rooms/available-rooms.service.ts @@ -0,0 +1,26 @@ +// Initializes the `available-rooms` service on path `/available-rooms` +import { ServiceAddons } from '@feathersjs/feathers'; +import { Application } from '../../declarations'; +import { AvailableRooms } from './available-rooms.class'; +import hooks from './available-rooms.hooks'; + +// Add this service to the service type index +declare module '../../declarations' { + interface ServiceTypes { + 'available-rooms': AvailableRooms & ServiceAddons; + } +} + +export default function (app: Application): void { + const options = { + paginate: app.get('paginate') + }; + + // Initialize our service with any options it requires + app.use('/available-rooms', new AvailableRooms(options, app)); + + // Get our initialized service so that we can register hooks + const service = app.service('available-rooms'); + + service.hooks(hooks); +} diff --git a/src/services/bookings/bookings.hooks.ts b/src/services/bookings/bookings.hooks.ts index a4914df..1ea6036 100644 --- a/src/services/bookings/bookings.hooks.ts +++ b/src/services/bookings/bookings.hooks.ts @@ -23,7 +23,7 @@ export default { // @ts-expect-error old defs in .d.ts lib files find: [iff(isUserType, setQuery("user", "_id")), handleSoftDelete()], // @ts-expect-error old defs in .d.ts lib files - get: [iff(isUserType, setQuery("user")), handleSoftDelete()], + get: [iff(isUserType, setQuery("user", "_id")), handleSoftDelete()], /** * @zakhaev26 * @todo diff --git a/src/services/index.ts b/src/services/index.ts index fcdea04..08e068e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -8,6 +8,7 @@ import forgotPassword from './forgot-password/forgot-password.service'; import sendOtp from './send-otp/send-otp.service'; import otp from './otp/otp.service'; import updateBookingStatus from './update-booking-status/update-booking-status.service'; +import availableRooms from './available-rooms/available-rooms.service'; // Don't remove this comment. It's needed to format import lines nicely. export default function (app: Application): void { @@ -20,4 +21,5 @@ export default function (app: Application): void { app.configure(otp); app.configure(sendOtp); app.configure(updateBookingStatus); + app.configure(availableRooms); } diff --git a/src/services/update-booking-status/isValidStatusMove.ts b/src/services/update-booking-status/isValidStatusMove.ts new file mode 100644 index 0000000..d7b669d --- /dev/null +++ b/src/services/update-booking-status/isValidStatusMove.ts @@ -0,0 +1,13 @@ +import BookingStatus from '../../constants/booking-status.enum'; + +const isValidRoomStatusMove = (currentStatus: string, newStatus: string): boolean => { + // Define the valid status transitions + const validTransitions: Record = { + [BookingStatus.PENDING]: [BookingStatus.APPROVED,BookingStatus.CANCELLED], + [BookingStatus.CANCELLED]: [], + [BookingStatus.APPROVED]: [], + }; + return validTransitions[currentStatus].includes(newStatus); +}; + +export default isValidRoomStatusMove; diff --git a/src/services/update-booking-status/update-booking-status.class.ts b/src/services/update-booking-status/update-booking-status.class.ts index 09c37dd..0f0d026 100644 --- a/src/services/update-booking-status/update-booking-status.class.ts +++ b/src/services/update-booking-status/update-booking-status.class.ts @@ -4,11 +4,12 @@ import { Paginated, Params, ServiceMethods, -} from "@feathersjs/feathers"; -import { Application } from "../../declarations"; -import { BadRequest, NotAuthenticated } from "@feathersjs/errors"; -import RolesEnum from "../../constants/roles.enum"; -import BookingStatus from "../../constants/booking-status.enum"; +} from '@feathersjs/feathers'; +import { Application } from '../../declarations'; +import { BadRequest, NotAuthenticated } from '@feathersjs/errors'; +import RolesEnum from '../../constants/roles.enum'; +import BookingStatus from '../../constants/booking-status.enum'; +import isValidStatusMove from './isValidStatusMove'; interface Data {} interface CreateDTO { @@ -62,19 +63,27 @@ export class UpdateBookingStatus implements ServiceMethods { if (!params || !params.user) throw new NotAuthenticated(); if (![RolesEnum.SUPER_ADMIN].includes(params.user.type)) throw new BadRequest( - "Only Super Admins are allowed to perform this task." + 'Only Super Admins are allowed to perform this task.' ); if (!id) { - throw new BadRequest("Please provide the Booking ID to be updated."); + throw new BadRequest('Please provide the Booking ID to be updated.'); + } + + const currentBooking = await this.app.service('bookings')._get(id); + if(!currentBooking) throw new BadRequest('Booking does not exist!'); + + + if(data.status && !isValidStatusMove(currentBooking.status, data.status)) { + throw new BadRequest('Invalid Status Move!'); } const reqBody: Record = {}; - if (data.paid) reqBody["paid"] = data.paid; - if (data.status) reqBody["status"] = data.status; + if (data.paid) reqBody['paid'] = data.paid; + if (data.status) reqBody['status'] = data.status; - reqBody["lastManagedBy"] = params.user._id; - const resp = await this.app.service("bookings")._patch(id, reqBody); + reqBody['lastManagedBy'] = params.user._id; + const resp = await this.app.service('bookings')._patch(id, reqBody); return resp; } diff --git a/test/services/available-rooms.test.ts b/test/services/available-rooms.test.ts new file mode 100644 index 0000000..ae3738d --- /dev/null +++ b/test/services/available-rooms.test.ts @@ -0,0 +1,8 @@ +import app from '../../src/app'; + +describe('\'available-rooms\' service', () => { + it('registered the service', () => { + const service = app.service('available-rooms'); + expect(service).toBeTruthy(); + }); +});