From 3a298b5fceacc070ad09305840414dc3d3c32270 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Sat, 29 Aug 2026 08:04:16 +0000 Subject: [PATCH] feat(notifications): name the train in booking notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every customer notice about a train said "your train" or nothing at all — the customer had no way to tell which departure a dispatch, pay window, cancellation or reschedule referred to. Only secured() named one, and it quoted the schedule reference (S-YYYY-NNNNN) rather than the train number that yards and customs actually use. Adds trainNumber()/trainTag() beside the existing scheduleLabel(), reusing the TrainSchedulesRepository already injected, and threads the number through dispatched, arrived, payNow, payDeadlineApproaching, payNowPartial, remainderPlaced, expired, displaced, rescheduled, allocatedOtherDay, removedFromTrain, scheduleCancelled and maintenanceMoved. scheduleLabel now prefers train_number over the schedule reference. Falls back to the reference while a number is unassigned, and to the previous wording when the schedule cannot be loaded — never a UUID. Several of these fire after the booking has been detached from its schedule (cancel, remove, expire all clear train_schedule_id before the notice goes out), so each method takes an optional trailing scheduleId and those callers pass the schedule the booking was just pulled off. Sync signatures are preserved with the void-async wrapper secured() already used, so no call site changes beyond the extra argument. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01A481tjLb6zEkLnk4c4wtVR --- .../scheduling-reschedule.service.ts | 9 +- .../train-scheduling/booking-batch.service.ts | 6 +- .../booking-notifier.service.spec.ts | 87 +++++++ .../booking-notifier.service.ts | 217 ++++++++++++------ .../train-scheduling/booking-split.service.ts | 8 +- .../services/train-scheduling.service.ts | 13 +- 6 files changed, 258 insertions(+), 82 deletions(-) create mode 100644 apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.spec.ts 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.