mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
358 lines
10 KiB
TypeScript
358 lines
10 KiB
TypeScript
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
import { Transform, Type } from 'class-transformer';
|
|
import {
|
|
ArrayMinSize,
|
|
IsArray,
|
|
IsBoolean,
|
|
IsDateString,
|
|
IsIn,
|
|
IsInt,
|
|
IsNumber,
|
|
IsOptional,
|
|
IsString,
|
|
IsUUID,
|
|
Max,
|
|
MaxLength,
|
|
Min,
|
|
Validate,
|
|
ValidateIf,
|
|
ValidateNested,
|
|
} from 'class-validator';
|
|
import { BOOKING_STATUSES, BOOKING_TYPES, FREIGHT_TYPES } from '../entities/booking.entity';
|
|
import { BookingFreightShapeConstraint } from './validators/booking-freight.validator';
|
|
|
|
const CONTRACT_TYPES = ['NEW', 'RENEWAL'] as const;
|
|
const EQUIPMENT_RETURNS = ['WITH_RETURN', 'WITHOUT_RETURN', 'NA'] as const;
|
|
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'DOMESTIC'] as const;
|
|
const PAYMENT_CURRENCIES = ['ETB', 'USD'] as const;
|
|
|
|
export {
|
|
BOOKING_STATUSES,
|
|
BOOKING_TYPES,
|
|
CONTRACT_TYPES,
|
|
EQUIPMENT_RETURNS,
|
|
FREIGHT_TYPES,
|
|
TRADE_DIRECTIONS,
|
|
PAYMENT_CURRENCIES,
|
|
};
|
|
|
|
export class CreateBookingContainerDto {
|
|
@ApiProperty({ format: 'uuid', description: 'FK to container_types.id' })
|
|
@IsUUID()
|
|
containerTypeId!: string;
|
|
|
|
@ApiProperty({ description: 'Quantity of containers', minimum: 1 })
|
|
@IsInt()
|
|
@Min(1)
|
|
@Transform(({ value }) => Number(value))
|
|
quantity!: number;
|
|
|
|
@ApiProperty({ description: 'VGM per container in tons', minimum: 0 })
|
|
@IsNumber()
|
|
@Min(0)
|
|
@Transform(({ value }) => Number(value))
|
|
vgmPerUnitTons!: number;
|
|
}
|
|
|
|
/**
|
|
* A contracted route (lane) of a general contract — a pure origin→destination
|
|
* pair the contract covers. Routes carry NO quantity; the contract draws from a
|
|
* single shared pool (the container quantities / bulk total on the booking). An
|
|
* order picks one lane (for scheduling + road billing) and draws from that pool.
|
|
*/
|
|
export class CreateContractRouteDto {
|
|
@ApiProperty({ format: 'uuid', description: 'FK to yards.id (origin)' })
|
|
@IsUUID()
|
|
originYardId!: string;
|
|
|
|
@ApiProperty({ format: 'uuid', description: 'FK to yards.id (destination)' })
|
|
@IsUUID()
|
|
destinationYardId!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Road distance (km) for this route; used to bill road orders.',
|
|
minimum: 0,
|
|
})
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(0)
|
|
@Transform(({ value }) =>
|
|
value === undefined || value === null || value === '' ? undefined : Number(value),
|
|
)
|
|
km?: number;
|
|
}
|
|
|
|
export class CreateBookingDto {
|
|
/** Class-level freight shape check (not a request field). */
|
|
@Validate(BookingFreightShapeConstraint)
|
|
freightShapeValidation?: boolean;
|
|
@ApiPropertyOptional({ description: 'Unique booking reference (auto-generated if omitted)' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
|
|
reference?: string;
|
|
|
|
// @ApiPropertyOptional({ format: 'uuid', description: 'Admin only: target customer (legacy)' })
|
|
// @IsOptional()
|
|
// @IsUUID()
|
|
// customerId?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Staff only: government booking flag' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Transform(({ value }) => value === 'true' || value === true)
|
|
isGovernment?: boolean;
|
|
|
|
/** @deprecated Government bookings now bill to a real government company. */
|
|
@ApiPropertyOptional({ description: 'Deprecated: free-text institution (superseded by companyId)' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
|
|
governmentInstitution?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'uuid',
|
|
description:
|
|
'Target company. Required for staff/government bookings; resolved from the auth token for customer self-bookings.',
|
|
})
|
|
@IsOptional()
|
|
@IsUUID()
|
|
companyId?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'uuid',
|
|
description:
|
|
'Explicit company profile (importer/exporter). Required for government bookings; commercial bookings auto-resolve from trade direction.',
|
|
})
|
|
@IsOptional()
|
|
@IsUUID()
|
|
companyProfileId?: string;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
trainId?: string;
|
|
|
|
/**
|
|
* Staff-only manual pin to a specific train. Customers omit this — they pick a
|
|
* DAY via {@link scheduledDate} and the batch engine assigns a train within
|
|
* that (route, day) pool. When provided, the schedule must be OPEN and on the
|
|
* booking route.
|
|
*/
|
|
@ApiPropertyOptional({
|
|
format: 'uuid',
|
|
description: 'Staff only: pin to a specific train schedule. Customers omit this.',
|
|
})
|
|
@IsOptional()
|
|
@IsUUID()
|
|
trainScheduleId?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
enum: BOOKING_TYPES,
|
|
default: 'ONE_TIME',
|
|
description:
|
|
'ONE_TIME (default) for a normal booking; GENERAL_CONTRACT for an umbrella contract drawn down by orders.',
|
|
})
|
|
@IsOptional()
|
|
@IsIn([...BOOKING_TYPES])
|
|
bookingType?: string;
|
|
|
|
/**
|
|
* The BINDING shipment day (the pool day key), validated against open train
|
|
* departures. Set later at the operation-request step — NOT at booking
|
|
* creation. Optional here; staff may still pin it directly.
|
|
*/
|
|
@ApiPropertyOptional({ example: '2026-06-15T00:00:00.000Z' })
|
|
@IsOptional()
|
|
@IsDateString()
|
|
scheduledDate?: string;
|
|
|
|
/**
|
|
* Non-binding shipment-date estimate captured in the booking wizard. Purely
|
|
* informational — NOT validated against train departures.
|
|
*/
|
|
@ApiPropertyOptional({ example: '2026-06-15T00:00:00.000Z' })
|
|
@IsOptional()
|
|
@IsDateString()
|
|
estimatedShipmentDate?: string;
|
|
|
|
@ApiProperty({ enum: CONTRACT_TYPES })
|
|
@IsIn([...CONTRACT_TYPES])
|
|
contractType!: string;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
@Transform(({ value }) => (value === '' || value == null ? undefined : value))
|
|
previousContractId?: string;
|
|
|
|
@ApiProperty({ format: 'uuid', description: 'FK to service_types.id' })
|
|
@IsUUID()
|
|
serviceTypeId!: string;
|
|
|
|
@ApiPropertyOptional()
|
|
@IsOptional()
|
|
@IsString()
|
|
firstMilePickupAddress?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'First-mile pickup latitude (-90..90)' })
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(-90)
|
|
@Max(90)
|
|
@Transform(({ value }) => (value == null || value === '' ? undefined : Number(value)))
|
|
firstMilePickupLat?: number;
|
|
|
|
@ApiPropertyOptional({ description: 'First-mile pickup longitude (-180..180)' })
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(-180)
|
|
@Max(180)
|
|
@Transform(({ value }) => (value == null || value === '' ? undefined : Number(value)))
|
|
firstMilePickupLng?: number;
|
|
|
|
@ApiPropertyOptional()
|
|
@IsOptional()
|
|
@IsString()
|
|
lastMileDeliveryAddress?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Last-mile delivery latitude (-90..90)' })
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(-90)
|
|
@Max(90)
|
|
@Transform(({ value }) => (value == null || value === '' ? undefined : Number(value)))
|
|
lastMileDeliveryLat?: number;
|
|
|
|
@ApiPropertyOptional({ description: 'Last-mile delivery longitude (-180..180)' })
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(-180)
|
|
@Max(180)
|
|
@Transform(({ value }) => (value == null || value === '' ? undefined : Number(value)))
|
|
lastMileDeliveryLng?: number;
|
|
|
|
@ApiPropertyOptional({ description: 'Whether EDR handles customs clearance' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Transform(({ value }) => value === 'true' || value === true)
|
|
customsClearingEnabled?: boolean;
|
|
|
|
@ApiPropertyOptional({ maxLength: 200, description: 'Customs clearing agent name (when customs is enabled)' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
customsClearingAgent?: string;
|
|
|
|
@ApiProperty({ enum: EQUIPMENT_RETURNS })
|
|
@IsIn([...EQUIPMENT_RETURNS])
|
|
equipmentReturn!: string;
|
|
|
|
@ApiProperty({ format: 'uuid', description: 'FK to yards.id (origin)' })
|
|
@IsUUID()
|
|
originYardId!: string;
|
|
|
|
@ApiProperty({ format: 'uuid', description: 'FK to yards.id (destination)' })
|
|
@IsUUID()
|
|
destinationYardId!: string;
|
|
|
|
/**
|
|
* GENERAL_CONTRACT only: the routes this contract reserves quantity across.
|
|
* Each entry has its own origin/destination and quantity; the first entry also
|
|
* matches the booking's originYardId/destinationYardId. Omitted for one-time
|
|
* bookings, which use the single origin/destination above.
|
|
*/
|
|
@ApiPropertyOptional({ type: [CreateContractRouteDto] })
|
|
@ValidateIf((o) => o.bookingType === 'GENERAL_CONTRACT')
|
|
@IsOptional()
|
|
@IsArray()
|
|
@ArrayMinSize(1)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => CreateContractRouteDto)
|
|
routes?: CreateContractRouteDto[];
|
|
|
|
@ApiProperty({ enum: TRADE_DIRECTIONS })
|
|
@IsIn([...TRADE_DIRECTIONS])
|
|
tradeDirection!: string;
|
|
|
|
@ApiProperty({ enum: FREIGHT_TYPES, description: 'CONTAINER or BULK (mutually exclusive cargo shape)' })
|
|
@IsIn([...FREIGHT_TYPES])
|
|
freightType!: string;
|
|
|
|
@ApiPropertyOptional({
|
|
format: 'uuid',
|
|
description: 'Required for BULK; must be omitted for CONTAINER',
|
|
})
|
|
@ValidateIf((o) => o.freightType === 'BULK')
|
|
@IsUUID()
|
|
cargoTypeId?: string;
|
|
|
|
@ApiPropertyOptional({ maxLength: 200 })
|
|
@IsOptional()
|
|
@IsString()
|
|
cargoFreeText?: string;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'FK to shipping_lines.id' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
shippingLineId?: string;
|
|
|
|
@ApiProperty({ description: 'Total cargo weight VGM in tons', minimum: 0 })
|
|
@IsNumber()
|
|
@Min(0)
|
|
@Transform(({ value }) => Number(value))
|
|
cargoTotalWeightVgm!: number;
|
|
|
|
@ApiPropertyOptional({ default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Transform(({ value }) => value === 'true' || value === true)
|
|
isHazardous?: boolean;
|
|
|
|
/**
|
|
* Booking-level refrigerated flag. For bulk freight this is the customer's
|
|
* reefer choice (containers derive reefer from the container type instead).
|
|
* ORed with per-container reefer when the REEFER surcharge is evaluated.
|
|
*/
|
|
@ApiPropertyOptional({ default: false })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Transform(({ value }) => value === 'true' || value === true)
|
|
isReefer?: boolean;
|
|
|
|
@ApiProperty({ enum: PAYMENT_CURRENCIES })
|
|
@IsIn([...PAYMENT_CURRENCIES])
|
|
paymentCurrency!: string;
|
|
|
|
@ApiPropertyOptional()
|
|
@IsOptional()
|
|
@IsString()
|
|
pnrCode?: string;
|
|
|
|
@ApiPropertyOptional()
|
|
@IsOptional()
|
|
@IsDateString()
|
|
startDate?: string;
|
|
|
|
@ApiPropertyOptional()
|
|
@IsOptional()
|
|
@IsDateString()
|
|
endDate?: string;
|
|
|
|
@ApiPropertyOptional()
|
|
@IsOptional()
|
|
@IsString()
|
|
financialTerms?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
type: [CreateBookingContainerDto],
|
|
description: 'Required for CONTAINER (min 1 line); must be empty for BULK',
|
|
})
|
|
@ValidateIf((o) => o.freightType === 'CONTAINER')
|
|
@IsArray()
|
|
@ArrayMinSize(1)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => CreateBookingContainerDto)
|
|
containers?: CreateBookingContainerDto[];
|
|
}
|