feat: implement MANUAL_ONLY status for bookings and update related logic

This commit is contained in:
Marshal
2026-08-25 22:01:03 +00:00
parent b926a3116e
commit 29371b0b39
8 changed files with 257 additions and 36 deletions

View File

@@ -170,7 +170,7 @@ describe('TrainSchedulingService', () => {
wagonAllocationContainerItemsRepository as never,
wagonAllocationBulkLoadsRepository as never,
trainCheckpointEventsRepository as never,
{} as never, // trainCompositionRemovalLogRepository
{ create: jest.fn() } as never, // trainCompositionRemovalLogRepository
{
autoUnloadArrivedBookings: jest.fn(),
autoUnloadExportAtDjibouti: jest.fn(),
@@ -182,7 +182,7 @@ describe('TrainSchedulingService', () => {
{
autoArriveAtFinalYard: jest.fn().mockResolvedValue([]),
} as never, // bookingJourneyService
{ dispatched: jest.fn(), arrived: jest.fn() } as never, // bookingNotifier
{ dispatched: jest.fn(), arrived: jest.fn(), removedFromTrain: jest.fn() } as never, // bookingNotifier
{ getLogoImageUrl: jest.fn().mockResolvedValue(null) } as never, // logoSettings
);
@@ -1640,6 +1640,85 @@ describe('TrainSchedulingService', () => {
});
});
describe('unassignBooking — MANUAL_ONLY status', () => {
const scheduleId = 'sched-rm-1';
const removed = makeBooking('bk-rm', 'BKG-RM', 100, 5, '20FT', 5, undefined, undefined, undefined, {
status: 'PAID',
wagonsRequired: 5,
});
const graph = {
id: scheduleId,
status: 'DRAFT',
originStationId: 'yard-origin',
destinationStationId: 'yard-destination',
trainSetId: 'ts-rm',
trainSet: {
id: 'ts-rm',
locomotive,
trainId: null,
wagons: [{ id: 'tsw-rm-1', allocations: [{ id: 'alloc-rm-1', bookingId: 'bk-rm' }] }],
},
scheduleBookings: [{ bookingId: 'bk-rm' }],
};
const txManager = {
getRepository: jest.fn(() => ({
find: jest.fn().mockResolvedValue([]),
findOne: jest.fn().mockResolvedValue(null),
update: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(undefined),
save: jest.fn().mockResolvedValue(undefined),
create: jest.fn((x: unknown) => x),
})),
};
beforeEach(() => {
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue(graph);
bookingsRepository.findById = jest.fn().mockResolvedValue(removed);
bookingsRepository.updateSchedulingFields.mockResolvedValue(undefined);
dataSource.transaction.mockImplementation(
async (fn: (m: unknown) => Promise<void>) => fn(txManager),
);
jest
.spyOn(
service as never as { getTrainScheduleById: (id: string) => Promise<unknown> },
'getTrainScheduleById' as never,
)
.mockResolvedValue({ id: scheduleId } as never);
});
it('marks a staff-removed paid booking MANUAL_ONLY and fully detaches it', async () => {
await service.unassignBooking(scheduleId, 'bk-rm', 'user-1');
expect(bookingsRepository.updateSchedulingFields).toHaveBeenCalledWith(
'bk-rm',
expect.objectContaining({
schedulingStatus: 'MANUAL_ONLY',
trainScheduleId: null,
wagonsRequired: null,
}),
expect.anything(),
);
expect(trainScheduleBookingsRepository.deleteByScheduleAndBooking).toHaveBeenCalledWith(
scheduleId,
'bk-rm',
expect.anything(),
);
expect(wagonAllocationContainerItemsRepository.deleteByAllocationIds).toHaveBeenCalledWith(
['alloc-rm-1'],
expect.anything(),
);
});
it('never marks ELIGIBLE — a removed booking must not rejoin the auto pool', async () => {
await service.unassignBooking(scheduleId, 'bk-rm', 'user-1');
const updates = bookingsRepository.updateSchedulingFields.mock.calls.map((c) => c[1]);
expect(updates.some((u) => u.schedulingStatus === 'ELIGIBLE')).toBe(false);
});
});
describe('updateCheckpoint — leg time correction', () => {
const t = (h: number) => new Date(Date.UTC(2026, 0, 1, h));
const schedule = {