Files
edr-platform/apps/edr-freight-api/src/modules/contracts/dto/create-contract.dto.ts

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[];
}