rule engine and configration and fix booking for custoemr

This commit is contained in:
marshal
2026-06-01 23:15:11 +03:00
parent b9ca0b8a82
commit 452ebdbc0a
52 changed files with 3355 additions and 3358 deletions

View File

@@ -3,7 +3,7 @@ import {
Param, ParseUUIDPipe, Patch, Post, Query,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
// import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
import { UpdateCargoTypeDto } from '../dto/update-cargo-type.dto';
import { CargoTypesService } from '../services/cargo-types.service';
@@ -39,8 +39,7 @@ export class CargoTypesController {
@Post()
@ApiOperation({ summary: 'Create a cargo type' })
create(@Body() dto: any) {
return dto;
create(@Body() dto: CreateCargoTypeDto) {
return this.service.create(dto);
}

View File

@@ -2,11 +2,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator';
export class CreateCargoTypeDto {
@ApiProperty({ description: 'Machine-readable code, e.g. BULK, BREAK_BULK', maxLength: 50 })
@IsString()
@MaxLength(50)
code!: string;
@ApiProperty({ description: 'Cargo type display name', maxLength: 255 })
@IsString()
@MaxLength(255)

View File

@@ -3,11 +3,6 @@ import { Transform } from 'class-transformer';
import { IsBoolean, IsInt, IsNumber, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
export class CreateContainerTypeDto {
@ApiProperty({ description: 'Unique container code, e.g. 20DV, 40HC', maxLength: 20 })
@IsString()
@MaxLength(20)
code!: string;
@ApiProperty({ description: 'Customer-facing label, e.g. "20ft Dry Container"', maxLength: 100 })
@IsString()
@MaxLength(100)

View File

@@ -2,11 +2,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
export class CreatePriorityRuleDto {
@ApiProperty({ description: 'Unique rule code, e.g. USD_PAYER, GOV_REQUEST', maxLength: 40 })
@IsString()
@MaxLength(40)
code!: string;
@ApiProperty({ description: 'Human-readable label', maxLength: 100 })
@IsString()
@MaxLength(100)

View File

@@ -3,7 +3,7 @@ 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 TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'BOTH'] as const;
const CURRENCIES = ['ETB', 'USD'] as const;
export class CreateRateDto {

View File

@@ -2,11 +2,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
export class CreateServiceTypeDto {
@ApiProperty({ description: 'Machine-readable code, e.g. RAIL_ONLY', maxLength: 50 })
@IsString()
@MaxLength(50)
code!: string;
@ApiProperty({ description: 'Service type display name', maxLength: 255 })
@IsString()
@MaxLength(255)

View File

@@ -10,11 +10,6 @@ const TRIGGER_CONDITIONS = [
] as const;
export class CreateSurchargeTypeDto {
@ApiProperty({ description: 'Unique code, e.g. HAZARD, REEFER, OVERWEIGHT', maxLength: 40 })
@IsString()
@MaxLength(40)
code!: string;
@ApiProperty({ description: 'Human-readable label', maxLength: 100 })
@IsString()
@MaxLength(100)

View File

@@ -2,14 +2,14 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsDateString, IsIn, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'ANY'] as const;
const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'BOTH'] as const;
export class CreateWeightLimitRuleDto {
@ApiProperty({ description: 'FK to container_types.id' })
@IsUUID()
containerTypeId!: string;
@ApiProperty({ enum: TRADE_DIRECTIONS, description: 'Trade direction: IMPORT, EXPORT, or ANY' })
@ApiProperty({ enum: TRADE_DIRECTIONS, description: 'Trade direction: IMPORT, EXPORT, or BOTH' })
@IsIn([...TRADE_DIRECTIONS])
tradeDirection!: string;

View File

@@ -2,11 +2,6 @@ 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)

View File

@@ -27,9 +27,9 @@ export class WeightLimitRulesRepository implements IWeightLimitRulesRepository {
.createQueryBuilder('rule')
.innerJoinAndSelect('rule.containerType', 'ct')
.where('rule.container_type_id = :containerTypeId', { containerTypeId })
.andWhere('(rule.trade_direction = :dir OR rule.trade_direction = :any)', {
.andWhere('(rule.trade_direction = :dir OR rule.trade_direction = :both)', {
dir: tradeDirection,
any: 'ANY',
both: 'BOTH',
})
.andWhere('rule.effective_from <= :now', { now })
.andWhere('(rule.effective_to IS NULL OR rule.effective_to > :now)', { now })

View File

@@ -1,5 +1,6 @@
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { ILike } from 'typeorm';
import { generateCode } from '../../../common/utils/generate-code.util';
import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
import { UpdateCargoTypeDto } from '../dto/update-cargo-type.dto';
import { CargoType } from '../entities/cargo-type.entity';
@@ -58,14 +59,15 @@ export class CargoTypesService {
/** Create a new cargo type. */
async create(dto: CreateCargoTypeDto): Promise<CargoType> {
const existing = await this.repository.findByCode(dto.code);
if (existing) throw new ConflictException(`Cargo type with code "${dto.code}" already exists`);
const code = generateCode(dto.cargoTypeName);
const existing = await this.repository.findByCode(code);
if (existing) throw new ConflictException(`Cargo type with name "${dto.cargoTypeName}" conflicts with existing code "${code}"`);
if (dto.parentGroupId) {
const parent = await this.repository.findById(dto.parentGroupId);
if (!parent) throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`);
}
return this.repository.create({
code: dto.code,
code,
cargoTypeName: dto.cargoTypeName,
parentGroupId: dto.parentGroupId ?? null,
showFreeTextBox: dto.showFreeTextBox ?? false,
@@ -78,12 +80,6 @@ export class CargoTypesService {
/** Update an existing cargo type. */
async update(id: string, dto: UpdateCargoTypeDto): Promise<CargoType> {
await this.findById(id);
if (dto.code) {
const conflict = await this.repository.findByCode(dto.code);
if (conflict && conflict.id !== id) {
throw new ConflictException(`Cargo type with code "${dto.code}" already exists`);
}
}
if (dto.parentGroupId) {
if (dto.parentGroupId === id) throw new ConflictException('A cargo type cannot be its own parent');
const parent = await this.repository.findById(dto.parentGroupId);

View File

@@ -1,4 +1,5 @@
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { generateCode } from '../../../common/utils/generate-code.util';
import { CreateContainerTypeDto } from '../dto/create-container-type.dto';
import { UpdateContainerTypeDto } from '../dto/update-container-type.dto';
import { ContainerType } from '../entities/container-type.entity';
@@ -27,7 +28,7 @@ export class ContainerTypesService {
const [data, total] = await this.repository.findAndCount({
where,
order: { code: 'ASC' },
order: { displayOrder: 'ASC' },
skip: (page - 1) * pageSize,
take: pageSize,
});
@@ -43,10 +44,11 @@ export class ContainerTypesService {
/** Create a new container type. */
async create(dto: CreateContainerTypeDto): Promise<ContainerType> {
const existing = await this.repository.findByCode(dto.code);
if (existing) throw new ConflictException(`Container type with code "${dto.code}" already exists`);
const code = generateCode(dto.label);
const existing = await this.repository.findByCode(code);
if (existing) throw new ConflictException(`Container type with label "${dto.label}" conflicts with existing code "${code}"`);
return this.repository.create({
code: dto.code,
code,
label: dto.label,
sizeFt: dto.sizeFt,
wagonsPerUnit: dto.wagonsPerUnit,
@@ -60,12 +62,6 @@ export class ContainerTypesService {
/** Update an existing container type. */
async update(id: string, dto: UpdateContainerTypeDto): Promise<ContainerType> {
await this.findById(id);
if (dto.code) {
const conflict = await this.repository.findByCode(dto.code);
if (conflict && conflict.id !== id) {
throw new ConflictException(`Container type with code "${dto.code}" already exists`);
}
}
const updated = await this.repository.update(id, dto);
if (!updated) throw new NotFoundException(`Container type ${id} not found`);
return updated;

View File

@@ -1,4 +1,5 @@
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { generateCode } from '../../../common/utils/generate-code.util';
import { CreatePriorityRuleDto } from '../dto/create-priority-rule.dto';
import { UpdatePriorityRuleDto } from '../dto/update-priority-rule.dto';
import { PriorityRule } from '../entities/priority-rule.entity';
@@ -27,7 +28,7 @@ export class PriorityRulesService {
const [data, total] = await this.repository.findAndCount({
where,
order: { code: 'ASC' },
order: { label: 'ASC' },
skip: (page - 1) * pageSize,
take: pageSize,
});
@@ -43,12 +44,13 @@ export class PriorityRulesService {
/** Create a new priority rule. */
async create(dto: CreatePriorityRuleDto): Promise<PriorityRule> {
const existing = await this.repository.findAll({ where: { code: dto.code } });
const code = generateCode(dto.label);
const existing = await this.repository.findAll({ where: { code } });
if (existing.length > 0) {
throw new ConflictException(`Priority rule with code "${dto.code}" already exists`);
throw new ConflictException(`Priority rule with label "${dto.label}" conflicts with existing code "${code}"`);
}
return this.repository.create({
code: dto.code,
code,
label: dto.label,
score: dto.score,
conditionCurrency: dto.conditionCurrency ?? null,
@@ -59,7 +61,8 @@ export class PriorityRulesService {
/** Update an existing priority rule. */
async update(id: string, dto: UpdatePriorityRuleDto): Promise<PriorityRule> {
await this.findById(id);
const updated = await this.repository.update(id, dto);
const { ...patch } = dto;
const updated = await this.repository.update(id, patch);
if (!updated) throw new NotFoundException(`Priority rule ${id} not found`);
return updated;
}

View File

@@ -1,5 +1,6 @@
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { ILike } from 'typeorm';
import { generateCode } from '../../../common/utils/generate-code.util';
import { CreateServiceTypeDto } from '../dto/create-service-type.dto';
import { UpdateServiceTypeDto } from '../dto/update-service-type.dto';
import { ServiceType } from '../entities/service-type.entity';
@@ -55,10 +56,11 @@ export class ServiceTypesService {
/** Create a new service type. */
async create(dto: CreateServiceTypeDto): Promise<ServiceType> {
const existing = await this.repository.findByCode(dto.code);
if (existing) throw new ConflictException(`Service type with code "${dto.code}" already exists`);
const code = generateCode(dto.serviceName);
const existing = await this.repository.findByCode(code);
if (existing) throw new ConflictException(`Service type with name "${dto.serviceName}" conflicts with existing code "${code}"`);
return this.repository.create({
code: dto.code,
code,
serviceName: dto.serviceName,
description: dto.description ?? null,
canBeBookedAlone: dto.canBeBookedAlone ?? true,
@@ -74,13 +76,8 @@ export class ServiceTypesService {
/** Update an existing service type. */
async update(id: string, dto: UpdateServiceTypeDto): Promise<ServiceType> {
await this.findById(id);
if (dto.code) {
const conflict = await this.repository.findByCode(dto.code);
if (conflict && conflict.id !== id) {
throw new ConflictException(`Service type with code "${dto.code}" already exists`);
}
}
const updated = await this.repository.update(id, dto);
const { ...patch } = dto;
const updated = await this.repository.update(id, patch);
if (!updated) throw new NotFoundException(`Service type ${id} not found`);
return updated;
}

View File

@@ -1,4 +1,5 @@
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { generateCode } from '../../../common/utils/generate-code.util';
import { CreateSurchargeTypeDto } from '../dto/create-surcharge-type.dto';
import { UpdateSurchargeTypeDto } from '../dto/update-surcharge-type.dto';
import { SurchargeType } from '../entities/surcharge-type.entity';
@@ -43,10 +44,11 @@ export class SurchargeTypesService {
/** Create a new surcharge type. */
async create(dto: CreateSurchargeTypeDto): Promise<SurchargeType> {
const existing = await this.repository.findByCode(dto.code);
if (existing) throw new ConflictException(`Surcharge type with code "${dto.code}" already exists`);
const code = generateCode(dto.label);
const existing = await this.repository.findByCode(code);
if (existing) throw new ConflictException(`Surcharge type with label "${dto.label}" conflicts with existing code "${code}"`);
return this.repository.create({
code: dto.code,
code,
label: dto.label,
triggerCondition: dto.triggerCondition as SurchargeType['triggerCondition'],
rateId: dto.rateId,
@@ -57,14 +59,7 @@ export class SurchargeTypesService {
/** Update an existing surcharge type. */
async update(id: string, dto: UpdateSurchargeTypeDto): Promise<SurchargeType> {
await this.findById(id);
if (dto.code) {
const conflict = await this.repository.findByCode(dto.code);
if (conflict && conflict.id !== id) {
throw new ConflictException(`Surcharge type with code "${dto.code}" already exists`);
}
}
const patch: Partial<SurchargeType> = {};
if (dto.code !== undefined) patch.code = dto.code;
if (dto.label !== undefined) patch.label = dto.label;
if (dto.triggerCondition !== undefined) patch.triggerCondition = dto.triggerCondition as SurchargeType['triggerCondition'];
if (dto.rateId !== undefined) patch.rateId = dto.rateId;

View File

@@ -1,4 +1,5 @@
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { generateCode } from '../../../common/utils/generate-code.util';
import { CreateYardDto } from '../dto/create-yard.dto';
import { UpdateYardDto } from '../dto/update-yard.dto';
import { Yard } from '../entities/yard.entity';
@@ -26,7 +27,7 @@ export class YardsService {
const [data, total] = await this.repository.findAndCount({
where,
order: { displayOrder: 'ASC', code: 'ASC' },
order: { displayOrder: 'ASC', label: 'ASC' },
skip: (page - 1) * pageSize,
take: pageSize,
});
@@ -42,10 +43,11 @@ export class YardsService {
/** Create a yard. */
async create(dto: CreateYardDto): Promise<Yard> {
const existing = await this.repository.findByCode(dto.code);
if (existing) throw new ConflictException(`Yard with code "${dto.code}" already exists`);
const code = generateCode(dto.label);
const existing = await this.repository.findByCode(code);
if (existing) throw new ConflictException(`Yard with label "${dto.label}" conflicts with existing code "${code}"`);
return this.repository.create({
code: dto.code,
code,
label: dto.label,
country: dto.country,
isActive: dto.isActive ?? true,
@@ -56,12 +58,6 @@ export class YardsService {
/** Update a yard. */
async update(id: string, dto: UpdateYardDto): Promise<Yard> {
await this.findById(id);
if (dto.code) {
const conflict = await this.repository.findByCode(dto.code);
if (conflict && conflict.id !== id) {
throw new ConflictException(`Yard with code "${dto.code}" already exists`);
}
}
const updated = await this.repository.update(id, dto);
if (!updated) throw new NotFoundException(`Yard ${id} not found`);
return updated;