feat: rewire vehicles controller to OCR VIN decode (refs #226)

Replace NHTSAClient with OcrClient in vehicles controller. Move cache
logic into VehiclesService with format-aware reads (Gemini vs legacy
NHTSA entries). Rename nhtsaValue to sourceValue in MatchedField.
Remove vpic config from Zod schema and YAML config files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-18 21:47:47 -06:00
parent 3cd61256ba
commit 5cbf9c764d
9 changed files with 220 additions and 93 deletions

View File

@@ -10,19 +10,18 @@ import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger';
import { CreateVehicleBody, UpdateVehicleBody, VehicleParams } from '../domain/vehicles.types';
import { getStorageService } from '../../../core/storage/storage.service';
import { NHTSAClient, DecodeVinRequest } from '../external/nhtsa';
import { ocrClient } from '../../ocr/external/ocr-client';
import type { DecodeVinRequest } from '../domain/vehicles.types';
import crypto from 'crypto';
import FileType from 'file-type';
import path from 'path';
export class VehiclesController {
private vehiclesService: VehiclesService;
private nhtsaClient: NHTSAClient;
constructor() {
const repository = new VehiclesRepository(pool);
this.vehiclesService = new VehiclesService(repository, pool);
this.nhtsaClient = new NHTSAClient(pool);
}
async getUserVehicles(request: FastifyRequest, reply: FastifyReply) {
@@ -378,7 +377,7 @@ export class VehiclesController {
}
/**
* Decode VIN using NHTSA vPIC API
* Decode VIN using OCR service (Gemini)
* POST /api/vehicles/decode-vin
* Requires Pro or Enterprise tier
*/
@@ -395,13 +394,34 @@ export class VehiclesController {
});
}
logger.info('VIN decode requested', { userId, vin: vin.substring(0, 6) + '...' });
// Validate VIN format
const sanitizedVin = vin.trim().toUpperCase();
const VIN_REGEX = /^[A-HJ-NPR-Z0-9]{17}$/;
if (!VIN_REGEX.test(sanitizedVin)) {
return reply.code(400).send({
error: 'INVALID_VIN',
message: 'Invalid VIN format. VIN must be exactly 17 characters and contain only letters (except I, O, Q) and numbers.'
});
}
// Validate and decode VIN
const response = await this.nhtsaClient.decodeVin(vin);
logger.info('VIN decode requested', { userId, vin: sanitizedVin.substring(0, 6) + '...' });
// Extract and map fields from NHTSA response
const decodedData = await this.vehiclesService.mapNHTSAResponse(response);
// Check cache first
const cached = await this.vehiclesService.getVinCached(sanitizedVin);
if (cached) {
logger.info('VIN decode cache hit', { userId });
const decodedData = await this.vehiclesService.mapVinDecodeResponse(cached);
return reply.code(200).send(decodedData);
}
// Call OCR service for VIN decode
const response = await ocrClient.decodeVin(sanitizedVin);
// Cache the response
await this.vehiclesService.saveVinCache(sanitizedVin, response);
// Map response to decoded vehicle data with dropdown matching
const decodedData = await this.vehiclesService.mapVinDecodeResponse(response);
logger.info('VIN decode successful', {
userId,
@@ -414,7 +434,7 @@ export class VehiclesController {
} catch (error: any) {
logger.error('VIN decode failed', { error, userId });
// Handle validation errors
// Handle VIN validation errors
if (error.message?.includes('Invalid VIN')) {
return reply.code(400).send({
error: 'INVALID_VIN',
@@ -422,16 +442,25 @@ export class VehiclesController {
});
}
// Handle timeout
if (error.message?.includes('timed out')) {
return reply.code(504).send({
error: 'VIN_DECODE_TIMEOUT',
message: 'NHTSA API request timed out. Please try again.'
// Handle OCR service errors by status code
if (error.statusCode === 503 || error.statusCode === 422) {
return reply.code(502).send({
error: 'VIN_DECODE_FAILED',
message: 'VIN decode service unavailable',
details: error.message
});
}
// Handle NHTSA API errors
if (error.message?.includes('NHTSA')) {
// Handle timeout
if (error.message?.includes('timed out') || error.message?.includes('aborted')) {
return reply.code(504).send({
error: 'VIN_DECODE_TIMEOUT',
message: 'VIN decode service timed out. Please try again.'
});
}
// Handle OCR service errors
if (error.message?.includes('OCR service error')) {
return reply.code(502).send({
error: 'VIN_DECODE_FAILED',
message: 'Unable to decode VIN from external service',