enhance booking and contract notification systems

- Added detailed logging for socket connection events in useBookingWindowSocket.
- Introduced new notification types for contract status and schedule updates.
- Updated notification visuals to include new icons for contract status.
- Enhanced notification href resolution for contract status and schedule updates.
- Implemented booking lifecycle notifier service for customer and staff notifications.
- Created contract notifier service for managing contract lifecycle notifications.
- Added end-to-end tests for booking window socket functionality.
This commit is contained in:
Marshal
2026-07-06 21:03:08 +00:00
parent 8bd00ea78a
commit 05b13e84a8
38 changed files with 1450 additions and 95 deletions

View File

@@ -12,6 +12,7 @@ import {
Injectable,
Logger,
NotFoundException,
Optional,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectDataSource } from '@nestjs/typeorm';
@@ -21,6 +22,7 @@ import { BookingsRepository } from '../bookings/bookings.repository';
import { Booking } from '../bookings/entities/booking.entity';
import { BookingContainer } from '../bookings/entities/booking-container.entity';
import { ClearanceMilestone } from '../contracts/entities/clearance-milestone.entity';
import { ClearanceMilestoneService } from '../contracts/clearance-milestone.service';
import { Container } from '../container-management/entities/container.entity';
import { Locomotive } from '../locomotives/entities/locomotive.entity';
import { LocomotivesRepository } from '../locomotives/locomotives.repository';
@@ -274,9 +276,45 @@ export class TrainSchedulingService {
private readonly warehouseInventoryService: WarehouseInventoryService,
private readonly pdfDocuments: WarehouseReleaseDocumentService,
private readonly bookingWindowGateway: BookingWindowGateway,
@Optional() private readonly milestoneService?: ClearanceMilestoneService,
private readonly configService?: ConfigService,
) {}
/**
* Complete customer-tracking clearance milestones for every booking on a
* schedule when a physical lifecycle event fires (dispatch, arrive, load,
* unload, gatepass). Uses the doc-trigger path, which is a silent no-op for
* bookings without milestone rows (non-customs bookings), so this is safe to
* call for every direction and flow. Never blocks the operational action.
*/
private async completeMilestonesForScheduleBookings(
scheduleId: string,
codes: string[],
): Promise<void> {
if (!this.milestoneService || codes.length === 0) return;
try {
const rows: Array<{ booking_id: string }> = await this.dataSource.query(
`SELECT tsb.booking_id
FROM freight.train_schedule_bookings tsb
WHERE tsb.train_schedule_id = $1
AND tsb.deleted_at IS NULL`,
[scheduleId],
);
for (const { booking_id } of rows) {
for (const code of codes) {
await this.milestoneService.completeByDocTrigger(
{ bookingId: booking_id },
code,
);
}
}
} catch (err) {
this.logger.warn(
`Milestone completion (${codes.join(', ')}) failed for schedule ${scheduleId}: ${(err as Error).message}`,
);
}
}
/**
* Push a schedule's current booking-window state over the socket so the
* portal home card and backoffice GL/batch views update in real time —
@@ -1121,26 +1159,32 @@ export class TrainSchedulingService {
{ country: schedule.destinationCountry },
);
if (direction === 'IMPORT') {
const result = await this.warehouseInventoryService.autoUnloadArrivedBookings(
scheduleId,
'SYSTEM_TRAIN_ARRIVAL',
);
// Customer tracking: cargo is off the train at the destination yard.
void this.completeMilestonesForScheduleBookings(scheduleId, ['OFFLOADED']);
return {
direction,
action: 'IMPORT_AUTO_UNLOAD',
status: 'COMPLETED',
result: await this.warehouseInventoryService.autoUnloadArrivedBookings(
scheduleId,
'SYSTEM_TRAIN_ARRIVAL',
),
result,
};
}
if (direction === 'EXPORT' && this.isDjiboutiPortDestination(`${schedule.destinationCode ?? ''} ${schedule.destinationName ?? ''}`)) {
const result = await this.warehouseInventoryService.autoUnloadExportAtDjibouti(
scheduleId,
'SYSTEM_TRAIN_ARRIVAL',
);
// Customer tracking: cargo is off the train at the Djibouti port.
void this.completeMilestonesForScheduleBookings(scheduleId, ['OFFLOADED']);
return {
direction,
action: 'EXPORT_DJIBOUTI_AUTO_UNLOAD',
status: 'COMPLETED',
result: await this.warehouseInventoryService.autoUnloadExportAtDjibouti(
scheduleId,
'SYSTEM_TRAIN_ARRIVAL',
),
result,
};
}
@@ -1443,6 +1487,20 @@ export class TrainSchedulingService {
// Dispatch closed the window — drop it from portal/GL cards right away.
void this.emitWindowState(scheduleId);
// Customer tracking: cargo is on the departing train — loading milestones
// plus the direction's "departed" handoff milestone.
if (schedule.direction === 'IMPORT' || schedule.direction === 'EXPORT') {
void this.completeMilestonesForScheduleBookings(scheduleId, [
// CARGO_ARRIVED is export-only (cargo reached the origin yard) — the
// doc-trigger path no-ops it for import bookings.
'CARGO_ARRIVED',
'READY_FOR_LOADING',
'LOADED',
schedule.direction === 'IMPORT'
? 'DEPARTED_FROM_DJIBOUTI'
: 'DEPARTED_TO_DJIBOUTI',
]);
}
return this.getTrainScheduleById(scheduleId);
}
@@ -1599,6 +1657,13 @@ export class TrainSchedulingService {
LoadingStatus.Loaded,
);
}
// Customer tracking: staff confirmed cargo is on the wagons (CARGO_ARRIVED
// is the export-side "cargo reached origin yard" step that precedes it).
void this.completeMilestonesForScheduleBookings(scheduleId, [
'CARGO_ARRIVED',
'READY_FOR_LOADING',
'LOADED',
]);
return this.getTrainScheduleById(scheduleId);
}
@@ -2417,6 +2482,13 @@ export class TrainSchedulingService {
}
});
// Customer tracking: the train reached the corridor's far end.
if (schedule.direction === 'IMPORT' || schedule.direction === 'EXPORT') {
void this.completeMilestonesForScheduleBookings(scheduleId, [
schedule.direction === 'IMPORT' ? 'ARRIVED_ETHIOPIA' : 'ARRIVED_AT_DJIBOUTI',
]);
}
const detail = await this.getTrainScheduleById(scheduleId);
const warehouseAutomation = await this.runWarehouseArrivalAutomation(scheduleId);
return Object.assign(detail, { warehouseAutomation });