|
|
|
|
@@ -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,
|
|
|
|
|
|