mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
- Added parameter to and for server-side free-text search on contract reference, company name, and booking details. - Introduced new validation errors in for container clashes and space issues when creating bookings. - Implemented paginated dropdown settings retrieval in . - Updated to fetch active yards using a new method that handles pagination. - Enhanced with a method to fetch all records by walking through pages. - Refactored to support filtering and pagination in schedule listings. - Improved to return a paginated list of facilities. - Updated UI components in and to utilize debounced search inputs for better performance. - Added alerts in to inform users about booking constraints related to splits and capacity. - Enhanced to display notifications for split bookings and capacity usage.
141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
import { ConflictException } from '@nestjs/common';
|
|
|
|
import { BookingBatchService } from './booking-batch.service';
|
|
import { Booking } from '../bookings/entities/booking.entity';
|
|
|
|
/**
|
|
* Export whole-booking single-train gate: an export booking never splits — it
|
|
* rides one train whole or is rejected. The report must try every fillable
|
|
* train on the day (first full → use the second), and when none fits, say how
|
|
* much space is still bookable so the customer knows what he CAN book.
|
|
*/
|
|
describe('BookingBatchService — exportSpaceReport (whole-booking, single train)', () => {
|
|
const DAY = new Date('2026-07-20T10:00:00Z');
|
|
|
|
const schedule = (id: string) => ({
|
|
id,
|
|
status: 'SCHEDULED',
|
|
direction: 'EXPORT',
|
|
scheduledDepartureDate: DAY,
|
|
bookingWindowStatus: 'OPEN',
|
|
windowPhase: null, // legacy gate: OPEN alone makes it fillable
|
|
});
|
|
|
|
const fullGraph = (id: string) => ({
|
|
id,
|
|
originStationId: 'yard-a',
|
|
destinationStationId: 'yard-b',
|
|
routeId: null, // legacy two-stop pseudo-route — no milestone query
|
|
scheduleBookings: [],
|
|
trainSet: {
|
|
locomotive: {
|
|
maxPullWeightTons: 500,
|
|
maxTrainLengthMeters: 140,
|
|
overageToleranceTons: 0,
|
|
overageToleranceMeters: 0,
|
|
},
|
|
},
|
|
});
|
|
|
|
const exportBooking = (cargoTons: number) =>
|
|
({
|
|
id: 'bk-exp',
|
|
freightType: 'BULK',
|
|
tradeDirection: 'EXPORT',
|
|
scheduledDate: DAY,
|
|
originYardId: 'yard-a',
|
|
destinationYardId: 'yard-b',
|
|
cargoTotalWeightVgm: cargoTons,
|
|
bookingContainers: [],
|
|
}) as unknown as Booking;
|
|
|
|
// A reserved bulk booking heavy enough to exhaust the 500t pull budget.
|
|
const heavyReserved = {
|
|
id: 'bk-heavy',
|
|
freightType: 'BULK',
|
|
originYardId: 'yard-a',
|
|
destinationYardId: 'yard-b',
|
|
cargoTotalWeightVgm: 476,
|
|
bookingContainers: [],
|
|
} as unknown as Booking;
|
|
|
|
let service: BookingBatchService;
|
|
let trainSchedulesRepository: {
|
|
findAll: jest.Mock;
|
|
findByIdWithFullGraph: jest.Mock;
|
|
findById: jest.Mock;
|
|
};
|
|
let bookingsRepository: { findReservedForSchedule: jest.Mock };
|
|
|
|
beforeEach(() => {
|
|
trainSchedulesRepository = {
|
|
findAll: jest.fn().mockResolvedValue([]),
|
|
findByIdWithFullGraph: jest
|
|
.fn()
|
|
.mockImplementation(async (id: string) => fullGraph(id)),
|
|
findById: jest.fn(),
|
|
};
|
|
bookingsRepository = { findReservedForSchedule: jest.fn().mockResolvedValue([]) };
|
|
|
|
// WagonType.find() → [] so representative default dims apply (bulk 60t
|
|
// payload / 23.4t tare / 14m); RouteMilestone is never queried (routeId null).
|
|
const genericRepo = { find: jest.fn().mockResolvedValue([]) };
|
|
const dataSource = { getRepository: jest.fn().mockReturnValue(genericRepo) };
|
|
|
|
service = new BookingBatchService(
|
|
dataSource as never,
|
|
bookingsRepository as never,
|
|
trainSchedulesRepository as never,
|
|
{} as never, // trainScheduleBookingsRepository
|
|
{} as never, // notifier
|
|
{} as never, // scheduler
|
|
{} as never, // trainSchedulingService
|
|
{} as never, // billing
|
|
{} as never, // bookingWindowGateway
|
|
{} as never, // pricingService
|
|
);
|
|
});
|
|
|
|
it('uses the other train when the first one is full', async () => {
|
|
trainSchedulesRepository.findAll.mockResolvedValue([
|
|
schedule('train-1'),
|
|
schedule('train-2'),
|
|
]);
|
|
bookingsRepository.findReservedForSchedule.mockImplementation(
|
|
async (id: string) => (id === 'train-1' ? [heavyReserved] : []),
|
|
);
|
|
|
|
const report = await service.exportSpaceReport(exportBooking(60));
|
|
|
|
expect(report.scheduleId).toBe('train-2');
|
|
});
|
|
|
|
it('rejects a booking no single train fits and reports the bookable space', async () => {
|
|
trainSchedulesRepository.findAll.mockResolvedValue([schedule('train-1')]);
|
|
|
|
const report = await service.exportSpaceReport(exportBooking(900));
|
|
|
|
expect(report.scheduleId).toBeNull();
|
|
expect(report.bestAvailable).not.toBeNull();
|
|
expect(report.bestAvailable!.cargoTons).toBeGreaterThan(0);
|
|
expect(report.bestAvailable!.cargoTons).toBeLessThan(900);
|
|
expect(report.fullMessage).toMatch(/largest remaining space is about .* tons/);
|
|
expect(report.fullMessage).toMatch(/single train whole/);
|
|
|
|
await expect(service.pickExportSchedule(exportBooking(900))).rejects.toThrow(
|
|
ConflictException,
|
|
);
|
|
});
|
|
|
|
it('says no train is accepting bookings when the day has none', async () => {
|
|
trainSchedulesRepository.findAll.mockResolvedValue([]);
|
|
|
|
const report = await service.exportSpaceReport(exportBooking(60));
|
|
|
|
expect(report.scheduleId).toBeNull();
|
|
expect(report.fullMessage).toBe(
|
|
'No export train is accepting bookings for this day',
|
|
);
|
|
});
|
|
});
|