mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
Merge pull request #52 from Tria-plc/freight_feature/booking/minio
implement rule engine module with dynamic booking evaluation
This commit is contained in:
@@ -18,8 +18,7 @@ import { NotificationsModule } from "./modules/notifications/notifications.modul
|
|||||||
import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module";
|
import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module";
|
||||||
import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module";
|
import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module";
|
||||||
import { OtpModule } from './modules/otp/otp.module';
|
import { OtpModule } from './modules/otp/otp.module';
|
||||||
import { ServiceTypesModule } from "./modules/service-types/service-types.module";
|
import { RuleEngineModule } from "./modules/rule-engine/rule-engine.module";
|
||||||
import { CargoTypesModule } from "./modules/cargo-types/cargo-types.module";
|
|
||||||
import { BackofficeModule } from "./modules/backoffice/backoffice.module";
|
import { BackofficeModule } from "./modules/backoffice/backoffice.module";
|
||||||
import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
||||||
|
|
||||||
@@ -47,8 +46,7 @@ import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
|||||||
FileUploadSettingsModule,
|
FileUploadSettingsModule,
|
||||||
DropdownSettingsModule,
|
DropdownSettingsModule,
|
||||||
OtpModule,
|
OtpModule,
|
||||||
ServiceTypesModule,
|
RuleEngineModule,
|
||||||
CargoTypesModule,
|
|
||||||
BackofficeModule,
|
BackofficeModule,
|
||||||
],
|
],
|
||||||
providers: [EdrOrgSeeder],
|
providers: [EdrOrgSeeder],
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
import {
|
||||||
|
MigrationInterface,
|
||||||
|
QueryRunner,
|
||||||
|
Table,
|
||||||
|
TableIndex,
|
||||||
|
TableForeignKey,
|
||||||
|
TableColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
export class AddRuleEngineTablesAndCodes1748514000000 implements MigrationInterface {
|
||||||
|
name = 'AddRuleEngineTablesAndCodes1748514000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// ── 1. Add `code` column to existing tables ───────────────────────────
|
||||||
|
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
'freight.service_types',
|
||||||
|
new TableColumn({
|
||||||
|
name: 'code',
|
||||||
|
type: 'varchar',
|
||||||
|
length: '50',
|
||||||
|
isNullable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE freight.service_types SET code = upper(replace(service_name, ' ', '_')) WHERE code IS NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.changeColumn(
|
||||||
|
'freight.service_types',
|
||||||
|
'code',
|
||||||
|
new TableColumn({ name: 'code', type: 'varchar', length: '50', isNullable: false }),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.service_types',
|
||||||
|
new TableIndex({ name: 'IDX_service_types_code', columnNames: ['code'], isUnique: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
'freight.cargo_types',
|
||||||
|
new TableColumn({
|
||||||
|
name: 'code',
|
||||||
|
type: 'varchar',
|
||||||
|
length: '50',
|
||||||
|
isNullable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE freight.cargo_types SET code = upper(replace(cargo_type_name, ' ', '_')) WHERE code IS NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.changeColumn(
|
||||||
|
'freight.cargo_types',
|
||||||
|
'code',
|
||||||
|
new TableColumn({ name: 'code', type: 'varchar', length: '50', isNullable: false }),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.cargo_types',
|
||||||
|
new TableIndex({ name: 'IDX_cargo_types_code', columnNames: ['code'], isUnique: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 2. surcharge_types ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'surcharge_types',
|
||||||
|
schema: 'freight',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||||
|
{ name: 'code', type: 'varchar', length: '50', isNullable: false },
|
||||||
|
{ name: 'name', type: 'varchar', length: '100', isNullable: false },
|
||||||
|
{ name: 'description', type: 'text', isNullable: true },
|
||||||
|
{ name: 'is_active', type: 'boolean', default: true },
|
||||||
|
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.surcharge_types',
|
||||||
|
new TableIndex({ name: 'IDX_surcharge_types_code', columnNames: ['code'], isUnique: true }),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.surcharge_types',
|
||||||
|
new TableIndex({ name: 'IDX_surcharge_types_is_active', columnNames: ['is_active'] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 3. surcharges ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'surcharges',
|
||||||
|
schema: 'freight',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||||
|
{ name: 'surcharge_type_id', type: 'uuid', isNullable: false },
|
||||||
|
{ name: 'fee_name', type: 'varchar', length: '255', isNullable: false },
|
||||||
|
{ name: 'trigger_description', type: 'text', isNullable: true },
|
||||||
|
{
|
||||||
|
name: 'calculation_method',
|
||||||
|
type: 'enum',
|
||||||
|
enum: ['PER_TON', 'FLAT_FEE', 'PERCENTAGE'],
|
||||||
|
default: `'PER_TON'`,
|
||||||
|
},
|
||||||
|
{ name: 'rate', type: 'numeric', precision: 10, scale: 2, isNullable: false },
|
||||||
|
{ name: 'currency', type: 'char', length: '3', default: `'USD'` },
|
||||||
|
{ name: 'apply_to_rail', type: 'boolean', default: false },
|
||||||
|
{ name: 'apply_to_first_mile', type: 'boolean', default: false },
|
||||||
|
{ name: 'apply_to_last_mile', type: 'boolean', default: false },
|
||||||
|
{ name: 'is_active', type: 'boolean', default: true },
|
||||||
|
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'freight.surcharges',
|
||||||
|
new TableForeignKey({
|
||||||
|
name: 'FK_surcharges_surcharge_type',
|
||||||
|
columnNames: ['surcharge_type_id'],
|
||||||
|
referencedTableName: 'freight.surcharge_types',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'RESTRICT',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.surcharges',
|
||||||
|
new TableIndex({ name: 'IDX_surcharges_surcharge_type_id', columnNames: ['surcharge_type_id'] }),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.surcharges',
|
||||||
|
new TableIndex({ name: 'IDX_surcharges_is_active', columnNames: ['is_active'] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 4. container_types ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'container_types',
|
||||||
|
schema: 'freight',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||||
|
{ name: 'size_code', type: 'varchar', length: '20', isNullable: false },
|
||||||
|
{ name: 'description', type: 'varchar', length: '100', isNullable: true },
|
||||||
|
{ name: 'containers_per_wagon', type: 'int', isNullable: false },
|
||||||
|
{ name: 'is_active', type: 'boolean', default: true },
|
||||||
|
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.container_types',
|
||||||
|
new TableIndex({ name: 'IDX_container_types_size_code', columnNames: ['size_code'], isUnique: true }),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.container_types',
|
||||||
|
new TableIndex({ name: 'IDX_container_types_is_active', columnNames: ['is_active'] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 5. weight_limit_rules ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'weight_limit_rules',
|
||||||
|
schema: 'freight',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||||
|
{ name: 'container_type_id', type: 'uuid', isNullable: false },
|
||||||
|
{
|
||||||
|
name: 'trade_direction',
|
||||||
|
type: 'enum',
|
||||||
|
enum: ['IMPORT', 'EXPORT', 'BOTH'],
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{ name: 'max_weight_tons', type: 'numeric', precision: 10, scale: 2, isNullable: false },
|
||||||
|
{ name: 'warning_threshold_tons', type: 'numeric', precision: 10, scale: 2, isNullable: false },
|
||||||
|
{
|
||||||
|
name: 'exceeded_action',
|
||||||
|
type: 'enum',
|
||||||
|
enum: ['WARNING_ONLY', 'HARD_BLOCK'],
|
||||||
|
default: `'WARNING_ONLY'`,
|
||||||
|
},
|
||||||
|
{ name: 'surcharge_id', type: 'uuid', isNullable: true },
|
||||||
|
{ name: 'is_active', type: 'boolean', default: true },
|
||||||
|
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'freight.weight_limit_rules',
|
||||||
|
new TableForeignKey({
|
||||||
|
name: 'FK_weight_limit_rules_container_type',
|
||||||
|
columnNames: ['container_type_id'],
|
||||||
|
referencedTableName: 'freight.container_types',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'RESTRICT',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'freight.weight_limit_rules',
|
||||||
|
new TableForeignKey({
|
||||||
|
name: 'FK_weight_limit_rules_surcharge',
|
||||||
|
columnNames: ['surcharge_id'],
|
||||||
|
referencedTableName: 'freight.surcharges',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.weight_limit_rules',
|
||||||
|
new TableIndex({ name: 'IDX_weight_limit_rules_container_type_id', columnNames: ['container_type_id'] }),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.weight_limit_rules',
|
||||||
|
new TableIndex({ name: 'IDX_weight_limit_rules_surcharge_id', columnNames: ['surcharge_id'] }),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.weight_limit_rules',
|
||||||
|
new TableIndex({ name: 'IDX_weight_limit_rules_is_active', columnNames: ['is_active'] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 6. priority_rules ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'priority_rules',
|
||||||
|
schema: 'freight',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||||
|
{
|
||||||
|
name: 'priority_type',
|
||||||
|
type: 'enum',
|
||||||
|
enum: ['USD_PAYER', 'RAIL_AND_FORWARDING', 'GOVERNMENT_ACCOUNT', 'HIGH_VOLUME_SHIPMENT'],
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{ name: 'rule_name', type: 'varchar', length: '255', isNullable: false },
|
||||||
|
{ name: 'description', type: 'text', isNullable: true },
|
||||||
|
{ name: 'activation_condition', type: 'text', isNullable: true },
|
||||||
|
{ name: 'bonus_points', type: 'int', default: 0 },
|
||||||
|
{ name: 'is_active', type: 'boolean', default: false },
|
||||||
|
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||||
|
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.priority_rules',
|
||||||
|
new TableIndex({ name: 'IDX_priority_rules_priority_type', columnNames: ['priority_type'], isUnique: true }),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
'freight.priority_rules',
|
||||||
|
new TableIndex({ name: 'IDX_priority_rules_is_active', columnNames: ['is_active'] }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable('freight.priority_rules', true);
|
||||||
|
await queryRunner.dropTable('freight.weight_limit_rules', true);
|
||||||
|
await queryRunner.dropTable('freight.container_types', true);
|
||||||
|
await queryRunner.dropTable('freight.surcharges', true);
|
||||||
|
await queryRunner.dropTable('freight.surcharge_types', true);
|
||||||
|
await queryRunner.dropIndex('freight.cargo_types', 'IDX_cargo_types_code');
|
||||||
|
await queryRunner.dropColumn('freight.cargo_types', 'code');
|
||||||
|
await queryRunner.dropIndex('freight.service_types', 'IDX_service_types_code');
|
||||||
|
await queryRunner.dropColumn('freight.service_types', 'code');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,14 @@ import { TypeOrmModule } from "@nestjs/typeorm";
|
|||||||
import { CustomersModule } from "../customers/customers.module";
|
import { CustomersModule } from "../customers/customers.module";
|
||||||
import { FilesModule } from "../files/files.module";
|
import { FilesModule } from "../files/files.module";
|
||||||
import { MinioModule } from "../minio/minio.module";
|
import { MinioModule } from "../minio/minio.module";
|
||||||
|
import { RuleEngineModule } from "../rule-engine/rule-engine.module";
|
||||||
import { BookingsController } from "./bookings.controller";
|
import { BookingsController } from "./bookings.controller";
|
||||||
import { BookingsRepository } from "./bookings.repository";
|
import { BookingsRepository } from "./bookings.repository";
|
||||||
import { BookingsService } from "./bookings.service";
|
import { BookingsService } from "./bookings.service";
|
||||||
import { Booking } from "./entities/booking.entity";
|
import { Booking } from "./entities/booking.entity";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Booking]), FilesModule, MinioModule, CustomersModule],
|
imports: [TypeOrmModule.forFeature([Booking]), FilesModule, MinioModule, CustomersModule, RuleEngineModule],
|
||||||
controllers: [BookingsController],
|
controllers: [BookingsController],
|
||||||
providers: [BookingsService, BookingsRepository],
|
providers: [BookingsService, BookingsRepository],
|
||||||
exports: [BookingsService],
|
exports: [BookingsService],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { IsNull, Not } from "typeorm";
|
|||||||
import { CustomersService } from "../customers/customers.service";
|
import { CustomersService } from "../customers/customers.service";
|
||||||
import { FilesService } from "../files/files.service";
|
import { FilesService } from "../files/files.service";
|
||||||
import { MinioService } from "../minio/minio.service";
|
import { MinioService } from "../minio/minio.service";
|
||||||
|
import { RuleEngineService } from "../rule-engine/rule-engine.service";
|
||||||
import { BookingsRepository } from "./bookings.repository";
|
import { BookingsRepository } from "./bookings.repository";
|
||||||
import { CreateBookingDto } from "./dto/create-booking.dto";
|
import { CreateBookingDto } from "./dto/create-booking.dto";
|
||||||
import { FilterBookingDto } from "./dto/filter-booking.dto";
|
import { FilterBookingDto } from "./dto/filter-booking.dto";
|
||||||
@@ -17,16 +18,6 @@ import { UpdateStatusDto } from "./dto/update-status.dto";
|
|||||||
import { Booking } from "./entities/booking.entity";
|
import { Booking } from "./entities/booking.entity";
|
||||||
import { FileRecord } from "../files/entities/file.entity";
|
import { FileRecord } from "../files/entities/file.entity";
|
||||||
|
|
||||||
/** Weight thresholds (tons) that trigger overweight surcharge alerts. */
|
|
||||||
const WEIGHT_LIMITS = {
|
|
||||||
IMPORT_20FT: 20,
|
|
||||||
EXPORT_20FT: 25,
|
|
||||||
ANY_40FT: 32.5,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** Bookings above this total VGM are considered high-volume. */
|
|
||||||
const HIGH_VOLUME_THRESHOLD_TONS = 500;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BookingsService {
|
export class BookingsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -34,6 +25,7 @@ export class BookingsService {
|
|||||||
private readonly filesService: FilesService,
|
private readonly filesService: FilesService,
|
||||||
private readonly minioService: MinioService,
|
private readonly minioService: MinioService,
|
||||||
private readonly customersService: CustomersService,
|
private readonly customersService: CustomersService,
|
||||||
|
private readonly ruleEngineService: RuleEngineService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ── helpers ──────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────
|
||||||
@@ -65,15 +57,6 @@ export class BookingsService {
|
|||||||
return explicit ?? false;
|
return explicit ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Calculate priority score based on currency and service type. */
|
|
||||||
private calculatePriorityScore(currency: string, serviceType: string): number {
|
|
||||||
let score = 0;
|
|
||||||
if (currency === "USD") score += 100;
|
|
||||||
if (serviceType === "RAIL_AND_FORWARDING") score += 50;
|
|
||||||
else if (serviceType === "RAIL_ONLY") score += 25;
|
|
||||||
return score;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Calculate required wagons: each 40ft = 1 wagon, each pair of 20ft = 1 wagon. */
|
/** Calculate required wagons: each 40ft = 1 wagon, each pair of 20ft = 1 wagon. */
|
||||||
private calculateWagonCount(
|
private calculateWagonCount(
|
||||||
containers: Array<{ type: string; qty: number }>,
|
containers: Array<{ type: string; qty: number }>,
|
||||||
@@ -87,33 +70,6 @@ export class BookingsService {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check per-container weight limits and return warnings if exceeded. */
|
|
||||||
private checkOverweight(
|
|
||||||
containers: Array<{ type: string; vgm: number }>,
|
|
||||||
tradeDirection: string,
|
|
||||||
): string[] {
|
|
||||||
const warnings: string[] = [];
|
|
||||||
for (const container of containers) {
|
|
||||||
if (container.type === "40FT" && container.vgm > WEIGHT_LIMITS.ANY_40FT) {
|
|
||||||
warnings.push(
|
|
||||||
`40FT container VGM ${container.vgm}t exceeds limit of ${WEIGHT_LIMITS.ANY_40FT}t`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (container.type === "20FT") {
|
|
||||||
const limit =
|
|
||||||
tradeDirection === "IMPORT"
|
|
||||||
? WEIGHT_LIMITS.IMPORT_20FT
|
|
||||||
: WEIGHT_LIMITS.EXPORT_20FT;
|
|
||||||
if (container.vgm > limit) {
|
|
||||||
warnings.push(
|
|
||||||
`20FT ${tradeDirection} container VGM ${container.vgm}t exceeds limit of ${limit}t`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return warnings;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ── CRUD ─────────────────────────────────────────────────────────────
|
// ── CRUD ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -143,16 +99,19 @@ export class BookingsService {
|
|||||||
dto.allowConsolidation,
|
dto.allowConsolidation,
|
||||||
);
|
);
|
||||||
|
|
||||||
const priorityScore = this.calculatePriorityScore(
|
// ── Rule engine evaluation ──────────────────────────────────────────
|
||||||
dto.paymentCurrency,
|
const ruleResult = await this.ruleEngineService.evaluate({
|
||||||
dto.serviceType,
|
freightType: dto.freightType,
|
||||||
);
|
serviceType: dto.serviceType,
|
||||||
|
paymentCurrency: dto.paymentCurrency,
|
||||||
const overweightWarnings = this.checkOverweight(
|
cargoTotalWeightVgm: dto.cargoTotalWeightVgm,
|
||||||
dto.containers,
|
tradeDirection: dto.tradeDirection,
|
||||||
dto.tradeDirection,
|
isHazardous: dto.isHazardous ?? false,
|
||||||
);
|
isRefrigerated: dto.isRefrigerated ?? false,
|
||||||
warnings.push(...overweightWarnings);
|
containers: dto.containers,
|
||||||
|
});
|
||||||
|
this.ruleEngineService.assertNoHardBlocks(ruleResult);
|
||||||
|
warnings.push(...ruleResult.warnings);
|
||||||
|
|
||||||
const wagonCount = this.calculateWagonCount(dto.containers);
|
const wagonCount = this.calculateWagonCount(dto.containers);
|
||||||
warnings.push(`Estimated wagons required: ${wagonCount}`);
|
warnings.push(`Estimated wagons required: ${wagonCount}`);
|
||||||
@@ -168,7 +127,7 @@ export class BookingsService {
|
|||||||
endDate: dto.endDate ? new Date(dto.endDate) : undefined,
|
endDate: dto.endDate ? new Date(dto.endDate) : undefined,
|
||||||
status: "DRAFT",
|
status: "DRAFT",
|
||||||
allowConsolidation,
|
allowConsolidation,
|
||||||
priorityScore,
|
priorityScore: ruleResult.priorityScore,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
@@ -208,15 +167,20 @@ export class BookingsService {
|
|||||||
dto.allowConsolidation,
|
dto.allowConsolidation,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Recalculate priority
|
// ── Rule engine re-evaluation ────────────────────────────────────────
|
||||||
const currency = dto.paymentCurrency ?? existing.paymentCurrency;
|
const ruleResult = await this.ruleEngineService.evaluate({
|
||||||
const serviceType = dto.serviceType ?? existing.serviceType;
|
freightType: dto.freightType ?? existing.freightType,
|
||||||
updates.priorityScore = this.calculatePriorityScore(currency, serviceType);
|
serviceType: dto.serviceType ?? existing.serviceType,
|
||||||
|
paymentCurrency: dto.paymentCurrency ?? existing.paymentCurrency,
|
||||||
// Overweight check
|
cargoTotalWeightVgm: dto.cargoTotalWeightVgm ?? existing.cargoTotalWeightVgm,
|
||||||
const direction = dto.tradeDirection ?? existing.tradeDirection;
|
tradeDirection: dto.tradeDirection ?? existing.tradeDirection,
|
||||||
const overweightWarnings = this.checkOverweight(containers, direction);
|
isHazardous: dto.isHazardous ?? existing.isHazardous ?? false,
|
||||||
warnings.push(...overweightWarnings);
|
isRefrigerated: dto.isRefrigerated ?? existing.isRefrigerated ?? false,
|
||||||
|
containers,
|
||||||
|
});
|
||||||
|
this.ruleEngineService.assertNoHardBlocks(ruleResult);
|
||||||
|
warnings.push(...ruleResult.warnings);
|
||||||
|
updates.priorityScore = ruleResult.priorityScore;
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
await this.filesService.uploadMany(id, "bookings", files);
|
await this.filesService.uploadMany(id, "bookings", files);
|
||||||
@@ -348,13 +312,13 @@ export class BookingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** SUBMIT: DRAFT → PENDING_LINE_STAFF or PENDING_DIRECTOR (bulk). */
|
/** SUBMIT: DRAFT → PENDING_LINE_STAFF or PENDING_DIRECTOR (cargo routing from rule engine). */
|
||||||
private async handleSubmit(booking: Booking): Promise<Booking> {
|
private async handleSubmit(booking: Booking): Promise<Booking> {
|
||||||
this.assertStatus(booking, ["DRAFT"]);
|
this.assertStatus(booking, ["DRAFT"]);
|
||||||
const isBulk =
|
const ruleResult = await this.ruleEngineService.evaluate(booking);
|
||||||
booking.freightType === "BULK" ||
|
const nextStatus = ruleResult.requiresDirectorApproval
|
||||||
booking.cargoTotalWeightVgm > HIGH_VOLUME_THRESHOLD_TONS;
|
? "PENDING_DIRECTOR"
|
||||||
const nextStatus = isBulk ? "PENDING_DIRECTOR" : "PENDING_LINE_STAFF";
|
: "PENDING_LINE_STAFF";
|
||||||
const updated = await this.bookingsRepository.update(booking.id, {
|
const updated = await this.bookingsRepository.update(booking.id, {
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
} as never);
|
} as never);
|
||||||
@@ -370,13 +334,11 @@ export class BookingsService {
|
|||||||
if (!actorId)
|
if (!actorId)
|
||||||
throw new BadRequestException("actorId is required for APPROVE_STAFF");
|
throw new BadRequestException("actorId is required for APPROVE_STAFF");
|
||||||
|
|
||||||
// Line staff cannot approve bulk
|
// Line staff cannot approve bookings that require director approval
|
||||||
if (
|
const ruleResult = await this.ruleEngineService.evaluate(booking);
|
||||||
booking.freightType === "BULK" ||
|
if (ruleResult.requiresDirectorApproval) {
|
||||||
booking.cargoTotalWeightVgm > HIGH_VOLUME_THRESHOLD_TONS
|
|
||||||
) {
|
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Line staff cannot approve bulk or high-volume bookings",
|
"Line staff cannot approve bookings that require director approval",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,10 +362,8 @@ export class BookingsService {
|
|||||||
if (!actorId)
|
if (!actorId)
|
||||||
throw new BadRequestException("actorId is required for APPROVE_DIRECTOR");
|
throw new BadRequestException("actorId is required for APPROVE_DIRECTOR");
|
||||||
|
|
||||||
const isBulk =
|
const ruleResult = await this.ruleEngineService.evaluate(booking);
|
||||||
booking.freightType === "BULK" ||
|
const nextStatus = ruleResult.requiresDirectorApproval ? "PENDING_CEO" : "SIGNED";
|
||||||
booking.cargoTotalWeightVgm > HIGH_VOLUME_THRESHOLD_TONS;
|
|
||||||
const nextStatus = isBulk ? "PENDING_CEO" : "SIGNED";
|
|
||||||
|
|
||||||
const updated = await this.bookingsRepository.update(booking.id, {
|
const updated = await this.bookingsRepository.update(booking.id, {
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
HttpCode,
|
|
||||||
HttpStatus,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
Query,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
|
||||||
|
|
||||||
import { CargoTypesService } from "./cargo-types.service";
|
|
||||||
import { CreateCargoTypeDto } from "./dto/create-cargo-type.dto";
|
|
||||||
import { FilterCargoTypeDto } from "./dto/filter-cargo-type.dto";
|
|
||||||
import { UpdateCargoTypeDto } from "./dto/update-cargo-type.dto";
|
|
||||||
|
|
||||||
@ApiTags("cargo-types")
|
|
||||||
@Controller("cargo-types")
|
|
||||||
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
|
||||||
@ApiBearerAuth()
|
|
||||||
export class CargoTypesController {
|
|
||||||
constructor(private readonly service: CargoTypesService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@ApiOperation({
|
|
||||||
summary: "List cargo types",
|
|
||||||
description: "Paginated list with optional filtering by isActive, requiresDirectorApproval, parentGroupId, and name search.",
|
|
||||||
})
|
|
||||||
findAll(@Query() filter: FilterCargoTypeDto) {
|
|
||||||
return this.service.findAll(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(":id")
|
|
||||||
@ApiOperation({ summary: "Get a cargo type by ID", description: "Returns the cargo type with parent and children relations." })
|
|
||||||
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
|
||||||
return this.service.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@ApiOperation({ summary: "Create a new cargo type" })
|
|
||||||
create(@Body() dto: CreateCargoTypeDto) {
|
|
||||||
return this.service.create(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch(":id")
|
|
||||||
@ApiOperation({ summary: "Update a cargo type" })
|
|
||||||
update(
|
|
||||||
@Param("id", ParseUUIDPipe) id: string,
|
|
||||||
@Body() dto: UpdateCargoTypeDto,
|
|
||||||
) {
|
|
||||||
return this.service.update(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(":id")
|
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
|
||||||
@ApiOperation({ summary: "Soft-delete a cargo type" })
|
|
||||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
|
||||||
return this.service.remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
||||||
|
|
||||||
import { CargoType } from "./entities/cargo-type.entity";
|
|
||||||
import { CARGO_TYPES_REPOSITORY } from "./interfaces/cargo-types.repository.interface";
|
|
||||||
import { CargoTypesRepository } from "./cargo-types.repository";
|
|
||||||
import { CargoTypesController } from "./cargo-types.controller";
|
|
||||||
import { CargoTypesService } from "./cargo-types.service";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TypeOrmModule.forFeature([CargoType])],
|
|
||||||
controllers: [CargoTypesController],
|
|
||||||
providers: [
|
|
||||||
CargoTypesRepository,
|
|
||||||
{
|
|
||||||
provide: CARGO_TYPES_REPOSITORY,
|
|
||||||
useExisting: CargoTypesRepository,
|
|
||||||
},
|
|
||||||
CargoTypesService,
|
|
||||||
],
|
|
||||||
exports: [CargoTypesService],
|
|
||||||
})
|
|
||||||
export class CargoTypesModule {}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { BaseRepository } from "@edr/api-common";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
|
||||||
import { FindManyOptions, Repository } from "typeorm";
|
|
||||||
|
|
||||||
import { CargoType } from "./entities/cargo-type.entity";
|
|
||||||
import { ICargoTypesRepository } from "./interfaces/cargo-types.repository.interface";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CargoTypesRepository
|
|
||||||
extends BaseRepository<CargoType>
|
|
||||||
implements ICargoTypesRepository
|
|
||||||
{
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(CargoType)
|
|
||||||
repository: Repository<CargoType>,
|
|
||||||
) {
|
|
||||||
super(repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
override findById(id: string): Promise<CargoType | null> {
|
|
||||||
return this.repository.findOne({
|
|
||||||
where: { id },
|
|
||||||
relations: { parent: true, children: true },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
override findAndCount(options?: FindManyOptions<CargoType>): Promise<[CargoType[], number]> {
|
|
||||||
return this.repository.findAndCount(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { ConflictException, Inject, Injectable, NotFoundException } from "@nestjs/common";
|
|
||||||
import { ILike } from "typeorm";
|
|
||||||
|
|
||||||
import { CreateCargoTypeDto } from "./dto/create-cargo-type.dto";
|
|
||||||
import { FilterCargoTypeDto } from "./dto/filter-cargo-type.dto";
|
|
||||||
import { UpdateCargoTypeDto } from "./dto/update-cargo-type.dto";
|
|
||||||
import { CargoType } from "./entities/cargo-type.entity";
|
|
||||||
import {
|
|
||||||
CARGO_TYPES_REPOSITORY,
|
|
||||||
ICargoTypesRepository,
|
|
||||||
} from "./interfaces/cargo-types.repository.interface";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CargoTypesService {
|
|
||||||
constructor(
|
|
||||||
@Inject(CARGO_TYPES_REPOSITORY)
|
|
||||||
private readonly repository: ICargoTypesRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all cargo types with pagination and optional filtering.
|
|
||||||
*/
|
|
||||||
async findAll(filter: FilterCargoTypeDto): Promise<{
|
|
||||||
data: CargoType[];
|
|
||||||
meta: {
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
totalPages: number;
|
|
||||||
};
|
|
||||||
}> {
|
|
||||||
const where: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
if (filter.isActive !== undefined) {
|
|
||||||
where.isActive = filter.isActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.requiresDirectorApproval !== undefined) {
|
|
||||||
where.requiresDirectorApproval = filter.requiresDirectorApproval;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.parentGroupId !== undefined) {
|
|
||||||
where.parentGroupId = filter.parentGroupId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.search) {
|
|
||||||
where.cargoTypeName = ILike(`%${filter.search}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [data, total] = await this.repository.findAndCount({
|
|
||||||
where,
|
|
||||||
order: { [filter.sortBy!]: filter.sortOrder },
|
|
||||||
skip: (filter.page! - 1) * filter.pageSize!,
|
|
||||||
take: filter.pageSize,
|
|
||||||
relations: { parent: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page: filter.page!,
|
|
||||||
pageSize: filter.pageSize!,
|
|
||||||
totalPages: Math.ceil(total / filter.pageSize!),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single cargo type by ID.
|
|
||||||
*/
|
|
||||||
async findById(id: string): Promise<CargoType> {
|
|
||||||
const entity = await this.repository.findById(id);
|
|
||||||
if (!entity) {
|
|
||||||
throw new NotFoundException(`Cargo type ${id} not found`);
|
|
||||||
}
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new cargo type.
|
|
||||||
*/
|
|
||||||
async create(dto: CreateCargoTypeDto): Promise<CargoType> {
|
|
||||||
// Validate parent exists if provided
|
|
||||||
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({
|
|
||||||
cargoTypeName: dto.cargoTypeName,
|
|
||||||
parentGroupId: dto.parentGroupId ?? null,
|
|
||||||
showFreeTextBox: dto.showFreeTextBox ?? false,
|
|
||||||
requiresDirectorApproval: dto.requiresDirectorApproval ?? false,
|
|
||||||
isActive: dto.isActive ?? true,
|
|
||||||
displayOrder: dto.displayOrder ?? 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an existing cargo type.
|
|
||||||
*/
|
|
||||||
async update(id: string, dto: UpdateCargoTypeDto): Promise<CargoType> {
|
|
||||||
await this.findById(id);
|
|
||||||
|
|
||||||
// Validate parent exists if provided
|
|
||||||
const parentGroupId = dto.parentGroupId;
|
|
||||||
if (parentGroupId) {
|
|
||||||
const parent = await this.repository.findById(parentGroupId);
|
|
||||||
if (!parent) {
|
|
||||||
throw new NotFoundException(`Parent cargo type ${parentGroupId} not found`);
|
|
||||||
}
|
|
||||||
// Prevent circular reference
|
|
||||||
if (parentGroupId === id) {
|
|
||||||
throw new ConflictException("A cargo type cannot be its own parent");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await this.repository.update(id, dto);
|
|
||||||
if (!updated) {
|
|
||||||
throw new NotFoundException(`Cargo type ${id} not found`);
|
|
||||||
}
|
|
||||||
return this.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Soft-delete a cargo type.
|
|
||||||
*/
|
|
||||||
async remove(id: string): Promise<void> {
|
|
||||||
await this.findById(id);
|
|
||||||
await this.repository.softDelete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
|
||||||
import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from "class-validator";
|
|
||||||
|
|
||||||
export class CreateCargoTypeDto {
|
|
||||||
@ApiProperty({ description: "Cargo type name", maxLength: 255 })
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(255)
|
|
||||||
cargoTypeName!: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Parent cargo type group ID (UUID) for hierarchical structure" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsUUID()
|
|
||||||
parentGroupId?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Show free text box for this cargo type", default: false })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
showFreeTextBox?: boolean = false;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Requires director approval for this cargo type", default: false })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
requiresDirectorApproval?: boolean = false;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Is the cargo type active", default: true })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isActive?: boolean = true;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Display order for UI", default: 1 })
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
displayOrder?: number = 1;
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
|
||||||
import { Transform, Type } from "class-transformer";
|
|
||||||
import { IsBoolean, IsIn, IsInt, IsOptional, IsString, IsUUID, Min } from "class-validator";
|
|
||||||
|
|
||||||
export class FilterCargoTypeDto {
|
|
||||||
@ApiPropertyOptional({ description: "Filter by active status" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@Transform(({ value }) => value === "true" || value === true)
|
|
||||||
isActive?: boolean;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Filter by requires director approval" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@Transform(({ value }) => value === "true" || value === true)
|
|
||||||
requiresDirectorApproval?: boolean;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Filter by parent group ID (or 'root' for top-level only)" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsUUID()
|
|
||||||
parentGroupId?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Search by cargo type name (case-insensitive)" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
search?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: ["cargoTypeName", "displayOrder", "createdAt"], default: "displayOrder" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(["cargoTypeName", "displayOrder", "createdAt"])
|
|
||||||
sortBy?: string = "displayOrder";
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "ASC" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(["ASC", "DESC"])
|
|
||||||
sortOrder?: "ASC" | "DESC" = "ASC";
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Page number", default: 1, minimum: 1 })
|
|
||||||
@IsOptional()
|
|
||||||
@Type(() => Number)
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
page?: number = 1;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Items per page", default: 20, minimum: 1 })
|
|
||||||
@IsOptional()
|
|
||||||
@Type(() => Number)
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
pageSize?: number = 20;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PartialType } from "@nestjs/mapped-types";
|
|
||||||
|
|
||||||
import { CreateCargoTypeDto } from "./create-cargo-type.dto";
|
|
||||||
|
|
||||||
export class UpdateCargoTypeDto extends PartialType(CreateCargoTypeDto) {}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { BaseEntity } from "@edr/api-common";
|
|
||||||
import { Column, Entity, Index, ManyToOne, OneToMany, JoinColumn } from "typeorm";
|
|
||||||
|
|
||||||
@Entity({ schema: "freight", name: "cargo_types" })
|
|
||||||
@Index(["isActive"])
|
|
||||||
@Index(["displayOrder"])
|
|
||||||
@Index(["parentGroupId"])
|
|
||||||
export class CargoType extends BaseEntity {
|
|
||||||
@Column({ name: "cargo_type_name", type: "varchar", length: 255, nullable: false })
|
|
||||||
cargoTypeName!: string;
|
|
||||||
|
|
||||||
@Column({ name: "parent_group_id", type: "uuid", nullable: true })
|
|
||||||
parentGroupId?: string | null;
|
|
||||||
|
|
||||||
@Column({ name: "show_free_text_box", type: "boolean", default: false })
|
|
||||||
showFreeTextBox!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: "requires_director_approval", type: "boolean", default: false })
|
|
||||||
requiresDirectorApproval!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: "is_active", type: "boolean", default: true })
|
|
||||||
isActive!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: "display_order", type: "int", default: 1 })
|
|
||||||
displayOrder!: number;
|
|
||||||
|
|
||||||
@ManyToOne(() => CargoType, (cargoType) => cargoType.children, {
|
|
||||||
nullable: true,
|
|
||||||
onDelete: "SET NULL",
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: "parent_group_id" })
|
|
||||||
parent?: CargoType | null;
|
|
||||||
|
|
||||||
@OneToMany(() => CargoType, (cargoType) => cargoType.parent)
|
|
||||||
children?: CargoType[];
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||||
|
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
|
||||||
|
import { UpdateCargoTypeDto } from '../dto/update-cargo-type.dto';
|
||||||
|
import { CargoTypesService } from '../services/cargo-types.service';
|
||||||
|
|
||||||
|
@ApiTags('cargo-types')
|
||||||
|
@Controller('cargo-types')
|
||||||
|
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class CargoTypesController {
|
||||||
|
constructor(private readonly service: CargoTypesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List cargo types' })
|
||||||
|
findAll(@Query() query: Record<string, string>) {
|
||||||
|
return this.service.findAll({
|
||||||
|
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
|
||||||
|
requiresDirectorApproval: query['requiresDirectorApproval'] !== undefined
|
||||||
|
? query['requiresDirectorApproval'] === 'true'
|
||||||
|
: undefined,
|
||||||
|
parentGroupId: query['parentGroupId'],
|
||||||
|
search: query['search'],
|
||||||
|
page: query['page'] ? parseInt(query['page'], 10) : undefined,
|
||||||
|
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
|
||||||
|
sortBy: query['sortBy'],
|
||||||
|
sortOrder: (query['sortOrder'] as 'ASC' | 'DESC') ?? 'ASC',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get a cargo type by ID' })
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create a cargo type' })
|
||||||
|
create(@Body() dto: CreateCargoTypeDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Update a cargo type' })
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateCargoTypeDto) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Soft-delete a cargo type' })
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||||
|
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CreateContainerTypeDto } from '../dto/create-container-type.dto';
|
||||||
|
import { UpdateContainerTypeDto } from '../dto/update-container-type.dto';
|
||||||
|
import { ContainerTypesService } from '../services/container-types.service';
|
||||||
|
|
||||||
|
@ApiTags('container-types')
|
||||||
|
@Controller('container-types')
|
||||||
|
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class ContainerTypesController {
|
||||||
|
constructor(private readonly service: ContainerTypesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List container types' })
|
||||||
|
findAll(@Query() query: Record<string, string>) {
|
||||||
|
return this.service.findAll({
|
||||||
|
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
|
||||||
|
page: query['page'] ? parseInt(query['page'], 10) : undefined,
|
||||||
|
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get a container type by ID' })
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create a container type' })
|
||||||
|
create(@Body() dto: CreateContainerTypeDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Update a container type' })
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContainerTypeDto) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Soft-delete a container type' })
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||||
|
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CreatePriorityRuleDto } from '../dto/create-priority-rule.dto';
|
||||||
|
import { UpdatePriorityRuleDto } from '../dto/update-priority-rule.dto';
|
||||||
|
import { PriorityRulesService } from '../services/priority-rules.service';
|
||||||
|
|
||||||
|
@ApiTags('priority-rules')
|
||||||
|
@Controller('priority-rules')
|
||||||
|
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class PriorityRulesController {
|
||||||
|
constructor(private readonly service: PriorityRulesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List priority rules' })
|
||||||
|
findAll(@Query() query: Record<string, string>) {
|
||||||
|
return this.service.findAll({
|
||||||
|
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
|
||||||
|
page: query['page'] ? parseInt(query['page'], 10) : undefined,
|
||||||
|
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get a priority rule by ID' })
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create a priority rule' })
|
||||||
|
create(@Body() dto: CreatePriorityRuleDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Update a priority rule' })
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdatePriorityRuleDto) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Soft-delete a priority rule' })
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||||
|
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CreateServiceTypeDto } from '../dto/create-service-type.dto';
|
||||||
|
import { UpdateServiceTypeDto } from '../dto/update-service-type.dto';
|
||||||
|
import { ServiceTypesService } from '../services/service-types.service';
|
||||||
|
|
||||||
|
@ApiTags('service-types')
|
||||||
|
@Controller('service-types')
|
||||||
|
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class ServiceTypesController {
|
||||||
|
constructor(private readonly service: ServiceTypesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List service types' })
|
||||||
|
findAll(@Query() query: Record<string, string>) {
|
||||||
|
return this.service.findAll({
|
||||||
|
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
|
||||||
|
canBeBookedAlone: query['canBeBookedAlone'] !== undefined ? query['canBeBookedAlone'] === 'true' : undefined,
|
||||||
|
search: query['search'],
|
||||||
|
page: query['page'] ? parseInt(query['page'], 10) : undefined,
|
||||||
|
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
|
||||||
|
sortBy: query['sortBy'],
|
||||||
|
sortOrder: (query['sortOrder'] as 'ASC' | 'DESC') ?? 'ASC',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get a service type by ID' })
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create a service type' })
|
||||||
|
create(@Body() dto: CreateServiceTypeDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Update a service type' })
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateServiceTypeDto) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Soft-delete a service type' })
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||||
|
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CreateSurchargeTypeDto } from '../dto/create-surcharge-type.dto';
|
||||||
|
import { UpdateSurchargeTypeDto } from '../dto/update-surcharge-type.dto';
|
||||||
|
import { SurchargeTypesService } from '../services/surcharge-types.service';
|
||||||
|
|
||||||
|
@ApiTags('surcharge-types')
|
||||||
|
@Controller('surcharge-types')
|
||||||
|
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class SurchargeTypesController {
|
||||||
|
constructor(private readonly service: SurchargeTypesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List surcharge types' })
|
||||||
|
findAll(@Query() query: Record<string, string>) {
|
||||||
|
return this.service.findAll({
|
||||||
|
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
|
||||||
|
page: query['page'] ? parseInt(query['page'], 10) : undefined,
|
||||||
|
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get a surcharge type by ID' })
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create a surcharge type' })
|
||||||
|
create(@Body() dto: CreateSurchargeTypeDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Update a surcharge type' })
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateSurchargeTypeDto) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Soft-delete a surcharge type' })
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||||
|
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CreateSurchargeDto } from '../dto/create-surcharge.dto';
|
||||||
|
import { UpdateSurchargeDto } from '../dto/update-surcharge.dto';
|
||||||
|
import { SurchargesService } from '../services/surcharges.service';
|
||||||
|
|
||||||
|
@ApiTags('surcharges')
|
||||||
|
@Controller('surcharges')
|
||||||
|
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class SurchargesController {
|
||||||
|
constructor(private readonly service: SurchargesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List surcharges' })
|
||||||
|
findAll(@Query() query: Record<string, string>) {
|
||||||
|
return this.service.findAll({
|
||||||
|
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
|
||||||
|
surchargeTypeId: query['surchargeTypeId'],
|
||||||
|
page: query['page'] ? parseInt(query['page'], 10) : undefined,
|
||||||
|
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get a surcharge by ID' })
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create a surcharge' })
|
||||||
|
create(@Body() dto: CreateSurchargeDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Update a surcharge' })
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateSurchargeDto) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Soft-delete a surcharge' })
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||||
|
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CreateWeightLimitRuleDto } from '../dto/create-weight-limit-rule.dto';
|
||||||
|
import { UpdateWeightLimitRuleDto } from '../dto/update-weight-limit-rule.dto';
|
||||||
|
import { WeightLimitRulesService } from '../services/weight-limit-rules.service';
|
||||||
|
|
||||||
|
@ApiTags('weight-limit-rules')
|
||||||
|
@Controller('weight-limit-rules')
|
||||||
|
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class WeightLimitRulesController {
|
||||||
|
constructor(private readonly service: WeightLimitRulesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List weight limit rules' })
|
||||||
|
findAll(@Query() query: Record<string, string>) {
|
||||||
|
return this.service.findAll({
|
||||||
|
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
|
||||||
|
containerTypeId: query['containerTypeId'],
|
||||||
|
page: query['page'] ? parseInt(query['page'], 10) : undefined,
|
||||||
|
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get a weight limit rule by ID' })
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create a weight limit rule' })
|
||||||
|
create(@Body() dto: CreateWeightLimitRuleDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Update a weight limit rule' })
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWeightLimitRuleDto) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Soft-delete a weight limit rule' })
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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)
|
||||||
|
cargoTypeName!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Parent group ID for hierarchical cargo types' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
parentGroupId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
showFreeTextBox?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
requiresDirectorApproval?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
displayOrder?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateContainerTypeDto {
|
||||||
|
@ApiProperty({ description: 'Size code, e.g. 20FT or 40FT', maxLength: 20 })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(20)
|
||||||
|
sizeCode!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Human-readable description', maxLength: 100 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Number of containers that fit per rail wagon (2 for 20FT, 1 for 40FT)' })
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
containersPerWagon!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsEnum, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||||
|
import { Freight } from '@edr/types';
|
||||||
|
|
||||||
|
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 })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
ruleName!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Explanation of when this rule is triggered' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: 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 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
bonusPoints!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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)
|
||||||
|
serviceName!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Detailed description' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
canBeBookedAlone?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includesFirstMile?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includesLastMile?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includesCustoms?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Priority bonus points awarded when this service is used', default: 0 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
priorityBonusPoints?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
displayOrder?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateSurchargeTypeDto {
|
||||||
|
@ApiProperty({ description: 'Unique code, e.g. HAZARDOUS, REFRIGERATED', maxLength: 50 })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(50)
|
||||||
|
code!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Display name', maxLength: 100 })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Description of when this surcharge type is triggered' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsEnum, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
||||||
|
import { Freight } from '@edr/types';
|
||||||
|
|
||||||
|
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({ description: 'Maximum allowed weight in tons before surcharge is applied' })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
maxWeightTons!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Weight at which a warning is issued (must be ≤ maxWeightTons)' })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
warningThresholdTons!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: Freight.ExceededAction, default: Freight.ExceededAction.WARNING_ONLY })
|
||||||
|
@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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateCargoTypeDto } from './create-cargo-type.dto';
|
||||||
|
|
||||||
|
export class UpdateCargoTypeDto extends PartialType(CreateCargoTypeDto) {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateContainerTypeDto } from './create-container-type.dto';
|
||||||
|
|
||||||
|
export class UpdateContainerTypeDto extends PartialType(CreateContainerTypeDto) {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreatePriorityRuleDto } from './create-priority-rule.dto';
|
||||||
|
|
||||||
|
export class UpdatePriorityRuleDto extends PartialType(CreatePriorityRuleDto) {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateServiceTypeDto } from './create-service-type.dto';
|
||||||
|
|
||||||
|
export class UpdateServiceTypeDto extends PartialType(CreateServiceTypeDto) {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateSurchargeTypeDto } from './create-surcharge-type.dto';
|
||||||
|
|
||||||
|
export class UpdateSurchargeTypeDto extends PartialType(CreateSurchargeTypeDto) {}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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 { CreateWeightLimitRuleDto } from './create-weight-limit-rule.dto';
|
||||||
|
|
||||||
|
export class UpdateWeightLimitRuleDto extends PartialType(CreateWeightLimitRuleDto) {}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { BaseEntity } from '@edr/api-common';
|
||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ schema: 'freight', name: 'cargo_types' })
|
||||||
|
@Index(['isActive'])
|
||||||
|
@Index(['displayOrder'])
|
||||||
|
@Index(['parentGroupId'])
|
||||||
|
@Index(['code'])
|
||||||
|
export class CargoType extends BaseEntity {
|
||||||
|
@Column({ name: 'code', type: 'varchar', length: 50, unique: true, default: '' })
|
||||||
|
code!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'cargo_type_name', type: 'varchar', length: 255 })
|
||||||
|
cargoTypeName!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'parent_group_id', type: 'uuid', nullable: true })
|
||||||
|
parentGroupId?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'show_free_text_box', type: 'boolean', default: false })
|
||||||
|
showFreeTextBox!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'requires_director_approval', type: 'boolean', default: false })
|
||||||
|
requiresDirectorApproval!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||||
|
isActive!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'display_order', type: 'int', default: 1 })
|
||||||
|
displayOrder!: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => CargoType, (ct) => ct.children, { nullable: true, onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'parent_group_id' })
|
||||||
|
parent?: CargoType | null;
|
||||||
|
|
||||||
|
@OneToMany(() => CargoType, (ct) => ct.parent)
|
||||||
|
children?: CargoType[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { BaseEntity } from '@edr/api-common';
|
||||||
|
import { Column, Entity, Index, OneToMany } from 'typeorm';
|
||||||
|
import { WeightLimitRule } from './weight-limit-rule.entity';
|
||||||
|
|
||||||
|
@Entity({ schema: 'freight', name: 'container_types' })
|
||||||
|
@Index(['sizeCode'])
|
||||||
|
@Index(['isActive'])
|
||||||
|
export class ContainerType extends BaseEntity {
|
||||||
|
@Column({ name: 'size_code', type: 'varchar', length: 20, unique: true })
|
||||||
|
sizeCode!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'description', type: 'varchar', length: 100, nullable: true })
|
||||||
|
description?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'containers_per_wagon', type: 'int' })
|
||||||
|
containersPerWagon!: number;
|
||||||
|
|
||||||
|
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||||
|
isActive!: boolean;
|
||||||
|
|
||||||
|
@OneToMany(() => WeightLimitRule, (rule) => rule.containerType)
|
||||||
|
weightLimitRules?: WeightLimitRule[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { BaseEntity } from '@edr/api-common';
|
||||||
|
import { Freight } from '@edr/types';
|
||||||
|
import { Column, Entity, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ schema: 'freight', name: 'priority_rules' })
|
||||||
|
@Index(['priorityType'])
|
||||||
|
@Index(['isActive'])
|
||||||
|
export class PriorityRule extends BaseEntity {
|
||||||
|
@Column({ name: 'priority_type', type: 'enum', enum: Freight.PriorityType, unique: true })
|
||||||
|
priorityType!: Freight.PriorityType;
|
||||||
|
|
||||||
|
@Column({ name: 'rule_name', type: 'varchar', length: 255 })
|
||||||
|
ruleName!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'description', type: 'text', nullable: true })
|
||||||
|
description?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'activation_condition', type: 'text', nullable: true })
|
||||||
|
activationCondition?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'bonus_points', type: 'int', default: 0 })
|
||||||
|
bonusPoints!: number;
|
||||||
|
|
||||||
|
@Column({ name: 'is_active', type: 'boolean', default: false })
|
||||||
|
isActive!: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { BaseEntity } from '@edr/api-common';
|
||||||
|
import { Column, Entity, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ schema: 'freight', name: 'service_types' })
|
||||||
|
@Index(['isActive'])
|
||||||
|
@Index(['displayOrder'])
|
||||||
|
@Index(['code'])
|
||||||
|
export class ServiceType extends BaseEntity {
|
||||||
|
@Column({ name: 'code', type: 'varchar', length: 50, unique: true, default: '' })
|
||||||
|
code!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'service_name', type: 'varchar', length: 255 })
|
||||||
|
serviceName!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'description', type: 'text', nullable: true })
|
||||||
|
description?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'can_be_booked_alone', type: 'boolean', default: true })
|
||||||
|
canBeBookedAlone!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'includes_first_mile', type: 'boolean', default: false })
|
||||||
|
includesFirstMile!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'includes_last_mile', type: 'boolean', default: false })
|
||||||
|
includesLastMile!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'includes_customs', type: 'boolean', default: false })
|
||||||
|
includesCustoms!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'priority_bonus_points', type: 'int', default: 0 })
|
||||||
|
priorityBonusPoints!: number;
|
||||||
|
|
||||||
|
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||||
|
isActive!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'display_order', type: 'int', default: 1 })
|
||||||
|
displayOrder!: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { BaseEntity } from '@edr/api-common';
|
||||||
|
import { Column, Entity, Index, OneToMany } from 'typeorm';
|
||||||
|
import { Surcharge } from './surcharge.entity';
|
||||||
|
|
||||||
|
@Entity({ schema: 'freight', name: 'surcharge_types' })
|
||||||
|
@Index(['code'])
|
||||||
|
@Index(['isActive'])
|
||||||
|
export class SurchargeType extends BaseEntity {
|
||||||
|
@Column({ name: 'code', type: 'varchar', length: 50, unique: true })
|
||||||
|
code!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'name', type: 'varchar', length: 100 })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'description', type: 'text', nullable: true })
|
||||||
|
description?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||||
|
isActive!: boolean;
|
||||||
|
|
||||||
|
@OneToMany(() => Surcharge, (s) => s.surchargeType)
|
||||||
|
surcharges?: Surcharge[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { BaseEntity } from '@edr/api-common';
|
||||||
|
import { Freight } from '@edr/types';
|
||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||||
|
import { SurchargeType } from './surcharge-type.entity';
|
||||||
|
import { WeightLimitRule } from './weight-limit-rule.entity';
|
||||||
|
|
||||||
|
@Entity({ schema: 'freight', name: 'surcharges' })
|
||||||
|
@Index(['surchargeTypeId'])
|
||||||
|
@Index(['isActive'])
|
||||||
|
export class Surcharge extends BaseEntity {
|
||||||
|
@Column({ name: 'surcharge_type_id', type: 'uuid' })
|
||||||
|
surchargeTypeId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => SurchargeType, (st) => st.surcharges)
|
||||||
|
@JoinColumn({ name: 'surcharge_type_id' })
|
||||||
|
surchargeType!: SurchargeType;
|
||||||
|
|
||||||
|
@Column({ name: 'fee_name', type: 'varchar', length: 255 })
|
||||||
|
feeName!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'trigger_description', type: 'text', nullable: true })
|
||||||
|
triggerDescription?: string | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'calculation_method',
|
||||||
|
type: 'enum',
|
||||||
|
enum: Freight.CalculationMethod,
|
||||||
|
default: Freight.CalculationMethod.PER_TON,
|
||||||
|
})
|
||||||
|
calculationMethod!: Freight.CalculationMethod;
|
||||||
|
|
||||||
|
@Column({ name: 'rate', type: 'numeric', precision: 10, scale: 2 })
|
||||||
|
rate!: number;
|
||||||
|
|
||||||
|
@Column({ name: 'currency', type: 'char', length: 3, default: 'USD' })
|
||||||
|
currency!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'apply_to_rail', type: 'boolean', default: false })
|
||||||
|
applyToRail!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'apply_to_first_mile', type: 'boolean', default: false })
|
||||||
|
applyToFirstMile!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'apply_to_last_mile', type: 'boolean', default: false })
|
||||||
|
applyToLastMile!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||||
|
isActive!: boolean;
|
||||||
|
|
||||||
|
@OneToMany(() => WeightLimitRule, (rule) => rule.surcharge)
|
||||||
|
weightLimitRules?: WeightLimitRule[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { BaseEntity } from '@edr/api-common';
|
||||||
|
import { Freight } from '@edr/types';
|
||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||||
|
import { ContainerType } from './container-type.entity';
|
||||||
|
import { Surcharge } from './surcharge.entity';
|
||||||
|
|
||||||
|
@Entity({ schema: 'freight', name: 'weight_limit_rules' })
|
||||||
|
@Index(['containerTypeId'])
|
||||||
|
@Index(['surchargeId'])
|
||||||
|
@Index(['isActive'])
|
||||||
|
export class WeightLimitRule extends BaseEntity {
|
||||||
|
@Column({ name: 'container_type_id', type: 'uuid' })
|
||||||
|
containerTypeId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => ContainerType, (ct) => ct.weightLimitRules)
|
||||||
|
@JoinColumn({ name: 'container_type_id' })
|
||||||
|
containerType!: ContainerType;
|
||||||
|
|
||||||
|
@Column({ name: 'trade_direction', type: 'enum', enum: Freight.TradeDirection })
|
||||||
|
tradeDirection!: Freight.TradeDirection;
|
||||||
|
|
||||||
|
@Column({ name: 'max_weight_tons', type: 'numeric', precision: 10, scale: 2 })
|
||||||
|
maxWeightTons!: number;
|
||||||
|
|
||||||
|
@Column({ name: 'warning_threshold_tons', type: 'numeric', precision: 10, scale: 2 })
|
||||||
|
warningThresholdTons!: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'exceeded_action',
|
||||||
|
type: 'enum',
|
||||||
|
enum: Freight.ExceededAction,
|
||||||
|
default: Freight.ExceededAction.WARNING_ONLY,
|
||||||
|
})
|
||||||
|
exceededAction!: Freight.ExceededAction;
|
||||||
|
|
||||||
|
@Column({ name: 'surcharge_id', type: 'uuid', nullable: true })
|
||||||
|
surchargeId?: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Surcharge, (s) => s.weightLimitRules, { nullable: true })
|
||||||
|
@JoinColumn({ name: 'surcharge_id' })
|
||||||
|
surcharge?: Surcharge | null;
|
||||||
|
|
||||||
|
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||||
|
isActive!: boolean;
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { FindManyOptions } from "typeorm";
|
import { FindManyOptions } from 'typeorm';
|
||||||
|
import { CargoType } from '../entities/cargo-type.entity';
|
||||||
import { CargoType } from "../entities/cargo-type.entity";
|
|
||||||
|
|
||||||
export interface ICargoTypesRepository {
|
export interface ICargoTypesRepository {
|
||||||
findById(id: string): Promise<CargoType | null>;
|
findById(id: string): Promise<CargoType | null>;
|
||||||
|
findByCode(code: string): Promise<CargoType | null>;
|
||||||
findAll(options?: FindManyOptions<CargoType>): Promise<CargoType[]>;
|
findAll(options?: FindManyOptions<CargoType>): Promise<CargoType[]>;
|
||||||
findAndCount(options?: FindManyOptions<CargoType>): Promise<[CargoType[], number]>;
|
findAndCount(options?: FindManyOptions<CargoType>): Promise<[CargoType[], number]>;
|
||||||
create(data: Partial<CargoType>): Promise<CargoType>;
|
create(data: Partial<CargoType>): Promise<CargoType>;
|
||||||
@@ -11,4 +11,4 @@ export interface ICargoTypesRepository {
|
|||||||
softDelete(id: string): Promise<void>;
|
softDelete(id: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CARGO_TYPES_REPOSITORY = Symbol("CARGO_TYPES_REPOSITORY");
|
export const CARGO_TYPES_REPOSITORY = Symbol('CARGO_TYPES_REPOSITORY');
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FindManyOptions } from 'typeorm';
|
||||||
|
import { ContainerType } from '../entities/container-type.entity';
|
||||||
|
|
||||||
|
export interface IContainerTypesRepository {
|
||||||
|
findById(id: string): Promise<ContainerType | null>;
|
||||||
|
findBySizeCode(sizeCode: string): Promise<ContainerType | null>;
|
||||||
|
findAll(options?: FindManyOptions<ContainerType>): Promise<ContainerType[]>;
|
||||||
|
findAndCount(options?: FindManyOptions<ContainerType>): Promise<[ContainerType[], number]>;
|
||||||
|
create(data: Partial<ContainerType>): Promise<ContainerType>;
|
||||||
|
update(id: string, data: Partial<ContainerType>): Promise<ContainerType | null>;
|
||||||
|
softDelete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CONTAINER_TYPES_REPOSITORY = Symbol('CONTAINER_TYPES_REPOSITORY');
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FindManyOptions } from 'typeorm';
|
||||||
|
import { PriorityRule } from '../entities/priority-rule.entity';
|
||||||
|
|
||||||
|
export interface IPriorityRulesRepository {
|
||||||
|
findById(id: string): Promise<PriorityRule | null>;
|
||||||
|
findAllActive(): Promise<PriorityRule[]>;
|
||||||
|
findAll(options?: FindManyOptions<PriorityRule>): Promise<PriorityRule[]>;
|
||||||
|
findAndCount(options?: FindManyOptions<PriorityRule>): Promise<[PriorityRule[], number]>;
|
||||||
|
create(data: Partial<PriorityRule>): Promise<PriorityRule>;
|
||||||
|
update(id: string, data: Partial<PriorityRule>): Promise<PriorityRule | null>;
|
||||||
|
softDelete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PRIORITY_RULES_REPOSITORY = Symbol('PRIORITY_RULES_REPOSITORY');
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { FindManyOptions } from "typeorm";
|
import { FindManyOptions } from 'typeorm';
|
||||||
|
import { ServiceType } from '../entities/service-type.entity';
|
||||||
import { ServiceType } from "../entities/service-type.entity";
|
|
||||||
|
|
||||||
export interface IServiceTypesRepository {
|
export interface IServiceTypesRepository {
|
||||||
findById(id: string): Promise<ServiceType | null>;
|
findById(id: string): Promise<ServiceType | null>;
|
||||||
|
findByCode(code: string): Promise<ServiceType | null>;
|
||||||
findAll(options?: FindManyOptions<ServiceType>): Promise<ServiceType[]>;
|
findAll(options?: FindManyOptions<ServiceType>): Promise<ServiceType[]>;
|
||||||
findAndCount(options?: FindManyOptions<ServiceType>): Promise<[ServiceType[], number]>;
|
findAndCount(options?: FindManyOptions<ServiceType>): Promise<[ServiceType[], number]>;
|
||||||
create(data: Partial<ServiceType>): Promise<ServiceType>;
|
create(data: Partial<ServiceType>): Promise<ServiceType>;
|
||||||
@@ -11,4 +11,4 @@ export interface IServiceTypesRepository {
|
|||||||
softDelete(id: string): Promise<void>;
|
softDelete(id: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SERVICE_TYPES_REPOSITORY = Symbol("SERVICE_TYPES_REPOSITORY");
|
export const SERVICE_TYPES_REPOSITORY = Symbol('SERVICE_TYPES_REPOSITORY');
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FindManyOptions } from 'typeorm';
|
||||||
|
import { SurchargeType } from '../entities/surcharge-type.entity';
|
||||||
|
|
||||||
|
export interface ISurchargeTypesRepository {
|
||||||
|
findById(id: string): Promise<SurchargeType | null>;
|
||||||
|
findByCode(code: string): Promise<SurchargeType | null>;
|
||||||
|
findAll(options?: FindManyOptions<SurchargeType>): Promise<SurchargeType[]>;
|
||||||
|
findAndCount(options?: FindManyOptions<SurchargeType>): Promise<[SurchargeType[], number]>;
|
||||||
|
create(data: Partial<SurchargeType>): Promise<SurchargeType>;
|
||||||
|
update(id: string, data: Partial<SurchargeType>): Promise<SurchargeType | null>;
|
||||||
|
softDelete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SURCHARGE_TYPES_REPOSITORY = Symbol('SURCHARGE_TYPES_REPOSITORY');
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FindManyOptions } from 'typeorm';
|
||||||
|
import { Surcharge } from '../entities/surcharge.entity';
|
||||||
|
|
||||||
|
export interface ISurchargesRepository {
|
||||||
|
findById(id: string): Promise<Surcharge | null>;
|
||||||
|
findByTypeCode(typeCode: string): Promise<Surcharge | null>;
|
||||||
|
findAll(options?: FindManyOptions<Surcharge>): Promise<Surcharge[]>;
|
||||||
|
findAndCount(options?: FindManyOptions<Surcharge>): Promise<[Surcharge[], number]>;
|
||||||
|
create(data: Partial<Surcharge>): Promise<Surcharge>;
|
||||||
|
update(id: string, data: Partial<Surcharge>): Promise<Surcharge | null>;
|
||||||
|
softDelete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SURCHARGES_REPOSITORY = Symbol('SURCHARGES_REPOSITORY');
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { FindManyOptions } from 'typeorm';
|
||||||
|
import { WeightLimitRule } from '../entities/weight-limit-rule.entity';
|
||||||
|
|
||||||
|
export interface IWeightLimitRulesRepository {
|
||||||
|
findById(id: string): Promise<WeightLimitRule | null>;
|
||||||
|
findActiveByContainerTypeAndDirection(
|
||||||
|
sizeCode: string,
|
||||||
|
tradeDirection: string,
|
||||||
|
): Promise<WeightLimitRule[]>;
|
||||||
|
findAll(options?: FindManyOptions<WeightLimitRule>): Promise<WeightLimitRule[]>;
|
||||||
|
findAndCount(options?: FindManyOptions<WeightLimitRule>): Promise<[WeightLimitRule[], number]>;
|
||||||
|
create(data: Partial<WeightLimitRule>): Promise<WeightLimitRule>;
|
||||||
|
update(id: string, data: Partial<WeightLimitRule>): Promise<WeightLimitRule | null>;
|
||||||
|
softDelete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WEIGHT_LIMIT_RULES_REPOSITORY = Symbol('WEIGHT_LIMIT_RULES_REPOSITORY');
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, FindManyOptions, Repository } from 'typeorm';
|
||||||
|
import { CargoType } from '../entities/cargo-type.entity';
|
||||||
|
import { ICargoTypesRepository } from '../interfaces/cargo-types.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CargoTypesRepository implements ICargoTypesRepository {
|
||||||
|
private readonly repo: Repository<CargoType>;
|
||||||
|
|
||||||
|
constructor(private readonly dataSource: DataSource) {
|
||||||
|
this.repo = this.dataSource.getRepository(CargoType);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Promise<CargoType | null> {
|
||||||
|
return this.repo.findOne({ where: { id }, relations: { parent: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findByCode(code: string): Promise<CargoType | null> {
|
||||||
|
return this.repo.findOne({ where: { code } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(options?: FindManyOptions<CargoType>): Promise<CargoType[]> {
|
||||||
|
return this.repo.find(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndCount(options?: FindManyOptions<CargoType>): Promise<[CargoType[], number]> {
|
||||||
|
return this.repo.findAndCount(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<CargoType>): Promise<CargoType> {
|
||||||
|
const entity = this.repo.create(data);
|
||||||
|
return this.repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<CargoType>): Promise<CargoType | null> {
|
||||||
|
await this.repo.update(id, data);
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(id: string): Promise<void> {
|
||||||
|
await this.repo.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, FindManyOptions, Repository } from 'typeorm';
|
||||||
|
import { ContainerType } from '../entities/container-type.entity';
|
||||||
|
import { IContainerTypesRepository } from '../interfaces/container-types.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ContainerTypesRepository implements IContainerTypesRepository {
|
||||||
|
private readonly repo: Repository<ContainerType>;
|
||||||
|
|
||||||
|
constructor(private readonly dataSource: DataSource) {
|
||||||
|
this.repo = this.dataSource.getRepository(ContainerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Promise<ContainerType | null> {
|
||||||
|
return this.repo.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findBySizeCode(sizeCode: string): Promise<ContainerType | null> {
|
||||||
|
return this.repo.findOne({ where: { sizeCode } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(options?: FindManyOptions<ContainerType>): Promise<ContainerType[]> {
|
||||||
|
return this.repo.find(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndCount(options?: FindManyOptions<ContainerType>): Promise<[ContainerType[], number]> {
|
||||||
|
return this.repo.findAndCount(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<ContainerType>): Promise<ContainerType> {
|
||||||
|
const entity = this.repo.create(data);
|
||||||
|
return this.repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<ContainerType>): Promise<ContainerType | null> {
|
||||||
|
await this.repo.update(id, data);
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(id: string): Promise<void> {
|
||||||
|
await this.repo.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, FindManyOptions, Repository } from 'typeorm';
|
||||||
|
import { PriorityRule } from '../entities/priority-rule.entity';
|
||||||
|
import { IPriorityRulesRepository } from '../interfaces/priority-rules.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PriorityRulesRepository implements IPriorityRulesRepository {
|
||||||
|
private readonly repo: Repository<PriorityRule>;
|
||||||
|
|
||||||
|
constructor(private readonly dataSource: DataSource) {
|
||||||
|
this.repo = this.dataSource.getRepository(PriorityRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Promise<PriorityRule | null> {
|
||||||
|
return this.repo.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findAllActive(): Promise<PriorityRule[]> {
|
||||||
|
return this.repo.find({ where: { isActive: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(options?: FindManyOptions<PriorityRule>): Promise<PriorityRule[]> {
|
||||||
|
return this.repo.find(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndCount(options?: FindManyOptions<PriorityRule>): Promise<[PriorityRule[], number]> {
|
||||||
|
return this.repo.findAndCount(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<PriorityRule>): Promise<PriorityRule> {
|
||||||
|
const entity = this.repo.create(data);
|
||||||
|
return this.repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<PriorityRule>): Promise<PriorityRule | null> {
|
||||||
|
await this.repo.update(id, data);
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(id: string): Promise<void> {
|
||||||
|
await this.repo.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, FindManyOptions, Repository } from 'typeorm';
|
||||||
|
import { ServiceType } from '../entities/service-type.entity';
|
||||||
|
import { IServiceTypesRepository } from '../interfaces/service-types.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServiceTypesRepository implements IServiceTypesRepository {
|
||||||
|
private readonly repo: Repository<ServiceType>;
|
||||||
|
|
||||||
|
constructor(private readonly dataSource: DataSource) {
|
||||||
|
this.repo = this.dataSource.getRepository(ServiceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Promise<ServiceType | null> {
|
||||||
|
return this.repo.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findByCode(code: string): Promise<ServiceType | null> {
|
||||||
|
return this.repo.findOne({ where: { code } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(options?: FindManyOptions<ServiceType>): Promise<ServiceType[]> {
|
||||||
|
return this.repo.find(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndCount(options?: FindManyOptions<ServiceType>): Promise<[ServiceType[], number]> {
|
||||||
|
return this.repo.findAndCount(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<ServiceType>): Promise<ServiceType> {
|
||||||
|
const entity = this.repo.create(data);
|
||||||
|
return this.repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<ServiceType>): Promise<ServiceType | null> {
|
||||||
|
await this.repo.update(id, data);
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(id: string): Promise<void> {
|
||||||
|
await this.repo.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, FindManyOptions, Repository } from 'typeorm';
|
||||||
|
import { SurchargeType } from '../entities/surcharge-type.entity';
|
||||||
|
import { ISurchargeTypesRepository } from '../interfaces/surcharge-types.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SurchargeTypesRepository implements ISurchargeTypesRepository {
|
||||||
|
private readonly repo: Repository<SurchargeType>;
|
||||||
|
|
||||||
|
constructor(private readonly dataSource: DataSource) {
|
||||||
|
this.repo = this.dataSource.getRepository(SurchargeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Promise<SurchargeType | null> {
|
||||||
|
return this.repo.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findByCode(code: string): Promise<SurchargeType | null> {
|
||||||
|
return this.repo.findOne({ where: { code } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(options?: FindManyOptions<SurchargeType>): Promise<SurchargeType[]> {
|
||||||
|
return this.repo.find(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndCount(options?: FindManyOptions<SurchargeType>): Promise<[SurchargeType[], number]> {
|
||||||
|
return this.repo.findAndCount(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<SurchargeType>): Promise<SurchargeType> {
|
||||||
|
const entity = this.repo.create(data);
|
||||||
|
return this.repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<SurchargeType>): Promise<SurchargeType | null> {
|
||||||
|
await this.repo.update(id, data);
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(id: string): Promise<void> {
|
||||||
|
await this.repo.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, FindManyOptions, Repository } from 'typeorm';
|
||||||
|
import { Surcharge } from '../entities/surcharge.entity';
|
||||||
|
import { ISurchargesRepository } from '../interfaces/surcharges.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SurchargesRepository implements ISurchargesRepository {
|
||||||
|
private readonly repo: Repository<Surcharge>;
|
||||||
|
|
||||||
|
constructor(private readonly dataSource: DataSource) {
|
||||||
|
this.repo = this.dataSource.getRepository(Surcharge);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Promise<Surcharge | null> {
|
||||||
|
return this.repo.findOne({ where: { id }, relations: { surchargeType: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findByTypeCode(typeCode: string): Promise<Surcharge | null> {
|
||||||
|
return this.repo.findOne({
|
||||||
|
where: { isActive: true, surchargeType: { code: typeCode } },
|
||||||
|
relations: { surchargeType: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(options?: FindManyOptions<Surcharge>): Promise<Surcharge[]> {
|
||||||
|
return this.repo.find(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndCount(options?: FindManyOptions<Surcharge>): Promise<[Surcharge[], number]> {
|
||||||
|
return this.repo.findAndCount(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<Surcharge>): Promise<Surcharge> {
|
||||||
|
const entity = this.repo.create(data);
|
||||||
|
return this.repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<Surcharge>): Promise<Surcharge | null> {
|
||||||
|
await this.repo.update(id, data);
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(id: string): Promise<void> {
|
||||||
|
await this.repo.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, FindManyOptions, Repository } from 'typeorm';
|
||||||
|
import { WeightLimitRule } from '../entities/weight-limit-rule.entity';
|
||||||
|
import { IWeightLimitRulesRepository } from '../interfaces/weight-limit-rules.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WeightLimitRulesRepository implements IWeightLimitRulesRepository {
|
||||||
|
private readonly repo: Repository<WeightLimitRule>;
|
||||||
|
|
||||||
|
constructor(private readonly dataSource: DataSource) {
|
||||||
|
this.repo = this.dataSource.getRepository(WeightLimitRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Promise<WeightLimitRule | null> {
|
||||||
|
return this.repo.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: { containerType: true, surcharge: { surchargeType: true } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findActiveByContainerTypeAndDirection(
|
||||||
|
sizeCode: string,
|
||||||
|
tradeDirection: string,
|
||||||
|
): Promise<WeightLimitRule[]> {
|
||||||
|
return this.repo
|
||||||
|
.createQueryBuilder('rule')
|
||||||
|
.innerJoinAndSelect('rule.containerType', 'ct')
|
||||||
|
.leftJoinAndSelect('rule.surcharge', 'surcharge')
|
||||||
|
.leftJoinAndSelect('surcharge.surchargeType', 'surchargeType')
|
||||||
|
.where('ct.size_code = :sizeCode', { sizeCode })
|
||||||
|
.andWhere('(rule.trade_direction = :dir OR rule.trade_direction = :both)', {
|
||||||
|
dir: tradeDirection,
|
||||||
|
both: 'BOTH',
|
||||||
|
})
|
||||||
|
.andWhere('rule.is_active = true')
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(options?: FindManyOptions<WeightLimitRule>): Promise<WeightLimitRule[]> {
|
||||||
|
return this.repo.find(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAndCount(options?: FindManyOptions<WeightLimitRule>): Promise<[WeightLimitRule[], number]> {
|
||||||
|
return this.repo.findAndCount(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<WeightLimitRule>): Promise<WeightLimitRule> {
|
||||||
|
const entity = this.repo.create(data);
|
||||||
|
return this.repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<WeightLimitRule>): Promise<WeightLimitRule | null> {
|
||||||
|
await this.repo.update(id, data);
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(id: string): Promise<void> {
|
||||||
|
await this.repo.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { CargoType } from './entities/cargo-type.entity';
|
||||||
|
import { ContainerType } from './entities/container-type.entity';
|
||||||
|
import { PriorityRule } from './entities/priority-rule.entity';
|
||||||
|
import { Surcharge } from './entities/surcharge.entity';
|
||||||
|
import { SurchargeType } from './entities/surcharge-type.entity';
|
||||||
|
import { ServiceType } from './entities/service-type.entity';
|
||||||
|
import { WeightLimitRule } from './entities/weight-limit-rule.entity';
|
||||||
|
|
||||||
|
import { CARGO_TYPES_REPOSITORY } from './interfaces/cargo-types.repository.interface';
|
||||||
|
import { CONTAINER_TYPES_REPOSITORY } from './interfaces/container-types.repository.interface';
|
||||||
|
import { PRIORITY_RULES_REPOSITORY } from './interfaces/priority-rules.repository.interface';
|
||||||
|
import { SURCHARGES_REPOSITORY } from './interfaces/surcharges.repository.interface';
|
||||||
|
import { SURCHARGE_TYPES_REPOSITORY } from './interfaces/surcharge-types.repository.interface';
|
||||||
|
import { SERVICE_TYPES_REPOSITORY } from './interfaces/service-types.repository.interface';
|
||||||
|
import { WEIGHT_LIMIT_RULES_REPOSITORY } from './interfaces/weight-limit-rules.repository.interface';
|
||||||
|
|
||||||
|
import { CargoTypesRepository } from './repositories/cargo-types.repository';
|
||||||
|
import { ContainerTypesRepository } from './repositories/container-types.repository';
|
||||||
|
import { PriorityRulesRepository } from './repositories/priority-rules.repository';
|
||||||
|
import { SurchargesRepository } from './repositories/surcharges.repository';
|
||||||
|
import { SurchargeTypesRepository } from './repositories/surcharge-types.repository';
|
||||||
|
import { ServiceTypesRepository } from './repositories/service-types.repository';
|
||||||
|
import { WeightLimitRulesRepository } from './repositories/weight-limit-rules.repository';
|
||||||
|
|
||||||
|
import { CargoTypesService } from './services/cargo-types.service';
|
||||||
|
import { ContainerTypesService } from './services/container-types.service';
|
||||||
|
import { PriorityRulesService } from './services/priority-rules.service';
|
||||||
|
import { SurchargesService } from './services/surcharges.service';
|
||||||
|
import { SurchargeTypesService } from './services/surcharge-types.service';
|
||||||
|
import { ServiceTypesService } from './services/service-types.service';
|
||||||
|
import { WeightLimitRulesService } from './services/weight-limit-rules.service';
|
||||||
|
|
||||||
|
import { CargoTypesController } from './controllers/cargo-types.controller';
|
||||||
|
import { ContainerTypesController } from './controllers/container-types.controller';
|
||||||
|
import { PriorityRulesController } from './controllers/priority-rules.controller';
|
||||||
|
import { SurchargesController } from './controllers/surcharges.controller';
|
||||||
|
import { SurchargeTypesController } from './controllers/surcharge-types.controller';
|
||||||
|
import { ServiceTypesController } from './controllers/service-types.controller';
|
||||||
|
import { WeightLimitRulesController } from './controllers/weight-limit-rules.controller';
|
||||||
|
|
||||||
|
import { RuleEngineService } from './rule-engine.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
CargoType,
|
||||||
|
ContainerType,
|
||||||
|
PriorityRule,
|
||||||
|
Surcharge,
|
||||||
|
SurchargeType,
|
||||||
|
ServiceType,
|
||||||
|
WeightLimitRule,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [
|
||||||
|
CargoTypesController,
|
||||||
|
ContainerTypesController,
|
||||||
|
PriorityRulesController,
|
||||||
|
SurchargesController,
|
||||||
|
SurchargeTypesController,
|
||||||
|
ServiceTypesController,
|
||||||
|
WeightLimitRulesController,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
// Repositories
|
||||||
|
CargoTypesRepository,
|
||||||
|
{ provide: CARGO_TYPES_REPOSITORY, useExisting: CargoTypesRepository },
|
||||||
|
ContainerTypesRepository,
|
||||||
|
{ provide: CONTAINER_TYPES_REPOSITORY, useExisting: ContainerTypesRepository },
|
||||||
|
PriorityRulesRepository,
|
||||||
|
{ provide: PRIORITY_RULES_REPOSITORY, useExisting: PriorityRulesRepository },
|
||||||
|
SurchargesRepository,
|
||||||
|
{ provide: SURCHARGES_REPOSITORY, useExisting: SurchargesRepository },
|
||||||
|
SurchargeTypesRepository,
|
||||||
|
{ provide: SURCHARGE_TYPES_REPOSITORY, useExisting: SurchargeTypesRepository },
|
||||||
|
ServiceTypesRepository,
|
||||||
|
{ provide: SERVICE_TYPES_REPOSITORY, useExisting: ServiceTypesRepository },
|
||||||
|
WeightLimitRulesRepository,
|
||||||
|
{ provide: WEIGHT_LIMIT_RULES_REPOSITORY, useExisting: WeightLimitRulesRepository },
|
||||||
|
// CRUD services
|
||||||
|
CargoTypesService,
|
||||||
|
ContainerTypesService,
|
||||||
|
PriorityRulesService,
|
||||||
|
SurchargesService,
|
||||||
|
SurchargeTypesService,
|
||||||
|
ServiceTypesService,
|
||||||
|
WeightLimitRulesService,
|
||||||
|
// Evaluation engine
|
||||||
|
RuleEngineService,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
RuleEngineService,
|
||||||
|
CargoTypesService,
|
||||||
|
ServiceTypesService,
|
||||||
|
ContainerTypesService,
|
||||||
|
SurchargeTypesService,
|
||||||
|
SurchargesService,
|
||||||
|
WeightLimitRulesService,
|
||||||
|
PriorityRulesService,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class RuleEngineModule {}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { Inject, Injectable, BadRequestException } from '@nestjs/common';
|
||||||
|
import { Freight } from '@edr/types';
|
||||||
|
import { Booking } from '../bookings/entities/booking.entity';
|
||||||
|
import {
|
||||||
|
ICargoTypesRepository,
|
||||||
|
CARGO_TYPES_REPOSITORY,
|
||||||
|
} from './interfaces/cargo-types.repository.interface';
|
||||||
|
import {
|
||||||
|
IServiceTypesRepository,
|
||||||
|
SERVICE_TYPES_REPOSITORY,
|
||||||
|
} from './interfaces/service-types.repository.interface';
|
||||||
|
import {
|
||||||
|
ISurchargesRepository,
|
||||||
|
SURCHARGES_REPOSITORY,
|
||||||
|
} from './interfaces/surcharges.repository.interface';
|
||||||
|
import {
|
||||||
|
IWeightLimitRulesRepository,
|
||||||
|
WEIGHT_LIMIT_RULES_REPOSITORY,
|
||||||
|
} from './interfaces/weight-limit-rules.repository.interface';
|
||||||
|
import {
|
||||||
|
IPriorityRulesRepository,
|
||||||
|
PRIORITY_RULES_REPOSITORY,
|
||||||
|
} from './interfaces/priority-rules.repository.interface';
|
||||||
|
|
||||||
|
export interface AppliedSurcharge {
|
||||||
|
feeName: string;
|
||||||
|
rate: number;
|
||||||
|
currency: string;
|
||||||
|
calculationMethod: Freight.CalculationMethod;
|
||||||
|
applyToRail: boolean;
|
||||||
|
applyToFirstMile: boolean;
|
||||||
|
applyToLastMile: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleEvaluationResult {
|
||||||
|
priorityScore: number;
|
||||||
|
appliedSurcharges: AppliedSurcharge[];
|
||||||
|
warnings: string[];
|
||||||
|
hardBlocked: string[];
|
||||||
|
requiresDirectorApproval: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RuleEngineService {
|
||||||
|
constructor(
|
||||||
|
@Inject(CARGO_TYPES_REPOSITORY)
|
||||||
|
private readonly cargoTypesRepo: ICargoTypesRepository,
|
||||||
|
@Inject(SERVICE_TYPES_REPOSITORY)
|
||||||
|
private readonly serviceTypesRepo: IServiceTypesRepository,
|
||||||
|
@Inject(SURCHARGES_REPOSITORY)
|
||||||
|
private readonly surchargesRepo: ISurchargesRepository,
|
||||||
|
@Inject(WEIGHT_LIMIT_RULES_REPOSITORY)
|
||||||
|
private readonly weightLimitRulesRepo: IWeightLimitRulesRepository,
|
||||||
|
@Inject(PRIORITY_RULES_REPOSITORY)
|
||||||
|
private readonly priorityRulesRepo: IPriorityRulesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate all rule engine rules against a booking snapshot.
|
||||||
|
* Returns the computed priority score, surcharges to apply, warnings,
|
||||||
|
* hard-block messages, and whether director approval is required.
|
||||||
|
* Callers must throw BadRequestException if hardBlocked is non-empty.
|
||||||
|
*/
|
||||||
|
async evaluate(
|
||||||
|
booking: Pick<
|
||||||
|
Booking,
|
||||||
|
| 'freightType'
|
||||||
|
| 'serviceType'
|
||||||
|
| 'paymentCurrency'
|
||||||
|
| 'cargoTotalWeightVgm'
|
||||||
|
| 'tradeDirection'
|
||||||
|
| 'isHazardous'
|
||||||
|
| 'isRefrigerated'
|
||||||
|
| 'containers'
|
||||||
|
>,
|
||||||
|
): Promise<RuleEvaluationResult> {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
const hardBlocked: string[] = [];
|
||||||
|
const appliedSurcharges: AppliedSurcharge[] = [];
|
||||||
|
let priorityScore = 0;
|
||||||
|
let requiresDirectorApproval = false;
|
||||||
|
|
||||||
|
// ── 1. Cargo routing ─────────────────────────────────────────────────
|
||||||
|
// Look up CargoType by code to determine director-approval routing.
|
||||||
|
if (booking.freightType) {
|
||||||
|
const cargoType = await this.cargoTypesRepo.findByCode(booking.freightType);
|
||||||
|
if (cargoType?.requiresDirectorApproval) {
|
||||||
|
requiresDirectorApproval = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Weight-limit check ────────────────────────────────────────────
|
||||||
|
// For each container group in the booking, find matching active rules
|
||||||
|
// and check whether the per-container VGM exceeds the max weight.
|
||||||
|
const containers = booking.containers ?? [];
|
||||||
|
for (const container of containers) {
|
||||||
|
const rules = await this.weightLimitRulesRepo.findActiveByContainerTypeAndDirection(
|
||||||
|
container.type,
|
||||||
|
booking.tradeDirection,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (container.vgm > rule.maxWeightTons) {
|
||||||
|
const msg =
|
||||||
|
`${container.type} container VGM ${container.vgm}t exceeds max ` +
|
||||||
|
`${rule.maxWeightTons}t (${booking.tradeDirection})`;
|
||||||
|
|
||||||
|
if (rule.exceededAction === Freight.ExceededAction.HARD_BLOCK) {
|
||||||
|
hardBlocked.push(msg);
|
||||||
|
} else {
|
||||||
|
warnings.push(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.surcharge) {
|
||||||
|
appliedSurcharges.push(this.mapSurcharge(rule.surcharge));
|
||||||
|
}
|
||||||
|
} else if (container.vgm > rule.warningThresholdTons) {
|
||||||
|
warnings.push(
|
||||||
|
`${container.type} container VGM ${container.vgm}t is approaching limit ` +
|
||||||
|
`of ${rule.maxWeightTons}t (${booking.tradeDirection})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3. Surcharge flags ───────────────────────────────────────────────
|
||||||
|
if (booking.isHazardous) {
|
||||||
|
const surcharge = await this.surchargesRepo.findByTypeCode('HAZARDOUS');
|
||||||
|
if (surcharge) appliedSurcharges.push(this.mapSurcharge(surcharge));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.isRefrigerated) {
|
||||||
|
const surcharge = await this.surchargesRepo.findByTypeCode('REFRIGERATED');
|
||||||
|
if (surcharge) appliedSurcharges.push(this.mapSurcharge(surcharge));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Priority scoring ──────────────────────────────────────────────
|
||||||
|
const priorityRules = await this.priorityRulesRepo.findAllActive();
|
||||||
|
|
||||||
|
for (const rule of priorityRules) {
|
||||||
|
switch (rule.priorityType) {
|
||||||
|
case Freight.PriorityType.USD_PAYER:
|
||||||
|
if (booking.paymentCurrency === 'USD') {
|
||||||
|
priorityScore += rule.bonusPoints;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Freight.PriorityType.RAIL_AND_FORWARDING: {
|
||||||
|
// Read bonus points from the matching ServiceType DB row
|
||||||
|
const serviceType = await this.serviceTypesRepo.findByCode(booking.serviceType);
|
||||||
|
if (serviceType && serviceType.priorityBonusPoints > 0) {
|
||||||
|
priorityScore += serviceType.priorityBonusPoints;
|
||||||
|
} else if (booking.serviceType === 'RAIL_AND_FORWARDING') {
|
||||||
|
// Fall back to the rule's own bonus_points if no ServiceType found
|
||||||
|
priorityScore += rule.bonusPoints;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Freight.PriorityType.HIGH_VOLUME_SHIPMENT:
|
||||||
|
if (booking.cargoTotalWeightVgm >= 300) {
|
||||||
|
priorityScore += rule.bonusPoints;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Freight.PriorityType.GOVERNMENT_ACCOUNT:
|
||||||
|
// TODO: integrate customer accountTier — evaluate when Customer entity is extended
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { priorityScore, appliedSurcharges, warnings, hardBlocked, requiresDirectorApproval };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard helper — throws BadRequestException if hardBlocked is non-empty.
|
||||||
|
* Call this immediately after evaluate() in BookingsService.
|
||||||
|
*/
|
||||||
|
assertNoHardBlocks(result: RuleEvaluationResult): void {
|
||||||
|
if (result.hardBlocked.length > 0) {
|
||||||
|
throw new BadRequestException(result.hardBlocked.join('; '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapSurcharge(s: { feeName: string; rate: number; currency: string; calculationMethod: Freight.CalculationMethod; applyToRail: boolean; applyToFirstMile: boolean; applyToLastMile: boolean }): AppliedSurcharge {
|
||||||
|
return {
|
||||||
|
feeName: s.feeName,
|
||||||
|
rate: s.rate,
|
||||||
|
currency: s.currency,
|
||||||
|
calculationMethod: s.calculationMethod,
|
||||||
|
applyToRail: s.applyToRail,
|
||||||
|
applyToFirstMile: s.applyToFirstMile,
|
||||||
|
applyToLastMile: s.applyToLastMile,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { ILike } from 'typeorm';
|
||||||
|
import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
|
||||||
|
import { UpdateCargoTypeDto } from '../dto/update-cargo-type.dto';
|
||||||
|
import { CargoType } from '../entities/cargo-type.entity';
|
||||||
|
import {
|
||||||
|
CARGO_TYPES_REPOSITORY,
|
||||||
|
ICargoTypesRepository,
|
||||||
|
} from '../interfaces/cargo-types.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CargoTypesService {
|
||||||
|
constructor(
|
||||||
|
@Inject(CARGO_TYPES_REPOSITORY)
|
||||||
|
private readonly repository: ICargoTypesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** List cargo types with pagination and optional filtering. */
|
||||||
|
async findAll(filter: {
|
||||||
|
isActive?: boolean;
|
||||||
|
requiresDirectorApproval?: boolean;
|
||||||
|
parentGroupId?: string;
|
||||||
|
search?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'ASC' | 'DESC';
|
||||||
|
}): Promise<{ data: CargoType[]; meta: { total: number; page: number; pageSize: number; totalPages: number } }> {
|
||||||
|
const page = filter.page ?? 1;
|
||||||
|
const pageSize = filter.pageSize ?? 20;
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
if (filter.isActive !== undefined) where.isActive = filter.isActive;
|
||||||
|
if (filter.requiresDirectorApproval !== undefined) where.requiresDirectorApproval = filter.requiresDirectorApproval;
|
||||||
|
if (filter.parentGroupId !== undefined) where.parentGroupId = filter.parentGroupId;
|
||||||
|
if (filter.search) where.cargoTypeName = ILike(`%${filter.search}%`);
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { [filter.sortBy ?? 'displayOrder']: filter.sortOrder ?? 'ASC' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
relations: { parent: true },
|
||||||
|
});
|
||||||
|
return { data, meta: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a single cargo type by ID. */
|
||||||
|
async findById(id: string): Promise<CargoType> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) throw new NotFoundException(`Cargo type ${id} not found`);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a cargo type by code. */
|
||||||
|
async findByCode(code: string): Promise<CargoType | null> {
|
||||||
|
return this.repository.findByCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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`);
|
||||||
|
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,
|
||||||
|
cargoTypeName: dto.cargoTypeName,
|
||||||
|
parentGroupId: dto.parentGroupId ?? null,
|
||||||
|
showFreeTextBox: dto.showFreeTextBox ?? false,
|
||||||
|
requiresDirectorApproval: dto.requiresDirectorApproval ?? false,
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
displayOrder: dto.displayOrder ?? 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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);
|
||||||
|
if (!parent) throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`);
|
||||||
|
}
|
||||||
|
const updated = await this.repository.update(id, dto);
|
||||||
|
if (!updated) throw new NotFoundException(`Cargo type ${id} not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soft-delete a cargo type. */
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { CreateContainerTypeDto } from '../dto/create-container-type.dto';
|
||||||
|
import { UpdateContainerTypeDto } from '../dto/update-container-type.dto';
|
||||||
|
import { ContainerType } from '../entities/container-type.entity';
|
||||||
|
import {
|
||||||
|
CONTAINER_TYPES_REPOSITORY,
|
||||||
|
IContainerTypesRepository,
|
||||||
|
} from '../interfaces/container-types.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ContainerTypesService {
|
||||||
|
constructor(
|
||||||
|
@Inject(CONTAINER_TYPES_REPOSITORY)
|
||||||
|
private readonly repository: IContainerTypesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** List container types with pagination. */
|
||||||
|
async findAll(filter: {
|
||||||
|
isActive?: boolean;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}): Promise<{ data: ContainerType[]; meta: { total: number; page: number; pageSize: number; totalPages: number } }> {
|
||||||
|
const page = filter.page ?? 1;
|
||||||
|
const pageSize = filter.pageSize ?? 20;
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
if (filter.isActive !== undefined) where.isActive = filter.isActive;
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { sizeCode: 'ASC' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
return { data, meta: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a single container type by ID. */
|
||||||
|
async findById(id: string): Promise<ContainerType> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) throw new NotFoundException(`Container type ${id} not found`);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new container type. */
|
||||||
|
async create(dto: CreateContainerTypeDto): Promise<ContainerType> {
|
||||||
|
const existing = await this.repository.findBySizeCode(dto.sizeCode);
|
||||||
|
if (existing) throw new ConflictException(`Container type with sizeCode "${dto.sizeCode}" already exists`);
|
||||||
|
return this.repository.create({
|
||||||
|
sizeCode: dto.sizeCode,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
containersPerWagon: dto.containersPerWagon,
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update an existing container type. */
|
||||||
|
async update(id: string, dto: UpdateContainerTypeDto): Promise<ContainerType> {
|
||||||
|
await this.findById(id);
|
||||||
|
if (dto.sizeCode) {
|
||||||
|
const conflict = await this.repository.findBySizeCode(dto.sizeCode);
|
||||||
|
if (conflict && conflict.id !== id) {
|
||||||
|
throw new ConflictException(`Container type with sizeCode "${dto.sizeCode}" already exists`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updated = await this.repository.update(id, dto);
|
||||||
|
if (!updated) throw new NotFoundException(`Container type ${id} not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soft-delete a container type. */
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { CreatePriorityRuleDto } from '../dto/create-priority-rule.dto';
|
||||||
|
import { UpdatePriorityRuleDto } from '../dto/update-priority-rule.dto';
|
||||||
|
import { PriorityRule } from '../entities/priority-rule.entity';
|
||||||
|
import {
|
||||||
|
IPriorityRulesRepository,
|
||||||
|
PRIORITY_RULES_REPOSITORY,
|
||||||
|
} from '../interfaces/priority-rules.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PriorityRulesService {
|
||||||
|
constructor(
|
||||||
|
@Inject(PRIORITY_RULES_REPOSITORY)
|
||||||
|
private readonly repository: IPriorityRulesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** List priority rules with pagination. */
|
||||||
|
async findAll(filter: {
|
||||||
|
isActive?: boolean;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}): Promise<{ data: PriorityRule[]; meta: { total: number; page: number; pageSize: number; totalPages: number } }> {
|
||||||
|
const page = filter.page ?? 1;
|
||||||
|
const pageSize = filter.pageSize ?? 20;
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
if (filter.isActive !== undefined) where.isActive = filter.isActive;
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { priorityType: 'ASC' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
return { data, meta: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a single priority rule by ID. */
|
||||||
|
async findById(id: string): Promise<PriorityRule> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) throw new NotFoundException(`Priority rule ${id} not found`);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new priority rule. */
|
||||||
|
async create(dto: CreatePriorityRuleDto): Promise<PriorityRule> {
|
||||||
|
const existing = await this.repository.findAll({ where: { priorityType: dto.priorityType } });
|
||||||
|
if (existing.length > 0) {
|
||||||
|
throw new ConflictException(`Priority rule for type "${dto.priorityType}" already exists`);
|
||||||
|
}
|
||||||
|
return this.repository.create({
|
||||||
|
priorityType: dto.priorityType,
|
||||||
|
ruleName: dto.ruleName,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
activationCondition: dto.activationCondition ?? null,
|
||||||
|
bonusPoints: dto.bonusPoints,
|
||||||
|
isActive: dto.isActive ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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);
|
||||||
|
if (!updated) throw new NotFoundException(`Priority rule ${id} not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soft-delete a priority rule. */
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { ILike } from 'typeorm';
|
||||||
|
import { CreateServiceTypeDto } from '../dto/create-service-type.dto';
|
||||||
|
import { UpdateServiceTypeDto } from '../dto/update-service-type.dto';
|
||||||
|
import { ServiceType } from '../entities/service-type.entity';
|
||||||
|
import {
|
||||||
|
IServiceTypesRepository,
|
||||||
|
SERVICE_TYPES_REPOSITORY,
|
||||||
|
} from '../interfaces/service-types.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServiceTypesService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SERVICE_TYPES_REPOSITORY)
|
||||||
|
private readonly repository: IServiceTypesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** List service types with pagination and optional filtering. */
|
||||||
|
async findAll(filter: {
|
||||||
|
isActive?: boolean;
|
||||||
|
canBeBookedAlone?: boolean;
|
||||||
|
search?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'ASC' | 'DESC';
|
||||||
|
}): Promise<{ data: ServiceType[]; meta: { total: number; page: number; pageSize: number; totalPages: number } }> {
|
||||||
|
const page = filter.page ?? 1;
|
||||||
|
const pageSize = filter.pageSize ?? 20;
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
if (filter.isActive !== undefined) where.isActive = filter.isActive;
|
||||||
|
if (filter.canBeBookedAlone !== undefined) where.canBeBookedAlone = filter.canBeBookedAlone;
|
||||||
|
if (filter.search) where.serviceName = ILike(`%${filter.search}%`);
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { [filter.sortBy ?? 'displayOrder']: filter.sortOrder ?? 'ASC' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
return { data, meta: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a single service type by ID. */
|
||||||
|
async findById(id: string): Promise<ServiceType> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) throw new NotFoundException(`Service type ${id} not found`);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a service type by code. */
|
||||||
|
async findByCode(code: string): Promise<ServiceType | null> {
|
||||||
|
return this.repository.findByCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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`);
|
||||||
|
return this.repository.create({
|
||||||
|
code: dto.code,
|
||||||
|
serviceName: dto.serviceName,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
canBeBookedAlone: dto.canBeBookedAlone ?? true,
|
||||||
|
includesFirstMile: dto.includesFirstMile ?? false,
|
||||||
|
includesLastMile: dto.includesLastMile ?? false,
|
||||||
|
includesCustoms: dto.includesCustoms ?? false,
|
||||||
|
priorityBonusPoints: dto.priorityBonusPoints ?? 0,
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
displayOrder: dto.displayOrder ?? 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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);
|
||||||
|
if (!updated) throw new NotFoundException(`Service type ${id} not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soft-delete a service type. */
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { CreateSurchargeTypeDto } from '../dto/create-surcharge-type.dto';
|
||||||
|
import { UpdateSurchargeTypeDto } from '../dto/update-surcharge-type.dto';
|
||||||
|
import { SurchargeType } from '../entities/surcharge-type.entity';
|
||||||
|
import {
|
||||||
|
ISurchargeTypesRepository,
|
||||||
|
SURCHARGE_TYPES_REPOSITORY,
|
||||||
|
} from '../interfaces/surcharge-types.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SurchargeTypesService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SURCHARGE_TYPES_REPOSITORY)
|
||||||
|
private readonly repository: ISurchargeTypesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** List surcharge types with pagination. */
|
||||||
|
async findAll(filter: {
|
||||||
|
isActive?: boolean;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}): Promise<{ data: SurchargeType[]; meta: { total: number; page: number; pageSize: number; totalPages: number } }> {
|
||||||
|
const page = filter.page ?? 1;
|
||||||
|
const pageSize = filter.pageSize ?? 20;
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
if (filter.isActive !== undefined) where.isActive = filter.isActive;
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { name: 'ASC' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
return { data, meta: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a single surcharge type by ID. */
|
||||||
|
async findById(id: string): Promise<SurchargeType> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) throw new NotFoundException(`Surcharge type ${id} not found`);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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`);
|
||||||
|
return this.repository.create({
|
||||||
|
code: dto.code,
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 updated = await this.repository.update(id, dto);
|
||||||
|
if (!updated) throw new NotFoundException(`Surcharge type ${id} not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soft-delete a surcharge type. */
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { CreateSurchargeDto } from '../dto/create-surcharge.dto';
|
||||||
|
import { UpdateSurchargeDto } from '../dto/update-surcharge.dto';
|
||||||
|
import { Surcharge } from '../entities/surcharge.entity';
|
||||||
|
import {
|
||||||
|
ISurchargesRepository,
|
||||||
|
SURCHARGES_REPOSITORY,
|
||||||
|
} from '../interfaces/surcharges.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SurchargesService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SURCHARGES_REPOSITORY)
|
||||||
|
private readonly repository: ISurchargesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** List surcharges with pagination. */
|
||||||
|
async findAll(filter: {
|
||||||
|
isActive?: boolean;
|
||||||
|
surchargeTypeId?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}): Promise<{ data: Surcharge[]; meta: { total: number; page: number; pageSize: number; totalPages: number } }> {
|
||||||
|
const page = filter.page ?? 1;
|
||||||
|
const pageSize = filter.pageSize ?? 20;
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
if (filter.isActive !== undefined) where.isActive = filter.isActive;
|
||||||
|
if (filter.surchargeTypeId) where.surchargeTypeId = filter.surchargeTypeId;
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
relations: { surchargeType: true },
|
||||||
|
order: { feeName: 'ASC' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
return { data, meta: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a single surcharge by ID. */
|
||||||
|
async findById(id: string): Promise<Surcharge> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) throw new NotFoundException(`Surcharge ${id} not found`);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new surcharge. */
|
||||||
|
async create(dto: CreateSurchargeDto): Promise<Surcharge> {
|
||||||
|
return this.repository.create({
|
||||||
|
surchargeTypeId: dto.surchargeTypeId,
|
||||||
|
feeName: dto.feeName,
|
||||||
|
triggerDescription: dto.triggerDescription ?? null,
|
||||||
|
calculationMethod: dto.calculationMethod,
|
||||||
|
rate: dto.rate,
|
||||||
|
currency: dto.currency,
|
||||||
|
applyToRail: dto.applyToRail ?? false,
|
||||||
|
applyToFirstMile: dto.applyToFirstMile ?? false,
|
||||||
|
applyToLastMile: dto.applyToLastMile ?? false,
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update an existing surcharge. */
|
||||||
|
async update(id: string, dto: UpdateSurchargeDto): Promise<Surcharge> {
|
||||||
|
await this.findById(id);
|
||||||
|
const updated = await this.repository.update(id, dto);
|
||||||
|
if (!updated) throw new NotFoundException(`Surcharge ${id} not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soft-delete a surcharge. */
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { CreateWeightLimitRuleDto } from '../dto/create-weight-limit-rule.dto';
|
||||||
|
import { UpdateWeightLimitRuleDto } from '../dto/update-weight-limit-rule.dto';
|
||||||
|
import { WeightLimitRule } from '../entities/weight-limit-rule.entity';
|
||||||
|
import {
|
||||||
|
IWeightLimitRulesRepository,
|
||||||
|
WEIGHT_LIMIT_RULES_REPOSITORY,
|
||||||
|
} from '../interfaces/weight-limit-rules.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WeightLimitRulesService {
|
||||||
|
constructor(
|
||||||
|
@Inject(WEIGHT_LIMIT_RULES_REPOSITORY)
|
||||||
|
private readonly repository: IWeightLimitRulesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** List weight limit rules with pagination. */
|
||||||
|
async findAll(filter: {
|
||||||
|
isActive?: boolean;
|
||||||
|
containerTypeId?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}): Promise<{ data: WeightLimitRule[]; meta: { total: number; page: number; pageSize: number; totalPages: number } }> {
|
||||||
|
const page = filter.page ?? 1;
|
||||||
|
const pageSize = filter.pageSize ?? 20;
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
if (filter.isActive !== undefined) where.isActive = filter.isActive;
|
||||||
|
if (filter.containerTypeId) where.containerTypeId = filter.containerTypeId;
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
relations: { containerType: true, surcharge: { surchargeType: true } },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
return { data, meta: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a single weight limit rule by ID. */
|
||||||
|
async findById(id: string): Promise<WeightLimitRule> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) throw new NotFoundException(`Weight limit rule ${id} not found`);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new weight limit rule. */
|
||||||
|
async create(dto: CreateWeightLimitRuleDto): Promise<WeightLimitRule> {
|
||||||
|
if (dto.warningThresholdTons > dto.maxWeightTons) {
|
||||||
|
throw new BadRequestException('warningThresholdTons must be ≤ maxWeightTons');
|
||||||
|
}
|
||||||
|
return this.repository.create({
|
||||||
|
containerTypeId: dto.containerTypeId,
|
||||||
|
tradeDirection: dto.tradeDirection,
|
||||||
|
maxWeightTons: dto.maxWeightTons,
|
||||||
|
warningThresholdTons: dto.warningThresholdTons,
|
||||||
|
exceededAction: dto.exceededAction,
|
||||||
|
surchargeId: dto.surchargeId ?? null,
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update an existing weight limit rule. */
|
||||||
|
async update(id: string, dto: UpdateWeightLimitRuleDto): Promise<WeightLimitRule> {
|
||||||
|
const existing = await this.findById(id);
|
||||||
|
const warning = dto.warningThresholdTons ?? existing.warningThresholdTons;
|
||||||
|
const max = dto.maxWeightTons ?? existing.maxWeightTons;
|
||||||
|
if (warning > max) throw new BadRequestException('warningThresholdTons must be ≤ maxWeightTons');
|
||||||
|
const updated = await this.repository.update(id, dto);
|
||||||
|
if (!updated) throw new NotFoundException(`Weight limit rule ${id} not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soft-delete a weight limit rule. */
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
|
||||||
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from "class-validator";
|
|
||||||
|
|
||||||
export class CreateServiceTypeDto {
|
|
||||||
@ApiProperty({ description: "Service type name", maxLength: 255 })
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(255)
|
|
||||||
serviceName!: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Detailed description of the service" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Can be booked alone", default: true })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
canBeBookedAlone?: boolean = true;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Includes first mile service", default: false })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
includesFirstMile?: boolean = false;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Includes last mile service", default: false })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
includesLastMile?: boolean = false;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Includes customs clearance", default: false })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
includesCustoms?: boolean = false;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Priority bonus points for booking priority", default: 0 })
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
@Min(0)
|
|
||||||
priorityBonusPoints?: number = 0;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Is the service type active", default: true })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isActive?: boolean = true;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Display order for UI", default: 1 })
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
displayOrder?: number = 1;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
|
||||||
import { Transform, Type } from "class-transformer";
|
|
||||||
import { IsBoolean, IsIn, IsInt, IsOptional, IsString, Min } from "class-validator";
|
|
||||||
|
|
||||||
export class FilterServiceTypeDto {
|
|
||||||
@ApiPropertyOptional({ description: "Filter by active status" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@Transform(({ value }) => value === "true" || value === true)
|
|
||||||
isActive?: boolean;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Filter by can be booked alone" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
@Transform(({ value }) => value === "true" || value === true)
|
|
||||||
canBeBookedAlone?: boolean;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Search by service name (case-insensitive)" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
search?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: ["serviceName", "displayOrder", "createdAt"], default: "displayOrder" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(["serviceName", "displayOrder", "createdAt"])
|
|
||||||
sortBy?: string = "displayOrder";
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "ASC" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(["ASC", "DESC"])
|
|
||||||
sortOrder?: "ASC" | "DESC" = "ASC";
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Page number", default: 1, minimum: 1 })
|
|
||||||
@IsOptional()
|
|
||||||
@Type(() => Number)
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
page?: number = 1;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Items per page", default: 20, minimum: 1 })
|
|
||||||
@IsOptional()
|
|
||||||
@Type(() => Number)
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
pageSize?: number = 20;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PartialType } from "@nestjs/mapped-types";
|
|
||||||
|
|
||||||
import { CreateServiceTypeDto } from "./create-service-type.dto";
|
|
||||||
|
|
||||||
export class UpdateServiceTypeDto extends PartialType(CreateServiceTypeDto) {}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { BaseEntity } from "@edr/api-common";
|
|
||||||
import { Column, Entity, Index } from "typeorm";
|
|
||||||
|
|
||||||
@Entity({ schema: "freight", name: "service_types" })
|
|
||||||
@Index(["isActive"])
|
|
||||||
@Index(["displayOrder"])
|
|
||||||
export class ServiceType extends BaseEntity {
|
|
||||||
@Column({ name: "service_name", type: "varchar", length: 255, nullable: false })
|
|
||||||
serviceName!: string;
|
|
||||||
|
|
||||||
@Column({ name: "description", type: "text", nullable: true })
|
|
||||||
description?: string | null;
|
|
||||||
|
|
||||||
@Column({ name: "can_be_booked_alone", type: "boolean", default: true })
|
|
||||||
canBeBookedAlone!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: "includes_first_mile", type: "boolean", default: false })
|
|
||||||
includesFirstMile!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: "includes_last_mile", type: "boolean", default: false })
|
|
||||||
includesLastMile!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: "includes_customs", type: "boolean", default: false })
|
|
||||||
includesCustoms!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: "priority_bonus_points", type: "int", default: 0 })
|
|
||||||
priorityBonusPoints!: number;
|
|
||||||
|
|
||||||
@Column({ name: "is_active", type: "boolean", default: true })
|
|
||||||
isActive!: boolean;
|
|
||||||
|
|
||||||
@Column({ name: "display_order", type: "int", default: 1 })
|
|
||||||
displayOrder!: number;
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
HttpCode,
|
|
||||||
HttpStatus,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
Query,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
|
||||||
|
|
||||||
import { CreateServiceTypeDto } from "./dto/create-service-type.dto";
|
|
||||||
import { FilterServiceTypeDto } from "./dto/filter-service-type.dto";
|
|
||||||
import { UpdateServiceTypeDto } from "./dto/update-service-type.dto";
|
|
||||||
import { ServiceTypesService } from "./service-types.service";
|
|
||||||
|
|
||||||
@ApiTags("service-types")
|
|
||||||
@Controller("service-types")
|
|
||||||
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
|
||||||
@ApiBearerAuth()
|
|
||||||
export class ServiceTypesController {
|
|
||||||
constructor(private readonly service: ServiceTypesService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@ApiOperation({
|
|
||||||
summary: "List service types",
|
|
||||||
description: "Paginated list with optional filtering by isActive, canBeBookedAlone, and name search.",
|
|
||||||
})
|
|
||||||
findAll(@Query() filter: FilterServiceTypeDto) {
|
|
||||||
return this.service.findAll(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(":id")
|
|
||||||
@ApiOperation({ summary: "Get a service type by ID" })
|
|
||||||
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
|
||||||
return this.service.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@ApiOperation({ summary: "Create a new service type" })
|
|
||||||
create(@Body() dto: CreateServiceTypeDto) {
|
|
||||||
return this.service.create(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch(":id")
|
|
||||||
@ApiOperation({ summary: "Update a service type" })
|
|
||||||
update(
|
|
||||||
@Param("id", ParseUUIDPipe) id: string,
|
|
||||||
@Body() dto: UpdateServiceTypeDto,
|
|
||||||
) {
|
|
||||||
return this.service.update(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(":id")
|
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
|
||||||
@ApiOperation({ summary: "Soft-delete a service type" })
|
|
||||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
|
||||||
return this.service.remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
||||||
|
|
||||||
import { ServiceType } from "./entities/service-type.entity";
|
|
||||||
import { SERVICE_TYPES_REPOSITORY } from "./interfaces/service-types.repository.interface";
|
|
||||||
import { ServiceTypesRepository } from "./service-types.repository";
|
|
||||||
import { ServiceTypesController } from "./service-types.controller";
|
|
||||||
import { ServiceTypesService } from "./service-types.service";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TypeOrmModule.forFeature([ServiceType])],
|
|
||||||
controllers: [ServiceTypesController],
|
|
||||||
providers: [
|
|
||||||
ServiceTypesRepository,
|
|
||||||
{
|
|
||||||
provide: SERVICE_TYPES_REPOSITORY,
|
|
||||||
useExisting: ServiceTypesRepository,
|
|
||||||
},
|
|
||||||
ServiceTypesService,
|
|
||||||
],
|
|
||||||
exports: [ServiceTypesService],
|
|
||||||
})
|
|
||||||
export class ServiceTypesModule {}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { BaseRepository } from "@edr/api-common";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
|
||||||
import { FindManyOptions, Repository } from "typeorm";
|
|
||||||
|
|
||||||
import { ServiceType } from "./entities/service-type.entity";
|
|
||||||
import { IServiceTypesRepository } from "./interfaces/service-types.repository.interface";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ServiceTypesRepository
|
|
||||||
extends BaseRepository<ServiceType>
|
|
||||||
implements IServiceTypesRepository
|
|
||||||
{
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(ServiceType)
|
|
||||||
repository: Repository<ServiceType>,
|
|
||||||
) {
|
|
||||||
super(repository);
|
|
||||||
}
|
|
||||||
|
|
||||||
override findById(id: string): Promise<ServiceType | null> {
|
|
||||||
return this.repository.findOne({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
override findAndCount(options?: FindManyOptions<ServiceType>): Promise<[ServiceType[], number]> {
|
|
||||||
return this.repository.findAndCount(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
|
||||||
import { ILike } from "typeorm";
|
|
||||||
|
|
||||||
import { CreateServiceTypeDto } from "./dto/create-service-type.dto";
|
|
||||||
import { FilterServiceTypeDto } from "./dto/filter-service-type.dto";
|
|
||||||
import { UpdateServiceTypeDto } from "./dto/update-service-type.dto";
|
|
||||||
import { ServiceType } from "./entities/service-type.entity";
|
|
||||||
import {
|
|
||||||
IServiceTypesRepository,
|
|
||||||
SERVICE_TYPES_REPOSITORY,
|
|
||||||
} from "./interfaces/service-types.repository.interface";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ServiceTypesService {
|
|
||||||
constructor(
|
|
||||||
@Inject(SERVICE_TYPES_REPOSITORY)
|
|
||||||
private readonly repository: IServiceTypesRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all service types with pagination and optional filtering.
|
|
||||||
*/
|
|
||||||
async findAll(filter: FilterServiceTypeDto): Promise<{
|
|
||||||
data: ServiceType[];
|
|
||||||
meta: {
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
totalPages: number;
|
|
||||||
};
|
|
||||||
}> {
|
|
||||||
const where: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
if (filter.isActive !== undefined) {
|
|
||||||
where.isActive = filter.isActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.canBeBookedAlone !== undefined) {
|
|
||||||
where.canBeBookedAlone = filter.canBeBookedAlone;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.search) {
|
|
||||||
where.serviceName = ILike(`%${filter.search}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [data, total] = await this.repository.findAndCount({
|
|
||||||
where,
|
|
||||||
order: { [filter.sortBy!]: filter.sortOrder },
|
|
||||||
skip: (filter.page! - 1) * filter.pageSize!,
|
|
||||||
take: filter.pageSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page: filter.page!,
|
|
||||||
pageSize: filter.pageSize!,
|
|
||||||
totalPages: Math.ceil(total / filter.pageSize!),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single service type by ID.
|
|
||||||
*/
|
|
||||||
async findById(id: string): Promise<ServiceType> {
|
|
||||||
const entity = await this.repository.findById(id);
|
|
||||||
if (!entity) {
|
|
||||||
throw new NotFoundException(`Service type ${id} not found`);
|
|
||||||
}
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new service type.
|
|
||||||
*/
|
|
||||||
async create(dto: CreateServiceTypeDto): Promise<ServiceType> {
|
|
||||||
return this.repository.create({
|
|
||||||
serviceName: dto.serviceName,
|
|
||||||
description: dto.description ?? null,
|
|
||||||
canBeBookedAlone: dto.canBeBookedAlone ?? true,
|
|
||||||
includesFirstMile: dto.includesFirstMile ?? false,
|
|
||||||
includesLastMile: dto.includesLastMile ?? false,
|
|
||||||
includesCustoms: dto.includesCustoms ?? false,
|
|
||||||
priorityBonusPoints: dto.priorityBonusPoints ?? 0,
|
|
||||||
isActive: dto.isActive ?? true,
|
|
||||||
displayOrder: dto.displayOrder ?? 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an existing service type.
|
|
||||||
*/
|
|
||||||
async update(id: string, dto: UpdateServiceTypeDto): Promise<ServiceType> {
|
|
||||||
await this.findById(id);
|
|
||||||
const updated = await this.repository.update(id, dto);
|
|
||||||
if (!updated) {
|
|
||||||
throw new NotFoundException(`Service type ${id} not found`);
|
|
||||||
}
|
|
||||||
return this.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Soft-delete a service type.
|
|
||||||
*/
|
|
||||||
async remove(id: string): Promise<void> {
|
|
||||||
await this.findById(id);
|
|
||||||
await this.repository.softDelete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,30 @@ import type { BaseEntity } from "../common";
|
|||||||
export * from "./file_upload_settings";
|
export * from "./file_upload_settings";
|
||||||
export * from "./dropdown_settings";
|
export * from "./dropdown_settings";
|
||||||
|
|
||||||
|
export enum TradeDirection {
|
||||||
|
IMPORT = 'IMPORT',
|
||||||
|
EXPORT = 'EXPORT',
|
||||||
|
BOTH = 'BOTH',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PriorityType {
|
||||||
|
USD_PAYER = 'USD_PAYER',
|
||||||
|
RAIL_AND_FORWARDING = 'RAIL_AND_FORWARDING',
|
||||||
|
GOVERNMENT_ACCOUNT = 'GOVERNMENT_ACCOUNT',
|
||||||
|
HIGH_VOLUME_SHIPMENT = 'HIGH_VOLUME_SHIPMENT',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ExceededAction {
|
||||||
|
WARNING_ONLY = 'WARNING_ONLY',
|
||||||
|
HARD_BLOCK = 'HARD_BLOCK',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CalculationMethod {
|
||||||
|
PER_TON = 'PER_TON',
|
||||||
|
FLAT_FEE = 'FLAT_FEE',
|
||||||
|
PERCENTAGE = 'PERCENTAGE',
|
||||||
|
}
|
||||||
|
|
||||||
export enum BookingStatus {
|
export enum BookingStatus {
|
||||||
Draft = "DRAFT",
|
Draft = "DRAFT",
|
||||||
Confirmed = "CONFIRMED",
|
Confirmed = "CONFIRMED",
|
||||||
|
|||||||
Reference in New Issue
Block a user