feat: parallel VIN decode (pcat+emex) + member since in profile settings
- Refactor VIN decode chain to launch PartsCatalogs and EMEX in parallel; EMEX is race-capped at 3 s — if it doesn't resolve in time, PL24 takes over - Decision tree: pcat=1 → use pcat; else await EMEX result; else PL24; else candidates - Add "Member Since" (Kayıt Tarihi) field to profile settings, sourced from user.createdAt - Add createdAt to auth store User interface Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -230,81 +230,120 @@ export class VehiclesService {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Corgi bypassed — go straight to external sources
|
||||
const corgiResult = null;
|
||||
const corgiKnown = false;
|
||||
let brandName: string | null = null;
|
||||
|
||||
// 2. PartsCatalogs (first external source)
|
||||
let pcatCandidates: PcatCar[] | null = null;
|
||||
try {
|
||||
const pcatResult = await this.partsCatalogsService.decodeVin(vin);
|
||||
if (pcatResult?.cars?.length === 1) {
|
||||
// Single car → use directly
|
||||
const car = pcatResult.cars[0];
|
||||
if (!brandName) brandName = this.extractBrandFromPcatCar(car) || null;
|
||||
const result: VinResolveResult = {
|
||||
brandName,
|
||||
model: car.name || null,
|
||||
year: this.extractYearFromPcatCar(car) || corgiResult?.modelYear || null,
|
||||
engine: this.extractParamFromPcatCar(car, "engine") || null,
|
||||
transmission: this.extractParamFromPcatCar(car, "transmission") || null,
|
||||
bodyType: this.extractParamFromPcatCar(car, "body") || null,
|
||||
rawData: {
|
||||
source: "parts-catalogs",
|
||||
catalogId: car.catalogId,
|
||||
carId: car.id,
|
||||
parameters: car.parameters || [],
|
||||
pcatCar: car,
|
||||
},
|
||||
// ── Parallel: PartsCatalogs + EMEX (EMEX capped at 3s) ──────────────
|
||||
const EMEX_RACE_MS = 3000;
|
||||
|
||||
const pcatPromise = this.partsCatalogsService
|
||||
.decodeVin(vin)
|
||||
.catch((err: Error) => {
|
||||
this.logger.warn(`PartsCatalogs decode failed for ${vin}: ${err.message}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
const emexBasePromise = this.emexService
|
||||
.decodeVinOrCandidates(vin)
|
||||
.catch((err: Error) => {
|
||||
this.logger.warn(`EMEX decode failed for ${vin}: ${err.message}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
// Emex result capped at 3s — if it doesn't arrive in time, falls back to PL24
|
||||
const emexTimedPromise = Promise.race([
|
||||
emexBasePromise,
|
||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), EMEX_RACE_MS)),
|
||||
]);
|
||||
|
||||
// Wait for PartsCatalogs first
|
||||
const pcatResult = await pcatPromise;
|
||||
|
||||
// Decision 1: pcat returned exactly 1 car → use it, ignore EMEX
|
||||
if (pcatResult?.cars?.length === 1) {
|
||||
const car = pcatResult.cars[0];
|
||||
if (!brandName) brandName = this.extractBrandFromPcatCar(car) || null;
|
||||
const result: VinResolveResult = {
|
||||
brandName,
|
||||
model: car.name || null,
|
||||
year: this.extractYearFromPcatCar(car) || null,
|
||||
engine: this.extractParamFromPcatCar(car, "engine") || null,
|
||||
transmission: this.extractParamFromPcatCar(car, "transmission") || null,
|
||||
bodyType: this.extractParamFromPcatCar(car, "body") || null,
|
||||
rawData: {
|
||||
source: "parts-catalogs",
|
||||
corgiKnown,
|
||||
corgiResult: corgiResult || null,
|
||||
};
|
||||
await this.redis.setJson(cacheKey, result, 300);
|
||||
return result;
|
||||
}
|
||||
if (pcatResult?.cars && pcatResult.cars.length > 1) {
|
||||
pcatCandidates = pcatResult.cars;
|
||||
this.logger.log(`PartsCatalogs returned ${pcatCandidates.length} candidates for ${vin}`);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`PartsCatalogs decode failed for ${vin}: ${(err as Error).message}`);
|
||||
catalogId: car.catalogId,
|
||||
carId: car.id,
|
||||
parameters: car.parameters || [],
|
||||
pcatCar: car,
|
||||
},
|
||||
source: "parts-catalogs",
|
||||
corgiKnown,
|
||||
corgiResult: null,
|
||||
};
|
||||
await this.redis.setJson(cacheKey, result, 300);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 3. PL24 (if PC had multiple results, or PC failed entirely)
|
||||
let pl24Vehicle: any = null;
|
||||
// Decision 2: pcat ≠ 1 → check EMEX result (may already be resolved)
|
||||
const emexResult = await emexTimedPromise;
|
||||
const emexSingleVehicle =
|
||||
emexResult?.type === "vehicle" && emexResult.vehicle.brand !== "UNKNOWN"
|
||||
? emexResult.vehicle
|
||||
: null;
|
||||
|
||||
if (emexSingleVehicle) {
|
||||
// pcat >1 or 0 AND emex returned exactly 1 vehicle → use EMEX
|
||||
if (!brandName) brandName = emexSingleVehicle.brand || null;
|
||||
const result: VinResolveResult = {
|
||||
brandName,
|
||||
model: emexSingleVehicle.model || null,
|
||||
year: emexSingleVehicle.year || null,
|
||||
engine: emexSingleVehicle.engineCode || emexSingleVehicle.engineType || null,
|
||||
transmission: emexSingleVehicle.transmission || null,
|
||||
bodyType: emexSingleVehicle.bodyType || null,
|
||||
rawData: emexSingleVehicle.raw || null,
|
||||
source: "emex",
|
||||
corgiKnown,
|
||||
corgiResult: null,
|
||||
};
|
||||
await this.redis.setJson(cacheKey, result, 300);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Decision 3: EMEX timed out / returned multiple / returned nothing → PL24
|
||||
this.logger.log(`PL24 fallback triggered for ${vin} (EMEX: ${emexResult?.type ?? "timeout"})`);
|
||||
|
||||
if (this.pl24Service.isDecodeable(vin)) {
|
||||
try {
|
||||
pl24Vehicle = await this.pl24Service.decodeVin(vin, userId);
|
||||
if (!brandName && pl24Vehicle) {
|
||||
brandName = this.pl24Service.getBrandName(vin) || null;
|
||||
const pl24Vehicle = await this.pl24Service.decodeVin(vin, userId);
|
||||
if (pl24Vehicle) {
|
||||
if (!brandName) brandName = this.pl24Service.getBrandName(vin) || null;
|
||||
const result: VinResolveResult = {
|
||||
brandName,
|
||||
model: pl24Vehicle.model || null,
|
||||
year: pl24Vehicle.year || null,
|
||||
engine: pl24Vehicle.engineType || pl24Vehicle.engineCode || null,
|
||||
transmission: pl24Vehicle.transmission || null,
|
||||
bodyType: pl24Vehicle.bodyType || null,
|
||||
rawData: pl24Vehicle,
|
||||
source: "pl24",
|
||||
corgiKnown,
|
||||
corgiResult: null,
|
||||
};
|
||||
await this.redis.setJson(cacheKey, result, 300);
|
||||
return result;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`PL24 decode failed for ${vin}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If PL24 succeeded, use PL24 regardless of PC candidates
|
||||
if (pl24Vehicle) {
|
||||
const result: VinResolveResult = {
|
||||
brandName: brandName || corgiResult?.brandName || null,
|
||||
model: pl24Vehicle.model || null,
|
||||
year: pl24Vehicle.year || corgiResult?.modelYear || null,
|
||||
engine: pl24Vehicle.engineType || pl24Vehicle.engineCode || null,
|
||||
transmission: pl24Vehicle.transmission || null,
|
||||
bodyType: pl24Vehicle.bodyType || null,
|
||||
rawData: pl24Vehicle,
|
||||
source: "pl24",
|
||||
corgiKnown,
|
||||
corgiResult: corgiResult || null,
|
||||
};
|
||||
await this.redis.setJson(cacheKey, result, 300);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 3b. PL24 failed + PC had multiple candidates → return candidates for user selection
|
||||
if (pcatCandidates && pcatCandidates.length > 1) {
|
||||
// PL24 also failed — surface candidates for user selection (last resort)
|
||||
const pcatCandidates = pcatResult?.cars && pcatResult.cars.length > 1 ? pcatResult.cars : null;
|
||||
if (pcatCandidates) {
|
||||
this.logger.log(`Returning ${pcatCandidates.length} pcat candidates for ${vin}`);
|
||||
return {
|
||||
brandName,
|
||||
model: null,
|
||||
@@ -315,69 +354,29 @@ export class VehiclesService {
|
||||
rawData: null,
|
||||
source: "parts-catalogs",
|
||||
corgiKnown,
|
||||
corgiResult: corgiResult || null,
|
||||
corgiResult: null,
|
||||
pcatCandidates,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. EMEX fallback (single HTTP call — no double fetch)
|
||||
let emexVehicle: import("../integrations/emex/emex.types").DecodedVehicle | null = null;
|
||||
if (!pl24Vehicle) {
|
||||
try {
|
||||
const emexResult = await this.emexService.decodeVinOrCandidates(vin);
|
||||
if (emexResult.type === 'candidates') {
|
||||
this.logger.log(`EMEX returned ${emexResult.candidates.length} candidates for ${vin}`);
|
||||
return {
|
||||
brandName,
|
||||
model: null,
|
||||
year: null,
|
||||
engine: null,
|
||||
transmission: null,
|
||||
bodyType: null,
|
||||
rawData: null,
|
||||
source: "emex",
|
||||
corgiKnown,
|
||||
corgiResult: corgiResult || null,
|
||||
emexCandidates: emexResult.candidates,
|
||||
};
|
||||
}
|
||||
if (emexResult.type === 'vehicle' && emexResult.vehicle.brand !== "UNKNOWN") {
|
||||
emexVehicle = emexResult.vehicle;
|
||||
if (!brandName) brandName = emexResult.vehicle.brand || null;
|
||||
} else if (emexResult.type === 'error') {
|
||||
// HTTP failed — try browser fallback
|
||||
const fallback = await this.emexService.decodeVin(vin);
|
||||
if (fallback && fallback.brand !== "UNKNOWN") {
|
||||
emexVehicle = fallback;
|
||||
if (!brandName) brandName = fallback.brand || null;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX fallback failed for ${vin}: ${(err as Error).message}`);
|
||||
}
|
||||
if (emexResult?.type === "candidates" && emexResult.candidates.length > 1) {
|
||||
this.logger.log(`Returning ${emexResult.candidates.length} EMEX candidates for ${vin}`);
|
||||
return {
|
||||
brandName,
|
||||
model: null,
|
||||
year: null,
|
||||
engine: null,
|
||||
transmission: null,
|
||||
bodyType: null,
|
||||
rawData: null,
|
||||
source: "emex",
|
||||
corgiKnown,
|
||||
corgiResult: null,
|
||||
emexCandidates: emexResult.candidates,
|
||||
};
|
||||
}
|
||||
|
||||
// Nothing recognized this VIN
|
||||
if (!emexVehicle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedSource = emexVehicle ? "emex" : "corgi";
|
||||
const result: VinResolveResult = {
|
||||
brandName: brandName || corgiResult?.brandName || emexVehicle?.brand || null,
|
||||
model: emexVehicle?.model || null,
|
||||
year: emexVehicle?.year || corgiResult?.modelYear || null,
|
||||
engine: emexVehicle?.engineCode || emexVehicle?.engineType || null,
|
||||
transmission: emexVehicle?.transmission || null,
|
||||
bodyType: emexVehicle?.bodyType || null,
|
||||
rawData: emexVehicle?.raw || null,
|
||||
source: resolvedSource,
|
||||
corgiKnown,
|
||||
corgiResult: corgiResult || null,
|
||||
};
|
||||
|
||||
await this.redis.setJson(cacheKey, result, 300);
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -402,7 +401,7 @@ export class VehiclesService {
|
||||
return {
|
||||
brandName,
|
||||
model: car.name || null,
|
||||
year: this.extractYearFromPcatCar(car) || corgiResult?.modelYear || null,
|
||||
year: this.extractYearFromPcatCar(car) || null,
|
||||
engine: this.extractParamFromPcatCar(car, "engine") || null,
|
||||
transmission: this.extractParamFromPcatCar(car, "transmission") || null,
|
||||
bodyType: this.extractParamFromPcatCar(car, "body") || null,
|
||||
@@ -433,12 +432,12 @@ export class VehiclesService {
|
||||
return null;
|
||||
}
|
||||
|
||||
const brandName = decoded.brand !== "UNKNOWN" ? decoded.brand : (corgiResult?.brandName || null);
|
||||
const brandName = decoded.brand !== "UNKNOWN" ? decoded.brand : null;
|
||||
|
||||
return {
|
||||
brandName,
|
||||
model: decoded.model || null,
|
||||
year: decoded.year || corgiResult?.modelYear || null,
|
||||
year: decoded.year || null,
|
||||
engine: decoded.engineCode || decoded.engineType || null,
|
||||
transmission: decoded.transmission || null,
|
||||
bodyType: decoded.bodyType || null,
|
||||
|
||||
Reference in New Issue
Block a user