mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
refactor booking priority logic and constants; adjust scoring thresholds and remove unused service types
This commit is contained in:
@@ -44,7 +44,6 @@ describe('BookingPricingService — domestic corridor', () => {
|
|||||||
{} as never,
|
{} as never,
|
||||||
{} as never,
|
{} as never,
|
||||||
ratesService as never,
|
ratesService as never,
|
||||||
{} as never,
|
|
||||||
exchangeService as never,
|
exchangeService as never,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
|||||||
|
|
||||||
import { ContainerTypesService } from '../rule-engine/services/container-types.service';
|
import { ContainerTypesService } from '../rule-engine/services/container-types.service';
|
||||||
import { RatesService } from '../rule-engine/services/rates.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 { Rate } from '../rule-engine/entities/rate.entity';
|
||||||
import { ExchangeService } from '@edr/api-common';
|
import { ExchangeService } from '@edr/api-common';
|
||||||
import {
|
import {
|
||||||
@@ -40,7 +39,6 @@ export class BookingPricingService {
|
|||||||
private readonly ruleEngineService: RuleEngineService,
|
private readonly ruleEngineService: RuleEngineService,
|
||||||
private readonly containerTypesService: ContainerTypesService,
|
private readonly containerTypesService: ContainerTypesService,
|
||||||
private readonly ratesService: RatesService,
|
private readonly ratesService: RatesService,
|
||||||
private readonly serviceTypesService: ServiceTypesService,
|
|
||||||
private readonly exchangeService: ExchangeService,
|
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<number> {
|
async computeSubmitPriorityScore(booking: Booking): Promise<number> {
|
||||||
const evalInput = await this.buildEvalInputForBooking(booking);
|
const evalInput = await this.buildEvalInputForBooking(booking);
|
||||||
const ruleResult = await this.ruleEngineService.evaluate(evalInput);
|
const ruleResult = await this.ruleEngineService.evaluate(evalInput);
|
||||||
let score = ruleResult.priorityScore;
|
return 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async computeBaseRailLinesWithRates(
|
private async computeBaseRailLinesWithRates(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const OVERVIEW_URGENT_PRIORITY_THRESHOLD = 1000;
|
export const OVERVIEW_URGENT_PRIORITY_THRESHOLD = 70;
|
||||||
|
|
||||||
export const OVERVIEW_NEEDS_ACTION_STATUSES = [
|
export const OVERVIEW_NEEDS_ACTION_STATUSES = [
|
||||||
'SUBMITTED',
|
'SUBMITTED',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
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 {
|
export class CreatePriorityConfigDto {
|
||||||
@ApiProperty({ description: 'Config type: WAGON or CURRENCY', enum: ['WAGON', 'CURRENCY'] })
|
@ApiProperty({ description: 'Config type: WAGON or CURRENCY', enum: ['WAGON', 'CURRENCY'] })
|
||||||
@@ -30,9 +30,15 @@ export class CreatePriorityConfigDto {
|
|||||||
@Min(0)
|
@Min(0)
|
||||||
maxWagonCount!: number;
|
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()
|
@IsInt()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
|
@Max(50)
|
||||||
scorePoints!: number;
|
scorePoints!: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ default: false, description: 'Feature flag — toggle without code deploy' })
|
@ApiPropertyOptional({ default: false, description: 'Feature flag — toggle without code deploy' })
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
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 {
|
export class CreateServiceTypeDto {
|
||||||
@ApiProperty({ description: 'Service type display name', maxLength: 255 })
|
@ApiProperty({ description: 'Service type display name', maxLength: 255 })
|
||||||
@@ -32,10 +32,15 @@ export class CreateServiceTypeDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
includesCustoms?: boolean;
|
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()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
|
@Max(15)
|
||||||
priorityBonusPoints?: number;
|
priorityBonusPoints?: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ default: true })
|
@ApiPropertyOptional({ default: true })
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export class PricingDataSeeder {
|
|||||||
includesFirstMile: true,
|
includesFirstMile: true,
|
||||||
includesLastMile: true,
|
includesLastMile: true,
|
||||||
includesCustoms: true,
|
includesCustoms: true,
|
||||||
priorityBonusPoints: 100,
|
priorityBonusPoints: 15,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
displayOrder: 2,
|
displayOrder: 2,
|
||||||
},
|
},
|
||||||
@@ -192,7 +192,7 @@ export class PricingDataSeeder {
|
|||||||
includesFirstMile: false,
|
includesFirstMile: false,
|
||||||
includesLastMile: false,
|
includesLastMile: false,
|
||||||
includesCustoms: false,
|
includesCustoms: false,
|
||||||
priorityBonusPoints: 50,
|
priorityBonusPoints: 10,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
displayOrder: 3,
|
displayOrder: 3,
|
||||||
},
|
},
|
||||||
@@ -382,9 +382,11 @@ private async seedWeightLimits(wlRepo: any, ctRepo: any): Promise<void> {
|
|||||||
this.logger.log("Seeded weight limit rules");
|
this.logger.log("Seeded weight limit rules");
|
||||||
}
|
}
|
||||||
private async seedPriorityConfigs(prRepo: any): Promise<void> {
|
private async seedPriorityConfigs(prRepo: any): Promise<void> {
|
||||||
// Wagon Count Block — independent, applies regardless of currency.
|
// Priority rule = Wagon Block + Currency Block (both additive; see RuleEngineService.evaluate).
|
||||||
// Currency Block — applies only to the matching payment currency, within the wagon range.
|
// Combined with the service-type bonus the total priority score caps at 100:
|
||||||
// Both blocks are additive (see RuleEngineService.evaluate).
|
// 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 = [
|
const rows = [
|
||||||
// ── Wagon Count Block ───────────────────────────────────────────────
|
// ── Wagon Count Block ───────────────────────────────────────────────
|
||||||
{ type: "WAGON", label: "Wagons 1–20", currency: null, minWagonCount: 1, maxWagonCount: 20, scorePoints: 0, displayOrder: 1 },
|
{ 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<void> {
|
|||||||
{ type: "WAGON", label: "Wagons 31–40", currency: null, minWagonCount: 31, maxWagonCount: 40, scorePoints: 30, displayOrder: 3 },
|
{ 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 },
|
{ type: "WAGON", label: "Wagons 41–50", currency: null, minWagonCount: 41, maxWagonCount: 50, scorePoints: 50, displayOrder: 4 },
|
||||||
// ── Payment Currency Block ──────────────────────────────────────────
|
// ── 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: "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 },
|
{ type: "CURRENCY", label: "ETB · Wagons 1–50", currency: "ETB", minWagonCount: 1, maxWagonCount: 50, scorePoints: 0, displayOrder: 7 },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Badge } from "@mantine/core";
|
import { Badge } from "@mantine/core";
|
||||||
|
|
||||||
export function BookingPriorityBadge({ score }: { score: number }) {
|
export function BookingPriorityBadge({ score }: { score: number }) {
|
||||||
if (score >= 1000) {
|
if (score >= 70) {
|
||||||
return (
|
return (
|
||||||
<Badge color="red" variant="filled" size="sm" radius="lg" tt="uppercase">
|
<Badge color="red" variant="filled" size="sm" radius="lg" tt="uppercase">
|
||||||
Urgent
|
Urgent
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (score >= 500) {
|
if (score >= 40) {
|
||||||
return (
|
return (
|
||||||
<Badge color="yellow" variant="filled" size="sm" radius="lg" tt="uppercase">
|
<Badge color="yellow" variant="filled" size="sm" radius="lg" tt="uppercase">
|
||||||
High
|
High
|
||||||
|
|||||||
Reference in New Issue
Block a user