feat: implement staff price adjustment feature for bookings

This commit is contained in:
Marshal
2026-06-23 22:54:32 +00:00
parent 196e296275
commit 2b4dfc6490
13 changed files with 364 additions and 89 deletions

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable, BadRequestException } from '@nestjs/common';
import { Inject, Injectable, Logger, 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';
@@ -89,6 +89,8 @@ export interface RuleEvaluationResult {
@Injectable()
export class RuleEngineService {
private readonly logger = new Logger(RuleEngineService.name);
constructor(
@Inject(CARGO_TYPES_REPOSITORY)
private readonly cargoTypesRepo: ICargoTypesRepository,
@@ -207,6 +209,14 @@ export class RuleEngineService {
const liveRates = await this.ratesRepo.findLiveRates();
const rateById = new Map(liveRates.map((r) => [r.id, r]));
// 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, {
isHazardous: input.isHazardous,
@@ -233,6 +243,11 @@ export class RuleEngineService {
}
}
// Safety guard: never include a surcharge with a non-positive amount (a
// zero-rate or zero-trigger line would otherwise show as a confusing
// "free" surcharge on the breakdown).
if (!(calculatedAmount > 0)) continue;
appliedModifiers.push({
surchargeTypeId: st.id,
surchargeTypeCode: st.code,
@@ -382,17 +397,20 @@ export class RuleEngineService {
allowConsolidation: boolean;
},
): boolean {
// 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':
return state.isHazardous;
return truthy(state.isHazardous);
case 'CARGO_FLAG_REEFER':
return state.hasReefer;
return truthy(state.hasReefer);
case 'VGM_EXCEEDS_LIMIT':
return state.hasOverweight;
return truthy(state.hasOverweight);
case 'SHIPPING_LINE_MAPPED':
return state.shippingLineMapped;
return truthy(state.shippingLineMapped);
case 'CONSOLIDATION_ENABLED':
return state.allowConsolidation;
return truthy(state.allowConsolidation);
default:
return false;
}