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 {
|
try {
|
||||||
const userId = (request as any).user.sub;
|
const userId = (request as any).user.sub;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
|
|
||||||
const vehicle = await this.vehiclesService.updateVehicle(id, request.body, userId);
|
const vehicle = await this.vehiclesService.updateVehicle(id, request.body, userId);
|
||||||
|
|
||||||
return reply.code(200).send(vehicle);
|
return reply.code(200).send(vehicle);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('Error updating vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub });
|
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') {
|
if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') {
|
||||||
return reply.code(404).send({
|
return reply.code(404).send({
|
||||||
error: 'Not Found',
|
error: 'Not Found',
|
||||||
message: 'Vehicle 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({
|
return reply.code(500).send({
|
||||||
error: 'Internal server error',
|
error: 'Internal server error',
|
||||||
message: 'Failed to update vehicle'
|
message: 'Failed to update vehicle'
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -95,8 +99,8 @@ export class VehiclesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateVehicle(
|
async updateVehicle(
|
||||||
id: string,
|
id: string,
|
||||||
data: UpdateVehicleRequest,
|
data: UpdateVehicleRequest,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<VehicleResponse> {
|
): Promise<VehicleResponse> {
|
||||||
// Verify ownership
|
// Verify ownership
|
||||||
@@ -107,7 +111,31 @@ export class VehiclesService {
|
|||||||
if (existing.userId !== userId) {
|
if (existing.userId !== userId) {
|
||||||
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) {
|
||||||
@@ -122,10 +150,10 @@ export class VehiclesService {
|
|||||||
if (!updated) {
|
if (!updated) {
|
||||||
throw new Error('Update failed');
|
throw new Error('Update failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate cache
|
// Invalidate cache
|
||||||
await this.invalidateUserCache(userId);
|
await this.invalidateUserCache(userId);
|
||||||
|
|
||||||
return this.toResponse(updated);
|
return this.toResponse(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -19,12 +19,20 @@ 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
|
||||||
if (/[IOQ]/i.test(vin)) return false;
|
if (/[IOQ]/i.test(vin)) return false;
|
||||||
|
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user