mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
248 lines
6.9 KiB
TypeScript
248 lines
6.9 KiB
TypeScript
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
import { Transform, Type } from 'class-transformer';
|
|
import {
|
|
ArrayMinSize,
|
|
IsArray,
|
|
IsBoolean,
|
|
IsIn,
|
|
IsNumber,
|
|
IsOptional,
|
|
IsString,
|
|
IsUUID,
|
|
Max,
|
|
MaxLength,
|
|
Min,
|
|
MinLength,
|
|
ValidateIf,
|
|
ValidateNested,
|
|
} from 'class-validator';
|
|
|
|
import { CONTRACT_KINDS } from '../entities/contract.entity';
|
|
|
|
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'DOMESTIC'] as const;
|
|
const FREIGHT_TYPES = ['CONTAINER', 'BULK'] as const;
|
|
const PAYMENT_CURRENCIES = ['ETB', 'USD'] as const;
|
|
const EQUIPMENT_RETURNS = ['with_return', 'without_return'] as const;
|
|
|
|
export {
|
|
CONTRACT_KINDS,
|
|
TRADE_DIRECTIONS,
|
|
FREIGHT_TYPES,
|
|
PAYMENT_CURRENCIES,
|
|
EQUIPMENT_RETURNS,
|
|
};
|
|
|
|
/** One cargo-scope row — a container size OR a bulk commodity. NO quantities. */
|
|
export class CreateContractCargoScopeDto {
|
|
@ApiPropertyOptional({ description: '"20ft" | "40ft"; omit for bulk' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(10)
|
|
containerSize?: string | null;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'FK to cargo_types.id' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
cargoTypeId?: string | null;
|
|
|
|
@ApiPropertyOptional({ maxLength: 200 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
cargoFreeText?: string | null;
|
|
|
|
@ApiPropertyOptional({
|
|
description:
|
|
'GENERAL only: total bookable quantity for this line (containers per size, or tons/items for bulk). Omit for uncapped.',
|
|
minimum: 1,
|
|
})
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(1)
|
|
@Transform(({ value }) => (value == null || value === '' ? null : Number(value)))
|
|
quantityCap?: number | null;
|
|
}
|
|
|
|
/** A contracted lane (origin → destination). Routes carry NO quantity. */
|
|
export class CreateContractRouteInputDto {
|
|
@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 billing distance (km); null for rail-only.',
|
|
minimum: 0,
|
|
})
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(0)
|
|
@Transform(({ value }) =>
|
|
value === undefined || value === null || value === '' ? undefined : Number(value),
|
|
)
|
|
km?: number;
|
|
|
|
@ApiPropertyOptional({ minimum: 0 })
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Min(0)
|
|
@Transform(({ value }) =>
|
|
value === undefined || value === null || value === '' ? undefined : Number(value),
|
|
)
|
|
sortOrder?: number;
|
|
}
|
|
|
|
export class CreateContractDto {
|
|
@ApiPropertyOptional({ description: 'Unique contract reference (auto-generated if omitted)' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
|
|
reference?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Staff only: government contract flag' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Transform(({ value }) => value === 'true' || value === true)
|
|
isGovernment?: boolean;
|
|
|
|
@ApiPropertyOptional({ description: 'Required when isGovernment is true' })
|
|
@ValidateIf((o) => o.isGovernment === true)
|
|
@IsString()
|
|
@MinLength(2)
|
|
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
|
|
governmentInstitution?: string;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'Admin only: target company' })
|
|
@ValidateIf((o) => o.isGovernment !== true)
|
|
@IsOptional()
|
|
@IsUUID()
|
|
companyId?: string;
|
|
|
|
@ApiProperty({ enum: CONTRACT_KINDS, description: 'ONE_TIME | GENERAL' })
|
|
@IsIn([...CONTRACT_KINDS])
|
|
contractKind!: string;
|
|
|
|
@ApiPropertyOptional({ format: 'uuid', description: 'FK to contracts.id when renewing' })
|
|
@IsOptional()
|
|
@IsUUID()
|
|
@Transform(({ value }) => (value === '' || value == null ? undefined : value))
|
|
renewalOfId?: string;
|
|
|
|
@ApiProperty({ enum: TRADE_DIRECTIONS })
|
|
@IsIn([...TRADE_DIRECTIONS])
|
|
tradeDirection!: string;
|
|
|
|
@ApiProperty({ enum: FREIGHT_TYPES })
|
|
@IsIn([...FREIGHT_TYPES])
|
|
freightType!: string;
|
|
|
|
@ApiProperty({ format: 'uuid', description: 'FK to service_types.id' })
|
|
@IsUUID()
|
|
serviceTypeId!: string;
|
|
|
|
@ApiProperty({ enum: PAYMENT_CURRENCIES })
|
|
@IsIn([...PAYMENT_CURRENCIES])
|
|
paymentCurrency!: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Whether EDR/GL handles customs clearance' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Transform(({ value }) => value === 'true' || value === true)
|
|
customsClearingEnabled?: boolean;
|
|
|
|
@ApiPropertyOptional({ maxLength: 200 })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(200)
|
|
customsClearingAgent?: string;
|
|
|
|
@ApiPropertyOptional({ enum: EQUIPMENT_RETURNS })
|
|
@IsOptional()
|
|
@IsIn([...EQUIPMENT_RETURNS])
|
|
equipmentReturn?: 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({ default: false, description: 'Sets contracts.is_hazardous' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Transform(({ value }) => value === 'true' || value === true)
|
|
isHazardous?: boolean;
|
|
|
|
@ApiPropertyOptional({ default: false, description: 'Sets contracts.is_reefer' })
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Transform(({ value }) => value === 'true' || value === true)
|
|
isReefer?: boolean;
|
|
|
|
@ApiPropertyOptional({ description: 'Contract document type (SPOT, etc.)' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(20)
|
|
contractType?: string;
|
|
|
|
@ApiProperty({
|
|
type: [CreateContractCargoScopeDto],
|
|
description:
|
|
'Cargo scope rows. CONTAINER: ≥1 size row. BULK: exactly one cargo-type row.',
|
|
})
|
|
@IsArray()
|
|
@ArrayMinSize(1)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => CreateContractCargoScopeDto)
|
|
cargoScope!: CreateContractCargoScopeDto[];
|
|
|
|
@ApiProperty({
|
|
type: [CreateContractRouteInputDto],
|
|
description: 'Contracted lanes. ONE_TIME: exactly 1. GENERAL: 1..N.',
|
|
})
|
|
@IsArray()
|
|
@ArrayMinSize(1)
|
|
@ValidateNested({ each: true })
|
|
@Type(() => CreateContractRouteInputDto)
|
|
routes!: CreateContractRouteInputDto[];
|
|
}
|