fix: Remove VIN Cache
All checks were successful
Deploy to Staging / Build Images (push) Successful in 3m36s
Deploy to Staging / Deploy to Staging (push) Successful in 52s
Deploy to Staging / Verify Staging (push) Successful in 8s
Deploy to Staging / Notify Staging Ready (push) Successful in 8s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Mirror Base Images / Mirror Base Images (push) Successful in 42s
All checks were successful
Deploy to Staging / Build Images (push) Successful in 3m36s
Deploy to Staging / Deploy to Staging (push) Successful in 52s
Deploy to Staging / Verify Staging (push) Successful in 8s
Deploy to Staging / Notify Staging Ready (push) Successful in 8s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Mirror Base Images / Mirror Base Images (push) Successful in 42s
This commit is contained in:
@@ -406,20 +406,9 @@ export class VehiclesController {
|
|||||||
|
|
||||||
logger.info('VIN decode requested', { userId, vin: sanitizedVin.substring(0, 6) + '...' });
|
logger.info('VIN decode requested', { userId, vin: sanitizedVin.substring(0, 6) + '...' });
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
const cached = await this.vehiclesService.getVinCached(sanitizedVin);
|
|
||||||
if (cached) {
|
|
||||||
logger.info('VIN decode cache hit', { userId });
|
|
||||||
const decodedData = await this.vehiclesService.mapVinDecodeResponse(cached);
|
|
||||||
return reply.code(200).send(decodedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call OCR service for VIN decode
|
// Call OCR service for VIN decode
|
||||||
const response = await ocrClient.decodeVin(sanitizedVin);
|
const response = await ocrClient.decodeVin(sanitizedVin);
|
||||||
|
|
||||||
// Cache the response
|
|
||||||
await this.vehiclesService.saveVinCache(sanitizedVin, response);
|
|
||||||
|
|
||||||
// Map response to decoded vehicle data with dropdown matching
|
// Map response to decoded vehicle data with dropdown matching
|
||||||
const decodedData = await this.vehiclesService.mapVinDecodeResponse(response);
|
const decodedData = await this.vehiclesService.mapVinDecodeResponse(response);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @ai-summary Business logic for vehicles feature
|
* @ai-summary Business logic for vehicles feature
|
||||||
* @ai-context Handles VIN decoding, caching, and business rules
|
* @ai-context Handles VIN decoding and business rules
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pool } from 'pg';
|
import { Pool } from 'pg';
|
||||||
@@ -594,72 +594,6 @@ export class VehiclesService {
|
|||||||
await cacheService.del(cacheKey);
|
await cacheService.del(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check vin_cache for existing VIN data.
|
|
||||||
* Format-aware: validates raw_data has `success` field (Gemini format).
|
|
||||||
* Old NHTSA-format entries are treated as cache misses and expire via TTL.
|
|
||||||
*/
|
|
||||||
async getVinCached(vin: string): Promise<VinDecodeResponse | null> {
|
|
||||||
try {
|
|
||||||
const result = await this.pool.query<{
|
|
||||||
raw_data: any;
|
|
||||||
cached_at: Date;
|
|
||||||
}>(
|
|
||||||
`SELECT raw_data, cached_at
|
|
||||||
FROM vin_cache
|
|
||||||
WHERE vin = $1
|
|
||||||
AND cached_at > NOW() - INTERVAL '365 days'`,
|
|
||||||
[vin]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawData = result.rows[0].raw_data;
|
|
||||||
|
|
||||||
// Format-aware check: Gemini responses have `success` field,
|
|
||||||
// old NHTSA responses do not. Treat old format as cache miss.
|
|
||||||
if (!rawData || typeof rawData !== 'object' || !('success' in rawData)) {
|
|
||||||
logger.debug('VIN cache format mismatch (legacy NHTSA entry), treating as miss', { vin });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug('VIN cache hit', { vin });
|
|
||||||
return rawData as VinDecodeResponse;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to check VIN cache', { vin, error });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save VIN decode response to cache with ON CONFLICT upsert.
|
|
||||||
*/
|
|
||||||
async saveVinCache(vin: string, response: VinDecodeResponse): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.pool.query(
|
|
||||||
`INSERT INTO vin_cache (vin, make, model, year, engine_type, body_type, raw_data, cached_at)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
|
||||||
ON CONFLICT (vin) DO UPDATE SET
|
|
||||||
make = EXCLUDED.make,
|
|
||||||
model = EXCLUDED.model,
|
|
||||||
year = EXCLUDED.year,
|
|
||||||
engine_type = EXCLUDED.engine_type,
|
|
||||||
body_type = EXCLUDED.body_type,
|
|
||||||
raw_data = EXCLUDED.raw_data,
|
|
||||||
cached_at = NOW()
|
|
||||||
WHERE (vin_cache.raw_data->>'confidence')::float <= $8`,
|
|
||||||
[vin, response.make, response.model, response.year, response.engine, response.bodyType, JSON.stringify(response), response.confidence ?? 1]
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.debug('VIN cached', { vin, confidence: response.confidence });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to cache VIN data', { vin, error });
|
|
||||||
// Don't throw - caching failure shouldn't break the decode flow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDropdownMakes(year: number): Promise<string[]> {
|
async getDropdownMakes(year: number): Promise<string[]> {
|
||||||
const vehicleDataService = getVehicleDataService();
|
const vehicleDataService = getVehicleDataService();
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|||||||
@@ -242,18 +242,6 @@ export interface DecodedVehicleData {
|
|||||||
transmission: MatchedField<string>;
|
transmission: MatchedField<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cached VIN data from vin_cache table */
|
|
||||||
export interface VinCacheEntry {
|
|
||||||
vin: string;
|
|
||||||
make: string | null;
|
|
||||||
model: string | null;
|
|
||||||
year: number | null;
|
|
||||||
engineType: string | null;
|
|
||||||
bodyType: string | null;
|
|
||||||
rawData: import('../../ocr/domain/ocr.types').VinDecodeResponse;
|
|
||||||
cachedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** VIN decode request body */
|
/** VIN decode request body */
|
||||||
export interface DecodeVinRequest {
|
export interface DecodeVinRequest {
|
||||||
vin: string;
|
vin: string;
|
||||||
|
|||||||
@@ -36,15 +36,13 @@ describe('Vehicles Integration Tests', () => {
|
|||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
// Clean up test database
|
// Clean up test database
|
||||||
await pool.query('DROP TABLE IF EXISTS vehicles CASCADE');
|
await pool.query('DROP TABLE IF EXISTS vehicles CASCADE');
|
||||||
await pool.query('DROP TABLE IF EXISTS vin_cache CASCADE');
|
|
||||||
await pool.query('DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE');
|
await pool.query('DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE');
|
||||||
await pool.end();
|
await pool.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Clean up test data before each test - more thorough cleanup
|
// Clean up test data before each test
|
||||||
await pool.query('DELETE FROM vehicles WHERE user_id = $1', ['test-user-123']);
|
await pool.query('DELETE FROM vehicles WHERE user_id = $1', ['test-user-123']);
|
||||||
await pool.query('DELETE FROM vin_cache WHERE vin LIKE $1', ['1HGBH41JXMN%']);
|
|
||||||
|
|
||||||
// Clear Redis cache for the test user
|
// Clear Redis cache for the test user
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user