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, UseInterceptors,
} from "@nestjs/common"; } from "@nestjs/common";
import { AnyFilesInterceptor } from "@nestjs/platform-express"; 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 { BookingsService } from "./bookings.service";
import { CreateBookingDto } from "./dto/create-booking.dto"; import { CreateBookingDto } from "./dto/create-booking.dto";
@@ -23,8 +29,9 @@ import { UpdateStatusDto } from "./dto/update-status.dto";
@ApiTags("bookings") @ApiTags("bookings")
@Controller("bookings") @Controller("bookings")
@ApiBearerAuth()
export class BookingsController { export class BookingsController {
constructor(private readonly bookingsService: BookingsService) {} constructor(private readonly bookingsService: BookingsService) { }
// ── 1. Create booking (multipart/form-data) ────────────────────────── // ── 1. Create booking (multipart/form-data) ──────────────────────────
@Post() @Post()
@@ -46,7 +53,16 @@ export class BookingsController {
@Body() dto: CreateBookingDto, @Body() dto: CreateBookingDto,
@UploadedFiles() files: Express.Multer.File[], @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 ?? []); return this.bookingsService.create(dto, files ?? []);
} }
@@ -56,7 +72,8 @@ export class BookingsController {
@ApiConsumes("multipart/form-data") @ApiConsumes("multipart/form-data")
@ApiOperation({ @ApiOperation({
summary: "Update a draft booking", 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 }) @ApiBody({ type: UpdateBookingDto })
update( update(
@@ -141,7 +158,8 @@ export class BookingsController {
@Delete(":id/consolidation") @Delete(":id/consolidation")
@ApiOperation({ @ApiOperation({
summary: "Remove consolidation pairing", 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) { removeConsolidation(@Param("id", ParseUUIDPipe) id: string) {
return this.bookingsService.removeConsolidation(id); return this.bookingsService.removeConsolidation(id);
@@ -151,7 +169,8 @@ export class BookingsController {
@Get(":id/consolidation") @Get(":id/consolidation")
@ApiOperation({ @ApiOperation({
summary: "Get consolidation details", 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) { getConsolidationDetails(@Param("id", ParseUUIDPipe) id: string) {
return this.bookingsService.getConsolidationDetails(id); 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([ const fileValueSchema = z.union([
z.custom<File>(), z.custom<File>(),
z.array(z.custom<File>()), z.array(z.custom<File>()),
@@ -152,10 +149,10 @@ const fileValueSchema = z.union([
export const bookingFormSchema = z export const bookingFormSchema = z
.object({ .object({
contractType: z.enum(["new", "renewal", ""]), contractType: z.enum(["new", "renewal"], "Select a contract type."),
previousContractRef: z.string(), previousContractRef: z.string(),
draftContractId: z.string(), draftContractId: z.string(),
serviceType: z.enum(["rail", "rail_forwarding", ""]), serviceType: z.enum(["rail", "rail_forwarding"], "Select a service type."),
firstMileEnabled: z.boolean(), firstMileEnabled: z.boolean(),
pickUpAddress: z.string(), pickUpAddress: z.string(),
lastMileEnabled: z.boolean(), lastMileEnabled: z.boolean(),
@@ -163,9 +160,9 @@ export const bookingFormSchema = z
equipmentReturn: z.enum(["with_return", "without_return"]), equipmentReturn: z.enum(["with_return", "without_return"]),
originYard: z.string(), originYard: z.string(),
destinationYard: z.string(), destinationYard: z.string(),
cargoType: z.enum(["container", "bulk", ""]), cargoType: z.enum(["container", "bulk"], "Select a cargo type."),
cargoWeight: z.string(), cargoWeight: z.string(),
freightType: z.enum(["bulk", "break_bulk", ""]), freightType: z.enum(["bulk", "break_bulk"]),
bulkCommodity: z.string(), bulkCommodity: z.string(),
bulkCommodityOther: z.string(), bulkCommodityOther: z.string(),
breakBulkType: z.string(), breakBulkType: z.string(),
@@ -191,14 +188,6 @@ export const bookingFormSchema = z
termsAccepted: z.boolean(), termsAccepted: z.boolean(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (!data.contractType) {
ctx.addIssue({
code: "custom",
path: ["contractType"],
message: "Select a contract type.",
});
}
if (data.contractType === "new" && !data.draftContractId.trim()) { if (data.contractType === "new" && !data.draftContractId.trim()) {
ctx.addIssue({ ctx.addIssue({
code: "custom", 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()) { if (data.firstMileEnabled && !data.pickUpAddress.trim()) {
ctx.addIssue({ ctx.addIssue({
code: "custom", 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.cargoType === "bulk") {
if (!data.freightType) { if (!data.freightType) {
ctx.addIssue({ ctx.addIssue({
@@ -385,11 +358,9 @@ export const bookingFormSchema = z
export type BookingFormValues = z.infer<typeof bookingFormSchema>; export type BookingFormValues = z.infer<typeof bookingFormSchema>;
export const initialBookingFormValues: BookingFormValues = { export const initialBookingFormValues: Partial<BookingFormValues> = {
contractType: "",
previousContractRef: "", previousContractRef: "",
draftContractId: "", draftContractId: "",
serviceType: "",
firstMileEnabled: false, firstMileEnabled: false,
pickUpAddress: "", pickUpAddress: "",
lastMileEnabled: false, lastMileEnabled: false,
@@ -397,9 +368,7 @@ export const initialBookingFormValues: BookingFormValues = {
equipmentReturn: "with_return", equipmentReturn: "with_return",
originYard: "", originYard: "",
destinationYard: "", destinationYard: "",
cargoType: "",
cargoWeight: "", cargoWeight: "",
freightType: "",
bulkCommodity: "", bulkCommodity: "",
bulkCommodityOther: "", bulkCommodityOther: "",
breakBulkType: "", breakBulkType: "",

View File

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

View File

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

View File

@@ -124,3 +124,35 @@ export interface IInvoice extends BaseEntity {
issuedAt: string; issuedAt: string;
dueAt: 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;
}