From 6d49c38f48af324e7330cb8facff2529529a4b42 Mon Sep 17 00:00:00 2001 From: marshal Date: Thu, 28 May 2026 10:31:31 +0300 Subject: [PATCH] allow customers booking --- .../modules/bookings/bookings.controller.ts | 5 ++- .../src/modules/bookings/bookings.module.ts | 3 +- .../modules/bookings/bookings.repository.ts | 12 +++++++ .../src/modules/bookings/bookings.service.ts | 32 +++++++++++++++++++ .../bookings/dto/create-booking.dto.ts | 20 ++++-------- 5 files changed, 56 insertions(+), 16 deletions(-) diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts index 13d54d8fb..f6a04fc30 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts @@ -9,6 +9,7 @@ import { Patch, Post, Query, + Request, UploadedFiles, UseInterceptors, } from "@nestjs/common"; @@ -52,6 +53,7 @@ export class BookingsController { create( @Body() dto: CreateBookingDto, @UploadedFiles() files: Express.Multer.File[], + @Request() req: any, ) { console.log( "[BookingsController] Files received:", @@ -63,7 +65,8 @@ export class BookingsController { mimetype: f.mimetype, })), ); - return this.bookingsService.create(dto, files ?? []); + const userId: string | undefined = req.user?.id ?? req.user?.sub; + return this.bookingsService.create(dto, files ?? [], userId); } // ── 2. Update draft booking (multipart/form-data) ───────────────────── diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts index d7c9c2506..9c1bb0a8c 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts @@ -1,6 +1,7 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; +import { CustomersModule } from "../customers/customers.module"; import { FilesModule } from "../files/files.module"; import { MinioModule } from "../minio/minio.module"; import { BookingsController } from "./bookings.controller"; @@ -9,7 +10,7 @@ import { BookingsService } from "./bookings.service"; import { Booking } from "./entities/booking.entity"; @Module({ - imports: [TypeOrmModule.forFeature([Booking]), FilesModule, MinioModule], + imports: [TypeOrmModule.forFeature([Booking]), FilesModule, MinioModule, CustomersModule], controllers: [BookingsController], providers: [BookingsService, BookingsRepository], exports: [BookingsService], diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts index 6cf3b2149..d91adccec 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts @@ -20,6 +20,18 @@ export class BookingsRepository extends BaseRepository { return this.repository.findOne({ where: { reference } }); } + /** Count bookings created in a specific year. */ + async countByYear(year: number): Promise { + const startDate = new Date(year, 0, 1); + const endDate = new Date(year + 1, 0, 1); + + return this.repository + .createQueryBuilder("booking") + .where("booking.created_at >= :startDate", { startDate }) + .andWhere("booking.created_at < :endDate", { endDate }) + .getCount(); + } + /** Find a booking by reference with associated files (polymorphic join). */ async findByReferenceWithFiles(reference: string): Promise { const booking = await this.repository diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index b04a016cb..0287cecc4 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -6,6 +6,7 @@ import { } from "@nestjs/common"; import { IsNull, Not } from "typeorm"; +import { CustomersService } from "../customers/customers.service"; import { FilesService } from "../files/files.service"; import { MinioService } from "../minio/minio.service"; import { BookingsRepository } from "./bookings.repository"; @@ -32,10 +33,23 @@ export class BookingsService { private readonly bookingsRepository: BookingsRepository, private readonly filesService: FilesService, private readonly minioService: MinioService, + private readonly customersService: CustomersService, ) {} // ── helpers ────────────────────────────────────────────────────────── + /** Generate a unique booking reference number. */ + private async generateReference(): Promise { + const year = new Date().getFullYear(); + const prefix = `BK-${year}`; + + // Get the count of bookings created this year + const count = await this.bookingsRepository.countByYear(year); + const sequenceNumber = String(count + 1).padStart(6, '0'); + + return `${prefix}-${sequenceNumber}`; + } + /** Resolve auto-consolidation flag. */ private resolveConsolidation( containers: Array<{ type: string; qty: number }> | undefined | null, @@ -107,9 +121,23 @@ export class BookingsService { async create( dto: CreateBookingDto, files: Express.Multer.File[], + userId?: string, ): Promise<{ booking: Booking; warnings: string[] }> { const warnings: string[] = []; + // Resolve customerId: use provided value (admin) or look up by IAM userId + let customerId = dto.customerId; + if (!customerId) { + if (!userId) { + throw new BadRequestException('customerId is required or must be resolvable from auth token'); + } + const customer = await this.customersService.findByUserId(userId); + customerId = customer.id; + } + + // Generate reference if not provided + const reference = dto.reference || await this.generateReference(); + const allowConsolidation = this.resolveConsolidation( dto.containers, dto.allowConsolidation, @@ -131,6 +159,10 @@ export class BookingsService { const booking = await this.bookingsRepository.create({ ...dto, + reference, + customerId, + totalAmount: 0, + paymentStatus: "PENDING", scheduledDate: new Date(dto.scheduledDate), startDate: dto.startDate ? new Date(dto.startDate) : undefined, endDate: dto.endDate ? new Date(dto.endDate) : undefined, diff --git a/apps/edr-freight-api/src/modules/bookings/dto/create-booking.dto.ts b/apps/edr-freight-api/src/modules/bookings/dto/create-booking.dto.ts index 0f29adc1a..259c8eafe 100644 --- a/apps/edr-freight-api/src/modules/bookings/dto/create-booking.dto.ts +++ b/apps/edr-freight-api/src/modules/bookings/dto/create-booking.dto.ts @@ -69,14 +69,16 @@ export class ContainerItem { export class CreateBookingDto { // ── core ───────────────────────────────────────────────────────────── - @ApiProperty({ description: "Unique booking reference" }) + @ApiPropertyOptional({ description: "Unique booking reference (auto-generated if not provided)" }) + @IsOptional() @IsString() @Transform(({ value }) => (typeof value === "string" ? value.trim() : value)) - reference!: string; + reference?: string; - @ApiProperty({ format: "uuid" }) + @ApiPropertyOptional({ format: "uuid", description: "Admin only: target customer. Omit to resolve from auth token." }) + @IsOptional() @IsUUID() - customerId!: string; + customerId?: string; @ApiPropertyOptional({ format: "uuid" }) @IsOptional() @@ -87,16 +89,6 @@ export class CreateBookingDto { @IsDateString() scheduledDate!: string; - @ApiProperty({ minimum: 0 }) - @IsNumber() - @Min(0) - @Transform(({ value }) => Number(value)) - totalAmount!: number; - - @ApiPropertyOptional({ enum: PAYMENT_STATUSES, default: "PENDING" }) - @IsOptional() - @IsIn([...PAYMENT_STATUSES]) - paymentStatus?: string; // ── contract ───────────────────────────────────────────────────────── @ApiProperty({ enum: CONTRACT_TYPES })