diff --git a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.spec.ts index d6e778216..05f216a7b 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.spec.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.spec.ts @@ -1134,7 +1134,7 @@ describe('TrainSchedulingService', () => { expect(html).toContain('2 (1 empty)'); }); - it('marks a leg slot on the import document as TO BE LOADED and keeps it out of the loaded tallies', () => { + it('drops a leg slot entirely from the import document — not part of the departing consist', () => { const loadList = { generatedAt: '2026-07-17T08:00:00.000Z', trainScheduleId: 'schedule-1', @@ -1176,15 +1176,16 @@ describe('TrainSchedulingService', () => { buildImportLoadListHtml: (l: unknown) => string; }).buildImportLoadListHtml(loadList); - expect(html).toContain('TO BE LOADED AT DIRE DAWA PORT'); - // Departure station of the leg slot is its board yard, not the origin. - expect(html).toContain('Dire Dawa Port'); - // Only the origin-loaded container counts; the leg slot's tallies separately. + // The leg slot (W-ICY, boards later at Dire Dawa) gets no row at all — + // it isn't on the departing consist. Only W-IMP appears. + expect(html).not.toContain('W-ICY'); + expect(html).not.toContain('ICY-001'); + expect(html).toContain('W-IMP'); + expect(html).toContain('Wagons1'); expect(html).toContain('Total containers1'); - expect(html).toContain('To load en route1 containers'); }); - it('marks a leg slot on the export document as TO LOAD AT its board yard and keeps it out of the tallies', () => { + it('drops a leg slot entirely from the export document — not part of the departing consist', () => { const sizedAllocation = { ...loadedAllocation, containerItems: [{ containerNumber: 'CONT-001', containerType: { sizeFt: 20 } }], @@ -1204,9 +1205,10 @@ describe('TrainSchedulingService', () => { pendingBoardYardLabelBySlot: new Map([['slot-leg', 'Dire Dawa Port']]), }); - expect(html).toContain('TO LOAD AT DIRE DAWA PORT'); + // The leg slot (W-LEG, boards later at Dire Dawa) gets no row at all. + expect(html).not.toContain('W-LEG'); + expect(html).toContain('Wagons1'); expect(html).toContain('Total containers1'); - expect(html).toContain('To load en route1 containers'); }); it('prints the consist-changes table for this stop, and omits it when there are none', () => { diff --git a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts index 19140d822..d20cc8dcf 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts @@ -3866,10 +3866,15 @@ export class TrainSchedulingService { const time = (value: unknown) => (value ? new Date(value as string | Date).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) : '-'); const bookingById = new Map((schedule.scheduleBookings ?? []).map((link) => [link.bookingId, link.booking])); // The document is checked against the physical train, so it has to run in - // consist order — the relation comes back unordered. - const wagons = [...(opts?.wagons ?? schedule.trainSet?.wagons ?? [])].sort( - (a, b) => Number(a.sequenceNo ?? 0) - Number(b.sequenceNo ?? 0), - ); + // consist order — the relation comes back unordered. Slots planned to + // couple at a LATER stop (pendingBoardYardLabelBySlot, origin docs only — + // intercity calls never pass it, their wagons list is already on-board + // only) are dropped here, not just tallied around: they are not part of + // the departing consist, so they get no row and no count on this document. + // Their own coupling shows up on THAT stop's own marshalling document. + const wagons = [...(opts?.wagons ?? schedule.trainSet?.wagons ?? [])] + .filter((wagon) => !opts?.pendingBoardYardLabelBySlot?.get(wagon.id)) + .sort((a, b) => Number(a.sequenceNo ?? 0) - Number(b.sequenceNo ?? 0)); // Empties sit on wagons that carry no booking allocation, keyed by the wagon // slot recorded when they were loaded. const emptiesByWagon = new Map(); @@ -3916,7 +3921,6 @@ export class TrainSchedulingService { `, ]; } - const pendingAt = opts?.pendingBoardYardLabelBySlot?.get(wagon.id); return allocations.map((allocation) => { const booking = allocation.booking ?? bookingById.get(allocation.bookingId); const cargoType = (booking as unknown as { cargoType?: { name?: string; code?: string } } | undefined)?.cargoType; @@ -3928,7 +3932,7 @@ export class TrainSchedulingService { const chassisNumbers = containerItems.map((item) => item.chassisNumber).filter(Boolean).join(', '); return ` ${wagonCells} - ${pendingAt ? `TO LOAD AT ${esc(pendingAt).toUpperCase()} — ` : ''}${esc(cargoType?.name ?? cargoType?.code ?? allocation.loadType)} + ${esc(cargoType?.name ?? cargoType?.code ?? allocation.loadType)} ${esc(companyName)} ${esc(containerNumbers || firstContainer?.containerNumber)} ${esc(chassisNumbers)} @@ -3965,27 +3969,20 @@ export class TrainSchedulingService { (wagon.allocations ?? []).length === 0 && !emptiesByWagon.get(Number(wagon.sequenceNo))?.length, ).length; - const loadsHere = (wagon: TrainSetWagon) => !opts?.pendingBoardYardLabelBySlot?.get(wagon.id); const totalWeight = wagons.reduce( (sum, wagon) => - sum + - (loadsHere(wagon) - ? (wagon.allocations ?? []).reduce((wagonSum, allocation) => wagonSum + Number(allocation.allocatedWeightTons || 0), 0) - : 0), + sum + (wagon.allocations ?? []).reduce((wagonSum, allocation) => wagonSum + Number(allocation.allocatedWeightTons || 0), 0), 0, ); // Container count summary (40ft, 20ft) — empties returning to Djibouti are - // physically on the train, so they count, and are called out on their own tile. - // Cargo boarding downstream is not on this train yet — it tallies separately. - let count40ft = 0, count20ft = 0, pendingContainers = 0; + // physically on the train, so they count, and are called out on their own + // tile. Cargo boarding downstream never enters this loop — `wagons` above + // already excludes those slots. + let count40ft = 0, count20ft = 0; wagons.forEach((wagon) => { (wagon.allocations ?? []).forEach((allocation) => { (allocation.containerItems ?? []).forEach((item) => { - if (!loadsHere(wagon)) { - pendingContainers++; - return; - } const size = this.resolveContainerItemSize(item); if (size === 40) count40ft++; else if (size === 20) count20ft++; @@ -4053,7 +4050,6 @@ export class TrainSchedulingService {
Containers 40ft${esc(count40ft)}
Containers 20ft${esc(count20ft)}
Total containers${esc(count40ft + count20ft)}
- ${pendingContainers ? `
To load en route${esc(pendingContainers)} containers
` : ''} ${emptyContainers.length ? `
Empty containers${esc(emptyContainers.length)}
` : ''}
Prepared person${esc(schedule.preparedByUserId)}
Check person${esc(schedule.checkedByUserId)}
@@ -4253,30 +4249,23 @@ export class TrainSchedulingService { .replace(/'/g, '''); const date = (value: unknown) => (value ? new Date(value as string | Date).toLocaleString('en-GB') : '-'); const status = loadList.operation.status; - // A leg slot (boardYard set) couples mid-corridor — its cargo is NOT on the - // physical train this Djibouti-side document is checked against, so it must - // stay out of the loaded tallies or the gate count stops matching. - const loadsHere = (wagon: (typeof loadList.wagons)[number]) => !wagon.boardYard; - const totalAllocations = loadList.wagons.reduce((sum, wagon) => sum + wagon.allocations.length, 0); - const totalWeight = loadList.wagons.reduce( - (sum, wagon) => - sum + - (loadsHere(wagon) - ? wagon.allocations.reduce((wagonSum, allocation) => wagonSum + Number(allocation.allocatedWeightTons || 0), 0) - : 0), + // A leg slot (boardYard set) couples mid-corridor — it is not part of the + // consist this Djibouti-side document is checked against yet, so it gets + // no row and no count here at all. Its own coupling shows up on THAT + // stop's own marshalling document once it actually happens. + const wagons = loadList.wagons.filter((wagon) => !wagon.boardYard); + const totalAllocations = wagons.reduce((sum, wagon) => sum + wagon.allocations.length, 0); + const totalWeight = wagons.reduce( + (sum, wagon) => sum + wagon.allocations.reduce((wagonSum, allocation) => wagonSum + Number(allocation.allocatedWeightTons || 0), 0), 0, ); - const emptyWagons = loadList.wagons.filter((wagon) => wagon.allocations.length === 0).length; + const emptyWagons = wagons.filter((wagon) => wagon.allocations.length === 0).length; - // Container count summary (40ft, 20ft) — loaded at origin vs. en route - let count40ft = 0, count20ft = 0, pendingContainers = 0; - loadList.wagons.forEach((wagon) => { + // Container count summary (40ft, 20ft) + let count40ft = 0, count20ft = 0; + wagons.forEach((wagon) => { wagon.allocations.forEach((allocation) => { (allocation.containerItems ?? []).forEach((item) => { - if (!loadsHere(wagon)) { - pendingContainers++; - return; - } const size = this.resolveContainerItemSize(item); if (size === 40) count40ft++; else if (size === 20) count20ft++; @@ -4284,7 +4273,7 @@ export class TrainSchedulingService { }); }); - const allocationRows = loadList.wagons + const allocationRows = wagons .flatMap((wagon) => { const wagonCells = `${esc(wagon.sequenceNo)} ${esc(wagon.wagonNumber)} @@ -4317,7 +4306,7 @@ export class TrainSchedulingService { ${esc(allocation.loadType)} ${esc(allocation.containerNumbers.length ? allocation.containerNumbers.join(', ') : '-')} ${esc(sealNumbers || '-')} - ${wagon.boardYard ? `TO BE LOADED AT ${esc(wagon.boardYard).toUpperCase()}` : ''} + ${esc(Number(allocation.allocatedWeightTons || 0).toFixed(3))} `; }, @@ -4384,13 +4373,12 @@ export class TrainSchedulingService {
Origin${esc(loadList.origin)}
Destination${esc(loadList.destination)}
Total bookings${esc(loadList.totalBookings)}
-
Wagons${esc(loadList.wagons.length)}${emptyWagons ? ` (${emptyWagons} empty)` : ''}
+
Wagons${esc(wagons.length)}${emptyWagons ? ` (${emptyWagons} empty)` : ''}
Allocations${esc(totalAllocations)}
Total weight${esc(totalWeight.toFixed(3))} T
Containers 40ft${esc(count40ft)}
Containers 20ft${esc(count20ft)}
Total containers${esc(count40ft + count20ft)}
- ${pendingContainers ? `
To load en route${esc(pendingContainers)} containers
` : ''}
Gatepass granted${esc(date(loadList.operation.gatepassGrantedAt))}