import { DaysEnum } from '@app/common/constants/days.enum'; import { timeToMinutes } from '@app/common/helper/timeToMinutes'; import { BookingEntityRepository } from '@app/common/modules/booking/repositories/booking.repository'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository'; import { UserRepository } from '@app/common/modules/user/repositories/user.repository'; import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { format } from 'date-fns'; import { Between } from 'typeorm/find-options/operator/Between'; import { BookingRequestDto } from '../dtos/booking-request.dto'; import { CreateBookingDto } from '../dtos/create-booking.dto'; import { MyBookingRequestDto } from '../dtos/my-booking-request.dto'; @Injectable() export class BookingService { constructor( private readonly bookingEntityRepository: BookingEntityRepository, private readonly spaceRepository: SpaceRepository, private readonly userRepository: UserRepository, ) {} async create(userUuid: string, dto: CreateBookingDto) { console.log(userUuid); const user = await this.userRepository.findOne({ where: { uuid: userUuid }, relations: ['userSpaces', 'userSpaces.space'], }); console.log(user.userSpaces); if (!user.userSpaces.some(({ space }) => space.uuid === dto.spaceUuid)) { throw new ForbiddenException( `User does not have permission to book this space: ${dto.spaceUuid}`, ); } // Validate time slots first this.validateTimeSlot(dto.startTime, dto.endTime); // fetch spaces exist const space = await this.getSpaceConfigurationAndBookings(dto.spaceUuid); // Validate booking availability this.validateBookingAvailability(space, dto); // Create and save booking return this.createBookings(space, userUuid, dto); } async findAll({ month, space }: BookingRequestDto, project: string) { const [monthNumber, year] = month.split('-').map(Number); const fromDate = new Date(year, monthNumber - 1, 1); const toDate = new Date(year, monthNumber, 0, 23, 59, 59); return this.bookingEntityRepository.find({ where: { space: { community: { project: { uuid: project } }, uuid: space ? space : undefined, }, date: Between(fromDate, toDate), }, relations: ['space', 'user', 'user.inviteUser'], order: { date: 'DESC' }, }); } async findMyBookings( { when }: MyBookingRequestDto, userUuid: string, project: string, ) { const now = new Date(); const nowTime = format(now, 'HH:mm'); const query = this.bookingEntityRepository .createQueryBuilder('booking') .leftJoinAndSelect('booking.space', 'space') .innerJoin( 'space.community', 'community', 'community.project = :project', { project }, ) .leftJoinAndSelect('booking.user', 'user') .where('user.uuid = :userUuid', { userUuid }); if (when === 'past') { query.andWhere( `(DATE(booking.date) < :today OR (DATE(booking.date) = :today AND booking.startTime < :nowTime))`, { today: now, nowTime }, ); } else if (when === 'future') { query.andWhere( `(DATE(booking.date) > :today OR (DATE(booking.date) = :today AND booking.startTime >= :nowTime))`, { today: now, nowTime }, ); } query.orderBy({ 'DATE(booking.date)': 'DESC', 'booking.startTime': 'DESC', }); return query.getMany(); } /** * Fetch space by UUID and throw an error if not found or if not configured for booking */ private async getSpaceConfigurationAndBookings( spaceUuid: string, ): Promise { const space = await this.spaceRepository.findOne({ where: { uuid: spaceUuid }, relations: ['bookableConfig', 'bookings'], }); if (!space) { throw new NotFoundException(`Space not found: ${spaceUuid}`); } if (!space.bookableConfig) { throw new NotFoundException( `This space is not configured for booking: ${spaceUuid}`, ); } return space; } /** * Ensure the slot start time is before the end time */ private validateTimeSlot(startTime: string, endTime: string): void { const start = timeToMinutes(startTime); const end = timeToMinutes(endTime); if (start >= end) { throw new BadRequestException( `End time must be after start time for slot: ${startTime}-${endTime}`, ); } } /** * check if the space is available for booking on the requested day * and if the requested time slot is within the available hours */ private validateBookingAvailability( space: SpaceEntity, dto: CreateBookingDto, ): void { // Check if the space is available for booking on the requested day const availableDays = space.bookableConfig?.daysAvailable || []; const requestedDay = new Date(dto.date).toLocaleDateString('en-US', { weekday: 'short', }) as DaysEnum; if (!availableDays.includes(requestedDay)) { const dayFullName = new Date(dto.date).toLocaleDateString('en-US', { weekday: 'long', }); throw new BadRequestException( `Space is not available for booking on ${dayFullName}s`, ); } const dtoStartTimeInMinutes = timeToMinutes(dto.startTime); const dtoEndTimeInMinutes = timeToMinutes(dto.endTime); if ( dtoStartTimeInMinutes < timeToMinutes(space.bookableConfig.startTime) || dtoEndTimeInMinutes > timeToMinutes(space.bookableConfig.endTime) ) { throw new BadRequestException( `Booking time must be within the available hours for space: ${space.spaceName}`, ); } const previousBookings = space.bookings.filter( (booking) => timeToMinutes(booking.startTime) < dtoEndTimeInMinutes && timeToMinutes(booking.endTime) > dtoStartTimeInMinutes && format(new Date(booking.date), 'yyyy-MM-dd') === format(new Date(dto.date), 'yyyy-MM-dd'), ); if (previousBookings.length > 0) { // tell the user what time is unavailable const unavailableTimes = previousBookings.map((booking) => { return `${booking.startTime}-${booking.endTime}`; }); throw new ConflictException( `Space is already booked during this times: ${unavailableTimes.join(', ')}`, ); } } /** * Create bookable space entries after all validations pass */ private async createBookings( space: SpaceEntity, user: string, { spaceUuid, date, ...dto }: CreateBookingDto, ) { const entry = this.bookingEntityRepository.create({ space: { uuid: spaceUuid }, user: { uuid: user }, ...dto, date: new Date(date), cost: space.bookableConfig?.points || null, }); return this.bookingEntityRepository.save(entry); } }