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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user