mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
- Added ContainerValidationService to handle 20ft weight-pairing logic. - Introduced validate20ftWeightPairing utility function to check weight differences. - Updated BookingPricingService to include overweight line details and pairing errors in price response. - Enhanced BookingTransitionService to reject submissions with unpairable 20ft containers. - Created ShipmentValidation interface for pre-submit validation of container contracts. - Integrated shipment validation into the contract booking process, providing warnings for overweight containers and hard blocks for pairing errors. - Updated front-end components to display validation results and prevent submission when errors are present.
77 lines
3.0 KiB
TypeScript
77 lines
3.0 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectDataSource } from '@nestjs/typeorm';
|
|
import { DataSource, In } from 'typeorm';
|
|
|
|
import { TrainSchedulingGlobalRules } from '../train-scheduling/entities/train-scheduling-global-rules.entity';
|
|
import { Booking } from './entities/booking.entity';
|
|
import { BookingContainerUnit } from './entities/booking-container-unit.entity';
|
|
import {
|
|
Container20ftUnit,
|
|
PairingViolation,
|
|
validate20ftWeightPairing,
|
|
} from './container-pairing.util';
|
|
|
|
/** Default 20ft pair weight-difference cap when no global rules row exists (matches the entity default). */
|
|
const DEFAULT_MAX_20FT_PAIR_DIFF_TONS = 10;
|
|
|
|
/**
|
|
* Booking-time container validations that need the customer-entered per-unit
|
|
* weights (`BookingContainerUnit`): the 20ft weight-pairing rule. Kept out of the
|
|
* rule engine (which works on line totals) because pairing is per physical unit.
|
|
*/
|
|
@Injectable()
|
|
export class ContainerValidationService {
|
|
constructor(@InjectDataSource() private readonly dataSource: DataSource) {}
|
|
|
|
private async maxPairDiffTons(): Promise<number> {
|
|
const row = await this.dataSource
|
|
.getRepository(TrainSchedulingGlobalRules)
|
|
.find({ order: { createdAt: 'ASC' }, take: 1 })
|
|
.then((rows) => rows[0] ?? null)
|
|
.catch(() => null);
|
|
const v = row?.max20ftPairWeightDiffTons;
|
|
const n = v == null ? NaN : Number(v);
|
|
return Number.isFinite(n) ? n : DEFAULT_MAX_20FT_PAIR_DIFF_TONS;
|
|
}
|
|
|
|
/** Load every 20ft container UNIT weight for a booking (customer-entered VGM). */
|
|
private async load20ftUnits(booking: Booking): Promise<Container20ftUnit[]> {
|
|
const lines = (booking.bookingContainers ?? []).filter(
|
|
(bc) => (bc.containerSize ?? '').includes('20'),
|
|
);
|
|
if (!lines.length) return [];
|
|
|
|
const units = await this.dataSource
|
|
.getRepository(BookingContainerUnit)
|
|
.find({
|
|
where: { bookingContainerId: In(lines.map((l) => l.id)) },
|
|
order: { sortOrder: 'ASC' },
|
|
});
|
|
|
|
return units.map((u) => ({
|
|
label: u.containerNumber || u.id.slice(0, 8),
|
|
grossWeightTons: Number(u.vgmTons ?? 0),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Validate the 20ft weight-pairing rule for a booking. Returns one message per
|
|
* pair whose weight difference exceeds the cap; empty when all 20ft can be
|
|
* balanced onto wagons (or there is nothing to pair). A lone odd 20ft is fine —
|
|
* it flows to consolidation. Callers hard-block a non-empty result.
|
|
*/
|
|
async validate20ftPairing(booking: Booking): Promise<PairingViolation[]> {
|
|
// Only bookings whose 20ft lines actually carry per-unit weights can be
|
|
// checked; contract-drawdown bookings do (units are required there).
|
|
const containerLines = booking.bookingContainers ?? [];
|
|
const has20ft = containerLines.some((bc) => (bc.containerSize ?? '').includes('20'));
|
|
if (!has20ft) return [];
|
|
|
|
const units = await this.load20ftUnits(booking);
|
|
if (units.length < 2) return [];
|
|
|
|
const maxDiff = await this.maxPairDiffTons();
|
|
return validate20ftWeightPairing(units, maxDiff);
|
|
}
|
|
}
|