import { BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { BookingEntity, BookingStatus } from '../../entities/booking.entity'; import { BusinessServiceEntity } from '../../entities/business-service.entity'; import { BusinessEntity } from '../../entities/business.entity'; import { UserEntity } from '../../entities/user.entity'; import { CreateBookingDto } from './dto/create-booking.dto'; import { UpdateBookingDto } from './dto/update-booking.dto'; type FindAllFilters = { customerId?: string; businessId?: string; status?: BookingStatus; }; @Injectable() export class BookingsService { private readonly minLeadMinutes: number; constructor( private readonly config: ConfigService, @InjectRepository(BookingEntity) private readonly repo: Repository, @InjectRepository(UserEntity) private readonly userRepo: Repository, @InjectRepository(BusinessEntity) private readonly businessRepo: Repository, @InjectRepository(BusinessServiceEntity) private readonly businessServiceRepo: Repository, ) { const raw = this.config.get('BOOKING_MIN_LEAD_MINUTES'); const parsed = raw !== undefined ? Number.parseInt(String(raw), 10) : NaN; this.minLeadMinutes = Number.isFinite(parsed) && parsed >= 0 ? parsed : 60; } async findAll(filters: FindAllFilters): Promise { const qb = this.repo .createQueryBuilder('b') .leftJoinAndSelect('b.customer', 'customer') .leftJoinAndSelect('b.business', 'business') .leftJoinAndSelect('b.businessService', 'businessService') .leftJoinAndSelect('businessService.serviceTemplate', 'template') .orderBy('b.scheduledAt', 'ASC'); if (filters.customerId) { qb.andWhere('b.customerId = :customerId', { customerId: filters.customerId, }); } if (filters.businessId) { qb.andWhere('b.businessId = :businessId', { businessId: filters.businessId, }); } if (filters.status) { qb.andWhere('b.status = :status', { status: filters.status }); } return qb.getMany(); } async findOne(id: string): Promise { const row = await this.repo.findOne({ where: { id }, relations: { customer: true, business: true, businessService: { serviceTemplate: true }, }, }); if (!row) { throw new NotFoundException('Booking not found'); } return row; } async create(dto: CreateBookingDto): Promise { const customer = await this.userRepo.findOne({ where: { id: dto.customerId }, }); if (!customer) { throw new NotFoundException('User not found'); } const business = await this.businessRepo.findOne({ where: { id: dto.businessId }, }); if (!business) { throw new NotFoundException('Business not found'); } const businessService = await this.businessServiceRepo.findOne({ where: { id: dto.businessServiceId }, relations: { business: true }, }); if (!businessService) { throw new NotFoundException('Business service not found'); } if (businessService.businessId !== dto.businessId) { throw new BadRequestException( 'businessService does not belong to the given business', ); } const scheduledAt = this.parseScheduledAt(dto.scheduledAt); this.assertLeadTime(scheduledAt); const entity = this.repo.create({ customerId: dto.customerId, businessId: dto.businessId, businessServiceId: dto.businessServiceId, scheduledAt, note: dto.note ?? null, status: BookingStatus.PENDING, }); return this.repo.save(entity); } async update(id: string, dto: UpdateBookingDto): Promise { const row = await this.findOne(id); if (dto.scheduledAt !== undefined) { const scheduledAt = this.parseScheduledAt(dto.scheduledAt); this.assertLeadTime(scheduledAt); row.scheduledAt = scheduledAt; } if (dto.status !== undefined) { row.status = dto.status; } if (dto.note !== undefined) { row.note = dto.note; } return this.repo.save(row); } async remove(id: string): Promise { const result = await this.repo.delete(id); if (!result.affected) { throw new NotFoundException('Booking not found'); } } private parseScheduledAt(raw: string): Date { const d = new Date(raw); if (Number.isNaN(d.getTime())) { throw new BadRequestException('scheduledAt must be a valid ISO datetime'); } return d; } private assertLeadTime(scheduledAt: Date): void { const minMs = this.minLeadMinutes * 60 * 1000; const earliest = Date.now() + minMs; if (scheduledAt.getTime() < earliest) { throw new BadRequestException( `scheduledAt must be at least ${this.minLeadMinutes} minute(s) after the current time`, ); } } }