mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 04:50:54 +00:00
feat: implement 20ft container weight-pairing validation
- 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.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user