157 lines
4.8 KiB
TypeScript
157 lines
4.8 KiB
TypeScript
/**
|
|
* @ai-summary VIN decoding service with circuit breaker and fallback
|
|
* @ai-context PostgreSQL first, vPIC API fallback, Redis caching
|
|
*/
|
|
import { Pool } from 'pg';
|
|
import CircuitBreaker from 'opossum';
|
|
import { VehicleDataRepository } from '../data/vehicle-data.repository';
|
|
import { VPICClient } from '../data/vpic-client';
|
|
import { PlatformCacheService } from './platform-cache.service';
|
|
import { VINDecodeResponse, VINDecodeResult } from '../models/responses';
|
|
import { logger } from '../../../core/logging/logger';
|
|
|
|
export class VINDecodeService {
|
|
private repository: VehicleDataRepository;
|
|
private vpicClient: VPICClient;
|
|
private cache: PlatformCacheService;
|
|
private circuitBreaker: CircuitBreaker;
|
|
|
|
constructor(cache: PlatformCacheService) {
|
|
this.cache = cache;
|
|
this.repository = new VehicleDataRepository();
|
|
this.vpicClient = new VPICClient();
|
|
|
|
this.circuitBreaker = new CircuitBreaker(
|
|
async (vin: string) => this.vpicClient.decodeVIN(vin),
|
|
{
|
|
timeout: 6000,
|
|
errorThresholdPercentage: 50,
|
|
resetTimeout: 30000,
|
|
name: 'vpic-api'
|
|
}
|
|
);
|
|
|
|
this.circuitBreaker.on('open', () => {
|
|
logger.warn('Circuit breaker opened for vPIC API');
|
|
});
|
|
|
|
this.circuitBreaker.on('halfOpen', () => {
|
|
logger.info('Circuit breaker half-open for vPIC API');
|
|
});
|
|
|
|
this.circuitBreaker.on('close', () => {
|
|
logger.info('Circuit breaker closed for vPIC API');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validate VIN format
|
|
*/
|
|
validateVIN(vin: string): { valid: boolean; error?: string } {
|
|
if (vin.length !== 17) {
|
|
return { valid: false, error: 'VIN must be exactly 17 characters' };
|
|
}
|
|
|
|
const invalidChars = /[IOQ]/i;
|
|
if (invalidChars.test(vin)) {
|
|
return { valid: false, error: 'VIN contains invalid characters (cannot contain I, O, Q)' };
|
|
}
|
|
|
|
const validFormat = /^[A-HJ-NPR-Z0-9]{17}$/i;
|
|
if (!validFormat.test(vin)) {
|
|
return { valid: false, error: 'VIN contains invalid characters' };
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Decode VIN with multi-tier strategy:
|
|
* 1. Check cache
|
|
* 2. Try PostgreSQL function
|
|
* 3. Fallback to vPIC API (with circuit breaker)
|
|
*/
|
|
async decodeVIN(pool: Pool, vin: string): Promise<VINDecodeResponse> {
|
|
const normalizedVIN = vin.toUpperCase().trim();
|
|
|
|
const validation = this.validateVIN(normalizedVIN);
|
|
if (!validation.valid) {
|
|
return {
|
|
vin: normalizedVIN,
|
|
result: null,
|
|
success: false,
|
|
error: validation.error
|
|
};
|
|
}
|
|
|
|
try {
|
|
const cached = await this.cache.getVINDecode(normalizedVIN);
|
|
if (cached) {
|
|
logger.debug('VIN decode result retrieved from cache', { vin: normalizedVIN });
|
|
return cached;
|
|
}
|
|
|
|
let result = await this.repository.decodeVIN(pool, normalizedVIN);
|
|
|
|
if (result) {
|
|
const response: VINDecodeResponse = {
|
|
vin: normalizedVIN,
|
|
result,
|
|
success: true
|
|
};
|
|
await this.cache.setVINDecode(normalizedVIN, response, true);
|
|
logger.info('VIN decoded successfully via PostgreSQL', { vin: normalizedVIN, make: result.make, model: result.model, year: result.year });
|
|
return response;
|
|
}
|
|
|
|
logger.info('VIN not found in PostgreSQL, attempting vPIC fallback', { vin: normalizedVIN });
|
|
|
|
try {
|
|
result = await this.circuitBreaker.fire(normalizedVIN) as VINDecodeResult | null;
|
|
|
|
if (result) {
|
|
const response: VINDecodeResponse = {
|
|
vin: normalizedVIN,
|
|
result,
|
|
success: true
|
|
};
|
|
await this.cache.setVINDecode(normalizedVIN, response, true);
|
|
logger.info('VIN decoded successfully via vPIC fallback', { vin: normalizedVIN, make: result.make, model: result.model, year: result.year });
|
|
return response;
|
|
}
|
|
} catch (circuitError) {
|
|
logger.warn('vPIC API unavailable or circuit breaker open', { vin: normalizedVIN, error: circuitError });
|
|
}
|
|
|
|
const failureResponse: VINDecodeResponse = {
|
|
vin: normalizedVIN,
|
|
result: null,
|
|
success: false,
|
|
error: 'VIN not found in database and external API unavailable'
|
|
};
|
|
|
|
await this.cache.setVINDecode(normalizedVIN, failureResponse, false);
|
|
return failureResponse;
|
|
|
|
} catch (error) {
|
|
logger.error('VIN decode error', { vin: normalizedVIN, error });
|
|
return {
|
|
vin: normalizedVIN,
|
|
result: null,
|
|
success: false,
|
|
error: 'Internal server error during VIN decoding'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get circuit breaker status
|
|
*/
|
|
getCircuitBreakerStatus(): { state: string; stats: any } {
|
|
return {
|
|
state: this.circuitBreaker.opened ? 'open' : this.circuitBreaker.halfOpen ? 'half-open' : 'closed',
|
|
stats: this.circuitBreaker.stats
|
|
};
|
|
}
|
|
}
|