mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 05:25:41 +00:00
implement contract booking functionality and MinIO setup
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateBookingDto } from "./create-booking.dto";
|
||||
|
||||
export class UpdateBookingDto extends PartialType(CreateBookingDto) {}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user