mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 04:15:43 +00:00
@@ -0,0 +1,44 @@
|
|||||||
|
import { ConflictException } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { VehiclesService } from './vehicles.service';
|
||||||
|
|
||||||
|
// One driver ⇒ one truck: create/update must refuse a driver already assigned
|
||||||
|
// to another (non-deleted) vehicle until they are detached.
|
||||||
|
describe('VehiclesService driver assignment guard', () => {
|
||||||
|
const otherTruck = { id: 'v2', plateNumber: '3-11111', assignedDriverId: 'd1' };
|
||||||
|
|
||||||
|
const makeService = (findOne: jest.Mock) =>
|
||||||
|
new VehiclesService(
|
||||||
|
{ findOne, create: jest.fn((x) => x), save: jest.fn(async (x) => x) } as any,
|
||||||
|
{ record: jest.fn() } as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
it('rejects create when the driver is on another truck', async () => {
|
||||||
|
// First findOne = plate uniqueness (null), second = driver holder.
|
||||||
|
const findOne = jest.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(otherTruck);
|
||||||
|
const svc = makeService(findOne);
|
||||||
|
await expect(
|
||||||
|
svc.create({ plateNumber: '3-22222', vehicleType: 'TRUCK', assignedDriverId: 'd1' } as any),
|
||||||
|
).rejects.toThrow(ConflictException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects update when reassigning a driver still attached elsewhere', async () => {
|
||||||
|
const findOne = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ id: 'v1', plateNumber: '3-22222', assignedDriverId: null }) // findById
|
||||||
|
.mockResolvedValueOnce(otherTruck); // driver holder
|
||||||
|
const svc = makeService(findOne);
|
||||||
|
await expect(svc.update('v1', { assignedDriverId: 'd1' } as any)).rejects.toThrow(
|
||||||
|
ConflictException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows update that keeps the same driver on the same truck', async () => {
|
||||||
|
const findOne = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ id: 'v1', plateNumber: '3-22222', assignedDriverId: 'd1' });
|
||||||
|
const svc = makeService(findOne);
|
||||||
|
await expect(svc.update('v1', { assignedDriverId: 'd1' } as any)).resolves.toBeDefined();
|
||||||
|
expect(findOne).toHaveBeenCalledTimes(1); // guard skipped — no holder lookup
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -20,6 +20,25 @@ export class VehiclesService {
|
|||||||
private readonly history: FleetHistoryService,
|
private readonly history: FleetHistoryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A driver holds one truck at a time — reassignment requires detaching them
|
||||||
|
* from their current truck first.
|
||||||
|
* ponytail: app-level guard only (race window); add a partial unique index on
|
||||||
|
* assigned_driver_id if concurrent fleet edits ever become real.
|
||||||
|
*/
|
||||||
|
private async assertDriverUnassigned(driverId: string, exceptVehicleId?: string): Promise<void> {
|
||||||
|
const holder = await this.vehicleRepo.findOne({
|
||||||
|
where: exceptVehicleId
|
||||||
|
? { assignedDriverId: driverId, id: Not(exceptVehicleId) }
|
||||||
|
: { assignedDriverId: driverId },
|
||||||
|
});
|
||||||
|
if (holder) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`This driver is already assigned to truck ${holder.plateNumber ?? holder.code ?? holder.id} — detach the driver from that truck first`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(dto: CreateVehicleDto): Promise<Vehicle> {
|
async create(dto: CreateVehicleDto): Promise<Vehicle> {
|
||||||
const existing = await this.vehicleRepo.findOne({
|
const existing = await this.vehicleRepo.findOne({
|
||||||
where: { plateNumber: dto.plateNumber },
|
where: { plateNumber: dto.plateNumber },
|
||||||
@@ -31,6 +50,10 @@ export class VehiclesService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.assignedDriverId) {
|
||||||
|
await this.assertDriverUnassigned(dto.assignedDriverId);
|
||||||
|
}
|
||||||
|
|
||||||
const registrationNumber = `REG-${dto.vehicleType}-${Date.now()}`;
|
const registrationNumber = `REG-${dto.vehicleType}-${Date.now()}`;
|
||||||
const vehicle = this.vehicleRepo.create({
|
const vehicle = this.vehicleRepo.create({
|
||||||
...dto,
|
...dto,
|
||||||
@@ -121,6 +144,10 @@ export class VehiclesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.assignedDriverId && dto.assignedDriverId !== vehicle.assignedDriverId) {
|
||||||
|
await this.assertDriverUnassigned(dto.assignedDriverId, id);
|
||||||
|
}
|
||||||
|
|
||||||
const prev = {
|
const prev = {
|
||||||
assignedDriverId: vehicle.assignedDriverId,
|
assignedDriverId: vehicle.assignedDriverId,
|
||||||
assignedDriverName: vehicle.assignedDriverName,
|
assignedDriverName: vehicle.assignedDriverName,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function WarehouseLocationCard({ bookingId }: WarehouseLocationCardProps)
|
|||||||
const latest = items[0];
|
const latest = items[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper withBorder radius="md" padding="lg">
|
<Paper withBorder radius="md" p="lg">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<WarehouseIcon size={18} />
|
<WarehouseIcon size={18} />
|
||||||
|
|||||||
Reference in New Issue
Block a user