refactor: streamline Booking module and service by removing unused imports and consolidating space validation logic

This commit is contained in:
faris Aljohari
2025-06-18 01:49:00 -06:00
parent df59e9a4a3
commit 274cdf741f
3 changed files with 55 additions and 75 deletions

View File

@ -1,16 +1,17 @@
import { Global, Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BookableSpaceController } from './controllers'; import { BookableSpaceController } from './controllers';
import { BookableSpaceService } from './services'; import { BookableSpaceService } from './services';
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories'; import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories';
import { BookableSpaceEntity } from '@app/common/modules/booking/entities'; import { SpaceRepository } from '@app/common/modules/space';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
@Global() @Global()
@Module({ @Module({
imports: [TypeOrmModule.forFeature([BookableSpaceEntity, SpaceEntity])],
controllers: [BookableSpaceController], controllers: [BookableSpaceController],
providers: [BookableSpaceService, BookableSpaceEntityRepository], providers: [
BookableSpaceService,
BookableSpaceEntityRepository,
SpaceRepository,
],
exports: [BookableSpaceService], exports: [BookableSpaceService],
}) })
export class BookingModule {} export class BookingModule {}

View File

@ -1,7 +1,6 @@
// dtos/bookable-space.dto.ts // dtos/bookable-space.dto.ts
import { DaysEnum } from '@app/common/constants/days.enum'; import { DaysEnum } from '@app/common/constants/days.enum';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { import {
IsArray, IsArray,
IsEnum, IsEnum,
@ -9,15 +8,26 @@ import {
IsString, IsString,
IsUUID, IsUUID,
IsInt, IsInt,
ValidateNested,
ArrayMinSize, ArrayMinSize,
ArrayMaxSize,
Min,
Max, Max,
Min,
Matches, Matches,
} from 'class-validator'; } 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({ @ApiProperty({
enum: DaysEnum, enum: DaysEnum,
isArray: true, isArray: true,
@ -25,7 +35,6 @@ export class BookingSlotDto {
}) })
@IsArray() @IsArray()
@ArrayMinSize(1, { message: 'At least one day must be selected' }) @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' }) @IsEnum(DaysEnum, { each: true, message: 'Invalid day provided' })
daysAvailable: DaysEnum[]; daysAvailable: DaysEnum[];
@ -45,31 +54,9 @@ export class BookingSlotDto {
}) })
endTime: string; endTime: string;
@ApiProperty({ example: 10, minimum: 0 }) @ApiProperty({ example: 10 })
@IsInt() @IsInt()
@Min(0, { message: 'Points cannot be negative' }) @Min(0, { message: 'Points cannot be negative' })
@Max(1000, { message: 'Points cannot exceed 1000' }) @Max(1000, { message: 'Points cannot exceed 1000' })
points: number; 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[];
}

View File

@ -1,50 +1,46 @@
// services/bookable-space.service.ts
import { import {
Injectable, Injectable,
NotFoundException, NotFoundException,
ConflictException, ConflictException,
BadRequestException, BadRequestException,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { CreateBookableSpaceDto } from '../dtos';
import { Repository, In } from 'typeorm';
import { BookingSlotDto, CreateBookableSpaceDto } from '../dtos';
import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.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 { 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'; import { timeToMinutes } from '@app/common/helper/timeToMinutes';
@Injectable() @Injectable()
export class BookableSpaceService { export class BookableSpaceService {
constructor( constructor(
@InjectRepository(BookableSpaceEntity) private readonly bookableSpaceEntityRepository: BookableSpaceEntityRepository,
private bookableRepo: Repository<BookableSpaceEntity>, private readonly spaceRepository: SpaceRepository,
@InjectRepository(SpaceEntity)
private spaceRepo: Repository<SpaceEntity>,
) {} ) {}
async create(dto: CreateBookableSpaceDto): Promise<BaseResponseDto> { async create(dto: CreateBookableSpaceDto): Promise<BaseResponseDto> {
// Validate time slots first // Validate time slots first
this.validateTimeSlots(dto.slots); this.validateTimeSlot(dto.startTime, dto.endTime);
// Validate spaces exist // fetch spaces exist
const spaces = await this.validateSpacesExist(dto.spaceUuids); const spaces = await this.getSpacesOrFindMissing(dto.spaceUuids);
// Validate no duplicate bookable configurations // Validate no duplicate bookable configurations
await this.validateNoDuplicateBookableConfigs(dto.spaceUuids); await this.validateNoDuplicateBookableConfigs(dto.spaceUuids);
// Create and save bookable spaces // 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[], spaceUuids: string[],
): Promise<SpaceEntity[]> { ): Promise<SpaceEntity[]> {
const spaces = await this.spaceRepo.find({ const spaces = await this.spaceRepository.find({
where: { uuid: In(spaceUuids) }, where: { uuid: In(spaceUuids) },
}); });
@ -67,7 +63,7 @@ export class BookableSpaceService {
private async validateNoDuplicateBookableConfigs( private async validateNoDuplicateBookableConfigs(
spaceUuids: string[], spaceUuids: string[],
): Promise<void> { ): Promise<void> {
const existingBookables = await this.bookableRepo.find({ const existingBookables = await this.bookableSpaceEntityRepository.find({
where: { space: { uuid: In(spaceUuids) } }, where: { space: { uuid: In(spaceUuids) } },
relations: ['space'], 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 { private validateTimeSlot(startTime: string, endTime: string): void {
slots.forEach((slot) => { const start = timeToMinutes(startTime);
const start = timeToMinutes(slot.startTime); const end = timeToMinutes(endTime);
const end = timeToMinutes(slot.endTime);
if (start >= end) { if (start >= end) {
throw new BadRequestException( throw new BadRequestException(
`End time must be after start time for slot: ${slot.startTime}-${slot.endTime}`, `End time must be after start time for slot: ${startTime}-${endTime}`,
); );
} }
});
} }
/** /**
@ -103,22 +97,20 @@ export class BookableSpaceService {
*/ */
private async createBookableSpaces( private async createBookableSpaces(
spaces: SpaceEntity[], spaces: SpaceEntity[],
slots: BookingSlotDto[], dto: CreateBookableSpaceDto,
): Promise<BaseResponseDto> { ): Promise<BaseResponseDto> {
try { try {
const entries = spaces.flatMap((space) => const entries = spaces.map((space) =>
slots.map((slot) => this.bookableSpaceEntityRepository.create({
this.bookableRepo.create({
space, space,
daysAvailable: slot.daysAvailable, daysAvailable: dto.daysAvailable,
startTime: slot.startTime, startTime: dto.startTime,
endTime: slot.endTime, endTime: dto.endTime,
points: slot.points, points: dto.points,
}), }),
),
); );
const data = await this.bookableRepo.save(entries); const data = await this.bookableSpaceEntityRepository.save(entries);
return new SuccessResponseDto({ return new SuccessResponseDto({
data, data,
message: 'Successfully added new bookable spaces', message: 'Successfully added new bookable spaces',