integrate global logistics staff user into seeder

refactor pricing data seeder to fold surcharge types into rates
update route meta subtitle to remove surcharge types
enhance RuleEngineFormDialog to support conditional field visibility
 remove surcharge types from URL constants and related services
add cargo leaf options query for bulk cargo type selection
update RuleEngineResourcePage to utilize cargo leaf options
modify resources configuration to remove surcharge types
implement migration to fold surcharge types into rates
create utility to derive legacy rate types from new rate structure
This commit is contained in:
Marshal
2026-06-23 23:15:32 +00:00
parent 2b4dfc6490
commit 9d81a2e1ee
30 changed files with 642 additions and 632 deletions

View File

@@ -1,8 +1,8 @@
import { Inject, Injectable, Logger, BadRequestException } from '@nestjs/common';
import { Inject, Injectable, BadRequestException } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { BookingApprovalStep } from '../bookings/entities/booking-approval-step.entity';
import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.entity';
import { TriggerCondition } from './entities/surcharge-type.entity';
import { Rate, RateTrigger } from './entities/rate.entity';
import {
ICargoTypesRepository,
CARGO_TYPES_REPOSITORY,
@@ -19,10 +19,6 @@ import {
IPriorityConfigsRepository,
PRIORITY_CONFIGS_REPOSITORY,
} from './interfaces/priority-configs.repository.interface';
import {
ISurchargeTypesRepository,
SURCHARGE_TYPES_REPOSITORY,
} from './interfaces/surcharge-types.repository.interface';
import {
IRatesRepository,
RATES_REPOSITORY,
@@ -63,11 +59,12 @@ export interface BookingEvaluationInput {
}
export interface AppliedCargoModifier {
surchargeTypeId: string;
surchargeTypeCode: string;
/** The trigger-based rate that produced this surcharge line. */
rateId: string;
/** Stable display/audit code, derived from the rate's trigger + rateType. */
surchargeCode: string;
triggerValue: number | null;
calculatedAmount: number;
rateId: string;
currency: string;
}
@@ -89,8 +86,6 @@ export interface RuleEvaluationResult {
@Injectable()
export class RuleEngineService {
private readonly logger = new Logger(RuleEngineService.name);
constructor(
@Inject(CARGO_TYPES_REPOSITORY)
private readonly cargoTypesRepo: ICargoTypesRepository,
@@ -100,8 +95,6 @@ export class RuleEngineService {
private readonly weightLimitRulesRepo: IWeightLimitRulesRepository,
@Inject(PRIORITY_CONFIGS_REPOSITORY)
private readonly priorityConfigsRepo: IPriorityConfigsRepository,
@Inject(SURCHARGE_TYPES_REPOSITORY)
private readonly surchargeTypesRepo: ISurchargeTypesRepository,
@Inject(RATES_REPOSITORY)
private readonly ratesRepo: IRatesRepository,
@Inject(APPROVAL_RULES_REPOSITORY)
@@ -205,20 +198,14 @@ export class RuleEngineService {
const hasReefer = input.containers.some((c) => c.isReefer);
const hasOverweight = containerWeightResults.some((r) => r.isOverweight);
const surchargeTypes = await this.surchargeTypesRepo.findAllActiveWithRate();
// Surcharges are now self-describing rates: any LIVE rate whose `trigger`
// is not ALWAYS. Each fires independently and stacks on top of base freight
// — hazard + reefer + overweight all add together, each with its own unit.
const liveRates = await this.ratesRepo.findLiveRates();
const rateById = new Map(liveRates.map((r) => [r.id, r]));
const surchargeRates = liveRates.filter((r) => r.trigger && r.trigger !== 'ALWAYS');
// TEMP diagnostic — trace the surcharge trigger state so we can confirm
// whether a "Hazardous" line is firing for a non-hazardous booking.
this.logger.debug(
`surcharge eval: isHazardous=${input.isHazardous} (type ${typeof input.isHazardous}) ` +
`hasReefer=${hasReefer} hasOverweight=${hasOverweight} ` +
`shippingLineMapped=${shippingLineMapped}`,
);
for (const st of surchargeTypes) {
const triggered = this.matchesTrigger(st.triggerCondition, {
for (const rate of surchargeRates) {
const triggered = this.matchesTrigger(rate.trigger, {
isHazardous: input.isHazardous,
hasReefer,
hasOverweight,
@@ -227,20 +214,16 @@ export class RuleEngineService {
});
if (!triggered) continue;
const rate = st.rate ?? rateById.get(st.rateId);
if (!rate) continue;
let triggerValue: number | null = null;
let calculatedAmount = Number(rate.rateValue);
if (st.triggerCondition === 'VGM_EXCEEDS_LIMIT') {
// Per-ton surcharges (typically OVERWEIGHT) bill against the excess tons.
if (rate.rateUnit === 'PER_TON' && rate.trigger === 'OVERWEIGHT') {
triggerValue = containerWeightResults.reduce(
(sum, r) => sum + (r.overweightExcessTons ?? 0),
0,
);
if (rate.rateUnit === 'PER_TON') {
calculatedAmount = triggerValue * Number(rate.rateValue);
}
calculatedAmount = triggerValue * Number(rate.rateValue);
}
// Safety guard: never include a surcharge with a non-positive amount (a
@@ -249,11 +232,10 @@ export class RuleEngineService {
if (!(calculatedAmount > 0)) continue;
appliedModifiers.push({
surchargeTypeId: st.id,
surchargeTypeCode: st.code,
rateId: rate.id,
surchargeCode: this.surchargeCode(rate),
triggerValue,
calculatedAmount,
rateId: rate.id,
currency: rate.currency,
});
}
@@ -388,7 +370,7 @@ export class RuleEngineService {
}
private matchesTrigger(
condition: TriggerCondition,
trigger: RateTrigger,
state: {
isHazardous: boolean;
hasReefer: boolean;
@@ -400,19 +382,26 @@ export class RuleEngineService {
// Coerce defensively: a flag may arrive as the string "true"/"false" (e.g.
// from multipart form-data) and a non-empty "false" string is truthy.
const truthy = (v: unknown): boolean => v === true || v === 'true';
switch (condition) {
case 'CARGO_FLAG_HAZARDOUS':
switch (trigger) {
case 'HAZARDOUS':
return truthy(state.isHazardous);
case 'CARGO_FLAG_REEFER':
case 'REEFER':
return truthy(state.hasReefer);
case 'VGM_EXCEEDS_LIMIT':
case 'OVERWEIGHT':
return truthy(state.hasOverweight);
case 'SHIPPING_LINE_MAPPED':
case 'SHIPPING_LINE':
return truthy(state.shippingLineMapped);
case 'CONSOLIDATION_ENABLED':
case 'CONSOLIDATION':
return truthy(state.allowConsolidation);
// CANCELLATION / DEMURRAGE / PIL_EXTRA_FEE are contextual charges applied
// explicitly elsewhere (not auto-triggered by a booking's cargo flags).
default:
return false;
}
}
/** Stable surcharge code for display + audit, derived from the rate. */
private surchargeCode(rate: Rate): string {
return rate.rateType ?? rate.trigger;
}
}