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

@@ -1,7 +1,10 @@
import { Freight } from "@edr/types";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import {
IsBoolean,
IsDateString,
IsEnum,
IsIn,
IsInt,
IsNumber,
IsOptional,
IsString,
@@ -9,25 +12,194 @@ import {
Min,
} from "class-validator";
const BOOKING_STATUSES = [
"DRAFT",
"PENDING_LINE_STAFF",
"PENDING_DIRECTOR",
"PENDING_CEO",
"APPROVED_PENDING_SIGNATURE",
"SIGNED",
"ACTIVE",
"EXPIRED",
"CANCELLED",
"PENDING_CONSOLIDATION",
"CONSOLIDATED",
] as const;
const CONTRACT_TYPES = ["NEW", "RENEWAL"] as const;
const SERVICE_TYPES = ["RAIL_ONLY", "RAIL_AND_FORWARDING"] as const;
const EQUIPMENT_RETURNS = ["WITH_RETURN", "WITHOUT_RETURN"] as const;
const FREIGHT_TYPES = ["BULK", "BREAK_BULK"] as const;
const TRADE_DIRECTIONS = ["IMPORT", "EXPORT"] as const;
const PAYMENT_CURRENCIES = ["ETB", "USD"] as const;
const PAYMENT_STATUSES = ["PENDING", "PAID", "OVERDUE", "CANCELLED", "REFUNDED"] as const;
const CONTAINER_TYPES = ["20FT", "40FT"] as const;
export {
BOOKING_STATUSES,
CONTRACT_TYPES,
SERVICE_TYPES,
EQUIPMENT_RETURNS,
FREIGHT_TYPES,
TRADE_DIRECTIONS,
PAYMENT_CURRENCIES,
PAYMENT_STATUSES,
CONTAINER_TYPES,
};
export class CreateBookingDto {
// ── core ─────────────────────────────────────────────────────────────
@ApiProperty({ description: "Unique booking reference" })
@IsString()
@Transform(({ value }) => (typeof value === "string" ? value.trim() : value))
reference!: string;
@ApiProperty({ format: "uuid" })
@IsUUID()
customerId!: string;
@ApiPropertyOptional({ format: "uuid" })
@IsOptional()
@IsUUID()
trainId?: string;
@ApiProperty({ example: "2026-06-15T00:00:00.000Z" })
@IsDateString()
scheduledDate!: string;
@ApiProperty({ minimum: 0 })
@IsNumber()
@Min(0)
@Transform(({ value }) => Number(value))
totalAmount!: number;
@ApiPropertyOptional({ enum: PAYMENT_STATUSES, default: "PENDING" })
@IsOptional()
@IsEnum(Freight.BookingStatus)
status?: Freight.BookingStatus;
@IsIn([...PAYMENT_STATUSES])
paymentStatus?: string;
// ── contract ─────────────────────────────────────────────────────────
@ApiProperty({ enum: CONTRACT_TYPES })
@IsIn([...CONTRACT_TYPES])
contractType!: string;
@ApiPropertyOptional({ format: "uuid", description: "For RENEWAL — previous contract/booking ID" })
@IsOptional()
@IsUUID()
@Transform(({ value }) => (value === "" || value == null ? undefined : value))
previousContractId?: string;
@ApiProperty({ enum: SERVICE_TYPES })
@IsIn([...SERVICE_TYPES])
serviceType!: string;
@ApiPropertyOptional({ default: false })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === "true" || value === true)
firstMileEnabled?: boolean;
@ApiPropertyOptional({ description: "Required when firstMileEnabled is true" })
@IsOptional()
@IsString()
firstMilePickupAddress?: string;
@ApiPropertyOptional({ default: false })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === "true" || value === true)
lastMileEnabled?: boolean;
@ApiPropertyOptional({ description: "Required when lastMileEnabled is true" })
@IsOptional()
@IsString()
lastMileDeliveryAddress?: string;
@ApiProperty({ enum: EQUIPMENT_RETURNS })
@IsIn([...EQUIPMENT_RETURNS])
equipmentReturn!: string;
@ApiProperty({ description: "Origin station name or code" })
@IsString()
originStation!: string;
@ApiProperty({ description: "Destination station name or code" })
@IsString()
destinationStation!: string;
@ApiProperty({ description: "Total cargo weight in VGM tons", minimum: 0 })
@IsNumber()
@Min(0)
@Transform(({ value }) => Number(value))
cargoTotalWeightVgm!: number;
@ApiProperty({ enum: FREIGHT_TYPES })
@IsIn([...FREIGHT_TYPES])
freightType!: string;
@ApiPropertyOptional({ description: "Coffee, Beans, Machinery, Ro-Ro, etc." })
@IsOptional()
@IsString()
freightSubtype?: string;
@ApiPropertyOptional({ default: false })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === "true" || value === true)
isHazardous?: boolean;
@ApiPropertyOptional({ default: false })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === "true" || value === true)
isRefrigerated?: boolean;
@ApiProperty({ enum: TRADE_DIRECTIONS })
@IsIn([...TRADE_DIRECTIONS])
tradeDirection!: string;
@ApiProperty({ enum: PAYMENT_CURRENCIES })
@IsIn([...PAYMENT_CURRENCIES])
paymentCurrency!: string;
@ApiPropertyOptional({ example: "2026-06-15" })
@IsOptional()
@IsDateString()
startDate?: string;
@ApiPropertyOptional({ example: "2027-06-15" })
@IsOptional()
@IsDateString()
endDate?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
financialTerms?: string;
// ── container ────────────────────────────────────────────────────────
@ApiProperty({ enum: CONTAINER_TYPES })
@IsIn([...CONTAINER_TYPES])
containerType!: string;
@ApiProperty({ minimum: 1 })
@IsInt()
@Min(1)
@Transform(({ value }) => Number(value))
containerQuantity!: number;
@ApiProperty({ description: "VGM per container in tons", minimum: 0 })
@IsNumber()
@Min(0)
@Transform(({ value }) => Number(value))
containerVgmPerUnit!: number;
@ApiPropertyOptional({
default: false,
description: "Auto-set to true when containerType=20FT and odd quantity. User may override.",
})
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === "true" || value === true)
allowConsolidation?: boolean;
}

View File

@@ -1,22 +1,87 @@
import { Freight } from "@edr/types";
import { Type } from "class-transformer";
import { IsEnum, IsInt, IsOptional, IsUUID, Min } from "class-validator";
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import { IsBoolean, IsIn, IsInt, IsOptional, IsString, IsUUID, Min } from "class-validator";
import {
BOOKING_STATUSES,
CONTRACT_TYPES,
CONTAINER_TYPES,
FREIGHT_TYPES,
PAYMENT_CURRENCIES,
SERVICE_TYPES,
TRADE_DIRECTIONS,
} from "./create-booking.dto";
export class FilterBookingDto {
@ApiPropertyOptional({ enum: BOOKING_STATUSES })
@IsOptional()
@IsEnum(Freight.BookingStatus)
status?: Freight.BookingStatus;
@IsIn([...BOOKING_STATUSES])
status?: string;
@ApiPropertyOptional({ format: "uuid" })
@IsOptional()
@IsUUID()
customerId?: string;
@ApiPropertyOptional({ enum: CONTRACT_TYPES })
@IsOptional()
@IsIn([...CONTRACT_TYPES])
contractType?: string;
@ApiPropertyOptional({ enum: SERVICE_TYPES })
@IsOptional()
@IsIn([...SERVICE_TYPES])
serviceType?: string;
@ApiPropertyOptional({ enum: TRADE_DIRECTIONS })
@IsOptional()
@IsIn([...TRADE_DIRECTIONS])
tradeDirection?: string;
@ApiPropertyOptional({ enum: PAYMENT_CURRENCIES })
@IsOptional()
@IsIn([...PAYMENT_CURRENCIES])
paymentCurrency?: string;
@ApiPropertyOptional({ enum: FREIGHT_TYPES })
@IsOptional()
@IsIn([...FREIGHT_TYPES])
freightType?: string;
@ApiPropertyOptional({ enum: CONTAINER_TYPES })
@IsOptional()
@IsIn([...CONTAINER_TYPES])
containerType?: string;
@ApiPropertyOptional({ description: "Filter consolidation-eligible bookings" })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === "true" || value === true)
allowConsolidation?: boolean;
@ApiPropertyOptional({ description: "Filter by consolidation partner presence (true = paired, false = unpaired)" })
@IsOptional()
@IsString()
consolidationPaired?: string;
@ApiPropertyOptional({ enum: ["createdAt", "priorityScore"], default: "createdAt" })
@IsOptional()
@IsIn(["createdAt", "priorityScore"])
sortBy?: string;
@ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "DESC" })
@IsOptional()
@IsIn(["ASC", "DESC"])
sortOrder?: "ASC" | "DESC";
@ApiPropertyOptional({ default: 1, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@ApiPropertyOptional({ default: 20, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsInt()

View File

@@ -0,0 +1,5 @@
import { PartialType } from "@nestjs/swagger";
import { CreateBookingDto } from "./create-booking.dto";
export class UpdateBookingDto extends PartialType(CreateBookingDto) {}

View File

@@ -0,0 +1,42 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsIn, IsOptional, IsString, IsUUID } from "class-validator";
const STATUS_ACTIONS = [
"SUBMIT",
"APPROVE_STAFF",
"APPROVE_DIRECTOR",
"APPROVE_CEO",
"REJECT",
"CANCEL",
"ACTIVATE",
"EXPIRE",
] as const;
export { STATUS_ACTIONS };
export class UpdateStatusDto {
@ApiProperty({
enum: STATUS_ACTIONS,
description:
"SUBMIT — send to approval queue | " +
"APPROVE_STAFF — line-staff approval | " +
"APPROVE_DIRECTOR — director approval / signature | " +
"APPROVE_CEO — CEO final signature | " +
"REJECT — reject at any pending stage | " +
"CANCEL — customer / admin cancellation | " +
"ACTIVATE — activate a signed booking | " +
"EXPIRE — mark an active booking as expired",
})
@IsIn([...STATUS_ACTIONS])
action!: string;
@ApiPropertyOptional({ format: "uuid", description: "Actor performing the action (staff/director/CEO)" })
@IsOptional()
@IsUUID()
actorId?: string;
@ApiPropertyOptional({ description: "Required for REJECT and CANCEL actions" })
@IsOptional()
@IsString()
reason?: string;
}