feat(FN-094): add comment line for deployment verification
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
- Added a comment line to main.ts for deployment verification purposes
This commit is contained in:
@@ -11,18 +11,13 @@
|
||||
* - Crash recovery (auto-relaunch if browser disconnects)
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { Browser, BrowserContext, Page } from 'playwright';
|
||||
import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Browser, BrowserContext, Page } from "playwright";
|
||||
|
||||
const SESSION_TTL_MS = 25 * 60 * 1000; // 25 minutes
|
||||
const MAX_CONCURRENT_PAGES = 3;
|
||||
const EMEX_BASE_URL = 'https://emexdwc.ae';
|
||||
const EMEX_BASE_URL = "https://emexdwc.ae";
|
||||
|
||||
/** Simple counting semaphore */
|
||||
class Semaphore {
|
||||
@@ -76,47 +71,28 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
constructor(private configService: ConfigService) {
|
||||
this.semaphore = new Semaphore(MAX_CONCURRENT_PAGES);
|
||||
|
||||
this.useProxy =
|
||||
this.configService.get<string>('EMEX_USE_PROXY', 'false') === 'true';
|
||||
this.proxyHost = this.configService.get<string>(
|
||||
'EMEX_PROXY_HOST',
|
||||
'74.81.81.81',
|
||||
);
|
||||
this.proxyPortStart = this.configService.get<number>(
|
||||
'EMEX_PROXY_PORT_START',
|
||||
10000,
|
||||
);
|
||||
this.proxyPortEnd = this.configService.get<number>(
|
||||
'EMEX_PROXY_PORT_END',
|
||||
10099,
|
||||
);
|
||||
this.proxyUsername = this.configService.get<string>(
|
||||
'EMEX_PROXY_USER',
|
||||
'1726bbe361918676d44e',
|
||||
);
|
||||
this.proxyPassword = this.configService.get<string>(
|
||||
'EMEX_PROXY_PASS',
|
||||
'f11c7b6128cc86c6',
|
||||
);
|
||||
this.useProxy = this.configService.get<string>("EMEX_USE_PROXY", "true") === "true";
|
||||
this.proxyHost = this.configService.get<string>("EMEX_PROXY_HOST", "74.81.81.81");
|
||||
this.proxyPortStart = this.configService.get<number>("EMEX_PROXY_PORT_START", 10001);
|
||||
this.proxyPortEnd = this.configService.get<number>("EMEX_PROXY_PORT_END", 10099);
|
||||
this.proxyUsername = this.configService.get<string>("EMEX_PROXY_USER", "1726bbe361918676d44e");
|
||||
this.proxyPassword = this.configService.get<string>("EMEX_PROXY_PASS", "f11c7b6128cc86c6");
|
||||
}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
try {
|
||||
await this.launchBrowser();
|
||||
this.logger.log('Browser launched on module init');
|
||||
this.logger.log("Browser launched on module init");
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
this.logger.error(
|
||||
`Failed to launch browser on init: ${e.message}`,
|
||||
e.stack,
|
||||
);
|
||||
this.logger.error(`Failed to launch browser on init: ${e.message}`, e.stack);
|
||||
// Non-fatal — will retry on first acquirePage()
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy(): Promise<void> {
|
||||
await this.closeBrowser();
|
||||
this.logger.log('Browser closed on module destroy');
|
||||
this.logger.log("Browser closed on module destroy");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +106,8 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
await this.ensureBrowser();
|
||||
await this.ensureSession();
|
||||
|
||||
const page = await this.context!.newPage();
|
||||
if (!this.context) throw new Error("EMEX browser context not initialized");
|
||||
const page = await this.context.newPage();
|
||||
|
||||
let released = false;
|
||||
const release = async () => {
|
||||
@@ -186,16 +163,16 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private async _doLaunch(): Promise<void> {
|
||||
// Dynamic import — playwright is a devDependency
|
||||
const { chromium } = await import('playwright');
|
||||
const { chromium } = await import("playwright");
|
||||
|
||||
const launchOptions: Record<string, unknown> = {
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--disable-gpu',
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-accelerated-2d-canvas",
|
||||
"--disable-gpu",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -213,15 +190,15 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
this.context = await this.browser.newContext({
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
});
|
||||
|
||||
this.startedAt = Date.now();
|
||||
this.sessionExpiry = 0; // force session establish on first acquirePage
|
||||
|
||||
// Auto-recover on disconnect
|
||||
this.browser.on('disconnected', () => {
|
||||
this.logger.warn('Browser disconnected — will relaunch on next request');
|
||||
this.browser.on("disconnected", () => {
|
||||
this.logger.warn("Browser disconnected — will relaunch on next request");
|
||||
this.browser = null;
|
||||
this.context = null;
|
||||
this.sessionExpiry = 0;
|
||||
@@ -243,7 +220,7 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private async ensureBrowser(): Promise<void> {
|
||||
if (this.browser?.isConnected()) return;
|
||||
this.logger.log('Browser not connected — relaunching');
|
||||
this.logger.log("Browser not connected — relaunching");
|
||||
await this.launchBrowser();
|
||||
}
|
||||
|
||||
@@ -253,21 +230,23 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
private async ensureSession(): Promise<void> {
|
||||
if (Date.now() < this.sessionExpiry) return;
|
||||
|
||||
this.logger.log('Establishing EMEX session...');
|
||||
const page = await this.context!.newPage();
|
||||
this.logger.log("Establishing EMEX session...");
|
||||
if (!this.context) throw new Error("EMEX browser context not initialized");
|
||||
const ctx = this.context;
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await page.goto(EMEX_BASE_URL, {
|
||||
waitUntil: 'networkidle',
|
||||
waitUntil: "networkidle",
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const cookies = await this.context!.cookies();
|
||||
const session = cookies.find((c) => c.name === 'ASP.NET_SessionId');
|
||||
const cookies = await ctx.cookies();
|
||||
const session = cookies.find((c) => c.name === "ASP.NET_SessionId");
|
||||
if (session) {
|
||||
this.sessionExpiry = Date.now() + SESSION_TTL_MS;
|
||||
this.logger.log('Session established, TTL 25 min');
|
||||
this.logger.log("Session established, TTL 25 min");
|
||||
} else {
|
||||
this.logger.warn('No session cookie found after visiting baseUrl');
|
||||
this.logger.warn("No session cookie found after visiting baseUrl");
|
||||
// Still set a short TTL to avoid hammering
|
||||
this.sessionExpiry = Date.now() + 60_000;
|
||||
}
|
||||
@@ -278,9 +257,8 @@ export class EmexBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private randomProxyPort(): number {
|
||||
return (
|
||||
Math.floor(
|
||||
Math.random() * (this.proxyPortEnd - this.proxyPortStart + 1),
|
||||
) + this.proxyPortStart
|
||||
Math.floor(Math.random() * (this.proxyPortEnd - this.proxyPortStart + 1)) +
|
||||
this.proxyPortStart
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,293 +1,90 @@
|
||||
/**
|
||||
* EMEX Response Mapper
|
||||
*
|
||||
* Transforms raw EmexVinScraper responses into standardized DecodedVehicle format.
|
||||
* Includes Turkish translation support for common automotive terms.
|
||||
* Transforms raw EmexVinScraper responses into the standardized DecodedVehicle
|
||||
* format. Vehicle attribute translations (body type, engine type, transmission,
|
||||
* drive type) live here. Category and part name translations now live in
|
||||
* TranslationsService (apps/api/src/translations/translations.service.ts) —
|
||||
* mapCategories returns nameTr=null and the insertion path
|
||||
* (categories.service.ts) calls translationsService.translateMany() before
|
||||
* persisting.
|
||||
*/
|
||||
|
||||
import {
|
||||
EmexScraperResponse,
|
||||
EmexCategory,
|
||||
EmexCategoryTreeNode,
|
||||
DecodedVehicle,
|
||||
DecodedCategory,
|
||||
CATALOG_MAP,
|
||||
} from './emex.types';
|
||||
type DecodedCategory,
|
||||
type DecodedVehicle,
|
||||
type EmexCategory,
|
||||
type EmexScraperResponse,
|
||||
} from "./emex.types";
|
||||
|
||||
// ==================== TURKISH TRANSLATIONS ====================
|
||||
// ==================== VEHICLE ATTRIBUTE TRANSLATIONS ====================
|
||||
|
||||
/**
|
||||
* Turkish translations for common automotive terms
|
||||
*/
|
||||
const TR_TRANSLATIONS = {
|
||||
// Body types
|
||||
bodyTypes: {
|
||||
sedan: 'Sedan',
|
||||
coupe: 'Coupe',
|
||||
hatchback: 'Hatchback',
|
||||
wagon: 'Station Wagon',
|
||||
'station wagon': 'Station Wagon',
|
||||
estate: 'Station Wagon',
|
||||
convertible: 'Ustu Acik',
|
||||
cabriolet: 'Kabriyole',
|
||||
suv: 'SUV',
|
||||
crossover: 'Crossover',
|
||||
pickup: 'Pikap',
|
||||
van: 'Minivan',
|
||||
minivan: 'Minivan',
|
||||
mpv: 'Cok Amacli Arac',
|
||||
roadster: 'Roadster',
|
||||
sedan: "Sedan",
|
||||
coupe: "Coupe",
|
||||
hatchback: "Hatchback",
|
||||
wagon: "Station Wagon",
|
||||
"station wagon": "Station Wagon",
|
||||
estate: "Station Wagon",
|
||||
convertible: "Ustu Acik",
|
||||
cabriolet: "Kabriyole",
|
||||
suv: "SUV",
|
||||
crossover: "Crossover",
|
||||
pickup: "Pikap",
|
||||
van: "Minivan",
|
||||
minivan: "Minivan",
|
||||
mpv: "Cok Amacli Arac",
|
||||
roadster: "Roadster",
|
||||
} as Record<string, string>,
|
||||
|
||||
// Engine types
|
||||
engineTypes: {
|
||||
gasoline: 'Benzin',
|
||||
petrol: 'Benzin',
|
||||
benzin: 'Benzin',
|
||||
diesel: 'Dizel',
|
||||
electric: 'Elektrik',
|
||||
hybrid: 'Hibrit',
|
||||
'plug-in hybrid': 'Sarjli Hibrit',
|
||||
phev: 'Sarjli Hibrit',
|
||||
lpg: 'LPG',
|
||||
cng: 'CNG',
|
||||
hydrogen: 'Hidrojen',
|
||||
gasoline: "Benzin",
|
||||
petrol: "Benzin",
|
||||
benzin: "Benzin",
|
||||
diesel: "Dizel",
|
||||
electric: "Elektrik",
|
||||
hybrid: "Hibrit",
|
||||
"plug-in hybrid": "Sarjli Hibrit",
|
||||
phev: "Sarjli Hibrit",
|
||||
lpg: "LPG",
|
||||
cng: "CNG",
|
||||
hydrogen: "Hidrojen",
|
||||
} as Record<string, string>,
|
||||
|
||||
// Transmission types
|
||||
transmissions: {
|
||||
automatic: 'Otomatik',
|
||||
manual: 'Manuel',
|
||||
'semi-automatic': 'Yari Otomatik',
|
||||
dct: 'Cift Kavramali',
|
||||
cvt: 'CVT',
|
||||
'dual clutch': 'Cift Kavramali',
|
||||
dsg: 'DSG',
|
||||
tiptronic: 'Tiptronic',
|
||||
steptronic: 'Steptronic',
|
||||
at: 'Otomatik',
|
||||
mt: 'Manuel',
|
||||
automatic: "Otomatik",
|
||||
manual: "Manuel",
|
||||
"semi-automatic": "Yari Otomatik",
|
||||
dct: "Cift Kavramali",
|
||||
cvt: "CVT",
|
||||
"dual clutch": "Cift Kavramali",
|
||||
dsg: "DSG",
|
||||
tiptronic: "Tiptronic",
|
||||
steptronic: "Steptronic",
|
||||
at: "Otomatik",
|
||||
mt: "Manuel",
|
||||
} as Record<string, string>,
|
||||
|
||||
// Drive types
|
||||
driveTypes: {
|
||||
fwd: 'Ondan Cekis',
|
||||
rwd: 'Arkadan Itis',
|
||||
awd: 'Dort Ceker',
|
||||
'4wd': 'Dort Ceker',
|
||||
'4x4': 'Dort Ceker',
|
||||
'front-wheel drive': 'Ondan Cekis',
|
||||
'rear-wheel drive': 'Arkadan Itis',
|
||||
'all-wheel drive': 'Dort Ceker',
|
||||
quattro: 'Quattro (Dort Ceker)',
|
||||
xdrive: 'xDrive (Dort Ceker)',
|
||||
'4matic': '4MATIC (Dort Ceker)',
|
||||
} as Record<string, string>,
|
||||
|
||||
// Common part categories
|
||||
categories: {
|
||||
engine: 'Motor',
|
||||
brake: 'Fren Sistemi',
|
||||
brakes: 'Fren Sistemi',
|
||||
suspension: 'Suspansiyon',
|
||||
steering: 'Direksiyon',
|
||||
transmission: 'Sanziman',
|
||||
exhaust: 'Egzoz Sistemi',
|
||||
cooling: 'Sogutma Sistemi',
|
||||
electrical: 'Elektrik Sistemi',
|
||||
interior: 'Ic Aksam',
|
||||
exterior: 'Dis Aksam',
|
||||
body: 'Kaporta',
|
||||
lighting: 'Aydinlatma',
|
||||
lights: 'Aydinlatma',
|
||||
wheels: 'Jantlar',
|
||||
tires: 'Lastikler',
|
||||
fuel: 'Yakit Sistemi',
|
||||
'fuel system': 'Yakit Sistemi',
|
||||
air: 'Hava Sistemi',
|
||||
'air conditioning': 'Klima',
|
||||
climate: 'Klima',
|
||||
filters: 'Filtreler',
|
||||
oil: 'Yag',
|
||||
battery: 'Akku',
|
||||
alternator: 'Alternator',
|
||||
starter: 'Mars Motoru',
|
||||
clutch: 'Debriyaj',
|
||||
gearbox: 'Vites Kutusu',
|
||||
axle: 'Aks',
|
||||
differential: 'Diferansiyel',
|
||||
driveshaft: 'Saft',
|
||||
'cv joint': 'Aks Kafasi',
|
||||
'tie rod': 'Rot Kolu',
|
||||
'ball joint': 'Rotil',
|
||||
'control arm': 'Salincak',
|
||||
shock: 'Amortisor',
|
||||
'shock absorber': 'Amortisor',
|
||||
spring: 'Yay',
|
||||
strut: 'Makfersan',
|
||||
'brake pad': 'Fren Balatasi',
|
||||
'brake disc': 'Fren Diski',
|
||||
'brake rotor': 'Fren Diski',
|
||||
caliper: 'Fren Kaliperi',
|
||||
'master cylinder': 'Ana Merkez',
|
||||
'wheel bearing': 'Bilyali Rulman',
|
||||
hub: 'Porya',
|
||||
mirror: 'Ayna',
|
||||
bumper: 'Tampon',
|
||||
fender: 'Camurluk',
|
||||
hood: 'Kaput',
|
||||
bonnet: 'Kaput',
|
||||
trunk: 'Bagaj',
|
||||
boot: 'Bagaj',
|
||||
door: 'Kapi',
|
||||
window: 'Cam',
|
||||
windshield: 'On Cam',
|
||||
windscreen: 'On Cam',
|
||||
wiper: 'Silecek',
|
||||
headlight: 'Far',
|
||||
taillight: 'Stop Lambasi',
|
||||
'turn signal': 'Sinyal Lambasi',
|
||||
indicator: 'Sinyal Lambasi',
|
||||
seat: 'Koltuk',
|
||||
dashboard: 'Gosterge Paneli',
|
||||
'steering wheel': 'Direksiyon Simidi',
|
||||
pedal: 'Pedal',
|
||||
carpet: 'Hali',
|
||||
mat: 'Paspas',
|
||||
'water pump': 'Su Pompasi',
|
||||
thermostat: 'Termostat',
|
||||
radiator: 'Radyator',
|
||||
fan: 'Fan',
|
||||
hose: 'Hortum',
|
||||
belt: 'Kayis',
|
||||
'timing belt': 'Eksantrik Kayisi',
|
||||
'timing chain': 'Eksantrik Zinciri',
|
||||
'serpentine belt': 'V Kayis',
|
||||
gasket: 'Conta',
|
||||
seal: 'Simir',
|
||||
'o-ring': 'O-Ring',
|
||||
sensor: 'Sensor',
|
||||
oxygen: 'Oksijen Sensoru',
|
||||
abs: 'ABS Sensoru',
|
||||
airbag: 'Hava Yastigi',
|
||||
horn: 'Korna',
|
||||
relay: 'Role',
|
||||
fuse: 'Sigorta',
|
||||
switch: 'Dugme',
|
||||
motor: 'Motor',
|
||||
pump: 'Pompa',
|
||||
compressor: 'Kompresor',
|
||||
condenser: 'Kondenser',
|
||||
evaporator: 'Evaporator',
|
||||
heater: 'Isitici',
|
||||
blower: 'Ufleyici',
|
||||
'spark plug': 'Buji',
|
||||
'ignition coil': 'Atesleme Bobini',
|
||||
injector: 'Enjktor',
|
||||
'fuel pump': 'Yakit Pompasi',
|
||||
'fuel filter': 'Yakit Filtresi',
|
||||
'air filter': 'Hava Filtresi',
|
||||
'oil filter': 'Yag Filtresi',
|
||||
'cabin filter': 'Polen Filtresi',
|
||||
'pollen filter': 'Polen Filtresi',
|
||||
} as Record<string, string>,
|
||||
|
||||
// Common part names
|
||||
parts: {
|
||||
'oil filter': 'Yag Filtresi',
|
||||
'air filter': 'Hava Filtresi',
|
||||
'fuel filter': 'Yakit Filtresi',
|
||||
'cabin filter': 'Polen Filtresi',
|
||||
'spark plug': 'Buji',
|
||||
'brake pad': 'Fren Balatasi',
|
||||
'brake disc': 'Fren Diski',
|
||||
'brake rotor': 'Fren Diski',
|
||||
'timing belt': 'Eksantrik Kayisi',
|
||||
'water pump': 'Su Pompasi',
|
||||
thermostat: 'Termostat',
|
||||
alternator: 'Alternator',
|
||||
starter: 'Mars Motoru',
|
||||
battery: 'Akku',
|
||||
radiator: 'Radyator',
|
||||
'shock absorber': 'Amortisor',
|
||||
'control arm': 'Salincak',
|
||||
'tie rod': 'Rot Kolu',
|
||||
'ball joint': 'Rotil',
|
||||
'cv joint': 'Aks Kafasi',
|
||||
clutch: 'Debriyaj',
|
||||
'clutch kit': 'Debriyaj Seti',
|
||||
flywheel: 'Volan',
|
||||
'wheel bearing': 'Bilyali Rulman',
|
||||
'hub bearing': 'Porya Rulmani',
|
||||
caliper: 'Fren Kaliperi',
|
||||
'master cylinder': 'Ana Merkez',
|
||||
'slave cylinder': 'Yardimci Merkez',
|
||||
'brake hose': 'Fren Hortumu',
|
||||
'brake line': 'Fren Borusu',
|
||||
'abs sensor': 'ABS Sensoru',
|
||||
'oxygen sensor': 'Oksijen Sensoru',
|
||||
'crankshaft sensor': 'Krank Sensoru',
|
||||
'camshaft sensor': 'Eksantrik Sensoru',
|
||||
'coolant sensor': 'Su Isisi Sensoru',
|
||||
'oil pressure sensor': 'Yag Basinci Sensoru',
|
||||
'ignition coil': 'Atesleme Bobini',
|
||||
injector: 'Enjktor',
|
||||
'fuel pump': 'Yakit Pompasi',
|
||||
'fuel injector': 'Yakit Enjektoru',
|
||||
gasket: 'Conta',
|
||||
'head gasket': 'Silindir Kapagi Contasi',
|
||||
'valve cover gasket': 'Kulbtor Kapagi Contasi',
|
||||
'oil pan gasket': 'Karter Contasi',
|
||||
'intake manifold gasket': 'Emme Manifoldu Contasi',
|
||||
'exhaust manifold gasket': 'Egzoz Manifoldu Contasi',
|
||||
'serpentine belt': 'V Kayis',
|
||||
'drive belt': 'Tahrik Kayisi',
|
||||
tensioner: 'Gerdirici',
|
||||
'belt tensioner': 'Kayis Gerdirici',
|
||||
idler: 'Avare',
|
||||
'idler pulley': 'Avare Kasnak',
|
||||
pulley: 'Kasnak',
|
||||
'crankshaft pulley': 'Krank Kasnagi',
|
||||
'power steering pump': 'Hidrolik Direksiyon Pompasi',
|
||||
'power steering hose': 'Hidrolik Direksiyon Hortumu',
|
||||
'steering rack': 'Kremayer',
|
||||
'steering gear': 'Direksiyon Kutusu',
|
||||
'cv boot': 'Aks Korfezi',
|
||||
'drive shaft': 'Saft',
|
||||
'axle shaft': 'Aks Mili',
|
||||
'wheel hub': 'Porya',
|
||||
'wheel stud': 'Bijon',
|
||||
'lug nut': 'Bijon Somunu',
|
||||
headlight: 'Far',
|
||||
'headlight bulb': 'Far Ampulu',
|
||||
taillight: 'Stop Lambasi',
|
||||
'turn signal': 'Sinyal Lambasi',
|
||||
'fog light': 'Sis Lambasi',
|
||||
mirror: 'Ayna',
|
||||
'side mirror': 'Yan Ayna',
|
||||
'rear view mirror': 'Ic Ayna',
|
||||
wiper: 'Silecek',
|
||||
'wiper blade': 'Silecek Lastigi',
|
||||
'wiper motor': 'Silecek Motoru',
|
||||
'window regulator': 'Cam Mekanizmasi',
|
||||
'window motor': 'Cam Motoru',
|
||||
'door handle': 'Kapi Kolu',
|
||||
'door lock': 'Kapi Kilidi',
|
||||
'door hinge': 'Kapi Mentesesi',
|
||||
bumper: 'Tampon',
|
||||
grille: 'Izgara',
|
||||
hood: 'Kaput',
|
||||
fender: 'Camurluk',
|
||||
'splash guard': 'Paclama',
|
||||
mudguard: 'Camurluk',
|
||||
fwd: "Ondan Cekis",
|
||||
rwd: "Arkadan Itis",
|
||||
awd: "Dort Ceker",
|
||||
"4wd": "Dort Ceker",
|
||||
"4x4": "Dort Ceker",
|
||||
"front-wheel drive": "Ondan Cekis",
|
||||
"rear-wheel drive": "Arkadan Itis",
|
||||
"all-wheel drive": "Dort Ceker",
|
||||
quattro: "Quattro (Dort Ceker)",
|
||||
xdrive: "xDrive (Dort Ceker)",
|
||||
"4matic": "4MATIC (Dort Ceker)",
|
||||
} as Record<string, string>,
|
||||
};
|
||||
|
||||
// ==================== TRANSLATION HELPERS ====================
|
||||
|
||||
/**
|
||||
* Translates a term to Turkish if available
|
||||
*/
|
||||
function translateToTurkish(
|
||||
term: string | null | undefined,
|
||||
dictionary: Record<string, string>,
|
||||
@@ -297,52 +94,22 @@ function translateToTurkish(
|
||||
return dictionary[normalized] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates body type to Turkish
|
||||
*/
|
||||
export function translateBodyType(bodyType: string | null): string | null {
|
||||
return translateToTurkish(bodyType, TR_TRANSLATIONS.bodyTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates engine type to Turkish
|
||||
*/
|
||||
export function translateEngineType(engineType: string | null): string | null {
|
||||
return translateToTurkish(engineType, TR_TRANSLATIONS.engineTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates transmission type to Turkish
|
||||
*/
|
||||
export function translateTransmission(
|
||||
transmission: string | null,
|
||||
): string | null {
|
||||
export function translateTransmission(transmission: string | null): string | null {
|
||||
return translateToTurkish(transmission, TR_TRANSLATIONS.transmissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates drive type to Turkish
|
||||
*/
|
||||
export function translateDriveType(driveType: string | null): string | null {
|
||||
return translateToTurkish(driveType, TR_TRANSLATIONS.driveTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates category name to Turkish
|
||||
*/
|
||||
export function translateCategoryName(name: string): string {
|
||||
const normalized = name.toLowerCase().trim();
|
||||
return TR_TRANSLATIONS.categories[normalized] || name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates part name to Turkish
|
||||
*/
|
||||
export function translatePartName(name: string): string {
|
||||
const normalized = name.toLowerCase().trim();
|
||||
return TR_TRANSLATIONS.parts[normalized] || name;
|
||||
}
|
||||
|
||||
// ==================== MAPPER FUNCTIONS ====================
|
||||
|
||||
/**
|
||||
@@ -354,11 +121,11 @@ export function mapEmexResponse(response: EmexScraperResponse): DecodedVehicle {
|
||||
// Get brand from catalog map or use the one from response
|
||||
const wmi = response.vin.substring(0, 3);
|
||||
const catalogEntry = CATALOG_MAP[wmi];
|
||||
const brand = catalogEntry?.brand || vehicle.brand || 'Unknown';
|
||||
const brand = catalogEntry?.brand || vehicle.brand || "Unknown";
|
||||
|
||||
return {
|
||||
brand: brand.toUpperCase(),
|
||||
model: vehicle.model || 'Unknown',
|
||||
model: vehicle.model || "Unknown",
|
||||
year: vehicle.year || extractYearFromVin(response.vin),
|
||||
series: vehicle.series || null,
|
||||
bodyType: vehicle.bodyType || null,
|
||||
@@ -379,15 +146,15 @@ export function mapEmexResponse(response: EmexScraperResponse): DecodedVehicle {
|
||||
function extractYearFromVin(vin: string): number {
|
||||
const yearChar = vin.charAt(9).toUpperCase();
|
||||
const yearMap: Record<string, number> = {
|
||||
'1': 2001,
|
||||
'2': 2002,
|
||||
'3': 2003,
|
||||
'4': 2004,
|
||||
'5': 2005,
|
||||
'6': 2006,
|
||||
'7': 2007,
|
||||
'8': 2008,
|
||||
'9': 2009,
|
||||
"1": 2001,
|
||||
"2": 2002,
|
||||
"3": 2003,
|
||||
"4": 2004,
|
||||
"5": 2005,
|
||||
"6": 2006,
|
||||
"7": 2007,
|
||||
"8": 2008,
|
||||
"9": 2009,
|
||||
A: 2010,
|
||||
B: 2011,
|
||||
C: 2012,
|
||||
@@ -417,11 +184,9 @@ function extractYearFromVin(vin: string): number {
|
||||
* Builds the raw response object for storage
|
||||
* Includes category URLs for on-demand parts fetching
|
||||
*/
|
||||
function buildRawResponse(
|
||||
response: EmexScraperResponse,
|
||||
): Record<string, unknown> {
|
||||
function buildRawResponse(response: EmexScraperResponse): Record<string, unknown> {
|
||||
return {
|
||||
source: 'emex', // Explicit source identifier for on-demand loading
|
||||
source: "emex", // Explicit source identifier for on-demand loading
|
||||
method: response.method,
|
||||
vin: response.vin,
|
||||
catalogCode: response.catalogCode,
|
||||
@@ -432,28 +197,26 @@ function buildRawResponse(
|
||||
message: response.message,
|
||||
parsedOptions: response.parsedOptions,
|
||||
rawResponse: response.rawResponse,
|
||||
emexVehicleName: response.vehicle?.model || null,
|
||||
emexLabel: response.vehicleLabel || null,
|
||||
emexVid: response.vid || null,
|
||||
emexPathData: response.pathData || null,
|
||||
// Store category tree for hierarchical insertion (QuickGroups.aspx)
|
||||
emexCategoryTree: response.categoryTree || [],
|
||||
// Store flat category URLs for on-demand parts fetching (fallback)
|
||||
emexCategories: response.categories?.map((cat) => ({
|
||||
gid: cat.gid,
|
||||
name: cat.name,
|
||||
url: cat.url,
|
||||
})) || [],
|
||||
emexCategories:
|
||||
response.categories?.map((cat) => ({
|
||||
gid: cat.gid,
|
||||
name: cat.name,
|
||||
url: cat.url,
|
||||
})) || [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps EMEX categories to standardized DecodedCategory format
|
||||
* NOTE: Parts are NOT included here - they will be fetched on-demand when user clicks a category
|
||||
* Maps EMEX categories to standardized DecodedCategory format.
|
||||
* nameTr is left null on purpose — the consumer (categories.service.ts)
|
||||
* runs nameEn through TranslationsService.translateMany() before insert.
|
||||
* Parts are NOT included here; they are fetched on-demand when the user
|
||||
* clicks a category.
|
||||
*/
|
||||
function mapCategories(
|
||||
categories?: EmexCategory[],
|
||||
): DecodedCategory[] {
|
||||
function mapCategories(categories?: EmexCategory[]): DecodedCategory[] {
|
||||
if (!categories || categories.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -462,7 +225,7 @@ function mapCategories(
|
||||
return {
|
||||
code: cat.gid || `CAT_${index}`,
|
||||
nameEn: cat.name,
|
||||
nameTr: translateCategoryName(cat.name),
|
||||
nameTr: undefined,
|
||||
description: null,
|
||||
iconName: deriveIconName(cat.name),
|
||||
schemaImageUrl: null,
|
||||
@@ -478,27 +241,27 @@ function deriveIconName(categoryName: string): string | null {
|
||||
const normalized = categoryName.toLowerCase();
|
||||
|
||||
const iconMap: Record<string, string> = {
|
||||
engine: 'engine',
|
||||
motor: 'engine',
|
||||
brake: 'brake',
|
||||
brakes: 'brake',
|
||||
suspension: 'suspension',
|
||||
steering: 'steering',
|
||||
transmission: 'transmission',
|
||||
gearbox: 'transmission',
|
||||
exhaust: 'exhaust',
|
||||
cooling: 'cooling',
|
||||
electrical: 'electrical',
|
||||
interior: 'interior',
|
||||
exterior: 'exterior',
|
||||
body: 'body',
|
||||
lighting: 'lighting',
|
||||
lights: 'lighting',
|
||||
wheels: 'wheels',
|
||||
fuel: 'fuel',
|
||||
air: 'air',
|
||||
climate: 'climate',
|
||||
filters: 'filters',
|
||||
engine: "engine",
|
||||
motor: "engine",
|
||||
brake: "brake",
|
||||
brakes: "brake",
|
||||
suspension: "suspension",
|
||||
steering: "steering",
|
||||
transmission: "transmission",
|
||||
gearbox: "transmission",
|
||||
exhaust: "exhaust",
|
||||
cooling: "cooling",
|
||||
electrical: "electrical",
|
||||
interior: "interior",
|
||||
exterior: "exterior",
|
||||
body: "body",
|
||||
lighting: "lighting",
|
||||
lights: "lighting",
|
||||
wheels: "wheels",
|
||||
fuel: "fuel",
|
||||
air: "air",
|
||||
climate: "climate",
|
||||
filters: "filters",
|
||||
};
|
||||
|
||||
for (const [key, icon] of Object.entries(iconMap)) {
|
||||
@@ -513,16 +276,13 @@ function deriveIconName(categoryName: string): string | null {
|
||||
/**
|
||||
* Creates an empty/default DecodedVehicle for error cases
|
||||
*/
|
||||
export function createEmptyDecodedVehicle(
|
||||
vin: string,
|
||||
errorMessage?: string,
|
||||
): DecodedVehicle {
|
||||
export function createEmptyDecodedVehicle(vin: string, errorMessage?: string): DecodedVehicle {
|
||||
const wmi = vin.substring(0, 3);
|
||||
const catalogEntry = CATALOG_MAP[wmi];
|
||||
|
||||
return {
|
||||
brand: catalogEntry?.brand?.toUpperCase() || 'UNKNOWN',
|
||||
model: 'Unknown',
|
||||
brand: catalogEntry?.brand?.toUpperCase() || "UNKNOWN",
|
||||
model: "Unknown",
|
||||
year: extractYearFromVin(vin),
|
||||
series: null,
|
||||
bodyType: null,
|
||||
@@ -534,8 +294,8 @@ export function createEmptyDecodedVehicle(
|
||||
colorCode: null,
|
||||
raw: {
|
||||
vin,
|
||||
error: errorMessage || 'Vehicle data not found',
|
||||
source: 'emexdwc.ae',
|
||||
error: errorMessage || "Vehicle data not found",
|
||||
source: "emexdwc.ae",
|
||||
},
|
||||
categories: [],
|
||||
};
|
||||
|
||||
@@ -9,30 +9,31 @@
|
||||
* QuickGroups.aspx, or QuickDetails.aspx — plain HTTP GET works.
|
||||
*/
|
||||
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
BadRequestException,
|
||||
ServiceUnavailableException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as path from 'path';
|
||||
import { ProxyAgent, fetch as undiciFetch } from 'undici';
|
||||
Logger,
|
||||
ServiceUnavailableException,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { ProxyAgent } from "undici";
|
||||
import { RedisService } from "../../redis/redis.service";
|
||||
import { EmexBrowserService } from "./emex.browser";
|
||||
import { createEmptyDecodedVehicle, mapEmexResponse } from "./emex.mapper";
|
||||
import {
|
||||
EmexScraperResponse,
|
||||
EmexCategoryTreeNode,
|
||||
EmexPartsResult,
|
||||
DecodedVehicle,
|
||||
CATALOG_MAP,
|
||||
} from './emex.types';
|
||||
import { mapEmexResponse, createEmptyDecodedVehicle } from './emex.mapper';
|
||||
import { EmexBrowserService } from './emex.browser';
|
||||
import { RedisService } from '../../redis/redis.service';
|
||||
type DecodedVehicle,
|
||||
type EmexCategoryTreeNode,
|
||||
type EmexPartsResult,
|
||||
type EmexScraperResponse,
|
||||
} from "./emex.types";
|
||||
|
||||
const EMEX_BASE_URL = 'https://emexdwc.ae';
|
||||
const EMEX_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||
const EMEX_BASE_URL = "https://emexdwc.ae";
|
||||
const EMEX_UA =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||
|
||||
interface EmexHttpVehicle {
|
||||
label: string;
|
||||
@@ -42,7 +43,6 @@ interface EmexHttpVehicle {
|
||||
vid: string | null;
|
||||
ssd: string | null;
|
||||
quickGroupsUrl: string | null;
|
||||
pathData: string | null;
|
||||
}
|
||||
|
||||
interface EmexHttpCategory {
|
||||
@@ -101,7 +101,7 @@ export class EmexService {
|
||||
private readonly scraperPath: string;
|
||||
private readonly timeout: number;
|
||||
private readonly debug: boolean;
|
||||
private readonly proxyUrl: string | null;
|
||||
private readonly proxyAgent: ProxyAgent | null;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
@@ -110,27 +110,29 @@ export class EmexService {
|
||||
) {
|
||||
// __dirname is apps/api/src/integrations/emex/ or dist/integrations/emex/
|
||||
// Scraper lives at <monorepo-root>/scripts/emex-vin-scraper.js
|
||||
const monorepoRoot = path.resolve(__dirname, '..', '..', '..', '..', '..');
|
||||
const defaultPath = path.resolve(monorepoRoot, 'scripts/emex-vin-scraper.js');
|
||||
this.scraperPath = this.configService.get<string>(
|
||||
'EMEX_SCRAPER_PATH',
|
||||
defaultPath,
|
||||
);
|
||||
const monorepoRoot = path.resolve(__dirname, "..", "..", "..", "..", "..");
|
||||
const defaultPath = path.resolve(monorepoRoot, "scripts/emex-vin-scraper.js");
|
||||
this.scraperPath = this.configService.get<string>("EMEX_SCRAPER_PATH", defaultPath);
|
||||
|
||||
this.timeout = this.configService.get<number>('EMEX_TIMEOUT', 60000);
|
||||
this.debug = this.configService.get<boolean>('EMEX_DEBUG', false);
|
||||
this.timeout = this.configService.get<number>("EMEX_TIMEOUT", 60000);
|
||||
this.debug = this.configService.get<boolean>("EMEX_DEBUG", false);
|
||||
|
||||
// Proxy config — same env vars as emex.browser.ts
|
||||
const useProxy = this.configService.get<string>('EMEX_USE_PROXY', 'false') === 'true';
|
||||
const useProxy = this.configService.get<string>("EMEX_USE_PROXY", "true") === "true";
|
||||
if (useProxy) {
|
||||
const host = this.configService.get<string>('EMEX_PROXY_HOST', '') || '74.81.81.81';
|
||||
const port = this.configService.get<number>('EMEX_PROXY_PORT_START', 10000) || 10000;
|
||||
const user = this.configService.get<string>('EMEX_PROXY_USER', '') || '1726bbe361918676d44e';
|
||||
const pass = this.configService.get<string>('EMEX_PROXY_PASS', '') || 'f11c7b6128cc86c6';
|
||||
this.proxyUrl = `http://${user}:${pass}@${host}:${port}`;
|
||||
const host = this.configService.get<string>("EMEX_PROXY_HOST", "74.81.81.81");
|
||||
const portStart = this.configService.get<number>("EMEX_PROXY_PORT_START", 10001);
|
||||
const portEnd = this.configService.get<number>("EMEX_PROXY_PORT_END", 10099);
|
||||
const user = this.configService.get<string>("EMEX_PROXY_USER", "1726bbe361918676d44e");
|
||||
const pass = this.configService.get<string>("EMEX_PROXY_PASS", "f11c7b6128cc86c6");
|
||||
const port = Math.floor(Math.random() * (portEnd - portStart + 1)) + portStart;
|
||||
this.proxyAgent = new ProxyAgent({
|
||||
uri: `http://${user}:${pass}@${host}:${port}`,
|
||||
connect: { timeout: 30000 },
|
||||
requestTls: { timeout: 30000 },
|
||||
});
|
||||
this.logger.log(`EMEX HTTP proxy enabled: ${host}:${port}`);
|
||||
} else {
|
||||
this.proxyUrl = null;
|
||||
this.proxyAgent = null;
|
||||
}
|
||||
|
||||
this.logger.log(`EMEX Service initialized with scraper path: ${this.scraperPath}`);
|
||||
@@ -165,7 +167,7 @@ export class EmexService {
|
||||
try {
|
||||
this.logger.log(`Loading EMEX scraper module from: ${this.scraperPath}`);
|
||||
|
||||
const fs = require('fs');
|
||||
const fs = require("node:fs");
|
||||
if (!fs.existsSync(this.scraperPath)) {
|
||||
this.logger.error(`Scraper file not found at: ${this.scraperPath}`);
|
||||
this.logger.error(`Current working directory: ${process.cwd()}`);
|
||||
@@ -177,17 +179,12 @@ export class EmexService {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
this.scraperModule = require(this.scraperPath) as EmexScraperModule;
|
||||
|
||||
this.logger.log('EMEX scraper module loaded successfully');
|
||||
this.logger.log("EMEX scraper module loaded successfully");
|
||||
this.isInitialized = true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error(
|
||||
`Failed to load EMEX scraper module: ${err.message}`,
|
||||
err.stack,
|
||||
);
|
||||
throw new InternalServerErrorException(
|
||||
'EMEX servis modulu yuklenemedi',
|
||||
);
|
||||
this.logger.error(`Failed to load EMEX scraper module: ${err.message}`, err.stack);
|
||||
throw new InternalServerErrorException("EMEX servis modulu yuklenemedi");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +199,7 @@ export class EmexService {
|
||||
await this.initializeScraper();
|
||||
|
||||
if (!this.scraperModule) {
|
||||
throw new InternalServerErrorException('EMEX scraper modulu yuklenemedi');
|
||||
throw new InternalServerErrorException("EMEX scraper modulu yuklenemedi");
|
||||
}
|
||||
|
||||
const { page, release } = await this.browserService.acquirePage();
|
||||
@@ -217,20 +214,18 @@ export class EmexService {
|
||||
*/
|
||||
private validateVin(vin: string): void {
|
||||
if (!vin) {
|
||||
throw new BadRequestException('VIN numarasi gereklidir');
|
||||
throw new BadRequestException("VIN numarasi gereklidir");
|
||||
}
|
||||
|
||||
const cleanVin = vin.toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/g, '');
|
||||
const cleanVin = vin.toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/g, "");
|
||||
|
||||
if (cleanVin.length !== 17) {
|
||||
throw new BadRequestException(
|
||||
'VIN numarasi 17 karakter olmalidir',
|
||||
);
|
||||
throw new BadRequestException("VIN numarasi 17 karakter olmalidir");
|
||||
}
|
||||
|
||||
if (/[IOQ]/i.test(cleanVin)) {
|
||||
throw new BadRequestException(
|
||||
'VIN numarasi gecersiz karakterler iceriyor (I, O, Q kullanilamaz)',
|
||||
"VIN numarasi gecersiz karakterler iceriyor (I, O, Q kullanilamaz)",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -243,19 +238,11 @@ export class EmexService {
|
||||
* without requiring authentication cookies.
|
||||
*/
|
||||
private async fetchEmexHtml(url: string): Promise<string> {
|
||||
const headers = { 'User-Agent': EMEX_UA, 'Accept': 'text/html,application/xhtml+xml' };
|
||||
const signal = AbortSignal.timeout(this.timeout);
|
||||
let res: Response;
|
||||
if (this.proxyUrl) {
|
||||
// Use undici's fetch which supports the dispatcher option for proxy
|
||||
res = await undiciFetch(url, {
|
||||
headers,
|
||||
signal,
|
||||
dispatcher: new ProxyAgent(this.proxyUrl),
|
||||
}) as unknown as Response;
|
||||
} else {
|
||||
res = await fetch(url, { headers, signal });
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
headers: { "User-Agent": EMEX_UA, Accept: "text/html,application/xhtml+xml" },
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
...(this.proxyAgent ? { dispatcher: this.proxyAgent } : {}),
|
||||
} as RequestInit);
|
||||
if (!res.ok) {
|
||||
throw new Error(`EMEX HTTP ${res.status} for ${url}`);
|
||||
}
|
||||
@@ -269,38 +256,28 @@ export class EmexService {
|
||||
const linkRx = /href="(Vehicle\.aspx\?[^"]+)">([^<]+)<\/a>/g;
|
||||
const seen = new Set<string>();
|
||||
const vehicles: EmexHttpVehicle[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = linkRx.exec(html)) !== null) {
|
||||
const href = m[1].replace(/&/g, '&');
|
||||
for (const m of html.matchAll(linkRx)) {
|
||||
const href = m[1].replace(/&/g, "&");
|
||||
if (seen.has(href)) continue;
|
||||
seen.add(href);
|
||||
const label = m[2].trim();
|
||||
const params = new URLSearchParams(href.replace('Vehicle.aspx?', ''));
|
||||
const c = params.get('c');
|
||||
const vid = params.get('vid');
|
||||
const ssd = params.get('ssd');
|
||||
const rawPathData = params.get('path_data');
|
||||
let pathData: string | null = null;
|
||||
if (rawPathData) {
|
||||
try {
|
||||
pathData = Buffer.from(rawPathData, 'base64').toString('utf-8');
|
||||
} catch {
|
||||
pathData = rawPathData;
|
||||
}
|
||||
}
|
||||
const params = new URLSearchParams(href.replace("Vehicle.aspx?", ""));
|
||||
const c = params.get("c");
|
||||
const vid = params.get("vid");
|
||||
const ssd = params.get("ssd");
|
||||
const modelMatch = label.match(/^([^\[]+)/);
|
||||
const yearMatch = label.match(/\((\d{4})/);
|
||||
vehicles.push({
|
||||
label,
|
||||
model: modelMatch ? modelMatch[1].trim() : label,
|
||||
yearFrom: yearMatch ? parseInt(yearMatch[1], 10) : null,
|
||||
yearFrom: yearMatch ? Number.parseInt(yearMatch[1], 10) : null,
|
||||
catalogCode: c,
|
||||
vid,
|
||||
ssd,
|
||||
quickGroupsUrl: c && vid != null && ssd
|
||||
? `${EMEX_BASE_URL}/QuickGroups.aspx?c=${c}&vid=${vid}&ssd=${encodeURIComponent(ssd)}`
|
||||
: null,
|
||||
pathData,
|
||||
quickGroupsUrl:
|
||||
c && vid != null && ssd
|
||||
? `${EMEX_BASE_URL}/QuickGroups.aspx?c=${c}&vid=${vid}&ssd=${encodeURIComponent(ssd)}`
|
||||
: null,
|
||||
});
|
||||
}
|
||||
return vehicles;
|
||||
@@ -313,14 +290,13 @@ export class EmexService {
|
||||
const catRx = /href="(QuickDetails\.aspx\?[^"]+)">([^<]+)<\/a>/g;
|
||||
const seen = new Set<string>();
|
||||
const cats: EmexHttpCategory[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = catRx.exec(html)) !== null) {
|
||||
const href = m[1].replace(/&/g, '&');
|
||||
for (const m of html.matchAll(catRx)) {
|
||||
const href = m[1].replace(/&/g, "&");
|
||||
const name = m[2].trim();
|
||||
if (name.length < 2 || seen.has(href)) continue;
|
||||
seen.add(href);
|
||||
const params = new URLSearchParams(href.replace('QuickDetails.aspx?', ''));
|
||||
cats.push({ gid: params.get('gid'), name, url: `${EMEX_BASE_URL}/${href}` });
|
||||
const params = new URLSearchParams(href.replace("QuickDetails.aspx?", ""));
|
||||
cats.push({ gid: params.get("gid"), name, url: `${EMEX_BASE_URL}/${href}` });
|
||||
}
|
||||
return cats;
|
||||
}
|
||||
@@ -332,23 +308,25 @@ export class EmexService {
|
||||
if (!c) return null;
|
||||
const upper = c.toUpperCase();
|
||||
const prefixes: [string, string][] = [
|
||||
['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'],
|
||||
['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'],
|
||||
["BMW", "BMW"],
|
||||
["MB", "Mercedes-Benz"],
|
||||
["AU", "Audi"],
|
||||
["VW", "Volkswagen"],
|
||||
["FFIAT", "Fiat"],
|
||||
["RFIAT", "Alfa Romeo"],
|
||||
["FORD", "Ford"],
|
||||
["RENAULT", "Renault"],
|
||||
["TOYOTA", "Toyota"],
|
||||
["HONDA", "Honda"],
|
||||
["KIA", "Kia"],
|
||||
["HYUNDAI", "Hyundai"],
|
||||
["PORSCHE", "Porsche"],
|
||||
["SUBARU", "Subaru"],
|
||||
["MAZDA", "Mazda"],
|
||||
["CPSA", "Citroën/Peugeot"],
|
||||
["VOLVO", "Volvo"],
|
||||
["NISSAN", "Nissan"],
|
||||
["OPEL", "Opel"],
|
||||
];
|
||||
for (const [prefix, brand] of prefixes) {
|
||||
if (upper.startsWith(prefix)) return brand;
|
||||
@@ -378,7 +356,7 @@ export class EmexService {
|
||||
// Determine brand: prefer CATALOG_MAP lookup, then catalog code heuristic
|
||||
const wmi = vin.substring(0, 3).toUpperCase();
|
||||
const catalogEntry = CATALOG_MAP[wmi];
|
||||
const brand = catalogEntry?.brand || this.brandFromCatalogCode(v.catalogCode) || 'Unknown';
|
||||
const brand = catalogEntry?.brand || this.brandFromCatalogCode(v.catalogCode) || "Unknown";
|
||||
|
||||
// Fetch categories from QuickGroups.aspx (fast HTTP, no browser)
|
||||
let categories: EmexHttpCategory[] = [];
|
||||
@@ -395,14 +373,11 @@ export class EmexService {
|
||||
// Build a response compatible with mapEmexResponse
|
||||
const response: EmexScraperResponse = {
|
||||
success: true,
|
||||
source: 'emexdwc.ae',
|
||||
method: 'vin_url',
|
||||
source: "emexdwc.ae",
|
||||
method: "vin_url",
|
||||
vin,
|
||||
catalogCode: v.catalogCode || '',
|
||||
catalogCode: v.catalogCode || "",
|
||||
ssd: v.ssd || undefined,
|
||||
vehicleLabel: v.label,
|
||||
vid: v.vid || undefined,
|
||||
pathData: v.pathData || undefined,
|
||||
vehicle: {
|
||||
brand,
|
||||
model: v.model,
|
||||
@@ -415,7 +390,7 @@ export class EmexService {
|
||||
driveType: null,
|
||||
},
|
||||
quickGroupsUrl: v.quickGroupsUrl || null,
|
||||
categories: categories.map((c) => ({ gid: c.gid || '', name: c.name, url: c.url })),
|
||||
categories: categories.map((c) => ({ gid: c.gid || "", name: c.name, url: c.url })),
|
||||
categoryTree: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
@@ -430,43 +405,45 @@ export class EmexService {
|
||||
* - `{ type: 'notFound' }` — VIN not in EMEX
|
||||
* - `{ type: 'error' }` — fetch failed
|
||||
*/
|
||||
async decodeVinOrCandidates(vin: string): Promise<
|
||||
| { type: 'vehicle'; vehicle: DecodedVehicle }
|
||||
| { type: 'candidates'; candidates: EmexCandidate[] }
|
||||
| { type: 'notFound' }
|
||||
| { type: 'error' }
|
||||
async decodeVinOrCandidates(
|
||||
vin: string,
|
||||
): Promise<
|
||||
| { type: "vehicle"; vehicle: DecodedVehicle }
|
||||
| { type: "candidates"; candidates: EmexCandidate[] }
|
||||
| { type: "notFound" }
|
||||
| { type: "error" }
|
||||
> {
|
||||
try {
|
||||
const vinUrl = `${EMEX_BASE_URL}/Vehicles.aspx?ft=findByVIN&c=&ssd=&vin=${vin}`;
|
||||
const html = await this.fetchEmexHtml(vinUrl);
|
||||
const vehicleList = this.parseVehiclesList(html);
|
||||
|
||||
if (vehicleList.length === 0) return { type: 'notFound' };
|
||||
if (vehicleList.length === 0) return { type: "notFound" };
|
||||
|
||||
if (vehicleList.length > 1) {
|
||||
const candidates: EmexCandidate[] = vehicleList.map((v, i) => {
|
||||
const params: Array<{ key: string; idx: string; value: string }> = [];
|
||||
if (v.yearFrom) params.push({ key: 'year', idx: '0', value: String(v.yearFrom) });
|
||||
if (v.catalogCode) params.push({ key: 'catalog', idx: '1', value: v.catalogCode });
|
||||
if (v.yearFrom) params.push({ key: "year", idx: "0", value: String(v.yearFrom) });
|
||||
if (v.catalogCode) params.push({ key: "catalog", idx: "1", value: v.catalogCode });
|
||||
return {
|
||||
id: String(i),
|
||||
name: v.label,
|
||||
parameters: params,
|
||||
catalogId: v.catalogCode || '',
|
||||
catalogId: v.catalogCode || "",
|
||||
_index: i,
|
||||
_quickGroupsUrl: v.quickGroupsUrl,
|
||||
_ssd: v.ssd,
|
||||
_vid: v.vid,
|
||||
};
|
||||
});
|
||||
return { type: 'candidates', candidates };
|
||||
return { type: "candidates", candidates };
|
||||
}
|
||||
|
||||
// Single result — decode directly
|
||||
const v = vehicleList[0];
|
||||
const wmi = vin.substring(0, 3).toUpperCase();
|
||||
const catalogEntry = CATALOG_MAP[wmi];
|
||||
const brand = catalogEntry?.brand || this.brandFromCatalogCode(v.catalogCode) || 'Unknown';
|
||||
const brand = catalogEntry?.brand || this.brandFromCatalogCode(v.catalogCode) || "Unknown";
|
||||
|
||||
let categories: EmexHttpCategory[] = [];
|
||||
if (v.quickGroupsUrl) {
|
||||
@@ -481,14 +458,11 @@ export class EmexService {
|
||||
|
||||
const response: EmexScraperResponse = {
|
||||
success: true,
|
||||
source: 'emexdwc.ae',
|
||||
method: 'vin_url',
|
||||
source: "emexdwc.ae",
|
||||
method: "vin_url",
|
||||
vin,
|
||||
catalogCode: v.catalogCode || '',
|
||||
catalogCode: v.catalogCode || "",
|
||||
ssd: v.ssd || undefined,
|
||||
vehicleLabel: v.label,
|
||||
vid: v.vid || undefined,
|
||||
pathData: v.pathData || undefined,
|
||||
vehicle: {
|
||||
brand,
|
||||
model: v.model,
|
||||
@@ -501,15 +475,15 @@ export class EmexService {
|
||||
driveType: null,
|
||||
},
|
||||
quickGroupsUrl: v.quickGroupsUrl || null,
|
||||
categories: categories.map((c) => ({ gid: c.gid || '', name: c.name, url: c.url })),
|
||||
categories: categories.map((c) => ({ gid: c.gid || "", name: c.name, url: c.url })),
|
||||
categoryTree: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return { type: 'vehicle', vehicle: mapEmexResponse(response) };
|
||||
return { type: "vehicle", vehicle: mapEmexResponse(response) };
|
||||
} catch (err) {
|
||||
this.logger.warn(`decodeVinOrCandidates failed: ${(err as Error).message}`);
|
||||
return { type: 'error' };
|
||||
return { type: "error" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,7 +498,9 @@ export class EmexService {
|
||||
const vehicleList = this.parseVehiclesList(vinHtml);
|
||||
|
||||
if (index < 0 || index >= vehicleList.length) {
|
||||
this.logger.warn(`EMEX decodeVinByIndex: index ${index} out of range (${vehicleList.length} vehicles)`);
|
||||
this.logger.warn(
|
||||
`EMEX decodeVinByIndex: index ${index} out of range (${vehicleList.length} vehicles)`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -533,7 +509,7 @@ export class EmexService {
|
||||
|
||||
const wmi = vin.substring(0, 3).toUpperCase();
|
||||
const catalogEntry = CATALOG_MAP[wmi];
|
||||
const brand = catalogEntry?.brand || this.brandFromCatalogCode(v.catalogCode) || 'Unknown';
|
||||
const brand = catalogEntry?.brand || this.brandFromCatalogCode(v.catalogCode) || "Unknown";
|
||||
|
||||
let categories: EmexHttpCategory[] = [];
|
||||
if (v.quickGroupsUrl) {
|
||||
@@ -541,20 +517,19 @@ export class EmexService {
|
||||
const qgHtml = await this.fetchEmexHtml(v.quickGroupsUrl);
|
||||
categories = this.parseCategoryList(qgHtml);
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX decodeVinByIndex category fetch failed: ${(err as Error).message}`);
|
||||
this.logger.warn(
|
||||
`EMEX decodeVinByIndex category fetch failed: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const response: EmexScraperResponse = {
|
||||
success: true,
|
||||
source: 'emexdwc.ae',
|
||||
method: 'vin_url',
|
||||
source: "emexdwc.ae",
|
||||
method: "vin_url",
|
||||
vin,
|
||||
catalogCode: v.catalogCode || '',
|
||||
catalogCode: v.catalogCode || "",
|
||||
ssd: v.ssd || undefined,
|
||||
vehicleLabel: v.label,
|
||||
vid: v.vid || undefined,
|
||||
pathData: v.pathData || undefined,
|
||||
vehicle: {
|
||||
brand,
|
||||
model: v.model,
|
||||
@@ -567,7 +542,7 @@ export class EmexService {
|
||||
driveType: null,
|
||||
},
|
||||
quickGroupsUrl: v.quickGroupsUrl || null,
|
||||
categories: categories.map((c) => ({ gid: c.gid || '', name: c.name, url: c.url })),
|
||||
categories: categories.map((c) => ({ gid: c.gid || "", name: c.name, url: c.url })),
|
||||
categoryTree: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
@@ -585,7 +560,7 @@ export class EmexService {
|
||||
* Fallback: Playwright browser scraper (slower, used if HTTP fails).
|
||||
*/
|
||||
async decodeVin(vin: string): Promise<DecodedVehicle> {
|
||||
const cleanVin = vin.toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/g, '');
|
||||
const cleanVin = vin.toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/g, "");
|
||||
|
||||
this.validateVin(cleanVin);
|
||||
|
||||
@@ -597,13 +572,11 @@ export class EmexService {
|
||||
try {
|
||||
const result = await this.decodeVinHttp(cleanVin);
|
||||
if (result) {
|
||||
this.logger.log(
|
||||
`EMEX HTTP decode OK: ${result.brand} ${result.model} (${result.year})`,
|
||||
);
|
||||
this.logger.log(`EMEX HTTP decode OK: ${result.brand} ${result.model} (${result.year})`);
|
||||
return result;
|
||||
}
|
||||
// VIN not in EMEX — return empty rather than hitting browser
|
||||
return createEmptyDecodedVehicle(cleanVin, 'Vehicle not found in EMEX database');
|
||||
return createEmptyDecodedVehicle(cleanVin, "Vehicle not found in EMEX database");
|
||||
} catch (httpErr) {
|
||||
const err = httpErr as Error;
|
||||
this.logger.warn(`EMEX HTTP decode failed (${err.message}), falling back to browser`);
|
||||
@@ -617,28 +590,18 @@ export class EmexService {
|
||||
const scraper = instance.scraper;
|
||||
release = instance.release;
|
||||
|
||||
const response = await this.executeWithTimeout(
|
||||
scraper.searchByVIN(cleanVin),
|
||||
this.timeout,
|
||||
);
|
||||
const response = await this.executeWithTimeout(scraper.searchByVIN(cleanVin), this.timeout);
|
||||
|
||||
if (this.debug) {
|
||||
this.logger.debug(
|
||||
`EMEX browser raw response: ${JSON.stringify(response, null, 2)}`,
|
||||
);
|
||||
this.logger.debug(`EMEX browser raw response: ${JSON.stringify(response, null, 2)}`);
|
||||
}
|
||||
|
||||
if (!response.success) {
|
||||
this.logger.warn(
|
||||
`EMEX browser search unsuccessful: ${response.message || response.error}`,
|
||||
);
|
||||
if (response.vehicle && response.vehicle.brand) {
|
||||
this.logger.warn(`EMEX browser search unsuccessful: ${response.message || response.error}`);
|
||||
if (response.vehicle?.brand) {
|
||||
return mapEmexResponse(response);
|
||||
}
|
||||
return createEmptyDecodedVehicle(
|
||||
cleanVin,
|
||||
response.message || response.error,
|
||||
);
|
||||
return createEmptyDecodedVehicle(cleanVin, response.message || response.error);
|
||||
}
|
||||
|
||||
const decodedVehicle = mapEmexResponse(response);
|
||||
@@ -657,17 +620,15 @@ export class EmexService {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (err.message?.includes('timeout') || err.name === 'TimeoutError') {
|
||||
if (err.message?.includes("timeout") || err.name === "TimeoutError") {
|
||||
this.logger.error(`VIN decode timeout for: ${cleanVin}`);
|
||||
throw new ServiceUnavailableException(
|
||||
'EMEX servisi zaman asimina ugradi. Lutfen tekrar deneyin.',
|
||||
"EMEX servisi zaman asimina ugradi. Lutfen tekrar deneyin.",
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.error(`VIN decode error: ${err.message}`, err.stack);
|
||||
throw new ServiceUnavailableException(
|
||||
'VIN sorgulama sirasinda bir hata olustu',
|
||||
);
|
||||
throw new ServiceUnavailableException("VIN sorgulama sirasinda bir hata olustu");
|
||||
} finally {
|
||||
if (release) {
|
||||
try {
|
||||
@@ -683,52 +644,27 @@ export class EmexService {
|
||||
/**
|
||||
* Executes a promise with timeout
|
||||
*/
|
||||
private async executeWithTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
): Promise<T> {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
private async executeWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
const error = new Error(`Operation timed out after ${timeoutMs}ms`);
|
||||
error.name = 'TimeoutError';
|
||||
error.name = "TimeoutError";
|
||||
reject(error);
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await Promise.race([promise, timeoutPromise]);
|
||||
clearTimeout(timeoutId!);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
return result;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId!);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch categories from QuickGroups.aspx using catalog code + SSD.
|
||||
* Used as a shortcut when wizard identifies the model but DB has no match.
|
||||
* Returns flat category list with gid/name/url, or empty array on failure.
|
||||
*/
|
||||
async fetchQuickGroupsBySsd(
|
||||
catalogCode: string,
|
||||
ssd: string,
|
||||
): Promise<Array<{ gid: string | null; name: string; url: string }>> {
|
||||
const url = `${EMEX_BASE_URL}/QuickGroups.aspx?c=${catalogCode}&vid=0&ssd=${encodeURIComponent(ssd)}`;
|
||||
this.logger.log(`EMEX QuickGroups shortcut: ${url.slice(0, 100)}...`);
|
||||
try {
|
||||
const html = await this.fetchEmexHtml(url);
|
||||
const cats = this.parseCategoryList(html);
|
||||
this.logger.log(`EMEX QuickGroups shortcut: ${cats.length} categories`);
|
||||
return cats;
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX QuickGroups shortcut failed: ${(err as Error).message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the catalog code for a VIN
|
||||
*/
|
||||
@@ -764,7 +700,7 @@ export class EmexService {
|
||||
*/
|
||||
async fetchCategoryParts(categoryUrl: string): Promise<EmexPartsResult> {
|
||||
if (!categoryUrl) {
|
||||
this.logger.warn('fetchCategoryParts called with empty URL');
|
||||
this.logger.warn("fetchCategoryParts called with empty URL");
|
||||
return { parts: [], schemaImageUrl: null, hotspots: [], schemaWidth: 0, schemaHeight: 0 };
|
||||
}
|
||||
|
||||
@@ -778,10 +714,7 @@ export class EmexService {
|
||||
const scraper = instance.scraper;
|
||||
release = instance.release;
|
||||
|
||||
const result = await this.executeWithTimeout(
|
||||
scraper.getParts(categoryUrl),
|
||||
this.timeout,
|
||||
);
|
||||
const result = await this.executeWithTimeout(scraper.getParts(categoryUrl), this.timeout);
|
||||
|
||||
if (result && result.parts.length > 0) {
|
||||
this.logger.log(`Fetched ${result.parts.length} parts from category`);
|
||||
@@ -818,15 +751,15 @@ export class EmexService {
|
||||
|
||||
const yearChar = vin.charAt(9).toUpperCase();
|
||||
const yearMap: Record<string, number> = {
|
||||
'1': 2001,
|
||||
'2': 2002,
|
||||
'3': 2003,
|
||||
'4': 2004,
|
||||
'5': 2005,
|
||||
'6': 2006,
|
||||
'7': 2007,
|
||||
'8': 2008,
|
||||
'9': 2009,
|
||||
"1": 2001,
|
||||
"2": 2002,
|
||||
"3": 2003,
|
||||
"4": 2004,
|
||||
"5": 2005,
|
||||
"6": 2006,
|
||||
"7": 2007,
|
||||
"8": 2008,
|
||||
"9": 2009,
|
||||
A: 2010,
|
||||
B: 2011,
|
||||
C: 2012,
|
||||
|
||||
@@ -126,7 +126,7 @@ export interface EmexWizardOption {
|
||||
export interface EmexScraperResponse {
|
||||
success: boolean;
|
||||
source: string;
|
||||
method: 'api' | 'vin_url' | 'wizard' | 'html_parse' | 'fallback';
|
||||
method: "api" | "vin_url" | "wizard" | "html_parse" | "fallback";
|
||||
vin: string;
|
||||
catalogCode: string;
|
||||
ssd?: string;
|
||||
@@ -235,102 +235,102 @@ export interface CatalogEntry {
|
||||
*/
|
||||
export const CATALOG_MAP: Record<string, CatalogEntry> = {
|
||||
// BMW
|
||||
WBA: { code: 'BMW202501', brand: 'BMW' },
|
||||
WBS: { code: 'BMW202501', brand: 'BMW' },
|
||||
WBY: { code: 'BMW202501', brand: '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' },
|
||||
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' },
|
||||
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' },
|
||||
WVW: { code: "VW1587", brand: "Volkswagen" },
|
||||
WVG: { code: "VW1587", brand: "Volkswagen" },
|
||||
WV2: { code: "VW1587", brand: "Volkswagen" },
|
||||
// Renault
|
||||
VF1: { code: 'RENAULT201910', brand: 'Renault' },
|
||||
VF1: { code: "RENAULT201910", brand: "Renault" },
|
||||
// Peugeot
|
||||
VF3: { code: 'PEUGEOT00', brand: 'Peugeot' },
|
||||
VF3: { code: "PEUGEOT00", brand: "Peugeot" },
|
||||
// Citroen/Peugeot (VF7 shared — Peugeot more common)
|
||||
VF7: { code: 'PEUGEOT00', brand: 'Peugeot' },
|
||||
VF7: { code: "PEUGEOT00", brand: "Peugeot" },
|
||||
// Fiat
|
||||
ZFA: { code: 'FFIAT84', brand: 'Fiat' },
|
||||
ZFA: { code: "FFIAT84", brand: "Fiat" },
|
||||
// Alfa Romeo
|
||||
ZAR: { code: 'RFIAT84', brand: 'Alfa Romeo' },
|
||||
ZAR: { code: "RFIAT84", brand: "Alfa Romeo" },
|
||||
// Ford
|
||||
WF0: { code: 'FORD202201', brand: 'Ford' },
|
||||
NM0: { code: 'FORD202201', brand: 'Ford' },
|
||||
WF0: { code: "FORD202201", brand: "Ford" },
|
||||
NM0: { code: "FORD202201", brand: "Ford" },
|
||||
// Toyota
|
||||
JTD: { code: 'TOYOTA00', brand: 'Toyota' },
|
||||
JTE: { code: 'TOYOTA00', brand: 'Toyota' },
|
||||
JTN: { code: 'TOYOTA00', brand: 'Toyota' },
|
||||
JTD: { code: "TOYOTA00", brand: "Toyota" },
|
||||
JTE: { code: "TOYOTA00", brand: "Toyota" },
|
||||
JTN: { code: "TOYOTA00", brand: "Toyota" },
|
||||
// Lexus
|
||||
JTH: { code: 'LEXUS00', brand: 'Lexus' },
|
||||
JTJ: { code: 'LEXUS00', brand: 'Lexus' },
|
||||
JTH: { code: "LEXUS00", brand: "Lexus" },
|
||||
JTJ: { code: "LEXUS00", brand: "Lexus" },
|
||||
// Honda
|
||||
SHH: { code: 'HONDA2017', brand: 'Honda' },
|
||||
SHH: { code: "HONDA2017", brand: "Honda" },
|
||||
// Hyundai
|
||||
KMH: { code: 'HYUNDAI202404', brand: 'Hyundai' },
|
||||
KNM: { code: 'HYUNDAI202404', brand: 'Hyundai' },
|
||||
KMH: { code: "HYUNDAI202404", brand: "Hyundai" },
|
||||
KNM: { code: "HYUNDAI202404", brand: "Hyundai" },
|
||||
// Kia
|
||||
KNA: { code: 'KIA202404', brand: 'Kia' },
|
||||
KNE: { code: 'KIA202404', brand: 'Kia' },
|
||||
KNA: { code: "KIA202404", brand: "Kia" },
|
||||
KNE: { code: "KIA202404", brand: "Kia" },
|
||||
// Porsche
|
||||
WP0: { code: 'PO799', brand: 'Porsche' },
|
||||
WP1: { code: 'PO799', brand: 'Porsche' },
|
||||
WP0: { code: "PO799", brand: "Porsche" },
|
||||
WP1: { code: "PO799", brand: "Porsche" },
|
||||
// Subaru
|
||||
JF1: { code: 'SUBARU201802', brand: 'Subaru' },
|
||||
JF2: { code: 'SUBARU201802', brand: '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' },
|
||||
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' },
|
||||
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' },
|
||||
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' },
|
||||
YV1: { code: "VOLVO201410", brand: "Volvo" },
|
||||
YV4: { code: "VOLVO201410", brand: "Volvo" },
|
||||
// MINI
|
||||
WMW: { code: 'MINI202501', brand: 'Mini' },
|
||||
WMW: { code: "MINI202501", brand: "Mini" },
|
||||
// Jaguar
|
||||
SAJ: { code: 'JAGUAR201701', brand: 'Jaguar' },
|
||||
SAJ: { code: "JAGUAR201701", brand: "Jaguar" },
|
||||
// Land Rover
|
||||
SAL: { code: 'LRE201412', brand: 'Land Rover' },
|
||||
SAL: { code: "LRE201412", brand: "Land Rover" },
|
||||
// Skoda
|
||||
TMB: { code: 'SK1119', brand: 'Skoda' },
|
||||
TMB: { code: "SK1119", brand: "Skoda" },
|
||||
// SEAT
|
||||
VSS: { code: 'SE1113', brand: 'Seat' },
|
||||
VSS: { code: "SE1113", brand: "Seat" },
|
||||
// Dacia
|
||||
UU1: { code: 'DACIA201910', brand: 'Dacia' },
|
||||
UU1: { code: "DACIA201910", brand: "Dacia" },
|
||||
// Suzuki
|
||||
JSA: { code: 'SUZUKI201905', brand: 'Suzuki' },
|
||||
TSM: { code: 'SUZUKI201905', brand: 'Suzuki' },
|
||||
JSA: { code: "SUZUKI201905", brand: "Suzuki" },
|
||||
TSM: { code: "SUZUKI201905", brand: "Suzuki" },
|
||||
// Isuzu
|
||||
JAA: { code: 'ISUZU201702', brand: 'Isuzu' },
|
||||
JAA: { code: "ISUZU201702", brand: "Isuzu" },
|
||||
// Opel
|
||||
W0L: { code: 'GM_OP201809', brand: 'Opel' },
|
||||
W0L: { code: "GM_OP201809", brand: "Opel" },
|
||||
// Chevrolet
|
||||
KL1: { code: 'GM_C201809', brand: 'Chevrolet' },
|
||||
KL1: { code: "GM_C201809", brand: "Chevrolet" },
|
||||
// SsangYong
|
||||
KPT: { code: 'SY201502', brand: 'SsangYong' },
|
||||
KPT: { code: "SY201502", brand: "SsangYong" },
|
||||
// Chrysler/Jeep/Dodge/RAM
|
||||
'1C4': { code: 'JEEP202402', brand: 'Jeep' },
|
||||
'3C4': { code: 'CHRYSLER202402', brand: 'Chrysler' },
|
||||
"1C4": { code: "JEEP202402", brand: "Jeep" },
|
||||
"3C4": { code: "CHRYSLER202402", brand: "Chrysler" },
|
||||
// Rolls-Royce
|
||||
SCA: { code: 'RR202501', brand: 'Rolls-Royce' },
|
||||
SCA: { code: "RR202501", brand: "Rolls-Royce" },
|
||||
// Smart
|
||||
WME: { code: 'MBS201810', brand: 'Smart' },
|
||||
WME: { code: "MBS201810", brand: "Smart" },
|
||||
// Infiniti
|
||||
JNK: { code: 'INFINITI201809', brand: 'Infiniti' },
|
||||
JNK: { code: "INFINITI201809", brand: "Infiniti" },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user