Homepage Redesign
This commit is contained in:
156
backend/src/features/platform/domain/vin-decode.service.ts
Normal file
156
backend/src/features/platform/domain/vin-decode.service.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user