diff --git a/apps/edr-freight-api/src/modules/scheduling-reschedule/scheduling-reschedule.service.ts b/apps/edr-freight-api/src/modules/scheduling-reschedule/scheduling-reschedule.service.ts index 7c41ee43a..d4cc405f4 100644 --- a/apps/edr-freight-api/src/modules/scheduling-reschedule/scheduling-reschedule.service.ts +++ b/apps/edr-freight-api/src/modules/scheduling-reschedule/scheduling-reschedule.service.ts @@ -270,7 +270,7 @@ export class SchedulingRescheduleService { // M12: only announce a new departure when the date actually moved — // `newDeparture` is null when the date was unchanged, so retained customers // are not falsely told the train was rescheduled. - await this.notifyRescheduleOutcome(dto, newDeparture); + await this.notifyRescheduleOutcome(scheduleId, dto, newDeparture); if (newDeparture) void this.trainSchedulingService.emitWindowState(scheduleId); return { plan, schedule: assignResult }; @@ -283,6 +283,7 @@ export class SchedulingRescheduleService { * company so the notifier has a phone/email to reach. */ private async notifyRescheduleOutcome( + scheduleId: string, dto: ExecuteRescheduleDto, newDeparture: Date | null, ): Promise { @@ -294,9 +295,9 @@ export class SchedulingRescheduleService { const booking = await this.loadBookingForNotify(bookingId); if (!booking) continue; if (isMaintenance) { - this.notifier.maintenanceMoved(booking, newDeparture); + this.notifier.maintenanceMoved(booking, newDeparture, scheduleId); } else { - this.notifier.rescheduled(booking, newDeparture); + this.notifier.rescheduled(booking, newDeparture, scheduleId); } } } @@ -307,7 +308,7 @@ export class SchedulingRescheduleService { for (const bookingId of dto.displacedBookingIds) { const booking = await this.loadBookingForNotify(bookingId); if (!booking) continue; - this.notifier.removedFromTrain(booking); + this.notifier.removedFromTrain(booking, scheduleId); } } } diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts index d787f737d..0ea941193 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts @@ -3448,7 +3448,7 @@ export class BookingBatchService implements OnModuleInit { schedule?.scheduledDepartureDate && eatDay(schedule.scheduledDepartureDate) !== previousDay ) { - this.notifier.allocatedOtherDay(fresh, schedule.scheduledDepartureDate); + this.notifier.allocatedOtherDay(fresh, schedule.scheduledDepartureDate, scheduleId); } } @@ -4148,7 +4148,7 @@ export class BookingBatchService implements OnModuleInit { if (reason === "no-capacity") { this.notifier.expiredNoCapacity(booking); } else { - this.notifier.expired(booking); + this.notifier.expired(booking, freedScheduleId); } this.logger.log( `[BATCH] EXPIRED ${booking.reference} — ` + @@ -4502,7 +4502,7 @@ export class BookingBatchService implements OnModuleInit { manager, ); }); - this.notifier.displaced(victim); + this.notifier.displaced(victim, scheduleId); budget.add(this.needFor(victim, wagonDims), victimLeg); // Displacing frees wagons the same way an expiry does — don't leave the // schedule stuck at FULL. diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.spec.ts new file mode 100644 index 000000000..644c2a687 --- /dev/null +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.spec.ts @@ -0,0 +1,87 @@ +import { BookingNotifierService } from './booking-notifier.service'; +import type { Booking } from '../bookings/entities/booking.entity'; + +/** + * Customers get told which train, by number. The trap is that half these + * notices fire AFTER the booking has been detached from its schedule (cancel, + * remove, expire) — `trainScheduleId` is already null there, so the caller has + * to hand the notifier the schedule it just pulled the booking off. + */ +describe('BookingNotifierService — train number in customer messages', () => { + const booking = (over: Partial = {}): Booking => + ({ + id: 'b-1', + reference: 'BKG-0001', + companyId: 'co-1', + trainScheduleId: 'sch-1', + ...over, + }) as Booking; + + let notifications: { directSend: jest.Mock }; + let inbox: { notify: jest.Mock }; + let trainSchedules: { findByIdWithStations: jest.Mock }; + let service: BookingNotifierService; + + const flush = () => new Promise((resolve) => setImmediate(resolve)); + const sms = () => notifications.directSend.mock.calls[0][2] as string; + + beforeEach(() => { + notifications = { directSend: jest.fn().mockResolvedValue(undefined) }; + inbox = { notify: jest.fn().mockResolvedValue(undefined) }; + trainSchedules = { + findByIdWithStations: jest.fn().mockResolvedValue({ + id: 'sch-1', + trainNumber: '1234', + reference: 'S-2026-00042', + scheduledDepartureDate: new Date('2026-09-01T06:00:00Z'), + }), + }; + service = new BookingNotifierService( + notifications as never, + inbox as never, + trainSchedules as never, + { + query: jest + .fn() + .mockResolvedValue([{ phone: '+251900000000', email: null }]), + } as never, + ); + }); + + it('names the train on a dispatch, resolved from the booking itself', async () => { + service.dispatched(booking(), 'Nagad', 'Indode'); + await flush(); + + expect(sms()).toContain('(train 1234)'); + expect(inbox.notify.mock.calls[0][0].body).toContain('(train 1234)'); + }); + + it('names the cancelled train even though the booking was already detached', async () => { + service.scheduleCancelled(booking({ trainScheduleId: null }), 'sch-1'); + await flush(); + + expect(trainSchedules.findByIdWithStations).toHaveBeenCalledWith('sch-1'); + expect(sms()).toContain('Train 1234 for booking BKG-0001 has been cancelled'); + }); + + it('falls back to the schedule reference until a train number is assigned', async () => { + trainSchedules.findByIdWithStations.mockResolvedValue({ + id: 'sch-1', + trainNumber: null, + reference: 'S-2026-00042', + }); + service.displaced(booking()); + await flush(); + + expect(sms()).toContain('(train S-2026-00042)'); + }); + + it('stays grammatical and leaks no id when the schedule cannot be loaded', async () => { + trainSchedules.findByIdWithStations.mockRejectedValue(new Error('db down')); + service.removedFromTrain(booking({ trainScheduleId: 'sch-1' })); + await flush(); + + expect(sms()).toContain('removed from its train during rescheduling'); + expect(sms()).not.toContain('sch-1'); + }); +}); diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.ts index 1c2d99401..7901ccb5c 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.ts @@ -30,8 +30,8 @@ export class BookingNotifierService { /** * Human-readable description of a train schedule for customer messages: - * reference (or train number) + route + departure date. Never leaks a UUID — - * falls back to a generic phrase when the schedule can't be loaded. + * train number (or schedule reference) + route + departure date. Never leaks + * a UUID — falls back to a generic phrase when the schedule can't be loaded. */ private async scheduleLabel(scheduleId?: string | null): Promise { const fallback = 'your selected train'; @@ -39,7 +39,7 @@ export class BookingNotifierService { try { const s = await this.trainSchedules.findByIdWithStations(scheduleId); if (!s) return fallback; - const ref = s.reference ?? s.trainNumber ?? null; + const ref = s.trainNumber ?? s.reference ?? null; const route = s.originStation?.label && s.destinationStation?.label ? ` (${s.originStation.label} → ${s.destinationStation.label})` @@ -56,6 +56,38 @@ export class BookingNotifierService { } } + /** + * The train_number customers and yards quote for a departure. Falls back to + * the schedule reference while the number is unassigned, and to null when the + * schedule cannot be loaded — never a UUID. + */ + private async trainNumber(scheduleId?: string | null): Promise { + if (!scheduleId) return null; + try { + const s = await this.trainSchedules.findByIdWithStations(scheduleId); + return s?.trainNumber ?? s?.reference ?? null; + } catch (err) { + this.logger.warn( + `trainNumber(${scheduleId}) failed: ${(err as Error).message}`, + ); + return null; + } + } + + /** + * ` (train 1234)` for splicing after a booking reference — empty when the + * train is unknown, so every message stays grammatical without a branch. + * + * Callers pass `scheduleId` explicitly wherever the booking has already been + * detached from its train by the time we notify (cancel, remove, expire): + * `b.trainScheduleId` is null there, and the whole point of those messages is + * to name the train the customer just lost. + */ + private async trainTag(b: Booking, scheduleId?: string | null): Promise { + const number = await this.trainNumber(scheduleId ?? b.trainScheduleId); + return number ? ` (train ${number})` : ''; + } + private ref(b: Booking): string { return `${b.reference}${b.isGovernment ? ' (gov)' : ''}`; } @@ -147,27 +179,44 @@ export class BookingNotifierService { } /** Train carrying the booking departed — dispatched origin → destination. */ - dispatched(b: Booking, origin: string | null, destination: string | null): void { - const msg = - `Your booking ${b.reference ?? b.id} has been dispatched` + - `${origin || destination ? ` from ${origin ?? '?'} to ${destination ?? '?'}` : ''}.`; - void this.notifyContact(b, msg, 'DISPATCHED'); - this.inApp(b, 'Shipment dispatched', msg); + dispatched( + b: Booking, + origin: string | null, + destination: string | null, + scheduleId?: string | null, + ): void { + void (async () => { + const train = await this.trainTag(b, scheduleId); + const msg = + `Your booking ${b.reference ?? b.id}${train} has been dispatched` + + `${origin || destination ? ` from ${origin ?? '?'} to ${destination ?? '?'}` : ''}.`; + void this.notifyContact(b, msg, 'DISPATCHED'); + this.inApp(b, 'Shipment dispatched', msg); + })(); } /** Train carrying the booking arrived at destination. */ - arrived(b: Booking, origin: string | null, destination: string | null): void { - const msg = - `Your booking ${b.reference ?? b.id} has arrived` + - `${destination ? ` at ${destination}` : ''}${origin ? ` (from ${origin})` : ''}.`; - void this.notifyContact(b, msg, 'ARRIVED'); - this.inApp(b, 'Shipment arrived', msg); + arrived( + b: Booking, + origin: string | null, + destination: string | null, + scheduleId?: string | null, + ): void { + void (async () => { + const train = await this.trainTag(b, scheduleId); + const msg = + `Your booking ${b.reference ?? b.id}${train} has arrived` + + `${destination ? ` at ${destination}` : ''}${origin ? ` (from ${origin})` : ''}.`; + void this.notifyContact(b, msg, 'ARRIVED'); + this.inApp(b, 'Shipment arrived', msg); + })(); } - async payNow(b: Booking, deadline: Date): Promise { + async payNow(b: Booking, deadline: Date, scheduleId?: string | null): Promise { const payMinutes = Math.max(1, Math.round((deadline.getTime() - Date.now()) / 60_000)); const eat = deadline.toLocaleString('en-GB', { timeZone: 'Africa/Addis_Ababa' }); - const msg = `Pay within ${payMinutes} minute${payMinutes === 1 ? '' : 's'} to secure train slot ${b.reference ?? b.id}. Deadline: ${eat} EAT.`; + const train = await this.trainTag(b, scheduleId); + const msg = `Pay within ${payMinutes} minute${payMinutes === 1 ? '' : 's'} to secure train slot ${b.reference ?? b.id}${train}. Deadline: ${eat} EAT.`; await this.notifyContact(b, msg, 'PAY NOW'); this.inApp(b, 'Payment window open', msg, { type: NotificationType.INVOICE_ISSUED, @@ -175,15 +224,20 @@ export class BookingNotifierService { } /** One warning shortly before the pay window closes (sent once per hold). */ - async payDeadlineApproaching(b: Booking, deadline: Date): Promise { + async payDeadlineApproaching( + b: Booking, + deadline: Date, + scheduleId?: string | null, + ): Promise { const minutesLeft = Math.max( 1, Math.round((deadline.getTime() - Date.now()) / 60_000), ); const eat = deadline.toLocaleString('en-GB', { timeZone: 'Africa/Addis_Ababa' }); + const train = await this.trainTag(b, scheduleId); const msg = `Payment reminder: about ${minutesLeft} minute${minutesLeft === 1 ? '' : 's'} left ` + - `to pay for booking ${b.reference ?? b.id}. Deadline: ${eat} EAT — ` + + `to pay for booking ${b.reference ?? b.id}${train}. Deadline: ${eat} EAT — ` + `unpaid reservations are released and the wagons go back on sale.`; await this.notifyContact(b, msg, 'PAY REMINDER'); // HIGH: minutes from losing the reserved wagons — must reach SMS/email. @@ -203,8 +257,10 @@ export class BookingNotifierService { deadline: Date, offeredWagons: number, totalWagons: number, + scheduleId?: string | null, ): Promise { const payMinutes = Math.max(1, Math.round((deadline.getTime() - Date.now()) / 60_000)); + const train = await this.trainTag(b, scheduleId); const eat = deadline.toLocaleString('en-GB', { timeZone: 'Africa/Addis_Ababa' }); const leftover = totalWagons - offeredWagons; // With auto-placement on, the leftover is booked FOR the customer on another @@ -215,7 +271,7 @@ export class BookingNotifierService { ? `The remaining ${leftover} will be booked for you on another train, with its own invoice. ` : `The remaining ${leftover} return${leftover === 1 ? 's' : ''} to your contract — book them yourself in a later window. `; const msg = - `Only ${offeredWagons} of ${totalWagons} wagons fit the train for booking ${b.reference ?? b.id}. ` + + `Only ${offeredWagons} of ${totalWagons} wagons fit the train for booking ${b.reference ?? b.id}${train}. ` + `Pay within ${payMinutes} minute${payMinutes === 1 ? '' : 's'} to accept and ship ${offeredWagons} wagon${offeredWagons === 1 ? '' : 's'} now. ` + leftoverCopy + `If you do not pay, the booking stays whole and you can rebook in the next window. Deadline: ${eat} EAT.`; @@ -234,15 +290,18 @@ export class BookingNotifierService { * are billed separately. Sent instead of leaving the customer to rebook. */ remainderPlaced(remainder: Booking, parentReference: string): void { - const msg = - `The wagons left over from booking ${parentReference} have been booked as ` + - `${remainder.reference ?? remainder.id} on another train. ` + - `It carries its own invoice — pay it to secure that slot.`; - void this.notifyContact(remainder, msg, 'REMAINDER BOOKED'); - this.inApp(remainder, 'Leftover wagons booked', msg, { - type: NotificationType.INVOICE_ISSUED, - priority: NotificationPriority.HIGH, - }); + void (async () => { + const number = await this.trainNumber(remainder.trainScheduleId); + const msg = + `The wagons left over from booking ${parentReference} have been booked as ` + + `${remainder.reference ?? remainder.id} on ${number ? `train ${number}` : 'another train'}. ` + + `It carries its own invoice — pay it to secure that slot.`; + void this.notifyContact(remainder, msg, 'REMAINDER BOOKED'); + this.inApp(remainder, 'Leftover wagons booked', msg, { + type: NotificationType.INVOICE_ISSUED, + priority: NotificationPriority.HIGH, + }); + })(); } secured( @@ -264,10 +323,13 @@ export class BookingNotifierService { })(); } - expired(b: Booking): void { - const msg = `Payment window expired for booking ${b.reference ?? b.id}. Reschedule or cancel — no re-approval needed.`; - void this.notifyContact(b, msg, 'EXPIRED'); - this.inApp(b, 'Payment window expired', msg); + expired(b: Booking, scheduleId?: string | null): void { + void (async () => { + const train = await this.trainTag(b, scheduleId); + const msg = `Payment window expired for booking ${b.reference ?? b.id}${train}. Reschedule or cancel — no re-approval needed.`; + void this.notifyContact(b, msg, 'EXPIRED'); + this.inApp(b, 'Payment window expired', msg); + })(); } /** @@ -303,21 +365,27 @@ export class BookingNotifierService { ); } - displaced(b: Booking): void { - const msg = `Booking ${b.reference ?? b.id} was displaced by a government booking. Move to another schedule or cancel.`; - void this.notifyContact(b, msg, 'DISPLACED'); - this.inApp(b, 'Booking displaced', msg); + displaced(b: Booking, scheduleId?: string | null): void { + void (async () => { + const train = await this.trainTag(b, scheduleId); + const msg = `Booking ${b.reference ?? b.id}${train} was displaced by a government booking. Move to another schedule or cancel.`; + void this.notifyContact(b, msg, 'DISPLACED'); + this.inApp(b, 'Booking displaced', msg); + })(); } /** * Staff rescheduled the train carrying this booking to a new departure date. * The booking stays on the train — only the date moved. */ - rescheduled(b: Booking, newDeparture: Date): void { - const when = newDeparture.toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE }); - const msg = `Booking ${b.reference ?? b.id} has been rescheduled. New departure date: ${when}.`; - void this.notifyContact(b, msg, 'RESCHEDULED'); - this.inApp(b, 'Booking rescheduled', msg); + rescheduled(b: Booking, newDeparture: Date, scheduleId?: string | null): void { + void (async () => { + const when = newDeparture.toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE }); + const train = await this.trainTag(b, scheduleId); + const msg = `Booking ${b.reference ?? b.id}${train} has been rescheduled. New departure date: ${when}.`; + void this.notifyContact(b, msg, 'RESCHEDULED'); + this.inApp(b, 'Booking rescheduled', msg); + })(); } /** @@ -325,49 +393,62 @@ export class BookingNotifierService { * the customer's original choice. In-app only — staff drove the change and * the allocation itself already notifies through the secured path. */ - allocatedOtherDay(b: Booking, newDeparture: Date): void { - const when = newDeparture.toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE }); - const msg = - `Booking ${b.reference ?? b.id} has been allocated to a train on a different date. ` + - `New departure date: ${when}.`; - this.inApp(b, 'Booking allocated to another date', msg); + allocatedOtherDay(b: Booking, newDeparture: Date, scheduleId?: string | null): void { + void (async () => { + const when = newDeparture.toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE }); + const train = await this.trainTag(b, scheduleId); + const msg = + `Booking ${b.reference ?? b.id}${train} has been allocated to a train on a different date. ` + + `New departure date: ${when}.`; + this.inApp(b, 'Booking allocated to another date', msg); + })(); } /** * Booking was removed from its train during a staff reschedule (not a government * pre-empt). It returns to eligible — the customer must rebook or reschedule. */ - removedFromTrain(b: Booking): void { - const msg = - `Booking ${b.reference ?? b.id} has been removed from its train during rescheduling. ` + - `Please rebook or select a new schedule from the portal.`; - void this.notifyContact(b, msg, 'REMOVED FROM TRAIN'); - this.inApp(b, 'Removed from train', msg); + removedFromTrain(b: Booking, scheduleId?: string | null): void { + void (async () => { + const number = await this.trainNumber(scheduleId ?? b.trainScheduleId); + const msg = + `Booking ${b.reference ?? b.id} has been removed from ` + + `${number ? `train ${number}` : 'its train'} during rescheduling. ` + + `Please rebook or select a new schedule from the portal.`; + void this.notifyContact(b, msg, 'REMOVED FROM TRAIN'); + this.inApp(b, 'Removed from train', msg); + })(); } /** * The train carrying this booking was cancelled. The booking is detached and * returns to the eligible pool — the customer must rebook or pick a new schedule. */ - scheduleCancelled(b: Booking): void { - const msg = - `The train for booking ${b.reference ?? b.id} has been cancelled. ` + - `Your booking is not lost — please rebook or select a new schedule from the portal.`; - void this.notifyContact(b, msg, 'TRAIN CANCELLED'); - // HIGH: a cancelled train invalidates the customer's plans — must reach SMS/email. - this.inApp(b, 'Train cancelled', msg, { priority: NotificationPriority.HIGH }); + scheduleCancelled(b: Booking, scheduleId?: string | null): void { + void (async () => { + const number = await this.trainNumber(scheduleId ?? b.trainScheduleId); + const msg = + `${number ? `Train ${number}` : 'The train'} for booking ${b.reference ?? b.id} has been cancelled. ` + + `Your booking is not lost — please rebook or select a new schedule from the portal.`; + void this.notifyContact(b, msg, 'TRAIN CANCELLED'); + // HIGH: a cancelled train invalidates the customer's plans — must reach SMS/email. + this.inApp(b, 'Train cancelled', msg, { priority: NotificationPriority.HIGH }); + })(); } /** * The train carrying this booking was moved for maintenance to a new departure * date. The booking stays on the train — only the date moved. */ - maintenanceMoved(b: Booking, newDeparture: Date): void { - const when = newDeparture.toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE }); - const msg = - `The train for booking ${b.reference ?? b.id} was rescheduled for maintenance. ` + - `New departure date: ${when}.`; - void this.notifyContact(b, msg, 'MAINTENANCE RESCHEDULE'); - this.inApp(b, 'Train maintenance reschedule', msg); + maintenanceMoved(b: Booking, newDeparture: Date, scheduleId?: string | null): void { + void (async () => { + const when = newDeparture.toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE }); + const number = await this.trainNumber(scheduleId ?? b.trainScheduleId); + const msg = + `${number ? `Train ${number}` : 'The train'} for booking ${b.reference ?? b.id} was rescheduled for maintenance. ` + + `New departure date: ${when}.`; + void this.notifyContact(b, msg, 'MAINTENANCE RESCHEDULE'); + this.inApp(b, 'Train maintenance reschedule', msg); + })(); } } diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-split.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-split.service.ts index ba4f08552..b04a6fd2b 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-split.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-split.service.ts @@ -199,7 +199,13 @@ export class BookingSplitService { status: 'OFFERED', }), ); - await this.notifier.payNowPartial(booking, deadline, sized.offeredWagons, sized.totalWagons); + await this.notifier.payNowPartial( + booking, + deadline, + sized.offeredWagons, + sized.totalWagons, + scheduleId, + ); return offer; } diff --git a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts index 858d14c90..8e6312402 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts @@ -442,8 +442,9 @@ export class TrainSchedulingService { relations: { company: true }, }); for (const b of bookings) { - if (event === 'dispatched') this.bookingNotifier.dispatched(b, origin, destination); - else this.bookingNotifier.arrived(b, origin, destination); + if (event === 'dispatched') + this.bookingNotifier.dispatched(b, origin, destination, schedule.id); + else this.bookingNotifier.arrived(b, origin, destination, schedule.id); } } catch (err) { this.logger.warn(`Failed to notify schedule bookings (${event}): ${(err as Error).message}`); @@ -1169,7 +1170,7 @@ export class TrainSchedulingService { }); for (const booking of allocatedBookings) { if (['CANCELLED', 'EXPIRED', 'REJECTED'].includes(booking.status)) continue; - this.bookingNotifier.rescheduled(booking, departure); + this.bookingNotifier.rescheduled(booking, departure, schedule.id); notifiedCount += 1; } } @@ -1374,7 +1375,7 @@ export class TrainSchedulingService { .getRepository(Booking) .update(aboard.map((b) => b.id), { scheduledDate: departure } as never); for (const booking of aboard) { - this.bookingNotifier.maintenanceMoved(booking, departure); + this.bookingNotifier.maintenanceMoved(booking, departure, id); } } @@ -2524,7 +2525,7 @@ export class TrainSchedulingService { .getRepository(Booking) .findOne({ where: { id: bookingId }, relations: { company: true } }); if (removedBooking && opts.notifyCustomer !== false) { - this.bookingNotifier.removedFromTrain(removedBooking); + this.bookingNotifier.removedFromTrain(removedBooking, scheduleId); } this.logger.log( `Booking ${bookingReference} removed from schedule ${scheduleId} by user ${userId ?? 'unknown'} — customer notified to reschedule or cancel.`, @@ -5592,7 +5593,7 @@ export class TrainSchedulingService { const booking = await this.bookingsRepository .findByIdWithFiles(sb.bookingId) .catch(() => null); - if (booking) this.bookingNotifier.scheduleCancelled(booking); + if (booking) this.bookingNotifier.scheduleCancelled(booking, schedule.id); } // Window retired (DONE) — remove the card from portal/GL lists right away.