mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 09:30:59 +00:00
complete rule engine and booking flow
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||
|
||||
const ROLES = ['LINE_STAFF', 'DIRECTOR', 'CEO'] as const;
|
||||
|
||||
export class CreateApprovalRuleDto {
|
||||
@ApiProperty({ description: 'True = Director+CEO chain; False = LineStaff+Director chain' })
|
||||
@IsBoolean()
|
||||
requiresDirectorApproval!: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Step sequence number (1 = first, 2 = second)', minimum: 1 })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
stepOrder!: number;
|
||||
|
||||
@ApiProperty({ enum: ROLES, description: 'Role required to action this step' })
|
||||
@IsString()
|
||||
@MaxLength(30)
|
||||
requiredRole!: string;
|
||||
|
||||
@ApiProperty({ description: 'Label shown in UI, e.g. "Review & Approve"', maxLength: 50 })
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
actionLabel!: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: ROLES, description: 'Role explicitly blocked from actioning this step' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(30)
|
||||
blocksRole?: string;
|
||||
}
|
||||
@@ -1,25 +1,48 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsInt, IsNumber, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
|
||||
|
||||
export class CreateContainerTypeDto {
|
||||
@ApiProperty({ description: 'Size code, e.g. 20FT or 40FT', maxLength: 20 })
|
||||
@ApiProperty({ description: 'Unique container code, e.g. 20DV, 40HC', maxLength: 20 })
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
sizeCode!: string;
|
||||
code!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Human-readable description', maxLength: 100 })
|
||||
@IsOptional()
|
||||
@ApiProperty({ description: 'Customer-facing label, e.g. "20ft Dry Container"', maxLength: 100 })
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
description?: string;
|
||||
label!: string;
|
||||
|
||||
@ApiProperty({ description: 'Number of containers that fit per rail wagon (2 for 20FT, 1 for 40FT)' })
|
||||
@ApiProperty({ description: 'Container size in feet: 20 or 40', enum: [20, 40] })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
containersPerWagon!: number;
|
||||
@Min(20)
|
||||
@Max(40)
|
||||
sizeFt!: number;
|
||||
|
||||
@ApiProperty({ description: 'Wagon fraction per container: 0.50 for 20ft, 1.00 for 40ft' })
|
||||
@IsNumber()
|
||||
@Min(0.01)
|
||||
@Transform(({ value }) => Number(value))
|
||||
wagonsPerUnit!: number;
|
||||
|
||||
@ApiPropertyOptional({ default: false, description: 'True if this is a reefer (refrigerated) container' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isReefer?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: false, description: 'True if this is an open-top container' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isOpenTop?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: 1, description: 'UI display sort order' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
displayOrder?: number;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsEnum, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||
import { Freight } from '@edr/types';
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||
|
||||
export class CreatePriorityRuleDto {
|
||||
@ApiProperty({ enum: Freight.PriorityType, description: 'Priority type (unique per rule)' })
|
||||
@IsEnum(Freight.PriorityType)
|
||||
priorityType!: Freight.PriorityType;
|
||||
|
||||
@ApiProperty({ description: 'Human-readable rule name', maxLength: 255 })
|
||||
@ApiProperty({ description: 'Unique rule code, e.g. USD_PAYER, GOV_REQUEST', maxLength: 40 })
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
ruleName!: string;
|
||||
@MaxLength(40)
|
||||
code!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Explanation of when this rule is triggered' })
|
||||
@IsOptional()
|
||||
@ApiProperty({ description: 'Human-readable label', maxLength: 100 })
|
||||
@IsString()
|
||||
description?: string;
|
||||
@MaxLength(100)
|
||||
label!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Technical expression describing the activation condition' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
activationCondition?: string;
|
||||
|
||||
@ApiProperty({ description: 'Points added to booking.priorityScore when this rule matches', default: 0 })
|
||||
@ApiProperty({ description: 'Points added to booking.priority_score when condition matches', default: 0 })
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
bonusPoints!: number;
|
||||
score!: number;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@ApiPropertyOptional({
|
||||
description: 'If set, rule only matches bookings with this payment currency (e.g. USD). Null = matches all.',
|
||||
maxLength: 5,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(5)
|
||||
conditionCurrency?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: false, description: 'Feature flag — toggle without code deploy' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsDateString, IsIn, IsNumber, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator';
|
||||
import { RATE_TYPES, RATE_UNITS } from '../entities/rate.entity';
|
||||
|
||||
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'ANY'] as const;
|
||||
const CURRENCIES = ['ETB', 'USD'] as const;
|
||||
|
||||
export class CreateRateDto {
|
||||
@ApiProperty({ enum: RATE_TYPES, description: 'Rate type identifier' })
|
||||
@IsIn([...RATE_TYPES])
|
||||
rateType!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'FK to container_types.id — null for non-container rates' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
containerTypeId?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: TRADE_DIRECTIONS, description: 'Trade direction. Null = direction-agnostic' })
|
||||
@IsOptional()
|
||||
@IsIn([...TRADE_DIRECTIONS])
|
||||
tradeDirection?: string;
|
||||
|
||||
@ApiProperty({ enum: CURRENCIES })
|
||||
@IsIn([...CURRENCIES])
|
||||
currency!: string;
|
||||
|
||||
@ApiProperty({ description: 'Numeric rate value', minimum: 0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Transform(({ value }) => Number(value))
|
||||
rateValue!: number;
|
||||
|
||||
@ApiProperty({ enum: RATE_UNITS, description: 'Unit basis for the rate' })
|
||||
@IsIn([...RATE_UNITS])
|
||||
rateUnit!: string;
|
||||
|
||||
@ApiProperty({ description: 'ID of the staff member (Director) proposing this rate' })
|
||||
@IsUUID()
|
||||
proposedByStaffId!: string;
|
||||
|
||||
@ApiProperty({ description: 'Date from which this rate is effective (ISO date)', example: '2025-01-01' })
|
||||
@IsDateString()
|
||||
effectiveFrom!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Date when this rate expires. Null = currently active', example: '2025-12-31' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
effectiveTo?: string;
|
||||
}
|
||||
|
||||
export class ApproveRateDto {
|
||||
@ApiProperty({ description: 'ID of the CEO approving this rate' })
|
||||
@IsUUID()
|
||||
approvedByCeoId!: string;
|
||||
}
|
||||
|
||||
export class SubmitRateForApprovalDto {
|
||||
@ApiPropertyOptional({ description: 'Optional note for the approval request', maxLength: 500 })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
note?: string;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
export class CreateShippingLineDto {
|
||||
@ApiProperty({ description: 'Unique shipping line code, e.g. MSC, PIL, MAERSK', maxLength: 20 })
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
code!: string;
|
||||
|
||||
@ApiProperty({ description: 'Customer-facing label', maxLength: 100 })
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
label!: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'If set, backend silently uses this code for pricing tier lookups (e.g. PIL → MAERSK)',
|
||||
maxLength: 20,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
mappedToCode?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
default: false,
|
||||
description: 'If true, quotation renders additional fee notice to customer',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
showExtraFeeNotice?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -1,21 +1,32 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { IsBoolean, IsIn, IsOptional, IsString, IsUUID, MaxLength } from 'class-validator';
|
||||
|
||||
const TRIGGER_CONDITIONS = [
|
||||
'CARGO_FLAG_HAZARDOUS',
|
||||
'CARGO_FLAG_REEFER',
|
||||
'VGM_EXCEEDS_LIMIT',
|
||||
'SHIPPING_LINE_MAPPED',
|
||||
'CONSOLIDATION_ENABLED',
|
||||
] as const;
|
||||
|
||||
export class CreateSurchargeTypeDto {
|
||||
@ApiProperty({ description: 'Unique code, e.g. HAZARDOUS, REFRIGERATED', maxLength: 50 })
|
||||
@ApiProperty({ description: 'Unique code, e.g. HAZARD, REEFER, OVERWEIGHT', maxLength: 40 })
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
@MaxLength(40)
|
||||
code!: string;
|
||||
|
||||
@ApiProperty({ description: 'Display name', maxLength: 100 })
|
||||
@ApiProperty({ description: 'Human-readable label', maxLength: 100 })
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
name!: string;
|
||||
label!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Description of when this surcharge type is triggered' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
@ApiProperty({ enum: TRIGGER_CONDITIONS, description: 'Condition that auto-fires this surcharge' })
|
||||
@IsIn([...TRIGGER_CONDITIONS])
|
||||
triggerCondition!: string;
|
||||
|
||||
@ApiProperty({ description: 'FK to rates.id — the LIVE rate used to price this surcharge' })
|
||||
@IsUUID()
|
||||
rateId!: string;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Length,
|
||||
MaxLength,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { Freight } from '@edr/types';
|
||||
|
||||
export class CreateSurchargeDto {
|
||||
@ApiProperty({ description: 'FK to surcharge_types.id' })
|
||||
@IsUUID()
|
||||
surchargeTypeId!: string;
|
||||
|
||||
@ApiProperty({ description: 'Display name for this surcharge line item', maxLength: 255 })
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
feeName!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Human-readable description of when this surcharge is triggered' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
triggerDescription?: string;
|
||||
|
||||
@ApiProperty({ enum: Freight.CalculationMethod, default: Freight.CalculationMethod.PER_TON })
|
||||
@IsEnum(Freight.CalculationMethod)
|
||||
calculationMethod!: Freight.CalculationMethod;
|
||||
|
||||
@ApiProperty({ description: 'Rate amount (per ton, flat, or percentage)' })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
rate!: number;
|
||||
|
||||
@ApiProperty({ description: 'ISO 4217 currency code', default: 'USD' })
|
||||
@IsString()
|
||||
@Length(3, 3)
|
||||
currency!: string;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
applyToRail?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
applyToFirstMile?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
applyToLastMile?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -1,38 +1,30 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsEnum, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
||||
import { Freight } from '@edr/types';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsDateString, IsIn, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
||||
|
||||
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'ANY'] as const;
|
||||
|
||||
export class CreateWeightLimitRuleDto {
|
||||
@ApiProperty({ description: 'FK to container_types.id' })
|
||||
@IsUUID()
|
||||
containerTypeId!: string;
|
||||
|
||||
@ApiProperty({ enum: Freight.TradeDirection, description: 'Trade direction this rule applies to' })
|
||||
@IsEnum(Freight.TradeDirection)
|
||||
tradeDirection!: Freight.TradeDirection;
|
||||
@ApiProperty({ enum: TRADE_DIRECTIONS, description: 'Trade direction: IMPORT, EXPORT, or ANY' })
|
||||
@IsIn([...TRADE_DIRECTIONS])
|
||||
tradeDirection!: string;
|
||||
|
||||
@ApiProperty({ description: 'Maximum allowed weight in tons before surcharge is applied' })
|
||||
@ApiProperty({ description: 'Maximum allowed VGM in tons', minimum: 0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
maxWeightTons!: number;
|
||||
@Transform(({ value }) => Number(value))
|
||||
maxVgmTons!: number;
|
||||
|
||||
@ApiProperty({ description: 'Weight at which a warning is issued (must be ≤ maxWeightTons)' })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
warningThresholdTons!: number;
|
||||
@ApiProperty({ description: 'Date from which this rule is active (ISO date)', example: '2024-01-01' })
|
||||
@IsDateString()
|
||||
effectiveFrom!: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: Freight.ExceededAction, default: Freight.ExceededAction.WARNING_ONLY })
|
||||
@ApiPropertyOptional({ description: 'Date when this rule expires (ISO date). Null = currently active', example: '2025-12-31' })
|
||||
@IsOptional()
|
||||
@IsEnum(Freight.ExceededAction)
|
||||
exceededAction?: Freight.ExceededAction;
|
||||
|
||||
@ApiPropertyOptional({ description: 'FK to surcharges.id — surcharge billed when max is exceeded' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
surchargeId?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
@IsDateString()
|
||||
effectiveTo?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||
|
||||
export class CreateYardDto {
|
||||
@ApiProperty({ description: 'Unique yard code, e.g. KALITY, DJIB_PORT', maxLength: 20 })
|
||||
@IsString()
|
||||
@MaxLength(20)
|
||||
code!: string;
|
||||
|
||||
@ApiProperty({ description: 'Customer-facing yard label', maxLength: 100 })
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
label!: string;
|
||||
|
||||
@ApiProperty({ description: 'Country where the yard is located, e.g. Ethiopia, Djibouti', maxLength: 50 })
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
country!: string;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: 1, description: 'UI display sort order' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
displayOrder?: number;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateApprovalRuleDto } from './create-approval-rule.dto';
|
||||
|
||||
export class UpdateApprovalRuleDto extends PartialType(CreateApprovalRuleDto) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateRateDto } from './create-rate.dto';
|
||||
|
||||
export class UpdateRateDto extends PartialType(CreateRateDto) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateShippingLineDto } from './create-shipping-line.dto';
|
||||
|
||||
export class UpdateShippingLineDto extends PartialType(CreateShippingLineDto) {}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateSurchargeDto } from './create-surcharge.dto';
|
||||
|
||||
export class UpdateSurchargeDto extends PartialType(CreateSurchargeDto) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateYardDto } from './create-yard.dto';
|
||||
|
||||
export class UpdateYardDto extends PartialType(CreateYardDto) {}
|
||||
Reference in New Issue
Block a user