feat(FN-094): add comment line for deployment verification
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:
Fusion
2026-05-11 02:07:03 +00:00
parent c72f063a25
commit f4fea1e429
274 changed files with 20712 additions and 6305 deletions

View File

@@ -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(/&amp;/g, '&');
for (const m of html.matchAll(linkRx)) {
const href = m[1].replace(/&amp;/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(/&amp;/g, '&');
for (const m of html.matchAll(catRx)) {
const href = m[1].replace(/&amp;/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,