From 7151bce28857b80bd0f90ae9faf8324496b1aa0c Mon Sep 17 00:00:00 2001 From: Marshal Date: Tue, 16 Jun 2026 08:56:52 +0000 Subject: [PATCH 1/3] changes --- ...ReplacePriorityRulesWithPriorityConfigs.ts | 39 ++++++++ .../bookings/booking-pricing.service.ts | 12 +++ .../src/modules/bookings/bookings.service.ts | 5 + .../priority-configs.controller.ts | 75 ++++++++++++++ .../controllers/priority-rules.controller.ts | 56 ----------- .../dto/create-priority-config.dto.ts | 42 ++++++++ .../dto/create-priority-rule.dto.ts | 28 ------ .../dto/update-priority-config.dto.ts | 4 + .../dto/update-priority-rule.dto.ts | 4 - .../entities/priority-config.entity.ts | 31 ++++++ .../entities/priority-rule.entity.ts | 22 ----- .../priority-configs.repository.interface.ts | 14 +++ .../priority-rules.repository.interface.ts | 14 --- .../priority-configs.repository.ts | 46 +++++++++ .../repositories/priority-rules.repository.ts | 43 -------- .../modules/rule-engine/rule-engine.module.ts | 22 ++--- .../rule-engine/rule-engine.service.ts | 32 +++--- .../services/priority-configs.service.ts | 98 +++++++++++++++++++ .../services/priority-rules.service.ts | 75 -------------- .../src/seed/freight-permissions.registry.ts | 4 +- .../src/seed/pricing-data.seeder.ts | 67 ++++++------- 21 files changed, 429 insertions(+), 304 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/1783000000000-ReplacePriorityRulesWithPriorityConfigs.ts create mode 100644 apps/edr-freight-api/src/modules/rule-engine/controllers/priority-configs.controller.ts delete mode 100644 apps/edr-freight-api/src/modules/rule-engine/controllers/priority-rules.controller.ts create mode 100644 apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-config.dto.ts delete mode 100644 apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-rule.dto.ts create mode 100644 apps/edr-freight-api/src/modules/rule-engine/dto/update-priority-config.dto.ts delete mode 100644 apps/edr-freight-api/src/modules/rule-engine/dto/update-priority-rule.dto.ts create mode 100644 apps/edr-freight-api/src/modules/rule-engine/entities/priority-config.entity.ts delete mode 100644 apps/edr-freight-api/src/modules/rule-engine/entities/priority-rule.entity.ts create mode 100644 apps/edr-freight-api/src/modules/rule-engine/interfaces/priority-configs.repository.interface.ts delete mode 100644 apps/edr-freight-api/src/modules/rule-engine/interfaces/priority-rules.repository.interface.ts create mode 100644 apps/edr-freight-api/src/modules/rule-engine/repositories/priority-configs.repository.ts delete mode 100644 apps/edr-freight-api/src/modules/rule-engine/repositories/priority-rules.repository.ts create mode 100644 apps/edr-freight-api/src/modules/rule-engine/services/priority-configs.service.ts delete mode 100644 apps/edr-freight-api/src/modules/rule-engine/services/priority-rules.service.ts diff --git a/apps/edr-freight-api/src/migrations/1783000000000-ReplacePriorityRulesWithPriorityConfigs.ts b/apps/edr-freight-api/src/migrations/1783000000000-ReplacePriorityRulesWithPriorityConfigs.ts new file mode 100644 index 000000000..b8eccbbaf --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1783000000000-ReplacePriorityRulesWithPriorityConfigs.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ReplacePriorityRulesWithPriorityConfigs1783000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE IF EXISTS freight.priority_configs;`); + } +} diff --git a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts index 7c6d363da..14d8a8dbe 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts @@ -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, }; } diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index 0845011e3..c07a658da 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -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, }; } diff --git a/apps/edr-freight-api/src/modules/rule-engine/controllers/priority-configs.controller.ts b/apps/edr-freight-api/src/modules/rule-engine/controllers/priority-configs.controller.ts new file mode 100644 index 000000000..36b863dd6 --- /dev/null +++ b/apps/edr-freight-api/src/modules/rule-engine/controllers/priority-configs.controller.ts @@ -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) { + 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); + } +} diff --git a/apps/edr-freight-api/src/modules/rule-engine/controllers/priority-rules.controller.ts b/apps/edr-freight-api/src/modules/rule-engine/controllers/priority-rules.controller.ts deleted file mode 100644 index bee5cf85b..000000000 --- a/apps/edr-freight-api/src/modules/rule-engine/controllers/priority-rules.controller.ts +++ /dev/null @@ -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) { - 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); - } -} diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-config.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-config.dto.ts new file mode 100644 index 000000000..140954e83 --- /dev/null +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-config.dto.ts @@ -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; +} diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-rule.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-rule.dto.ts deleted file mode 100644 index 16b01fc81..000000000 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-rule.dto.ts +++ /dev/null @@ -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; -} diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/update-priority-config.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/update-priority-config.dto.ts new file mode 100644 index 000000000..ad2e01bfc --- /dev/null +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/update-priority-config.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreatePriorityConfigDto } from './create-priority-config.dto'; + +export class UpdatePriorityConfigDto extends PartialType(CreatePriorityConfigDto) {} diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/update-priority-rule.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/update-priority-rule.dto.ts deleted file mode 100644 index f1e5c9be3..000000000 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/update-priority-rule.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { CreatePriorityRuleDto } from './create-priority-rule.dto'; - -export class UpdatePriorityRuleDto extends PartialType(CreatePriorityRuleDto) {} diff --git a/apps/edr-freight-api/src/modules/rule-engine/entities/priority-config.entity.ts b/apps/edr-freight-api/src/modules/rule-engine/entities/priority-config.entity.ts new file mode 100644 index 000000000..df60b3ea1 --- /dev/null +++ b/apps/edr-freight-api/src/modules/rule-engine/entities/priority-config.entity.ts @@ -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; +} diff --git a/apps/edr-freight-api/src/modules/rule-engine/entities/priority-rule.entity.ts b/apps/edr-freight-api/src/modules/rule-engine/entities/priority-rule.entity.ts deleted file mode 100644 index b04cca95d..000000000 --- a/apps/edr-freight-api/src/modules/rule-engine/entities/priority-rule.entity.ts +++ /dev/null @@ -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; -} diff --git a/apps/edr-freight-api/src/modules/rule-engine/interfaces/priority-configs.repository.interface.ts b/apps/edr-freight-api/src/modules/rule-engine/interfaces/priority-configs.repository.interface.ts new file mode 100644 index 000000000..e4ca08234 --- /dev/null +++ b/apps/edr-freight-api/src/modules/rule-engine/interfaces/priority-configs.repository.interface.ts @@ -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; + findAll(options?: FindManyOptions): Promise; + findAndCount(options?: FindManyOptions): Promise<[PriorityConfig[], number]>; + findAllActive(): Promise; + create(data: Partial): Promise; + update(id: string, data: Partial): Promise; + softDelete(id: string): Promise; +} diff --git a/apps/edr-freight-api/src/modules/rule-engine/interfaces/priority-rules.repository.interface.ts b/apps/edr-freight-api/src/modules/rule-engine/interfaces/priority-rules.repository.interface.ts deleted file mode 100644 index 608d06e4c..000000000 --- a/apps/edr-freight-api/src/modules/rule-engine/interfaces/priority-rules.repository.interface.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { FindManyOptions } from 'typeorm'; -import { PriorityRule } from '../entities/priority-rule.entity'; - -export interface IPriorityRulesRepository { - findById(id: string): Promise; - findAllActive(): Promise; - findAll(options?: FindManyOptions): Promise; - findAndCount(options?: FindManyOptions): Promise<[PriorityRule[], number]>; - create(data: Partial): Promise; - update(id: string, data: Partial): Promise; - softDelete(id: string): Promise; -} - -export const PRIORITY_RULES_REPOSITORY = Symbol('PRIORITY_RULES_REPOSITORY'); diff --git a/apps/edr-freight-api/src/modules/rule-engine/repositories/priority-configs.repository.ts b/apps/edr-freight-api/src/modules/rule-engine/repositories/priority-configs.repository.ts new file mode 100644 index 000000000..d8326b794 --- /dev/null +++ b/apps/edr-freight-api/src/modules/rule-engine/repositories/priority-configs.repository.ts @@ -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; + + constructor(private readonly dataSource: DataSource) { + this.repo = this.dataSource.getRepository(PriorityConfig); + } + + async findById(id: string): Promise { + return this.repo.findOne({ where: { id } }); + } + + async findAll(options?: FindManyOptions): Promise { + return this.repo.find(options); + } + + async findAndCount(options?: FindManyOptions): Promise<[PriorityConfig[], number]> { + return this.repo.findAndCount(options); + } + + async findAllActive(): Promise { + return this.repo.find({ + where: { isActive: true }, + order: { displayOrder: 'ASC' }, + }); + } + + async create(data: Partial): Promise { + const entity = this.repo.create(data); + return this.repo.save(entity); + } + + async update(id: string, data: Partial): Promise { + await this.repo.update(id, data); + return this.findById(id); + } + + async softDelete(id: string): Promise { + await this.repo.softDelete(id); + } +} diff --git a/apps/edr-freight-api/src/modules/rule-engine/repositories/priority-rules.repository.ts b/apps/edr-freight-api/src/modules/rule-engine/repositories/priority-rules.repository.ts deleted file mode 100644 index fa51de65a..000000000 --- a/apps/edr-freight-api/src/modules/rule-engine/repositories/priority-rules.repository.ts +++ /dev/null @@ -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; - - constructor(private readonly dataSource: DataSource) { - this.repo = this.dataSource.getRepository(PriorityRule); - } - - findById(id: string): Promise { - return this.repo.findOne({ where: { id } }); - } - - findAllActive(): Promise { - return this.repo.find({ where: { isActive: true } }); - } - - findAll(options?: FindManyOptions): Promise { - return this.repo.find(options); - } - - findAndCount(options?: FindManyOptions): Promise<[PriorityRule[], number]> { - return this.repo.findAndCount(options); - } - - async create(data: Partial): Promise { - const entity = this.repo.create(data); - return this.repo.save(entity); - } - - async update(id: string, data: Partial): Promise { - await this.repo.update(id, data); - return this.findById(id); - } - - async softDelete(id: string): Promise { - await this.repo.softDelete(id); - } -} diff --git a/apps/edr-freight-api/src/modules/rule-engine/rule-engine.module.ts b/apps/edr-freight-api/src/modules/rule-engine/rule-engine.module.ts index 4af5066ce..49f1c446c 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/rule-engine.module.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/rule-engine.module.ts @@ -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, diff --git a/apps/edr-freight-api/src/modules/rule-engine/rule-engine.service.ts b/apps/edr-freight-api/src/modules/rule-engine/rule-engine.service.ts index 9bf3635e9..35dbef868 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/rule-engine.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/rule-engine.service.ts @@ -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; } } diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/priority-configs.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/priority-configs.service.ts new file mode 100644 index 000000000..173c63f21 --- /dev/null +++ b/apps/edr-freight-api/src/modules/rule-engine/services/priority-configs.service.ts @@ -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 = {}; + 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 { + const entity = await this.repository.findById(id); + if (!entity) throw new NotFoundException(`Priority config ${id} not found`); + return entity; + } + + async create(dto: CreatePriorityConfigDto): Promise { + 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 { + 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 { + await this.findById(id); + await this.repository.softDelete(id); + } + + async reorder(ids: string[]): Promise { + await this.displayOrder.reorderByIds(PriorityConfig, 'displayOrder', ids); + } + + async moveOrder(id: string, direction: 'up' | 'down'): Promise { + 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'); + } + } +} diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/priority-rules.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/priority-rules.service.ts deleted file mode 100644 index 07e282aba..000000000 --- a/apps/edr-freight-api/src/modules/rule-engine/services/priority-rules.service.ts +++ /dev/null @@ -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 = {}; - 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 { - 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 { - 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 { - 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 { - await this.findById(id); - await this.repository.softDelete(id); - } -} diff --git a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts index 14d599ab3..0ee62fa7c 100644 --- a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts +++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts @@ -16,7 +16,7 @@ export const RULE_ENGINE_RESOURCE_SLUGS = [ 'shipping-lines', 'weight-limit-rules', 'surcharge-types', - 'priority-rules', + 'priority-configs', 'rates', 'approval-rules', ] as const; @@ -65,7 +65,7 @@ const RULE_ENGINE_PERMISSION_IDS: Record [ct.code, ct])); @@ -381,41 +381,34 @@ private async seedWeightLimits(wlRepo: any, ctRepo: any): Promise { this.logger.log("Seeded weight limit rules"); } - private async seedPriorityRules(prRepo: any): Promise { - const existing = await prRepo.find({ - where: [ - { code: "USD_PRIORITY" }, - { code: "STANDARD_PRIORITY" }, - { code: "GOVERNMENT_ACCOUNT" }, - ], - }); - for (const r of existing) { - await prRepo.remove(r); + private async seedPriorityConfigs(prRepo: any): Promise { + // Wagon Count Block — independent, applies regardless of currency. + // Currency Block — applies only to the matching payment currency, within the wagon range. + // Both blocks are additive (see RuleEngineService.evaluate). + const rows = [ + // ── 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 }, + { type: "WAGON", label: "Wagons 31–40", currency: null, minWagonCount: 31, maxWagonCount: 40, scorePoints: 30, displayOrder: 3 }, + { 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([ - 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"); + this.logger.log("Seeded priority configs"); } private async seedRates( From 0778021d86afcd0f8d6fc4c173469f2fcc13ee42 Mon Sep 17 00:00:00 2001 From: Marshal Date: Tue, 16 Jun 2026 09:26:57 +0000 Subject: [PATCH 2/3] implement priority --- apps/edr-freight-web/backoffice/src/App.tsx | 2 +- .../backoffice/src/constants/URLS.ts | 4 +- .../src/pages/ruleEngine/config/resources.ts | 45 ++++++++++++++----- .../services/ruleEngine/ruleEngine.service.ts | 6 +-- .../backoffice/src/types/rule-engine/index.ts | 2 +- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 744fa17d8..dcbb6f50a 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -317,7 +317,7 @@ const App = () => { } + element={} /> } /> diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index 7449ae7f3..99f817b74 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -212,8 +212,8 @@ export const URL_CONSTANTS = { WAGON_TYPES: "/wagon-types", WAGON_TYPE_BY_ID: (id: string) => `/wagon-types/${id}`, - PRIORITY_RULES: "/priority-rules", - PRIORITY_RULE_BY_ID: (id: string) => `/priority-rules/${id}`, + PRIORITY_CONFIGS: "/priority-configs", + PRIORITY_CONFIG_BY_ID: (id: string) => `/priority-configs/${id}`, SERVICE_TYPES: "/service-types", SERVICE_TYPE_BY_ID: (id: string) => `/service-types/${id}`, diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts index 689f994c0..6ac2b29c7 100644 --- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts +++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts @@ -110,7 +110,15 @@ const RATE_UNITS = ["PER_WAGON", "PER_TON", "PER_CONTAINER", "PER_KM", "FLAT"].m value: v, })); -const CURRENCIES = [{ label: "USD", value: "USD" }]; +const CURRENCIES = [ + { label: "USD", value: "USD" }, + { label: "ETB", value: "ETB" }, +]; + +const PRIORITY_CONFIG_TYPES = [ + { label: "Wagon count", value: "WAGON" }, + { label: "Payment currency", value: "CURRENCY" }, +]; const codeColumn = (key: string, header = "Code"): ResourceColumn => ({ id: key, @@ -226,29 +234,42 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [ ], }, { - slug: "priority-rules", + slug: "priority-configs", label: "Priority Rules", category: "rules", - subtitle: "Booking priority scoring rules", + subtitle: "Wagon-count and payment-currency scoring rules", searchPlaceholder: "Search priority rules...", + orderConfig: { field: "displayOrder", label: "Display order" }, columns: [ - codeColumn("code"), { id: "label", header: "Label", accessorKey: "label" }, - { id: "score", header: "Score", accessorKey: "score", format: "number" }, - { id: "conditionCurrency", header: "Currency", accessorKey: "conditionCurrency" }, + { id: "type", header: "Type", accessorKey: "type" }, + { id: "currency", header: "Currency", accessorKey: "currency" }, + { id: "minWagonCount", header: "Min wagons", accessorKey: "minWagonCount", format: "number" }, + { id: "maxWagonCount", header: "Max wagons", accessorKey: "maxWagonCount", format: "number" }, + { id: "scorePoints", header: "Points", accessorKey: "scorePoints", format: "number" }, activeColumn, ], formFields: [ { name: "label", label: "Label", type: "text", required: true }, - { name: "score", label: "Score", type: "number", required: true }, { - name: "conditionCurrency", - label: "Condition currency", + name: "type", + label: "Type", + type: "select", + required: true, + options: PRIORITY_CONFIG_TYPES, + placeholder: "Wagon count or payment currency", + }, + { + name: "currency", + label: "Currency (required for Payment currency type)", type: "select", optional: true, - options: [{ label: "Any", value: RULE_ENGINE_SELECT_NONE }, ...CURRENCIES], - placeholder: "Any currency (optional)", + options: [{ label: "None", value: RULE_ENGINE_SELECT_NONE }, ...CURRENCIES], + placeholder: "Leave as None for Wagon count rules", }, + { name: "minWagonCount", label: "Min wagon count", type: "number", required: true }, + { name: "maxWagonCount", label: "Max wagon count", type: "number", required: true }, + { name: "scorePoints", label: "Score points", type: "number", required: true }, { name: "isActive", label: "Active", type: "boolean" }, ], }, @@ -500,7 +521,7 @@ export const getCategorySidebarChildren = ( })); export const DEFAULT_CONFIGURATION_SLUG: RuleEngineResourceSlug = "cargo-types"; -export const DEFAULT_RULES_SLUG: RuleEngineResourceSlug = "priority-rules"; +export const DEFAULT_RULES_SLUG: RuleEngineResourceSlug = "priority-configs"; /** @deprecated Use DEFAULT_CONFIGURATION_SLUG */ export const DEFAULT_RULE_ENGINE_SLUG = DEFAULT_CONFIGURATION_SLUG; diff --git a/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts b/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts index 38328c782..9f092f46d 100644 --- a/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts @@ -28,7 +28,7 @@ const RESOURCE_BASE: Record = { "cargo-types": URL_CONSTANTS.RULE_ENGINE.CARGO_TYPES, "container-types": URL_CONSTANTS.RULE_ENGINE.CONTAINER_TYPES, "wagon-types": URL_CONSTANTS.RULE_ENGINE.WAGON_TYPES, - "priority-rules": URL_CONSTANTS.RULE_ENGINE.PRIORITY_RULES, + "priority-configs": URL_CONSTANTS.RULE_ENGINE.PRIORITY_CONFIGS, "service-types": URL_CONSTANTS.RULE_ENGINE.SERVICE_TYPES, "surcharge-types": URL_CONSTANTS.RULE_ENGINE.SURCHARGE_TYPES, "weight-limit-rules": URL_CONSTANTS.RULE_ENGINE.WEIGHT_LIMIT_RULES, @@ -46,8 +46,8 @@ const byIdPath = (resource: RuleEngineResourceSlug, id: string): string => { return URL_CONSTANTS.RULE_ENGINE.CONTAINER_TYPE_BY_ID(id); case "wagon-types": return URL_CONSTANTS.RULE_ENGINE.WAGON_TYPE_BY_ID(id); - case "priority-rules": - return URL_CONSTANTS.RULE_ENGINE.PRIORITY_RULE_BY_ID(id); + case "priority-configs": + return URL_CONSTANTS.RULE_ENGINE.PRIORITY_CONFIG_BY_ID(id); case "service-types": return URL_CONSTANTS.RULE_ENGINE.SERVICE_TYPE_BY_ID(id); case "surcharge-types": diff --git a/apps/edr-freight-web/backoffice/src/types/rule-engine/index.ts b/apps/edr-freight-web/backoffice/src/types/rule-engine/index.ts index 041907f59..69cb05058 100644 --- a/apps/edr-freight-web/backoffice/src/types/rule-engine/index.ts +++ b/apps/edr-freight-web/backoffice/src/types/rule-engine/index.ts @@ -2,7 +2,7 @@ export type RuleEngineResourceSlug = | "cargo-types" | "container-types" | "wagon-types" - | "priority-rules" + | "priority-configs" | "service-types" | "surcharge-types" | "weight-limit-rules" From 3afccbd660510699abcec422c9358a1c86195607 Mon Sep 17 00:00:00 2001 From: Marshal Date: Tue, 16 Jun 2026 10:24:22 +0000 Subject: [PATCH 3/3] fix priority ui --- apps/edr-freight-web/backoffice/src/App.tsx | 8 ++++---- .../components/ruleEngine/RuleEngineFormDialog.tsx | 14 ++++++++++++-- .../pages/ruleEngine/RuleEngineResourcePage.tsx | 9 +++++++-- .../src/pages/ruleEngine/config/resources.ts | 9 +++++---- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index dcbb6f50a..b957e0f87 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -179,10 +179,10 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ icon: , children: [ ...getCategorySidebarChildren("configuration"), - { - label: "Train scheduling rules", - href: "/dashboard/configuration/train-scheduling-rules", - }, + // { + // label: "Train scheduling rules", + // href: "/dashboard/configuration/train-scheduling-rules", + // }, ], }, { diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx index cc8a68688..61edd76f9 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineFormDialog.tsx @@ -165,7 +165,17 @@ const RuleEngineFormDialog = ({ } }, [open, fields, initialRecord]); - const formRows = useMemo(() => buildFormRows(fields), [fields]); + const visibleFields = useMemo( + () => + fields.filter( + (field) => + !field.hideWhen || + !field.hideWhen.equals.includes(String(values[field.hideWhen.field] ?? "")), + ), + [fields, values], + ); + + const formRows = useMemo(() => buildFormRows(visibleFields), [visibleFields]); const setField = (name: string, value: unknown) => { setValues((current) => ({ ...current, [name]: value })); @@ -175,7 +185,7 @@ const RuleEngineFormDialog = ({ event.preventDefault(); const payload: Record = {}; - for (const field of fields) { + for (const field of visibleFields) { const raw = values[field.name]; if (field.type === "number") { if (raw === "" || raw === undefined) continue; diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx index 64e79f8e7..f03fb199e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx @@ -296,8 +296,13 @@ const RuleEngineResourcePage = () => { }; const handleFormSubmit = (values: Record) => { - const payload = - config.slug === "rates" ? { ...values, currency: "USD" } : values; + let payload = values; + if (config.slug === "rates") { + payload = { ...values, currency: "USD" }; + } else if (config.slug === "priority-configs") { + // Label is required by the backend but hidden in the UI for now. + payload = { ...values, label: String(Date.now()) }; + } if (editing?.id) { update.mutate( diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts index 6ac2b29c7..b29654653 100644 --- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts +++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/config/resources.ts @@ -34,6 +34,8 @@ export interface FormFieldDef { optional?: boolean; options?: { label: string; value: string }[]; placeholder?: string; + /** Hide this field when another field currently equals one of these values. */ + hideWhen?: { field: string; equals: string[] }; } export interface RuleEngineOrderConfig { @@ -241,7 +243,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [ searchPlaceholder: "Search priority rules...", orderConfig: { field: "displayOrder", label: "Display order" }, columns: [ - { id: "label", header: "Label", accessorKey: "label" }, { id: "type", header: "Type", accessorKey: "type" }, { id: "currency", header: "Currency", accessorKey: "currency" }, { id: "minWagonCount", header: "Min wagons", accessorKey: "minWagonCount", format: "number" }, @@ -250,7 +251,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [ activeColumn, ], formFields: [ - { name: "label", label: "Label", type: "text", required: true }, { name: "type", label: "Type", @@ -261,11 +261,12 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [ }, { name: "currency", - label: "Currency (required for Payment currency type)", + label: "Currency", type: "select", optional: true, options: [{ label: "None", value: RULE_ENGINE_SELECT_NONE }, ...CURRENCIES], - placeholder: "Leave as None for Wagon count rules", + placeholder: "Select a currency", + hideWhen: { field: "type", equals: ["WAGON"] }, }, { name: "minWagonCount", label: "Min wagon count", type: "number", required: true }, { name: "maxWagonCount", label: "Max wagon count", type: "number", required: true },