mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 23:00:57 +00:00
enhance gate pass and freight payment handling in train scheduling
- Updated the logic in to ensure that a booking only earns its gate pass once the freight charges are settled. - Added logging for bookings that have not settled freight payment when securing gate passes. - Modified seeders to ensure that bookings have associated company profiles to prevent data inconsistencies. - Updated freight permissions to include new clearance actions for bookings. - Enhanced the UI to reflect changes in the clearance process, including new shipment request pages and improved status handling in the clearance action panel. - Adjusted the contract clearance list to accommodate both customs contracts and shipment bookings. - Improved the handling of GENERAL contracts in various components to ensure proper booking flow and visibility.
This commit is contained in:
@@ -630,4 +630,104 @@ describe('BookingBatchService — PAID reconcile', () => {
|
||||
expect(check({ ...importGeneral, contractKind: null } as Booking, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('settleDueReservations — expire then promote the waiting list', () => {
|
||||
const originYardId = 'yard-origin';
|
||||
const destinationYardId = 'yard-dest';
|
||||
const trainId = 'train-a';
|
||||
// 14m / 70t default wagon → two wagon slots on this locomotive.
|
||||
const smallLoco = { maxPullWeightTons: 200, maxTrainLengthMeters: 28 };
|
||||
|
||||
const booking = (id: string, priority: number, overrides = {}): Booking =>
|
||||
({
|
||||
id,
|
||||
reference: id,
|
||||
isGovernment: false,
|
||||
priorityScore: priority,
|
||||
status: 'FULLY_EXECUTED',
|
||||
wagonsRequired: 1,
|
||||
cargoTotalWeightVgm: 10,
|
||||
freightType: 'CONTAINER',
|
||||
bookingContainers: [],
|
||||
originYardId,
|
||||
destinationYardId,
|
||||
trainScheduleId: trainId,
|
||||
...overrides,
|
||||
}) as unknown as Booking;
|
||||
|
||||
beforeEach(() => {
|
||||
const scheduleRow = {
|
||||
id: trainId,
|
||||
maxWagons: 2,
|
||||
bookingWindowStatus: 'CLOSED',
|
||||
windowPhase: 'PAYMENT',
|
||||
direction: 'IMPORT',
|
||||
trainSetId: `set-${trainId}`,
|
||||
trainSet: { locomotive: smallLoco },
|
||||
scheduleBookings: [],
|
||||
scheduledDepartureDate: new Date('2026-06-20T06:00:00.000Z'),
|
||||
originStationId: originYardId,
|
||||
destinationStationId: destinationYardId,
|
||||
};
|
||||
trainSchedulesRepository.findAll.mockResolvedValue([{ ...scheduleRow }]);
|
||||
trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue(scheduleRow);
|
||||
trainSchedulesRepository.findById.mockResolvedValue({
|
||||
id: trainId,
|
||||
bookingWindowStatus: 'CLOSED',
|
||||
windowPhase: 'PAYMENT',
|
||||
scheduledDepartureDate: scheduleRow.scheduledDepartureDate,
|
||||
originStationId: originYardId,
|
||||
destinationStationId: destinationYardId,
|
||||
});
|
||||
});
|
||||
|
||||
it('promotes a waiting booking into the wagons an expired reservation frees', async () => {
|
||||
// One reservation whose pay window lapsed, and one booking on the waiting list.
|
||||
const lapsed = booking('lapsed', 50, {
|
||||
status: 'SELECTED_FOR_BATCH',
|
||||
paymentDeadline: new Date(Date.now() - 60_000),
|
||||
});
|
||||
const waiting = booking('waiting', 10, { trainScheduleId: null });
|
||||
|
||||
bookingsRepository.findReservedForSchedule
|
||||
.mockResolvedValueOnce([lapsed]) // settleReserved sees the lapsed one
|
||||
.mockResolvedValue([]); // afterwards nothing is reserved
|
||||
// The day pool the top-up draws from: only the waiting booking is eligible.
|
||||
bookingsRepository.findBatchPoolByCorridorDay
|
||||
.mockResolvedValueOnce([waiting])
|
||||
.mockResolvedValue([]);
|
||||
|
||||
await service.settleDueReservations(trainId);
|
||||
|
||||
// The lapsed reservation expired...
|
||||
expect(notifier.expired).toHaveBeenCalledTimes(1);
|
||||
expect((notifier.expired.mock.calls[0][0] as Booking).id).toBe('lapsed');
|
||||
// ...and the waiting booking was promoted in the SAME settle, not next cycle.
|
||||
expect(notifier.payNow).toHaveBeenCalledTimes(1);
|
||||
expect((notifier.payNow.mock.calls[0][0] as Booking).id).toBe('waiting');
|
||||
});
|
||||
|
||||
it('serialises concurrent settles so the same reservation is not settled twice', async () => {
|
||||
const lapsed = booking('lapsed', 50, {
|
||||
status: 'SELECTED_FOR_BATCH',
|
||||
paymentDeadline: new Date(Date.now() - 60_000),
|
||||
});
|
||||
// Both callers read the reservation; the lock must stop the second from
|
||||
// acting on rows the first already expired. (The PAYMENT transition and the
|
||||
// tick's overdue backstop do exactly this, in the same second.)
|
||||
let reads = 0;
|
||||
bookingsRepository.findReservedForSchedule.mockImplementation(() => {
|
||||
reads += 1;
|
||||
return Promise.resolve(reads === 1 ? [lapsed] : []);
|
||||
});
|
||||
bookingsRepository.findBatchPoolByCorridorDay.mockResolvedValue([]);
|
||||
|
||||
await Promise.all([
|
||||
service.settleDueReservations(trainId),
|
||||
service.settleDueReservations(trainId),
|
||||
]);
|
||||
|
||||
expect(notifier.expired).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user