feat: enhance booking and scheduling features with shipping line support and improved search functionality

This commit is contained in:
Marshal
2026-08-16 08:18:40 +00:00
parent 9f53114778
commit 5ce5cbe29d
8 changed files with 225 additions and 75 deletions

View File

@@ -72,6 +72,8 @@ describe('SchedulingRescheduleService', () => {
previewTrainSchedule: jest.fn(),
unassignBooking: jest.fn(),
assignBookingsToSchedule: jest.fn(),
windowFieldsForNewDeparture: jest.fn().mockResolvedValue({}),
emitWindowState: jest.fn().mockResolvedValue(undefined),
};
schedulingRescheduleRepository = {
createEvent: jest.fn().mockResolvedValue({ id: 'event-1' }),
@@ -213,6 +215,13 @@ describe('SchedulingRescheduleService', () => {
});
trainSchedulesRepository.updateStatus.mockResolvedValue(undefined);
trainSchedulingService.assignBookingsToSchedule.mockResolvedValue({ id: 'sched-1' });
// An OPEN window's close must follow the new departure (this is the
// portal's "closes in" countdown) — the derived fields ride along with the
// date write.
const newCloses = new Date('2099-06-22T08:00:00.000Z');
trainSchedulingService.windowFieldsForNewDeparture.mockResolvedValue({
windowClosesAt: newCloses,
});
const result = await service.maintenanceReschedule(
'sched-1',
@@ -228,9 +237,16 @@ describe('SchedulingRescheduleService', () => {
expect(trainSchedulesRepository.updateStatus).toHaveBeenCalledWith(
'sched-1',
'DRAFT',
{ scheduledDepartureDate: new Date('2099-06-22T10:00:00.000Z') },
{
scheduledDepartureDate: new Date('2099-06-22T10:00:00.000Z'),
windowClosesAt: newCloses,
},
txManager,
);
expect(trainSchedulingService.windowFieldsForNewDeparture).toHaveBeenCalledWith(
schedule,
new Date('2099-06-22T10:00:00.000Z'),
);
expect(schedulingRescheduleRepository.createEvent).toHaveBeenCalledWith(
expect.objectContaining({
trigger: 'TRAIN_MAINTENANCE',

View File

@@ -227,17 +227,25 @@ export class SchedulingRescheduleService {
// through this manager without editing TrainSchedulingService. A failure
// between those steps and this block can still leave partial state; a human
// must finish the full cross-service transaction threading.
// The booking window must follow the new departure (an OPEN window's
// "closes in" countdown is capped at departure close offset; PRE_WINDOW /
// DONE re-derive their open/close). Same math as maintenanceReschedule.
const windowFields = newDeparture
? await this.trainSchedulingService.windowFieldsForNewDeparture(
schedule,
newDeparture,
)
: {};
await this.dataSource.transaction(async (manager) => {
if (newDeparture) {
// M7: raw write of scheduledDepartureDate. We deliberately do NOT
// delegate to TrainSchedulingService.updateScheduleDate, which only
// permits a date change while windowPhase === 'PRE_WINDOW' and would
// reject reschedules of already-open (SCHEDULED) trains. Consequence:
// the booking-window fields are NOT re-derived for the new date here.
// Raw write of scheduledDepartureDate: updateScheduleDate only permits a
// date change while windowPhase === 'PRE_WINDOW' and would reject
// reschedules of already-open (SCHEDULED) trains.
await this.trainSchedulesRepository.updateStatus(
scheduleId,
schedule.status as TrainScheduleStatus,
{ scheduledDepartureDate: newDeparture },
{ scheduledDepartureDate: newDeparture, ...windowFields },
manager,
);
}
@@ -263,6 +271,7 @@ export class SchedulingRescheduleService {
// `newDeparture` is null when the date was unchanged, so retained customers
// are not falsely told the train was rescheduled.
await this.notifyRescheduleOutcome(dto, newDeparture);
if (newDeparture) void this.trainSchedulingService.emitWindowState(scheduleId);
return { plan, schedule: assignResult };
}