This commit is contained in:
Marshal
2026-07-07 18:40:32 +00:00
parent b57840c907
commit 8addfdf3e2
7 changed files with 529 additions and 74 deletions

View File

@@ -22,6 +22,7 @@ describe('BookingBatchService — PAID reconcile', () => {
findBatchPool: jest.Mock;
findBatchPoolByRouteDay: jest.Mock;
findBatchPoolByCorridorDay: jest.Mock;
findUnacceptedForRouteDay: jest.Mock;
findReservedForSchedule: jest.Mock;
update: jest.Mock;
};
@@ -55,6 +56,7 @@ describe('BookingBatchService — PAID reconcile', () => {
findBatchPool: jest.fn().mockResolvedValue([]),
findBatchPoolByRouteDay: jest.fn().mockResolvedValue([]),
findBatchPoolByCorridorDay: jest.fn().mockResolvedValue([]),
findUnacceptedForRouteDay: jest.fn().mockResolvedValue([]),
findReservedForSchedule: jest.fn().mockResolvedValue([]),
update: jest.fn().mockResolvedValue(undefined),
};
@@ -332,4 +334,177 @@ describe('BookingBatchService — PAID reconcile', () => {
expect(notifier.payNow).not.toHaveBeenCalled();
});
});
describe('expireUnacceptedForRouteDay — doc-review sweep', () => {
const originYardId = 'yard-origin';
const destinationYardId = 'yard-dest';
const day = '2026-06-20';
const pendingBooking = {
id: 'pending-1',
reference: 'BK-PENDING-1',
status: 'OPERATION_REQUEST_PENDING',
isGovernment: false,
originYardId,
destinationYardId,
} as unknown as Booking;
beforeEach(() => {
// One fillable schedule on this corridor/day so corridorYardsForRouteDay
// resolves a non-empty yard set (legacy two-stop route → [origin, dest]).
trainSchedulesRepository.findAll.mockResolvedValue([
{
id: 'sched-1',
originStationId: originYardId,
destinationStationId: destinationYardId,
scheduledDepartureDate: new Date('2026-06-20T06:00:00.000Z'),
},
]);
});
it('expires each un-accepted booking and clears its scheduled day', async () => {
bookingsRepository.findUnacceptedForRouteDay.mockResolvedValue([pendingBooking]);
await service.expireUnacceptedForRouteDay({
originYardId,
destinationYardId,
day,
});
expect(bookingsRepository.findUnacceptedForRouteDay).toHaveBeenCalledWith(
expect.arrayContaining([originYardId, destinationYardId]),
day,
);
expect(bookingsRepository.update).toHaveBeenCalledWith(
'pending-1',
expect.objectContaining({
status: 'EXPIRED',
schedulingStatus: 'ELIGIBLE',
scheduledDate: null,
}),
);
expect(notifier.expired).toHaveBeenCalledWith(pendingBooking);
});
it('is a no-op when nothing is un-accepted', async () => {
bookingsRepository.findUnacceptedForRouteDay.mockResolvedValue([]);
await service.expireUnacceptedForRouteDay({
originYardId,
destinationYardId,
day,
});
expect(bookingsRepository.update).not.toHaveBeenCalled();
expect(notifier.expired).not.toHaveBeenCalled();
});
it('does nothing when the route-day has no fillable schedule', async () => {
trainSchedulesRepository.findAll.mockResolvedValue([]);
await service.expireUnacceptedForRouteDay({
originYardId,
destinationYardId,
day,
});
expect(bookingsRepository.findUnacceptedForRouteDay).not.toHaveBeenCalled();
});
});
describe('maybeOfferPartial — split-eligibility gate', () => {
const importGeneral = {
id: 'b1',
reference: 'b1',
isGovernment: false,
tradeDirection: 'IMPORT',
contractKind: 'GENERAL',
consolidationPartnerId: null,
} as unknown as Booking;
const call = (booking: Booking, isPair: boolean): boolean =>
(
service as unknown as {
isSplitEligible: (b: Booking, p: boolean) => boolean;
}
).isSplitEligible(booking, isPair);
it('allows IMPORT + GENERAL when splitService is present', () => {
const withSplit = new BookingBatchService(
dataSource as never,
bookingsRepository as never,
trainSchedulesRepository as never,
trainScheduleBookingsRepository as never,
notifier as never,
{ addTimeout: jest.fn(), deleteTimeout: jest.fn(), doesExist: jest.fn() } as never,
trainSchedulingService as never,
{ syncPayableDueDate: jest.fn(), expirePayable: jest.fn() } as never,
{ emitPhase: jest.fn() } as never,
undefined,
{ findOpenOffer: jest.fn() } as never,
);
const eligible = (
withSplit as unknown as {
isSplitEligible: (b: Booking, p: boolean) => boolean;
}
).isSplitEligible(importGeneral, false);
expect(eligible).toBe(true);
});
it('allows IMPORT + ONE_TIME (promoted to GENERAL on split)', () => {
const withSplit = new BookingBatchService(
dataSource as never,
bookingsRepository as never,
trainSchedulesRepository as never,
trainScheduleBookingsRepository as never,
notifier as never,
{ addTimeout: jest.fn(), deleteTimeout: jest.fn(), doesExist: jest.fn() } as never,
trainSchedulingService as never,
{ syncPayableDueDate: jest.fn(), expirePayable: jest.fn() } as never,
{ emitPhase: jest.fn() } as never,
undefined,
{ findOpenOffer: jest.fn() } as never,
);
const eligible = (
withSplit as unknown as {
isSplitEligible: (b: Booking, p: boolean) => boolean;
}
).isSplitEligible(
{ ...importGeneral, contractKind: 'ONE_TIME' } as Booking,
false,
);
expect(eligible).toBe(true);
});
it('rejects when splitService is absent (default test service)', () => {
// `service` from the outer beforeEach was built without a splitService.
expect(call(importGeneral, false)).toBe(false);
});
it('rejects EXPORT, government, consolidated pairs, and other directions', () => {
const withSplit = new BookingBatchService(
dataSource as never,
bookingsRepository as never,
trainSchedulesRepository as never,
trainScheduleBookingsRepository as never,
notifier as never,
{ addTimeout: jest.fn(), deleteTimeout: jest.fn(), doesExist: jest.fn() } as never,
trainSchedulingService as never,
{ syncPayableDueDate: jest.fn(), expirePayable: jest.fn() } as never,
{ emitPhase: jest.fn() } as never,
undefined,
{ findOpenOffer: jest.fn() } as never,
);
const check = (
withSplit as unknown as {
isSplitEligible: (b: Booking, p: boolean) => boolean;
}
).isSplitEligible.bind(withSplit);
expect(check({ ...importGeneral, tradeDirection: 'EXPORT' } as Booking, false)).toBe(false);
expect(check({ ...importGeneral, isGovernment: true } as Booking, false)).toBe(false);
expect(check(importGeneral, true)).toBe(false); // consolidated pair
expect(check({ ...importGeneral, contractKind: null } as Booking, false)).toBe(false);
});
});
});