mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix: test
This commit is contained in:
@@ -80,7 +80,12 @@ const makeBooking = (
|
||||
|
||||
describe('TrainSchedulingService', () => {
|
||||
let service: TrainSchedulingService;
|
||||
let dataSource: { getRepository: jest.Mock; transaction: jest.Mock; query: jest.Mock };
|
||||
let dataSource: {
|
||||
getRepository: jest.Mock;
|
||||
transaction: jest.Mock;
|
||||
query: jest.Mock;
|
||||
manager: { getRepository: jest.Mock };
|
||||
};
|
||||
let bookingsRepository: Record<string, jest.Mock>;
|
||||
let locomotivesRepository: { findById: jest.Mock; findAll: jest.Mock };
|
||||
let wagonTypesRepository: { findAll: jest.Mock };
|
||||
@@ -91,11 +96,23 @@ describe('TrainSchedulingService', () => {
|
||||
let wagonAllocationBulkLoadsRepository: Record<string, jest.Mock>;
|
||||
|
||||
beforeEach(() => {
|
||||
// findGroupSiblings runs a query builder off dataSource.manager; default it
|
||||
// to "no sibling schedules" so isolated unit tests don't need to wire it.
|
||||
const emptySiblingQb = {
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
dataSource = {
|
||||
getRepository: jest.fn(),
|
||||
transaction: jest.fn(),
|
||||
// Raw-SQL helper lookups (e.g. builtTrainIdOfSchedule) default to "no rows".
|
||||
query: jest.fn().mockResolvedValue([]),
|
||||
manager: {
|
||||
getRepository: jest.fn(() => ({
|
||||
createQueryBuilder: jest.fn(() => emptySiblingQb),
|
||||
})),
|
||||
},
|
||||
};
|
||||
bookingsRepository = {
|
||||
findEligibleForScheduling: jest.fn(),
|
||||
@@ -110,9 +127,11 @@ describe('TrainSchedulingService', () => {
|
||||
findByIdWithFullGraph: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
maxReferenceSequence: jest.fn().mockResolvedValue(0),
|
||||
};
|
||||
trainScheduleBookingsRepository = {
|
||||
findByBookingIds: jest.fn(),
|
||||
findByScheduleId: jest.fn().mockResolvedValue([]),
|
||||
createMany: jest.fn(),
|
||||
deleteByScheduleAndBooking: jest.fn(),
|
||||
};
|
||||
@@ -327,7 +346,11 @@ describe('TrainSchedulingService', () => {
|
||||
});
|
||||
|
||||
it('allows preview when bookings are already on the target schedule', async () => {
|
||||
const bookings = [makeBooking('b1', 'BKG-CONT-001', 500, 20, '40FT', 20)];
|
||||
// A booking already pinned to the target schedule is exempt from the
|
||||
// corridor/day/status gates — mark it so on the entity, matching the link row.
|
||||
const bookings = [
|
||||
{ ...makeBooking('b1', 'BKG-CONT-001', 500, 20, '40FT', 20), trainScheduleId: 'sched-target' },
|
||||
];
|
||||
|
||||
wagonTypesRepository.findAll.mockResolvedValue([nw5]);
|
||||
bookingsRepository.findByIdsForScheduling.mockResolvedValue(bookings);
|
||||
@@ -354,7 +377,10 @@ describe('TrainSchedulingService', () => {
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('allows preview when selected bookings are on different schedule dates', async () => {
|
||||
it('flags a booking scheduled for a different day than the train departure', async () => {
|
||||
// The old cross-booking "must share the same schedule date" rule is gone;
|
||||
// the live rule is that every booking must match the departure day. b2
|
||||
// departs a day later, so it's the one flagged.
|
||||
const bookings = [
|
||||
makeBooking('b1', 'BKG-CONT-001', 500, 20, '40FT', 20, '2026-06-20T08:00:00.000Z'),
|
||||
makeBooking('b2', 'BKG-CONT-002', 300, 10, '20FT', 10, '2026-06-21T14:00:00.000Z'),
|
||||
@@ -375,7 +401,8 @@ describe('TrainSchedulingService', () => {
|
||||
expect(result.violations).not.toContain(
|
||||
'Selected bookings must share the same schedule date',
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.violations.some((v) => v.includes('different day'))).toBe(true);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects bookings that are not in schedulable status', async () => {
|
||||
@@ -408,6 +435,8 @@ describe('TrainSchedulingService', () => {
|
||||
originYardId: 'yard-origin',
|
||||
destinationYardId: 'yard-destination',
|
||||
isActive: true,
|
||||
status: 'AVAILABLE',
|
||||
direction: 'IMPORT',
|
||||
};
|
||||
|
||||
const locomotive2 = { ...locomotive, id: 'loc-2', code: 'LOC-002' };
|
||||
@@ -421,6 +450,12 @@ describe('TrainSchedulingService', () => {
|
||||
const trainScheduleRepo = {
|
||||
create: jest.fn().mockImplementation((value) => value),
|
||||
save: jest.fn().mockResolvedValue({ id: 'schedule-1' }),
|
||||
// findGroupWindowAnchor looks for same-day sibling schedules; none here.
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn().mockResolvedValue([]),
|
||||
})),
|
||||
};
|
||||
const trainSetRepo = {
|
||||
create: jest.fn().mockImplementation((value) => value),
|
||||
@@ -464,19 +499,21 @@ describe('TrainSchedulingService', () => {
|
||||
callback(manager),
|
||||
);
|
||||
|
||||
// Departure must clear the import lead window (≥ importWindowLeadDays ahead
|
||||
// of now), so use a comfortably-future date rather than a hardcoded one.
|
||||
const futureDeparture = new Date(Date.now() + 10 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const result = await service.createContainerTrainSchedule({
|
||||
routeId: 'route-1',
|
||||
scheduleDate: '2026-06-20T08:00:00.000Z',
|
||||
scheduleDate: futureDeparture,
|
||||
locomotiveIds: ['loc-1', 'loc-2'],
|
||||
});
|
||||
|
||||
expect(trainSetRepo.save).toHaveBeenCalled();
|
||||
expect(trainScheduleRepo.save).toHaveBeenCalled();
|
||||
expect(trainSetLocomotiveRepo.save).toHaveBeenCalled();
|
||||
expect(lockedLocomotiveRepo.update).toHaveBeenCalledWith(
|
||||
{ id: expect.objectContaining({ _type: 'in', _value: ['loc-1', 'loc-2'] }) },
|
||||
{ status: 'ASSIGNED' },
|
||||
);
|
||||
// Advance scheduling locks locomotives but does NOT flip them to ASSIGNED —
|
||||
// one locomotive may sit on several future schedules.
|
||||
expect(lockedLocomotiveRepo.update).not.toHaveBeenCalled();
|
||||
expect(result.id).toBe('schedule-1');
|
||||
});
|
||||
|
||||
@@ -492,7 +529,7 @@ describe('TrainSchedulingService', () => {
|
||||
destinationYardId: 'yard-destination',
|
||||
status: 'PAID',
|
||||
bookingContainers: [],
|
||||
cargoType: { code: 'COFFEE' },
|
||||
cargoType: { id: 'cargo-coffee', code: 'COFFEE', wagonTypes: [cw3] },
|
||||
};
|
||||
|
||||
wagonTypesRepository.findAll.mockImplementation(async ({ where }: { where?: { code?: string } }) => {
|
||||
@@ -511,7 +548,9 @@ describe('TrainSchedulingService', () => {
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.summary.wagonType).toBe('MIXED');
|
||||
// Mixed freight now labels the summary by the concrete wagon type codes it uses.
|
||||
expect(result.summary.wagonType).toContain('NW5');
|
||||
expect(result.summary.wagonType).toContain('CW3');
|
||||
expect(result.wagonPlan.length).toBeGreaterThan(2);
|
||||
expect(result.containerUnits).toHaveLength(2);
|
||||
});
|
||||
@@ -536,9 +575,11 @@ describe('TrainSchedulingService', () => {
|
||||
});
|
||||
|
||||
it('rejects create when the locked locomotive is no longer available', async () => {
|
||||
// Advance scheduling only hard-blocks OUT_OF_SERVICE locomotives; other
|
||||
// non-AVAILABLE states (e.g. ASSIGNED) downgrade to a warning.
|
||||
const manager = {
|
||||
getRepository: jest.fn(() => ({
|
||||
findOne: jest.fn().mockResolvedValue({ ...locomotive, status: 'ASSIGNED' }),
|
||||
findOne: jest.fn().mockResolvedValue({ ...locomotive, status: 'OUT_OF_SERVICE' }),
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -551,6 +592,8 @@ describe('TrainSchedulingService', () => {
|
||||
originYardId: 'yard-origin',
|
||||
destinationYardId: 'yard-destination',
|
||||
isActive: true,
|
||||
status: 'AVAILABLE',
|
||||
direction: 'IMPORT',
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -907,7 +950,7 @@ describe('TrainSchedulingService', () => {
|
||||
});
|
||||
|
||||
describe('getAvailableLocomotivesForRoute', () => {
|
||||
it('returns locomotives at the route origin yard', async () => {
|
||||
it('returns every in-service locomotive, annotated with origin-yard presence', async () => {
|
||||
const routeId = 'route-export';
|
||||
const originYardId = 'yard-addis';
|
||||
const routeRepo = {
|
||||
@@ -915,6 +958,7 @@ describe('TrainSchedulingService', () => {
|
||||
id: routeId,
|
||||
name: 'Addis → Djibouti',
|
||||
isActive: true,
|
||||
status: 'AVAILABLE',
|
||||
originYardId,
|
||||
originYard: { country: 'Ethiopia' },
|
||||
destinationYard: { country: 'Djibouti' },
|
||||
@@ -924,21 +968,21 @@ describe('TrainSchedulingService', () => {
|
||||
if ((entity as { name?: string })?.name === 'Route') return routeRepo;
|
||||
return { findOne: jest.fn(), update: jest.fn() };
|
||||
});
|
||||
// Advance-scheduling picker: nothing is filtered by yard — every in-service
|
||||
// locomotive is returned and annotated with whether it's at the origin yet.
|
||||
locomotivesRepository.findAll.mockResolvedValue([
|
||||
{ id: 'l2', code: 'EXP', status: 'AVAILABLE', currentYardId: originYardId },
|
||||
{ id: 'l3', code: 'FAR', status: 'ASSIGNED', currentYardId: 'yard-elsewhere' },
|
||||
]);
|
||||
|
||||
const result = await service.getAvailableLocomotivesForRoute(routeId);
|
||||
|
||||
expect(locomotivesRepository.findAll).toHaveBeenCalledWith({
|
||||
where: { status: 'AVAILABLE', currentYardId: originYardId },
|
||||
order: { code: 'ASC' },
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].code).toBe('EXP');
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.find((l) => l.code === 'EXP')?.atOriginYard).toBe(true);
|
||||
expect(result.find((l) => l.code === 'FAR')?.atOriginYard).toBe(false);
|
||||
});
|
||||
|
||||
it('returns all locomotives returned by the repository for domestic routes', async () => {
|
||||
it('rejects intercity (domestic) routes — intercity scheduling is not offered', async () => {
|
||||
const routeId = 'route-domestic';
|
||||
const originYardId = 'yard-addis';
|
||||
const routeRepo = {
|
||||
@@ -946,6 +990,7 @@ describe('TrainSchedulingService', () => {
|
||||
id: routeId,
|
||||
name: 'Addis → Dire Dawa',
|
||||
isActive: true,
|
||||
status: 'AVAILABLE',
|
||||
originYardId,
|
||||
originYard: { country: 'Ethiopia' },
|
||||
destinationYard: { country: 'Ethiopia' },
|
||||
@@ -955,14 +1000,10 @@ describe('TrainSchedulingService', () => {
|
||||
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', currentYardId: originYardId },
|
||||
{ id: 'l2', code: 'EXP', status: 'AVAILABLE', currentYardId: originYardId },
|
||||
]);
|
||||
|
||||
const result = await service.getAvailableLocomotivesForRoute(routeId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
await expect(
|
||||
service.getAvailableLocomotivesForRoute(routeId),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user