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

@@ -0,0 +1,55 @@
import { Entity, Column, Index } from 'typeorm';
import { BaseEntity } from '@edr/api-common';
/**
* Append-only audit log for fleet activity. One row per transition. Queried by
* `vehicleId` (vehicle timeline) or `driverId` (driver timeline); an event may
* carry both so a driver↔vehicle assignment or a mile assignment shows on both.
* `createdAt` (from BaseEntity) is the event time.
*/
export enum FleetEventType {
DRIVER_REGISTERED = 'DRIVER_REGISTERED',
VEHICLE_REGISTERED = 'VEHICLE_REGISTERED',
DRIVER_ASSIGNED = 'DRIVER_ASSIGNED',
DRIVER_UNASSIGNED = 'DRIVER_UNASSIGNED',
VEHICLE_STATUS_CHANGED = 'VEHICLE_STATUS_CHANGED',
VEHICLE_AVAILABILITY_CHANGED = 'VEHICLE_AVAILABILITY_CHANGED',
MILE_VEHICLE_ASSIGNED = 'MILE_VEHICLE_ASSIGNED',
MILE_VEHICLE_RELEASED = 'MILE_VEHICLE_RELEASED',
MILE_STATUS_CHANGED = 'MILE_STATUS_CHANGED',
}
@Entity({ name: 'fleet_events', schema: 'freight' })
export class FleetEvent extends BaseEntity {
@Column({ name: 'event_type', type: 'varchar' })
eventType!: FleetEventType;
@Index()
@Column({ name: 'vehicle_id', type: 'uuid', nullable: true })
vehicleId?: string | null;
@Index()
@Column({ name: 'driver_id', type: 'uuid', nullable: true })
driverId?: string | null;
@Column({ name: 'first_mile_id', type: 'uuid', nullable: true })
firstMileId?: string | null;
@Column({ name: 'last_mile_id', type: 'uuid', nullable: true })
lastMileId?: string | null;
/** Previous value for a transition (e.g. old status/availability). */
@Column({ name: 'from_value', type: 'varchar', nullable: true })
fromValue?: string | null;
/** New value for a transition (e.g. new status/availability). */
@Column({ name: 'to_value', type: 'varchar', nullable: true })
toValue?: string | null;
/** Human-readable summary token (driver name, plate, booking ref, mile). */
@Column({ name: 'label', type: 'varchar', nullable: true })
label?: string | null;
@Column({ name: 'metadata', type: 'jsonb', nullable: true })
metadata?: Record<string, unknown> | null;
}

View File

@@ -0,0 +1,17 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FleetEvent } from './entities/fleet-event.entity';
import { FleetHistoryService } from './fleet-history.service';
/**
* Global so any fleet-touching service (vehicles, drivers, first/last-mile) can
* inject FleetHistoryService to append audit events without each module having
* to import this one.
*/
@Global()
@Module({
imports: [TypeOrmModule.forFeature([FleetEvent])],
providers: [FleetHistoryService],
exports: [FleetHistoryService],
})
export class FleetHistoryModule {}

View File

@@ -0,0 +1,54 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FleetEvent, FleetEventType } from './entities/fleet-event.entity';
export interface FleetEventInput {
eventType: FleetEventType;
vehicleId?: string | null;
driverId?: string | null;
firstMileId?: string | null;
lastMileId?: string | null;
fromValue?: string | null;
toValue?: string | null;
label?: string | null;
metadata?: Record<string, unknown> | null;
}
@Injectable()
export class FleetHistoryService {
private readonly logger = new Logger(FleetHistoryService.name);
constructor(
@InjectRepository(FleetEvent)
private readonly eventRepo: Repository<FleetEvent>,
) {}
/**
* Append an audit event. Best-effort: recording history must never break the
* business operation that triggered it, so failures are logged and swallowed.
*/
async record(input: FleetEventInput): Promise<void> {
try {
await this.eventRepo.save(this.eventRepo.create(input));
} catch (err) {
this.logger.error(
`Failed to record fleet event ${input.eventType}: ${String(err)}`,
);
}
}
getVehicleHistory(vehicleId: string): Promise<FleetEvent[]> {
return this.eventRepo.find({
where: { vehicleId },
order: { createdAt: 'DESC' },
});
}
getDriverHistory(driverId: string): Promise<FleetEvent[]> {
return this.eventRepo.find({
where: { driverId },
order: { createdAt: 'DESC' },
});
}
}