fix issue

This commit is contained in:
Marshal
2026-08-22 00:49:53 +00:00
parent bad418d79e
commit b6c1efa043
22 changed files with 1448 additions and 88 deletions

View File

@@ -8821,6 +8821,13 @@ export class TrainSchedulingService {
allocatedWeightTons: roundTons(Number(allocation.allocatedWeightTons)),
loadType: allocation.loadType ?? null,
status: allocation.status,
// THIS load's own corridor, not the wagon's union span.
// A wagon reused across disjoint legs carries two loads
// with different yards; without these the leg board can
// only draw one merged bar and cannot say which load
// rides which leg.
originYardId: allocation.booking?.originYardId ?? null,
destinationYardId: allocation.booking?.destinationYardId ?? null,
containerItems: (containerItemsByAllocation.get(allocation.id) ?? []).map(
(item) => ({
id: item.id,

View File

@@ -35,11 +35,34 @@ export type DeferredBookingRow = {
shortage?: BookingWagonShortage | null;
};
/** A booking the customer has already paid for. */
const isPaid = (booking: Booking): boolean =>
booking.paymentStatus === 'PAID' || booking.status === 'PAID';
/**
* Seating order for the wagon planner.
*
* Government first, then PAID bookings, then priority score, then date.
*
* Payment ranks above priority score on purpose: money has changed hands and
* the customer was promised space on THIS train. Without it the planner
* seated an unpaid booking that merely arrived earlier and left a paid one
* with no wagon — the reported S-2026-00045 case, where a paid 695T bulk
* booking lost every wagon to unpaid container bookings and vanished from
* the train with free PW2 still standing in the consist.
*
* This only decides who is seated FIRST when the train is oversubscribed. It
* never invents capacity: an oversubscribed train still defers someone, and
* that someone is now the party who has not paid.
*/
export function sortBookingsForScheduling(bookings: Booking[]): Booking[] {
return [...bookings].sort((a, b) => {
const govDiff = Number(Boolean(b.isGovernment)) - Number(Boolean(a.isGovernment));
if (govDiff !== 0) return govDiff;
const paidDiff = Number(isPaid(b)) - Number(isPaid(a));
if (paidDiff !== 0) return paidDiff;
const priorityDiff = (b.priorityScore ?? 0) - (a.priorityScore ?? 0);
if (priorityDiff !== 0) return priorityDiff;

View File

@@ -154,10 +154,46 @@ const shortageFor = (
),
)
: Math.max(1, containerWagonsForLines(booking.bookingContainers ?? []));
const wagonsAvailable = candidates.reduce(
(sum, wt) => sum + availableOf(wt.id),
0,
);
const freeByType = candidates.map((wt) => ({ wt, free: availableOf(wt.id) }));
const wagonsAvailable = freeByType.reduce((sum, c) => sum + c.free, 0);
// PER_TON bulk: a bare wagon COUNT lies when the types carry different
// tonnage for this cargo. 14 NW5 (30T) + 10 PW2 (20T) is "24 wagons free"
// against a 24-wagon need, yet only 620T of the 695T booking fits — which
// is how a deferral could read "needs 24, 24 available (short 1)". Size the
// shortfall in the wagons the cargo's OWN caps require: how many more
// wagons of the best remaining type would carry the leftover tonnage.
const tons = bookingCargoTons(booking);
const perItem =
Number(booking.bulkTotalWeightTons ?? 0) > 0 &&
Number(booking.cargoTotalWeightVgm ?? 0) > 0;
if (booking.freightType === 'BULK' && !perItem && tons > 0) {
let seatable = 0;
let usedWagons = 0;
for (const { wt, free } of freeByType) {
const perWagon = bulkTonsPerWagon(booking.cargoType, wt.id, Number(wt.capacityTons));
if (!(perWagon > 0) || free <= 0) continue;
seatable += free * perWagon;
usedWagons += free;
}
if (seatable < tons) {
const bestPerWagon = Math.max(
1,
...candidates.map((wt) =>
bulkTonsPerWagon(booking.cargoType, wt.id, Number(wt.capacityTons)),
),
);
return {
wagonTypeCodes: [...new Set(candidates.map((wt) => wt.code))].join('/'),
wagonsNeeded,
wagonsAvailable: usedWagons,
// Wagons of the best type still missing to carry the leftover tonnage.
wagonsShort: Math.max(1, Math.ceil((tons - seatable) / bestPerWagon)),
};
}
}
return {
wagonTypeCodes: [...new Set(candidates.map((wt) => wt.code))].join('/'),
wagonsNeeded,