mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 19:30:57 +00:00
Merge pull request #171 from Tria-plc/freight_feature/priority
Freight feature/priority
This commit is contained in:
@@ -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 {
|
||||
freightType: booking.freightType as 'CONTAINER' | 'BULK',
|
||||
cargoTypeId: booking.cargoTypeId ?? null,
|
||||
@@ -192,6 +203,7 @@ export class BookingPricingService {
|
||||
isGovernment: booking.isGovernment,
|
||||
allowConsolidation: booking.allowConsolidation,
|
||||
shippingLineId: booking.shippingLineId,
|
||||
totalWagons,
|
||||
containers,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,9 +120,13 @@ export class BookingsService {
|
||||
vgmPerUnitTons: c.vgmPerUnitTons,
|
||||
totalVgmTons,
|
||||
isReefer: ct.isReefer,
|
||||
wagonsRequired: c.quantity * (Number(ct.wagonsPerUnit) || 1),
|
||||
};
|
||||
}),
|
||||
);
|
||||
const totalWagons = Math.ceil(
|
||||
containers.reduce((sum, c) => sum + c.wagonsRequired, 0),
|
||||
);
|
||||
|
||||
return {
|
||||
freightType: dto.freightType,
|
||||
@@ -135,6 +139,7 @@ export class BookingsService {
|
||||
allowConsolidation:
|
||||
dto.freightType === 'CONTAINER' ? dto.allowConsolidation : false,
|
||||
shippingLineId: dto.shippingLineId,
|
||||
totalWagons,
|
||||
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 { CargoTypesController } from './controllers/cargo-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 { ServiceTypesController } from './controllers/service-types.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 { CargoType } from './entities/cargo-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 { ServiceType } from './entities/service-type.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 { 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 { PRIORITY_CONFIGS_REPOSITORY } from './interfaces/priority-configs.repository.interface';
|
||||
import { RATES_REPOSITORY } from './interfaces/rates.repository.interface';
|
||||
import { SERVICE_TYPES_REPOSITORY } from './interfaces/service-types.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 { CargoTypesRepository } from './repositories/cargo-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 { ServiceTypesRepository } from './repositories/service-types.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 { CargoTypesService } from './services/cargo-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 { ServiceTypesService } from './services/service-types.service';
|
||||
import { ShippingLinesService } from './services/shipping-lines.service';
|
||||
@@ -70,7 +70,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
TypeOrmModule.forFeature([
|
||||
CargoType,
|
||||
ContainerType,
|
||||
PriorityRule,
|
||||
PriorityConfig,
|
||||
SurchargeType,
|
||||
ServiceType,
|
||||
WeightLimitRule,
|
||||
@@ -87,7 +87,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
controllers: [
|
||||
CargoTypesController,
|
||||
ContainerTypesController,
|
||||
PriorityRulesController,
|
||||
PriorityConfigsController,
|
||||
SurchargeTypesController,
|
||||
ServiceTypesController,
|
||||
WeightLimitRulesController,
|
||||
@@ -101,8 +101,8 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
{ provide: CARGO_TYPES_REPOSITORY, useExisting: CargoTypesRepository },
|
||||
ContainerTypesRepository,
|
||||
{ provide: CONTAINER_TYPES_REPOSITORY, useExisting: ContainerTypesRepository },
|
||||
PriorityRulesRepository,
|
||||
{ provide: PRIORITY_RULES_REPOSITORY, useExisting: PriorityRulesRepository },
|
||||
PriorityConfigsRepository,
|
||||
{ provide: PRIORITY_CONFIGS_REPOSITORY, useExisting: PriorityConfigsRepository },
|
||||
SurchargeTypesRepository,
|
||||
{ provide: SURCHARGE_TYPES_REPOSITORY, useExisting: SurchargeTypesRepository },
|
||||
ServiceTypesRepository,
|
||||
@@ -119,7 +119,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
{ provide: APPROVAL_RULES_REPOSITORY, useExisting: ApprovalRulesRepository },
|
||||
CargoTypesService,
|
||||
ContainerTypesService,
|
||||
PriorityRulesService,
|
||||
PriorityConfigsService,
|
||||
SurchargeTypesService,
|
||||
ServiceTypesService,
|
||||
WeightLimitRulesService,
|
||||
@@ -137,7 +137,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
ContainerTypesService,
|
||||
SurchargeTypesService,
|
||||
WeightLimitRulesService,
|
||||
PriorityRulesService,
|
||||
PriorityConfigsService,
|
||||
YardsService,
|
||||
ShippingLinesService,
|
||||
RatesService,
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
WEIGHT_LIMIT_RULES_REPOSITORY,
|
||||
} from './interfaces/weight-limit-rules.repository.interface';
|
||||
import {
|
||||
IPriorityRulesRepository,
|
||||
PRIORITY_RULES_REPOSITORY,
|
||||
} from './interfaces/priority-rules.repository.interface';
|
||||
IPriorityConfigsRepository,
|
||||
PRIORITY_CONFIGS_REPOSITORY,
|
||||
} from './interfaces/priority-configs.repository.interface';
|
||||
import {
|
||||
ISurchargeTypesRepository,
|
||||
SURCHARGE_TYPES_REPOSITORY,
|
||||
@@ -58,6 +58,7 @@ export interface BookingEvaluationInput {
|
||||
isGovernment?: boolean;
|
||||
allowConsolidation?: boolean;
|
||||
shippingLineId?: string | null;
|
||||
totalWagons: number;
|
||||
containers: BookingContainerEvalInput[];
|
||||
}
|
||||
|
||||
@@ -95,8 +96,8 @@ export class RuleEngineService {
|
||||
private readonly serviceTypesRepo: IServiceTypesRepository,
|
||||
@Inject(WEIGHT_LIMIT_RULES_REPOSITORY)
|
||||
private readonly weightLimitRulesRepo: IWeightLimitRulesRepository,
|
||||
@Inject(PRIORITY_RULES_REPOSITORY)
|
||||
private readonly priorityRulesRepo: IPriorityRulesRepository,
|
||||
@Inject(PRIORITY_CONFIGS_REPOSITORY)
|
||||
private readonly priorityConfigsRepo: IPriorityConfigsRepository,
|
||||
@Inject(SURCHARGE_TYPES_REPOSITORY)
|
||||
private readonly surchargeTypesRepo: ISurchargeTypesRepository,
|
||||
@Inject(RATES_REPOSITORY)
|
||||
@@ -172,13 +173,20 @@ export class RuleEngineService {
|
||||
priorityScore += serviceType.priorityBonusPoints;
|
||||
}
|
||||
|
||||
const priorityRules = await this.priorityRulesRepo.findAllActive();
|
||||
for (const rule of priorityRules) {
|
||||
if (
|
||||
rule.conditionCurrency === null ||
|
||||
rule.conditionCurrency === input.paymentCurrency
|
||||
) {
|
||||
priorityScore += rule.score;
|
||||
// Additive priority blocks, each keyed on the booking's total wagon count:
|
||||
// - WAGON rules apply regardless of currency.
|
||||
// - CURRENCY rules apply only when the payment currency matches.
|
||||
const priorityConfigs = await this.priorityConfigsRepo.findAllActive();
|
||||
const wagonsInRange = (cfg: { minWagonCount: number; maxWagonCount: number }) =>
|
||||
input.totalWagons >= cfg.minWagonCount &&
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user