From 8d44b66dd323684745812b61df011ff945efc90c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 17 Jun 2025 22:02:08 -0600 Subject: [PATCH 1/7] feat: add BookableSpace entity, DTO, repository, and integrate with Space entity --- libs/common/src/database/database.module.ts | 2 + .../booking/booking.repository.module.ts | 11 ++++ .../booking/dtos/bookable-space.dto.ts | 23 +++++++++ libs/common/src/modules/booking/dtos/index.ts | 1 + .../booking/entities/bookable-space.entity.ts | 51 +++++++++++++++++++ .../src/modules/booking/entities/index.ts | 1 + .../repositories/booking.repository.ts | 10 ++++ .../src/modules/booking/repositories/index.ts | 1 + .../modules/space/entities/space.entity.ts | 4 ++ 9 files changed, 104 insertions(+) create mode 100644 libs/common/src/modules/booking/booking.repository.module.ts create mode 100644 libs/common/src/modules/booking/dtos/bookable-space.dto.ts create mode 100644 libs/common/src/modules/booking/dtos/index.ts create mode 100644 libs/common/src/modules/booking/entities/bookable-space.entity.ts create mode 100644 libs/common/src/modules/booking/entities/index.ts create mode 100644 libs/common/src/modules/booking/repositories/booking.repository.ts create mode 100644 libs/common/src/modules/booking/repositories/index.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 2196901..921844f 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -59,6 +59,7 @@ import { } from '../modules/user/entities'; import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/entities'; +import { BookableSpaceEntity } from '../modules/booking/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -119,6 +120,7 @@ import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/entities PresenceSensorDailySpaceEntity, AqiSpaceDailyPollutantStatsEntity, SpaceDailyOccupancyDurationEntity, + BookableSpaceEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/booking/booking.repository.module.ts b/libs/common/src/modules/booking/booking.repository.module.ts new file mode 100644 index 0000000..fb5601b --- /dev/null +++ b/libs/common/src/modules/booking/booking.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BookableSpaceEntity } from './entities/bookable-space.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([BookableSpaceEntity])], +}) +export class BookableRepositoryModule {} diff --git a/libs/common/src/modules/booking/dtos/bookable-space.dto.ts b/libs/common/src/modules/booking/dtos/bookable-space.dto.ts new file mode 100644 index 0000000..8c7c41a --- /dev/null +++ b/libs/common/src/modules/booking/dtos/bookable-space.dto.ts @@ -0,0 +1,23 @@ +import { DaysEnum } from '@app/common/constants/days.enum'; +import { IsArray, IsEnum, IsInt, IsNotEmpty, IsString } from 'class-validator'; + +export class BookableSpaceDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsArray() + @IsEnum(DaysEnum, { each: true }) + daysAvailable: DaysEnum[]; + + @IsString() + @IsNotEmpty() + startTime: string; + + @IsString() + @IsNotEmpty() + endTime: string; + + @IsInt() + points: number; +} diff --git a/libs/common/src/modules/booking/dtos/index.ts b/libs/common/src/modules/booking/dtos/index.ts new file mode 100644 index 0000000..29762ae --- /dev/null +++ b/libs/common/src/modules/booking/dtos/index.ts @@ -0,0 +1 @@ +export * from './bookable-space.dto'; diff --git a/libs/common/src/modules/booking/entities/bookable-space.entity.ts b/libs/common/src/modules/booking/entities/bookable-space.entity.ts new file mode 100644 index 0000000..d5a12ac --- /dev/null +++ b/libs/common/src/modules/booking/entities/bookable-space.entity.ts @@ -0,0 +1,51 @@ +import { + Entity, + Column, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, + Unique, +} from 'typeorm'; +import { SpaceEntity } from '../../space/entities/space.entity'; +import { DaysEnum } from '@app/common/constants/days.enum'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { BookableSpaceDto } from '../dtos'; + +@Entity('bookable-space') +@Unique(['space']) +export class BookableSpaceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @ManyToOne(() => SpaceEntity, (space) => space.bookableConfigs, { + onDelete: 'CASCADE', + }) + space: SpaceEntity; + + @Column({ + type: 'enum', + enum: DaysEnum, + array: true, + nullable: false, + }) + daysAvailable: DaysEnum[]; + + @Column({ type: 'time' }) + startTime: string; + + @Column({ type: 'time' }) + endTime: string; + + @Column({ type: 'int' }) + points: number; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/libs/common/src/modules/booking/entities/index.ts b/libs/common/src/modules/booking/entities/index.ts new file mode 100644 index 0000000..7a921ce --- /dev/null +++ b/libs/common/src/modules/booking/entities/index.ts @@ -0,0 +1 @@ +export * from './bookable-space.entity'; diff --git a/libs/common/src/modules/booking/repositories/booking.repository.ts b/libs/common/src/modules/booking/repositories/booking.repository.ts new file mode 100644 index 0000000..87a1c49 --- /dev/null +++ b/libs/common/src/modules/booking/repositories/booking.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { BookableSpaceEntity } from '../entities/bookable-space.entity'; + +@Injectable() +export class BookableSpaceEntityRepository extends Repository { + constructor(private dataSource: DataSource) { + super(BookableSpaceEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/booking/repositories/index.ts b/libs/common/src/modules/booking/repositories/index.ts new file mode 100644 index 0000000..ab4b40c --- /dev/null +++ b/libs/common/src/modules/booking/repositories/index.ts @@ -0,0 +1 @@ +export * from './booking.repository'; diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 56b5d4f..2b0db0f 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -13,6 +13,7 @@ import { SubspaceEntity } from './subspace/subspace.entity'; import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities'; import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities'; import { SpaceDailyOccupancyDurationEntity } from '../../occupancy/entities'; +import { BookableSpaceEntity } from '../../booking/entities'; @Entity({ name: 'space' }) export class SpaceEntity extends AbstractEntity { @@ -126,6 +127,9 @@ export class SpaceEntity extends AbstractEntity { ) occupancyDaily: SpaceDailyOccupancyDurationEntity[]; + @OneToMany(() => BookableSpaceEntity, (bookable) => bookable.space) + bookableConfigs: BookableSpaceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); From 332b2f5851d9054d3a0f0af1592a404f2a1acdc1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 17 Jun 2025 22:02:13 -0600 Subject: [PATCH 2/7] feat: implement Booking module with BookableSpace entity, controller, service, and DTOs for managing bookable spaces --- libs/common/src/constants/controller-route.ts | 9 ++ libs/common/src/helper/timeToMinutes.ts | 5 + src/app.module.ts | 2 + src/booking/booking.module.ts | 16 +++ .../controllers/bookable-space.controller.ts | 30 ++++ src/booking/controllers/index.ts | 1 + src/booking/dtos/create-bookable-space.dto.ts | 75 ++++++++++ src/booking/dtos/index.ts | 1 + src/booking/index.ts | 1 + .../services/bookable-space.service.ts | 135 ++++++++++++++++++ src/booking/services/index.ts | 1 + 11 files changed, 276 insertions(+) create mode 100644 libs/common/src/helper/timeToMinutes.ts create mode 100644 src/booking/booking.module.ts create mode 100644 src/booking/controllers/bookable-space.controller.ts create mode 100644 src/booking/controllers/index.ts create mode 100644 src/booking/dtos/create-bookable-space.dto.ts create mode 100644 src/booking/dtos/index.ts create mode 100644 src/booking/index.ts create mode 100644 src/booking/services/bookable-space.service.ts create mode 100644 src/booking/services/index.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index cc37eee..353a9fb 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -69,7 +69,16 @@ export class ControllerRoute { 'Retrieve the list of all regions registered in Syncrow.'; }; }; + static BOOKABLE_SPACES = class { + public static readonly ROUTE = 'bookable-spaces'; + static ACTIONS = class { + public static readonly ADD_BOOKABLE_SPACES_SUMMARY = + 'Add new bookable spaces'; + public static readonly ADD_BOOKABLE_SPACES_DESCRIPTION = + 'This endpoint allows you to add new bookable spaces by providing the required details.'; + }; + }; static COMMUNITY = class { public static readonly ROUTE = '/projects/:projectUuid/communities'; static ACTIONS = class { diff --git a/libs/common/src/helper/timeToMinutes.ts b/libs/common/src/helper/timeToMinutes.ts new file mode 100644 index 0000000..8bf0e9b --- /dev/null +++ b/libs/common/src/helper/timeToMinutes.ts @@ -0,0 +1,5 @@ +// Convert time string (HH:mm) to minutes +export function timeToMinutes(time: string): number { + const [hours, minutes] = time.split(':').map(Number); + return hours * 60 + minutes; +} diff --git a/src/app.module.ts b/src/app.module.ts index ce64932..6bc32bd 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -39,6 +39,7 @@ import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston import { AqiModule } from './aqi/aqi.module'; import { OccupancyModule } from './occupancy/occupancy.module'; import { WeatherModule } from './weather/weather.module'; +import { BookingModule } from './booking'; @Module({ imports: [ ConfigModule.forRoot({ @@ -82,6 +83,7 @@ import { WeatherModule } from './weather/weather.module'; OccupancyModule, WeatherModule, AqiModule, + BookingModule, ], providers: [ { diff --git a/src/booking/booking.module.ts b/src/booking/booking.module.ts new file mode 100644 index 0000000..6ce9c9a --- /dev/null +++ b/src/booking/booking.module.ts @@ -0,0 +1,16 @@ +import { Global, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BookableSpaceController } from './controllers'; +import { BookableSpaceService } from './services'; +import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories'; +import { BookableSpaceEntity } from '@app/common/modules/booking/entities'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; + +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([BookableSpaceEntity, SpaceEntity])], + controllers: [BookableSpaceController], + providers: [BookableSpaceService, BookableSpaceEntityRepository], + exports: [BookableSpaceService], +}) +export class BookingModule {} diff --git a/src/booking/controllers/bookable-space.controller.ts b/src/booking/controllers/bookable-space.controller.ts new file mode 100644 index 0000000..08b5179 --- /dev/null +++ b/src/booking/controllers/bookable-space.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { ControllerRoute } from '@app/common/constants/controller-route'; + +import { CreateBookableSpaceDto } from '../dtos'; +import { BookableSpaceService } from '../services'; + +@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 { + return this.bookableSpaceService.create(dto); + } +} diff --git a/src/booking/controllers/index.ts b/src/booking/controllers/index.ts new file mode 100644 index 0000000..d239862 --- /dev/null +++ b/src/booking/controllers/index.ts @@ -0,0 +1 @@ +export * from './bookable-space.controller'; diff --git a/src/booking/dtos/create-bookable-space.dto.ts b/src/booking/dtos/create-bookable-space.dto.ts new file mode 100644 index 0000000..a8d88ba --- /dev/null +++ b/src/booking/dtos/create-bookable-space.dto.ts @@ -0,0 +1,75 @@ +// dtos/bookable-space.dto.ts +import { DaysEnum } from '@app/common/constants/days.enum'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsEnum, + IsNotEmpty, + IsString, + IsUUID, + IsInt, + ValidateNested, + ArrayMinSize, + ArrayMaxSize, + Min, + Max, + Matches, +} from 'class-validator'; + +export class BookingSlotDto { + @ApiProperty({ + enum: DaysEnum, + isArray: true, + example: [DaysEnum.MON, DaysEnum.WED, DaysEnum.FRI], + }) + @IsArray() + @ArrayMinSize(1, { message: 'At least one day must be selected' }) + @ArrayMaxSize(7, { message: 'Cannot select more than 7 days' }) + @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, minimum: 0 }) + @IsInt() + @Min(0, { message: 'Points cannot be negative' }) + @Max(1000, { message: 'Points cannot exceed 1000' }) + points: number; +} + +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({ type: BookingSlotDto, isArray: true }) + @IsArray() + @ArrayMinSize(1, { message: 'At least one booking slot must be provided' }) + @ValidateNested({ each: true }) + @Type(() => BookingSlotDto) + slots: BookingSlotDto[]; +} diff --git a/src/booking/dtos/index.ts b/src/booking/dtos/index.ts new file mode 100644 index 0000000..c56d66c --- /dev/null +++ b/src/booking/dtos/index.ts @@ -0,0 +1 @@ +export * from './create-bookable-space.dto'; diff --git a/src/booking/index.ts b/src/booking/index.ts new file mode 100644 index 0000000..770d226 --- /dev/null +++ b/src/booking/index.ts @@ -0,0 +1 @@ +export * from './booking.module'; diff --git a/src/booking/services/bookable-space.service.ts b/src/booking/services/bookable-space.service.ts new file mode 100644 index 0000000..57b573a --- /dev/null +++ b/src/booking/services/bookable-space.service.ts @@ -0,0 +1,135 @@ +// services/bookable-space.service.ts +import { + Injectable, + NotFoundException, + ConflictException, + BadRequestException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, In } from 'typeorm'; +import { BookingSlotDto, CreateBookableSpaceDto } from '../dtos'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { BookableSpaceEntity } from '@app/common/modules/booking/entities'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { timeToMinutes } from '@app/common/helper/timeToMinutes'; + +@Injectable() +export class BookableSpaceService { + constructor( + @InjectRepository(BookableSpaceEntity) + private bookableRepo: Repository, + + @InjectRepository(SpaceEntity) + private spaceRepo: Repository, + ) {} + + async create(dto: CreateBookableSpaceDto): Promise { + // Validate time slots first + this.validateTimeSlots(dto.slots); + + // Validate spaces exist + const spaces = await this.validateSpacesExist(dto.spaceUuids); + + // Validate no duplicate bookable configurations + await this.validateNoDuplicateBookableConfigs(dto.spaceUuids); + + // Create and save bookable spaces + return this.createBookableSpaces(spaces, dto.slots); + } + + /** + * Validate that all specified spaces exist + */ + private async validateSpacesExist( + spaceUuids: string[], + ): Promise { + const spaces = await this.spaceRepo.find({ + where: { uuid: In(spaceUuids) }, + }); + + 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.bookableRepo.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(', ')}`, + ); + } + } + + /** + * Validate time slots have valid format and logical times + */ + private validateTimeSlots(slots: BookingSlotDto[]): void { + slots.forEach((slot) => { + const start = timeToMinutes(slot.startTime); + const end = timeToMinutes(slot.endTime); + + if (start >= end) { + throw new BadRequestException( + `End time must be after start time for slot: ${slot.startTime}-${slot.endTime}`, + ); + } + }); + } + + /** + * Create bookable space entries after all validations pass + */ + private async createBookableSpaces( + spaces: SpaceEntity[], + slots: BookingSlotDto[], + ): Promise { + try { + const entries = spaces.flatMap((space) => + slots.map((slot) => + this.bookableRepo.create({ + space, + daysAvailable: slot.daysAvailable, + startTime: slot.startTime, + endTime: slot.endTime, + points: slot.points, + }), + ), + ); + + const data = await this.bookableRepo.save(entries); + return new SuccessResponseDto({ + data, + message: 'Successfully added new bookable spaces', + }); + } catch (error) { + if (error.code === '23505') { + throw new ConflictException( + 'Duplicate bookable space configuration detected', + ); + } + throw error; + } + } +} diff --git a/src/booking/services/index.ts b/src/booking/services/index.ts new file mode 100644 index 0000000..f8a6199 --- /dev/null +++ b/src/booking/services/index.ts @@ -0,0 +1 @@ +export * from './bookable-space.service'; From 8c34c68ba6a32bcea5d25201b63e29a66ce3f5a3 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 18 Jun 2025 01:48:35 -0600 Subject: [PATCH 3/7] refactor: remove BookableSpaceDto and its index export --- .../booking/dtos/bookable-space.dto.ts | 23 ------------------- libs/common/src/modules/booking/dtos/index.ts | 1 - 2 files changed, 24 deletions(-) delete mode 100644 libs/common/src/modules/booking/dtos/bookable-space.dto.ts delete mode 100644 libs/common/src/modules/booking/dtos/index.ts diff --git a/libs/common/src/modules/booking/dtos/bookable-space.dto.ts b/libs/common/src/modules/booking/dtos/bookable-space.dto.ts deleted file mode 100644 index 8c7c41a..0000000 --- a/libs/common/src/modules/booking/dtos/bookable-space.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DaysEnum } from '@app/common/constants/days.enum'; -import { IsArray, IsEnum, IsInt, IsNotEmpty, IsString } from 'class-validator'; - -export class BookableSpaceDto { - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsArray() - @IsEnum(DaysEnum, { each: true }) - daysAvailable: DaysEnum[]; - - @IsString() - @IsNotEmpty() - startTime: string; - - @IsString() - @IsNotEmpty() - endTime: string; - - @IsInt() - points: number; -} diff --git a/libs/common/src/modules/booking/dtos/index.ts b/libs/common/src/modules/booking/dtos/index.ts deleted file mode 100644 index 29762ae..0000000 --- a/libs/common/src/modules/booking/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './bookable-space.dto'; From df59e9a4a381e2092144d07e62c5867a7f948bf9 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 18 Jun 2025 01:48:46 -0600 Subject: [PATCH 4/7] refactor: update BookableSpaceEntity relationship to OneToOne with SpaceEntity --- .../booking/entities/bookable-space.entity.ts | 13 +++++-------- .../src/modules/space/entities/space.entity.ts | 13 ++++++++++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/libs/common/src/modules/booking/entities/bookable-space.entity.ts b/libs/common/src/modules/booking/entities/bookable-space.entity.ts index d5a12ac..bbe5e90 100644 --- a/libs/common/src/modules/booking/entities/bookable-space.entity.ts +++ b/libs/common/src/modules/booking/entities/bookable-space.entity.ts @@ -1,19 +1,17 @@ import { Entity, Column, - ManyToOne, CreateDateColumn, UpdateDateColumn, - Unique, + OneToOne, + JoinColumn, } from 'typeorm'; import { SpaceEntity } from '../../space/entities/space.entity'; import { DaysEnum } from '@app/common/constants/days.enum'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { BookableSpaceDto } from '../dtos'; @Entity('bookable-space') -@Unique(['space']) -export class BookableSpaceEntity extends AbstractEntity { +export class BookableSpaceEntity extends AbstractEntity { @Column({ type: 'uuid', default: () => 'gen_random_uuid()', @@ -21,9 +19,8 @@ export class BookableSpaceEntity extends AbstractEntity { }) public uuid: string; - @ManyToOne(() => SpaceEntity, (space) => space.bookableConfigs, { - onDelete: 'CASCADE', - }) + @OneToOne(() => SpaceEntity, (space) => space.bookableConfigs) + @JoinColumn({ name: 'space_uuid' }) space: SpaceEntity; @Column({ diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 2b0db0f..5200872 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -1,4 +1,11 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, +} from 'typeorm'; import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user/entities'; @@ -127,8 +134,8 @@ export class SpaceEntity extends AbstractEntity { ) occupancyDaily: SpaceDailyOccupancyDurationEntity[]; - @OneToMany(() => BookableSpaceEntity, (bookable) => bookable.space) - bookableConfigs: BookableSpaceEntity[]; + @OneToOne(() => BookableSpaceEntity, (bookable) => bookable.space) + bookableConfigs: BookableSpaceEntity; constructor(partial: Partial) { super(); From 274cdf741fa78bc343e67ae8843caa18fd4e914e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 18 Jun 2025 01:49:00 -0600 Subject: [PATCH 5/7] refactor: streamline Booking module and service by removing unused imports and consolidating space validation logic --- src/booking/booking.module.ts | 11 +-- src/booking/dtos/create-bookable-space.dto.ts | 45 ++++------- .../services/bookable-space.service.ts | 74 +++++++++---------- 3 files changed, 55 insertions(+), 75 deletions(-) diff --git a/src/booking/booking.module.ts b/src/booking/booking.module.ts index 6ce9c9a..fc9b2db 100644 --- a/src/booking/booking.module.ts +++ b/src/booking/booking.module.ts @@ -1,16 +1,17 @@ import { Global, Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { BookableSpaceController } from './controllers'; import { BookableSpaceService } from './services'; import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories'; -import { BookableSpaceEntity } from '@app/common/modules/booking/entities'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { SpaceRepository } from '@app/common/modules/space'; @Global() @Module({ - imports: [TypeOrmModule.forFeature([BookableSpaceEntity, SpaceEntity])], controllers: [BookableSpaceController], - providers: [BookableSpaceService, BookableSpaceEntityRepository], + providers: [ + BookableSpaceService, + BookableSpaceEntityRepository, + SpaceRepository, + ], exports: [BookableSpaceService], }) export class BookingModule {} diff --git a/src/booking/dtos/create-bookable-space.dto.ts b/src/booking/dtos/create-bookable-space.dto.ts index a8d88ba..6508c36 100644 --- a/src/booking/dtos/create-bookable-space.dto.ts +++ b/src/booking/dtos/create-bookable-space.dto.ts @@ -1,7 +1,6 @@ // dtos/bookable-space.dto.ts import { DaysEnum } from '@app/common/constants/days.enum'; import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; import { IsArray, IsEnum, @@ -9,15 +8,26 @@ import { IsString, IsUUID, IsInt, - ValidateNested, ArrayMinSize, - ArrayMaxSize, - Min, Max, + Min, Matches, } from 'class-validator'; -export class BookingSlotDto { +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, @@ -25,7 +35,6 @@ export class BookingSlotDto { }) @IsArray() @ArrayMinSize(1, { message: 'At least one day must be selected' }) - @ArrayMaxSize(7, { message: 'Cannot select more than 7 days' }) @IsEnum(DaysEnum, { each: true, message: 'Invalid day provided' }) daysAvailable: DaysEnum[]; @@ -45,31 +54,9 @@ export class BookingSlotDto { }) endTime: string; - @ApiProperty({ example: 10, minimum: 0 }) + @ApiProperty({ example: 10 }) @IsInt() @Min(0, { message: 'Points cannot be negative' }) @Max(1000, { message: 'Points cannot exceed 1000' }) points: number; } - -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({ type: BookingSlotDto, isArray: true }) - @IsArray() - @ArrayMinSize(1, { message: 'At least one booking slot must be provided' }) - @ValidateNested({ each: true }) - @Type(() => BookingSlotDto) - slots: BookingSlotDto[]; -} diff --git a/src/booking/services/bookable-space.service.ts b/src/booking/services/bookable-space.service.ts index 57b573a..409ee35 100644 --- a/src/booking/services/bookable-space.service.ts +++ b/src/booking/services/bookable-space.service.ts @@ -1,50 +1,46 @@ -// services/bookable-space.service.ts import { Injectable, NotFoundException, ConflictException, BadRequestException, } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; -import { BookingSlotDto, CreateBookableSpaceDto } from '../dtos'; +import { CreateBookableSpaceDto } from '../dtos'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { BookableSpaceEntity } from '@app/common/modules/booking/entities'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories'; +import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository'; +import { In } from 'typeorm'; import { timeToMinutes } from '@app/common/helper/timeToMinutes'; @Injectable() export class BookableSpaceService { constructor( - @InjectRepository(BookableSpaceEntity) - private bookableRepo: Repository, - - @InjectRepository(SpaceEntity) - private spaceRepo: Repository, + private readonly bookableSpaceEntityRepository: BookableSpaceEntityRepository, + private readonly spaceRepository: SpaceRepository, ) {} async create(dto: CreateBookableSpaceDto): Promise { // Validate time slots first - this.validateTimeSlots(dto.slots); + this.validateTimeSlot(dto.startTime, dto.endTime); - // Validate spaces exist - const spaces = await this.validateSpacesExist(dto.spaceUuids); + // 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.slots); + return this.createBookableSpaces(spaces, dto); } /** - * Validate that all specified spaces exist + * Fetch spaces by UUIDs and throw an error if any are missing */ - private async validateSpacesExist( + private async getSpacesOrFindMissing( spaceUuids: string[], ): Promise { - const spaces = await this.spaceRepo.find({ + const spaces = await this.spaceRepository.find({ where: { uuid: In(spaceUuids) }, }); @@ -67,7 +63,7 @@ export class BookableSpaceService { private async validateNoDuplicateBookableConfigs( spaceUuids: string[], ): Promise { - const existingBookables = await this.bookableRepo.find({ + const existingBookables = await this.bookableSpaceEntityRepository.find({ where: { space: { uuid: In(spaceUuids) } }, relations: ['space'], }); @@ -83,19 +79,17 @@ export class BookableSpaceService { } /** - * Validate time slots have valid format and logical times + * Ensure the slot start time is before the end time */ - private validateTimeSlots(slots: BookingSlotDto[]): void { - slots.forEach((slot) => { - const start = timeToMinutes(slot.startTime); - const end = timeToMinutes(slot.endTime); + 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: ${slot.startTime}-${slot.endTime}`, - ); - } - }); + if (start >= end) { + throw new BadRequestException( + `End time must be after start time for slot: ${startTime}-${endTime}`, + ); + } } /** @@ -103,22 +97,20 @@ export class BookableSpaceService { */ private async createBookableSpaces( spaces: SpaceEntity[], - slots: BookingSlotDto[], + dto: CreateBookableSpaceDto, ): Promise { try { - const entries = spaces.flatMap((space) => - slots.map((slot) => - this.bookableRepo.create({ - space, - daysAvailable: slot.daysAvailable, - startTime: slot.startTime, - endTime: slot.endTime, - points: slot.points, - }), - ), + const entries = spaces.map((space) => + this.bookableSpaceEntityRepository.create({ + space, + daysAvailable: dto.daysAvailable, + startTime: dto.startTime, + endTime: dto.endTime, + points: dto.points, + }), ); - const data = await this.bookableRepo.save(entries); + const data = await this.bookableSpaceEntityRepository.save(entries); return new SuccessResponseDto({ data, message: 'Successfully added new bookable spaces', From 49cc7629622e278b89d543906ff5530e43953f3e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 18 Jun 2025 01:56:51 -0600 Subject: [PATCH 6/7] fix duplication from conflict merge --- libs/common/src/modules/space/entities/space.entity.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index cfab8ce..d3c84e0 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -17,12 +17,8 @@ import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities'; import { SceneEntity } from '../../scene/entities'; import { SpaceModelEntity } from '../../space-model'; import { UserSpaceEntity } from '../../user/entities'; -import { SpaceDto } from '../dtos'; import { SpaceProductAllocationEntity } from './space-product-allocation.entity'; import { SubspaceEntity } from './subspace/subspace.entity'; -import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities'; -import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities'; -import { SpaceDailyOccupancyDurationEntity } from '../../occupancy/entities'; import { BookableSpaceEntity } from '../../booking/entities'; @Entity({ name: 'space' }) From d3d84da5e3414b22991a32cadb495b9ab832e12e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 23 Jun 2025 00:39:29 -0600 Subject: [PATCH 7/7] fix: correct property name from bookableConfigs to bookableConfig in BookableSpaceEntity and SpaceEntity --- .../src/modules/booking/entities/bookable-space.entity.ts | 2 +- libs/common/src/modules/space/entities/space.entity.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/src/modules/booking/entities/bookable-space.entity.ts b/libs/common/src/modules/booking/entities/bookable-space.entity.ts index bbe5e90..3db1165 100644 --- a/libs/common/src/modules/booking/entities/bookable-space.entity.ts +++ b/libs/common/src/modules/booking/entities/bookable-space.entity.ts @@ -19,7 +19,7 @@ export class BookableSpaceEntity extends AbstractEntity { }) public uuid: string; - @OneToOne(() => SpaceEntity, (space) => space.bookableConfigs) + @OneToOne(() => SpaceEntity, (space) => space.bookableConfig) @JoinColumn({ name: 'space_uuid' }) space: SpaceEntity; diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index d3c84e0..763ffec 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -124,7 +124,7 @@ export class SpaceEntity extends AbstractEntity { occupancyDaily: SpaceDailyOccupancyDurationEntity[]; @OneToOne(() => BookableSpaceEntity, (bookable) => bookable.space) - bookableConfigs: BookableSpaceEntity; + bookableConfig: BookableSpaceEntity; constructor(partial: Partial) { super();