From f79fda79b9010441c553830177b2a30572b7d616 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Thu, 1 Jan 2026 10:05:56 -0600 Subject: [PATCH] fix: Short VIN Storage - Issue #1 --- .../vehicles/api/vehicles.controller.ts | 17 +++++-- .../vehicles/api/vehicles.validation.ts | 11 ++++- .../vehicles/data/vehicles.repository.ts | 12 +++-- .../vehicles/domain/vehicles.service.ts | 46 +++++++++++++++---- .../vehicles/domain/vehicles.types.ts | 11 +++++ .../src/shared-minimal/utils/validators.ts | 14 ++++-- docs/PROMPTS.md | 7 ++- .../vehicles/components/VehicleForm.tsx | 18 +++++--- .../features/vehicles/types/vehicles.types.ts | 2 + 9 files changed, 109 insertions(+), 29 deletions(-) diff --git a/backend/src/features/vehicles/api/vehicles.controller.ts b/backend/src/features/vehicles/api/vehicles.controller.ts index 1b1297f..b422421 100644 --- a/backend/src/features/vehicles/api/vehicles.controller.ts +++ b/backend/src/features/vehicles/api/vehicles.controller.ts @@ -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' diff --git a/backend/src/features/vehicles/api/vehicles.validation.ts b/backend/src/features/vehicles/api/vehicles.validation.ts index 6e04aa2..e2538c4 100644 --- a/backend/src/features/vehicles/api/vehicles.validation.ts +++ b/backend/src/features/vehicles/api/vehicles.validation.ts @@ -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'), diff --git a/backend/src/features/vehicles/data/vehicles.repository.ts b/backend/src/features/vehicles/data/vehicles.repository.ts index c555c5b..6e58b26 100644 --- a/backend/src/features/vehicles/data/vehicles.repository.ts +++ b/backend/src/features/vehicles/data/vehicles.repository.ts @@ -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); diff --git a/backend/src/features/vehicles/domain/vehicles.service.ts b/backend/src/features/vehicles/domain/vehicles.service.ts index 83a8be7..8c1d2cb 100644 --- a/backend/src/features/vehicles/domain/vehicles.service.ts +++ b/backend/src/features/vehicles/domain/vehicles.service.ts @@ -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 { 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 { // 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); } diff --git a/backend/src/features/vehicles/domain/vehicles.types.ts b/backend/src/features/vehicles/domain/vehicles.types.ts index 1ac0a5d..3c14334 100644 --- a/backend/src/features/vehicles/domain/vehicles.types.ts +++ b/backend/src/features/vehicles/domain/vehicles.types.ts @@ -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; diff --git a/backend/src/shared-minimal/utils/validators.ts b/backend/src/shared-minimal/utils/validators.ts index 7c22a72..8f20fe6 100644 --- a/backend/src/shared-minimal/utils/validators.ts +++ b/backend/src/shared-minimal/utils/validators.ts @@ -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); } \ No newline at end of file diff --git a/docs/PROMPTS.md b/docs/PROMPTS.md index f8a1464..d5c7d0d 100644 --- a/docs/PROMPTS.md +++ b/docs/PROMPTS.md @@ -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 diff --git a/frontend/src/features/vehicles/components/VehicleForm.tsx b/frontend/src/features/vehicles/components/VehicleForm.tsx index d6963b6..21894a4 100644 --- a/frontend/src/features/vehicles/components/VehicleForm.tsx +++ b/frontend/src/features/vehicles/components/VehicleForm.tsx @@ -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; }, diff --git a/frontend/src/features/vehicles/types/vehicles.types.ts b/frontend/src/features/vehicles/types/vehicles.types.ts index f5d9973..7cf3d27 100644 --- a/frontend/src/features/vehicles/types/vehicles.types.ts +++ b/frontend/src/features/vehicles/types/vehicles.types.ts @@ -41,6 +41,8 @@ export interface CreateVehicleRequest { } export interface UpdateVehicleRequest { + vin?: string; + year?: number; make?: string; model?: string; engine?: string;