mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 07:10:57 +00:00
Intercity bookings ride the wagons freed by earlier unloads (e.g. import containers uncoupled at Dire Dawa). loadBooking now auto-allocates a DOMESTIC booking onto on-train slots whose cargo has all departed — greedy in consist order by capacity, container numbers copied for the marshalling tally. Falls back to unallocated load when nothing is free.
146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
import { BookingJourneyService } from './booking-journey.service';
|
|
|
|
/**
|
|
* autoPlaceOnFreedWagons: intercity cargo boards the wagons freed by earlier
|
|
* unloads. Exercised directly with a stubbed EntityManager — the surrounding
|
|
* loadBooking flow is integration-tested through the running app.
|
|
*/
|
|
describe('BookingJourneyService.autoPlaceOnFreedWagons', () => {
|
|
const service = new BookingJourneyService(
|
|
{} as never, // dataSource
|
|
{} as never, // yardFacilities
|
|
{} as never, // facilityHandling
|
|
{ emit: jest.fn() } as never, // events
|
|
);
|
|
|
|
const schedule = { id: 'sched-1', trainSetId: 'ts-1' };
|
|
const booking = {
|
|
id: 'booking-1',
|
|
reference: 'BK-1',
|
|
cargoTotalWeightVgm: 50,
|
|
freightType: 'CONTAINER',
|
|
};
|
|
|
|
const makeManager = (slots: unknown[], existingAllocs: unknown[] = []) => {
|
|
const savedAllocs: Array<Record<string, unknown>> = [];
|
|
const savedItems: Array<Record<string, unknown>> = [];
|
|
const allocQb = {
|
|
innerJoinAndSelect: jest.fn().mockReturnThis(),
|
|
innerJoin: jest.fn().mockReturnThis(),
|
|
where: jest.fn().mockReturnThis(),
|
|
getMany: jest.fn().mockResolvedValue(existingAllocs),
|
|
};
|
|
const slotQb = {
|
|
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
|
innerJoin: jest.fn().mockReturnThis(),
|
|
where: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
getMany: jest.fn().mockResolvedValue(slots),
|
|
};
|
|
let allocId = 0;
|
|
const manager = {
|
|
getRepository: jest.fn((entity: { name?: string }) => {
|
|
const name = entity?.name;
|
|
if (name === 'WagonBookingAllocation') {
|
|
return {
|
|
createQueryBuilder: jest.fn(() => allocQb),
|
|
create: jest.fn((v: Record<string, unknown>) => v),
|
|
save: jest.fn(async (v: Record<string, unknown>) => {
|
|
const row = { ...v, id: `alloc-${++allocId}` };
|
|
savedAllocs.push(row);
|
|
return row;
|
|
}),
|
|
update: jest.fn(),
|
|
};
|
|
}
|
|
if (name === 'TrainSetWagon') {
|
|
return { createQueryBuilder: jest.fn(() => slotQb) };
|
|
}
|
|
if (name === 'BookingContainer') {
|
|
return {
|
|
find: jest.fn().mockResolvedValue([
|
|
{
|
|
id: 'line-1',
|
|
containerNumber: 'LINE-001',
|
|
containerTypeId: 'ct-20',
|
|
units: [{ containerNumber: 'UNIT-001' }, { containerNumber: 'UNIT-002' }],
|
|
},
|
|
]),
|
|
};
|
|
}
|
|
if (name === 'WagonAllocationContainerItem') {
|
|
return {
|
|
create: jest.fn((v: Record<string, unknown>) => v),
|
|
save: jest.fn(async (v: Record<string, unknown>) => {
|
|
savedItems.push(v);
|
|
return v;
|
|
}),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected repository ${name}`);
|
|
}),
|
|
};
|
|
return { manager, savedAllocs, savedItems };
|
|
};
|
|
|
|
const call = (manager: unknown) =>
|
|
(service as never as {
|
|
autoPlaceOnFreedWagons: (m: unknown, s: unknown, b: unknown) => Promise<void>;
|
|
}).autoPlaceOnFreedWagons(manager, schedule, booking);
|
|
|
|
it('places the booking on freed slots in consist order, with container items', async () => {
|
|
const slots = [
|
|
// Active cargo still riding — NOT freed.
|
|
{ id: 'slot-1', sequenceNo: 1, capacityTons: 60, allocations: [{ status: 'LOADED' }] },
|
|
// Freed by an earlier unload.
|
|
{ id: 'slot-2', sequenceNo: 2, capacityTons: 60, allocations: [{ status: 'DEPARTED' }] },
|
|
{ id: 'slot-3', sequenceNo: 3, capacityTons: 60, allocations: [] },
|
|
];
|
|
const { manager, savedAllocs, savedItems } = makeManager(slots);
|
|
|
|
await call(manager);
|
|
|
|
// 50 t fits on the first freed slot alone.
|
|
expect(savedAllocs).toHaveLength(1);
|
|
expect(savedAllocs[0]).toMatchObject({
|
|
trainSetWagonId: 'slot-2',
|
|
bookingId: 'booking-1',
|
|
allocatedWeightTons: 50,
|
|
status: 'LOADED',
|
|
});
|
|
// One item per physical unit, on the first allocation.
|
|
expect(savedItems.map((i) => i.containerNumber)).toEqual(['UNIT-001', 'UNIT-002']);
|
|
expect(savedItems.every((i) => i.wagonBookingAllocationId === 'alloc-1')).toBe(true);
|
|
});
|
|
|
|
it('spills over onto the next freed slot when one is not enough', async () => {
|
|
const slots = [
|
|
{ id: 'slot-2', sequenceNo: 2, capacityTons: 30, allocations: [{ status: 'DEPARTED' }] },
|
|
{ id: 'slot-3', sequenceNo: 3, capacityTons: 30, allocations: [] },
|
|
];
|
|
const { manager, savedAllocs } = makeManager(slots);
|
|
|
|
await call(manager);
|
|
|
|
expect(savedAllocs.map((a) => [a.trainSetWagonId, a.allocatedWeightTons])).toEqual([
|
|
['slot-2', 30],
|
|
['slot-3', 20],
|
|
]);
|
|
});
|
|
|
|
it('does nothing when the booking already has allocations', async () => {
|
|
const { manager, savedAllocs } = makeManager([], [{ id: 'existing' }]);
|
|
await call(manager);
|
|
expect(savedAllocs).toHaveLength(0);
|
|
});
|
|
|
|
it('loads without allocation when no wagon is free', async () => {
|
|
const slots = [
|
|
{ id: 'slot-1', sequenceNo: 1, capacityTons: 60, allocations: [{ status: 'LOADED' }] },
|
|
];
|
|
const { manager, savedAllocs } = makeManager(slots);
|
|
await expect(call(manager)).resolves.toBeUndefined();
|
|
expect(savedAllocs).toHaveLength(0);
|
|
});
|
|
});
|