Files
edr-platform/apps/edr-freight-api/src/modules/contracts/dto/create-booking-under-contract.dto.ts
Marshal 22a3fb98ee feat: Implement consolidated booking functionality
- Added support for viewing and managing consolidated bookings in BookingRequestDetailPage.
- Enhanced BookingRequestsPage to display paired bookings in a single row.
- Introduced pairedDecision method in bookings service to handle decisions for both halves of a consolidated pair.
- Updated contracts service to include methods for manual consolidation of odd-20ft bookings.
- Created new components for selecting and editing consolidation partners.
- Added tests for paired decision logic and manual consolidation scenarios.
- Updated UI to reflect changes in booking handling and provide user feedback for odd container counts.
2026-08-18 12:50:35 +00:00

265 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ApiHideProperty, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDateString,
IsIn,
IsInt,
IsNumber,
IsOptional,
IsString,
IsUUID,
Matches,
Min,
ValidateNested,
} from 'class-validator';
import { PAYMENT_CURRENCIES } from './create-contract.dto';
/** Per-shipment equipment return — "NA" stays contract-level only. */
const SHIPMENT_EQUIPMENT_RETURNS = ['WITH_RETURN', 'WITHOUT_RETURN'] as const;
/** One physical container under a booking line — entered at booking time. */
export class CreateContainerUnitDto {
@ApiProperty({ description: 'ISO 6346 container number, e.g. ABCD1234567' })
@IsString()
@Transform(({ value }) => (typeof value === 'string' ? value.trim().toUpperCase() : value))
@Matches(/^[A-Z]{4}\d{7}$/, {
message: 'containerNumber must match ISO container format, e.g. ABCD1234567',
})
containerNumber!: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
sealNumber?: string;
@ApiProperty({ description: 'VGM in tons', minimum: 0 })
@IsNumber()
@Min(0)
@Transform(({ value }) => Number(value))
vgmTons!: number;
@ApiPropertyOptional({ default: false })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true' || value === true)
isHazardous?: boolean;
@ApiPropertyOptional({ default: false })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true' || value === true)
isReefer?: boolean;
@ApiPropertyOptional({
default: false,
description: 'This container ships back empty (equipment return).',
})
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true' || value === true)
isReturn?: boolean;
}
export class CreateBookingContainerLineDto {
@ApiProperty({ description: '"20ft" | "40ft" — must be in the contract scope' })
@IsString()
containerSize!: string;
@ApiProperty({ minimum: 1 })
@IsInt()
@Min(1)
@Transform(({ value }) => Number(value))
quantity!: number;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsInt()
@Min(0)
@Transform(({ value }) => Number(value))
hazardousQuantity?: number;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsInt()
@Min(0)
@Transform(({ value }) => Number(value))
reeferQuantity?: number;
@ApiPropertyOptional({
minimum: 0,
description:
'How many units of this line ship with empty-container return (≤ quantity). ' +
'Only allowed when the contract was created WITH_RETURN (container freight).',
})
@IsOptional()
@IsInt()
@Min(0)
@Transform(({ value }) => Number(value))
returnQuantity?: number;
@ApiProperty({ type: [CreateContainerUnitDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateContainerUnitDto)
units!: CreateContainerUnitDto[];
}
export class CreateBulkLineDto {
@ApiPropertyOptional({ format: 'uuid' })
@IsOptional()
@IsUUID()
cargoTypeId?: string | null;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsNumber()
@Min(0)
@Transform(({ value }) => Number(value))
cargoWeightTons?: number;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsInt()
@Min(0)
@Transform(({ value }) => Number(value))
itemCount?: number;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsInt()
@Min(0)
@Transform(({ value }) => Number(value))
hazardousQuantity?: number;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsInt()
@Min(0)
@Transform(({ value }) => Number(value))
reeferQuantity?: number;
}
/** Shipment booking created under a contract (Path A customer, Path B GL ET). */
export class CreateBookingUnderContractDto {
@ApiPropertyOptional({
format: 'uuid',
description: 'Required for GENERAL multi-route contracts; ONE_TIME auto-selected.',
})
@IsOptional()
@IsUUID()
contractRouteId?: string;
/**
* The contract quotes in USD; the customer picks the billing currency here.
* Omitted → the contract's own currency (USD for contracts created under the
* current rule, the grandfathered currency for older ones). Intercity is
* forced to ETB by the service regardless of what is sent.
*/
@ApiPropertyOptional({
enum: PAYMENT_CURRENCIES,
description: 'Billing currency for this shipment. Intercity is always ETB.',
})
@IsOptional()
@IsIn([...PAYMENT_CURRENCIES])
paymentCurrency?: string;
@ApiPropertyOptional({
description:
'Binding shipment day. Omitted for intercity (DOMESTIC) bookings — staff assign a passing train later.',
example: '2026-07-15',
})
@IsOptional()
@IsDateString()
scheduledDate?: string;
@ApiPropertyOptional({
description:
'EXPORT rail only: the specific train (schedule id) picked from ' +
'GET /bookings/:id/export-trains for the shipment day. The reserve path ' +
'locks onto this train; 409 when it no longer fits. Ignored otherwise.',
})
@IsOptional()
@IsUUID()
trainScheduleId?: string;
@ApiPropertyOptional({
enum: SHIPMENT_EQUIPMENT_RETURNS,
description:
'Per-shipment equipment return override; omitted → the contract default applies.',
})
@IsOptional()
@IsIn([...SHIPMENT_EQUIPMENT_RETURNS])
equipmentReturn?: string;
@ApiPropertyOptional({ type: [CreateBookingContainerLineDto] })
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateBookingContainerLineDto)
containers?: CreateBookingContainerLineDto[];
@ApiPropertyOptional({ type: [CreateBulkLineDto] })
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateBulkLineDto)
bulkLines?: CreateBulkLineDto[];
@ApiPropertyOptional({
description: 'What the containers carry — captured per booking (container freight).',
})
@IsOptional()
@IsString()
cargoFreeText?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
notes?: string;
/**
* Internal: set by the manual GL pair-completion path, never by a client.
* Suppresses the automatic wagon-consolidation gate for this completion
* because the caller links the shared wagon itself. Excluded from the public
* schema so a client cannot set it to bypass the gate on a lone booking.
*/
@ApiHideProperty()
@IsOptional()
@IsBoolean()
skipAutoConsolidation?: boolean;
}
/**
* Complete an odd-20ft customs booking together with the partner booking GL
* picked to share its wagon. Each half carries its own full completion payload —
* the two bookings stay separately priced and separately invoiced, they only
* share the wagon.
*/
export class CompleteConsolidatedPairDto {
@ApiProperty({
format: 'uuid',
description: 'The booking chosen to share this bookings wagon.',
})
@IsUUID()
partnerBookingId!: string;
@ApiProperty({
type: CreateBookingUnderContractDto,
description: 'Completion payload for the booking in the URL.',
})
@ValidateNested()
@Type(() => CreateBookingUnderContractDto)
booking!: CreateBookingUnderContractDto;
@ApiProperty({
type: CreateBookingUnderContractDto,
description: 'Completion payload for the partner booking.',
})
@ValidateNested()
@Type(() => CreateBookingUnderContractDto)
partner!: CreateBookingUnderContractDto;
}