contrat,booking,global logestic

This commit is contained in:
Marshal
2026-06-26 23:24:48 +00:00
parent f931342f31
commit 01d53c218c
105 changed files with 19573 additions and 909 deletions

View File

@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, Max, Min } from 'class-validator';
export class AcceptContractDto {
@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: 3650,
example: 365,
})
@IsInt()
@Min(1)
@Max(3650)
validityDays!: number;
}

View File

@@ -0,0 +1,36 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, MinLength } from 'class-validator';
export class ApproveStepDto {
@ApiProperty({ description: 'LINE_STAFF | DIRECTOR | CEO' })
@IsString()
requiredRole!: string;
}
export class RequestChangesDto {
@ApiProperty({ description: 'Note explaining what the customer must fix' })
@IsString()
@MinLength(1)
note!: string;
}
export class RejectContractDto {
@ApiProperty()
@IsString()
@MinLength(1)
reason!: string;
}
export class RejectStepDto {
@ApiProperty()
@IsString()
@MinLength(1)
reason!: string;
}
export class CancelContractDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
reason?: string;
}

View File

@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
export class ContractListSummaryMetricsDto {
@ApiProperty({ example: 42 })
inQueue!: number;
@ApiProperty({ example: 10 })
onThisPage!: number;
@ApiProperty({ example: 8 })
needsAction!: number;
}
export class ContractListSummaryDto {
@ApiProperty({ type: ContractListSummaryMetricsDto })
metrics!: ContractListSummaryMetricsDto;
@ApiProperty({ description: 'Count per contract status', type: 'object', additionalProperties: { type: 'number' } })
statusCounts!: Record<string, number>;
}

View File

@@ -0,0 +1,138 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsDateString,
IsInt,
IsNumber,
IsOptional,
IsString,
IsUUID,
Min,
ValidateNested,
} from 'class-validator';
/** One physical container under a booking line — entered at booking time. */
export class CreateContainerUnitDto {
@ApiProperty()
@IsString()
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;
}
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;
@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;
}
/** 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;
@ApiProperty({ description: 'Binding shipment day.', example: '2026-07-15' })
@IsDateString()
scheduledDate!: 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()
@IsOptional()
@IsString()
notes?: string;
}

View File

@@ -0,0 +1,245 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsDateString,
IsIn,
IsNumber,
IsOptional,
IsString,
IsUUID,
Max,
MaxLength,
Min,
MinLength,
ValidateIf,
ValidateNested,
} from 'class-validator';
import { CONTRACT_KINDS } from '../entities/contract.entity';
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'DOMESTIC'] as const;
const FREIGHT_TYPES = ['CONTAINER', 'BULK'] as const;
const PAYMENT_CURRENCIES = ['ETB', 'USD'] as const;
const EQUIPMENT_RETURNS = ['with_return', 'without_return'] as const;
export {
CONTRACT_KINDS,
TRADE_DIRECTIONS,
FREIGHT_TYPES,
PAYMENT_CURRENCIES,
EQUIPMENT_RETURNS,
};
/** One cargo-scope row — a container size OR a bulk commodity. NO quantities. */
export class CreateContractCargoScopeDto {
@ApiPropertyOptional({ description: '"20ft" | "40ft"; omit for bulk' })
@IsOptional()
@IsString()
@MaxLength(10)
containerSize?: string | null;
@ApiPropertyOptional({ format: 'uuid', description: 'FK to cargo_types.id' })
@IsOptional()
@IsUUID()
cargoTypeId?: string | null;
@ApiPropertyOptional({ maxLength: 200 })
@IsOptional()
@IsString()
@MaxLength(200)
cargoFreeText?: string | null;
}
/** A contracted lane (origin → destination). Routes carry NO quantity. */
export class CreateContractRouteInputDto {
@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({
description: 'Road billing distance (km); null for rail-only.',
minimum: 0,
})
@IsOptional()
@IsNumber()
@Min(0)
@Transform(({ value }) =>
value === undefined || value === null || value === '' ? undefined : Number(value),
)
km?: number;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsNumber()
@Min(0)
@Transform(({ value }) =>
value === undefined || value === null || value === '' ? undefined : Number(value),
)
sortOrder?: number;
}
export class CreateContractDto {
@ApiPropertyOptional({ description: 'Unique contract reference (auto-generated if omitted)' })
@IsOptional()
@IsString()
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
reference?: string;
@ApiPropertyOptional({ description: 'Staff only: government contract flag' })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true' || value === true)
isGovernment?: boolean;
@ApiPropertyOptional({ description: 'Required when isGovernment is true' })
@ValidateIf((o) => o.isGovernment === true)
@IsString()
@MinLength(2)
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
governmentInstitution?: string;
@ApiPropertyOptional({ format: 'uuid', description: 'Admin only: target company' })
@ValidateIf((o) => o.isGovernment !== true)
@IsOptional()
@IsUUID()
companyId?: string;
@ApiProperty({ enum: CONTRACT_KINDS, description: 'ONE_TIME | GENERAL' })
@IsIn([...CONTRACT_KINDS])
contractKind!: string;
@ApiPropertyOptional({ format: 'uuid', description: 'FK to contracts.id when renewing' })
@IsOptional()
@IsUUID()
@Transform(({ value }) => (value === '' || value == null ? undefined : value))
renewalOfId?: string;
@ApiProperty({ enum: TRADE_DIRECTIONS })
@IsIn([...TRADE_DIRECTIONS])
tradeDirection!: string;
@ApiProperty({ enum: FREIGHT_TYPES })
@IsIn([...FREIGHT_TYPES])
freightType!: string;
@ApiProperty({ format: 'uuid', description: 'FK to service_types.id' })
@IsUUID()
serviceTypeId!: string;
@ApiProperty({ enum: PAYMENT_CURRENCIES })
@IsIn([...PAYMENT_CURRENCIES])
paymentCurrency!: string;
@ApiPropertyOptional({ description: 'Whether EDR/GL handles customs clearance' })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true' || value === true)
customsClearingEnabled?: boolean;
@ApiPropertyOptional({ maxLength: 200 })
@IsOptional()
@IsString()
@MaxLength(200)
customsClearingAgent?: string;
@ApiPropertyOptional({ enum: EQUIPMENT_RETURNS })
@IsOptional()
@IsIn([...EQUIPMENT_RETURNS])
equipmentReturn?: string;
@ApiPropertyOptional()
@IsOptional()
@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({ default: false, description: 'Sets contracts.is_hazardous' })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true' || value === true)
isHazardous?: boolean;
@ApiPropertyOptional({ default: false, description: 'Sets contracts.is_reefer' })
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true' || value === true)
isReefer?: boolean;
@ApiPropertyOptional({
description: 'Non-binding estimate from the wizard (NOT validated against departures)',
example: '2026-07-15T00:00:00.000Z',
})
@IsOptional()
@IsDateString()
estimatedShipmentDate?: string;
@ApiPropertyOptional({ description: 'Contract document type (SPOT, etc.)' })
@IsOptional()
@IsString()
@MaxLength(20)
contractType?: string;
@ApiProperty({
type: [CreateContractCargoScopeDto],
description:
'Cargo scope rows. CONTAINER: ≥1 size row. BULK: exactly one cargo-type row.',
})
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => CreateContractCargoScopeDto)
cargoScope!: CreateContractCargoScopeDto[];
@ApiProperty({
type: [CreateContractRouteInputDto],
description: 'Contracted lanes. ONE_TIME: exactly 1. GENERAL: 1..N.',
})
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => CreateContractRouteInputDto)
routes!: CreateContractRouteInputDto[];
}

View File

@@ -0,0 +1,93 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
import { CONTRACT_STATUSES, CONTRACT_KINDS } from '../entities/contract.entity';
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'DOMESTIC'] as const;
const FREIGHT_TYPES = ['CONTAINER', 'BULK'] as const;
const PAYMENT_CURRENCIES = ['ETB', 'USD'] as const;
export class FilterContractDto {
@ApiPropertyOptional({ enum: CONTRACT_STATUSES })
@IsOptional()
@IsIn([...CONTRACT_STATUSES])
status?: string;
@ApiPropertyOptional({
description:
'Filter by statuses: comma-separated or repeated query params. Overrides status when set.',
})
@IsOptional()
@Transform(({ value }) => {
if (value === undefined || value === null || value === '') return undefined;
if (Array.isArray(value)) return value.map(String).join(',');
return String(value);
})
statuses?: string;
@ApiPropertyOptional({ format: 'uuid' })
@IsOptional()
@IsUUID()
companyId?: string;
@ApiPropertyOptional({ format: 'uuid' })
@IsOptional()
@IsUUID()
companyProfileId?: string;
@ApiPropertyOptional({ enum: CONTRACT_KINDS })
@IsOptional()
@IsIn([...CONTRACT_KINDS])
contractKind?: string;
@ApiPropertyOptional({ format: 'uuid' })
@IsOptional()
@IsUUID()
serviceTypeId?: string;
@ApiPropertyOptional({ enum: FREIGHT_TYPES })
@IsOptional()
@IsIn([...FREIGHT_TYPES])
freightType?: string;
@ApiPropertyOptional({ enum: TRADE_DIRECTIONS })
@IsOptional()
@IsIn([...TRADE_DIRECTIONS])
tradeDirection?: string;
@ApiPropertyOptional({ enum: PAYMENT_CURRENCIES })
@IsOptional()
@IsIn([...PAYMENT_CURRENCIES])
paymentCurrency?: string;
@ApiPropertyOptional({ description: 'Filter contracts created on/after this date (ISO)' })
@IsOptional()
@IsDateString()
createdFrom?: string;
@ApiPropertyOptional({ description: 'Filter contracts created on/before this date (ISO)' })
@IsOptional()
@IsDateString()
createdTo?: string;
@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', 'contractValidUntil'])
sortBy?: string;
@ApiPropertyOptional({ enum: ['ASC', 'DESC'], default: 'DESC' })
@IsOptional()
@IsIn(['ASC', 'DESC'])
sortOrder?: 'ASC' | 'DESC';
}

View File

@@ -0,0 +1,9 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class RenewContractDto {
@ApiPropertyOptional({ description: 'Reference of the prior contract being renewed.' })
@IsOptional()
@IsString()
previousContractReference?: string;
}

View File

@@ -0,0 +1,18 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsIn, IsOptional, IsString, MinLength } from 'class-validator';
export class ReviewClearanceDocumentDto {
@ApiProperty({ description: 'The document fileKey being reviewed' })
@IsString()
@MinLength(1)
fileKey!: string;
@ApiProperty({ enum: ['APPROVED', 'QUERIED'] })
@IsIn(['APPROVED', 'QUERIED'])
status!: 'APPROVED' | 'QUERIED';
@ApiPropertyOptional({ description: 'Required when querying a document' })
@IsOptional()
@IsString()
note?: string;
}

View File

@@ -0,0 +1,23 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsIn, IsOptional, IsString, MinLength } from 'class-validator';
export class SignContractDto {
@ApiProperty({ enum: ['CUSTOMER', 'STAFF', 'DIRECTOR', 'CEO'] })
@IsIn(['CUSTOMER', 'STAFF', 'DIRECTOR', 'CEO'])
role!: 'CUSTOMER' | 'STAFF' | 'DIRECTOR' | 'CEO';
@ApiProperty({ description: 'PNG signature image as base64 (with or without data URL prefix)' })
@IsString()
@MinLength(20)
signatureImageBase64!: string;
@ApiProperty()
@IsString()
@MinLength(1)
signerDisplayName!: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
consentText?: string;
}

View File

@@ -0,0 +1,6 @@
import { PartialType } from '@nestjs/swagger';
import { CreateContractDto } from './create-contract.dto';
/** Partial contract update — allowed only in DRAFT / CHANGES_REQUESTED. */
export class UpdateContractDto extends PartialType(CreateContractDto) {}