Files
edr-platform/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.spec.ts
Hagernesh 89dba01cd7 feat(train-scheduling): notify customer of CAS on direct-to-train load
Direct truck-to-train export cargo skips the warehouse, so the existing
carriage acceptance sheet ready notice (fired on warehouse receive)
never reached these bookings. Their handover moment is the load itself.

Extract notifyCarriageAcceptanceReady into a shared notifications util
(was private to WarehouseInventoryService) and call it from
BookingJourneyService.loadBooking for EXPORT + DIRECT_TO_TRAIN bookings,
right after the GRN gate, before the load transaction proceeds.
2026-08-13 09:58:04 +00:00

148 lines
5.3 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
{} as never, // notifications
{} as never, // inbox
);
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);
});
});