implement contract booking functionality and MinIO setup

This commit is contained in:
marshal
2026-05-23 09:31:52 +03:00
parent f9a9d9923c
commit 82c6b1284e
16 changed files with 1231 additions and 40 deletions

View File

@@ -6,42 +6,154 @@ import {
HttpCode,
Param,
ParseUUIDPipe,
Patch,
Post,
Query,
UploadedFiles,
UseInterceptors,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { AnyFilesInterceptor } from "@nestjs/platform-express";
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger";
import { BookingsService } from "./bookings.service";
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")
export class BookingsController {
constructor(private readonly bookingsService: BookingsService) {}
// ── 1. Create booking (multipart/form-data) ──────────────────────────
@Post()
@ApiOperation({ summary: "Create a new freight booking" })
create(@Body() dto: CreateBookingDto) {
return this.bookingsService.create(dto);
@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 containerType=20FT and odd quantity.",
})
@ApiBody({
description:
"Booking form data. Attach files with any field name (e.g. passport, tin_certificate). " +
"File metadata is stored in the documents JSONB column.",
type: CreateBookingDto,
})
create(
@Body() dto: CreateBookingDto,
@UploadedFiles() files: Express.Multer.File[],
) {
console.log('[BookingsController] Files received:', files?.length, files?.map(f => ({ fieldname: f.fieldname, originalname: f.originalname, size: f.size, mimetype: f.mimetype })));
return this.bookingsService.create(dto, files ?? []);
}
// ── 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)" })
@ApiOperation({
summary: "List freight bookings (paginated)",
description:
"Filter by status, customerId, contractType, serviceType, tradeDirection, " +
"paymentCurrency, freightType, containerType, allowConsolidation, consolidationPaired. " +
"Sort by createdAt or priorityScore.",
})
findAll(@Query() filter: FilterBookingDto) {
return this.bookingsService.findAll(filter);
}
// ── 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" })
@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 compatible 20FT partner (same origin, destination, tradeDirection). " +
"If a partner is found, both bookings are paired. If not, the booking enters the consolidation queue.",
})
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);
}
}