feat(emex): wizard-based vehicle selection via GetWizard2 API
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Replace static DB-based dropdown chain with live wizard from emexdwc.ae. The wizard API (GetWizard2) works without auth and shows all models/options for each catalog (e.g. AU1587 now shows 103 Audi models vs 11 before). Backend: proxy GetWizard2 + search DB for matching vehicles after wizard completes. Frontend: step-by-step wizard with breadcrumb navigation, search, and reset. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,22 @@ export class EmexCatalogController {
|
||||
return this.emexCatalogService.getVehicles(code);
|
||||
}
|
||||
|
||||
@Get("brands/:code/wizard")
|
||||
getWizard(
|
||||
@Param("code") code: string,
|
||||
@Query("ssd") ssd?: string,
|
||||
) {
|
||||
return this.emexCatalogService.getWizard(code, ssd || "");
|
||||
}
|
||||
|
||||
@Get("brands/:code/wizard-vehicles")
|
||||
getWizardVehicles(
|
||||
@Param("code") code: string,
|
||||
@Query("name") name: string,
|
||||
) {
|
||||
return this.emexCatalogService.getWizardVehicles(code, name);
|
||||
}
|
||||
|
||||
@Get("vehicles/:id/groups")
|
||||
getVehicleGroups(@Param("id") id: string) {
|
||||
return this.emexCatalogService.getVehicleGroups(id);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable, Logger } from "@nestjs/common";
|
||||
import { Inject, Injectable, Logger, ServiceUnavailableException } from "@nestjs/common";
|
||||
import { eq, and, sql, ilike, or } from "drizzle-orm";
|
||||
import { DATABASE, Database } from "../database/database.provider";
|
||||
import {
|
||||
@@ -272,6 +272,121 @@ export class EmexCatalogService {
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Wizard (proxy emexdwc.ae GetWizard2) ─────────────
|
||||
|
||||
private static readonly EMEX_BASE = "https://emexdwc.ae";
|
||||
private static readonly EMEX_HDR: Record<string, string> = {
|
||||
Accept: "application/json",
|
||||
Referer: "https://emexdwc.ae/CatalogParamSearch.aspx",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
||||
};
|
||||
|
||||
async getWizard(catalogCode: string, ssd: string): Promise<unknown> {
|
||||
const cacheKey = `emex:wizard:${catalogCode}:${ssd || "_root"}`;
|
||||
const cached = await this.redis.getJson(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
catalogCode,
|
||||
ssd,
|
||||
_tstamp: String(Date.now()),
|
||||
});
|
||||
const url = `${EmexCatalogService.EMEX_BASE}/api/Catalog.svc/GetWizard2?${params}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: EmexCatalogService.EMEX_HDR,
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`EMEX wizard HTTP ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
await this.redis.setJson(cacheKey, data, 3600); // 1h cache
|
||||
return data;
|
||||
} catch (err) {
|
||||
this.logger.error(`getWizard failed: ${(err as Error).message}`);
|
||||
throw new ServiceUnavailableException("EMEX wizard servisi kulanilamiyor");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find vehicles in our DB that match a wizard "Sales Designation" name.
|
||||
* Called after the user completes the wizard and picks a specific vehicle name.
|
||||
*/
|
||||
async getWizardVehicles(catalogCode: string, name: string): Promise<EmexVehicleDto[]> {
|
||||
const cacheKey = `emex:wv:${catalogCode}:${Buffer.from(name).toString("base64url").slice(0, 32)}`;
|
||||
const cached = await this.redis.getJson<EmexVehicleDto[]>(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const [catalog] = await this.db
|
||||
.select({ id: emexCatalogs.id })
|
||||
.from(emexCatalogs)
|
||||
.where(eq(emexCatalogs.catalogId, catalogCode))
|
||||
.limit(1);
|
||||
|
||||
if (!catalog) return [];
|
||||
|
||||
// Exact match on name
|
||||
let rows = await this.db
|
||||
.select({
|
||||
id: emexVehicles.id,
|
||||
vehicleId: emexVehicles.vehicleId,
|
||||
name: emexVehicles.name,
|
||||
engine: emexVehicles.engine,
|
||||
engineCode: emexVehicles.engineCode,
|
||||
bodyType: emexVehicles.bodyType,
|
||||
transmission: emexVehicles.transmission,
|
||||
driveType: emexVehicles.driveType,
|
||||
fuelType: emexVehicles.fuelType,
|
||||
yearFrom: emexVehicles.yearFrom,
|
||||
yearTo: emexVehicles.yearTo,
|
||||
optionsRaw: emexVehicles.optionsRaw,
|
||||
})
|
||||
.from(emexVehicles)
|
||||
.where(
|
||||
and(
|
||||
eq(emexVehicles.catalogId, catalog.id),
|
||||
eq(emexVehicles.name, name),
|
||||
),
|
||||
)
|
||||
.orderBy(emexVehicles.optionsRaw);
|
||||
|
||||
// Partial match fallback: search by prefix
|
||||
if (rows.length === 0) {
|
||||
rows = await this.db
|
||||
.select({
|
||||
id: emexVehicles.id,
|
||||
vehicleId: emexVehicles.vehicleId,
|
||||
name: emexVehicles.name,
|
||||
engine: emexVehicles.engine,
|
||||
engineCode: emexVehicles.engineCode,
|
||||
bodyType: emexVehicles.bodyType,
|
||||
transmission: emexVehicles.transmission,
|
||||
driveType: emexVehicles.driveType,
|
||||
fuelType: emexVehicles.fuelType,
|
||||
yearFrom: emexVehicles.yearFrom,
|
||||
yearTo: emexVehicles.yearTo,
|
||||
optionsRaw: emexVehicles.optionsRaw,
|
||||
})
|
||||
.from(emexVehicles)
|
||||
.where(
|
||||
and(
|
||||
eq(emexVehicles.catalogId, catalog.id),
|
||||
ilike(emexVehicles.name, `${name}%`),
|
||||
),
|
||||
)
|
||||
.orderBy(emexVehicles.optionsRaw)
|
||||
.limit(100);
|
||||
}
|
||||
|
||||
if (rows.length > 0) {
|
||||
await this.redis.setJson(cacheKey, rows, CACHE_TTL.vehicles);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
async searchByOem(query: string): Promise<EmexSearchResult[]> {
|
||||
if (!query || query.length < 3) return [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user