modules 050219

This commit is contained in:
2026-05-09 17:29:22 +03:30
parent c8a3e4ccb7
commit 03ac02bee9
16 changed files with 726 additions and 0 deletions
@@ -0,0 +1,52 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
} from '@nestjs/common';
import { BookingEntity, BookingStatus } from '../../entities/booking.entity';
import { BookingsService } from './bookings.service';
import { CreateBookingDto } from './dto/create-booking.dto';
import { UpdateBookingDto } from './dto/update-booking.dto';
@Controller('bookings')
export class BookingsController {
constructor(private readonly service: BookingsService) {}
@Get()
findAll(
@Query('customerId') customerId?: string,
@Query('businessId') businessId?: string,
@Query('status') status?: BookingStatus,
): Promise<BookingEntity[]> {
return this.service.findAll({ customerId, businessId, status });
}
@Get(':id')
findOne(@Param('id') id: string): Promise<BookingEntity> {
return this.service.findOne(id);
}
@Post()
create(@Body() payload: CreateBookingDto): Promise<BookingEntity> {
return this.service.create(payload);
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() payload: UpdateBookingDto,
): Promise<BookingEntity> {
return this.service.update(id, payload);
}
@Delete(':id')
async remove(@Param('id') id: string): Promise<{ success: true }> {
await this.service.remove(id);
return { success: true };
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BookingEntity } from '../../entities/booking.entity';
import { BusinessServiceEntity } from '../../entities/business-service.entity';
import { BusinessEntity } from '../../entities/business.entity';
import { UserEntity } from '../../entities/user.entity';
import { BookingsController } from './bookings.controller';
import { BookingsService } from './bookings.service';
@Module({
imports: [
TypeOrmModule.forFeature([
BookingEntity,
UserEntity,
BusinessEntity,
BusinessServiceEntity,
]),
],
controllers: [BookingsController],
providers: [BookingsService],
})
export class BookingsModule {}
+171
View File
@@ -0,0 +1,171 @@
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<BookingEntity>,
@InjectRepository(UserEntity)
private readonly userRepo: Repository<UserEntity>,
@InjectRepository(BusinessEntity)
private readonly businessRepo: Repository<BusinessEntity>,
@InjectRepository(BusinessServiceEntity)
private readonly businessServiceRepo: Repository<BusinessServiceEntity>,
) {
const raw = this.config.get<string | undefined>('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<BookingEntity[]> {
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<BookingEntity> {
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<BookingEntity> {
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<BookingEntity> {
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<void> {
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`,
);
}
}
}
@@ -0,0 +1,8 @@
export class CreateBookingDto {
customerId!: string;
businessId!: string;
businessServiceId!: string;
/** ISO 8601 datetime string */
scheduledAt!: string;
note?: string | null;
}
@@ -0,0 +1,7 @@
import { BookingStatus } from '../../../entities/booking.entity';
export class UpdateBookingDto {
scheduledAt?: string;
status?: BookingStatus;
note?: string | null;
}