mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
auto allocation and batch managemnt, tracking the train
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ConflictException } from '@nestjs/common';
|
||||
import { BadRequestException, ConflictException } from '@nestjs/common';
|
||||
import { WagonReadiness, WagonStatus } from '@edr/types';
|
||||
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
@@ -25,6 +25,7 @@ const locomotive = {
|
||||
maxPullWeightTons: 3500,
|
||||
maxTrainLengthMeters: 760,
|
||||
status: 'AVAILABLE',
|
||||
readiness: WagonReadiness.ImportReady,
|
||||
};
|
||||
|
||||
const cw3 = {
|
||||
@@ -420,6 +421,9 @@ describe('TrainSchedulingService', () => {
|
||||
if (entity === TrainSchedulingGlobalRules) {
|
||||
return { find: jest.fn().mockResolvedValue([]) };
|
||||
}
|
||||
if (entity === WagonType) {
|
||||
return { find: jest.fn().mockResolvedValue([nw5, cw3]) };
|
||||
}
|
||||
throw new Error(`Unexpected repository ${(entity as { name?: string })?.name}`);
|
||||
});
|
||||
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({ id: 'schedule-1' });
|
||||
@@ -572,4 +576,202 @@ describe('TrainSchedulingService', () => {
|
||||
}),
|
||||
).rejects.toBeInstanceOf(ConflictException);
|
||||
});
|
||||
|
||||
it('flags physical fleet shortfall when export schedule lacks EXPORT_READY wagons', async () => {
|
||||
const exportBooking = makeBooking(
|
||||
'exp-1',
|
||||
'BKG-EXP',
|
||||
50,
|
||||
1,
|
||||
'40FT',
|
||||
1,
|
||||
'2026-06-20T08:00:00.000Z',
|
||||
'yard-addis',
|
||||
'yard-djibouti',
|
||||
{
|
||||
originYard: { label: 'Addis Ababa', code: 'ADDIS', country: 'Ethiopia' },
|
||||
destinationYard: { label: 'Djibouti', code: 'DJIBOUTI', country: 'Djibouti' },
|
||||
},
|
||||
);
|
||||
|
||||
wagonTypesRepository.findAll.mockResolvedValue([nw5]);
|
||||
bookingsRepository.findByIdsForScheduling.mockResolvedValue([exportBooking]);
|
||||
trainScheduleBookingsRepository.findByBookingIds.mockResolvedValue([]);
|
||||
locomotivesRepository.findAll.mockResolvedValue([locomotive]);
|
||||
|
||||
const importOnlyFleet = Array.from({ length: 5 }, (_, index) => ({
|
||||
id: `wagon-nw5-${index}`,
|
||||
wagonTypeId: nw5.id,
|
||||
wagonNumber: `WGN-${index}`,
|
||||
status: WagonStatus.Available,
|
||||
readiness: WagonReadiness.ImportReady,
|
||||
currentTrainScheduleId: null,
|
||||
}));
|
||||
|
||||
dataSource.getRepository.mockImplementation((entity: unknown) => {
|
||||
if (entity === TrainSchedulingGlobalRules) {
|
||||
return { find: jest.fn().mockResolvedValue([]) };
|
||||
}
|
||||
if (entity === Wagon) {
|
||||
return { find: jest.fn().mockResolvedValue(importOnlyFleet) };
|
||||
}
|
||||
if (entity === WagonType) {
|
||||
return { find: jest.fn().mockResolvedValue([nw5]) };
|
||||
}
|
||||
return { find: jest.fn().mockResolvedValue([]), findOne: jest.fn().mockResolvedValue(null) };
|
||||
});
|
||||
|
||||
const result = await service.previewContainerTrainSchedule({
|
||||
bookingIds: ['exp-1'],
|
||||
scheduleDate: '2026-06-20T08:00:00.000Z',
|
||||
originStationId: 'yard-addis',
|
||||
destinationStationId: 'yard-djibouti',
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(
|
||||
result.violations.some((v) => v.includes('EXPORT_READY') && v.includes('NW5')),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('assignBookingsToSchedule rejects when physical wagons cannot be pinned', async () => {
|
||||
const scheduleId = 'sched-assign-1';
|
||||
const trainSetId = 'train-set-1';
|
||||
const booking = makeBooking('b-pin', 'BKG-PIN', 50, 1, '40FT', 1);
|
||||
|
||||
wagonTypesRepository.findAll.mockResolvedValue([nw5]);
|
||||
bookingsRepository.findByIdsForScheduling.mockResolvedValue([{ ...booking, trainScheduleId: scheduleId }]);
|
||||
trainScheduleBookingsRepository.findByBookingIds.mockResolvedValue([]);
|
||||
locomotivesRepository.findAll.mockResolvedValue([locomotive]);
|
||||
trainSchedulesRepository.findById.mockResolvedValue({
|
||||
id: scheduleId,
|
||||
direction: 'IMPORT',
|
||||
});
|
||||
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({
|
||||
id: scheduleId,
|
||||
status: 'DRAFT',
|
||||
direction: 'IMPORT',
|
||||
trainSetId,
|
||||
originStationId: 'yard-origin',
|
||||
destinationStationId: 'yard-destination',
|
||||
scheduledDepartureDate: new Date('2026-06-20T08:00:00.000Z'),
|
||||
trainSet: {
|
||||
id: trainSetId,
|
||||
locomotive,
|
||||
wagons: [],
|
||||
},
|
||||
scheduleBookings: [],
|
||||
});
|
||||
|
||||
dataSource.getRepository.mockImplementation((entity: unknown) => {
|
||||
if (entity === TrainSchedulingGlobalRules) {
|
||||
return { find: jest.fn().mockResolvedValue([]) };
|
||||
}
|
||||
if (entity === Wagon) {
|
||||
return { find: jest.fn().mockResolvedValue([]) };
|
||||
}
|
||||
if (entity === WagonType) {
|
||||
return { find: jest.fn().mockResolvedValue([nw5]) };
|
||||
}
|
||||
return { find: jest.fn().mockResolvedValue([]), findOne: jest.fn().mockResolvedValue(null) };
|
||||
});
|
||||
|
||||
const wagonRepo = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const trainSetWagonRepo = {
|
||||
delete: jest.fn(),
|
||||
create: jest.fn((v) => v),
|
||||
save: jest.fn(async (rows) =>
|
||||
rows.map((r: { sequenceNo: number; wagonTypeId: string }, i: number) => ({
|
||||
...r,
|
||||
id: `slot-${i + 1}`,
|
||||
})),
|
||||
),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const manager = {
|
||||
getRepository: jest.fn((entity: unknown) => {
|
||||
if (entity === Wagon) return wagonRepo;
|
||||
if (entity === WagonType) return { find: jest.fn().mockResolvedValue([nw5]) };
|
||||
if (entity === TrainSetWagon) return trainSetWagonRepo;
|
||||
if ((entity as { name?: string })?.name === 'TrainSet') return { update: jest.fn() };
|
||||
if ((entity as { name?: string })?.name === 'TrainScheduleBooking') return { delete: jest.fn() };
|
||||
if ((entity as { name?: string })?.name === 'WagonBookingAllocation') {
|
||||
return {
|
||||
create: jest.fn((v) => v),
|
||||
save: jest.fn(async (v) => ({ ...v, id: 'alloc-1' })),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
}
|
||||
return { delete: jest.fn(), update: jest.fn(), find: jest.fn().mockResolvedValue([]) };
|
||||
}),
|
||||
};
|
||||
dataSource.transaction.mockImplementation(async (cb: (m: typeof manager) => Promise<void>) =>
|
||||
cb(manager),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.assignBookingsToSchedule(
|
||||
scheduleId,
|
||||
{ bookingIds: ['b-pin'], containerPlacements: [] },
|
||||
'CONTAINER',
|
||||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
describe('getAvailableLocomotivesForRoute', () => {
|
||||
it('filters to export-ready locomotives on Ethiopia → Djibouti routes', async () => {
|
||||
const routeId = 'route-export';
|
||||
const routeRepo = {
|
||||
findOne: jest.fn().mockResolvedValue({
|
||||
id: routeId,
|
||||
name: 'Addis → Djibouti',
|
||||
isActive: true,
|
||||
originYard: { country: 'Ethiopia' },
|
||||
destinationYard: { country: 'Djibouti' },
|
||||
}),
|
||||
};
|
||||
dataSource.getRepository.mockImplementation((entity: unknown) => {
|
||||
if ((entity as { name?: string })?.name === 'Route') return routeRepo;
|
||||
return { findOne: jest.fn(), update: jest.fn() };
|
||||
});
|
||||
locomotivesRepository.findAll.mockResolvedValue([
|
||||
{ id: 'l1', code: 'IMP', status: 'AVAILABLE', readiness: WagonReadiness.ImportReady },
|
||||
{ id: 'l2', code: 'EXP', status: 'AVAILABLE', readiness: WagonReadiness.ExportReady },
|
||||
]);
|
||||
|
||||
const result = await service.getAvailableLocomotivesForRoute(routeId);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].code).toBe('EXP');
|
||||
});
|
||||
|
||||
it('returns all available locomotives on domestic routes', async () => {
|
||||
const routeId = 'route-domestic';
|
||||
const routeRepo = {
|
||||
findOne: jest.fn().mockResolvedValue({
|
||||
id: routeId,
|
||||
name: 'Addis → Dire Dawa',
|
||||
isActive: true,
|
||||
originYard: { country: 'Ethiopia' },
|
||||
destinationYard: { country: 'Ethiopia' },
|
||||
}),
|
||||
};
|
||||
dataSource.getRepository.mockImplementation((entity: unknown) => {
|
||||
if ((entity as { name?: string })?.name === 'Route') return routeRepo;
|
||||
return { findOne: jest.fn(), update: jest.fn() };
|
||||
});
|
||||
locomotivesRepository.findAll.mockResolvedValue([
|
||||
{ id: 'l1', code: 'IMP', status: 'AVAILABLE', readiness: WagonReadiness.ImportReady },
|
||||
{ id: 'l2', code: 'EXP', status: 'AVAILABLE', readiness: WagonReadiness.ExportReady },
|
||||
]);
|
||||
|
||||
const result = await service.getAvailableLocomotivesForRoute(routeId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user