fix(freight): gate loading on booking paymentStatus only; default schedule voyage no. to train's voyage number

This commit is contained in:
marshal
2026-09-02 21:15:48 +00:00
parent d51bed5630
commit 3e274cc2a2
30 changed files with 629 additions and 117 deletions

View File

@@ -442,8 +442,11 @@ 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);
} else {
this.bookingNotifier.arrived(b, origin, destination, schedule);
}
}
} catch (err) {
this.logger.warn(`Failed to notify schedule bookings (${event}): ${(err as Error).message}`);
@@ -1169,7 +1172,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);
notifiedCount += 1;
}
}
@@ -1374,7 +1377,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, schedule, dto.reason);
}
}
@@ -1871,6 +1874,10 @@ export class TrainSchedulingService {
status: TrainScheduleStatusEnum.Scheduled,
direction,
trainNumber: pairTrainNumber ?? undefined,
// Staff-entered at creation; the UI defaults it to the built train's
// own voyage number (Train.trainName). Fall back to the pair train
// number here only for non-UI callers that send none.
voyageNumber: dto.voyageNumber?.trim() || pairTrainNumber || null,
maxWagons,
plannedWagonYards,
reverseWagonOrder: dto.reverseWagonOrder ?? false,
@@ -2530,7 +2537,8 @@ export class TrainSchedulingService {
.getRepository(Booking)
.findOne({ where: { id: bookingId }, relations: { company: true } });
if (removedBooking && opts.notifyCustomer !== false) {
this.bookingNotifier.removedFromTrain(removedBooking);
// The booking's train_schedule_id is already cleared — name the run explicitly.
this.bookingNotifier.removedFromTrain(removedBooking, schedule);
}
this.logger.log(
`Booking ${bookingReference} removed from schedule ${scheduleId} by user ${userId ?? 'unknown'} — customer notified to reschedule or cancel.`,
@@ -3101,7 +3109,7 @@ export class TrainSchedulingService {
AND b.deleted_at IS NULL
AND b.origin_yard_id = $2
AND b.loaded_at IS NULL
AND (b.status = 'PAID' OR (b.is_government = true AND b.status = 'APPROVED'))
AND (b.payment_status = 'PAID' OR (b.is_government = true AND b.status = 'APPROVED'))
AND ($4::uuid[] IS NULL OR b.id = ANY($4::uuid[]))`,
[
scheduleId,
@@ -3318,7 +3326,7 @@ export class TrainSchedulingService {
AND b.loading_started_at IS NULL
AND COALESCE(tsb.loading_status, 'UNLOADED') <> 'LOADED'
AND b.is_government = false
AND (b.status = 'PAID'
AND (b.payment_status = 'PAID'
OR (b.shipping_line_company_id IS NOT NULL AND b.status = 'FULLY_EXECUTED'))`,
[scheduleId, originYardId],
);
@@ -3432,7 +3440,7 @@ export class TrainSchedulingService {
// milestone still counts as paid — the clearance views self-heal the row on
// read, and the gate pass must not lag behind that.
for (const booking of bookings) {
if (booking.paymentStatus === 'PAID' || booking.status === 'PAID') {
if (booking.paymentStatus === 'PAID') {
paidBookingIds.add(booking.id);
}
}
@@ -5667,7 +5675,8 @@ export class TrainSchedulingService {
const booking = await this.bookingsRepository
.findByIdWithFiles(sb.bookingId)
.catch(() => null);
if (booking) this.bookingNotifier.scheduleCancelled(booking);
// Detached above, so pass the cancelled schedule for its train/voyage numbers.
if (booking) this.bookingNotifier.scheduleCancelled(booking, schedule);
}
// Window retired (DONE) — remove the card from portal/GL lists right away.
@@ -10271,6 +10280,8 @@ export class TrainSchedulingService {
// without the wagons' tare. The legs tab shows this per booking.
cargoWeightTons: sb.booking ? bookingCargoTons(sb.booking) : 0,
status: sb.booking?.status ?? null,
// Loadability is decided by the payment status, not `status`.
paymentStatus: sb.booking?.paymentStatus ?? null,
schedulingStatus: sb.booking?.schedulingStatus ?? null,
freightType: sb.booking?.freightType ?? null,
// Which leg of the corridor this booking rides — the workspace can't
@@ -11584,7 +11595,11 @@ export class TrainSchedulingService {
return assignability.shortage;
}
/** Paid (or government) bookings that may be loaded onto wagons — excludes expired / awaiting payment. */
/**
* Paid (or government) bookings that may be loaded onto wagons — excludes
* expired / awaiting payment. "Paid" is read from the PAYMENT status only;
* the booking status is not a reliable payment signal.
*/
private isReadyToLoadBooking(booking: {
status: string;
paymentStatus?: string | null;
@@ -11594,7 +11609,7 @@ export class TrainSchedulingService {
if (booking.status === 'SELECTED_FOR_BATCH' || booking.status === 'AWAITING_PAYMENT') {
return false;
}
if (booking.status === 'PAID' || booking.paymentStatus === 'PAID') return true;
if (booking.paymentStatus === 'PAID') return true;
if (booking.isGovernment) return true;
return false;
}