feat: QuickGroups shortcut — VIN SSD + vid=0 for fast category fetch
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled

When EMEX DB has no vehicle match (Ford EU, Mercedes etc.), instead of
falling back to full on-demand VIN decode (~8s), use the VIN-decode SSD
directly with QuickGroups.aspx?c={catalog}&vid=0&ssd={vinSsd} (~2-3s).

Tested: Ford Focus CB4 VIN SSD returns 376 categories via QuickGroups.

New step 3.5 in the cascade:
1. pathData → matchByName (DB-only, ~50ms)
2. emexVehicleName → matchByName (DB-only, ~50ms)
3. matchBySsd wizard walk (DB, ~2-3s)
3.5. VIN SSD → QuickGroups shortcut (~2-3s) ← NEW
4. Fallback → full on-demand scrape (~8s)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 15:24:38 +00:00
parent a74a073009
commit 7a6b42c2e7
3 changed files with 60 additions and 0 deletions

View File

@@ -75,6 +75,8 @@ export interface EmexVehicleMatch {
catalogCode: string;
vehicleName: string | null;
candidates: EmexVehicleDto[];
/** When DB has no match but wizard found the model, provides a QuickGroups URL for on-demand category fetch */
wizardQuickGroupsUrl?: string | null;
}
export interface EmexSearchResult {

View File

@@ -290,6 +290,42 @@ export class CategoriesService {
}
}
// Step 3.5: VIN SSD → QuickGroups shortcut (~2-3s, skips full VIN decode)
if (catalogCode && dbCategories.length === 0) {
const ssd = vRawData?.ssd as string | undefined;
if (ssd) {
try {
this.logger.log(`EMEX step 3.5: QuickGroups shortcut (c=${catalogCode}, ssd=${ssd.slice(0, 30)}...)`);
const qgCats = await this.emexService.fetchQuickGroupsBySsd(catalogCode, ssd);
if (qgCats.length > 0) {
const seenNames = new Set<string>();
const uniqueCats = qgCats.filter((c) => {
if (seenNames.has(c.name)) return false;
seenNames.add(c.name);
return true;
});
const insertData = uniqueCats.map((c) => ({
vehicleId,
catalogVehicleId: null as string | null,
name: c.name,
nameOriginal: c.name,
parentId: null as string | null,
externalId: c.gid,
linkPath: c.url || null,
linkWid: null as string | null,
source: "emex" as const,
}));
dbCategories = await this.db.insert(categories).values(insertData).onConflictDoNothing().returning();
this.logger.log(`Stored ${dbCategories.length} EMEX QuickGroups categories for ${vehicle.vin}`);
}
} catch (err) {
this.logger.warn(`EMEX QuickGroups shortcut failed: ${(err as Error).message}`);
}
}
}
// Step 4: Fallback → on-demand EMEX scrape (existing behavior, ~6-13s)
if (dbCategories.length === 0) {
this.logger.log(`EMEX step 4: on-demand fallback for ${vehicle.vin}`);

View File

@@ -697,6 +697,28 @@ export class EmexService {
}
}
/**
* 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
*/