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

@@ -128,6 +128,15 @@ export class VehiclesController {
}); });
} }
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({ return reply.code(500).send({
error: 'Internal server error', error: 'Internal server error',
message: 'Failed to update vehicle' message: 'Failed to update vehicle'

View File

@@ -17,11 +17,20 @@ export const createVehicleSchema = z.object({
}); });
export const updateVehicleSchema = 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(), nickname: z.string().min(1).max(100).optional(),
color: z.string().min(1).max(50).optional(), color: z.string().min(1).max(50).optional(),
licensePlate: z.string().min(1).max(20).optional(), licensePlate: z.string().min(1).max(20).optional(),
odometerReading: z.number().min(0).max(9999999).optional(), odometerReading: z.number().min(0).max(9999999).optional(),
}).strict(); });
export const vehicleIdSchema = z.object({ export const vehicleIdSchema = z.object({
id: z.string().uuid('Invalid vehicle ID format'), id: z.string().uuid('Invalid vehicle ID format'),

View File

@@ -80,6 +80,14 @@ export class VehiclesRepository {
let paramCount = 1; let paramCount = 1;
// Build dynamic update query // 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) { if (data.make !== undefined) {
fields.push(`make = $${paramCount++}`); fields.push(`make = $${paramCount++}`);
values.push(data.make); values.push(data.make);
@@ -88,10 +96,6 @@ export class VehiclesRepository {
fields.push(`model = $${paramCount++}`); fields.push(`model = $${paramCount++}`);
values.push(data.model); values.push(data.model);
} }
if (data.year !== undefined) {
fields.push(`year = $${paramCount++}`);
values.push(data.year);
}
if (data.engine !== undefined) { if (data.engine !== undefined) {
fields.push(`engine = $${paramCount++}`); fields.push(`engine = $${paramCount++}`);
values.push(data.engine); values.push(data.engine);

View File

@@ -16,7 +16,7 @@ import { cacheService } from '../../../core/config/redis';
import { getStorageService } from '../../../core/storage/storage.service'; import { getStorageService } from '../../../core/storage/storage.service';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as path from 'path'; 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 { normalizeMakeName, normalizeModelName } from './name-normalizer';
import { getVehicleDataService, getPool } from '../../platform'; import { getVehicleDataService, getPool } from '../../platform';
@@ -31,12 +31,16 @@ export class VehiclesService {
async createVehicle(data: CreateVehicleRequest, userId: string): Promise<VehicleResponse> { async createVehicle(data: CreateVehicleRequest, userId: string): Promise<VehicleResponse> {
logger.info('Creating vehicle', { userId, vin: data.vin, licensePlate: data.licensePlate }); 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; const isPreModern = data.year && data.year < 1981;
if (data.vin) { if (data.vin) {
// Validate VIN format only for modern vehicles (1981+) // Validate VIN format based on vehicle year
if (!isPreModern && !isValidVIN(data.vin)) { 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'); throw new Error('Invalid VIN format');
} }
// Duplicate check only when VIN is present // Duplicate check only when VIN is present
@@ -108,6 +112,30 @@ export class VehiclesService {
throw new Error('Unauthorized'); 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 // Normalize any provided name fields
const normalized: UpdateVehicleRequest = { ...data } as any; const normalized: UpdateVehicleRequest = { ...data } as any;
if (data.make !== undefined) { if (data.make !== undefined) {

View File

@@ -47,6 +47,8 @@ export interface CreateVehicleRequest {
} }
export interface UpdateVehicleRequest { export interface UpdateVehicleRequest {
vin?: string;
year?: number;
make?: string; make?: string;
model?: string; model?: string;
engine?: string; engine?: string;
@@ -117,6 +119,15 @@ export interface CreateVehicleBody {
} }
export interface UpdateVehicleBody { export interface UpdateVehicleBody {
vin?: string;
year?: number;
make?: string;
model?: string;
engine?: string;
transmission?: string;
trimLevel?: string;
driveType?: string;
fuelType?: string;
nickname?: string; nickname?: string;
color?: string; color?: string;
licensePlate?: string; licensePlate?: string;

View File

@@ -19,7 +19,7 @@ export function isValidUUID(uuid: string): boolean {
} }
export function isValidVIN(vin: 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; if (vin.length !== 17) return false;
// VIN cannot contain I, O, or Q // VIN cannot contain I, O, or Q
@@ -28,3 +28,11 @@ export function isValidVIN(vin: string): boolean {
// Must be alphanumeric // Must be alphanumeric
return /^[A-HJ-NPR-Z0-9]{17}$/i.test(vin); 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 *** *** CHANGES TO IMPLEMENT ***
- Research this code base and ask iterative questions to compile a complete plan. - 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 - We will pair troubleshoot this. Tell me what logs and things to run and I will
- There is a bug in the vehicles screen - There is a bug in vehicles form
- The pencil icon on the vehicle cards does not take you to the edit screen. It does nothing. - 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 const vehicleSchema = z
.object({ .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(), year: z.number().min(1950).max(new Date().getFullYear() + 1).nullable().optional(),
make: z.string().nullable().optional(), make: z.string().nullable().optional(),
model: z.string().nullable().optional(), model: z.string().nullable().optional(),
@@ -30,11 +30,13 @@ const vehicleSchema = z
.refine( .refine(
(data) => { (data) => {
// Pre-1981 vehicles have no VIN/plate requirement // 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 vin = (data.vin || '').trim();
const plate = (data.licensePlate || '').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 (vin.length === 17) return true;
if (plate.length > 0) return true; if (plate.length > 0) return true;
return false; return false;
@@ -46,12 +48,16 @@ const vehicleSchema = z
) )
.refine( .refine(
(data) => { (data) => {
// Pre-1981 vehicles have no VIN format requirement // Pre-1981 vehicles accept any length VIN (1-17 chars)
if (data.year && data.year < 1981) return true; 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 vin = (data.vin || '').trim();
const plate = (data.licensePlate || '').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; if (plate.length > 0) return true;
return vin.length === 17 || vin.length === 0; return vin.length === 17 || vin.length === 0;
}, },

View File

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