mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
booking cancellation
This commit is contained in:
@@ -25,6 +25,19 @@ import { IYardsRepository, YARDS_REPOSITORY } from '../interfaces/yards.reposito
|
||||
|
||||
/** Categories priced per rail leg — they carry an origin → destination yard pair. */
|
||||
const BASE_FREIGHT_CATEGORIES: readonly Rate['appliesTo'][] = ['BULK', 'CONTAINER', 'INTERCITY'];
|
||||
/**
|
||||
* Surcharges sold per cargo kind: the admin says container or bulk, a
|
||||
* container fee then names its container type and a bulk fee its commodity.
|
||||
*/
|
||||
const CARGO_KIND_TRIGGERS: readonly Rate['trigger'][] = ['CUSTOMS_CLEARANCE', 'CANCELLATION'];
|
||||
/** Surcharges that keep a trade direction (everything else is direction-agnostic). */
|
||||
const DIRECTED_SURCHARGE_TRIGGERS: readonly Rate['trigger'][] = [
|
||||
'CUSTOMS_CLEARANCE',
|
||||
'CANCELLATION',
|
||||
'WITH_RETURN',
|
||||
'LASHING',
|
||||
'FUEL',
|
||||
];
|
||||
|
||||
/** The yard pair a rate scopes to, already validated against its direction. */
|
||||
interface YardScope {
|
||||
@@ -152,7 +165,11 @@ export class RatesService {
|
||||
appliesTo: Rate['appliesTo'],
|
||||
trigger: Rate['trigger'],
|
||||
): boolean {
|
||||
return this.isRouteScoped(appliesTo, trigger) || trigger === 'LASHING';
|
||||
return (
|
||||
this.isRouteScoped(appliesTo, trigger) ||
|
||||
trigger === 'LASHING' ||
|
||||
trigger === 'CANCELLATION'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,10 +261,13 @@ export class RatesService {
|
||||
}): void {
|
||||
const { appliesTo, trigger, tradeDirection, intercityKind, cargoKind } = input;
|
||||
const { containerTypeId, cargoTypeId } = input;
|
||||
if (trigger === 'CUSTOMS_CLEARANCE') {
|
||||
if (trigger === 'CUSTOMS_CLEARANCE' || trigger === 'CANCELLATION') {
|
||||
// Both fees are sold per direction + cargo kind + type: customs clearance
|
||||
// per lane, the wagon cancellation fee per direction only.
|
||||
const fee = trigger === 'CANCELLATION' ? 'cancellation fee' : 'customs clearance';
|
||||
if (tradeDirection !== 'IMPORT' && tradeDirection !== 'EXPORT') {
|
||||
throw new BadRequestException(
|
||||
'A customs clearance rate must say whether it covers IMPORT or EXPORT.',
|
||||
`A ${fee} rate must say whether it covers IMPORT or EXPORT.`,
|
||||
);
|
||||
}
|
||||
// Sold per cargo kind: a container fee names the container type it covers
|
||||
@@ -255,29 +275,29 @@ export class RatesService {
|
||||
// that absence is what marks it as the bulk fee.
|
||||
if (cargoKind !== 'CONTAINER' && cargoKind !== 'BULK') {
|
||||
throw new BadRequestException(
|
||||
'A customs clearance rate must say whether it covers containers or bulk.',
|
||||
`A ${fee} rate must say whether it covers containers or bulk.`,
|
||||
);
|
||||
}
|
||||
if (cargoKind === 'CONTAINER' && !containerTypeId) {
|
||||
throw new BadRequestException(
|
||||
'A container customs clearance rate must name the container type it covers.',
|
||||
`A container ${fee} rate must name the container type it covers.`,
|
||||
);
|
||||
}
|
||||
if (cargoKind === 'BULK' && containerTypeId) {
|
||||
throw new BadRequestException(
|
||||
'A bulk customs clearance rate cannot be scoped to a container type.',
|
||||
`A bulk ${fee} rate cannot be scoped to a container type.`,
|
||||
);
|
||||
}
|
||||
// The bulk customs fee names the commodity it covers (sugar and
|
||||
// fertilizer clear differently).
|
||||
// The bulk fee names the commodity it covers (sugar and fertilizer
|
||||
// clear — and cancel — differently).
|
||||
if (cargoKind === 'BULK' && !cargoTypeId) {
|
||||
throw new BadRequestException(
|
||||
'A bulk customs clearance rate must name the bulk cargo type it covers.',
|
||||
`A bulk ${fee} rate must name the bulk cargo type it covers.`,
|
||||
);
|
||||
}
|
||||
if (cargoKind === 'CONTAINER' && cargoTypeId) {
|
||||
throw new BadRequestException(
|
||||
'A container customs clearance rate cannot be scoped to a bulk cargo type.',
|
||||
`A container ${fee} rate cannot be scoped to a bulk cargo type.`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -547,22 +567,21 @@ export class RatesService {
|
||||
const trigger = dto.trigger as Rate['trigger'];
|
||||
// Surcharges (trigger ≠ ALWAYS) carry no direction/scope — clear them so
|
||||
// the engine never accidentally narrows a surcharge by container/direction.
|
||||
// Exceptions: customs clearance and empty-container return keep direction +
|
||||
// container type — both are sold per lane (and per container type).
|
||||
// Exceptions: the directed surcharges (customs clearance, cancellation,
|
||||
// empty-container return, lashing, fuel) keep direction + cargo scope.
|
||||
const isSurcharge = trigger !== 'ALWAYS';
|
||||
const cargoKind =
|
||||
trigger === 'CUSTOMS_CLEARANCE'
|
||||
? ((dto.cargoKind as 'CONTAINER' | 'BULK' | undefined) ?? null)
|
||||
: null;
|
||||
const cargoKind = CARGO_KIND_TRIGGERS.includes(trigger)
|
||||
? ((dto.cargoKind as 'CONTAINER' | 'BULK' | undefined) ?? null)
|
||||
: null;
|
||||
const containerTypeId =
|
||||
trigger === 'WITH_RETURN' ||
|
||||
(trigger === 'CUSTOMS_CLEARANCE' && cargoKind === 'CONTAINER')
|
||||
(CARGO_KIND_TRIGGERS.includes(trigger) && cargoKind === 'CONTAINER')
|
||||
? (dto.containerTypeId ?? null)
|
||||
: isSurcharge
|
||||
? null
|
||||
: (dto.containerTypeId ?? null);
|
||||
const cargoTypeId =
|
||||
(trigger === 'CUSTOMS_CLEARANCE' && cargoKind === 'BULK') ||
|
||||
(CARGO_KIND_TRIGGERS.includes(trigger) && cargoKind === 'BULK') ||
|
||||
trigger === 'LASHING' ||
|
||||
trigger === 'FUEL'
|
||||
? (dto.cargoTypeId ?? null)
|
||||
@@ -574,10 +593,7 @@ export class RatesService {
|
||||
// intercity lane is stored as DOMESTIC, since appliesTo = OTHER says
|
||||
// nothing about the direction.)
|
||||
const tradeDirection =
|
||||
trigger === 'CUSTOMS_CLEARANCE' ||
|
||||
trigger === 'WITH_RETURN' ||
|
||||
trigger === 'LASHING' ||
|
||||
trigger === 'FUEL'
|
||||
DIRECTED_SURCHARGE_TRIGGERS.includes(trigger)
|
||||
? (dto.tradeDirection ?? null)
|
||||
: isSurcharge || appliesTo === 'INTERCITY'
|
||||
? null
|
||||
@@ -758,16 +774,15 @@ export class RatesService {
|
||||
|
||||
// A patch that leaves the cargo kind unsaid keeps the one the rate already
|
||||
// has — read back off its container scope (container fees carry the type).
|
||||
const cargoKind =
|
||||
trigger !== 'CUSTOMS_CLEARANCE'
|
||||
? null
|
||||
: ((dto.cargoKind as 'CONTAINER' | 'BULK' | undefined) ??
|
||||
(existing.containerTypeId ? 'CONTAINER' : 'BULK'));
|
||||
const cargoKind = !CARGO_KIND_TRIGGERS.includes(trigger)
|
||||
? null
|
||||
: ((dto.cargoKind as 'CONTAINER' | 'BULK' | undefined) ??
|
||||
(existing.containerTypeId ? 'CONTAINER' : 'BULK'));
|
||||
|
||||
const keepsContainerType =
|
||||
!isSurcharge ||
|
||||
trigger === 'WITH_RETURN' ||
|
||||
(trigger === 'CUSTOMS_CLEARANCE' && cargoKind === 'CONTAINER');
|
||||
(CARGO_KIND_TRIGGERS.includes(trigger) && cargoKind === 'CONTAINER');
|
||||
const containerTypeId = !keepsContainerType
|
||||
? null
|
||||
: dto.containerTypeId !== undefined
|
||||
@@ -775,7 +790,7 @@ export class RatesService {
|
||||
: existing.containerTypeId;
|
||||
const keepsCargoType =
|
||||
!isSurcharge ||
|
||||
(trigger === 'CUSTOMS_CLEARANCE' && cargoKind === 'BULK') ||
|
||||
(CARGO_KIND_TRIGGERS.includes(trigger) && cargoKind === 'BULK') ||
|
||||
trigger === 'LASHING' ||
|
||||
trigger === 'FUEL';
|
||||
const cargoTypeId = !keepsCargoType
|
||||
@@ -784,10 +799,7 @@ export class RatesService {
|
||||
? dto.cargoTypeId
|
||||
: existing.cargoTypeId;
|
||||
const tradeDirection =
|
||||
trigger === 'CUSTOMS_CLEARANCE' ||
|
||||
trigger === 'WITH_RETURN' ||
|
||||
trigger === 'LASHING' ||
|
||||
trigger === 'FUEL'
|
||||
DIRECTED_SURCHARGE_TRIGGERS.includes(trigger)
|
||||
? dto.tradeDirection !== undefined
|
||||
? dto.tradeDirection
|
||||
: existing.tradeDirection
|
||||
|
||||
Reference in New Issue
Block a user