eims integration master test complete

This commit is contained in:
Hagernesh
2026-08-12 14:08:28 +00:00
parent 8d53dc17ce
commit 2d4da8110b
38 changed files with 2270 additions and 173 deletions

View File

@@ -129,6 +129,7 @@ import {
perEdgeConsistUsage,
validateContainerPlacements,
validateMixedTrainLimitsPerEdge,
MAX_TEU_SLOTS_PER_WAGON,
type ContainerPlacementInput,
type WagonPlanSlot,
} from '../utils/wagon-plan.util';
@@ -2930,6 +2931,10 @@ export class TrainSchedulingService {
// dispatch pre-check keeps reporting these bookings as unloaded).
const wagonAssignedIds = await this.getWagonAssignedBookingIds(scheduleId);
if (wagonAssignedIds.size) {
// Re-check the 1×40ft/2×20ft-per-wagon packing rule right before loading
// is confirmed — allocation time already enforces it, but a wagon swap or
// an edited allocation since then could have broken it unnoticed.
await this.assertWagonContainerCapacity(scheduleId);
// Export cargo must be received at the warehouse with a GRN before it can
// be confirmed loaded — an allocation is not proof the goods are in hand.
if (this.isExportSchedule(schedule)) {
@@ -3397,6 +3402,72 @@ export class TrainSchedulingService {
}
}
/**
* Re-check the 1×40ft / 2×20ft-per-wagon packing rule at loading confirmation
* time. `validateContainerPlacements` already enforces this the moment a
* booking is allocated to a wagon, but nothing re-checks it afterwards — a
* wagon swap, an edited allocation, or a container item added out-of-band
* between allocation and loading could still leave a wagon over its 2-TEU
* capacity undetected until the train is already loaded. This closes that gap
* by summing the TEU actually persisted per wagon (40ft = 2 TEU, 20ft = 1 TEU,
* same size resolution as the marshalling document) right before loading is
* confirmed.
*/
private async assertWagonContainerCapacity(scheduleId: string): Promise<void> {
const rows: Array<{ sequenceNo: number; wagonNumber: string | null; teuUsed: string }> =
await this.dataSource.query(
`SELECT tsw.sequence_no AS "sequenceNo",
pw.wagon_number AS "wagonNumber",
SUM(
CASE COALESCE(ct.size_ft, bct.size_ft)
WHEN 40 THEN 2
WHEN 20 THEN 1
ELSE
CASE
WHEN bc.container_size ILIKE '%40%' THEN 2
WHEN bc.container_size ILIKE '%20%' THEN 1
ELSE 0
END
END
) AS "teuUsed"
FROM freight.train_set_wagons tsw
JOIN freight.wagon_booking_allocations wba
ON wba.train_set_wagon_id = tsw.id AND wba.deleted_at IS NULL
JOIN freight.wagon_allocation_container_items wci
ON wci.wagon_booking_allocation_id = wba.id AND wci.deleted_at IS NULL
LEFT JOIN freight.container_types ct ON ct.id = wci.container_type_id
LEFT JOIN freight.booking_container bc ON bc.id = wci.booking_container_id
LEFT JOIN freight.container_types bct ON bct.id = bc.container_type_id
LEFT JOIN freight.wagons pw ON pw.id = tsw.physical_wagon_id
WHERE tsw.train_set_id = (
SELECT train_set_id FROM freight.train_schedules WHERE id = $1
)
AND tsw.deleted_at IS NULL
GROUP BY tsw.id, tsw.sequence_no, pw.wagon_number
HAVING SUM(
CASE COALESCE(ct.size_ft, bct.size_ft)
WHEN 40 THEN 2
WHEN 20 THEN 1
ELSE
CASE
WHEN bc.container_size ILIKE '%40%' THEN 2
WHEN bc.container_size ILIKE '%20%' THEN 1
ELSE 0
END
END
) > $2`,
[scheduleId, MAX_TEU_SLOTS_PER_WAGON],
);
if (rows.length) {
const labels = rows
.map((r) => `wagon #${r.sequenceNo}${r.wagonNumber ? ` (${r.wagonNumber})` : ''}`)
.join(', ');
throw new BadRequestException(
`These wagons exceed capacity (max 1×40ft or 2×20ft per wagon) — fix the container placement before confirming loading: ${labels}.`,
);
}
}
private buildImportLoadListHtml(loadList: Awaited<ReturnType<TrainSchedulingService['generateImportLoadList']>>): string {
const esc = (value: unknown) =>
String(value ?? '-')