mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 19:30:57 +00:00
feat(fleet): validate vehicle plate numbers against a letters-and-digits format
Plate, power-plate and trailer accepted any free text — a vehicle could be
saved with a plate of "assadasd". They must be letters, a hyphen, then digits,
like ET-9875 or AA-8642.
The server now enforces it on CreateVehicleDto (and UpdateVehicleDto via
PartialType): each plate is trimmed and upper-cased, then matched against
^[A-Z]{2,3}-\d{2,6}$, so "et-9875" is accepted and stored as ET-9875 while an
empty optional trailer/power plate still passes.
The fleet form gains the same check inline: FleetFormFieldDef takes an optional
pattern, the dialog tests it on submit against the upper-cased value, and the
vehicle config points plate and trailer at a regex that mirrors the server's.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
|
||||
import { CreateVehicleDto } from './create-vehicle.dto';
|
||||
|
||||
const base = {
|
||||
vehicleType: 'TRUCK',
|
||||
manufacturer: 'IVECO',
|
||||
model: 'HYT',
|
||||
year: 2020,
|
||||
fuelType: 'DIESEL',
|
||||
capacity: 0,
|
||||
status: 'ACTIVE',
|
||||
};
|
||||
|
||||
const errorsFor = (over: Record<string, unknown>) =>
|
||||
validate(plainToInstance(CreateVehicleDto, { ...base, ...over }));
|
||||
|
||||
const plateErrors = (
|
||||
errors: Awaited<ReturnType<typeof errorsFor>>,
|
||||
property: string,
|
||||
) => errors.find((e) => e.property === property && e.constraints?.matches);
|
||||
|
||||
describe('CreateVehicleDto — plate format', () => {
|
||||
it('accepts a plate like ET-9875', async () => {
|
||||
expect(plateErrors(await errorsFor({ plateNumber: 'ET-9875' }), 'plateNumber')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts a plate like AA-8642', async () => {
|
||||
expect(plateErrors(await errorsFor({ plateNumber: 'AA-8642' }), 'plateNumber')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('upper-cases a lower-case plate before validating', async () => {
|
||||
const dto = plainToInstance(CreateVehicleDto, { ...base, plateNumber: 'et-9875' });
|
||||
expect(dto.plateNumber).toBe('ET-9875');
|
||||
expect(plateErrors(await validate(dto), 'plateNumber')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects a free-text plate like assadasd', async () => {
|
||||
expect(plateErrors(await errorsFor({ plateNumber: 'assadasd' }), 'plateNumber')).toBeDefined();
|
||||
});
|
||||
|
||||
it('rejects a plate with no letters or no digits', async () => {
|
||||
expect(plateErrors(await errorsFor({ plateNumber: '1234' }), 'plateNumber')).toBeDefined();
|
||||
expect(plateErrors(await errorsFor({ plateNumber: 'ABCD' }), 'plateNumber')).toBeDefined();
|
||||
});
|
||||
|
||||
it('rejects a bad trailer plate but allows a valid one', async () => {
|
||||
expect(
|
||||
plateErrors(await errorsFor({ plateNumber: 'ET-1', trailerPlateNo: 'asdasdasda' }), 'trailerPlateNo'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
plateErrors(await errorsFor({ plateNumber: 'ET-1', trailerPlateNo: 'AA-8642' }), 'trailerPlateNo'),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('allows an empty trailer plate (optional)', async () => {
|
||||
const errors = await errorsFor({ plateNumber: 'ET-1', trailerPlateNo: '' });
|
||||
expect(plateErrors(errors, 'trailerPlateNo')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,30 @@
|
||||
import { IsString, IsEnum, IsNumber, IsOptional, IsUUID } from 'class-validator';
|
||||
import { IsString, IsEnum, IsNumber, IsOptional, IsUUID, Matches } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { VehicleType, FuelType, VehicleStatus, VehicleAvailability } from '../entities/vehicle.entity';
|
||||
|
||||
/**
|
||||
* A vehicle plate is two or three letters, a hyphen, then two to six digits —
|
||||
* e.g. ET-9875 or AA-8642. Kept in one place so plate, power-plate and trailer
|
||||
* all match and the message stays consistent.
|
||||
*/
|
||||
export const VEHICLE_PLATE_REGEX = /^[A-Z]{2,3}-\d{2,6}$/;
|
||||
export const VEHICLE_PLATE_MESSAGE =
|
||||
'must be letters and numbers like ET-9875 or AA-8642';
|
||||
|
||||
/**
|
||||
* Trim and upper-case a plate before validating, so "et-9875" is accepted. An
|
||||
* empty optional plate (trailer/power) becomes undefined so @IsOptional skips it
|
||||
* rather than failing the pattern.
|
||||
*/
|
||||
const normalizePlate = ({ value }: { value: unknown }) => {
|
||||
if (typeof value !== 'string') return value;
|
||||
const trimmed = value.trim().toUpperCase();
|
||||
return trimmed === '' ? undefined : trimmed;
|
||||
};
|
||||
|
||||
export class CreateVehicleDto {
|
||||
@Transform(normalizePlate)
|
||||
@Matches(VEHICLE_PLATE_REGEX, { message: `Plate number ${VEHICLE_PLATE_MESSAGE}` })
|
||||
@IsString()
|
||||
plateNumber!: string;
|
||||
|
||||
@@ -47,10 +70,14 @@ export class CreateVehicleDto {
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(normalizePlate)
|
||||
@Matches(VEHICLE_PLATE_REGEX, { message: `Power plate number ${VEHICLE_PLATE_MESSAGE}` })
|
||||
@IsString()
|
||||
powerPlateNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(normalizePlate)
|
||||
@Matches(VEHICLE_PLATE_REGEX, { message: `Trailer plate number ${VEHICLE_PLATE_MESSAGE}` })
|
||||
@IsString()
|
||||
trailerPlateNo?: string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user