mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 20:40:55 +00:00
Merge pull request #782 from Tria-plc/freight_feature/usermanagement
add lashing surcharge for cargo types with hasLashing flag
This commit is contained in:
@@ -37,6 +37,14 @@ export class CreateCargoTypeDto {
|
||||
@IsBoolean()
|
||||
requiresDirectorApproval?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
default: false,
|
||||
description: 'When true, bookings of this cargo type incur the flat LASHING surcharge.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hasLashing?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
|
||||
@@ -53,6 +53,14 @@ export class CargoType extends BaseEntity {
|
||||
@Column({ name: 'requires_director_approval', type: 'boolean', default: false })
|
||||
requiresDirectorApproval!: boolean;
|
||||
|
||||
/**
|
||||
* When true, any booking of this cargo type incurs the flat LASHING surcharge
|
||||
* (the LASHING-trigger rate). Set on commodities that need EDR-provided
|
||||
* lashing/securing; leave false for cargo that ships without it.
|
||||
*/
|
||||
@Column({ name: 'has_lashing', type: 'boolean', default: false })
|
||||
hasLashing!: boolean;
|
||||
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export function deriveRateType(input: {
|
||||
case 'SHIPPING_LINE':
|
||||
return 'DOUBLE_HANDLING';
|
||||
case 'CONSOLIDATION':
|
||||
case 'LASHING':
|
||||
return 'LASHING';
|
||||
case 'CANCELLATION':
|
||||
return 'CANCELLATION_FEE';
|
||||
|
||||
@@ -36,6 +36,9 @@ export function allowedRateUnits(input: {
|
||||
case 'CUSTOMS_CLEARANCE':
|
||||
// Flat per clearance (ONE_TIME contract) / per shipment request (GENERAL).
|
||||
return ['FLAT'];
|
||||
case 'LASHING':
|
||||
// Flat cargo-securing fee, billed once per booking.
|
||||
return ['FLAT'];
|
||||
case 'CONSOLIDATION':
|
||||
return ['PER_CONTAINER', 'FLAT'];
|
||||
case 'SHIPPING_LINE':
|
||||
|
||||
@@ -78,6 +78,9 @@ export const RATE_TRIGGERS = [
|
||||
'WITH_RETURN',
|
||||
'SHIPPING_LINE',
|
||||
'CONSOLIDATION',
|
||||
// Cargo securing / lashing. Fires when the booking's cargo type has
|
||||
// hasLashing = true. Flat fee, billed once per booking.
|
||||
'LASHING',
|
||||
'CANCELLATION',
|
||||
'DEMURRAGE',
|
||||
'PIL_EXTRA_FEE',
|
||||
|
||||
@@ -6,6 +6,12 @@ import { Rate } from '../entities/rate.entity';
|
||||
export interface IRatesRepository {
|
||||
findById(id: string): Promise<Rate | null>;
|
||||
findLiveRates(): Promise<Rate[]>;
|
||||
/**
|
||||
* LIVE rates with the yard / container / cargo relations eagerly joined, so
|
||||
* lanes can be rendered with human labels (contract rate schedule). Ordered
|
||||
* for a stable, readable schedule table.
|
||||
*/
|
||||
findLiveRatesDetailed(): Promise<Rate[]>;
|
||||
findByPattern(pattern: {
|
||||
rateType: string;
|
||||
rateUnit: string;
|
||||
|
||||
@@ -25,6 +25,22 @@ export class RatesRepository implements IRatesRepository {
|
||||
.getMany();
|
||||
}
|
||||
|
||||
findLiveRatesDetailed(): Promise<Rate[]> {
|
||||
return this.repo
|
||||
.createQueryBuilder('rate')
|
||||
.leftJoinAndSelect('rate.originYard', 'originYard')
|
||||
.leftJoinAndSelect('rate.destinationYard', 'destinationYard')
|
||||
.leftJoinAndSelect('rate.containerType', 'containerType')
|
||||
.leftJoinAndSelect('rate.cargoType', 'cargoType')
|
||||
.where('rate.status = :status', { status: 'LIVE' })
|
||||
.orderBy('rate.appliesTo', 'ASC')
|
||||
.addOrderBy('rate.tradeDirection', 'ASC')
|
||||
.addOrderBy('originYard.label', 'ASC')
|
||||
.addOrderBy('destinationYard.label', 'ASC')
|
||||
.addOrderBy('rate.rateValue', 'ASC')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a non-superseded rate matching an identity pattern — the same tuple the
|
||||
* `UQ_rates_pattern` unique index enforces. Used to reject duplicates before
|
||||
|
||||
@@ -61,6 +61,12 @@ export interface BookingEvaluationInput {
|
||||
isGovernment?: boolean;
|
||||
allowConsolidation?: boolean;
|
||||
shippingLineId?: string | null;
|
||||
/**
|
||||
* Booking's cargo type needs EDR-provided lashing/securing (cargoType
|
||||
* hasLashing = true). Fires the flat LASHING surcharge. Resolved by the
|
||||
* engine from cargoTypeId when omitted.
|
||||
*/
|
||||
hasLashing?: boolean;
|
||||
totalWagons: number;
|
||||
/**
|
||||
* Total bulk tonnage on the booking (cargoTotalWeightVgm). Used to scale
|
||||
@@ -132,12 +138,22 @@ export class RuleEngineService {
|
||||
requiresDirectorApproval = true;
|
||||
}
|
||||
|
||||
// Lashing is a cargo-type property: a booking incurs the flat LASHING
|
||||
// surcharge when its cargo type has hasLashing = true. Resolve it here so
|
||||
// matchesTrigger can fire the LASHING rate. Falls back to an explicit
|
||||
// input flag when no cargo type is set (e.g. container bookings).
|
||||
let hasLashing = input.hasLashing === true;
|
||||
if (input.cargoTypeId) {
|
||||
const cargoType = await this.cargoTypesRepo.findById(input.cargoTypeId);
|
||||
if (!cargoType) {
|
||||
hardBlocked.push(`Cargo type ${input.cargoTypeId} not found`);
|
||||
} else if (cargoType.requiresDirectorApproval) {
|
||||
requiresDirectorApproval = true;
|
||||
} else {
|
||||
if (cargoType.requiresDirectorApproval) {
|
||||
requiresDirectorApproval = true;
|
||||
}
|
||||
if (cargoType.hasLashing) {
|
||||
hasLashing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +253,7 @@ export class RuleEngineService {
|
||||
hasOverweight,
|
||||
shippingLineMapped,
|
||||
allowConsolidation: input.allowConsolidation ?? false,
|
||||
hasLashing,
|
||||
});
|
||||
if (!triggered) continue;
|
||||
|
||||
@@ -466,6 +483,7 @@ export class RuleEngineService {
|
||||
hasOverweight: boolean;
|
||||
shippingLineMapped: boolean;
|
||||
allowConsolidation: boolean;
|
||||
hasLashing: boolean;
|
||||
},
|
||||
): boolean {
|
||||
// Coerce defensively: a flag may arrive as the string "true"/"false" (e.g.
|
||||
@@ -484,6 +502,8 @@ export class RuleEngineService {
|
||||
return truthy(state.shippingLineMapped);
|
||||
case 'CONSOLIDATION':
|
||||
return truthy(state.allowConsolidation);
|
||||
case 'LASHING':
|
||||
return truthy(state.hasLashing);
|
||||
// CANCELLATION / DEMURRAGE / PIL_EXTRA_FEE are contextual charges applied
|
||||
// explicitly elsewhere (not auto-triggered by a booking's cargo flags).
|
||||
default:
|
||||
|
||||
@@ -44,6 +44,14 @@ export class RatesService {
|
||||
return this.repository.findLiveRates();
|
||||
}
|
||||
|
||||
/**
|
||||
* LIVE rates with yard / container / cargo relations joined — used to render
|
||||
* the origin → destination rate schedule inside generated contracts.
|
||||
*/
|
||||
async findLiveRatesDetailed(): Promise<Rate[]> {
|
||||
return this.repository.findLiveRatesDetailed();
|
||||
}
|
||||
|
||||
/** Get a rate by ID. */
|
||||
async findById(id: string): Promise<Rate> {
|
||||
const entity = await this.repository.findById(id);
|
||||
|
||||
Reference in New Issue
Block a user