Replace Playwright browser with pure HTTP for EMEX VIN decode:
- Vehicles.aspx, QuickGroups.aspx, QuickDetails.aspx all work
without authentication — no session/browser needed
- VIN decode drops from ~10-20s (Playwright) to ~1-5s (HTTP)
- Playwright browser kept as fallback if HTTP fails
Add multi-candidate selection for EMEX (mirrors PartsCatalogs flow):
- EmexCandidate interface matches PcatCar shape so VehicleSelectModal
renders without changes
- decodeVinOrCandidates(): single HTTP fetch returns vehicle OR
candidates (avoids double Vehicles.aspx request)
- decodeVinByIndex(): resolves user-selected candidate by index
- vehicles.service: emexCandidates in VinResolveResult, emexCarIndex
param in decodeVin/resolveVin, resolveEmexCarByIndex() method
- vehicles.controller: @Body("emexCarIndex") accepted
- search.tsx: candidateSource state, handleCandidateSelect sends
emexCarIndex or pcatCarId based on source
Test VIN NM417800006410193 (Fiat SIENA) now shows 2-option modal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
5.1 KiB
JavaScript
148 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* EMEX HTTP-only VIN decode test
|
|
* Tests replacing Playwright scraper with pure fetch + HTML parsing
|
|
* Run: node scripts/emex-http-test.js <VIN>
|
|
*/
|
|
|
|
const vin = process.argv[2] || "NM417800006410193";
|
|
const BASE = "https://emexdwc.ae";
|
|
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
|
|
async function fetchHtml(url) {
|
|
const t0 = Date.now();
|
|
const res = await fetch(url, {
|
|
headers: { "User-Agent": UA, "Accept": "text/html,application/xhtml+xml" },
|
|
redirect: "follow",
|
|
});
|
|
const html = await res.text();
|
|
const ms = Date.now() - t0;
|
|
console.log(` [${res.status}] ${url.replace(BASE, "")} — ${html.length} bytes, ${ms}ms`);
|
|
return { html, status: res.status, ms };
|
|
}
|
|
|
|
/**
|
|
* Parse Vehicles.aspx response — extract vehicle list
|
|
*/
|
|
function parseVehiclesList(html) {
|
|
// Match <tr onclick="window.location = 'Vehicle.aspx?...'" ...>
|
|
// <td><a href="Vehicle.aspx?c=X&vid=Y&ssd=Z&...">LABEL</a></td>
|
|
const linkRx = /href="(Vehicle\.aspx\?[^"]+)">([^<]+)<\/a>/g;
|
|
const seen = new Set();
|
|
const vehicles = [];
|
|
let m;
|
|
while ((m = linkRx.exec(html)) !== null) {
|
|
const href = m[1].replace(/&/g, "&");
|
|
const label = m[2].trim();
|
|
if (seen.has(href)) continue;
|
|
seen.add(href);
|
|
const params = new URLSearchParams(href.replace("Vehicle.aspx?", ""));
|
|
const c = params.get("c");
|
|
const vid = params.get("vid");
|
|
const ssd = params.get("ssd");
|
|
// Parse model and year range from label like "SIENA [SIENA EXTRA EUROPA REST.(2002-2012)]"
|
|
const modelMatch = label.match(/^([^\[]+)/);
|
|
const yearMatch = label.match(/\((\d{4})/);
|
|
vehicles.push({
|
|
label,
|
|
catalogCode: c,
|
|
vid,
|
|
ssd,
|
|
model: modelMatch ? modelMatch[1].trim() : label,
|
|
yearFrom: yearMatch ? parseInt(yearMatch[1]) : null,
|
|
quickGroupsUrl: c && vid != null && ssd
|
|
? `${BASE}/QuickGroups.aspx?c=${c}&vid=${vid}&ssd=${encodeURIComponent(ssd)}`
|
|
: null,
|
|
});
|
|
}
|
|
return vehicles;
|
|
}
|
|
|
|
/**
|
|
* Parse QuickGroups.aspx response — extract flat category list
|
|
*/
|
|
function parseCategories(html) {
|
|
// Category links go to QuickDetails.aspx?c=X&gid=Y&vid=Z&ssd=...
|
|
const catRx = /href="(QuickDetails\.aspx\?[^"]+)">([^<]+)<\/a>/g;
|
|
const seen = new Set();
|
|
const cats = [];
|
|
let m;
|
|
while ((m = catRx.exec(html)) !== null) {
|
|
const href = m[1].replace(/&/g, "&");
|
|
const label = m[2].trim();
|
|
if (label.length < 2 || seen.has(href)) continue;
|
|
seen.add(href);
|
|
const params = new URLSearchParams(href.replace("QuickDetails.aspx?", ""));
|
|
cats.push({
|
|
gid: params.get("gid"),
|
|
name: label,
|
|
url: `${BASE}/${href}`,
|
|
});
|
|
}
|
|
return cats;
|
|
}
|
|
|
|
/**
|
|
* Determine brand from catalog code (EMEX catalog code → brand)
|
|
*/
|
|
function brandFromCatalogCode(c) {
|
|
if (!c) return null;
|
|
const map = {
|
|
BMW: "BMW", MB: "Mercedes-Benz", AU: "Audi", VW: "Volkswagen",
|
|
FIAT: "Fiat", RFIAT: "Alfa Romeo", FORD: "Ford", RENAULT: "Renault",
|
|
TOYOTA: "Toyota", HONDA: "Honda", KIA: "Kia", HYUNDAI: "Hyundai",
|
|
PORSCHE: "Porsche", SUBARU: "Subaru", MAZDA: "Mazda", PEUGEOT: "Peugeot",
|
|
CPSA: "Citroën/Peugeot",
|
|
};
|
|
for (const [prefix, brand] of Object.entries(map)) {
|
|
if (c.toUpperCase().includes(prefix)) return brand;
|
|
}
|
|
// Extract letters from code like "FFIAT84" → FIAT
|
|
const letterMatch = c.replace(/[0-9]/g, "").replace(/^[A-Z]/, "");
|
|
return letterMatch || c;
|
|
}
|
|
|
|
async function decodeVin(vin) {
|
|
console.log(`\n=== EMEX HTTP VIN Decode: ${vin} ===`);
|
|
const t0 = Date.now();
|
|
|
|
// Step 1: VIN search — no session/login needed!
|
|
const { html: vinHtml } = await fetchHtml(`${BASE}/Vehicles.aspx?ft=findByVIN&c=&ssd=&vin=${vin}`);
|
|
const vehicles = parseVehiclesList(vinHtml);
|
|
console.log(`\nFound ${vehicles.length} vehicle(s):`);
|
|
vehicles.forEach((v, i) => console.log(` [${i}] ${v.label} (c=${v.catalogCode})`));
|
|
|
|
if (vehicles.length === 0) {
|
|
console.log("\n✗ No vehicles found — VIN not in EMEX database");
|
|
return null;
|
|
}
|
|
|
|
const v = vehicles[0];
|
|
const brand = brandFromCatalogCode(v.catalogCode);
|
|
|
|
console.log(`\nVehicle data:`);
|
|
console.log(` Brand: ${brand}`);
|
|
console.log(` Model: ${v.model}`);
|
|
console.log(` Year: ${v.yearFrom}`);
|
|
console.log(` CatCode: ${v.catalogCode}`);
|
|
console.log(` QG URL: ${v.quickGroupsUrl}`);
|
|
|
|
// Step 2: Fetch categories from QuickGroups.aspx
|
|
let categories = [];
|
|
if (v.quickGroupsUrl) {
|
|
const { html: qgHtml } = await fetchHtml(v.quickGroupsUrl);
|
|
categories = parseCategories(qgHtml);
|
|
console.log(`\nCategories (${categories.length}):`);
|
|
categories.slice(0, 10).forEach((c) => console.log(` [${c.gid}] ${c.name}`));
|
|
if (categories.length > 10) console.log(` ... and ${categories.length - 10} more`);
|
|
}
|
|
|
|
const total = Date.now() - t0;
|
|
console.log(`\n✓ Total time: ${total}ms`);
|
|
console.log(` (Playwright baseline was ~10-20s)`);
|
|
|
|
return { brand, model: v.model, year: v.yearFrom, catalogCode: v.catalogCode, ssd: v.ssd, quickGroupsUrl: v.quickGroupsUrl, categories };
|
|
}
|
|
|
|
decodeVin(vin).catch(console.error);
|