mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
Merge branch 'freight/develop' into freight/feature/edr_org_seeder
This commit is contained in:
15
apps/edr-freight-api/src/common/utils/generate-code.util.ts
Normal file
15
apps/edr-freight-api/src/common/utils/generate-code.util.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Derives a stable, uppercase, underscore-separated code from a human-readable name.
|
||||
*
|
||||
* Examples:
|
||||
* "Hazard Surcharge" → "HAZARD_SURCHARGE"
|
||||
* "20ft Dry Container" → "20FT_DRY_CONTAINER"
|
||||
* "Kality Yard (ET)" → "KALITY_YARD_ET"
|
||||
*/
|
||||
export function generateCode(name: string): string {
|
||||
return name
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Align weight_limit_rules.trade_direction with app code: IMPORT, EXPORT, BOTH (not ANY).
|
||||
*/
|
||||
export class NormalizeWeightLimitTradeDirectionBoth1749000000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'NormalizeWeightLimitTradeDirectionBoth1749000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
UPDATE freight.weight_limit_rules
|
||||
SET trade_direction = 'BOTH'
|
||||
WHERE trade_direction::text = 'ANY';
|
||||
EXCEPTION WHEN undefined_table OR undefined_column THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(_queryRunner: QueryRunner): Promise<void> {
|
||||
// No-op: ANY is not a valid enum value in PostgreSQL.
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user