From 434d9dd421ce53ffc97d0744dda6d48fd631460a Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:19:50 -0600 Subject: [PATCH] refactor: remove booking module and related files --- src/booking/booking.module.ts | 29 -- .../controllers/bookable-space.controller.ts | 107 ----- src/booking/controllers/booking.controller.ts | 107 ----- .../dtos/bookable-space-request.dto.ts | 31 -- .../dtos/bookable-space-response.dto.ts | 59 --- src/booking/dtos/booking-request.dto.ts | 23 -- src/booking/dtos/booking-response.dto.ts | 88 ----- src/booking/dtos/create-bookable-space.dto.ts | 63 --- src/booking/dtos/create-booking.dto.ts | 35 -- src/booking/dtos/my-booking-request.dto.ts | 14 - src/booking/dtos/update-bookable-space.dto.ts | 12 - src/booking/index.ts | 1 - .../services/bookable-space.service.ts | 370 ------------------ src/booking/services/booking.service.ts | 218 ----------- 14 files changed, 1157 deletions(-) delete mode 100644 src/booking/booking.module.ts delete mode 100644 src/booking/controllers/bookable-space.controller.ts delete mode 100644 src/booking/controllers/booking.controller.ts delete mode 100644 src/booking/dtos/bookable-space-request.dto.ts delete mode 100644 src/booking/dtos/bookable-space-response.dto.ts delete mode 100644 src/booking/dtos/booking-request.dto.ts delete mode 100644 src/booking/dtos/booking-response.dto.ts delete mode 100644 src/booking/dtos/create-bookable-space.dto.ts delete mode 100644 src/booking/dtos/create-booking.dto.ts delete mode 100644 src/booking/dtos/my-booking-request.dto.ts delete mode 100644 src/booking/dtos/update-bookable-space.dto.ts delete mode 100644 src/booking/index.ts delete mode 100644 src/booking/services/bookable-space.service.ts delete mode 100644 src/booking/services/booking.service.ts diff --git a/src/booking/booking.module.ts b/src/booking/booking.module.ts deleted file mode 100644 index 490f96b..0000000 --- a/src/booking/booking.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BookingRepositoryModule } from '@app/common/modules/booking/booking.repository.module'; -import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories/bookable-space.repository'; -import { BookingEntityRepository } from '@app/common/modules/booking/repositories/booking.repository'; -import { SpaceRepository } from '@app/common/modules/space'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { EmailService } from '@app/common/util/email/email.service'; -import { Global, Module } from '@nestjs/common'; -import { BookableSpaceController } from './controllers/bookable-space.controller'; -import { BookingController } from './controllers/booking.controller'; -import { BookableSpaceService } from './services/bookable-space.service'; -import { BookingService } from './services/booking.service'; - -@Global() -@Module({ - imports: [BookingRepositoryModule], - controllers: [BookableSpaceController, BookingController], - providers: [ - BookableSpaceService, - BookingService, - EmailService, - BookableSpaceEntityRepository, - BookingEntityRepository, - - SpaceRepository, - UserRepository, - ], - exports: [BookableSpaceService, BookingService], -}) -export class BookingModule {} diff --git a/src/booking/controllers/bookable-space.controller.ts b/src/booking/controllers/bookable-space.controller.ts deleted file mode 100644 index 99452f6..0000000 --- a/src/booking/controllers/bookable-space.controller.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { ControllerRoute } from '@app/common/constants/controller-route'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { - Body, - Controller, - Get, - Param, - ParseUUIDPipe, - Post, - Put, - Query, - Req, - UseGuards, -} from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { PageResponse } from '@app/common/dto/pagination.response.dto'; -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { plainToInstance } from 'class-transformer'; -import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto'; -import { BookableSpaceResponseDto } from '../dtos/bookable-space-response.dto'; -import { CreateBookableSpaceDto } from '../dtos/create-bookable-space.dto'; -import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto'; -import { BookableSpaceService } from '../services/bookable-space.service'; - -@ApiTags('Booking Module') -@Controller({ - version: EnableDisableStatusEnum.ENABLED, - path: ControllerRoute.BOOKABLE_SPACES.ROUTE, -}) -export class BookableSpaceController { - constructor(private readonly bookableSpaceService: BookableSpaceService) {} - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Post() - @ApiOperation({ - summary: - ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_SUMMARY, - description: - ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_DESCRIPTION, - }) - async create(@Body() dto: CreateBookableSpaceDto): Promise { - const result = await this.bookableSpaceService.create(dto); - return new SuccessResponseDto({ - data: result, - message: 'Successfully created bookable spaces', - }); - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get() - @ApiOperation({ - summary: - ControllerRoute.BOOKABLE_SPACES.ACTIONS.GET_ALL_BOOKABLE_SPACES_SUMMARY, - description: - ControllerRoute.BOOKABLE_SPACES.ACTIONS - .GET_ALL_BOOKABLE_SPACES_DESCRIPTION, - }) - async findAll( - @Query() query: BookableSpaceRequestDto, - @Req() req: Request, - ): Promise> { - const project = req['user']?.project?.uuid; - if (!project) { - throw new Error('Project UUID is required in the request'); - } - const { data, pagination } = await this.bookableSpaceService.findAll( - query, - project, - ); - return new PageResponse( - { - data: data.map((space) => - plainToInstance(BookableSpaceResponseDto, space, { - excludeExtraneousValues: true, - }), - ), - message: 'Successfully fetched all bookable spaces', - }, - pagination, - ); - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Put(':spaceUuid') - @ApiOperation({ - summary: - ControllerRoute.BOOKABLE_SPACES.ACTIONS.UPDATE_BOOKABLE_SPACES_SUMMARY, - description: - ControllerRoute.BOOKABLE_SPACES.ACTIONS - .UPDATE_BOOKABLE_SPACES_DESCRIPTION, - }) - async update( - @Param('spaceUuid', ParseUUIDPipe) spaceUuid: string, - @Body() dto: UpdateBookableSpaceDto, - ): Promise { - const result = await this.bookableSpaceService.update(spaceUuid, dto); - return new SuccessResponseDto({ - data: result, - message: 'Successfully updated bookable spaces', - }); - } -} diff --git a/src/booking/controllers/booking.controller.ts b/src/booking/controllers/booking.controller.ts deleted file mode 100644 index bdbe516..0000000 --- a/src/booking/controllers/booking.controller.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { ControllerRoute } from '@app/common/constants/controller-route'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { - Body, - Controller, - Get, - Post, - Query, - Req, - UseGuards, -} from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { plainToInstance } from 'class-transformer'; -import { AdminRoleGuard } from 'src/guards/admin.role.guard'; -import { BookingRequestDto } from '../dtos/booking-request.dto'; -import { BookingResponseDto } from '../dtos/booking-response.dto'; -import { CreateBookingDto } from '../dtos/create-booking.dto'; -import { MyBookingRequestDto } from '../dtos/my-booking-request.dto'; -import { BookingService } from '../services/booking.service'; - -@ApiTags('Booking Module') -@Controller({ - version: EnableDisableStatusEnum.ENABLED, - path: ControllerRoute.BOOKING.ROUTE, -}) -export class BookingController { - constructor(private readonly bookingService: BookingService) {} - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Post() - @ApiOperation({ - summary: ControllerRoute.BOOKING.ACTIONS.ADD_BOOKING_SUMMARY, - description: ControllerRoute.BOOKING.ACTIONS.ADD_BOOKING_DESCRIPTION, - }) - async create( - @Body() dto: CreateBookingDto, - @Req() req: Request, - ): Promise { - const userUuid = req['user']?.uuid; - if (!userUuid) { - throw new Error('User UUID is required in the request'); - } - const result = await this.bookingService.create(userUuid, dto); - return new SuccessResponseDto({ - data: result, - message: 'Successfully created booking', - }); - } - - @ApiBearerAuth() - @UseGuards(AdminRoleGuard) - @Get() - @ApiOperation({ - summary: ControllerRoute.BOOKING.ACTIONS.GET_ALL_BOOKINGS_SUMMARY, - description: ControllerRoute.BOOKING.ACTIONS.GET_ALL_BOOKINGS_DESCRIPTION, - }) - async findAll( - @Query() query: BookingRequestDto, - @Req() req: Request, - ): Promise { - const project = req['user']?.project?.uuid; - if (!project) { - throw new Error('Project UUID is required in the request'); - } - const result = await this.bookingService.findAll(query, project); - return new SuccessResponseDto({ - data: plainToInstance(BookingResponseDto, result, { - excludeExtraneousValues: true, - }), - message: 'Successfully fetched all bookings', - }); - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('my-bookings') - @ApiOperation({ - summary: ControllerRoute.BOOKING.ACTIONS.GET_MY_BOOKINGS_SUMMARY, - description: ControllerRoute.BOOKING.ACTIONS.GET_MY_BOOKINGS_DESCRIPTION, - }) - async findMyBookings( - @Query() query: MyBookingRequestDto, - @Req() req: Request, - ): Promise { - const userUuid = req['user']?.uuid; - const project = req['user']?.project?.uuid; - if (!project) { - throw new Error('Project UUID is required in the request'); - } - const result = await this.bookingService.findMyBookings( - query, - userUuid, - project, - ); - return new SuccessResponseDto({ - data: plainToInstance(BookingResponseDto, result, { - excludeExtraneousValues: true, - }), - message: 'Successfully fetched all bookings', - }); - } -} diff --git a/src/booking/dtos/bookable-space-request.dto.ts b/src/booking/dtos/bookable-space-request.dto.ts deleted file mode 100644 index e958aa0..0000000 --- a/src/booking/dtos/bookable-space-request.dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { BooleanValues } from '@app/common/constants/boolean-values.enum'; -import { PaginationRequestWithSearchGetListDto } from '@app/common/dto/pagination-with-search.request.dto'; -import { ApiProperty, OmitType } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsBoolean, IsNotEmpty, IsOptional } from 'class-validator'; - -export class BookableSpaceRequestDto extends OmitType( - PaginationRequestWithSearchGetListDto, - ['includeSpaces'], -) { - @ApiProperty({ - type: Boolean, - required: false, - }) - @IsBoolean() - @IsOptional() - @Transform(({ obj }) => { - return obj.active === BooleanValues.TRUE; - }) - active?: boolean; - - @ApiProperty({ - type: Boolean, - }) - @IsBoolean() - @IsNotEmpty() - @Transform(({ obj }) => { - return obj.configured === BooleanValues.TRUE; - }) - configured: boolean; -} diff --git a/src/booking/dtos/bookable-space-response.dto.ts b/src/booking/dtos/bookable-space-response.dto.ts deleted file mode 100644 index 3e89bd2..0000000 --- a/src/booking/dtos/bookable-space-response.dto.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Expose, Type } from 'class-transformer'; -export class BookableSpaceConfigResponseDto { - @ApiProperty() - @Expose() - uuid: string; - - @ApiProperty({ - type: [String], - }) - @Expose() - daysAvailable: string[]; - - @ApiProperty() - @Expose() - startTime: string; - - @ApiProperty() - @Expose() - endTime: string; - - @ApiProperty({ - type: Boolean, - }) - @Expose() - active: boolean; - - @ApiProperty({ - type: Number, - nullable: true, - }) - @Expose() - points?: number; -} - -export class BookableSpaceResponseDto { - @ApiProperty() - @Expose() - uuid: string; - - @ApiProperty() - @Expose() - spaceUuid: string; - - @ApiProperty() - @Expose() - spaceName: string; - - @ApiProperty() - @Expose() - virtualLocation: string; - - @ApiProperty({ - type: BookableSpaceConfigResponseDto, - }) - @Expose() - @Type(() => BookableSpaceConfigResponseDto) - bookableConfig: BookableSpaceConfigResponseDto; -} diff --git a/src/booking/dtos/booking-request.dto.ts b/src/booking/dtos/booking-request.dto.ts deleted file mode 100644 index b76257f..0000000 --- a/src/booking/dtos/booking-request.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsUUID, Matches } from 'class-validator'; - -export class BookingRequestDto { - @ApiProperty({ - description: 'Month in MM/YYYY format', - example: '07/2025', - }) - @IsNotEmpty() - @Matches(/^(0[1-9]|1[0-2])\/\d{4}$/, { - message: 'Date must be in MM/YYYY format', - }) - month: string; - - @ApiProperty({ - description: 'Space UUID', - example: '550e8400-e29b-41d4-a716-446655440000', - required: false, - }) - @IsOptional() - @IsUUID('4') - space?: string; -} diff --git a/src/booking/dtos/booking-response.dto.ts b/src/booking/dtos/booking-response.dto.ts deleted file mode 100644 index a8a0bc7..0000000 --- a/src/booking/dtos/booking-response.dto.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Expose, Transform, Type } from 'class-transformer'; - -export class BookingUserResponseDto { - @ApiProperty() - @Expose() - uuid: string; - - @ApiProperty() - @Expose() - firstName: string; - - @ApiProperty() - @Expose() - lastName: string; - - @ApiProperty({ - type: String, - nullable: true, - }) - @Expose() - email: string; - - @ApiProperty({ - type: String, - nullable: true, - }) - @Expose() - @Transform(({ obj }) => obj.inviteUser?.companyName || null) - companyName: string; - - @ApiProperty({ - type: String, - nullable: true, - }) - @Expose() - phoneNumber: string; -} - -export class BookingSpaceResponseDto { - @ApiProperty() - @Expose() - uuid: string; - - @ApiProperty() - @Expose() - spaceName: string; -} - -export class BookingResponseDto { - @ApiProperty() - @Expose() - uuid: string; - - @ApiProperty({ - type: Date, - }) - @Expose() - date: Date; - - @ApiProperty() - @Expose() - startTime: string; - - @ApiProperty() - @Expose() - endTime: string; - - @ApiProperty({ - type: Number, - }) - @Expose() - cost: number; - - @ApiProperty({ - type: BookingUserResponseDto, - }) - @Type(() => BookingUserResponseDto) - @Expose() - user: BookingUserResponseDto; - - @ApiProperty({ - type: BookingSpaceResponseDto, - }) - @Type(() => BookingSpaceResponseDto) - @Expose() - space: BookingSpaceResponseDto; -} diff --git a/src/booking/dtos/create-bookable-space.dto.ts b/src/booking/dtos/create-bookable-space.dto.ts deleted file mode 100644 index f7389f1..0000000 --- a/src/booking/dtos/create-bookable-space.dto.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { DaysEnum } from '@app/common/constants/days.enum'; -import { ApiProperty } from '@nestjs/swagger'; -import { - ArrayMinSize, - IsArray, - IsEnum, - IsInt, - IsNotEmpty, - IsOptional, - IsString, - IsUUID, - Matches, - Max, - Min, -} from 'class-validator'; - -export class CreateBookableSpaceDto { - @ApiProperty({ - type: 'string', - isArray: true, - example: [ - '3fa85f64-5717-4562-b3fc-2c963f66afa6', - '4fa85f64-5717-4562-b3fc-2c963f66afa7', - ], - }) - @IsArray() - @ArrayMinSize(1, { message: 'At least one space must be selected' }) - @IsUUID('all', { each: true, message: 'Invalid space UUID provided' }) - spaceUuids: string[]; - - @ApiProperty({ - enum: DaysEnum, - isArray: true, - example: [DaysEnum.MON, DaysEnum.WED, DaysEnum.FRI], - }) - @IsArray() - @ArrayMinSize(1, { message: 'At least one day must be selected' }) - @IsEnum(DaysEnum, { each: true, message: 'Invalid day provided' }) - daysAvailable: DaysEnum[]; - - @ApiProperty({ example: '09:00' }) - @IsString() - @IsNotEmpty({ message: 'Start time cannot be empty' }) - @Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, { - message: 'Start time must be in HH:mm format (24-hour)', - }) - startTime: string; - - @ApiProperty({ example: '17:00' }) - @IsString() - @IsNotEmpty({ message: 'End time cannot be empty' }) - @Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, { - message: 'End time must be in HH:mm format (24-hour)', - }) - endTime: string; - - @ApiProperty({ example: 10, required: false }) - @IsOptional() - @IsInt() - @Min(0, { message: 'Points cannot be negative' }) - @Max(1000, { message: 'Points cannot exceed 1000' }) - points?: number; -} diff --git a/src/booking/dtos/create-booking.dto.ts b/src/booking/dtos/create-booking.dto.ts deleted file mode 100644 index 4f453b2..0000000 --- a/src/booking/dtos/create-booking.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsDate, IsNotEmpty, IsString, IsUUID, Matches } from 'class-validator'; - -export class CreateBookingDto { - @ApiProperty({ - type: 'string', - example: '4fa85f64-5717-4562-b3fc-2c963f66afa7', - }) - @IsNotEmpty() - @IsUUID('4', { message: 'Invalid space UUID provided' }) - spaceUuid: string; - - @ApiProperty({ - type: Date, - }) - @IsNotEmpty() - @IsDate() - date: Date; - - @ApiProperty({ example: '09:00' }) - @IsString() - @IsNotEmpty({ message: 'Start time cannot be empty' }) - @Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, { - message: 'Start time must be in HH:mm format (24-hour)', - }) - startTime: string; - - @ApiProperty({ example: '17:00' }) - @IsString() - @IsNotEmpty({ message: 'End time cannot be empty' }) - @Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, { - message: 'End time must be in HH:mm format (24-hour)', - }) - endTime: string; -} diff --git a/src/booking/dtos/my-booking-request.dto.ts b/src/booking/dtos/my-booking-request.dto.ts deleted file mode 100644 index 402aa36..0000000 --- a/src/booking/dtos/my-booking-request.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsIn, IsOptional } from 'class-validator'; - -export class MyBookingRequestDto { - @ApiProperty({ - description: 'Filter bookings by time period', - example: 'past', - enum: ['past', 'future'], - required: false, - }) - @IsOptional() - @IsIn(['past', 'future']) - when?: 'past' | 'future'; -} diff --git a/src/booking/dtos/update-bookable-space.dto.ts b/src/booking/dtos/update-bookable-space.dto.ts deleted file mode 100644 index 2b7f048..0000000 --- a/src/booking/dtos/update-bookable-space.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger'; -import { IsBoolean, IsOptional } from 'class-validator'; -import { CreateBookableSpaceDto } from './create-bookable-space.dto'; - -export class UpdateBookableSpaceDto extends PartialType( - OmitType(CreateBookableSpaceDto, ['spaceUuids']), -) { - @ApiProperty({ type: Boolean }) - @IsOptional() - @IsBoolean() - active?: boolean; -} diff --git a/src/booking/index.ts b/src/booking/index.ts deleted file mode 100644 index 770d226..0000000 --- a/src/booking/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './booking.module'; diff --git a/src/booking/services/bookable-space.service.ts b/src/booking/services/bookable-space.service.ts deleted file mode 100644 index c497f83..0000000 --- a/src/booking/services/bookable-space.service.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { PageResponseDto } from '@app/common/dto/pagination.response.dto'; -import { timeToMinutes } from '@app/common/helper/timeToMinutes'; -import { TypeORMCustomModel } from '@app/common/models/typeOrmCustom.model'; -import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories/bookable-space.repository'; -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 { EmailService } from '@app/common/util/email/email.service'; -import { to12HourFormat } from '@app/common/util/time-to-12-hours-convetion'; -import { - BadRequestException, - ConflictException, - Injectable, - NotFoundException, -} from '@nestjs/common'; -import { format } from 'date-fns'; -import { Brackets, In } from 'typeorm'; -import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto'; -import { CreateBookableSpaceDto } from '../dtos/create-bookable-space.dto'; -import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto'; - -@Injectable() -export class BookableSpaceService { - constructor( - private readonly emailService: EmailService, - private readonly bookableSpaceEntityRepository: BookableSpaceEntityRepository, - private readonly bookingEntityRepository: BookingEntityRepository, - private readonly spaceRepository: SpaceRepository, - ) {} - - async create(dto: CreateBookableSpaceDto) { - // Validate time slots first - this.validateTimeSlot(dto.startTime, dto.endTime); - - // fetch spaces exist - const spaces = await this.getSpacesOrFindMissing(dto.spaceUuids); - - // Validate no duplicate bookable configurations - await this.validateNoDuplicateBookableConfigs(dto.spaceUuids); - - // Create and save bookable spaces - return this.createBookableSpaces(spaces, dto); - } - - async findAll( - { active, page, size, configured, search }: BookableSpaceRequestDto, - project: string, - ): Promise<{ - data: BaseResponseDto['data']; - pagination: PageResponseDto; - }> { - let qb = this.spaceRepository - .createQueryBuilder('space') - .leftJoinAndSelect('space.parent', 'parentSpace') - .leftJoinAndSelect('space.community', 'community') - .where('community.project = :project', { project }); - - if (search) { - qb = qb.andWhere( - '(space.spaceName ILIKE :search OR community.name ILIKE :search OR parentSpace.spaceName ILIKE :search)', - { search: `%${search}%` }, - ); - } - if (configured) { - qb = qb - .leftJoinAndSelect('space.bookableConfig', 'bookableConfig') - .andWhere('bookableConfig.uuid IS NOT NULL'); - if (active !== undefined) { - qb = qb.andWhere('bookableConfig.active = :active', { active }); - } - } else { - qb = qb - .leftJoinAndSelect('space.bookableConfig', 'bookableConfig') - .andWhere('bookableConfig.uuid IS NULL'); - } - const customModel = TypeORMCustomModel(this.spaceRepository); - - const { baseResponseDto, paginationResponseDto } = - await customModel.findAll({ page, size, modelName: 'space' }, qb); - return { - data: baseResponseDto.data.map((space) => { - return { - ...space, - virtualLocation: `${space.community?.name} - ${space.parent ? space.parent?.spaceName + ' - ' : ''}${space.spaceName}`, - }; - }), - pagination: paginationResponseDto, - }; - } - - /** - * Update bookable space configuration - */ - async update(spaceUuid: string, dto: UpdateBookableSpaceDto) { - // fetch spaces exist - const space = (await this.getSpacesOrFindMissing([spaceUuid]))[0]; - - if (!space.bookableConfig) { - throw new NotFoundException( - `Bookable configuration not found for space: ${spaceUuid}`, - ); - } - if (dto.startTime || dto.endTime) { - // Validate time slots first - this.validateTimeSlot( - dto.startTime || space.bookableConfig.startTime, - dto.endTime || space.bookableConfig.endTime, - ); - if ( - dto.startTime != space.bookableConfig.startTime || - dto.endTime != space.bookableConfig.endTime || - dto.daysAvailable != space.bookableConfig.daysAvailable - ) { - this.handleTimingUpdate( - { - daysAvailable: - dto.daysAvailable || space.bookableConfig.daysAvailable, - startTime: dto.startTime || space.bookableConfig.startTime, - endTime: dto.endTime || space.bookableConfig.endTime, - }, - space, - ); - } - } - - if ( - dto.active !== undefined && - dto.active !== space.bookableConfig.active - ) { - this.handleAvailabilityUpdate(dto.active, space); - } - - Object.assign(space.bookableConfig, dto); - return this.bookableSpaceEntityRepository.save(space.bookableConfig); - } - - private async handleTimingUpdate( - dto: UpdateBookableSpaceDto, - space: SpaceEntity, - ): Promise { - const affectedUsers = await this.getAffectedBookings(space.uuid); - if (!affectedUsers.length) return; - - const groupedParams = this.groupBookingsByUser(affectedUsers); - - return this.emailService.sendUpdateBookingTimingEmailWithTemplate( - groupedParams, - { - space_name: space.spaceName, - start_time: to12HourFormat(dto.startTime), - end_time: to12HourFormat(dto.endTime), - days: dto.daysAvailable.join(', '), - }, - ); - } - - private async getAffectedBookings(spaceUuid: string) { - const today = new Date(); - const nowTime = format(today, 'HH:mm'); - - const bookingWithDayCte = this.bookingEntityRepository - .createQueryBuilder('b') - .select('b.*') - .addSelect( - ` - CASE EXTRACT(DOW FROM b.date) - WHEN 0 THEN 'Sun' - WHEN 1 THEN 'Mon' - WHEN 2 THEN 'Tue' - WHEN 3 THEN 'Wed' - WHEN 4 THEN 'Thu' - WHEN 5 THEN 'Fri' - WHEN 6 THEN 'Sat' - END::"bookable-space_days_available_enum" - `, - 'booking_day', - ) - .where( - `(DATE(b.date) > :today OR (DATE(b.date) = :today AND b.startTime >= :nowTime))`, - { today, nowTime }, - ) - .andWhere('b.space_uuid = :spaceUuid', { spaceUuid }); - - const query = this.bookableSpaceEntityRepository - .createQueryBuilder('bs') - .distinct(true) - .addCommonTableExpression(bookingWithDayCte, 'booking_with_day') - .select('u.first_name', 'name') - .addSelect('u.email', 'email') - .addSelect('DATE(bwd.date)', 'date') - .addSelect('bwd.start_time', 'start_time') - .addSelect('bwd.end_time', 'end_time') - .from('booking_with_day', 'bwd') - .innerJoin('user', 'u', 'u.uuid = bwd.user_uuid') - .where('bs.space_uuid = :spaceUuid', { spaceUuid }) - .andWhere( - new Brackets((qb) => { - qb.where('NOT (bwd.booking_day = ANY(bs.days_available))') - .orWhere('bwd.start_time < bs.start_time') - .orWhere('bwd.end_time > bs.end_time'); - }), - ); - - return query.getRawMany<{ - name: string; - email: string; - date: string; - start_time: string; - end_time: string; - }>(); - } - - private groupBookingsByUser( - bookings: { - name: string; - email: string; - date: string; - start_time: string; - end_time: string; - }[], - ): { - name: string; - email: string; - bookings: { date: string; start_time: string; end_time: string }[]; - }[] { - const grouped: Record< - string, - { - name: string; - email: string; - bookings: { date: string; start_time: string; end_time: string }[]; - } - > = {}; - - for (const { name, email, date, start_time, end_time } of bookings) { - const formattedDate = format(new Date(date), 'yyyy-MM-dd'); - const formattedStartTime = to12HourFormat(start_time); - const formattedEndTime = to12HourFormat(end_time); - - if (!grouped[email]) { - grouped[email] = { - name, - email, - bookings: [], - }; - } - - grouped[email].bookings.push({ - date: formattedDate, - start_time: formattedStartTime, - end_time: formattedEndTime, - }); - } - - return Object.values(grouped); - } - - private async handleAvailabilityUpdate( - active: boolean, - space: SpaceEntity, - ): Promise { - space = await this.spaceRepository.findOne({ - where: { uuid: space.uuid }, - relations: ['userSpaces', 'userSpaces.user'], - }); - const emails = space.userSpaces.map((userSpace) => ({ - email: userSpace.user.email, - name: userSpace.user.firstName, - })); - if (!emails.length) return Promise.resolve(); - - return this.emailService.sendUpdateBookingAvailabilityEmailWithTemplate( - emails, - { - availability: active ? 'Available' : 'Unavailable', - space_name: space.spaceName, - isAvailable: active, - }, - ); - } - - /** - * Fetch spaces by UUIDs and throw an error if any are missing - */ - private async getSpacesOrFindMissing( - spaceUuids: string[], - ): Promise { - const spaces = await this.spaceRepository.find({ - where: { uuid: In(spaceUuids) }, - relations: ['bookableConfig'], - }); - - if (spaces.length !== spaceUuids.length) { - const foundUuids = spaces.map((s) => s.uuid); - const missingUuids = spaceUuids.filter( - (uuid) => !foundUuids.includes(uuid), - ); - throw new NotFoundException( - `Spaces not found: ${missingUuids.join(', ')}`, - ); - } - - return spaces; - } - - /** - * Validate there are no existing bookable configurations for these spaces - */ - private async validateNoDuplicateBookableConfigs( - spaceUuids: string[], - ): Promise { - const existingBookables = await this.bookableSpaceEntityRepository.find({ - where: { space: { uuid: In(spaceUuids) } }, - relations: ['space'], - }); - - if (existingBookables.length > 0) { - const existingUuids = [ - ...new Set(existingBookables.map((b) => b.space.uuid)), - ]; - throw new ConflictException( - `Bookable configuration already exists for spaces: ${existingUuids.join(', ')}`, - ); - } - } - - /** - * 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}`, - ); - } - } - - /** - * Create bookable space entries after all validations pass - */ - private async createBookableSpaces( - spaces: SpaceEntity[], - dto: CreateBookableSpaceDto, - ) { - try { - const entries = spaces.map((space) => - this.bookableSpaceEntityRepository.create({ - space, - daysAvailable: dto.daysAvailable, - startTime: dto.startTime, - endTime: dto.endTime, - points: dto.points, - }), - ); - - return this.bookableSpaceEntityRepository.save(entries); - } catch (error) { - if (error.code === '23505') { - throw new ConflictException( - 'Duplicate bookable space configuration detected', - ); - } - throw error; - } - } -} diff --git a/src/booking/services/booking.service.ts b/src/booking/services/booking.service.ts deleted file mode 100644 index 2148300..0000000 --- a/src/booking/services/booking.service.ts +++ /dev/null @@ -1,218 +0,0 @@ -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); - } -}