diff --git a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts index 471fcb6f2..1c1b490dd 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts @@ -44,7 +44,6 @@ describe('BookingPricingService — domestic corridor', () => { {} as never, {} as never, ratesService as never, - {} as never, exchangeService as never, ); }); 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 fbafaa911..746e7d4f3 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 @@ -2,7 +2,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { ContainerTypesService } from '../rule-engine/services/container-types.service'; import { RatesService } from '../rule-engine/services/rates.service'; -import { ServiceTypesService } from '../rule-engine/services/service-types.service'; import { Rate } from '../rule-engine/entities/rate.entity'; import { ExchangeService } from '@edr/api-common'; import { @@ -40,7 +39,6 @@ export class BookingPricingService { private readonly ruleEngineService: RuleEngineService, private readonly containerTypesService: ContainerTypesService, private readonly ratesService: RatesService, - private readonly serviceTypesService: ServiceTypesService, private readonly exchangeService: ExchangeService, ) {} @@ -255,27 +253,18 @@ export class BookingPricingService { }; } - /** Recompute priority on submit (USD + service tier). */ + /** + * Recompute priority on submit. + * + * The full priority model is additive and capped at 100: + * service-type bonus (≤ 15) + wagon block (≤ 50) + currency block (≤ 35). + * All three components are produced by RuleEngineService.evaluate, so submit + * simply re-runs the engine — there is no extra submit-time inflation. + */ async computeSubmitPriorityScore(booking: Booking): Promise { const evalInput = await this.buildEvalInputForBooking(booking); const ruleResult = await this.ruleEngineService.evaluate(evalInput); - let score = ruleResult.priorityScore; - - const serviceType = await this.serviceTypesService.findById(booking.serviceTypeId); - if (booking.paymentCurrency === 'USD' && serviceType) { - const code = (serviceType.code ?? '').toUpperCase(); - const hasForwarding = - serviceType.includesFirstMile || - serviceType.includesLastMile || - code.includes('FORWARD') || - code.includes('Y'); - const railOnly = code.includes('RAIL') && !hasForwarding; - - if (hasForwarding) score += 1000; - else if (railOnly || code.includes('X')) score += 500; - } - - return score; + return ruleResult.priorityScore; } private async computeBaseRailLinesWithRates( diff --git a/apps/edr-freight-api/src/modules/overview/overview.constants.ts b/apps/edr-freight-api/src/modules/overview/overview.constants.ts index fed9a76c7..6181d446e 100644 --- a/apps/edr-freight-api/src/modules/overview/overview.constants.ts +++ b/apps/edr-freight-api/src/modules/overview/overview.constants.ts @@ -1,4 +1,4 @@ -export const OVERVIEW_URGENT_PRIORITY_THRESHOLD = 1000; +export const OVERVIEW_URGENT_PRIORITY_THRESHOLD = 70; export const OVERVIEW_NEEDS_ACTION_STATUSES = [ 'SUBMITTED', 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 index 140954e83..d2ca44d93 100644 --- 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 @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsBoolean, IsIn, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator'; +import { IsBoolean, IsIn, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator'; export class CreatePriorityConfigDto { @ApiProperty({ description: 'Config type: WAGON or CURRENCY', enum: ['WAGON', 'CURRENCY'] }) @@ -30,9 +30,15 @@ export class CreatePriorityConfigDto { @Min(0) maxWagonCount!: number; - @ApiProperty({ description: 'Points awarded when booking matches this rule', default: 0 }) + @ApiProperty({ + description: + 'Points awarded when booking matches this rule. Capped so the priority blocks sum to ≤ 100 alongside the service-type bonus (service ≤ 15 + wagon ≤ 50 + currency ≤ 35). WAGON configs should not exceed 50; CURRENCY configs should not exceed 35.', + default: 0, + maximum: 50, + }) @IsInt() @Min(0) + @Max(50) scorePoints!: number; @ApiPropertyOptional({ default: false, description: 'Feature flag — toggle without code deploy' }) diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-service-type.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-service-type.dto.ts index 4683d448d..d68625fdc 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-service-type.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-service-type.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator'; +import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, Max, MaxLength, Min } from 'class-validator'; export class CreateServiceTypeDto { @ApiProperty({ description: 'Service type display name', maxLength: 255 }) @@ -32,10 +32,15 @@ export class CreateServiceTypeDto { @IsBoolean() includesCustoms?: boolean; - @ApiPropertyOptional({ description: 'Priority bonus points awarded when this service is used', default: 0 }) + @ApiPropertyOptional({ + description: 'Priority bonus points awarded when this service is used (0–15)', + default: 0, + maximum: 15, + }) @IsOptional() @IsInt() @Min(0) + @Max(15) priorityBonusPoints?: number; @ApiPropertyOptional({ default: true }) diff --git a/apps/edr-freight-api/src/seed/pricing-data.seeder.ts b/apps/edr-freight-api/src/seed/pricing-data.seeder.ts index 75d17be98..28cebc9e7 100644 --- a/apps/edr-freight-api/src/seed/pricing-data.seeder.ts +++ b/apps/edr-freight-api/src/seed/pricing-data.seeder.ts @@ -180,7 +180,7 @@ export class PricingDataSeeder { includesFirstMile: true, includesLastMile: true, includesCustoms: true, - priorityBonusPoints: 100, + priorityBonusPoints: 15, isActive: true, displayOrder: 2, }, @@ -192,7 +192,7 @@ export class PricingDataSeeder { includesFirstMile: false, includesLastMile: false, includesCustoms: false, - priorityBonusPoints: 50, + priorityBonusPoints: 10, isActive: true, displayOrder: 3, }, @@ -382,9 +382,11 @@ private async seedWeightLimits(wlRepo: any, ctRepo: any): Promise { this.logger.log("Seeded weight limit rules"); } 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). + // Priority rule = Wagon Block + Currency Block (both additive; see RuleEngineService.evaluate). + // Combined with the service-type bonus the total priority score caps at 100: + // service-type bonus (≤ 15) + wagon block (≤ 50) + currency block (≤ 35) = 100. + // Wagon Count Block — independent, applies regardless of currency. Max 50. + // Currency Block — applies only to the matching payment currency, within the wagon range. Max 35. const rows = [ // ── Wagon Count Block ─────────────────────────────────────────────── { type: "WAGON", label: "Wagons 1–20", currency: null, minWagonCount: 1, maxWagonCount: 20, scorePoints: 0, displayOrder: 1 }, @@ -392,7 +394,7 @@ private async seedWeightLimits(wlRepo: any, ctRepo: any): Promise { { 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 1–25", currency: "USD", minWagonCount: 1, maxWagonCount: 25, scorePoints: 15, 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 }, ]; diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingPriorityBadge.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingPriorityBadge.tsx index 5a17e4568..460e9b334 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingPriorityBadge.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingPriorityBadge.tsx @@ -1,14 +1,14 @@ import { Badge } from "@mantine/core"; export function BookingPriorityBadge({ score }: { score: number }) { - if (score >= 1000) { + if (score >= 70) { return ( Urgent ); } - if (score >= 500) { + if (score >= 40) { return ( High