feat: EMEX VIN decode → DB catalog entegrasyonu
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
- CATALOG_MAP'e ~40 yeni WMI eklendi (Mitsubishi, Nissan, Volvo, vb.) - brandFromCatalogCode listesi 55 DB kataloğuna genişletildi - brand="UNKNOWN" olsa bile catalogCode varsa EMEX sonucu kabul edilir - emex-catalog: linkPath'li kategorilerde parçalar DB'den yüklenir (scraper bug fix) - VIN cache: emex_vehicle_vins tablosu ile tekrar VIN'lerde emexdwc.ae atlanır - getCategoryTree Step 0: VIN cache lookup (~10ms) - Match sonrası otomatik VIN cache doldurma - Proaktif cache: ilk decode sonrası matchByName + saveVinCache Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { DATABASE, Database } from "../database/database.provider";
|
||||
import {
|
||||
emexCatalogs,
|
||||
emexVehicles,
|
||||
emexVehicleVins,
|
||||
emexPartGroups,
|
||||
emexParts,
|
||||
emexPartNumbers,
|
||||
@@ -77,6 +78,8 @@ export interface EmexVehicleMatch {
|
||||
candidates: EmexVehicleDto[];
|
||||
/** When DB has no match but wizard found the model, provides a QuickGroups URL for on-demand category fetch */
|
||||
wizardQuickGroupsUrl?: string | null;
|
||||
/** Brand name from emex_catalogs (populated by lookupVinCache) */
|
||||
brandName?: string | null;
|
||||
}
|
||||
|
||||
export interface EmexSearchResult {
|
||||
@@ -610,4 +613,85 @@ export class EmexCatalogService {
|
||||
await this.redis.setJson(cacheKey, match, CACHE_TTL.match);
|
||||
return match;
|
||||
}
|
||||
|
||||
// ── VIN Cache ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Look up a VIN in the emex_vehicle_vins cache table.
|
||||
* Returns the matched EmexVehicleMatch if found, null otherwise.
|
||||
*/
|
||||
async lookupVinCache(vin: string): Promise<EmexVehicleMatch | null> {
|
||||
const cacheKey = `emex:vincache:${vin}`;
|
||||
const cached = await this.redis.getJson<EmexVehicleMatch>(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const [row] = await this.db
|
||||
.select({
|
||||
emexVehicleId: emexVehicleVins.emexVehicleId,
|
||||
vehicleId: emexVehicles.vehicleId,
|
||||
name: emexVehicles.name,
|
||||
engine: emexVehicles.engine,
|
||||
engineCode: emexVehicles.engineCode,
|
||||
bodyType: emexVehicles.bodyType,
|
||||
transmission: emexVehicles.transmission,
|
||||
driveType: emexVehicles.driveType,
|
||||
fuelType: emexVehicles.fuelType,
|
||||
yearFrom: emexVehicles.yearFrom,
|
||||
yearTo: emexVehicles.yearTo,
|
||||
optionsRaw: emexVehicles.optionsRaw,
|
||||
catalogId: emexCatalogs.catalogId,
|
||||
brandName: emexCatalogs.brandName,
|
||||
})
|
||||
.from(emexVehicleVins)
|
||||
.innerJoin(emexVehicles, eq(emexVehicleVins.emexVehicleId, emexVehicles.id))
|
||||
.innerJoin(emexCatalogs, eq(emexVehicles.catalogId, emexCatalogs.id))
|
||||
.where(eq(emexVehicleVins.vin, vin))
|
||||
.limit(1);
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
const candidate: EmexVehicleDto = {
|
||||
id: row.emexVehicleId!,
|
||||
vehicleId: row.vehicleId,
|
||||
name: row.name,
|
||||
engine: row.engine,
|
||||
engineCode: row.engineCode,
|
||||
bodyType: row.bodyType,
|
||||
transmission: row.transmission,
|
||||
driveType: row.driveType,
|
||||
fuelType: row.fuelType,
|
||||
yearFrom: row.yearFrom,
|
||||
yearTo: row.yearTo,
|
||||
optionsRaw: row.optionsRaw,
|
||||
};
|
||||
|
||||
const match: EmexVehicleMatch = {
|
||||
vehicleId: row.emexVehicleId!,
|
||||
catalogCode: row.catalogId,
|
||||
vehicleName: row.name,
|
||||
candidates: [candidate],
|
||||
brandName: row.brandName,
|
||||
};
|
||||
|
||||
await this.redis.setJson(cacheKey, match, 86400); // 24h
|
||||
return match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a VIN → emex_vehicle mapping to the cache table.
|
||||
* Uses ON CONFLICT DO NOTHING to handle concurrent inserts.
|
||||
*/
|
||||
async saveVinCache(vin: string, emexVehicleId: string): Promise<void> {
|
||||
try {
|
||||
await this.db
|
||||
.insert(emexVehicleVins)
|
||||
.values({ vin, emexVehicleId })
|
||||
.onConflictDoNothing();
|
||||
this.logger.log(`VIN cache saved: ${vin} → ${emexVehicleId}`);
|
||||
// Invalidate Redis cache so next lookup picks up the DB row
|
||||
await this.redis.del(`emex:vincache:${vin}`);
|
||||
} catch (err) {
|
||||
this.logger.warn(`VIN cache save failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export class CategoriesService {
|
||||
}
|
||||
}
|
||||
|
||||
// If still no categories, try EMEX: pathData → name → ssd → on-demand fallback
|
||||
// If still no categories, try EMEX: VIN cache → pathData → name → ssd → on-demand fallback
|
||||
if (dbCategories.length === 0 && vehicle.vin) {
|
||||
this.logger.log(`No PL24 categories for ${vehicle.vin}, trying EMEX catalog match`);
|
||||
|
||||
@@ -232,6 +232,27 @@ export class CategoriesService {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: save VIN→emex_vehicle cache after successful match
|
||||
const cacheVinMatch = (match: { vehicleId: string }) => {
|
||||
if (vehicle.vin) {
|
||||
this.emexCatalogService.saveVinCache(vehicle.vin, match.vehicleId)
|
||||
.catch((err) => this.logger.warn(`VIN cache save failed: ${(err as Error).message}`));
|
||||
}
|
||||
};
|
||||
|
||||
// Step 0: VIN → emex_vehicle_vins cache (DB-only, ~10ms)
|
||||
if (dbCategories.length === 0) {
|
||||
try {
|
||||
const vinMatch = await this.emexCatalogService.lookupVinCache(vehicle.vin);
|
||||
if (vinMatch) {
|
||||
this.logger.log(`EMEX step 0: VIN cache hit → ${vinMatch.vehicleId}`);
|
||||
await insertEmexMatch(vinMatch);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX VIN cache lookup failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: pathData → extract vehicle name → matchByName (DB-only, ~50ms)
|
||||
if (catalogCode && dbCategories.length === 0) {
|
||||
const pathData = vRawData?.emexPathData as string | undefined;
|
||||
@@ -244,6 +265,7 @@ export class CategoriesService {
|
||||
if (match) {
|
||||
this.logger.log(`EMEX pathData match found: ${match.vehicleId}`);
|
||||
await insertEmexMatch(match);
|
||||
cacheVinMatch(match);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX pathData match failed: ${(err as Error).message}`);
|
||||
@@ -262,6 +284,7 @@ export class CategoriesService {
|
||||
if (match) {
|
||||
this.logger.log(`EMEX vehicleName match found: ${match.vehicleId}`);
|
||||
await insertEmexMatch(match);
|
||||
cacheVinMatch(match);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX vehicleName match failed: ${(err as Error).message}`);
|
||||
@@ -283,6 +306,7 @@ export class CategoriesService {
|
||||
if (match) {
|
||||
this.logger.log(`EMEX SSD match found: ${match.vehicleId}`);
|
||||
await insertEmexMatch(match);
|
||||
cacheVinMatch(match);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX SSD match failed: ${(err as Error).message}`);
|
||||
@@ -812,7 +836,52 @@ export class CategoriesService {
|
||||
}
|
||||
}
|
||||
} else if (vehicle && category.source === "emex") {
|
||||
// EMEX: fetch parts + schema image via Puppeteer from QuickDetails URL
|
||||
if (category.linkPath?.startsWith("emex-catalog:")) {
|
||||
// ── DB-backed path: emex-catalog:{emexVehicleId}:{emexGroupId} ──
|
||||
try {
|
||||
const [, emexVehicleId, emexGroupId] = category.linkPath.split(":");
|
||||
const groupParts = await this.emexCatalogService.getGroupParts(emexVehicleId, emexGroupId);
|
||||
|
||||
if (needParts && groupParts.parts.length > 0) {
|
||||
const insertData = groupParts.parts.map((p) => ({
|
||||
vehicleId: vehicle.id,
|
||||
categoryId,
|
||||
oemCode: p.oemCode || "N/A",
|
||||
name: p.name || "Unknown",
|
||||
nameOriginal: p.name || null,
|
||||
description: null as string | null,
|
||||
quantity: p.quantity ?? null,
|
||||
position: p.position || null,
|
||||
hotspotIndex: p.hotspotIndex ?? null,
|
||||
source: "emex" as const,
|
||||
}));
|
||||
|
||||
dbParts = await this.db.insert(parts).values(insertData).returning();
|
||||
this.logger.log(`Stored ${dbParts.length} EMEX DB parts for category ${categoryId}`);
|
||||
}
|
||||
|
||||
// Schema pics — images are already on storage.sase.tr
|
||||
if (needImage && groupParts.schemaPics.length > 0) {
|
||||
for (const sp of groupParts.schemaPics) {
|
||||
if (!sp.url) continue;
|
||||
const [inserted] = await this.db
|
||||
.insert(schemaPics)
|
||||
.values({
|
||||
categoryId,
|
||||
imageUrl: sp.url,
|
||||
hotspots: JSON.stringify({ width: sp.width || 0, height: sp.height || 0, items: [] }),
|
||||
source: "emex",
|
||||
})
|
||||
.returning();
|
||||
pics.push(inserted);
|
||||
}
|
||||
this.logger.log(`Stored ${pics.length} EMEX DB schema pics for category ${categoryId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to fetch EMEX DB parts for category ${categoryId}: ${(err as Error).message}`);
|
||||
}
|
||||
} else {
|
||||
// ── URL-based path: fetch parts + schema image via Puppeteer from QuickDetails URL ──
|
||||
try {
|
||||
const emexResult = await this.emexService.fetchCategoryParts(category.linkPath);
|
||||
|
||||
@@ -887,6 +956,7 @@ export class CategoriesService {
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to fetch EMEX parts for category ${categoryId}: ${(err as Error).message}`);
|
||||
}
|
||||
} // end: URL-based EMEX path
|
||||
} else if (vehicle) {
|
||||
// PL24: fetch parts + schema image via PL24 API
|
||||
const rawData = vehicle.rawData as any;
|
||||
|
||||
@@ -72,7 +72,7 @@ export const emexVehicleVins = pgTable(
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("emex_vehicle_vins_vin_idx").on(table.vin),
|
||||
uniqueIndex("emex_vehicle_vins_vin_idx").on(table.vin),
|
||||
index("emex_vehicle_vins_vehicle_id_idx").on(table.emexVehicleId),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -332,13 +332,23 @@ export class EmexService {
|
||||
if (!c) return null;
|
||||
const upper = c.toUpperCase();
|
||||
const prefixes: [string, string][] = [
|
||||
['BMW', 'BMW'], ['MB', 'Mercedes-Benz'], ['AU', 'Audi'],
|
||||
['VW', 'Volkswagen'], ['FFIAT', 'Fiat'], ['RFIAT', 'Alfa Romeo'],
|
||||
['FORD', 'Ford'], ['RENAULT', 'Renault'], ['TOYOTA', 'Toyota'],
|
||||
['BMW', 'BMW'], ['MB', 'Mercedes-Benz'], ['MBS', 'Smart'],
|
||||
['AU', 'Audi'], ['VW', 'Volkswagen'],
|
||||
['FFIAT', 'Fiat'], ['CFIAT', 'Abarth'], ['RFIAT', 'Alfa Romeo'],
|
||||
['LFIAT', 'Lancia'], ['TFIAT', 'Fiat'],
|
||||
['FORD', 'Ford'], ['RENAULT', 'Renault'], ['DACIA', 'Dacia'],
|
||||
['TOYOTA', 'Toyota'], ['LEXUS', 'Lexus'],
|
||||
['HONDA', 'Honda'], ['KIA', 'Kia'], ['HYUNDAI', 'Hyundai'],
|
||||
['PORSCHE', 'Porsche'], ['SUBARU', 'Subaru'], ['MAZDA', 'Mazda'],
|
||||
['CPSA', 'Citroën/Peugeot'], ['VOLVO', 'Volvo'], ['NISSAN', 'Nissan'],
|
||||
['OPEL', 'Opel'],
|
||||
['PO', 'Porsche'], ['SUBARU', 'Subaru'], ['MAZDA', 'Mazda'],
|
||||
['MMC', 'Mitsubishi'], ['NISSAN', 'Nissan'], ['INFINITI', 'Infiniti'],
|
||||
['PEUGEOT', 'Peugeot'], ['CITROEN', 'Citroen'],
|
||||
['VOLVO', 'Volvo'], ['JAGUAR', 'Jaguar'], ['LRE', 'Land Rover'],
|
||||
['MINI', 'Mini'], ['RR', 'Rolls-Royce'],
|
||||
['GM_OP', 'Opel'], ['GM_VX', 'Vauxhall'], ['GM_C', 'Chevrolet'],
|
||||
['GM_B', 'Buick'], ['GM_K', 'Cadillac'], ['GM_G', 'GMC'],
|
||||
['SK', 'Skoda'], ['SE', 'Seat'], ['SY', 'SsangYong'],
|
||||
['ISUZU', 'Isuzu'], ['SUZUKI', 'Suzuki'],
|
||||
['CHRYSLER', 'Chrysler'], ['DODGE', 'Dodge'], ['JEEP', 'Jeep'], ['RAM', 'Ram'],
|
||||
];
|
||||
for (const [prefix, brand] of prefixes) {
|
||||
if (upper.startsWith(prefix)) return brand;
|
||||
|
||||
@@ -234,33 +234,103 @@ export interface CatalogEntry {
|
||||
* WMI (World Manufacturer Identifier) to catalog mapping
|
||||
*/
|
||||
export const CATALOG_MAP: Record<string, CatalogEntry> = {
|
||||
// BMW
|
||||
WBA: { code: 'BMW202501', brand: 'BMW' },
|
||||
WBS: { code: 'BMW202501', brand: 'BMW' },
|
||||
WBY: { code: 'BMW202501', brand: 'BMW' },
|
||||
// Mercedes-Benz
|
||||
WDB: { code: 'MB201810', brand: 'Mercedes-Benz' },
|
||||
WDD: { code: 'MB201810', brand: 'Mercedes-Benz' },
|
||||
WDC: { code: 'MB201810', brand: 'Mercedes-Benz' },
|
||||
WDF: { code: 'MB201810', brand: 'Mercedes-Benz' },
|
||||
// Audi
|
||||
WAU: { code: 'AU1587', brand: 'Audi' },
|
||||
TRU: { code: 'AU1587', brand: 'Audi' },
|
||||
// Volkswagen
|
||||
WVW: { code: 'VW1587', brand: 'Volkswagen' },
|
||||
WVG: { code: 'VW1587', brand: 'Volkswagen' },
|
||||
WV2: { code: 'VW1587', brand: 'Volkswagen' },
|
||||
// Renault
|
||||
VF1: { code: 'RENAULT201910', brand: 'Renault' },
|
||||
VF7: { code: 'CPSA01', brand: 'Peugeot' },
|
||||
VF3: { code: 'CPSA01', brand: 'Peugeot' },
|
||||
ZFA: { code: 'CFIAT84', brand: 'Fiat' },
|
||||
// Peugeot
|
||||
VF3: { code: 'PEUGEOT00', brand: 'Peugeot' },
|
||||
// Citroen/Peugeot (VF7 shared — Peugeot more common)
|
||||
VF7: { code: 'PEUGEOT00', brand: 'Peugeot' },
|
||||
// Fiat
|
||||
ZFA: { code: 'FFIAT84', brand: 'Fiat' },
|
||||
// Alfa Romeo
|
||||
ZAR: { code: 'RFIAT84', brand: 'Alfa Romeo' },
|
||||
// Ford
|
||||
WF0: { code: 'FORD202201', brand: 'Ford' },
|
||||
NM0: { code: 'FORD202201', brand: 'Ford' },
|
||||
// Toyota
|
||||
JTD: { code: 'TOYOTA00', brand: 'Toyota' },
|
||||
JTE: { code: 'TOYOTA00', brand: 'Toyota' },
|
||||
SHH: { code: 'HONDA00', brand: 'Honda' },
|
||||
KNM: { code: 'HYUNDAI00', brand: 'Hyundai' },
|
||||
KNA: { code: 'KIA00', brand: 'Kia' },
|
||||
JTN: { code: 'TOYOTA00', brand: 'Toyota' },
|
||||
// Lexus
|
||||
JTH: { code: 'LEXUS00', brand: 'Lexus' },
|
||||
JTJ: { code: 'LEXUS00', brand: 'Lexus' },
|
||||
// Honda
|
||||
SHH: { code: 'HONDA2017', brand: 'Honda' },
|
||||
// Hyundai
|
||||
KMH: { code: 'HYUNDAI202404', brand: 'Hyundai' },
|
||||
KNM: { code: 'HYUNDAI202404', brand: 'Hyundai' },
|
||||
// Kia
|
||||
KNA: { code: 'KIA202404', brand: 'Kia' },
|
||||
KNE: { code: 'KIA202404', brand: 'Kia' },
|
||||
// Porsche
|
||||
WP0: { code: 'PO799', brand: 'Porsche' },
|
||||
WP1: { code: 'PO799', brand: 'Porsche' },
|
||||
// Subaru
|
||||
JF1: { code: 'SUBARU201802', brand: 'Subaru' },
|
||||
JF2: { code: 'SUBARU201802', brand: 'Subaru' },
|
||||
// Mazda
|
||||
JMZ: { code: 'MAZDA2020', brand: 'Mazda' },
|
||||
JM1: { code: 'MAZDA2020', brand: 'Mazda' },
|
||||
JM3: { code: 'MAZDA2020', brand: 'Mazda' },
|
||||
// Mitsubishi
|
||||
JMY: { code: 'MMC202501', brand: 'Mitsubishi' },
|
||||
JMB: { code: 'MMC202501', brand: 'Mitsubishi' },
|
||||
JA3: { code: 'MMC202501', brand: 'Mitsubishi' },
|
||||
JA4: { code: 'MMC202501', brand: 'Mitsubishi' },
|
||||
JA7: { code: 'MMC202501', brand: 'Mitsubishi' },
|
||||
// Nissan
|
||||
JN1: { code: 'NISSAN201809', brand: 'Nissan' },
|
||||
JN8: { code: 'NISSAN201809', brand: 'Nissan' },
|
||||
VSK: { code: 'NISSAN201809', brand: 'Nissan' },
|
||||
// Volvo
|
||||
YV1: { code: 'VOLVO201410', brand: 'Volvo' },
|
||||
YV4: { code: 'VOLVO201410', brand: 'Volvo' },
|
||||
// MINI
|
||||
WMW: { code: 'MINI202501', brand: 'Mini' },
|
||||
// Jaguar
|
||||
SAJ: { code: 'JAGUAR201701', brand: 'Jaguar' },
|
||||
// Land Rover
|
||||
SAL: { code: 'LRE201412', brand: 'Land Rover' },
|
||||
// Skoda
|
||||
TMB: { code: 'SK1119', brand: 'Skoda' },
|
||||
// SEAT
|
||||
VSS: { code: 'SE1113', brand: 'Seat' },
|
||||
// Dacia
|
||||
UU1: { code: 'DACIA201910', brand: 'Dacia' },
|
||||
// Suzuki
|
||||
JSA: { code: 'SUZUKI201905', brand: 'Suzuki' },
|
||||
TSM: { code: 'SUZUKI201905', brand: 'Suzuki' },
|
||||
// Isuzu
|
||||
JAA: { code: 'ISUZU201702', brand: 'Isuzu' },
|
||||
// Opel
|
||||
W0L: { code: 'GM_OP201809', brand: 'Opel' },
|
||||
// Chevrolet
|
||||
KL1: { code: 'GM_C201809', brand: 'Chevrolet' },
|
||||
// SsangYong
|
||||
KPT: { code: 'SY201502', brand: 'SsangYong' },
|
||||
// Chrysler/Jeep/Dodge/RAM
|
||||
'1C4': { code: 'JEEP202402', brand: 'Jeep' },
|
||||
'3C4': { code: 'CHRYSLER202402', brand: 'Chrysler' },
|
||||
// Rolls-Royce
|
||||
SCA: { code: 'RR202501', brand: 'Rolls-Royce' },
|
||||
// Smart
|
||||
WME: { code: 'MBS201810', brand: 'Smart' },
|
||||
// Infiniti
|
||||
JNK: { code: 'INFINITI201809', brand: 'Infiniti' },
|
||||
};
|
||||
|
||||
@@ -8,10 +8,11 @@ import { EmexModule } from "../integrations/emex/emex.module";
|
||||
import { PartsCatalogsModule } from "../integrations/parts-catalogs/parts-catalogs.module";
|
||||
import { BrandsModule } from "../brands/brands.module";
|
||||
import { CategoriesModule } from "../categories/categories.module";
|
||||
import { CatalogModule } from "../catalog/catalog.module";
|
||||
import { JobsModule } from "../jobs/jobs.module";
|
||||
|
||||
@Module({
|
||||
imports: [CorgiModule, PL24Module, VinApiModule, EmexModule, PartsCatalogsModule, BrandsModule, CategoriesModule, JobsModule],
|
||||
imports: [CorgiModule, PL24Module, VinApiModule, EmexModule, PartsCatalogsModule, BrandsModule, CategoriesModule, CatalogModule, JobsModule],
|
||||
controllers: [VehiclesController],
|
||||
providers: [VehiclesService],
|
||||
exports: [VehiclesService],
|
||||
|
||||
@@ -66,6 +66,12 @@ function createService(dbOrOverrides: any = {}) {
|
||||
add: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const emexCatalogService = {
|
||||
lookupVinCache: vi.fn().mockResolvedValue(null),
|
||||
saveVinCache: vi.fn().mockResolvedValue(undefined),
|
||||
matchByName: vi.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
const service = new VehiclesService(
|
||||
db as any,
|
||||
prefetchQueue as any,
|
||||
@@ -73,6 +79,7 @@ function createService(dbOrOverrides: any = {}) {
|
||||
pl24Service as any,
|
||||
vinApiService as any,
|
||||
emexService as any,
|
||||
emexCatalogService as any,
|
||||
partsCatalogsService as any,
|
||||
redisService as any,
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ import { CorgiService } from "../integrations/corgi/corgi.service";
|
||||
import { PL24Service } from "../integrations/pl24/pl24.service";
|
||||
import { VinApiService } from "../integrations/vin-api/vin-api.service";
|
||||
import { EmexService } from "../integrations/emex/emex.service";
|
||||
import { EmexCatalogService } from "../catalog/emex-catalog.service";
|
||||
import { PartsCatalogsService } from "../integrations/parts-catalogs/parts-catalogs.service";
|
||||
import type { PcatCar } from "../integrations/parts-catalogs/parts-catalogs.types";
|
||||
import type { EmexCandidate } from "../integrations/emex/emex.service";
|
||||
@@ -59,6 +60,7 @@ export class VehiclesService {
|
||||
private pl24Service: PL24Service,
|
||||
private vinApiService: VinApiService,
|
||||
private emexService: EmexService,
|
||||
private emexCatalogService: EmexCatalogService,
|
||||
private partsCatalogsService: PartsCatalogsService,
|
||||
private redis: RedisService,
|
||||
) {}
|
||||
@@ -326,9 +328,42 @@ export class VehiclesService {
|
||||
BYPASS: EMEX test — pcat/p24 devre dışı */
|
||||
const pl24Vehicle: any = null; // BYPASS: pl24Vehicle null — EMEX bloğu çalışsın
|
||||
|
||||
// 4. EMEX fallback (single HTTP call — no double fetch)
|
||||
// 4. EMEX fallback — first check VIN cache, then emexdwc.ae
|
||||
let emexVehicle: import("../integrations/emex/emex.types").DecodedVehicle | null = null;
|
||||
if (!pl24Vehicle) {
|
||||
// 4a. VIN cache: skip emexdwc.ae entirely for repeat VINs
|
||||
try {
|
||||
const vinCacheMatch = await this.emexCatalogService.lookupVinCache(vin);
|
||||
if (vinCacheMatch && vinCacheMatch.candidates.length > 0) {
|
||||
const candidate = vinCacheMatch.candidates[0];
|
||||
if (!brandName) brandName = vinCacheMatch.brandName || null;
|
||||
const result: VinResolveResult = {
|
||||
brandName,
|
||||
model: candidate.name || null,
|
||||
year: candidate.yearFrom || corgiResult?.modelYear || null,
|
||||
engine: candidate.engineCode || candidate.engine || null,
|
||||
transmission: candidate.transmission || null,
|
||||
bodyType: candidate.bodyType || null,
|
||||
rawData: {
|
||||
source: "emex",
|
||||
catalogCode: vinCacheMatch.catalogCode,
|
||||
emexVehicleId: vinCacheMatch.vehicleId,
|
||||
emexPathData: `Name: ${candidate.name}`,
|
||||
emexVehicleName: candidate.name,
|
||||
},
|
||||
source: "emex",
|
||||
corgiKnown,
|
||||
corgiResult: corgiResult || null,
|
||||
};
|
||||
await this.redis.setJson(cacheKey, result, 300);
|
||||
this.logger.log(`EMEX VIN cache hit for ${vin} → ${vinCacheMatch.vehicleId}`);
|
||||
return result;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX VIN cache lookup failed: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
// 4b. emexdwc.ae HTTP (single call — no double fetch)
|
||||
try {
|
||||
const emexResult = await this.emexService.decodeVinOrCandidates(vin);
|
||||
if (emexResult.type === 'candidates') {
|
||||
@@ -347,7 +382,7 @@ export class VehiclesService {
|
||||
emexCandidates: emexResult.candidates,
|
||||
};
|
||||
}
|
||||
if (emexResult.type === 'vehicle' && emexResult.vehicle.brand !== "UNKNOWN") {
|
||||
if (emexResult.type === 'vehicle' && (emexResult.vehicle.brand !== "UNKNOWN" || (emexResult.vehicle.raw as any)?.catalogCode)) {
|
||||
emexVehicle = emexResult.vehicle;
|
||||
if (!brandName) brandName = emexResult.vehicle.brand || null;
|
||||
} else if (emexResult.type === 'error') {
|
||||
@@ -361,6 +396,24 @@ export class VehiclesService {
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX fallback failed for ${vin}: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
// 4c. Proactive VIN cache: if emex decoded, try matchByName and cache for next time
|
||||
if (emexVehicle) {
|
||||
const rawCatalogCode = (emexVehicle.raw as any)?.catalogCode as string | undefined;
|
||||
const rawPathData = (emexVehicle.raw as any)?.emexPathData as string | undefined;
|
||||
if (rawCatalogCode && rawPathData) {
|
||||
const pathName = rawPathData.replace(/^Name:\s*/i, "").trim();
|
||||
if (pathName) {
|
||||
this.emexCatalogService.matchByName(rawCatalogCode, pathName)
|
||||
.then((match) => {
|
||||
if (match) {
|
||||
return this.emexCatalogService.saveVinCache(vin, match.vehicleId);
|
||||
}
|
||||
})
|
||||
.catch((err) => this.logger.warn(`Proactive VIN cache failed: ${(err as Error).message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing recognized this VIN
|
||||
|
||||
Reference in New Issue
Block a user