import { Body, Controller, Delete, Get, HttpCode, Param, ParseUUIDPipe, Patch, Post, Query, Request, UploadedFiles, UseInterceptors, } from "@nestjs/common"; import { AnyFilesInterceptor } from "@nestjs/platform-express"; import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOkResponse, ApiOperation, ApiTags, } from "@nestjs/swagger"; import { BookingReferenceDataService } from "./booking-reference-data.service"; import { BookingsService } from "./bookings.service"; import { BookingReferenceDataDto } from "./dto/booking-reference-data.dto"; import { CreateBookingDto } from "./dto/create-booking.dto"; import { FilterBookingDto } from "./dto/filter-booking.dto"; import { UpdateBookingDto } from "./dto/update-booking.dto"; import { UpdateStatusDto } from "./dto/update-status.dto"; @ApiTags("bookings") @Controller("bookings") @ApiBearerAuth() export class BookingsController { constructor( private readonly bookingsService: BookingsService, private readonly bookingReferenceDataService: BookingReferenceDataService, ) {} // ── 1. Create booking (multipart/form-data) ────────────────────────── @Post() @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Create a new freight booking", description: "Accepts all booking fields as form fields + dynamic file keys (e.g. passport, license). " + "Auto-enables consolidation when container quantity does not fill a whole wagon; attempts partner match or PENDING_CONSOLIDATION.", }) @ApiBody({ description: "Booking form data. Attach files with any field name (e.g. passport, tin_certificate). " + "Each uploaded file is saved as a row in the files table (resource=bookings).", type: CreateBookingDto, }) create( @Body() dto: CreateBookingDto, @UploadedFiles() files: Express.Multer.File[], @Request() req: any, ) { console.log( "[BookingsController] Files received:", files?.length, files?.map((f) => ({ fieldname: f.fieldname, originalname: f.originalname, size: f.size, mimetype: f.mimetype, })), ); const userId: string | undefined = req.user?.id ?? req.user?.sub; return this.bookingsService.create(dto, files ?? [], userId); } // ── 2. Update draft booking (multipart/form-data) ───────────────────── @Patch(":id") @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Update a draft booking", description: "Only DRAFT bookings can be updated. New files are merged into existing documents.", }) @ApiBody({ type: UpdateBookingDto }) update( @Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateBookingDto, @UploadedFiles() files: Express.Multer.File[], ) { return this.bookingsService.update(id, dto, files ?? []); } // ── 3. List bookings (paginated + filtered) ─────────────────────────── @Get() @ApiOperation({ summary: "List freight bookings (paginated)", description: "Filter by status, customerId, contractType, serviceTypeId, cargoTypeId, tradeDirection, " + "paymentCurrency, allowConsolidation, consolidationPaired. " + "Sort by createdAt or priorityScore.", }) findAll(@Query() filter: FilterBookingDto) { return this.bookingsService.findAll(filter); } // ── Booking form catalog (must be before :id) ───────────────────────── @Get("reference-data") @ApiOperation({ summary: "Booking form catalog", description: "Returns yards, container types (grouped by size), service types, shipping lines, " + "and hierarchical cargo types for the booking UI in a single payload.", }) @ApiOkResponse({ type: BookingReferenceDataDto }) getReferenceData(): Promise { return this.bookingReferenceDataService.getReferenceData(); } // ── 5. Lookup by reference (must be before :id to avoid conflict) ───── @Get("by-reference/:reference") @ApiOperation({ summary: "Get a freight booking by reference number", description: "Lookup booking by its human-readable reference string.", }) findByReference(@Param("reference") reference: string) { return this.bookingsService.findByReference(reference); } // ── 4. Get single booking by ID ─────────────────────────────────────── @Get(":id") @ApiOperation({ summary: "Get a freight booking by ID" }) findOne(@Param("id", ParseUUIDPipe) id: string) { return this.bookingsService.findById(id); } // ── 6. Soft-delete (DRAFT only) ─────────────────────────────────────── @Delete(":id") @HttpCode(204) @ApiOperation({ summary: "Soft-delete a freight booking", description: "Only DRAFT bookings can be deleted.", }) remove(@Param("id", ParseUUIDPipe) id: string) { return this.bookingsService.remove(id); } // ── 7. Unified status transition ────────────────────────────────────── @Patch(":id/status") @ApiOperation({ summary: "Transition booking status", description: "Unified endpoint for all status transitions. Actions: " + "SUBMIT, APPROVE_STAFF, APPROVE_DIRECTOR, APPROVE_CEO, REJECT, CANCEL, ACTIVATE, EXPIRE. " + "Approval routing: Standard → LINE_STAFF → DIRECTOR → SIGNED. " + "Bulk/high-volume → DIRECTOR → CEO → SIGNED.", }) updateStatus( @Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateStatusDto, ) { return this.bookingsService.updateStatus(id, dto); } // ── 8. Request or auto-pair consolidation ───────────────────────────── @Post(":id/consolidation") @ApiOperation({ summary: "Request freight consolidation", description: "Searches for a partner whose container quantity complements yours to fill whole wagon(s) " + "(same route, same container type). Pairs on match or sets PENDING_CONSOLIDATION with a status message.", }) requestConsolidation(@Param("id", ParseUUIDPipe) id: string) { return this.bookingsService.requestConsolidation(id); } // ── 9. Remove consolidation pairing ─────────────────────────────────── @Delete(":id/consolidation") @ApiOperation({ summary: "Remove consolidation pairing", description: "Unpairs both bookings and returns them to PENDING_CONSOLIDATION status.", }) removeConsolidation(@Param("id", ParseUUIDPipe) id: string) { return this.bookingsService.removeConsolidation(id); } // ── 10. Get consolidation details ───────────────────────────────────── @Get(":id/consolidation") @ApiOperation({ summary: "Get consolidation details", description: "Returns partner booking details and split billing information.", }) getConsolidationDetails(@Param("id", ParseUUIDPipe) id: string) { return this.bookingsService.getConsolidationDetails(id); } }