Homepage Redesign
This commit is contained in:
@@ -1,283 +0,0 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import CircuitBreaker from 'opossum';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export interface MakeItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ModelItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TrimItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface EngineItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TransmissionItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface VINDecodeResult {
|
||||
make?: string;
|
||||
model?: string;
|
||||
year?: number;
|
||||
trim_name?: string;
|
||||
engine_description?: string;
|
||||
transmission_description?: string;
|
||||
confidence_score?: number;
|
||||
vehicle_type?: string;
|
||||
}
|
||||
|
||||
export interface VINDecodeResponse {
|
||||
vin: string;
|
||||
result?: VINDecodeResult;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PlatformVehiclesClientConfig {
|
||||
baseURL: string;
|
||||
timeout?: number;
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client for MVP Platform Vehicles Service
|
||||
* Provides hierarchical vehicle API and VIN decoding with circuit breaker pattern
|
||||
*/
|
||||
export class PlatformVehiclesClient {
|
||||
private readonly httpClient: AxiosInstance;
|
||||
private readonly logger: Logger | undefined;
|
||||
private readonly circuitBreakers: Map<string, CircuitBreaker> = new Map();
|
||||
|
||||
constructor(config: PlatformVehiclesClientConfig) {
|
||||
this.logger = config.logger;
|
||||
|
||||
// Initialize HTTP client
|
||||
this.httpClient = axios.create({
|
||||
baseURL: config.baseURL,
|
||||
timeout: config.timeout || 3000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Setup response interceptors for logging
|
||||
this.httpClient.interceptors.response.use(
|
||||
(response) => {
|
||||
const processingTime = response.headers['x-process-time'];
|
||||
if (processingTime) {
|
||||
this.logger?.debug(`Platform API response time: ${processingTime}ms`);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
this.logger?.error(`Platform API error: ${error.message}`);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Initialize circuit breakers for each endpoint
|
||||
this.initializeCircuitBreakers();
|
||||
}
|
||||
|
||||
private initializeCircuitBreakers(): void {
|
||||
const circuitBreakerOptions = {
|
||||
timeout: 3000,
|
||||
errorThresholdPercentage: 50,
|
||||
resetTimeout: 30000,
|
||||
name: 'platform-vehicles',
|
||||
};
|
||||
|
||||
// Create circuit breakers for each endpoint type
|
||||
const endpoints = ['years', 'makes', 'models', 'trims', 'engines', 'transmissions', 'vindecode'];
|
||||
|
||||
endpoints.forEach(endpoint => {
|
||||
const breaker = new CircuitBreaker(this.makeRequest.bind(this), {
|
||||
...circuitBreakerOptions,
|
||||
name: `platform-vehicles-${endpoint}`,
|
||||
});
|
||||
|
||||
// Setup fallback handlers
|
||||
breaker.fallback(() => {
|
||||
this.logger?.warn(`Circuit breaker fallback triggered for ${endpoint}`);
|
||||
return this.getFallbackResponse(endpoint);
|
||||
});
|
||||
|
||||
// Setup event handlers
|
||||
breaker.on('open', () => {
|
||||
this.logger?.error(`Circuit breaker opened for ${endpoint}`);
|
||||
});
|
||||
|
||||
breaker.on('halfOpen', () => {
|
||||
this.logger?.info(`Circuit breaker half-open for ${endpoint}`);
|
||||
});
|
||||
|
||||
breaker.on('close', () => {
|
||||
this.logger?.info(`Circuit breaker closed for ${endpoint}`);
|
||||
});
|
||||
|
||||
this.circuitBreakers.set(endpoint, breaker);
|
||||
});
|
||||
}
|
||||
|
||||
private async makeRequest(endpoint: string, params?: Record<string, any>): Promise<any> {
|
||||
const response = await this.httpClient.get(`/api/v1/vehicles/${endpoint}`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
private getFallbackResponse(endpoint: string): any {
|
||||
// Return empty arrays/objects for fallback
|
||||
switch (endpoint) {
|
||||
case 'makes':
|
||||
return { makes: [] };
|
||||
case 'models':
|
||||
return { models: [] };
|
||||
case 'trims':
|
||||
return { trims: [] };
|
||||
case 'engines':
|
||||
return { engines: [] };
|
||||
case 'transmissions':
|
||||
return { transmissions: [] };
|
||||
case 'vindecode':
|
||||
return { vin: '', result: null, success: false, error: 'Service unavailable' };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available model years
|
||||
*/
|
||||
async getYears(): Promise<number[]> {
|
||||
const breaker = this.circuitBreakers.get('years')!;
|
||||
try {
|
||||
const response: any = await breaker.fire('years');
|
||||
return Array.isArray(response) ? response : [];
|
||||
} catch (error) {
|
||||
this.logger?.error(`Failed to get years: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get makes for a specific year
|
||||
* Hierarchical API: First level - requires year only
|
||||
*/
|
||||
async getMakes(year: number): Promise<MakeItem[]> {
|
||||
const breaker = this.circuitBreakers.get('makes')!;
|
||||
|
||||
try {
|
||||
const response: any = await breaker.fire('makes', { year });
|
||||
this.logger?.debug(`Retrieved ${response.makes?.length || 0} makes for year ${year}`);
|
||||
return response.makes || [];
|
||||
} catch (error) {
|
||||
this.logger?.error(`Failed to get makes for year ${year}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get models for year and make
|
||||
* Hierarchical API: Second level - requires year and make_id
|
||||
*/
|
||||
async getModels(year: number, makeId: number): Promise<ModelItem[]> {
|
||||
const breaker = this.circuitBreakers.get('models')!;
|
||||
|
||||
try {
|
||||
const response: any = await breaker.fire('models', { year, make_id: makeId });
|
||||
this.logger?.debug(`Retrieved ${response.models?.length || 0} models for year ${year}, make ${makeId}`);
|
||||
return response.models || [];
|
||||
} catch (error) {
|
||||
this.logger?.error(`Failed to get models for year ${year}, make ${makeId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trims for year, make, and model
|
||||
* Hierarchical API: Third level - requires year, make_id, and model_id
|
||||
*/
|
||||
async getTrims(year: number, makeId: number, modelId: number): Promise<TrimItem[]> {
|
||||
const breaker = this.circuitBreakers.get('trims')!;
|
||||
|
||||
try {
|
||||
const response: any = await breaker.fire('trims', { year, make_id: makeId, model_id: modelId });
|
||||
this.logger?.debug(`Retrieved ${response.trims?.length || 0} trims for year ${year}, make ${makeId}, model ${modelId}`);
|
||||
return response.trims || [];
|
||||
} catch (error) {
|
||||
this.logger?.error(`Failed to get trims for year ${year}, make ${makeId}, model ${modelId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engines for year, make, and model
|
||||
* Hierarchical API: Third level - requires year, make_id, and model_id
|
||||
*/
|
||||
async getEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<EngineItem[]> {
|
||||
const breaker = this.circuitBreakers.get('engines')!;
|
||||
|
||||
try {
|
||||
const response: any = await breaker.fire('engines', { year, make_id: makeId, model_id: modelId, trim_id: trimId });
|
||||
this.logger?.debug(`Retrieved ${response.engines?.length || 0} engines for year ${year}, make ${makeId}, model ${modelId}, trim ${trimId}`);
|
||||
return response.engines || [];
|
||||
} catch (error) {
|
||||
this.logger?.error(`Failed to get engines for year ${year}, make ${makeId}, model ${modelId}, trim ${trimId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transmissions for year, make, and model
|
||||
* Hierarchical API: Third level - requires year, make_id, and model_id
|
||||
*/
|
||||
async getTransmissions(year: number, makeId: number, modelId: number): Promise<TransmissionItem[]> {
|
||||
const breaker = this.circuitBreakers.get('transmissions')!;
|
||||
|
||||
try {
|
||||
const response: any = await breaker.fire('transmissions', { year, make_id: makeId, model_id: modelId });
|
||||
this.logger?.debug(`Retrieved ${response.transmissions?.length || 0} transmissions for year ${year}, make ${makeId}, model ${modelId}`);
|
||||
return response.transmissions || [];
|
||||
} catch (error) {
|
||||
this.logger?.error(`Failed to get transmissions for year ${year}, make ${makeId}, model ${modelId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode VIN using platform service
|
||||
* Uses PostgreSQL vpic.f_decode_vin() function with confidence scoring
|
||||
*/
|
||||
async decodeVIN(vin: string): Promise<VINDecodeResponse> {
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.post('/api/v1/vehicles/vindecode', { vin });
|
||||
this.logger?.debug(`VIN decode response for ${vin}: success=${response.data.success}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger?.error(`Failed to decode VIN ${vin}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check for the platform service
|
||||
*/
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
await this.httpClient.get('/health');
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger?.error(`Platform service health check failed: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Types for MVP Platform Vehicles Service integration
|
||||
// These types match the FastAPI response models
|
||||
|
||||
export interface MakeItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ModelItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TrimItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface EngineItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TransmissionItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface MakesResponse {
|
||||
makes: MakeItem[];
|
||||
}
|
||||
|
||||
export interface ModelsResponse {
|
||||
models: ModelItem[];
|
||||
}
|
||||
|
||||
export interface TrimsResponse {
|
||||
trims: TrimItem[];
|
||||
}
|
||||
|
||||
export interface EnginesResponse {
|
||||
engines: EngineItem[];
|
||||
}
|
||||
|
||||
export interface TransmissionsResponse {
|
||||
transmissions: TransmissionItem[];
|
||||
}
|
||||
|
||||
export interface VINDecodeResult {
|
||||
make?: string;
|
||||
model?: string;
|
||||
year?: number;
|
||||
trim_name?: string;
|
||||
engine_description?: string;
|
||||
transmission_description?: string;
|
||||
horsepower?: number;
|
||||
torque?: number; // ft-lb
|
||||
top_speed?: number; // mph
|
||||
fuel?: 'gasoline' | 'diesel' | 'electric';
|
||||
confidence_score?: number;
|
||||
vehicle_type?: string;
|
||||
}
|
||||
|
||||
export interface VINDecodeRequest {
|
||||
vin: string;
|
||||
}
|
||||
|
||||
export interface VINDecodeResponse {
|
||||
vin: string;
|
||||
result?: VINDecodeResult;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
status: string;
|
||||
database: string;
|
||||
cache: string;
|
||||
version: string;
|
||||
etl_last_run?: string;
|
||||
}
|
||||
|
||||
// Configuration for platform vehicles client
|
||||
export interface PlatformVehiclesConfig {
|
||||
baseURL: string;
|
||||
apiKey: string;
|
||||
timeout?: number;
|
||||
retryAttempts?: number;
|
||||
circuitBreakerOptions?: {
|
||||
timeout: number;
|
||||
errorThresholdPercentage: number;
|
||||
resetTimeout: number;
|
||||
};
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
/**
|
||||
* @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 { appConfig } from '../../../../core/config/config-loader';
|
||||
import { logger } from '../../../../core/logging/logger';
|
||||
import { cacheService } from '../../../../core/config/redis';
|
||||
import {
|
||||
VPICResponse,
|
||||
VPICDecodeResult,
|
||||
VPICMake,
|
||||
VPICModel,
|
||||
VPICTransmission,
|
||||
VPICEngine,
|
||||
VPICTrim,
|
||||
DropdownDataResponse
|
||||
} from './vpic.types';
|
||||
|
||||
export class VPICClient {
|
||||
private readonly baseURL = appConfig.config.external.vpic.url;
|
||||
private readonly cacheTTL = 30 * 24 * 60 * 60; // 30 days in seconds
|
||||
private readonly dropdownCacheTTL = 7 * 24 * 60 * 60; // 7 days for dropdown data
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
async getAllMakes(): Promise<VPICMake[]> {
|
||||
const cacheKey = 'vpic:makes';
|
||||
|
||||
try {
|
||||
const cached = await cacheService.get<VPICMake[]>(cacheKey);
|
||||
if (cached) {
|
||||
logger.debug('Makes cache hit');
|
||||
return cached;
|
||||
}
|
||||
|
||||
logger.info('Calling vPIC API for makes');
|
||||
const response = await axios.get<{ Count: number; Message: string; Results: VPICMake[] }>(
|
||||
`${this.baseURL}/GetAllMakes?format=json`
|
||||
);
|
||||
|
||||
const makes = response.data.Results || [];
|
||||
await cacheService.set(cacheKey, makes, this.dropdownCacheTTL);
|
||||
|
||||
return makes;
|
||||
} catch (error) {
|
||||
logger.error('Get makes failed', { error });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getModelsForMake(make: string): Promise<VPICModel[]> {
|
||||
const cacheKey = `vpic:models:${make}`;
|
||||
|
||||
try {
|
||||
const cached = await cacheService.get<VPICModel[]>(cacheKey);
|
||||
if (cached) {
|
||||
logger.debug('Models cache hit', { make });
|
||||
return cached;
|
||||
}
|
||||
|
||||
logger.info('Calling vPIC API for models', { make });
|
||||
const response = await axios.get<{ Count: number; Message: string; Results: VPICModel[] }>(
|
||||
`${this.baseURL}/GetModelsForMake/${encodeURIComponent(make)}?format=json`
|
||||
);
|
||||
|
||||
const models = response.data.Results || [];
|
||||
await cacheService.set(cacheKey, models, this.dropdownCacheTTL);
|
||||
|
||||
return models;
|
||||
} catch (error) {
|
||||
logger.error('Get models failed', { make, error });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getTransmissionTypes(): Promise<VPICTransmission[]> {
|
||||
return this.getVariableValues('Transmission Style', 'transmissions');
|
||||
}
|
||||
|
||||
async getEngineConfigurations(): Promise<VPICEngine[]> {
|
||||
return this.getVariableValues('Engine Configuration', 'engines');
|
||||
}
|
||||
|
||||
async getTrimLevels(): Promise<VPICTrim[]> {
|
||||
return this.getVariableValues('Trim', 'trims');
|
||||
}
|
||||
|
||||
private async getVariableValues(
|
||||
variable: string,
|
||||
cachePrefix: string
|
||||
): Promise<VPICTransmission[] | VPICEngine[] | VPICTrim[]> {
|
||||
const cacheKey = `vpic:${cachePrefix}`;
|
||||
|
||||
try {
|
||||
const cached = await cacheService.get<VPICTransmission[]>(cacheKey);
|
||||
if (cached) {
|
||||
logger.debug('Variable values cache hit', { variable });
|
||||
return cached;
|
||||
}
|
||||
|
||||
logger.info('Calling vPIC API for variable values', { variable });
|
||||
const response = await axios.get<DropdownDataResponse>(
|
||||
`${this.baseURL}/GetVehicleVariableValuesList/${encodeURIComponent(variable)}?format=json`
|
||||
);
|
||||
|
||||
const values = response.data.Results || [];
|
||||
await cacheService.set(cacheKey, values, this.dropdownCacheTTL);
|
||||
|
||||
return values;
|
||||
} catch (error) {
|
||||
logger.error('Get variable values failed', { variable, error });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const vpicClient = new VPICClient();
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* @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[];
|
||||
}
|
||||
|
||||
export interface VPICMake {
|
||||
Make_ID: number;
|
||||
Make_Name: string;
|
||||
}
|
||||
|
||||
export interface VPICModel {
|
||||
Make_ID: number;
|
||||
Make_Name: string;
|
||||
Model_ID: number;
|
||||
Model_Name: string;
|
||||
}
|
||||
|
||||
export interface VPICDropdownItem {
|
||||
Id: number;
|
||||
Name: string;
|
||||
}
|
||||
|
||||
export interface VPICTransmission extends VPICDropdownItem {}
|
||||
export interface VPICEngine extends VPICDropdownItem {}
|
||||
export interface VPICTrim extends VPICDropdownItem {}
|
||||
|
||||
export interface DropdownDataResponse {
|
||||
Count: number;
|
||||
Message: string;
|
||||
SearchCriteria: string;
|
||||
Results: VPICDropdownItem[];
|
||||
}
|
||||
Reference in New Issue
Block a user