feat: enhance booking flow with multi-route support and onboarding document integration

- Introduced multi-route functionality in the booking process, allowing users to add additional routes for general contracts.
- Updated the StepDocuments component to display onboarding documents automatically attached to bookings.
- Refactored Step4Route to manage extra routes and quantities dynamically.
- Improved Step8Review to reflect the new onboarding document handling and updated submission readiness checks.
- Added new API endpoints and services for managing contract route lines and rejecting bookings.
- Removed the allowConsolidation field from the booking model as it is now managed by the system.
- Created migrations for the new contract route lines table and updated related entities.
This commit is contained in:
Marshal
2026-06-23 20:40:48 +00:00
parent eb85f8d32d
commit 080a08229d
38 changed files with 1457 additions and 286 deletions

View File

@@ -53,6 +53,30 @@ export class CreateBookingContainerDto {
vgmPerUnitTons!: number;
}
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({
format: 'uuid',
description: 'Container type for CONTAINER contracts; omit for BULK',
})
@IsOptional()
@IsUUID()
containerTypeId?: string;
@ApiProperty({ description: 'Contracted quantity for this route', minimum: 1 })
@IsNumber()
@Min(0)
@Transform(({ value }) => Number(value))
quantity!: number;
}
export class CreateBookingDto {
/** Class-level freight shape check (not a request field). */
@Validate(BookingFreightShapeConstraint)
@@ -161,6 +185,21 @@ export class CreateBookingDto {
@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;
@@ -233,10 +272,4 @@ export class CreateBookingDto {
@ValidateNested({ each: true })
@Type(() => CreateBookingContainerDto)
containers?: CreateBookingContainerDto[];
@ApiPropertyOptional({ default: false })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true' || value === true)
allowConsolidation?: boolean;
}

View File

@@ -96,11 +96,6 @@ export class FilterBookingDto {
@IsIn([...PAYMENT_STATUSES])
paymentStatus?: string;
@ApiPropertyOptional()
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
allowConsolidation?: boolean;
@ApiPropertyOptional({ description: 'true | false — filter paired consolidation' })
@IsOptional()
consolidationPaired?: string;

View File

@@ -7,9 +7,22 @@ export class PriceLineItemDto {
@ApiProperty()
description!: string;
/** Computed line total (unitAmount × quantity). Retained for totals elsewhere. */
@ApiProperty()
amount!: number;
/** Price for a single unit of this charge (e.g. one 20ft container, one ton). */
@ApiProperty()
unitAmount!: number;
/** Unit the rate is charged per: PER_CONTAINER | PER_TON | PER_WAGON | PER_KM | FLAT. */
@ApiProperty()
unit!: string;
/** How many units this charge applies to (containers, tons, wagons; 1 for FLAT). */
@ApiProperty()
quantity!: number;
@ApiProperty()
currency!: string;
}

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, MinLength } from 'class-validator';
export class RequestChangesDto {
@ApiProperty({ description: 'Staff note explaining what the customer must fix' })
@@ -34,3 +34,12 @@ export class CancelBookingDto {
@MinLength(1)
reason!: string;
}
export class RejectBookingDto {
@ApiPropertyOptional({
description: 'Optional reason the customer rejected the price estimate',
})
@IsOptional()
@IsString()
reason?: string;
}