mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
- 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.
265 lines
7.2 KiB
TypeScript
265 lines
7.2 KiB
TypeScript
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 booking’s 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;
|
||
}
|