mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 11:21:18 +00:00
Merge pull request #799 from Tria-plc/freight_feature/usermanagement
add per-container handling options for hazardous, reefer, and return…
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { deriveRateType } from './rate-type.util';
|
||||
|
||||
describe('deriveRateType — surcharge triggers', () => {
|
||||
// Every surcharge trigger must land on its own rateType. A trigger with no
|
||||
// mapping falls through to the base-freight branch and is silently stored as
|
||||
// CANCELLATION_FEE, which both mislabels the booking's rate snapshot and
|
||||
// hides the rate from contract pricing (which looks rateTypes up by name).
|
||||
it.each([
|
||||
['HAZARDOUS', 'HAZARD_SURCHARGE'],
|
||||
['REEFER', 'REEFER_SURCHARGE'],
|
||||
['WITH_RETURN', 'RETURN_SURCHARGE'],
|
||||
['OVERWEIGHT', 'OVERWEIGHT_PER_TON'],
|
||||
['SHIPPING_LINE', 'DOUBLE_HANDLING'],
|
||||
['CONSOLIDATION', 'LASHING'],
|
||||
['CANCELLATION', 'CANCELLATION_FEE'],
|
||||
['DEMURRAGE', 'DEMURRAGE'],
|
||||
['PIL_EXTRA_FEE', 'PIL_EXTRA_FEE'],
|
||||
['CUSTOMS_CLEARANCE', 'CUSTOMS_CLEARANCE'],
|
||||
] as const)('maps trigger %s to %s', (trigger, expected) => {
|
||||
expect(deriveRateType({ appliesTo: 'OTHER', trigger })).toBe(expected);
|
||||
});
|
||||
|
||||
it('does not fall back to CANCELLATION_FEE for the empty-return service', () => {
|
||||
expect(deriveRateType({ appliesTo: 'OTHER', trigger: 'WITH_RETURN' })).not.toBe(
|
||||
'CANCELLATION_FEE',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,12 @@ export function deriveRateType(input: {
|
||||
return 'HAZARD_SURCHARGE';
|
||||
case 'REEFER':
|
||||
return 'REEFER_SURCHARGE';
|
||||
// Empty-container return service. Contract pricing looks this rateType up
|
||||
// by name, so without the mapping a WITH_RETURN rate fell through to the
|
||||
// base-freight branch and was stored as CANCELLATION_FEE — invisible to
|
||||
// the contract, and mislabelled on the booking's snapshot.
|
||||
case 'WITH_RETURN':
|
||||
return 'RETURN_SURCHARGE';
|
||||
case 'OVERWEIGHT':
|
||||
return 'OVERWEIGHT_PER_TON';
|
||||
case 'SHIPPING_LINE':
|
||||
|
||||
@@ -42,6 +42,14 @@ export interface BookingContainerEvalInput {
|
||||
isReefer?: boolean;
|
||||
isOverweight?: boolean;
|
||||
overweightExcessTons?: number | null;
|
||||
/**
|
||||
* How many individual containers on this line opted into each handling
|
||||
* service. PER_CONTAINER surcharges bill these counts, not the line
|
||||
* quantity — 20 containers with 10 hazardous bill hazard on 10.
|
||||
*/
|
||||
hazardousQuantity?: number;
|
||||
reeferQuantity?: number;
|
||||
returnQuantity?: number;
|
||||
}
|
||||
|
||||
export interface BookingEvaluationInput {
|
||||
@@ -270,6 +278,27 @@ export class RuleEngineService {
|
||||
(sum, r) => sum + (r.overweightExcessTons ?? 0),
|
||||
0,
|
||||
);
|
||||
/**
|
||||
* Containers that opted into this trigger's handling service, summed
|
||||
* across lines. null when the trigger isn't per-container handling (or
|
||||
* no line carries a count) so the caller falls back to the full count.
|
||||
*/
|
||||
const optedInCount = (trigger: string | null): number | null => {
|
||||
const field =
|
||||
trigger === 'HAZARDOUS'
|
||||
? 'hazardousQuantity'
|
||||
: trigger === 'REEFER'
|
||||
? 'reeferQuantity'
|
||||
: trigger === 'WITH_RETURN'
|
||||
? 'returnQuantity'
|
||||
: null;
|
||||
if (!field) return null;
|
||||
const total = input.containers.reduce(
|
||||
(sum, c) => sum + Number(c[field] ?? 0),
|
||||
0,
|
||||
);
|
||||
return total > 0 ? total : null;
|
||||
};
|
||||
|
||||
let triggerValue: number | null = null;
|
||||
let calculatedAmount: number;
|
||||
@@ -285,7 +314,11 @@ export class RuleEngineService {
|
||||
calculatedAmount = triggerValue * rateValue;
|
||||
break;
|
||||
case 'PER_CONTAINER':
|
||||
triggerValue = containerCount;
|
||||
// Handling surcharges bill only the containers that opted in, not the
|
||||
// whole line — 20 containers with 10 hazardous bill hazard on 10.
|
||||
// Legacy bookings carry no per-container counts (all 0) while their
|
||||
// booking-level flag is set, so fall back to the full count there.
|
||||
triggerValue = optedInCount(rate.trigger) ?? containerCount;
|
||||
calculatedAmount = triggerValue * rateValue;
|
||||
break;
|
||||
case 'PER_WAGON':
|
||||
|
||||
Reference in New Issue
Block a user