mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
complete rule engine and booking flow
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
@@ -12,117 +12,81 @@ import {
|
||||
IsUUID,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
} from 'class-validator';
|
||||
import { BOOKING_STATUSES } from '../entities/booking.entity';
|
||||
|
||||
const BOOKING_STATUSES = [
|
||||
"DRAFT",
|
||||
"PENDING_LINE_STAFF",
|
||||
"PENDING_DIRECTOR",
|
||||
"PENDING_CEO",
|
||||
"APPROVED_PENDING_SIGNATURE",
|
||||
"SIGNED",
|
||||
"ACTIVE",
|
||||
"EXPIRED",
|
||||
"CANCELLED",
|
||||
"PENDING_CONSOLIDATION",
|
||||
"CONSOLIDATED",
|
||||
] as const;
|
||||
|
||||
const CONTRACT_TYPES = ["NEW", "RENEWAL"] as const;
|
||||
const SERVICE_TYPES = ["RAIL_ONLY", "RAIL_AND_FORWARDING"] as const;
|
||||
const EQUIPMENT_RETURNS = ["WITH_RETURN", "WITHOUT_RETURN"] as const;
|
||||
const FREIGHT_TYPES = ["BULK", "BREAK_BULK"] as const;
|
||||
const TRADE_DIRECTIONS = ["IMPORT", "EXPORT"] as const;
|
||||
const PAYMENT_CURRENCIES = ["ETB", "USD"] as const;
|
||||
const PAYMENT_STATUSES = ["PENDING", "PAID", "OVERDUE", "CANCELLED", "REFUNDED"] as const;
|
||||
const CONTAINER_TYPES = ["20FT", "40FT"] as const;
|
||||
const CONTRACT_TYPES = ['NEW', 'RENEWAL'] as const;
|
||||
const EQUIPMENT_RETURNS = ['WITH_RETURN', 'WITHOUT_RETURN', 'NA'] as const;
|
||||
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'DOMESTIC'] as const;
|
||||
const PAYMENT_CURRENCIES = ['ETB', 'USD'] as const;
|
||||
|
||||
export {
|
||||
BOOKING_STATUSES,
|
||||
CONTRACT_TYPES,
|
||||
SERVICE_TYPES,
|
||||
EQUIPMENT_RETURNS,
|
||||
FREIGHT_TYPES,
|
||||
TRADE_DIRECTIONS,
|
||||
PAYMENT_CURRENCIES,
|
||||
PAYMENT_STATUSES,
|
||||
CONTAINER_TYPES,
|
||||
};
|
||||
|
||||
export class ContainerItem {
|
||||
@ApiProperty({ enum: CONTAINER_TYPES, description: "Container type (20FT or 40FT)" })
|
||||
@IsIn([...CONTAINER_TYPES])
|
||||
type!: string;
|
||||
export class CreateBookingContainerDto {
|
||||
@ApiProperty({ format: 'uuid', description: 'FK to container_types.id' })
|
||||
@IsUUID()
|
||||
containerTypeId!: string;
|
||||
|
||||
@ApiProperty({ description: "Quantity of containers", minimum: 1 })
|
||||
@ApiProperty({ description: 'Quantity of containers', minimum: 1 })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Transform(({ value }) => Number(value))
|
||||
qty!: number;
|
||||
quantity!: number;
|
||||
|
||||
@ApiProperty({ description: "VGM per container in tons", minimum: 0 })
|
||||
@ApiProperty({ description: 'VGM per container in tons', minimum: 0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Transform(({ value }) => Number(value))
|
||||
vgm!: number;
|
||||
vgmPerUnitTons!: number;
|
||||
}
|
||||
|
||||
export class CreateBookingDto {
|
||||
// ── core ─────────────────────────────────────────────────────────────
|
||||
@ApiPropertyOptional({ description: "Unique booking reference (auto-generated if not provided)" })
|
||||
@ApiPropertyOptional({ description: 'Unique booking reference (auto-generated if omitted)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (typeof value === "string" ? value.trim() : value))
|
||||
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
|
||||
reference?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: "uuid", description: "Admin only: target customer. Omit to resolve from auth token." })
|
||||
@ApiPropertyOptional({ format: 'uuid', description: 'Admin only: target customer' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
customerId?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: "uuid" })
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
trainId?: string;
|
||||
|
||||
@ApiProperty({ example: "2026-06-15T00:00:00.000Z" })
|
||||
@ApiProperty({ example: '2026-06-15T00:00:00.000Z' })
|
||||
@IsDateString()
|
||||
scheduledDate!: string;
|
||||
|
||||
|
||||
// ── contract ─────────────────────────────────────────────────────────
|
||||
@ApiProperty({ enum: CONTRACT_TYPES })
|
||||
@IsIn([...CONTRACT_TYPES])
|
||||
contractType!: string;
|
||||
|
||||
@ApiPropertyOptional({ format: "uuid", description: "For RENEWAL — previous contract/booking ID" })
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Transform(({ value }) => (value === "" || value == null ? undefined : value))
|
||||
@Transform(({ value }) => (value === '' || value == null ? undefined : value))
|
||||
previousContractId?: string;
|
||||
|
||||
@ApiProperty({ enum: SERVICE_TYPES })
|
||||
@IsIn([...SERVICE_TYPES])
|
||||
serviceType!: string;
|
||||
@ApiProperty({ format: 'uuid', description: 'FK to service_types.id' })
|
||||
@IsUUID()
|
||||
serviceTypeId!: string;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => value === "true" || value === true)
|
||||
firstMileEnabled?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: "Required when firstMileEnabled is true" })
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstMilePickupAddress?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => value === "true" || value === true)
|
||||
lastMileEnabled?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: "Required when lastMileEnabled is true" })
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastMileDeliveryAddress?: string;
|
||||
@@ -131,55 +95,59 @@ export class CreateBookingDto {
|
||||
@IsIn([...EQUIPMENT_RETURNS])
|
||||
equipmentReturn!: string;
|
||||
|
||||
@ApiProperty({ description: "Origin station name or code" })
|
||||
@IsString()
|
||||
originStation!: string;
|
||||
@ApiProperty({ format: 'uuid', description: 'FK to yards.id (origin)' })
|
||||
@IsUUID()
|
||||
originYardId!: string;
|
||||
|
||||
@ApiProperty({ description: "Destination station name or code" })
|
||||
@IsString()
|
||||
destinationStation!: string;
|
||||
|
||||
@ApiProperty({ description: "Total cargo weight in VGM tons", minimum: 0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Transform(({ value }) => Number(value))
|
||||
cargoTotalWeightVgm!: number;
|
||||
|
||||
@ApiProperty({ enum: FREIGHT_TYPES })
|
||||
@IsIn([...FREIGHT_TYPES])
|
||||
freightType!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Coffee, Beans, Machinery, Ro-Ro, etc." })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
freightSubtype?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => value === "true" || value === true)
|
||||
isHazardous?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => value === "true" || value === true)
|
||||
isRefrigerated?: boolean;
|
||||
@ApiProperty({ format: 'uuid', description: 'FK to yards.id (destination)' })
|
||||
@IsUUID()
|
||||
destinationYardId!: string;
|
||||
|
||||
@ApiProperty({ enum: TRADE_DIRECTIONS })
|
||||
@IsIn([...TRADE_DIRECTIONS])
|
||||
tradeDirection!: string;
|
||||
|
||||
@ApiProperty({ format: 'uuid', description: 'FK to cargo_types.id' })
|
||||
@IsUUID()
|
||||
cargoTypeId!: string;
|
||||
|
||||
@ApiPropertyOptional({ maxLength: 200 })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cargoFreeText?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid', description: 'FK to shipping_lines.id' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
shippingLineId?: string;
|
||||
|
||||
@ApiProperty({ description: 'Total cargo weight VGM in tons', minimum: 0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Transform(({ value }) => Number(value))
|
||||
cargoTotalWeightVgm!: number;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => value === 'true' || value === true)
|
||||
isHazardous?: boolean;
|
||||
|
||||
@ApiProperty({ enum: PAYMENT_CURRENCIES })
|
||||
@IsIn([...PAYMENT_CURRENCIES])
|
||||
paymentCurrency!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "2026-06-15" })
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
pnrCode?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "2027-06-15" })
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
@@ -189,19 +157,15 @@ export class CreateBookingDto {
|
||||
@IsString()
|
||||
financialTerms?: string;
|
||||
|
||||
// ── containers ────────────────────────────────────────────────────────
|
||||
@ApiProperty({ type: [ContainerItem], description: "Array of container specifications" })
|
||||
@ApiProperty({ type: [CreateBookingContainerDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ContainerItem)
|
||||
containers!: ContainerItem[];
|
||||
@Type(() => CreateBookingContainerDto)
|
||||
containers!: CreateBookingContainerDto[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
default: false,
|
||||
description: "Auto-set to true when any 20FT container has odd quantity. User may override.",
|
||||
})
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => value === "true" || value === true)
|
||||
@Transform(({ value }) => value === 'true' || value === true)
|
||||
allowConsolidation?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { IsBoolean, IsIn, IsInt, IsOptional, IsString, IsUUID, Min } from "class-validator";
|
||||
|
||||
import {
|
||||
BOOKING_STATUSES,
|
||||
CONTRACT_TYPES,
|
||||
FREIGHT_TYPES,
|
||||
PAYMENT_CURRENCIES,
|
||||
SERVICE_TYPES,
|
||||
TRADE_DIRECTIONS,
|
||||
} from "./create-booking.dto";
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsIn, IsOptional, IsUUID } from 'class-validator';
|
||||
import { BOOKING_STATUSES, PAYMENT_CURRENCIES, TRADE_DIRECTIONS } from './create-booking.dto';
|
||||
|
||||
export class FilterBookingDto {
|
||||
@ApiPropertyOptional({ enum: BOOKING_STATUSES })
|
||||
@@ -17,20 +9,24 @@ export class FilterBookingDto {
|
||||
@IsIn([...BOOKING_STATUSES])
|
||||
status?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: "uuid" })
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
customerId?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: CONTRACT_TYPES })
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsIn([...CONTRACT_TYPES])
|
||||
contractType?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: SERVICE_TYPES })
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsIn([...SERVICE_TYPES])
|
||||
serviceType?: string;
|
||||
@IsUUID()
|
||||
serviceTypeId?: string;
|
||||
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
cargoTypeId?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: TRADE_DIRECTIONS })
|
||||
@IsOptional()
|
||||
@@ -42,43 +38,31 @@ export class FilterBookingDto {
|
||||
@IsIn([...PAYMENT_CURRENCIES])
|
||||
paymentCurrency?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: FREIGHT_TYPES })
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsIn([...FREIGHT_TYPES])
|
||||
freightType?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Filter consolidation-eligible bookings" })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => value === "true" || value === true)
|
||||
@Transform(({ value }) => value === 'true' || value === true)
|
||||
allowConsolidation?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: "Filter by consolidation partner presence (true = paired, false = unpaired)" })
|
||||
@ApiPropertyOptional({ description: 'true | false — filter paired consolidation' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
consolidationPaired?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: ["createdAt", "priorityScore"], default: "createdAt" })
|
||||
@ApiPropertyOptional({ default: 1 })
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value ? parseInt(value, 10) : 1))
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ default: 20 })
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value ? parseInt(value, 10) : 20))
|
||||
pageSize?: number;
|
||||
|
||||
@ApiPropertyOptional({ default: 'createdAt' })
|
||||
@IsOptional()
|
||||
@IsIn(["createdAt", "priorityScore"])
|
||||
sortBy?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "DESC" })
|
||||
@ApiPropertyOptional({ enum: ['ASC', 'DESC'], default: 'DESC' })
|
||||
@IsOptional()
|
||||
@IsIn(["ASC", "DESC"])
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
|
||||
@ApiPropertyOptional({ default: 1, minimum: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ default: 20, minimum: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number = 20;
|
||||
@IsIn(['ASC', 'DESC'])
|
||||
sortOrder?: 'ASC' | 'DESC';
|
||||
}
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsIn, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsIn, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
const STATUS_ACTIONS = [
|
||||
"SUBMIT",
|
||||
"APPROVE_STAFF",
|
||||
"APPROVE_DIRECTOR",
|
||||
"APPROVE_CEO",
|
||||
"REJECT",
|
||||
"CANCEL",
|
||||
"ACTIVATE",
|
||||
"EXPIRE",
|
||||
'SUBMIT',
|
||||
'SEND_QUOTATION',
|
||||
'APPROVE_QUOTATION',
|
||||
'REJECT_QUOTATION',
|
||||
'APPROVE_STEP',
|
||||
'APPROVE',
|
||||
'CUSTOMER_SIGN',
|
||||
'MARK_FULLY_EXECUTED',
|
||||
'MARK_PAID',
|
||||
'START_TRANSIT',
|
||||
'COMPLETE',
|
||||
'REJECT',
|
||||
'CANCEL',
|
||||
] as const;
|
||||
|
||||
export { STATUS_ACTIONS };
|
||||
|
||||
export class UpdateStatusDto {
|
||||
@ApiProperty({
|
||||
enum: STATUS_ACTIONS,
|
||||
description:
|
||||
"SUBMIT — send to approval queue | " +
|
||||
"APPROVE_STAFF — line-staff approval | " +
|
||||
"APPROVE_DIRECTOR — director approval / signature | " +
|
||||
"APPROVE_CEO — CEO final signature | " +
|
||||
"REJECT — reject at any pending stage | " +
|
||||
"CANCEL — customer / admin cancellation | " +
|
||||
"ACTIVATE — activate a signed booking | " +
|
||||
"EXPIRE — mark an active booking as expired",
|
||||
})
|
||||
@ApiProperty({ enum: STATUS_ACTIONS })
|
||||
@IsIn([...STATUS_ACTIONS])
|
||||
action!: string;
|
||||
|
||||
@ApiPropertyOptional({ format: "uuid", description: "Actor performing the action (staff/director/CEO)" })
|
||||
@ApiPropertyOptional({ format: 'uuid', description: 'Staff/director/CEO actor' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
actorId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Required for REJECT and CANCEL actions" })
|
||||
@ApiPropertyOptional({ description: 'Required role for APPROVE_STEP (LINE_STAFF, DIRECTOR, CEO)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
requiredRole?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Required for REJECT, REJECT_QUOTATION, CANCEL' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
reason?: string;
|
||||
|
||||
Reference in New Issue
Block a user