This commit is contained in:
natib21
2026-07-03 15:21:23 +00:00
parent e881e8de84
commit 0e6ebda6f6
15 changed files with 661 additions and 7 deletions

View File

@@ -14,6 +14,8 @@ import { FirstMileContainerAllocation } from "./entities/first-mile-container-al
import { FirstMileRepository } from "./first-mile.repository";
import { OnEvent } from "@nestjs/event-emitter";
import { InvoiceEventPayload } from "../billing/billing.service";
import { FleetHistoryService } from "../fleet-history/fleet-history.service";
import { FleetEventType } from "../fleet-history/entities/fleet-event.entity";
type FirstMileListFilter = {
status?: FirstMileStatus;
@@ -43,8 +45,21 @@ export class FirstMileService {
private readonly vehiclesService: VehiclesService,
private readonly driversService: DriversService,
private readonly smsClient: SmsClientService,
private readonly history: FleetHistoryService,
) { }
/** Resolve the driver currently assigned to a vehicle, for stamping mile
* events onto that driver's timeline. Best-effort — never throws. */
private async resolveDriverId(vehicleId?: string | null): Promise<string | null> {
if (!vehicleId) return null;
try {
const vehicle = await this.vehiclesService.findById(vehicleId);
return vehicle.assignedDriverId ?? null;
} catch {
return null;
}
}
/**
* Look up a booking by its human-readable reference and confirm it has been
* paid before any first-mile work proceeds. Throws if the reference is
@@ -210,6 +225,14 @@ export class FirstMileService {
if (dto.vehicleId) {
await this.vehiclesService.setAvailability(dto.vehicleId, VehicleAvailability.BUSY);
await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
vehicleId: dto.vehicleId,
firstMileId: record.id,
driverId: await this.resolveDriverId(dto.vehicleId),
label: record.status,
metadata: { mile: 'FIRST' },
});
}
return record;
@@ -278,6 +301,26 @@ export class FirstMileService {
if (existing.vehicleId) {
await this.vehiclesService.releaseIfUnused([existing.vehicleId]);
}
// Audit the mile↔vehicle (re)assignment on both vehicle and driver lines.
if (existing.vehicleId) {
await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_RELEASED,
vehicleId: existing.vehicleId,
firstMileId: id,
driverId: await this.resolveDriverId(existing.vehicleId),
metadata: { mile: 'FIRST' },
});
}
if (dto.vehicleId) {
await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
vehicleId: dto.vehicleId,
firstMileId: id,
driverId: await this.resolveDriverId(dto.vehicleId),
label: updated.status,
metadata: { mile: 'FIRST' },
});
}
}
// Notify assigned driver on every explicit vehicle assignment or reassignment
@@ -285,6 +328,19 @@ export class FirstMileService {
void this.notifyDriverAssignment(dto.vehicleId, existing);
}
if (dto.status !== undefined && dto.status !== existing.status) {
const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null;
await this.history.record({
eventType: FleetEventType.MILE_STATUS_CHANGED,
firstMileId: id,
vehicleId,
driverId: await this.resolveDriverId(vehicleId),
fromValue: existing.status,
toValue: dto.status,
metadata: { mile: 'FIRST' },
});
}
// Trip finished — release the vehicles it was holding
if (dto.status === 'RECEIVED_TO_PORT' && existing.status !== 'RECEIVED_TO_PORT') {
await this.releaseVehicles(updated);
@@ -301,6 +357,19 @@ export class FirstMileService {
throw new NotFoundException(`First-mile record ${id} not found`);
}
if (status !== existing.status) {
const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null;
await this.history.record({
eventType: FleetEventType.MILE_STATUS_CHANGED,
firstMileId: id,
vehicleId,
driverId: await this.resolveDriverId(vehicleId),
fromValue: existing.status,
toValue: status,
metadata: { mile: 'FIRST' },
});
}
if (status === 'RECEIVED_TO_PORT' && existing.status !== 'RECEIVED_TO_PORT') {
await this.releaseVehicles(updated);
}