mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 03:05:42 +00:00
changes
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class ReplacePriorityRulesWithPriorityConfigs1783000000000 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE freight.priority_configs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
type VARCHAR(20) NOT NULL CHECK (type IN ('WAGON', 'CURRENCY')),
|
||||||
|
label VARCHAR(100) NOT NULL,
|
||||||
|
currency VARCHAR(5) NULL,
|
||||||
|
min_wagon_count INT NOT NULL,
|
||||||
|
max_wagon_count INT NOT NULL,
|
||||||
|
score_points INT NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
display_order INT NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ NULL,
|
||||||
|
CONSTRAINT chk_wagon_range CHECK (min_wagon_count <= max_wagon_count),
|
||||||
|
CONSTRAINT chk_currency_for_type CHECK (
|
||||||
|
(type = 'WAGON' AND currency IS NULL) OR
|
||||||
|
(type = 'CURRENCY' AND currency IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX idx_priority_configs_type_active ON freight.priority_configs (type, is_active);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX idx_priority_configs_currency_type ON freight.priority_configs (currency, type);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS freight.priority_configs;`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -182,6 +182,17 @@ export class BookingPricingService {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
// Wagon count is persisted per container line at booking creation; sum it.
|
||||||
|
const totalWagons =
|
||||||
|
booking.freightType === 'CONTAINER'
|
||||||
|
? Math.ceil(
|
||||||
|
(booking.bookingContainers ?? []).reduce(
|
||||||
|
(sum, bc) => sum + Number(bc.wagonsRequired ?? 0),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
freightType: booking.freightType as 'CONTAINER' | 'BULK',
|
freightType: booking.freightType as 'CONTAINER' | 'BULK',
|
||||||
cargoTypeId: booking.cargoTypeId ?? null,
|
cargoTypeId: booking.cargoTypeId ?? null,
|
||||||
@@ -192,6 +203,7 @@ export class BookingPricingService {
|
|||||||
isGovernment: booking.isGovernment,
|
isGovernment: booking.isGovernment,
|
||||||
allowConsolidation: booking.allowConsolidation,
|
allowConsolidation: booking.allowConsolidation,
|
||||||
shippingLineId: booking.shippingLineId,
|
shippingLineId: booking.shippingLineId,
|
||||||
|
totalWagons,
|
||||||
containers,
|
containers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,9 +120,13 @@ export class BookingsService {
|
|||||||
vgmPerUnitTons: c.vgmPerUnitTons,
|
vgmPerUnitTons: c.vgmPerUnitTons,
|
||||||
totalVgmTons,
|
totalVgmTons,
|
||||||
isReefer: ct.isReefer,
|
isReefer: ct.isReefer,
|
||||||
|
wagonsRequired: c.quantity * (Number(ct.wagonsPerUnit) || 1),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const totalWagons = Math.ceil(
|
||||||
|
containers.reduce((sum, c) => sum + c.wagonsRequired, 0),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
freightType: dto.freightType,
|
freightType: dto.freightType,
|
||||||
@@ -135,6 +139,7 @@ export class BookingsService {
|
|||||||
allowConsolidation:
|
allowConsolidation:
|
||||||
dto.freightType === 'CONTAINER' ? dto.allowConsolidation : false,
|
dto.freightType === 'CONTAINER' ? dto.allowConsolidation : false,
|
||||||
shippingLineId: dto.shippingLineId,
|
shippingLineId: dto.shippingLineId,
|
||||||
|
totalWagons,
|
||||||
containers,
|
containers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||||
|
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CreatePriorityConfigDto } from '../dto/create-priority-config.dto';
|
||||||
|
import { UpdatePriorityConfigDto } from '../dto/update-priority-config.dto';
|
||||||
|
import { MoveOrderDto } from '../dto/move-order.dto';
|
||||||
|
import { ReorderItemsDto } from '../dto/reorder-items.dto';
|
||||||
|
import { PriorityConfigsService } from '../services/priority-configs.service';
|
||||||
|
|
||||||
|
@ApiTags('priority-configs')
|
||||||
|
@Controller('priority-configs')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class PriorityConfigsController {
|
||||||
|
constructor(private readonly service: PriorityConfigsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@RuleEngineView('priority-configs')
|
||||||
|
@ApiOperation({ summary: 'List priority configs' })
|
||||||
|
findAll(@Query() query: Record<string, string>) {
|
||||||
|
return this.service.findAll({
|
||||||
|
type: (query['type'] as 'WAGON' | 'CURRENCY') || undefined,
|
||||||
|
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')
|
||||||
|
@RuleEngineView('priority-configs')
|
||||||
|
@ApiOperation({ summary: 'Get a priority config by ID' })
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@RuleEngineManage('priority-configs')
|
||||||
|
@ApiOperation({ summary: 'Create a priority config' })
|
||||||
|
create(@Body() dto: CreatePriorityConfigDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('reorder')
|
||||||
|
@RuleEngineManage('priority-configs')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Bulk reorder priority configs by ID list' })
|
||||||
|
reorder(@Body() dto: ReorderItemsDto) {
|
||||||
|
return this.service.reorder(dto.ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/move-order')
|
||||||
|
@RuleEngineManage('priority-configs')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Move a priority config up or down in display order' })
|
||||||
|
moveOrder(@Param('id', ParseUUIDPipe) id: string, @Body() dto: MoveOrderDto) {
|
||||||
|
return this.service.moveOrder(id, dto.direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@RuleEngineManage('priority-configs')
|
||||||
|
@ApiOperation({ summary: 'Update a priority config' })
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdatePriorityConfigDto) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@RuleEngineManage('priority-configs')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Soft-delete a priority config' })
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import {
|
|
||||||
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
|
||||||
Param, ParseUUIDPipe, Patch, Post, Query,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
|
|
||||||
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')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
export class PriorityRulesController {
|
|
||||||
constructor(private readonly service: PriorityRulesService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@RuleEngineView('priority-rules')
|
|
||||||
@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')
|
|
||||||
@RuleEngineView('priority-rules')
|
|
||||||
@ApiOperation({ summary: 'Get a priority rule by ID' })
|
|
||||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
|
||||||
return this.service.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@RuleEngineManage('priority-rules')
|
|
||||||
@ApiOperation({ summary: 'Create a priority rule' })
|
|
||||||
create(@Body() dto: CreatePriorityRuleDto) {
|
|
||||||
return this.service.create(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch(':id')
|
|
||||||
@RuleEngineManage('priority-rules')
|
|
||||||
@ApiOperation({ summary: 'Update a priority rule' })
|
|
||||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdatePriorityRuleDto) {
|
|
||||||
return this.service.update(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
@RuleEngineManage('priority-rules')
|
|
||||||
@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,42 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsIn, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreatePriorityConfigDto {
|
||||||
|
@ApiProperty({ description: 'Config type: WAGON or CURRENCY', enum: ['WAGON', 'CURRENCY'] })
|
||||||
|
@IsIn(['WAGON', 'CURRENCY'])
|
||||||
|
type!: 'WAGON' | 'CURRENCY';
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Human-readable label', maxLength: 100 })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
label!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Currency code (e.g., USD, ETB). Required for type=CURRENCY, must be null for type=WAGON',
|
||||||
|
maxLength: 5,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(5)
|
||||||
|
currency?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Minimum wagon count in range (inclusive)' })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
minWagonCount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Maximum wagon count in range (inclusive)' })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
maxWagonCount!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Points awarded when booking matches this rule', default: 0 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
scorePoints!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: false, description: 'Feature flag — toggle without code deploy' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
||||||
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
|
||||||
|
|
||||||
export class CreatePriorityRuleDto {
|
|
||||||
@ApiProperty({ description: 'Human-readable label', maxLength: 100 })
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(100)
|
|
||||||
label!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'Points added to booking.priority_score when condition matches', default: 0 })
|
|
||||||
@IsInt()
|
|
||||||
@Min(0)
|
|
||||||
score!: number;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: 'If set, rule only matches bookings with this payment currency (e.g. USD). Null = matches all.',
|
|
||||||
maxLength: 5,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(5)
|
|
||||||
conditionCurrency?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ default: false, description: 'Feature flag — toggle without code deploy' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreatePriorityConfigDto } from './create-priority-config.dto';
|
||||||
|
|
||||||
|
export class UpdatePriorityConfigDto extends PartialType(CreatePriorityConfigDto) {}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
|
||||||
import { CreatePriorityRuleDto } from './create-priority-rule.dto';
|
|
||||||
|
|
||||||
export class UpdatePriorityRuleDto extends PartialType(CreatePriorityRuleDto) {}
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { BaseEntity } from '@edr/api-common';
|
||||||
|
import { Column, Entity, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ schema: 'freight', name: 'priority_configs' })
|
||||||
|
@Index(['type', 'isActive'])
|
||||||
|
@Index(['currency', 'type'])
|
||||||
|
export class PriorityConfig extends BaseEntity {
|
||||||
|
@Column({ name: 'type', type: 'varchar', length: 20 })
|
||||||
|
type!: 'WAGON' | 'CURRENCY';
|
||||||
|
|
||||||
|
@Column({ name: 'label', type: 'varchar', length: 100 })
|
||||||
|
label!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'currency', type: 'varchar', length: 5, nullable: true })
|
||||||
|
currency?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'min_wagon_count', type: 'int' })
|
||||||
|
minWagonCount!: number;
|
||||||
|
|
||||||
|
@Column({ name: 'max_wagon_count', type: 'int' })
|
||||||
|
maxWagonCount!: number;
|
||||||
|
|
||||||
|
@Column({ name: 'score_points', type: 'int', default: 0 })
|
||||||
|
scorePoints!: number;
|
||||||
|
|
||||||
|
@Column({ name: 'is_active', type: 'boolean', default: false })
|
||||||
|
isActive!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'display_order', type: 'int', default: 1 })
|
||||||
|
displayOrder!: number;
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { BaseEntity } from '@edr/api-common';
|
|
||||||
import { Column, Entity, Index } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity({ schema: 'freight', name: 'priority_rules' })
|
|
||||||
@Index(['code'])
|
|
||||||
@Index(['isActive'])
|
|
||||||
export class PriorityRule extends BaseEntity {
|
|
||||||
@Column({ name: 'code', type: 'varchar', length: 40, unique: true })
|
|
||||||
code!: string;
|
|
||||||
|
|
||||||
@Column({ name: 'label', type: 'varchar', length: 100, nullable: true })
|
|
||||||
label!: string;
|
|
||||||
|
|
||||||
@Column({ name: 'score', type: 'int', default: 0, nullable: true })
|
|
||||||
score!: number;
|
|
||||||
|
|
||||||
@Column({ name: 'condition_currency', type: 'varchar', length: 5, nullable: true })
|
|
||||||
conditionCurrency?: string | null;
|
|
||||||
|
|
||||||
@Column({ name: 'is_active', type: 'boolean', default: false })
|
|
||||||
isActive!: boolean;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FindManyOptions } from 'typeorm';
|
||||||
|
import { PriorityConfig } from '../entities/priority-config.entity';
|
||||||
|
|
||||||
|
export const PRIORITY_CONFIGS_REPOSITORY = Symbol('PRIORITY_CONFIGS_REPOSITORY');
|
||||||
|
|
||||||
|
export interface IPriorityConfigsRepository {
|
||||||
|
findById(id: string): Promise<PriorityConfig | null>;
|
||||||
|
findAll(options?: FindManyOptions<PriorityConfig>): Promise<PriorityConfig[]>;
|
||||||
|
findAndCount(options?: FindManyOptions<PriorityConfig>): Promise<[PriorityConfig[], number]>;
|
||||||
|
findAllActive(): Promise<PriorityConfig[]>;
|
||||||
|
create(data: Partial<PriorityConfig>): Promise<PriorityConfig>;
|
||||||
|
update(id: string, data: Partial<PriorityConfig>): Promise<PriorityConfig | null>;
|
||||||
|
softDelete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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');
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, FindManyOptions, Repository } from 'typeorm';
|
||||||
|
import { PriorityConfig } from '../entities/priority-config.entity';
|
||||||
|
import { IPriorityConfigsRepository } from '../interfaces/priority-configs.repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PriorityConfigsRepository implements IPriorityConfigsRepository {
|
||||||
|
private readonly repo: Repository<PriorityConfig>;
|
||||||
|
|
||||||
|
constructor(private readonly dataSource: DataSource) {
|
||||||
|
this.repo = this.dataSource.getRepository(PriorityConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<PriorityConfig | null> {
|
||||||
|
return this.repo.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(options?: FindManyOptions<PriorityConfig>): Promise<PriorityConfig[]> {
|
||||||
|
return this.repo.find(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAndCount(options?: FindManyOptions<PriorityConfig>): Promise<[PriorityConfig[], number]> {
|
||||||
|
return this.repo.findAndCount(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllActive(): Promise<PriorityConfig[]> {
|
||||||
|
return this.repo.find({
|
||||||
|
where: { isActive: true },
|
||||||
|
order: { displayOrder: 'ASC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<PriorityConfig>): Promise<PriorityConfig> {
|
||||||
|
const entity = this.repo.create(data);
|
||||||
|
return this.repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<PriorityConfig>): Promise<PriorityConfig | null> {
|
||||||
|
await this.repo.update(id, data);
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(id: string): Promise<void> {
|
||||||
|
await this.repo.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { ApprovalRulesController } from './controllers/approval-rules.controller';
|
import { ApprovalRulesController } from './controllers/approval-rules.controller';
|
||||||
import { CargoTypesController } from './controllers/cargo-types.controller';
|
import { CargoTypesController } from './controllers/cargo-types.controller';
|
||||||
import { ContainerTypesController } from './controllers/container-types.controller';
|
import { ContainerTypesController } from './controllers/container-types.controller';
|
||||||
import { PriorityRulesController } from './controllers/priority-rules.controller';
|
import { PriorityConfigsController } from './controllers/priority-configs.controller';
|
||||||
import { RatesController } from './controllers/rates.controller';
|
import { RatesController } from './controllers/rates.controller';
|
||||||
import { ServiceTypesController } from './controllers/service-types.controller';
|
import { ServiceTypesController } from './controllers/service-types.controller';
|
||||||
import { ShippingLinesController } from './controllers/shipping-lines.controller';
|
import { ShippingLinesController } from './controllers/shipping-lines.controller';
|
||||||
@@ -15,7 +15,7 @@ import { YardsController } from './controllers/yards.controller';
|
|||||||
import { ApprovalRule } from './entities/approval-rule.entity';
|
import { ApprovalRule } from './entities/approval-rule.entity';
|
||||||
import { CargoType } from './entities/cargo-type.entity';
|
import { CargoType } from './entities/cargo-type.entity';
|
||||||
import { ContainerType } from './entities/container-type.entity';
|
import { ContainerType } from './entities/container-type.entity';
|
||||||
import { PriorityRule } from './entities/priority-rule.entity';
|
import { PriorityConfig } from './entities/priority-config.entity';
|
||||||
import { Rate } from './entities/rate.entity';
|
import { Rate } from './entities/rate.entity';
|
||||||
import { ServiceType } from './entities/service-type.entity';
|
import { ServiceType } from './entities/service-type.entity';
|
||||||
import { ShippingLine } from './entities/shipping-line.entity';
|
import { ShippingLine } from './entities/shipping-line.entity';
|
||||||
@@ -26,7 +26,7 @@ import { Yard } from './entities/yard.entity';
|
|||||||
import { APPROVAL_RULES_REPOSITORY } from './interfaces/approval-rules.repository.interface';
|
import { APPROVAL_RULES_REPOSITORY } from './interfaces/approval-rules.repository.interface';
|
||||||
import { CARGO_TYPES_REPOSITORY } from './interfaces/cargo-types.repository.interface';
|
import { CARGO_TYPES_REPOSITORY } from './interfaces/cargo-types.repository.interface';
|
||||||
import { CONTAINER_TYPES_REPOSITORY } from './interfaces/container-types.repository.interface';
|
import { CONTAINER_TYPES_REPOSITORY } from './interfaces/container-types.repository.interface';
|
||||||
import { PRIORITY_RULES_REPOSITORY } from './interfaces/priority-rules.repository.interface';
|
import { PRIORITY_CONFIGS_REPOSITORY } from './interfaces/priority-configs.repository.interface';
|
||||||
import { RATES_REPOSITORY } from './interfaces/rates.repository.interface';
|
import { RATES_REPOSITORY } from './interfaces/rates.repository.interface';
|
||||||
import { SERVICE_TYPES_REPOSITORY } from './interfaces/service-types.repository.interface';
|
import { SERVICE_TYPES_REPOSITORY } from './interfaces/service-types.repository.interface';
|
||||||
import { SHIPPING_LINES_REPOSITORY } from './interfaces/shipping-lines.repository.interface';
|
import { SHIPPING_LINES_REPOSITORY } from './interfaces/shipping-lines.repository.interface';
|
||||||
@@ -37,7 +37,7 @@ import { YARDS_REPOSITORY } from './interfaces/yards.repository.interface';
|
|||||||
import { ApprovalRulesRepository } from './repositories/approval-rules.repository';
|
import { ApprovalRulesRepository } from './repositories/approval-rules.repository';
|
||||||
import { CargoTypesRepository } from './repositories/cargo-types.repository';
|
import { CargoTypesRepository } from './repositories/cargo-types.repository';
|
||||||
import { ContainerTypesRepository } from './repositories/container-types.repository';
|
import { ContainerTypesRepository } from './repositories/container-types.repository';
|
||||||
import { PriorityRulesRepository } from './repositories/priority-rules.repository';
|
import { PriorityConfigsRepository } from './repositories/priority-configs.repository';
|
||||||
import { RatesRepository } from './repositories/rates.repository';
|
import { RatesRepository } from './repositories/rates.repository';
|
||||||
import { ServiceTypesRepository } from './repositories/service-types.repository';
|
import { ServiceTypesRepository } from './repositories/service-types.repository';
|
||||||
import { ShippingLinesRepository } from './repositories/shipping-lines.repository';
|
import { ShippingLinesRepository } from './repositories/shipping-lines.repository';
|
||||||
@@ -49,7 +49,7 @@ import { ApprovalRulesService } from './services/approval-rules.service';
|
|||||||
import { DisplayOrderService } from './services/display-order.service';
|
import { DisplayOrderService } from './services/display-order.service';
|
||||||
import { CargoTypesService } from './services/cargo-types.service';
|
import { CargoTypesService } from './services/cargo-types.service';
|
||||||
import { ContainerTypesService } from './services/container-types.service';
|
import { ContainerTypesService } from './services/container-types.service';
|
||||||
import { PriorityRulesService } from './services/priority-rules.service';
|
import { PriorityConfigsService } from './services/priority-configs.service';
|
||||||
import { RatesService } from './services/rates.service';
|
import { RatesService } from './services/rates.service';
|
||||||
import { ServiceTypesService } from './services/service-types.service';
|
import { ServiceTypesService } from './services/service-types.service';
|
||||||
import { ShippingLinesService } from './services/shipping-lines.service';
|
import { ShippingLinesService } from './services/shipping-lines.service';
|
||||||
@@ -70,7 +70,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
|||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
CargoType,
|
CargoType,
|
||||||
ContainerType,
|
ContainerType,
|
||||||
PriorityRule,
|
PriorityConfig,
|
||||||
SurchargeType,
|
SurchargeType,
|
||||||
ServiceType,
|
ServiceType,
|
||||||
WeightLimitRule,
|
WeightLimitRule,
|
||||||
@@ -87,7 +87,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
|||||||
controllers: [
|
controllers: [
|
||||||
CargoTypesController,
|
CargoTypesController,
|
||||||
ContainerTypesController,
|
ContainerTypesController,
|
||||||
PriorityRulesController,
|
PriorityConfigsController,
|
||||||
SurchargeTypesController,
|
SurchargeTypesController,
|
||||||
ServiceTypesController,
|
ServiceTypesController,
|
||||||
WeightLimitRulesController,
|
WeightLimitRulesController,
|
||||||
@@ -101,8 +101,8 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
|||||||
{ provide: CARGO_TYPES_REPOSITORY, useExisting: CargoTypesRepository },
|
{ provide: CARGO_TYPES_REPOSITORY, useExisting: CargoTypesRepository },
|
||||||
ContainerTypesRepository,
|
ContainerTypesRepository,
|
||||||
{ provide: CONTAINER_TYPES_REPOSITORY, useExisting: ContainerTypesRepository },
|
{ provide: CONTAINER_TYPES_REPOSITORY, useExisting: ContainerTypesRepository },
|
||||||
PriorityRulesRepository,
|
PriorityConfigsRepository,
|
||||||
{ provide: PRIORITY_RULES_REPOSITORY, useExisting: PriorityRulesRepository },
|
{ provide: PRIORITY_CONFIGS_REPOSITORY, useExisting: PriorityConfigsRepository },
|
||||||
SurchargeTypesRepository,
|
SurchargeTypesRepository,
|
||||||
{ provide: SURCHARGE_TYPES_REPOSITORY, useExisting: SurchargeTypesRepository },
|
{ provide: SURCHARGE_TYPES_REPOSITORY, useExisting: SurchargeTypesRepository },
|
||||||
ServiceTypesRepository,
|
ServiceTypesRepository,
|
||||||
@@ -119,7 +119,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
|||||||
{ provide: APPROVAL_RULES_REPOSITORY, useExisting: ApprovalRulesRepository },
|
{ provide: APPROVAL_RULES_REPOSITORY, useExisting: ApprovalRulesRepository },
|
||||||
CargoTypesService,
|
CargoTypesService,
|
||||||
ContainerTypesService,
|
ContainerTypesService,
|
||||||
PriorityRulesService,
|
PriorityConfigsService,
|
||||||
SurchargeTypesService,
|
SurchargeTypesService,
|
||||||
ServiceTypesService,
|
ServiceTypesService,
|
||||||
WeightLimitRulesService,
|
WeightLimitRulesService,
|
||||||
@@ -137,7 +137,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
|||||||
ContainerTypesService,
|
ContainerTypesService,
|
||||||
SurchargeTypesService,
|
SurchargeTypesService,
|
||||||
WeightLimitRulesService,
|
WeightLimitRulesService,
|
||||||
PriorityRulesService,
|
PriorityConfigsService,
|
||||||
YardsService,
|
YardsService,
|
||||||
ShippingLinesService,
|
ShippingLinesService,
|
||||||
RatesService,
|
RatesService,
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import {
|
|||||||
WEIGHT_LIMIT_RULES_REPOSITORY,
|
WEIGHT_LIMIT_RULES_REPOSITORY,
|
||||||
} from './interfaces/weight-limit-rules.repository.interface';
|
} from './interfaces/weight-limit-rules.repository.interface';
|
||||||
import {
|
import {
|
||||||
IPriorityRulesRepository,
|
IPriorityConfigsRepository,
|
||||||
PRIORITY_RULES_REPOSITORY,
|
PRIORITY_CONFIGS_REPOSITORY,
|
||||||
} from './interfaces/priority-rules.repository.interface';
|
} from './interfaces/priority-configs.repository.interface';
|
||||||
import {
|
import {
|
||||||
ISurchargeTypesRepository,
|
ISurchargeTypesRepository,
|
||||||
SURCHARGE_TYPES_REPOSITORY,
|
SURCHARGE_TYPES_REPOSITORY,
|
||||||
@@ -58,6 +58,7 @@ export interface BookingEvaluationInput {
|
|||||||
isGovernment?: boolean;
|
isGovernment?: boolean;
|
||||||
allowConsolidation?: boolean;
|
allowConsolidation?: boolean;
|
||||||
shippingLineId?: string | null;
|
shippingLineId?: string | null;
|
||||||
|
totalWagons: number;
|
||||||
containers: BookingContainerEvalInput[];
|
containers: BookingContainerEvalInput[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +96,8 @@ export class RuleEngineService {
|
|||||||
private readonly serviceTypesRepo: IServiceTypesRepository,
|
private readonly serviceTypesRepo: IServiceTypesRepository,
|
||||||
@Inject(WEIGHT_LIMIT_RULES_REPOSITORY)
|
@Inject(WEIGHT_LIMIT_RULES_REPOSITORY)
|
||||||
private readonly weightLimitRulesRepo: IWeightLimitRulesRepository,
|
private readonly weightLimitRulesRepo: IWeightLimitRulesRepository,
|
||||||
@Inject(PRIORITY_RULES_REPOSITORY)
|
@Inject(PRIORITY_CONFIGS_REPOSITORY)
|
||||||
private readonly priorityRulesRepo: IPriorityRulesRepository,
|
private readonly priorityConfigsRepo: IPriorityConfigsRepository,
|
||||||
@Inject(SURCHARGE_TYPES_REPOSITORY)
|
@Inject(SURCHARGE_TYPES_REPOSITORY)
|
||||||
private readonly surchargeTypesRepo: ISurchargeTypesRepository,
|
private readonly surchargeTypesRepo: ISurchargeTypesRepository,
|
||||||
@Inject(RATES_REPOSITORY)
|
@Inject(RATES_REPOSITORY)
|
||||||
@@ -172,13 +173,20 @@ export class RuleEngineService {
|
|||||||
priorityScore += serviceType.priorityBonusPoints;
|
priorityScore += serviceType.priorityBonusPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
const priorityRules = await this.priorityRulesRepo.findAllActive();
|
// Additive priority blocks, each keyed on the booking's total wagon count:
|
||||||
for (const rule of priorityRules) {
|
// - WAGON rules apply regardless of currency.
|
||||||
if (
|
// - CURRENCY rules apply only when the payment currency matches.
|
||||||
rule.conditionCurrency === null ||
|
const priorityConfigs = await this.priorityConfigsRepo.findAllActive();
|
||||||
rule.conditionCurrency === input.paymentCurrency
|
const wagonsInRange = (cfg: { minWagonCount: number; maxWagonCount: number }) =>
|
||||||
) {
|
input.totalWagons >= cfg.minWagonCount &&
|
||||||
priorityScore += rule.score;
|
input.totalWagons <= cfg.maxWagonCount;
|
||||||
|
|
||||||
|
for (const cfg of priorityConfigs) {
|
||||||
|
const applies =
|
||||||
|
cfg.type === 'WAGON' ||
|
||||||
|
(cfg.type === 'CURRENCY' && cfg.currency === input.paymentCurrency);
|
||||||
|
if (applies && wagonsInRange(cfg)) {
|
||||||
|
priorityScore += cfg.scorePoints;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { CreatePriorityConfigDto } from '../dto/create-priority-config.dto';
|
||||||
|
import { UpdatePriorityConfigDto } from '../dto/update-priority-config.dto';
|
||||||
|
import { PriorityConfig } from '../entities/priority-config.entity';
|
||||||
|
import {
|
||||||
|
IPriorityConfigsRepository,
|
||||||
|
PRIORITY_CONFIGS_REPOSITORY,
|
||||||
|
} from '../interfaces/priority-configs.repository.interface';
|
||||||
|
import { DisplayOrderService } from './display-order.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PriorityConfigsService {
|
||||||
|
constructor(
|
||||||
|
@Inject(PRIORITY_CONFIGS_REPOSITORY)
|
||||||
|
private readonly repository: IPriorityConfigsRepository,
|
||||||
|
private readonly displayOrder: DisplayOrderService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(filter: {
|
||||||
|
type?: 'WAGON' | 'CURRENCY';
|
||||||
|
isActive?: boolean;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}): Promise<{ data: PriorityConfig[]; 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.type !== undefined) where.type = filter.type;
|
||||||
|
if (filter.isActive !== undefined) where.isActive = filter.isActive;
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { displayOrder: 'ASC' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
return { data, meta: { total, page, pageSize, totalPages: Math.ceil(total / pageSize) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<PriorityConfig> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) throw new NotFoundException(`Priority config ${id} not found`);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreatePriorityConfigDto): Promise<PriorityConfig> {
|
||||||
|
this.validateCurrencyField(dto.type, dto.currency);
|
||||||
|
|
||||||
|
const displayOrder = await this.displayOrder.resolveCreateOrder(PriorityConfig, 'displayOrder', {});
|
||||||
|
|
||||||
|
return this.repository.create({
|
||||||
|
type: dto.type,
|
||||||
|
label: dto.label,
|
||||||
|
currency: dto.currency ?? null,
|
||||||
|
minWagonCount: dto.minWagonCount,
|
||||||
|
maxWagonCount: dto.maxWagonCount,
|
||||||
|
scorePoints: dto.scorePoints ?? 0,
|
||||||
|
isActive: dto.isActive ?? false,
|
||||||
|
displayOrder,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, dto: UpdatePriorityConfigDto): Promise<PriorityConfig> {
|
||||||
|
const existing = await this.findById(id);
|
||||||
|
|
||||||
|
const type = dto.type ?? existing.type;
|
||||||
|
const currency = dto.currency !== undefined ? dto.currency : existing.currency;
|
||||||
|
this.validateCurrencyField(type, currency);
|
||||||
|
|
||||||
|
const { ...patch } = dto;
|
||||||
|
const updated = await this.repository.update(id, patch);
|
||||||
|
if (!updated) throw new NotFoundException(`Priority config ${id} not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reorder(ids: string[]): Promise<void> {
|
||||||
|
await this.displayOrder.reorderByIds(PriorityConfig, 'displayOrder', ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveOrder(id: string, direction: 'up' | 'down'): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.displayOrder.moveOne(PriorityConfig, 'displayOrder', id, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateCurrencyField(type: 'WAGON' | 'CURRENCY', currency: string | undefined | null): void {
|
||||||
|
if (type === 'CURRENCY' && !currency) {
|
||||||
|
throw new BadRequestException('currency field is required when type is CURRENCY');
|
||||||
|
}
|
||||||
|
if (type === 'WAGON' && currency) {
|
||||||
|
throw new BadRequestException('currency field must be null when type is WAGON');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
||||||
import { generateCode } from '../../../common/utils/generate-code.util';
|
|
||||||
import { CreatePriorityRuleDto } from '../dto/create-priority-rule.dto';
|
|
||||||
import { UpdatePriorityRuleDto } from '../dto/update-priority-rule.dto';
|
|
||||||
import { PriorityRule } from '../entities/priority-rule.entity';
|
|
||||||
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: { label: '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 code = generateCode(dto.label);
|
|
||||||
const existing = await this.repository.findAll({ where: { code } });
|
|
||||||
if (existing.length > 0) {
|
|
||||||
throw new ConflictException(`Priority rule with label "${dto.label}" conflicts with existing code "${code}"`);
|
|
||||||
}
|
|
||||||
return this.repository.create({
|
|
||||||
code,
|
|
||||||
label: dto.label,
|
|
||||||
score: dto.score,
|
|
||||||
conditionCurrency: dto.conditionCurrency ?? null,
|
|
||||||
isActive: dto.isActive ?? false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Update an existing priority rule. */
|
|
||||||
async update(id: string, dto: UpdatePriorityRuleDto): Promise<PriorityRule> {
|
|
||||||
await this.findById(id);
|
|
||||||
const { ...patch } = dto;
|
|
||||||
const updated = await this.repository.update(id, patch);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ export const RULE_ENGINE_RESOURCE_SLUGS = [
|
|||||||
'shipping-lines',
|
'shipping-lines',
|
||||||
'weight-limit-rules',
|
'weight-limit-rules',
|
||||||
'surcharge-types',
|
'surcharge-types',
|
||||||
'priority-rules',
|
'priority-configs',
|
||||||
'rates',
|
'rates',
|
||||||
'approval-rules',
|
'approval-rules',
|
||||||
] as const;
|
] as const;
|
||||||
@@ -65,7 +65,7 @@ const RULE_ENGINE_PERMISSION_IDS: Record<RuleEngineResourceSlug, { view: string;
|
|||||||
'shipping-lines': { view: 'b2000001-0001-4000-8000-000000000009', manage: 'b2000001-0001-4000-8000-00000000000a' },
|
'shipping-lines': { view: 'b2000001-0001-4000-8000-000000000009', manage: 'b2000001-0001-4000-8000-00000000000a' },
|
||||||
'weight-limit-rules': { view: 'b2000001-0001-4000-8000-00000000000b', manage: 'b2000001-0001-4000-8000-00000000000c' },
|
'weight-limit-rules': { view: 'b2000001-0001-4000-8000-00000000000b', manage: 'b2000001-0001-4000-8000-00000000000c' },
|
||||||
'surcharge-types': { view: 'b2000001-0001-4000-8000-00000000000d', manage: 'b2000001-0001-4000-8000-00000000000e' },
|
'surcharge-types': { view: 'b2000001-0001-4000-8000-00000000000d', manage: 'b2000001-0001-4000-8000-00000000000e' },
|
||||||
'priority-rules': { view: 'b2000001-0001-4000-8000-00000000000f', manage: 'b2000001-0001-4000-8000-000000000010' },
|
'priority-configs': { view: 'b2000001-0001-4000-8000-00000000000f', manage: 'b2000001-0001-4000-8000-000000000010' },
|
||||||
rates: { view: 'b2000001-0001-4000-8000-000000000011', manage: 'b2000001-0001-4000-8000-000000000012' },
|
rates: { view: 'b2000001-0001-4000-8000-000000000011', manage: 'b2000001-0001-4000-8000-000000000012' },
|
||||||
'approval-rules': { view: 'b2000001-0001-4000-8000-000000000013', manage: 'b2000001-0001-4000-8000-000000000014' },
|
'approval-rules': { view: 'b2000001-0001-4000-8000-000000000013', manage: 'b2000001-0001-4000-8000-000000000014' },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DataSource } from "typeorm";
|
|||||||
import { BookingCargoModifier } from "../modules/bookings/entities/booking-cargo-modifier.entity";
|
import { BookingCargoModifier } from "../modules/bookings/entities/booking-cargo-modifier.entity";
|
||||||
import { CargoType } from "../modules/rule-engine/entities/cargo-type.entity";
|
import { CargoType } from "../modules/rule-engine/entities/cargo-type.entity";
|
||||||
import { ContainerType } from "../modules/rule-engine/entities/container-type.entity";
|
import { ContainerType } from "../modules/rule-engine/entities/container-type.entity";
|
||||||
import { PriorityRule } from "../modules/rule-engine/entities/priority-rule.entity";
|
import { PriorityConfig } from "../modules/rule-engine/entities/priority-config.entity";
|
||||||
import { Rate } from "../modules/rule-engine/entities/rate.entity";
|
import { Rate } from "../modules/rule-engine/entities/rate.entity";
|
||||||
import { ServiceType } from "../modules/rule-engine/entities/service-type.entity";
|
import { ServiceType } from "../modules/rule-engine/entities/service-type.entity";
|
||||||
import { ShippingLine } from "../modules/rule-engine/entities/shipping-line.entity";
|
import { ShippingLine } from "../modules/rule-engine/entities/shipping-line.entity";
|
||||||
@@ -30,13 +30,13 @@ export class PricingDataSeeder {
|
|||||||
const yRepo = manager.getRepository(Yard);
|
const yRepo = manager.getRepository(Yard);
|
||||||
const slRepo = manager.getRepository(ShippingLine);
|
const slRepo = manager.getRepository(ShippingLine);
|
||||||
const wlRepo = manager.getRepository(WeightLimitRule);
|
const wlRepo = manager.getRepository(WeightLimitRule);
|
||||||
const prRepo = manager.getRepository(PriorityRule);
|
const prRepo = manager.getRepository(PriorityConfig);
|
||||||
const rRepo = manager.getRepository(Rate);
|
const rRepo = manager.getRepository(Rate);
|
||||||
|
|
||||||
await this.upsertReferenceData(manager, ctRepo, stRepo, yRepo, slRepo);
|
await this.upsertReferenceData(manager, ctRepo, stRepo, yRepo, slRepo);
|
||||||
await this.seedDomesticRoute(manager, yRepo);
|
await this.seedDomesticRoute(manager, yRepo);
|
||||||
await this.seedWeightLimits(wlRepo, ctRepo);
|
await this.seedWeightLimits(wlRepo, ctRepo);
|
||||||
await this.seedPriorityRules(prRepo);
|
await this.seedPriorityConfigs(prRepo);
|
||||||
const containerTypes = await ctRepo.find();
|
const containerTypes = await ctRepo.find();
|
||||||
const ctByCode = new Map(containerTypes.map((ct) => [ct.code, ct]));
|
const ctByCode = new Map(containerTypes.map((ct) => [ct.code, ct]));
|
||||||
|
|
||||||
@@ -381,41 +381,34 @@ private async seedWeightLimits(wlRepo: any, ctRepo: any): Promise<void> {
|
|||||||
|
|
||||||
this.logger.log("Seeded weight limit rules");
|
this.logger.log("Seeded weight limit rules");
|
||||||
}
|
}
|
||||||
private async seedPriorityRules(prRepo: any): Promise<void> {
|
private async seedPriorityConfigs(prRepo: any): Promise<void> {
|
||||||
const existing = await prRepo.find({
|
// Wagon Count Block — independent, applies regardless of currency.
|
||||||
where: [
|
// Currency Block — applies only to the matching payment currency, within the wagon range.
|
||||||
{ code: "USD_PRIORITY" },
|
// Both blocks are additive (see RuleEngineService.evaluate).
|
||||||
{ code: "STANDARD_PRIORITY" },
|
const rows = [
|
||||||
{ code: "GOVERNMENT_ACCOUNT" },
|
// ── Wagon Count Block ───────────────────────────────────────────────
|
||||||
],
|
{ type: "WAGON", label: "Wagons 1–20", currency: null, minWagonCount: 1, maxWagonCount: 20, scorePoints: 0, displayOrder: 1 },
|
||||||
});
|
{ type: "WAGON", label: "Wagons 21–30", currency: null, minWagonCount: 21, maxWagonCount: 30, scorePoints: 15, displayOrder: 2 },
|
||||||
for (const r of existing) {
|
{ type: "WAGON", label: "Wagons 31–40", currency: null, minWagonCount: 31, maxWagonCount: 40, scorePoints: 30, displayOrder: 3 },
|
||||||
await prRepo.remove(r);
|
{ type: "WAGON", label: "Wagons 41–50", currency: null, minWagonCount: 41, maxWagonCount: 50, scorePoints: 50, displayOrder: 4 },
|
||||||
|
// ── Payment Currency Block ──────────────────────────────────────────
|
||||||
|
{ type: "CURRENCY", label: "USD · Wagons 1–25", currency: "USD", minWagonCount: 1, maxWagonCount: 25, scorePoints: 17, displayOrder: 5 },
|
||||||
|
{ type: "CURRENCY", label: "USD · Wagons 26–50", currency: "USD", minWagonCount: 26, maxWagonCount: 50, scorePoints: 35, displayOrder: 6 },
|
||||||
|
{ type: "CURRENCY", label: "ETB · Wagons 1–50", currency: "ETB", minWagonCount: 1, maxWagonCount: 50, scorePoints: 0, displayOrder: 7 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const existing = await prRepo.findOne({
|
||||||
|
where: { type: row.type, label: row.label },
|
||||||
|
withDeleted: true,
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
await prRepo.save({ ...existing, ...row, isActive: true, deletedAt: null });
|
||||||
|
} else {
|
||||||
|
await prRepo.save(prRepo.create({ ...row, isActive: true }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await prRepo.save([
|
this.logger.log("Seeded priority configs");
|
||||||
prRepo.create({
|
|
||||||
code: "USD_PRIORITY",
|
|
||||||
label: "USD Payment Priority",
|
|
||||||
score: 200,
|
|
||||||
conditionCurrency: "USD",
|
|
||||||
isActive: true,
|
|
||||||
}),
|
|
||||||
prRepo.create({
|
|
||||||
code: "STANDARD_PRIORITY",
|
|
||||||
label: "Standard Priority",
|
|
||||||
score: 50,
|
|
||||||
conditionCurrency: null,
|
|
||||||
isActive: true,
|
|
||||||
}),
|
|
||||||
prRepo.create({
|
|
||||||
code: "GOVERNMENT_ACCOUNT",
|
|
||||||
label: "Government Account Priority",
|
|
||||||
score: 50000,
|
|
||||||
conditionCurrency: null,
|
|
||||||
isActive: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
this.logger.log("Seeded priority rules");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async seedRates(
|
private async seedRates(
|
||||||
|
|||||||
Reference in New Issue
Block a user