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

This commit is contained in:
Eric Gullickson
2026-01-01 10:05:56 -06:00
parent 7631d961c5
commit f79fda79b9
9 changed files with 109 additions and 29 deletions

View File

@@ -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'

View File

@@ -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'),

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -30,8 +30,11 @@ You are a senior software engineer specializsing in NodeJS, Typescript, front en
*** CHANGES TO IMPLEMENT ***
- Research this code base and ask iterative questions to compile a complete plan.
- We will pair troubleshoot this. Tell me what logs and things to run and I will
- There is a bug in the vehicles screen
- The pencil icon on the vehicle cards does not take you to the edit screen. It does nothing.
- There is a bug in vehicles form
- There was logic added for fewer than 17 digits in the VIN number for vehicles older than 1980
- The application was built with many contraints on 17 digit VIN numbers and now the database doesn't store VIN numbers with fewer than 17
- Displatch explore agents to follow the data flow and find the remaining gate that is stopping the data from being stored.
- There are no front end or API errors

View File

@@ -13,7 +13,7 @@ import { VehicleImageUpload } from './VehicleImageUpload';
const vehicleSchema = z
.object({
vin: z.string().nullable().optional().transform(val => val ?? undefined),
vin: z.string().max(17).nullable().optional().transform(val => val ?? undefined),
year: z.number().min(1950).max(new Date().getFullYear() + 1).nullable().optional(),
make: z.string().nullable().optional(),
model: z.string().nullable().optional(),
@@ -30,11 +30,13 @@ const vehicleSchema = z
.refine(
(data) => {
// Pre-1981 vehicles have no VIN/plate requirement
if (data.year && data.year < 1981) return true;
if (data.year && data.year < 1981) {
return true;
}
const vin = (data.vin || '').trim();
const plate = (data.licensePlate || '').trim();
// Must have either a valid 17-char VIN or a non-empty license plate
// 1981+: Must have either a valid 17-char VIN or a non-empty license plate
if (vin.length === 17) return true;
if (plate.length > 0) return true;
return false;
@@ -46,12 +48,16 @@ const vehicleSchema = z
)
.refine(
(data) => {
// Pre-1981 vehicles have no VIN format requirement
if (data.year && data.year < 1981) return true;
// Pre-1981 vehicles accept any length VIN (1-17 chars)
if (data.year && data.year < 1981) {
const vin = (data.vin || '').trim();
// Empty is fine, or any length up to 17
return vin.length === 0 || (vin.length >= 1 && vin.length <= 17);
}
const vin = (data.vin || '').trim();
const plate = (data.licensePlate || '').trim();
// If VIN provided but not 17 and no plate, fail; if plate exists, allow any VIN (or empty)
// 1981+: If plate exists, allow any VIN (or empty); otherwise VIN must be exactly 17 or empty
if (plate.length > 0) return true;
return vin.length === 17 || vin.length === 0;
},

View File

@@ -41,6 +41,8 @@ export interface CreateVehicleRequest {
}
export interface UpdateVehicleRequest {
vin?: string;
year?: number;
make?: string;
model?: string;
engine?: string;