Initial Commit

This commit is contained in:
Eric Gullickson
2025-09-17 16:09:15 -05:00
parent 0cdb9803de
commit a052040e3a
373 changed files with 437090 additions and 6773 deletions

View File

@@ -0,0 +1,293 @@
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;
apiKey: string;
tenantId?: 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();
private readonly tenantId: string | undefined;
constructor(config: PlatformVehiclesClientConfig) {
this.logger = config.logger;
this.tenantId = config.tenantId || process.env.TENANT_ID;
// Initialize HTTP client
this.httpClient = axios.create({
baseURL: config.baseURL,
timeout: config.timeout || 3000,
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
},
});
// Inject tenant header for all requests when available
if (this.tenantId) {
this.httpClient.defaults.headers.common['X-Tenant-ID'] = this.tenantId;
}
// 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;
}
}
}

View File

@@ -0,0 +1,91 @@
// 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;
};
}