mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 22:25:42 +00:00
feat(bookings): add contract validity window and customs clearing features
- Introduced contract validity days, valid from and valid until fields in the booking model. - Updated booking acceptance logic to enforce validity window constraints. - Added customs clearing agent and first/last mile pickup/delivery coordinates to the booking model. - Implemented LocationPicker component for address selection with map integration using Leaflet. - Enhanced booking review step to display customs clearing agent details. - Updated API and DTOs to accommodate new booking fields. - Added migration scripts for database schema changes. - Implemented tests for booking acceptance and DTO transformations.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import 'reflect-metadata';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { CreateBookingDto } from './create-booking.dto';
|
||||
|
||||
/**
|
||||
* Boolean flags arrive as STRINGS over multipart/form-data ("true" / "false").
|
||||
* The global freight ValidationPipe runs with enableImplicitConversion = false,
|
||||
* so only the explicit @Transform on each flag coerces it. This pins that the
|
||||
* literal string "false" maps to boolean `false` — class-transformer's implicit
|
||||
* boolean coercion would otherwise turn any non-empty string (including "false")
|
||||
* into `true`, silently flagging non-hazardous bookings as hazardous.
|
||||
*/
|
||||
describe('CreateBookingDto — boolean coercion from multipart strings', () => {
|
||||
// Mirror the production pipe: explicit transforms only, no implicit coercion.
|
||||
const toDto = (plain: Record<string, unknown>) =>
|
||||
plainToInstance(CreateBookingDto, plain, {
|
||||
enableImplicitConversion: false,
|
||||
}) as unknown as CreateBookingDto;
|
||||
|
||||
it('maps the string "false" to boolean false for every flag', () => {
|
||||
const dto = toDto({
|
||||
isHazardous: 'false',
|
||||
isGovernment: 'false',
|
||||
customsClearingEnabled: 'false',
|
||||
});
|
||||
expect(dto.isHazardous).toBe(false);
|
||||
expect(dto.isGovernment).toBe(false);
|
||||
expect(dto.customsClearingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('maps the string "true" to boolean true for every flag', () => {
|
||||
const dto = toDto({
|
||||
isHazardous: 'true',
|
||||
isGovernment: 'true',
|
||||
customsClearingEnabled: 'true',
|
||||
});
|
||||
expect(dto.isHazardous).toBe(true);
|
||||
expect(dto.isGovernment).toBe(true);
|
||||
expect(dto.customsClearingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('still coerces numeric form strings to numbers', () => {
|
||||
const dto = toDto({ cargoTotalWeightVgm: '12.5' });
|
||||
expect(dto.cargoTotalWeightVgm).toBe(12.5);
|
||||
expect(typeof dto.cargoTotalWeightVgm).toBe('number');
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Max,
|
||||
MaxLength,
|
||||
Min,
|
||||
MinLength,
|
||||
Validate,
|
||||
@@ -168,11 +170,55 @@ export class CreateBookingDto {
|
||||
@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({ description: 'Whether EDR handles customs clearance' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => value === 'true' || value === true)
|
||||
customsClearingEnabled?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ maxLength: 200, description: 'Customs clearing agent name (when customs is enabled)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
customsClearingAgent?: string;
|
||||
|
||||
@ApiProperty({ enum: EQUIPMENT_RETURNS })
|
||||
@IsIn([...EQUIPMENT_RETURNS])
|
||||
equipmentReturn!: string;
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsIn, IsNumber, IsOptional, IsString, Min, MinLength } from 'class-validator';
|
||||
import {
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class RequestChangesDto {
|
||||
@ApiProperty({ description: 'Staff note explaining what the customer must fix' })
|
||||
@@ -8,6 +17,21 @@ export class RequestChangesDto {
|
||||
note!: string;
|
||||
}
|
||||
|
||||
export class AcceptIntakeDto {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'How many days the contract stays valid, counted from the accept date. ' +
|
||||
'The contract is valid from now through now + validityDays.',
|
||||
minimum: 1,
|
||||
maximum: 365,
|
||||
example: 30,
|
||||
})
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(365)
|
||||
validityDays!: number;
|
||||
}
|
||||
|
||||
export class StaffRejectDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
|
||||
Reference in New Issue
Block a user