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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user