Files
edr-platform/apps/edr-freight-api/src/modules/scheduling-reschedule/scheduling-reschedule.service.spec.ts

231 lines
7.5 KiB
TypeScript

import { BadRequestException } from '@nestjs/common';
import { compareSchedulingPriority } from '../scheduling/compare-scheduling-priority.util';
import { SchedulingRescheduleService } from './scheduling-reschedule.service';
const makeBooking = (
id: string,
reference: string,
extra: Record<string, unknown> = {},
) => ({
id,
reference,
freightType: 'CONTAINER',
cargoTotalWeightVgm: 100,
scheduledDate: new Date('2026-06-20T08:00:00.000Z'),
originYardId: 'yard-origin',
destinationYardId: 'yard-destination',
status: 'PAID',
isGovernment: false,
priorityScore: 50,
bookingContainers: [
{
id: `${id}-line`,
wagonsRequired: 5,
quantity: 1,
vgmPerUnitTons: 100,
},
],
...extra,
});
describe('compareSchedulingPriority', () => {
it('orders government before commercial', () => {
const sorted = [
{
isGovernment: false,
priorityScore: 50000,
scheduledDate: new Date('2026-06-20'),
},
{
isGovernment: true,
priorityScore: 100,
scheduledDate: new Date('2026-06-25'),
},
].sort(compareSchedulingPriority);
expect(sorted[0]?.isGovernment).toBe(true);
});
});
describe('SchedulingRescheduleService', () => {
let service: SchedulingRescheduleService;
let trainSchedulesRepository: Record<string, jest.Mock>;
let bookingsRepository: Record<string, jest.Mock>;
let trainSchedulingService: Record<string, jest.Mock>;
let schedulingRescheduleRepository: Record<string, jest.Mock>;
beforeEach(() => {
trainSchedulesRepository = {
findByIdWithFullGraph: jest.fn(),
updateStatus: jest.fn(),
};
bookingsRepository = {
findByIdsForScheduling: jest.fn(),
updateSchedulingFields: jest.fn(),
};
trainSchedulingService = {
previewTrainSchedule: jest.fn(),
unassignBooking: jest.fn(),
assignBookingsToSchedule: jest.fn(),
};
schedulingRescheduleRepository = {
createEvent: jest.fn().mockResolvedValue({ id: 'event-1' }),
};
service = new SchedulingRescheduleService(
trainSchedulesRepository as never,
bookingsRepository as never,
trainSchedulingService as never,
schedulingRescheduleRepository as never,
);
});
it('rejects reschedule on dispatched trains', async () => {
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({
id: 'sched-1',
status: 'DISPATCHED',
scheduleBookings: [],
});
await expect(
service.previewReschedule('sched-1', {
incomingBookingIds: ['gov-1'],
trigger: 'GOVERNMENT_PREEMPT',
}),
).rejects.toBeInstanceOf(BadRequestException);
});
it('displaces lower-priority commercial when government incoming exceeds capacity', async () => {
const commercial = makeBooking('c1', 'BKG-COMM', { priorityScore: 10, isGovernment: false });
const government = makeBooking('g1', 'BKG-GOV', {
isGovernment: true,
priorityScore: 60000,
governmentInstitution: 'Ministry',
});
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({
id: 'sched-1',
status: 'DRAFT',
scheduledDepartureDate: new Date('2026-06-20T08:00:00.000Z'),
originStationId: 'yard-origin',
destinationStationId: 'yard-destination',
scheduleBookings: [{ bookingId: 'c1', booking: commercial }],
});
bookingsRepository.findByIdsForScheduling.mockResolvedValue([government]);
trainSchedulingService.previewTrainSchedule.mockImplementation(
async ({ bookingIds }: { bookingIds: string[] }) => ({
valid: bookingIds.length <= 1,
violations: bookingIds.length > 1 ? ['Train capacity exceeded'] : [],
warnings: [],
}),
);
const plan = await service.previewReschedule('sched-1', {
incomingBookingIds: ['g1'],
trigger: 'GOVERNMENT_PREEMPT',
});
expect(plan.retained.map((b) => b.id)).toEqual(['g1']);
expect(plan.displaced.map((b) => b.id)).toEqual(['c1']);
expect(plan.finalBookingIds).toEqual(['g1']);
});
it('readmits high-priority commercial when spare capacity remains', async () => {
const low = makeBooking('c-low', 'BKG-LOW', { priorityScore: 5 });
const high = makeBooking('c-high', 'BKG-HIGH', { priorityScore: 500 });
const government = makeBooking('g1', 'BKG-GOV', { isGovernment: true, priorityScore: 60000 });
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({
id: 'sched-1',
status: 'DRAFT',
scheduledDepartureDate: new Date('2026-06-20T08:00:00.000Z'),
originStationId: 'yard-origin',
destinationStationId: 'yard-destination',
scheduleBookings: [
{ bookingId: 'c-low', booking: low },
{ bookingId: 'c-high', booking: high },
],
});
bookingsRepository.findByIdsForScheduling.mockResolvedValue([government]);
const fitAttempts = new Map<string, number>();
trainSchedulingService.previewTrainSchedule.mockImplementation(
async ({ bookingIds }: { bookingIds: string[] }) => {
const key = [...bookingIds].sort().join(',');
const attempt = (fitAttempts.get(key) ?? 0) + 1;
fitAttempts.set(key, attempt);
const fits =
bookingIds.length === 1 ||
(key === 'c-high,g1' && attempt > 1);
return {
valid: fits,
violations: fits ? [] : ['Train capacity exceeded'],
warnings: [],
};
},
);
const plan = await service.previewReschedule('sched-1', {
incomingBookingIds: ['g1'],
trigger: 'GOVERNMENT_PREEMPT',
});
expect(plan.retained.map((b) => b.id)).toEqual(['g1']);
expect(plan.readmitted.map((b) => b.id)).toEqual(['c-high']);
expect(plan.displaced.map((b) => b.id)).toEqual(['c-low']);
expect(plan.finalBookingIds).toEqual(['g1', 'c-high']);
});
it('maintenance reschedule updates departure and rebalances bookings', async () => {
const commercial = makeBooking('c1', 'BKG-COMM', { priorityScore: 10 });
const schedule = {
id: 'sched-1',
status: 'DRAFT',
scheduledDepartureDate: new Date('2026-06-20T08:00:00.000Z'),
originStationId: 'yard-origin',
destinationStationId: 'yard-destination',
scheduleBookings: [{ bookingId: 'c1', booking: commercial }],
};
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue(schedule);
bookingsRepository.findByIdsForScheduling.mockResolvedValue([commercial]);
trainSchedulingService.previewTrainSchedule.mockResolvedValue({
valid: true,
violations: [],
warnings: [],
});
trainSchedulesRepository.updateStatus.mockResolvedValue(undefined);
trainSchedulingService.assignBookingsToSchedule.mockResolvedValue({ id: 'sched-1' });
const result = await service.maintenanceReschedule(
'sched-1',
{
incomingBookingIds: ['c1'],
trigger: 'TRAIN_MAINTENANCE',
reason: 'Locomotive service',
newDepartureDate: '2026-06-22T10:00:00.000Z',
},
'staff-1',
);
expect(trainSchedulesRepository.updateStatus).toHaveBeenCalledWith(
'sched-1',
'DRAFT',
{ scheduledDepartureDate: new Date('2026-06-22T10:00:00.000Z') },
);
expect(schedulingRescheduleRepository.createEvent).toHaveBeenCalledWith(
expect.objectContaining({
trigger: 'TRAIN_MAINTENANCE',
actorUserId: 'staff-1',
reason: 'Locomotive service',
}),
);
expect(result.plan.trigger).toBe('TRAIN_MAINTENANCE');
expect(result.plan.finalBookingIds).toEqual(['c1']);
});
});