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:
Sase Dev
2026-03-06 20:31:20 +00:00
parent 24c8f066d5
commit 9d2e534e56
5 changed files with 135 additions and 120 deletions

View File

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

View File

@@ -201,6 +201,19 @@ export function SettingsContent() {
onChange={(e) => setPhone(e.target.value)}
/>
</div>
{user?.createdAt && (
<div className="space-y-2">
<Label>{t("settings.profile.memberSince")}</Label>
<Input
value={new Date(user.createdAt).toLocaleDateString("tr-TR", {
day: "numeric",
month: "long",
year: "numeric",
})}
disabled
/>
</div>
)}
<Button type="submit" disabled={savingProfile}>
{savingProfile ? t("common.saving") : t("common.save")}
</Button>

View File

@@ -257,6 +257,7 @@
"email": "Email",
"phone": "Phone",
"phonePlaceholder": "+90 5xx xxx xx xx",
"memberSince": "Member Since",
"updated": "Profile updated.",
"updateFailed": "Failed to update profile."
},

View File

@@ -257,6 +257,7 @@
"email": "E-posta",
"phone": "Telefon",
"phonePlaceholder": "+90 5xx xxx xx xx",
"memberSince": "Kayıt Tarihi",
"updated": "Profil güncellendi.",
"updateFailed": "Profil güncellenirken hata oluştu."
},

View File

@@ -7,6 +7,7 @@ interface User {
image: string | null;
role: string;
referralCode: string | null;
createdAt: string | Date | null;
}
interface AuthState {