MVP Build
This commit is contained in:
78
backend/src/features/vehicles/external/vpic/vpic.client.ts
vendored
Normal file
78
backend/src/features/vehicles/external/vpic/vpic.client.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @ai-summary NHTSA vPIC API client for VIN decoding
|
||||
* @ai-context Caches results for 30 days since vehicle specs don't change
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { env } from '../../../../core/config/environment';
|
||||
import { logger } from '../../../../core/logging/logger';
|
||||
import { cacheService } from '../../../../core/config/redis';
|
||||
import { VPICResponse, VPICDecodeResult } from './vpic.types';
|
||||
|
||||
export class VPICClient {
|
||||
private readonly baseURL = env.VPIC_API_URL;
|
||||
private readonly cacheTTL = 30 * 24 * 60 * 60; // 30 days in seconds
|
||||
|
||||
async decodeVIN(vin: string): Promise<VPICDecodeResult | null> {
|
||||
const cacheKey = `vpic:vin:${vin}`;
|
||||
|
||||
try {
|
||||
// Check cache first
|
||||
const cached = await cacheService.get<VPICDecodeResult>(cacheKey);
|
||||
if (cached) {
|
||||
logger.debug('VIN decode cache hit', { vin });
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Call vPIC API
|
||||
logger.info('Calling vPIC API', { vin });
|
||||
const response = await axios.get<VPICResponse>(
|
||||
`${this.baseURL}/DecodeVin/${vin}?format=json`
|
||||
);
|
||||
|
||||
if (response.data.Count === 0) {
|
||||
logger.warn('VIN decode returned no results', { vin });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse response
|
||||
const result = this.parseVPICResponse(response.data);
|
||||
|
||||
// Cache successful result
|
||||
if (result) {
|
||||
await cacheService.set(cacheKey, result, this.cacheTTL);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('VIN decode failed', { vin, error });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseVPICResponse(response: VPICResponse): VPICDecodeResult | null {
|
||||
const getValue = (variable: string): string | undefined => {
|
||||
const result = response.Results.find(r => r.Variable === variable);
|
||||
return result?.Value || undefined;
|
||||
};
|
||||
|
||||
const make = getValue('Make');
|
||||
const model = getValue('Model');
|
||||
const year = getValue('Model Year');
|
||||
|
||||
if (!make || !model || !year) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
make,
|
||||
model,
|
||||
year: parseInt(year, 10),
|
||||
engineType: getValue('Engine Model'),
|
||||
bodyType: getValue('Body Class'),
|
||||
rawData: response.Results,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const vpicClient = new VPICClient();
|
||||
26
backend/src/features/vehicles/external/vpic/vpic.types.ts
vendored
Normal file
26
backend/src/features/vehicles/external/vpic/vpic.types.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @ai-summary NHTSA vPIC API types
|
||||
*/
|
||||
|
||||
export interface VPICResponse {
|
||||
Count: number;
|
||||
Message: string;
|
||||
SearchCriteria: string;
|
||||
Results: VPICResult[];
|
||||
}
|
||||
|
||||
export interface VPICResult {
|
||||
Value: string | null;
|
||||
ValueId: string | null;
|
||||
Variable: string;
|
||||
VariableId: number;
|
||||
}
|
||||
|
||||
export interface VPICDecodeResult {
|
||||
make: string;
|
||||
model: string;
|
||||
year: number;
|
||||
engineType?: string;
|
||||
bodyType?: string;
|
||||
rawData: VPICResult[];
|
||||
}
|
||||
Reference in New Issue
Block a user