fix issues

This commit is contained in:
Marshal
2026-07-03 13:40:10 +00:00
parent e14df58a47
commit 52f48b688d
11 changed files with 343 additions and 59 deletions

View File

@@ -0,0 +1,71 @@
import type { RateAppliesTo, RateTrigger, RateUnit } from './rate.entity';
/**
* Which rate units make sense for a given rate shape. The weighting basis is
* driven by the *type* of thing being billed — a container leg bills per
* container, bulk freight per ton, an intercity move can be per-km, a
* cancellation is a flat/per-invoice fee, and overweight is always per excess
* ton. This keeps the rate table dynamic yet non-conflicting: the admin can
* only pick a unit the pricing engine knows how to apply.
*
* Returned lists are ordered with the most natural/default unit first.
*/
export function allowedRateUnits(input: {
appliesTo: RateAppliesTo;
trigger: RateTrigger;
}): RateUnit[] {
const { appliesTo, trigger } = input;
// Surcharges (Applies to = Other) are governed by their trigger.
if (appliesTo === 'OTHER') {
switch (trigger) {
case 'OVERWEIGHT':
// Overweight always bills the excess tonnage — per ton, nothing else.
return ['PER_TON'];
case 'REEFER':
case 'HAZARDOUS':
// Scale with the freight shape: per container for boxes, per ton for bulk.
return ['PER_CONTAINER', 'PER_TON'];
case 'DEMURRAGE':
return ['PER_CONTAINER', 'PER_TON'];
case 'CANCELLATION':
return ['FLAT', 'PER_INVOICE'];
case 'CONSOLIDATION':
return ['PER_CONTAINER', 'FLAT'];
case 'SHIPPING_LINE':
case 'PIL_EXTRA_FEE':
return ['PER_CONTAINER', 'FLAT'];
default:
return ['FLAT', 'PER_TON', 'PER_CONTAINER'];
}
}
// Base freight + first/last mile scale with the cargo type.
switch (appliesTo) {
case 'CONTAINER':
return ['PER_CONTAINER', 'PER_WAGON'];
case 'BULK':
return ['PER_TON', 'PER_WAGON'];
case 'INTERCITY':
return ['PER_CONTAINER', 'PER_TON', 'PER_WAGON', 'PER_KM'];
case 'FIRST_MILE':
case 'LAST_MILE':
return ['PER_CONTAINER', 'PER_TON', 'PER_KM', 'FLAT'];
default:
return ['FLAT'];
}
}
/** The default (first / most natural) unit for a rate shape. */
export function defaultRateUnit(input: { appliesTo: RateAppliesTo; trigger: RateTrigger }): RateUnit {
return allowedRateUnits(input)[0];
}
/** True when `unit` is a valid weighting basis for the given rate shape. */
export function isRateUnitAllowed(input: {
appliesTo: RateAppliesTo;
trigger: RateTrigger;
unit: RateUnit;
}): boolean {
return allowedRateUnits(input).includes(input.unit);
}

View File

@@ -81,7 +81,6 @@ export type RateTrigger = typeof RATE_TRIGGERS[number];
@Entity({ schema: 'freight', name: 'rates' })
@Index(['rateType'])
@Index(['status'])
@Index(['effectiveFrom'])
@Index(['containerTypeId'])
@Index(['trigger'])
export class Rate extends BaseEntity {
@@ -131,10 +130,4 @@ export class Rate extends BaseEntity {
@Column({ name: 'approved_at', type: 'timestamptz', nullable: true })
approvedAt?: Date | null;
@Column({ name: 'effective_from', type: 'date' })
effectiveFrom!: Date;
@Column({ name: 'effective_to', type: 'date', nullable: true })
effectiveTo?: Date | null;
}

View File

@@ -5,7 +5,6 @@ import { ContainerType } from './container-type.entity';
@Entity({ schema: 'freight', name: 'weight_limit_rules' })
@Index(['containerTypeId'])
@Index(['tradeDirection'])
@Index(['effectiveFrom'])
export class WeightLimitRule extends BaseEntity {
@Column({ name: 'container_type_id', type: 'uuid' })
containerTypeId!: string;
@@ -19,10 +18,4 @@ export class WeightLimitRule extends BaseEntity {
@Column({ name: 'max_vgm_tons', type: 'numeric', precision: 8, scale: 3, nullable: true })
maxVgmTons!: number;
@Column({ name: 'effective_from', type: 'date', nullable: true })
effectiveFrom!: Date;
@Column({ name: 'effective_to', type: 'date', nullable: true })
effectiveTo?: Date | null;
}