feat(booking): Introduce comprehensive booking DTO, refine form validation, and enhance API docs

This commit is contained in:
ghost2023
2026-05-23 11:03:47 +03:00
parent a252b88dfb
commit 8a5652d7d9
5 changed files with 66 additions and 52 deletions

View File

@@ -13,7 +13,13 @@ import {
UseInterceptors,
} from "@nestjs/common";
import { AnyFilesInterceptor } from "@nestjs/platform-express";
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger";
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiOperation,
ApiTags,
} from "@nestjs/swagger";
import { BookingsService } from "./bookings.service";
import { CreateBookingDto } from "./dto/create-booking.dto";
@@ -23,8 +29,9 @@ import { UpdateStatusDto } from "./dto/update-status.dto";
@ApiTags("bookings")
@Controller("bookings")
@ApiBearerAuth()
export class BookingsController {
constructor(private readonly bookingsService: BookingsService) {}
constructor(private readonly bookingsService: BookingsService) { }
// ── 1. Create booking (multipart/form-data) ──────────────────────────
@Post()
@@ -46,7 +53,16 @@ export class BookingsController {
@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 })));
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 ?? []);
}
@@ -56,7 +72,8 @@ export class BookingsController {
@ApiConsumes("multipart/form-data")
@ApiOperation({
summary: "Update a draft booking",
description: "Only DRAFT bookings can be updated. New files are merged into existing documents.",
description:
"Only DRAFT bookings can be updated. New files are merged into existing documents.",
})
@ApiBody({ type: UpdateBookingDto })
update(
@@ -141,7 +158,8 @@ export class BookingsController {
@Delete(":id/consolidation")
@ApiOperation({
summary: "Remove consolidation pairing",
description: "Unpairs both bookings and returns them to PENDING_CONSOLIDATION status.",
description:
"Unpairs both bookings and returns them to PENDING_CONSOLIDATION status.",
})
removeConsolidation(@Param("id", ParseUUIDPipe) id: string) {
return this.bookingsService.removeConsolidation(id);
@@ -151,7 +169,8 @@ export class BookingsController {
@Get(":id/consolidation")
@ApiOperation({
summary: "Get consolidation details",
description: "Returns partner booking details and split billing information.",
description:
"Returns partner booking details and split billing information.",
})
getConsolidationDetails(@Param("id", ParseUUIDPipe) id: string) {
return this.bookingsService.getConsolidationDetails(id);

View File

@@ -141,9 +141,6 @@ export const BOOKING_DOCS_SETTING = {
],
};
const requiredString = (message: string) =>
z.string().trim().min(1, { message });
const fileValueSchema = z.union([
z.custom<File>(),
z.array(z.custom<File>()),
@@ -152,10 +149,10 @@ const fileValueSchema = z.union([
export const bookingFormSchema = z
.object({
contractType: z.enum(["new", "renewal", ""]),
contractType: z.enum(["new", "renewal"], "Select a contract type."),
previousContractRef: z.string(),
draftContractId: z.string(),
serviceType: z.enum(["rail", "rail_forwarding", ""]),
serviceType: z.enum(["rail", "rail_forwarding"], "Select a service type."),
firstMileEnabled: z.boolean(),
pickUpAddress: z.string(),
lastMileEnabled: z.boolean(),
@@ -163,9 +160,9 @@ export const bookingFormSchema = z
equipmentReturn: z.enum(["with_return", "without_return"]),
originYard: z.string(),
destinationYard: z.string(),
cargoType: z.enum(["container", "bulk", ""]),
cargoType: z.enum(["container", "bulk"], "Select a cargo type."),
cargoWeight: z.string(),
freightType: z.enum(["bulk", "break_bulk", ""]),
freightType: z.enum(["bulk", "break_bulk"]),
bulkCommodity: z.string(),
bulkCommodityOther: z.string(),
breakBulkType: z.string(),
@@ -191,14 +188,6 @@ export const bookingFormSchema = z
termsAccepted: z.boolean(),
})
.superRefine((data, ctx) => {
if (!data.contractType) {
ctx.addIssue({
code: "custom",
path: ["contractType"],
message: "Select a contract type.",
});
}
if (data.contractType === "new" && !data.draftContractId.trim()) {
ctx.addIssue({
code: "custom",
@@ -215,14 +204,6 @@ export const bookingFormSchema = z
});
}
if (!data.serviceType) {
ctx.addIssue({
code: "custom",
path: ["serviceType"],
message: "Select a service type.",
});
}
if (data.firstMileEnabled && !data.pickUpAddress.trim()) {
ctx.addIssue({
code: "custom",
@@ -267,14 +248,6 @@ export const bookingFormSchema = z
});
}
if (!data.cargoType) {
ctx.addIssue({
code: "custom",
path: ["cargoType"],
message: "Select a cargo type.",
});
}
if (data.cargoType === "bulk") {
if (!data.freightType) {
ctx.addIssue({
@@ -385,11 +358,9 @@ export const bookingFormSchema = z
export type BookingFormValues = z.infer<typeof bookingFormSchema>;
export const initialBookingFormValues: BookingFormValues = {
contractType: "",
export const initialBookingFormValues: Partial<BookingFormValues> = {
previousContractRef: "",
draftContractId: "",
serviceType: "",
firstMileEnabled: false,
pickUpAddress: "",
lastMileEnabled: false,
@@ -397,9 +368,7 @@ export const initialBookingFormValues: BookingFormValues = {
equipmentReturn: "with_return",
originYard: "",
destinationYard: "",
cargoType: "",
cargoWeight: "",
freightType: "",
bulkCommodity: "",
bulkCommodityOther: "",
breakBulkType: "",

View File

@@ -2,13 +2,7 @@ import type { Freight, PaginatedResponse } from "@edr/types";
import { api } from "./crud";
export interface CreateBookingPayload {
reference: string;
customerId: string;
scheduledDate: string;
totalAmount: number;
trainId?: string;
}
export type CreateBookingPayload = Freight.CreateBookingDto;
export const bookingsService = {
list: async (): Promise<PaginatedResponse<Freight.IBooking>> => {

View File

@@ -1,14 +1,14 @@
import type { Freight, PaginatedResponse } from "@edr/types";
import { api } from "../utils/api";
import { client } from "../utils/api";
export const consignmentsService = {
list: async (): Promise<PaginatedResponse<Freight.IConsignment>> => {
const { data } = await api.get("/consignments");
const { data } = await client.get("/consignments");
return data.data;
},
get: async (id: string): Promise<Freight.IConsignment> => {
const { data } = await api.get(`/consignments/${id}`);
const { data } = await client.get(`/consignments/${id}`);
return data.data;
},
};

View File

@@ -124,3 +124,35 @@ export interface IInvoice extends BaseEntity {
issuedAt: string;
dueAt: string;
}
export interface CreateBookingDto {
reference: string;
customerId: string;
trainId?: string;
scheduledDate: string;
totalAmount: number;
paymentStatus?: string;
contractType: "NEW" | "RENEWAL";
previousContractId?: string;
serviceType: "RAIL_ONLY" | "RAIL_AND_FORWARDING";
firstMileEnabled?: boolean;
firstMilePickupAddress?: string;
lastMileEnabled?: boolean;
lastMileDeliveryAddress?: string;
equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN";
originStation: string;
destinationStation: string;
cargoTotalWeightVgm: number;
freightType: "BULK" | "BREAK_BULK";
freightSubtype?: string;
isHazardous?: boolean;
isRefrigerated?: boolean;
tradeDirection: "IMPORT" | "EXPORT";
paymentCurrency: string;
allowConsolidation?: boolean;
}