add hard capacity ceiling to weight limit rules

This commit is contained in:
Marshal
2026-07-04 01:11:23 +00:00
parent 97cc9d76b1
commit 8ea2c8e95a
19 changed files with 340 additions and 196 deletions

View File

@@ -133,6 +133,13 @@ export class ContractBookingService {
});
}
// Hard capacity gate: a container line whose total weight exceeds the
// container type's max capacity can never be booked — no surcharge path,
// no override. Checked before any row is written.
if (freightType === 'CONTAINER') {
await this.assertWithinMaxCapacity(contract, dto);
}
// Denormalize route/direction/freight onto the booking for the scheduling engine.
const booking = await this.bookingsRepository.create({
reference,
@@ -608,6 +615,7 @@ export class ContractBookingService {
overweightSurchargeAmount: number;
currency: string | null;
pairingErrors: string[];
capacityErrors: string[];
lineItems: PriceLineItemDto[];
totalAmount: number;
}> {
@@ -621,6 +629,7 @@ export class ContractBookingService {
overweightSurchargeAmount: 0,
currency: null,
pairingErrors: [],
capacityErrors: [],
lineItems: [],
totalAmount: 0,
};
@@ -695,16 +704,63 @@ export class ContractBookingService {
(v) => v.message,
);
// Hard capacity ceiling — a non-empty result means the create call will be
// rejected, so the form can block submit up front.
const capacityErrors = await this.ruleEngineService.capacityViolations(
resolved.map(({ line, ct, totalVgmTons }) => ({
containerTypeId: ct.id,
quantity: line.quantity,
totalVgmTons,
})),
contract.tradeDirection,
);
return {
overweightLines: computed.overweightLines,
overweightSurchargeAmount,
currency: computed.currency,
pairingErrors,
capacityErrors,
lineItems: computed.lineItems,
totalAmount: computed.totalAmount,
};
}
/**
* Throws when any container line's total weight exceeds the hard capacity
* ceiling of its weight limit rule. Mirrors validateShipment's line
* resolution so the gate matches what the form preview reported.
*/
private async assertWithinMaxCapacity(
contract: Contract,
dto: CreateBookingUnderContractDto,
): Promise<void> {
const lines = dto.containers ?? [];
if (!lines.length) return;
const containers = await Promise.all(
lines.map(async (line) => {
const ct = await this.resolveContainerTypeForSize(
line.containerSize,
contract.isReefer || (line.reeferQuantity ?? 0) > 0,
);
const totalVgmTons = (line.units ?? []).reduce(
(s, u) => s + Number(u.vgmTons ?? 0),
0,
);
return { containerTypeId: ct.id, quantity: line.quantity, totalVgmTons };
}),
);
const violations = await this.ruleEngineService.capacityViolations(
containers,
contract.tradeDirection,
);
if (violations.length) {
throw new BadRequestException(violations.join('; '));
}
}
private async max20ftPairDiffTons(): Promise<number> {
const row = await this.dataSource
.getRepository(TrainSchedulingGlobalRules)