fix: Short VIN Storage - Issue #1
All checks were successful
Deploy to Staging / Build Images (push) Successful in 4m31s
Deploy to Staging / Deploy to Staging (push) Successful in 36s
Deploy to Staging / Verify Staging (push) Successful in 5s
Deploy to Staging / Notify Staging Ready (push) Successful in 5s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
All checks were successful
Deploy to Staging / Build Images (push) Successful in 4m31s
Deploy to Staging / Deploy to Staging (push) Successful in 36s
Deploy to Staging / Verify Staging (push) Successful in 5s
Deploy to Staging / Notify Staging Ready (push) Successful in 5s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
This commit is contained in:
@@ -114,20 +114,29 @@ export class VehiclesController {
|
||||
try {
|
||||
const userId = (request as any).user.sub;
|
||||
const { id } = request.params;
|
||||
|
||||
|
||||
const vehicle = await this.vehiclesService.updateVehicle(id, request.body, userId);
|
||||
|
||||
|
||||
return reply.code(200).send(vehicle);
|
||||
} catch (error: any) {
|
||||
logger.error('Error updating vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub });
|
||||
|
||||
|
||||
if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') {
|
||||
return reply.code(404).send({
|
||||
error: 'Not Found',
|
||||
message: 'Vehicle not found'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (error.message === 'Invalid VIN format' ||
|
||||
error.message === 'Invalid VIN format for pre-1981 vehicle' ||
|
||||
error.message === 'Vehicle with this VIN already exists') {
|
||||
return reply.code(400).send({
|
||||
error: 'Bad Request',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(500).send({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to update vehicle'
|
||||
|
||||
@@ -17,11 +17,20 @@ export const createVehicleSchema = z.object({
|
||||
});
|
||||
|
||||
export const updateVehicleSchema = z.object({
|
||||
vin: z.string().max(17).optional(),
|
||||
year: z.number().min(1900).max(new Date().getFullYear() + 1).optional(),
|
||||
make: z.string().max(100).optional(),
|
||||
model: z.string().max(100).optional(),
|
||||
engine: z.string().max(100).optional(),
|
||||
transmission: z.string().max(100).optional(),
|
||||
trimLevel: z.string().max(100).optional(),
|
||||
driveType: z.string().max(50).optional(),
|
||||
fuelType: z.string().max(50).optional(),
|
||||
nickname: z.string().min(1).max(100).optional(),
|
||||
color: z.string().min(1).max(50).optional(),
|
||||
licensePlate: z.string().min(1).max(20).optional(),
|
||||
odometerReading: z.number().min(0).max(9999999).optional(),
|
||||
}).strict();
|
||||
});
|
||||
|
||||
export const vehicleIdSchema = z.object({
|
||||
id: z.string().uuid('Invalid vehicle ID format'),
|
||||
|
||||
@@ -80,6 +80,14 @@ export class VehiclesRepository {
|
||||
let paramCount = 1;
|
||||
|
||||
// Build dynamic update query
|
||||
if (data.vin !== undefined) {
|
||||
fields.push(`vin = $${paramCount++}`);
|
||||
values.push(data.vin && data.vin.trim().length > 0 ? data.vin.trim() : null);
|
||||
}
|
||||
if (data.year !== undefined) {
|
||||
fields.push(`year = $${paramCount++}`);
|
||||
values.push(data.year);
|
||||
}
|
||||
if (data.make !== undefined) {
|
||||
fields.push(`make = $${paramCount++}`);
|
||||
values.push(data.make);
|
||||
@@ -88,10 +96,6 @@ export class VehiclesRepository {
|
||||
fields.push(`model = $${paramCount++}`);
|
||||
values.push(data.model);
|
||||
}
|
||||
if (data.year !== undefined) {
|
||||
fields.push(`year = $${paramCount++}`);
|
||||
values.push(data.year);
|
||||
}
|
||||
if (data.engine !== undefined) {
|
||||
fields.push(`engine = $${paramCount++}`);
|
||||
values.push(data.engine);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { cacheService } from '../../../core/config/redis';
|
||||
import { getStorageService } from '../../../core/storage/storage.service';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { isValidVIN } from '../../../shared-minimal/utils/validators';
|
||||
import { isValidVIN, isValidPreModernVIN } from '../../../shared-minimal/utils/validators';
|
||||
import { normalizeMakeName, normalizeModelName } from './name-normalizer';
|
||||
import { getVehicleDataService, getPool } from '../../platform';
|
||||
|
||||
@@ -31,12 +31,16 @@ export class VehiclesService {
|
||||
async createVehicle(data: CreateVehicleRequest, userId: string): Promise<VehicleResponse> {
|
||||
logger.info('Creating vehicle', { userId, vin: data.vin, licensePlate: data.licensePlate });
|
||||
|
||||
// Pre-1981 vehicles have no VIN format requirement
|
||||
// Pre-1981 vehicles have relaxed VIN format requirements
|
||||
const isPreModern = data.year && data.year < 1981;
|
||||
|
||||
if (data.vin) {
|
||||
// Validate VIN format only for modern vehicles (1981+)
|
||||
if (!isPreModern && !isValidVIN(data.vin)) {
|
||||
// Validate VIN format based on vehicle year
|
||||
if (isPreModern) {
|
||||
if (!isValidPreModernVIN(data.vin)) {
|
||||
throw new Error('Invalid VIN format for pre-1981 vehicle');
|
||||
}
|
||||
} else if (!isValidVIN(data.vin)) {
|
||||
throw new Error('Invalid VIN format');
|
||||
}
|
||||
// Duplicate check only when VIN is present
|
||||
@@ -95,8 +99,8 @@ export class VehiclesService {
|
||||
}
|
||||
|
||||
async updateVehicle(
|
||||
id: string,
|
||||
data: UpdateVehicleRequest,
|
||||
id: string,
|
||||
data: UpdateVehicleRequest,
|
||||
userId: string
|
||||
): Promise<VehicleResponse> {
|
||||
// Verify ownership
|
||||
@@ -107,7 +111,31 @@ export class VehiclesService {
|
||||
if (existing.userId !== userId) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
|
||||
// Determine the effective year for validation (use new year if provided, else existing)
|
||||
const effectiveYear = data.year !== undefined ? data.year : existing.year;
|
||||
const isPreModern = effectiveYear && effectiveYear < 1981;
|
||||
|
||||
// Validate VIN if provided
|
||||
if (data.vin !== undefined && data.vin.trim().length > 0) {
|
||||
const trimmedVin = data.vin.trim();
|
||||
// Validate VIN format based on vehicle year
|
||||
if (isPreModern) {
|
||||
if (!isValidPreModernVIN(trimmedVin)) {
|
||||
throw new Error('Invalid VIN format for pre-1981 vehicle');
|
||||
}
|
||||
} else if (!isValidVIN(trimmedVin)) {
|
||||
throw new Error('Invalid VIN format');
|
||||
}
|
||||
// Check for duplicate VIN (only if VIN is changing)
|
||||
if (trimmedVin !== existing.vin) {
|
||||
const duplicate = await this.repository.findByUserAndVIN(userId, trimmedVin);
|
||||
if (duplicate && duplicate.id !== id) {
|
||||
throw new Error('Vehicle with this VIN already exists');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize any provided name fields
|
||||
const normalized: UpdateVehicleRequest = { ...data } as any;
|
||||
if (data.make !== undefined) {
|
||||
@@ -122,10 +150,10 @@ export class VehiclesService {
|
||||
if (!updated) {
|
||||
throw new Error('Update failed');
|
||||
}
|
||||
|
||||
|
||||
// Invalidate cache
|
||||
await this.invalidateUserCache(userId);
|
||||
|
||||
|
||||
return this.toResponse(updated);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ export interface CreateVehicleRequest {
|
||||
}
|
||||
|
||||
export interface UpdateVehicleRequest {
|
||||
vin?: string;
|
||||
year?: number;
|
||||
make?: string;
|
||||
model?: string;
|
||||
engine?: string;
|
||||
@@ -117,6 +119,15 @@ export interface CreateVehicleBody {
|
||||
}
|
||||
|
||||
export interface UpdateVehicleBody {
|
||||
vin?: string;
|
||||
year?: number;
|
||||
make?: string;
|
||||
model?: string;
|
||||
engine?: string;
|
||||
transmission?: string;
|
||||
trimLevel?: string;
|
||||
driveType?: string;
|
||||
fuelType?: string;
|
||||
nickname?: string;
|
||||
color?: string;
|
||||
licensePlate?: string;
|
||||
|
||||
@@ -19,12 +19,20 @@ export function isValidUUID(uuid: string): boolean {
|
||||
}
|
||||
|
||||
export function isValidVIN(vin: string): boolean {
|
||||
// VIN must be exactly 17 characters
|
||||
// VIN must be exactly 17 characters (modern VIN standard since 1981)
|
||||
if (vin.length !== 17) return false;
|
||||
|
||||
|
||||
// VIN cannot contain I, O, or Q
|
||||
if (/[IOQ]/i.test(vin)) return false;
|
||||
|
||||
|
||||
// Must be alphanumeric
|
||||
return /^[A-HJ-NPR-Z0-9]{17}$/i.test(vin);
|
||||
}
|
||||
|
||||
export function isValidPreModernVIN(vin: string): boolean {
|
||||
// Pre-1981 vehicles had non-standardized VINs of varying lengths (1-17 characters)
|
||||
if (vin.length < 1 || vin.length > 17) return false;
|
||||
|
||||
// Must be alphanumeric (allow I, O, Q for pre-1981 as they weren't banned yet)
|
||||
return /^[A-Z0-9]+$/i.test(vin);
|
||||
}
|
||||
Reference in New Issue
Block a user