feat: Implement container hazardous-cargo surcharge logic

- Added support for container hazardous-cargo surcharge (HAZARDOUS billed PER_CONTAINER) in the rule engine.
- Introduced new method  in  to calculate and apply container hazard charges based on booking details.
- Updated  to handle per-container hazard rates, ensuring they are scoped by trade direction and lane.
- Enhanced tests to cover scenarios for container hazard rates, including validation for required fields and conflict checks.
- Created a migration to update existing rates and enforce new constraints for container hazard rates in the database.
This commit is contained in:
marshal
2026-09-06 22:28:57 +00:00
parent 1eb9f10354
commit 00c0d86a8a
11 changed files with 789 additions and 98 deletions

View File

@@ -88,6 +88,10 @@ export type RateAppliesTo = typeof RATE_APPLIES_TO[number];
*/
export const RATE_TRIGGERS = [
'ALWAYS',
// Hazardous cargo. Two shapes under one trigger, told apart by the unit:
// PER_CONTAINER is the container surcharge, sold per direction + lane and
// optionally per box size (20ft / 40ft) like the empty-return service;
// PER_TON is the bulk surcharge, direction-agnostic and unscoped.
'HAZARDOUS',
'OVERWEIGHT',
'REEFER',
@@ -121,6 +125,16 @@ export type RateTrigger = typeof RATE_TRIGGERS[number];
export const isCustomsClearanceTrigger = (trigger: string): boolean =>
trigger === 'CUSTOMS_CLEARANCE' || trigger === 'ETHIOPIAN_CUSTOMS_CLEARANCE';
/**
* The container hazardous-cargo surcharge: HAZARDOUS billed per container.
* It is sold per trade direction + origin → destination lane, optionally
* narrowed to one container type (20ft / 40ft), and priced by the
* route-matched block in RuleEngineService — never by the additive loop.
* The per-ton (bulk) hazard rate keeps the old global, unscoped shape.
*/
export const isContainerHazardRate = (trigger: string, rateUnit: string): boolean =>
trigger === 'HAZARDOUS' && rateUnit === 'PER_CONTAINER';
@Entity({ schema: 'freight', name: 'rates' })
@Index(['rateType'])
@Index(['status'])
@@ -159,8 +173,10 @@ export class Rate extends BaseEntity {
/**
* The leg this rate prices. Base freight (trigger = ALWAYS) is quoted per
* route — "container import, Djibouti → Dire Dawa" — so both yards are
* required for BULK/CONTAINER/INTERCITY and NULL for everything else. The
* `CK_rates_yard_scope` DB constraint enforces both halves of that.
* required for BULK/CONTAINER/EMPTY_CONTAINER/INTERCITY, for the lane-sold
* surcharges (customs clearance, empty return, fuel, container hazard) and
* NULL for everything else. The `CK_rates_yard_scope` DB constraint enforces
* both halves of that.
*/
@Column({ name: 'origin_yard_id', type: 'uuid', nullable: true })
originYardId?: string | null;