Files
backend/src/booking/services/booking.service.ts
2025-07-17 15:22:20 +04:00

219 lines
6.9 KiB
TypeScript

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<SpaceEntity> {
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);
}
}