feat: Add VIN decoding with NHTSA vPIC API (refs #9)
- Add NHTSA client for VIN decoding with caching and validation - Add POST /api/vehicles/decode-vin endpoint with tier gating - Add dropdown matching service with confidence levels - Add decode button to VehicleForm with tier check - Responsive layout: stacks on mobile, inline on desktop - Only populate empty fields (preserve user input) Backend: - NHTSAClient with 5s timeout, VIN validation, vin_cache table - Tier gating with 'vehicle.vinDecode' feature key (Pro+) - Tiered matching: high (exact), medium (normalized), none Frontend: - Decode button with loading state and error handling - UpgradeRequiredDialog for free tier users - Mobile-first responsive layout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,16 +10,19 @@ 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 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);
|
||||
this.nhtsaClient = new NHTSAClient(pool);
|
||||
}
|
||||
|
||||
async getUserVehicles(request: FastifyRequest, reply: FastifyReply) {
|
||||
@@ -309,6 +312,75 @@ export class VehiclesController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode VIN using NHTSA vPIC API
|
||||
* POST /api/vehicles/decode-vin
|
||||
* Requires Pro or Enterprise tier
|
||||
*/
|
||||
async decodeVin(request: FastifyRequest<{ Body: DecodeVinRequest }>, reply: FastifyReply) {
|
||||
const userId = (request as any).user?.sub;
|
||||
|
||||
try {
|
||||
const { vin } = request.body;
|
||||
|
||||
if (!vin || typeof vin !== 'string') {
|
||||
return reply.code(400).send({
|
||||
error: 'INVALID_VIN',
|
||||
message: 'VIN is required'
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('VIN decode requested', { userId, vin: vin.substring(0, 6) + '...' });
|
||||
|
||||
// Validate and decode VIN
|
||||
const response = await this.nhtsaClient.decodeVin(vin);
|
||||
|
||||
// Extract and map fields from NHTSA response
|
||||
const decodedData = await this.vehiclesService.mapNHTSAResponse(response);
|
||||
|
||||
logger.info('VIN decode successful', {
|
||||
userId,
|
||||
hasYear: !!decodedData.year.value,
|
||||
hasMake: !!decodedData.make.value,
|
||||
hasModel: !!decodedData.model.value
|
||||
});
|
||||
|
||||
return reply.code(200).send(decodedData);
|
||||
} catch (error: any) {
|
||||
logger.error('VIN decode failed', { error, userId });
|
||||
|
||||
// Handle validation errors
|
||||
if (error.message?.includes('Invalid VIN')) {
|
||||
return reply.code(400).send({
|
||||
error: 'INVALID_VIN',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
// 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 NHTSA API errors
|
||||
if (error.message?.includes('NHTSA')) {
|
||||
return reply.code(502).send({
|
||||
error: 'VIN_DECODE_FAILED',
|
||||
message: 'Unable to decode VIN from external service',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(500).send({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to decode VIN'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async uploadImage(request: FastifyRequest<{ Params: VehicleParams }>, reply: FastifyReply) {
|
||||
const userId = (request as any).user.sub;
|
||||
const vehicleId = request.params.id;
|
||||
|
||||
Reference in New Issue
Block a user