mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
refactor(train-scheduling): rename and restructure container movement logic
This commit is contained in:
@@ -5,7 +5,6 @@ import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||
import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
|
||||
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
||||
import { WagonAllocationContainerItem } from '../train-schedules/entities/wagon-allocation-container-item.entity';
|
||||
import { WagonBookingAllocation } from '../train-schedules/entities/wagon-booking-allocation.entity';
|
||||
import { TrainSchedulingService } from './train-scheduling.service';
|
||||
|
||||
@@ -1083,72 +1082,75 @@ describe('TrainSchedulingService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveContainerItem — staff rearrange', () => {
|
||||
const wagon1 = { id: 'w1', sequenceNo: 1, capacityTons: 61 };
|
||||
const wagon2 = { id: 'w2', sequenceNo: 2, capacityTons: 61 };
|
||||
const schedule = {
|
||||
describe('moveWagonLoad — staff rearrange', () => {
|
||||
const containerType = {
|
||||
code: 'NX70',
|
||||
supportedLoadTypes: ['CONTAINER'],
|
||||
supportsContainer: true,
|
||||
};
|
||||
let slotA: Record<string, unknown>;
|
||||
let slotB: Record<string, unknown>;
|
||||
let allocsByWagon: Record<string, Array<Record<string, unknown>>>;
|
||||
let allocRepo: { find: jest.Mock; update: jest.Mock };
|
||||
let slotRepo: { update: jest.Mock };
|
||||
let wagonRepo: { findOne: jest.Mock };
|
||||
|
||||
const makeSchedule = (over: Record<string, unknown> = {}) => ({
|
||||
id: 'sched-1',
|
||||
status: 'SCHEDULED',
|
||||
trainSet: { wagons: [wagon1, wagon2] },
|
||||
};
|
||||
|
||||
let sourceAlloc: Record<string, unknown>;
|
||||
let item: Record<string, unknown>;
|
||||
let itemRepo: { findOne: jest.Mock; update: jest.Mock; count: jest.Mock };
|
||||
let allocRepo: {
|
||||
find: jest.Mock;
|
||||
findOne: jest.Mock;
|
||||
create: jest.Mock;
|
||||
save: jest.Mock;
|
||||
update: jest.Mock;
|
||||
delete: jest.Mock;
|
||||
};
|
||||
let wagon2Allocs: Array<Record<string, unknown>>;
|
||||
trainSetId: 'ts-1',
|
||||
trainSet: { trainId: 'train-1', wagons: [slotA, slotB] },
|
||||
...over,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sourceAlloc = {
|
||||
id: 'alloc-1',
|
||||
trainSetWagonId: 'w1',
|
||||
bookingId: 'b1',
|
||||
allocatedWeightTons: 20,
|
||||
loadType: 'CONTAINER',
|
||||
status: 'PLANNED',
|
||||
containerItems: [],
|
||||
slotA = {
|
||||
id: 'wA',
|
||||
sequenceNo: 1,
|
||||
capacityTons: 61,
|
||||
lengthMeters: 14,
|
||||
assignedWeightTons: 40,
|
||||
status: 'RESERVED',
|
||||
boardYardId: 'yard-1',
|
||||
alightYardId: null,
|
||||
wagonType: containerType,
|
||||
};
|
||||
item = {
|
||||
id: 'item-1',
|
||||
wagonBookingAllocationId: 'alloc-1',
|
||||
positionOnWagon: 1,
|
||||
grossWeightTons: 20,
|
||||
containerType: { sizeFt: 20 },
|
||||
allocation: sourceAlloc,
|
||||
slotB = {
|
||||
id: 'wB',
|
||||
sequenceNo: 2,
|
||||
capacityTons: 61,
|
||||
lengthMeters: 14,
|
||||
assignedWeightTons: 25,
|
||||
status: 'RESERVED',
|
||||
boardYardId: null,
|
||||
alightYardId: null,
|
||||
wagonType: containerType,
|
||||
};
|
||||
allocsByWagon = {
|
||||
// 20ft pair (two allocations sharing wagon A) — must travel together.
|
||||
wA: [
|
||||
{ id: 'alloc-a1', trainSetWagonId: 'wA', bookingId: 'b1', allocatedWeightTons: 20, loadType: 'CONTAINER' },
|
||||
{ id: 'alloc-a2', trainSetWagonId: 'wA', bookingId: 'b2', allocatedWeightTons: 20, loadType: 'CONTAINER' },
|
||||
],
|
||||
// one 40ft on wagon B.
|
||||
wB: [
|
||||
{ id: 'alloc-b1', trainSetWagonId: 'wB', bookingId: 'b3', allocatedWeightTons: 25, loadType: 'CONTAINER' },
|
||||
],
|
||||
};
|
||||
sourceAlloc.containerItems = [item];
|
||||
wagon2Allocs = [];
|
||||
|
||||
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue(schedule);
|
||||
itemRepo = {
|
||||
findOne: jest.fn().mockResolvedValue(item),
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
count: jest.fn().mockResolvedValue(0),
|
||||
};
|
||||
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue(makeSchedule());
|
||||
allocRepo = {
|
||||
find: jest.fn().mockImplementation(({ where }: { where: { trainSetWagonId: string } }) =>
|
||||
Promise.resolve(where.trainSetWagonId === 'w1' ? [sourceAlloc] : wagon2Allocs),
|
||||
),
|
||||
findOne: jest.fn().mockImplementation(({ where }: { where: { id?: string } }) =>
|
||||
Promise.resolve(where.id === 'alloc-1' ? { ...sourceAlloc } : null),
|
||||
),
|
||||
create: jest.fn((v: unknown) => v),
|
||||
save: jest.fn().mockImplementation((v: Record<string, unknown>) =>
|
||||
Promise.resolve({ ...v, id: 'alloc-new' }),
|
||||
Promise.resolve(allocsByWagon[where.trainSetWagonId] ?? []),
|
||||
),
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
slotRepo = { update: jest.fn().mockResolvedValue(undefined) };
|
||||
wagonRepo = { findOne: jest.fn().mockResolvedValue(null) };
|
||||
dataSource.getRepository.mockImplementation((entity: unknown) => {
|
||||
if (entity === WagonAllocationContainerItem) return itemRepo;
|
||||
if (entity === WagonBookingAllocation) return allocRepo;
|
||||
if (entity === TrainSetWagon) return slotRepo;
|
||||
if (entity === Wagon) return wagonRepo;
|
||||
return { find: jest.fn().mockResolvedValue([]) };
|
||||
});
|
||||
dataSource.transaction.mockImplementation(
|
||||
@@ -1164,61 +1166,87 @@ describe('TrainSchedulingService', () => {
|
||||
});
|
||||
|
||||
it('rejects moves on a dispatched train', async () => {
|
||||
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({
|
||||
...schedule,
|
||||
status: 'DISPATCHED',
|
||||
});
|
||||
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue(
|
||||
makeSchedule({ status: 'DISPATCHED' }),
|
||||
);
|
||||
await expect(
|
||||
service.moveContainerItem('sched-1', 'item-1', { targetTrainSetWagonId: 'w2' }),
|
||||
service.moveWagonLoad('sched-1', 'wA', { targetWagonId: 'wB' }),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
expect(dataSource.transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a target wagon that has no TEU room left', async () => {
|
||||
wagon2Allocs = [
|
||||
{
|
||||
id: 'alloc-2',
|
||||
trainSetWagonId: 'w2',
|
||||
bookingId: 'b2',
|
||||
allocatedWeightTons: 25,
|
||||
loadType: 'CONTAINER',
|
||||
containerItems: [{ id: 'item-40', containerType: { sizeFt: 40 } }],
|
||||
},
|
||||
];
|
||||
it('404s when the target is neither a slot nor a consist wagon of this train', async () => {
|
||||
await expect(
|
||||
service.moveContainerItem('sched-1', 'item-1', { targetTrainSetWagonId: 'w2' }),
|
||||
).rejects.toThrow(/no room/);
|
||||
service.moveWagonLoad('sched-1', 'wA', { targetWagonId: 'nope' }),
|
||||
).rejects.toThrow(/not part of this schedule/);
|
||||
});
|
||||
|
||||
it('rejects a bulk-loaded target wagon', async () => {
|
||||
wagon2Allocs = [
|
||||
{
|
||||
id: 'alloc-2',
|
||||
trainSetWagonId: 'w2',
|
||||
bookingId: 'b2',
|
||||
allocatedWeightTons: 40,
|
||||
loadType: 'BULK',
|
||||
containerItems: [],
|
||||
},
|
||||
];
|
||||
await expect(
|
||||
service.moveContainerItem('sched-1', 'item-1', { targetTrainSetWagonId: 'w2' }),
|
||||
).rejects.toThrow(/bulk/);
|
||||
});
|
||||
it('swaps two loaded wagons: every allocation crosses over, load fields swap', async () => {
|
||||
await service.moveWagonLoad('sched-1', 'wA', { targetWagonId: 'wB' });
|
||||
|
||||
it('moves a container to an empty wagon and re-homes its allocation', async () => {
|
||||
await service.moveContainerItem('sched-1', 'item-1', { targetTrainSetWagonId: 'w2' });
|
||||
|
||||
// A new allocation for the booking was created on the target wagon…
|
||||
expect(allocRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ trainSetWagonId: 'w2', bookingId: 'b1' }),
|
||||
);
|
||||
// …the container item now hangs off it…
|
||||
expect(itemRepo.update).toHaveBeenCalledWith('item-1', {
|
||||
wagonBookingAllocationId: 'alloc-new',
|
||||
// The 20ft pair moved together onto wagon B…
|
||||
expect(allocRepo.update).toHaveBeenCalledWith('alloc-a1', { trainSetWagonId: 'wB' });
|
||||
expect(allocRepo.update).toHaveBeenCalledWith('alloc-a2', { trainSetWagonId: 'wB' });
|
||||
// …and the 40ft came back to wagon A.
|
||||
expect(allocRepo.update).toHaveBeenCalledWith('alloc-b1', { trainSetWagonId: 'wA' });
|
||||
// Load-coupled slot fields follow their loads.
|
||||
expect(slotRepo.update).toHaveBeenCalledWith('wB', {
|
||||
assignedWeightTons: 40,
|
||||
status: 'RESERVED',
|
||||
boardYardId: 'yard-1',
|
||||
alightYardId: null,
|
||||
});
|
||||
// …and the emptied source allocation was deleted, not left at 0 items.
|
||||
expect(allocRepo.delete).toHaveBeenCalledWith('alloc-1');
|
||||
expect(slotRepo.update).toHaveBeenCalledWith('wA', {
|
||||
assignedWeightTons: 25,
|
||||
status: 'RESERVED',
|
||||
boardYardId: null,
|
||||
alightYardId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('repins the slot onto an empty consist-only wagon (the 404 case)', async () => {
|
||||
wagonRepo.findOne.mockResolvedValue({
|
||||
id: 'phys-9',
|
||||
wagonTypeId: 'wt-1',
|
||||
wagonNumber: 'WGN-9',
|
||||
wagonType: { ...containerType, capacityTons: 70, lengthMeters: 14 },
|
||||
});
|
||||
|
||||
await service.moveWagonLoad('sched-1', 'wA', { targetWagonId: 'phys-9' });
|
||||
|
||||
expect(wagonRepo.findOne).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { id: 'phys-9', trainId: 'train-1' } }),
|
||||
);
|
||||
// Repin: wagon identity moves onto the slot; allocations stay put.
|
||||
expect(slotRepo.update).toHaveBeenCalledWith('wA', {
|
||||
physicalWagonId: 'phys-9',
|
||||
wagonTypeId: 'wt-1',
|
||||
capacityTons: 70,
|
||||
lengthMeters: 14,
|
||||
});
|
||||
expect(allocRepo.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a bulk load onto a wagon whose type only supports containers', async () => {
|
||||
allocsByWagon.wA = [
|
||||
{ id: 'alloc-bulk', trainSetWagonId: 'wA', bookingId: 'b9', allocatedWeightTons: 50, loadType: 'BULK' },
|
||||
];
|
||||
allocsByWagon.wB = [];
|
||||
|
||||
await expect(
|
||||
service.moveWagonLoad('sched-1', 'wA', { targetWagonId: 'wB' }),
|
||||
).rejects.toThrow(/cannot carry a bulk load/);
|
||||
});
|
||||
|
||||
it('rejects when the incoming load exceeds the receiving wagon payload', async () => {
|
||||
allocsByWagon.wA = [
|
||||
{ id: 'alloc-heavy', trainSetWagonId: 'wA', bookingId: 'b9', allocatedWeightTons: 70, loadType: 'CONTAINER' },
|
||||
];
|
||||
allocsByWagon.wB = [];
|
||||
|
||||
await expect(
|
||||
service.moveWagonLoad('sched-1', 'wA', { targetWagonId: 'wB' }),
|
||||
).rejects.toThrow(/over its/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user