docs: update INDEX.md + add catalog module, Ford/PSA legacy catalog, shared vehicles
- CatalogModule: VIN-less PL24 catalog browser (brands, models, categories, parts) - Supports P5 Modern (REST) and P4 Legacy (Ford, PSA) catalog architectures - Ford variant selector (model-year/engine/gearbox), PSA variant selector (body/engine/gearbox) - New API endpoints: ford-config, psa-bodies, psa-engines, psa-gearboxes, brands/:name/catalogs - Shared vehicles: vehicles table decoupled from users via userVehicles junction table - PL24 Ford Legacy service: comprehensive HTML-scraping for Ford/PSA/Hyundai/Kia/Nissan/Opel/Volvo - PL24 types and service updated for P4 Legacy brand support - Categories/parts service updated for dual FK (vehicleId + catalogVehicleId) pattern - Catalog browser frontend routes and components - docs/INDEX.md: updated with all new endpoints, components, hooks, routes (2026-03-02) - docs/pl24-catalog/: per-brand catalog exploration docs - scripts/migration-shared-vehicles.sql, pl24-catalog-explorer.js, posthog-dashboards.sh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ import {
|
||||
getServiceApiPath,
|
||||
getServiceConfig,
|
||||
isP5Modern,
|
||||
isLegacyArchitecture,
|
||||
SERVICE_TO_BRAND,
|
||||
} from "./pl24.types";
|
||||
|
||||
@@ -76,14 +77,12 @@ export class PL24Service {
|
||||
);
|
||||
}
|
||||
|
||||
// Dispatch legacy architectures to their dedicated services
|
||||
// Dispatch P4 legacy architectures to the generic legacy service
|
||||
if (!isP5Modern(serviceName)) {
|
||||
if (serviceName === "fordt_parts") {
|
||||
return this.fordLegacyService.decodeVin(cleanVin);
|
||||
if (isLegacyArchitecture(serviceName)) {
|
||||
return this.fordLegacyService.decodeVinForService(cleanVin, serviceName);
|
||||
}
|
||||
this.logger.warn(
|
||||
`Legacy architecture not supported yet: ${serviceName}`,
|
||||
);
|
||||
this.logger.warn(`Unknown architecture for service: ${serviceName}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -255,11 +254,18 @@ export class PL24Service {
|
||||
async fetchPartsByPath(
|
||||
linkPath: string,
|
||||
serviceName: string,
|
||||
body?: string,
|
||||
engine?: string,
|
||||
gearbox?: string,
|
||||
): Promise<PL24PartsResponse> {
|
||||
// Ford legacy dispatch
|
||||
if (this.isFordLegacyPath(linkPath)) {
|
||||
if (this.isP4LegacyPath(linkPath)) {
|
||||
return this.fordLegacyService.fetchPartsByPath(linkPath, serviceName);
|
||||
}
|
||||
// PSA image-board dispatch
|
||||
if (this.isPsaBoardPath(linkPath)) {
|
||||
return this.fordLegacyService.fetchPsaParts(linkPath, serviceName, body, engine, gearbox);
|
||||
}
|
||||
|
||||
await this.touchActivity();
|
||||
const pathHash = createHash("sha256").update(linkPath).digest("hex").substring(0, 16);
|
||||
@@ -378,11 +384,22 @@ export class PL24Service {
|
||||
async fetchSubGroupsByPath(
|
||||
linkPath: string,
|
||||
serviceName: string,
|
||||
body?: string,
|
||||
engine?: string,
|
||||
gearbox?: string,
|
||||
): Promise<PL24MainGroup[]> {
|
||||
// Ford legacy dispatch
|
||||
if (this.isFordLegacyPath(linkPath)) {
|
||||
if (this.isP4LegacyPath(linkPath)) {
|
||||
return this.fordLegacyService.fetchSubGroupsByPath(linkPath, serviceName);
|
||||
}
|
||||
// PSA scope dispatch ("psa::{svc}::scope=..." → main groups)
|
||||
if (this.isPsaPath(linkPath)) {
|
||||
return this.fordLegacyService.fetchPsaSubGroups(linkPath, body, engine, gearbox);
|
||||
}
|
||||
// PSA illustrations dispatch (/psa/.../json-illustrations.action → illustrations)
|
||||
if (this.isPsaIllusPath(linkPath)) {
|
||||
return this.fordLegacyService.fetchPsaIllustrations(linkPath, serviceName, body, engine, gearbox);
|
||||
}
|
||||
|
||||
this.logger.log(`Fetching sub-groups by path: ${linkPath}`);
|
||||
await this.touchActivity();
|
||||
@@ -406,6 +423,122 @@ export class PL24Service {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch category tree scopes for a PSA legacy catalog vehicle.
|
||||
* Delegates to fordLegacyService which handles PSA HTML scraping flow.
|
||||
*/
|
||||
async fetchMainGroupsForPsa(
|
||||
serviceName: string,
|
||||
familyId: string,
|
||||
salesTypeId: string,
|
||||
mode: string,
|
||||
upds: string,
|
||||
body?: string,
|
||||
engine?: string,
|
||||
gearbox?: string,
|
||||
): Promise<PL24DecodedCategory[]> {
|
||||
return this.fordLegacyService.fetchMainGroupsForPsa(
|
||||
serviceName,
|
||||
familyId,
|
||||
salesTypeId,
|
||||
mode,
|
||||
upds,
|
||||
body,
|
||||
engine,
|
||||
gearbox,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch body types for a PSA catalog vehicle variant selector.
|
||||
*/
|
||||
async fetchPsaBodies(
|
||||
svc: string,
|
||||
familyId: string,
|
||||
salesTypeId: string,
|
||||
mode: string,
|
||||
upds: string,
|
||||
): Promise<{ code: string; name: string }[]> {
|
||||
return this.fordLegacyService.fetchPsaBodies(svc, familyId, salesTypeId, mode, upds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch engines for a PSA catalog vehicle given a selected body code.
|
||||
*/
|
||||
async fetchPsaEnginesForBody(
|
||||
svc: string,
|
||||
familyId: string,
|
||||
salesTypeId: string,
|
||||
bodyCode: string,
|
||||
mode: string,
|
||||
upds: string,
|
||||
): Promise<{ code: string; name: string }[]> {
|
||||
return this.fordLegacyService.fetchPsaEnginesForBody(svc, familyId, salesTypeId, bodyCode, mode, upds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch gearboxes for a PSA catalog vehicle given selected body + engine codes.
|
||||
*/
|
||||
async fetchPsaGearboxes(
|
||||
svc: string,
|
||||
familyId: string,
|
||||
salesTypeId: string,
|
||||
bodyCode: string,
|
||||
engineCode: string,
|
||||
mode: string,
|
||||
upds: string,
|
||||
): Promise<{ code: string; name: string }[]> {
|
||||
return this.fordLegacyService.fetchPsaGearboxes(svc, familyId, salesTypeId, bodyCode, engineCode, mode, upds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch model config (variant options) for a Ford catalog vehicle.
|
||||
*/
|
||||
async fetchFordModelConfig(
|
||||
svc: string,
|
||||
familyId: string,
|
||||
mode: string,
|
||||
upds: string,
|
||||
): Promise<{
|
||||
modelYears: { code: string; name: string }[];
|
||||
engines: { code: string; name: string }[];
|
||||
gearboxes: { code: string; name: string }[];
|
||||
}> {
|
||||
return this.fordLegacyService.fetchFordModelConfig(svc, familyId, mode, upds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch model config (year variant options) for a Volvo catalog vehicle.
|
||||
*/
|
||||
async fetchVolvoModelConfig(
|
||||
svc: string,
|
||||
mdlId: string,
|
||||
mode: string,
|
||||
upds: string,
|
||||
): Promise<{
|
||||
modelYears: { code: string; name: string }[];
|
||||
engines: { code: string; name: string }[];
|
||||
gearboxes: { code: string; name: string }[];
|
||||
}> {
|
||||
return this.fordLegacyService.fetchVolvoModelConfig(svc, mdlId, mode, upds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch main category groups for a Ford catalog vehicle variant.
|
||||
*/
|
||||
async fetchFordMainGroups(
|
||||
svc: string,
|
||||
familyId: string,
|
||||
modelYear: string,
|
||||
engine: string,
|
||||
gearbox: string,
|
||||
mode: string,
|
||||
upds: string,
|
||||
catCode?: string,
|
||||
): Promise<PL24DecodedCategory[]> {
|
||||
return this.fordLegacyService.fetchFordMainGroups(svc, familyId, modelYear, engine, gearbox, mode, upds, catCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-fetch main groups using a stored mainGroupsPath.
|
||||
*/
|
||||
@@ -414,6 +547,21 @@ export class PL24Service {
|
||||
mainGroupsPath: string,
|
||||
): Promise<PL24DecodedCategory[]> {
|
||||
await this.touchActivity();
|
||||
// Ford legacy catalog vehicles store the vehicle.action URL as catalogPath.
|
||||
// Dispatch to fordLegacyService which fetches and extracts group links from the HTML.
|
||||
if (this.isP4LegacyPath(mainGroupsPath)) {
|
||||
const groups = await this.fordLegacyService.fetchSubGroupsByPath(mainGroupsPath, serviceName);
|
||||
return groups.map((g) => ({
|
||||
code: g.code,
|
||||
nameEn: g.name,
|
||||
nameTr: g.name,
|
||||
description: g.description || null,
|
||||
iconUrl: g.iconUrl || null,
|
||||
subGroups: [],
|
||||
linkPath: g.linkPath,
|
||||
linkWid: g.linkWid,
|
||||
}));
|
||||
}
|
||||
try {
|
||||
await this.authService.authorizeService(serviceName);
|
||||
const headers = await this.authService.buildAuthHeaders(serviceName);
|
||||
@@ -553,6 +701,15 @@ export class PL24Service {
|
||||
return isP5Modern(serviceName) || serviceName === "fordt_parts";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this VIN's WMI maps to any PL24 service (P5 Modern or P4 Legacy).
|
||||
* Use this to gate VIN decode attempts; use isSupported() for catalog browser eligibility.
|
||||
*/
|
||||
isDecodeable(vin: string): boolean {
|
||||
if (!vin || vin.length < 3) return false;
|
||||
return !!this.getServiceName(vin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported brands list.
|
||||
*/
|
||||
@@ -1017,7 +1174,9 @@ export class PL24Service {
|
||||
}
|
||||
|
||||
const partRecords = records.filter(
|
||||
(record) => record.characteristic !== "sectionrow" && record.partno,
|
||||
(record) =>
|
||||
record.characteristic !== "sectionrow" &&
|
||||
(record.partno || (record.values as Record<string, unknown>)?.partno),
|
||||
);
|
||||
|
||||
return partRecords.map((part) => {
|
||||
@@ -1152,7 +1311,9 @@ export class PL24Service {
|
||||
const mercedesMatch = imageUrl.match(/[?&]illu=([a-zA-Z0-9_]+)/);
|
||||
if (mercedesMatch) return mercedesMatch[1];
|
||||
|
||||
return null;
|
||||
// PSA and other legacy brands use ticket-based URLs that don't match above patterns.
|
||||
// Fall back to a hash of the URL so caching still works.
|
||||
return createHash("sha256").update(imageUrl).digest("hex").substring(0, 24);
|
||||
}
|
||||
|
||||
// ==================== PRIVATE: Brand-specific flows ====================
|
||||
@@ -1183,8 +1344,20 @@ export class PL24Service {
|
||||
return `${url.pathname}?${url.searchParams.toString()}`;
|
||||
}
|
||||
|
||||
private isFordLegacyPath(linkPath: string): boolean {
|
||||
return linkPath.includes("/ford/") && linkPath.includes(".action");
|
||||
private isP4LegacyPath(linkPath: string): boolean {
|
||||
return linkPath.includes(".action");
|
||||
}
|
||||
|
||||
private isPsaPath(linkPath: string): boolean {
|
||||
return linkPath.startsWith("psa::");
|
||||
}
|
||||
|
||||
private isPsaIllusPath(linkPath: string): boolean {
|
||||
return linkPath.includes("/psa/") && linkPath.includes("json-illustrations.action");
|
||||
}
|
||||
|
||||
private isPsaBoardPath(linkPath: string): boolean {
|
||||
return linkPath.includes("/psa/") && linkPath.includes("image-board.action");
|
||||
}
|
||||
|
||||
private isDaimlerService(serviceName: string): boolean {
|
||||
@@ -1540,6 +1713,237 @@ export class PL24Service {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PUBLIC: Catalog browse (VIN-less) ====================
|
||||
|
||||
/**
|
||||
* Fetch a list of vehicles/models for a service (VIN-less catalog browse).
|
||||
* Tries P5 Modern selection wizard endpoints.
|
||||
* Returns empty array if unavailable (requires discovery to find correct endpoint).
|
||||
*/
|
||||
async fetchVehicleList(serviceName: string): Promise<
|
||||
Array<{
|
||||
vehicleId: string;
|
||||
model: string;
|
||||
year?: string;
|
||||
engine?: string;
|
||||
bodyType?: string;
|
||||
transmission?: string;
|
||||
market?: string;
|
||||
catalogPath?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}>
|
||||
> {
|
||||
await this.touchActivity();
|
||||
|
||||
// Legacy architecture dispatch
|
||||
const serviceConfig = getServiceConfig(serviceName);
|
||||
if (serviceConfig?.architecture === "LEGACY_PSA") {
|
||||
return this.fordLegacyService.fetchVehicleListForPsa(serviceName);
|
||||
}
|
||||
if (serviceConfig?.architecture === "LEGACY_FORD") {
|
||||
return this.fordLegacyService.fetchVehicleListForFord(serviceName);
|
||||
}
|
||||
if (serviceConfig?.architecture === "LEGACY_HYUNDAI_KIA") {
|
||||
return this.fordLegacyService.fetchVehicleListForHyundaiKia(serviceName);
|
||||
}
|
||||
if (serviceConfig?.architecture === "LEGACY_NISSAN") {
|
||||
return this.fordLegacyService.fetchVehicleListForNissan(serviceName);
|
||||
}
|
||||
if (serviceConfig?.architecture === "LEGACY_OPEL") {
|
||||
return this.fordLegacyService.fetchVehicleListForOpel(serviceName);
|
||||
}
|
||||
if (serviceConfig?.architecture === "LEGACY_VOLVO") {
|
||||
return this.fordLegacyService.fetchVehicleListForVolvo(serviceName);
|
||||
}
|
||||
|
||||
const cacheKey = `${PL24_DEFAULTS.CACHE_PREFIX}vehicle_list:${serviceName}`;
|
||||
const cached = await this.redis.getJson<any[]>(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
await this.authService.authorizeService(serviceName);
|
||||
const headers = await this.authService.buildAuthHeaders(serviceName);
|
||||
|
||||
const catalogBase = getServiceApiPath(serviceName);
|
||||
|
||||
// Discovered via Playwright explorer (scripts/pl24-catalog-explorer.js → docs/pl24-catalog/*.md)
|
||||
// Each P5 backend uses a different initial model listing endpoint
|
||||
const BACKEND_MODEL_PATH: Record<string, string> = {
|
||||
p5vwag: "/extern/vehicle/modelfamilies", // VW, Audi, Skoda, SEAT, Cupra, Porsche, Bentley
|
||||
p5bmw: "/extern/vehicle/models", // BMW, MINI, Motorrad
|
||||
p5daimler: "/extern/vehicle/scope", // Mercedes-Benz, smart
|
||||
p5renault: "/extern/vehicle/catalogs", // Renault, Dacia, Alpine
|
||||
p5jlr: "/extern/vehicle/models", // Jaguar, Land Rover
|
||||
p5toyota: "/extern/vehicle/modelFamilies", // Toyota, Lexus (capital F)
|
||||
p5mitsubishi: "/extern/vehicles/vehiclesOverview", // Mitsubishi
|
||||
p5suzuki: "/extern/vehicle/modelFamilies", // Suzuki
|
||||
p5man: "/extern/model/categories", // MAN trucks
|
||||
};
|
||||
|
||||
// catalogBase is like "/p5vwag" — strip leading slash for map lookup
|
||||
const backendKey = catalogBase.replace(/^\//, "");
|
||||
const modelPath = BACKEND_MODEL_PATH[backendKey] ?? "/extern/vehicle/modelfamilies";
|
||||
|
||||
const url = `${this.baseUrl}${catalogBase}${modelPath}?lang=${this.language}&serviceName=${serviceName}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as Record<string, any>;
|
||||
let vehicles = this.parseVehicleListResponse(data, serviceName);
|
||||
|
||||
// p5daimler "scope" endpoint returns 1 record whose link.path points to "modeltype" (Smart).
|
||||
// Follow that link to retrieve the actual model type list (C450–C454).
|
||||
if (vehicles.length === 1 && vehicles[0].catalogPath?.includes("modeltype")) {
|
||||
const modeltypePath = vehicles[0].catalogPath;
|
||||
const modeltypeUrl = modeltypePath.startsWith("http")
|
||||
? modeltypePath
|
||||
: `${this.baseUrl}${modeltypePath}`;
|
||||
try {
|
||||
const modeltypeResp = await fetch(modeltypeUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (modeltypeResp.ok) {
|
||||
const modeltypeData = (await modeltypeResp.json()) as Record<string, any>;
|
||||
const modeltypeVehicles = this.parseVehicleListResponse(modeltypeData, serviceName);
|
||||
if (modeltypeVehicles.length > 0) {
|
||||
this.logger.log(`Smart: modeltype returned ${modeltypeVehicles.length} models`);
|
||||
vehicles = modeltypeVehicles;
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(`Smart: modeltype HTTP ${modeltypeResp.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`Smart: modeltype fetch failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (vehicles.length > 0) {
|
||||
await this.redis.setJson(cacheKey, vehicles, 86400); // 24h
|
||||
return vehicles;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`No vehicle list available for ${serviceName} (HTTP ${response.status})`);
|
||||
return [];
|
||||
} catch (err) {
|
||||
this.logger.warn(`fetchVehicleList failed for ${serviceName}: ${(err as Error).message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private parseVehicleListResponse(
|
||||
data: Record<string, any>,
|
||||
serviceName: string,
|
||||
): Array<{
|
||||
vehicleId: string;
|
||||
model: string;
|
||||
year?: string;
|
||||
engine?: string;
|
||||
bodyType?: string;
|
||||
transmission?: string;
|
||||
market?: string;
|
||||
catalogPath?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}> {
|
||||
let records: any[] = [];
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
records = data;
|
||||
} else if (Array.isArray(data.data?.records)) {
|
||||
records = data.data.records;
|
||||
} else if (Array.isArray(data.vehicles)) {
|
||||
records = data.vehicles;
|
||||
} else if (Array.isArray(data.models)) {
|
||||
records = data.models;
|
||||
} else if (Array.isArray(data.data)) {
|
||||
records = data.data;
|
||||
}
|
||||
|
||||
return records
|
||||
.filter((r) => r.id || r.vehicleId || r.vid)
|
||||
.map((r) => {
|
||||
const values = r.values || {};
|
||||
const vehicleId = String(r.id || r.vehicleId || r.vid || "");
|
||||
// modelfamilies uses values.caption; older formats use values.model / r.description
|
||||
const model =
|
||||
values.caption || values.model || values.description || r.description || r.name || vehicleId;
|
||||
const link = r.link || {};
|
||||
|
||||
return {
|
||||
vehicleId,
|
||||
model,
|
||||
year: values.year || values.modelYear || r.year || undefined,
|
||||
engine: values.engine || values.engineCode || undefined,
|
||||
bodyType: values.bodyType || values.body || undefined,
|
||||
transmission: values.transmission || undefined,
|
||||
market: values.market || undefined,
|
||||
catalogPath: link.path || undefined,
|
||||
metadata: r,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Explore a P5 Modern service catalog — tries various endpoints and returns raw results.
|
||||
* Used by admin endpoint for discovery.
|
||||
*/
|
||||
async exploreP5Service(serviceName: string): Promise<Record<string, any>> {
|
||||
await this.touchActivity();
|
||||
const results: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
await this.authService.authorizeService(serviceName);
|
||||
const headers = await this.authService.buildAuthHeaders(serviceName);
|
||||
const catalogBase = getServiceApiPath(serviceName);
|
||||
|
||||
const probeEndpoints = [
|
||||
"extern/vehicles",
|
||||
"extern/vehicleList",
|
||||
"extern/models",
|
||||
"extern/selection/vehicles",
|
||||
"extern/selection/rootNode",
|
||||
"extern/catalogs",
|
||||
"extern/modelSeries",
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
probeEndpoints.map(async (endpoint) => {
|
||||
const url = `${this.baseUrl}${catalogBase}/${endpoint}?lang=${this.language}&serviceName=${serviceName}`;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const status = response.status;
|
||||
let body: any = null;
|
||||
if (response.ok) {
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {
|
||||
body = await response.text();
|
||||
}
|
||||
}
|
||||
results[endpoint] = { status, body: body ? JSON.stringify(body).substring(0, 2000) : null };
|
||||
} catch (err) {
|
||||
results[endpoint] = { error: (err as Error).message };
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
results._authError = (err as Error).message;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private parseJlrIllustrations(
|
||||
response: unknown,
|
||||
): Array<{ btnr: number; name: string; code: string }> {
|
||||
|
||||
@@ -276,7 +276,7 @@ export const PL24_SERVICE_CATALOGS: Record<string, PL24CatalogConfig> = {
|
||||
// Mitsubishi
|
||||
mmc_parts: {
|
||||
basePath: "/pl24-app/mmc_parts",
|
||||
apiPath: "/p5mmc",
|
||||
apiPath: "/p5mitsubishi",
|
||||
architecture: "P5_MODERN",
|
||||
},
|
||||
|
||||
@@ -287,12 +287,84 @@ export const PL24_SERVICE_CATALOGS: Record<string, PL24CatalogConfig> = {
|
||||
architecture: "P5_MODERN",
|
||||
},
|
||||
|
||||
// Ford
|
||||
fordt_parts: {
|
||||
basePath: "/ford/fordt_parts",
|
||||
apiPath: "/ford/fordt_parts",
|
||||
// ==================== LEGACY P4 ARCHITECTURE ====================
|
||||
|
||||
// PSA Group (Citroën, Peugeot)
|
||||
citroen_parts: {
|
||||
basePath: "/psa",
|
||||
apiPath: "/psa",
|
||||
architecture: "LEGACY_PSA",
|
||||
},
|
||||
citroenDs_parts: {
|
||||
basePath: "/psa",
|
||||
apiPath: "/psa",
|
||||
architecture: "LEGACY_PSA",
|
||||
},
|
||||
peugeot_parts: {
|
||||
basePath: "/psa",
|
||||
apiPath: "/psa",
|
||||
architecture: "LEGACY_PSA",
|
||||
},
|
||||
|
||||
// Ford Group
|
||||
fordp_parts: {
|
||||
basePath: "/ford",
|
||||
apiPath: "/ford",
|
||||
architecture: "LEGACY_FORD",
|
||||
},
|
||||
fordt_parts: {
|
||||
basePath: "/ford",
|
||||
apiPath: "/ford",
|
||||
architecture: "LEGACY_FORD",
|
||||
},
|
||||
|
||||
// Hyundai-Kia Automotive Group
|
||||
hyundai_parts: {
|
||||
basePath: "/hyundai-kia-automotive-group",
|
||||
apiPath: "/hyundai-kia-automotive-group",
|
||||
architecture: "LEGACY_HYUNDAI_KIA",
|
||||
},
|
||||
kia_parts: {
|
||||
basePath: "/hyundai-kia-automotive-group",
|
||||
apiPath: "/hyundai-kia-automotive-group",
|
||||
architecture: "LEGACY_HYUNDAI_KIA",
|
||||
},
|
||||
|
||||
// Nissan/Infiniti
|
||||
nissan_parts: {
|
||||
basePath: "/nissan",
|
||||
apiPath: "/nissan",
|
||||
architecture: "LEGACY_NISSAN",
|
||||
},
|
||||
infiniti_parts: {
|
||||
basePath: "/nissan",
|
||||
apiPath: "/nissan",
|
||||
architecture: "LEGACY_NISSAN",
|
||||
},
|
||||
|
||||
// GM / Stellantis (Opel, Vauxhall)
|
||||
opel_parts: {
|
||||
basePath: "/opel",
|
||||
apiPath: "/opel",
|
||||
architecture: "LEGACY_OPEL",
|
||||
},
|
||||
vauxhall_parts: {
|
||||
basePath: "/opel",
|
||||
apiPath: "/opel",
|
||||
architecture: "LEGACY_OPEL",
|
||||
},
|
||||
|
||||
// Volvo/Polestar
|
||||
volvo_parts: {
|
||||
basePath: "/volvo",
|
||||
apiPath: "/volvo",
|
||||
architecture: "LEGACY_VOLVO",
|
||||
},
|
||||
polestar_parts: {
|
||||
basePath: "/volvo",
|
||||
apiPath: "/volvo",
|
||||
architecture: "LEGACY_VOLVO",
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== HELPER FUNCTIONS ====================
|
||||
@@ -436,11 +508,43 @@ export const PL24_WMI_SERVICE_MAP: Record<string, string> = {
|
||||
MA3: "suzuki_parts",
|
||||
MBH: "suzuki_parts",
|
||||
|
||||
// Ford
|
||||
// Ford Commercial (Transit vans — Turkey Otosan, etc.)
|
||||
NM0: "fordt_parts",
|
||||
WF0: "fordt_parts",
|
||||
"1FA": "fordt_parts",
|
||||
"3FA": "fordt_parts",
|
||||
|
||||
// Ford Passenger (European passenger cars — Germany Cologne plant)
|
||||
WF0: "fordp_parts",
|
||||
"1FA": "fordp_parts",
|
||||
"3FA": "fordp_parts",
|
||||
|
||||
// Hyundai
|
||||
KMH: "hyundai_parts", // Hyundai Korea Motor House
|
||||
TMK: "hyundai_parts", // Hyundai (Turkey/other markets)
|
||||
|
||||
// Kia
|
||||
KNA: "kia_parts", // Kia (worldwide production)
|
||||
U5Y: "kia_parts", // Kia Slovakia
|
||||
|
||||
// Nissan
|
||||
JN1: "nissan_parts", // Nissan Japan (passenger)
|
||||
JN6: "nissan_parts", // Nissan Japan (pickup/van)
|
||||
JN8: "nissan_parts", // Nissan Japan (SUV)
|
||||
VNK: "nissan_parts", // Nissan UK/Europe
|
||||
|
||||
// Infiniti
|
||||
JNK: "infiniti_parts", // Infiniti (Japan/Korea)
|
||||
|
||||
// Opel / Vauxhall
|
||||
W0L: "opel_parts", // Opel AG (Germany)
|
||||
|
||||
// Citroën (PSA)
|
||||
VF7: "citroen_parts", // Citroën SA (France)
|
||||
|
||||
// Peugeot (PSA)
|
||||
VF3: "peugeot_parts", // Peugeot SA (France)
|
||||
|
||||
// Volvo
|
||||
YV1: "volvo_parts", // Volvo Cars (Sweden)
|
||||
YV4: "volvo_parts", // Volvo Cars (specific models)
|
||||
};
|
||||
|
||||
// ==================== VEHICLE TYPES ====================
|
||||
@@ -451,6 +555,11 @@ export interface PL24CatalogInfo {
|
||||
catalogPath: string;
|
||||
baseUrl: string;
|
||||
mainGroupsPath?: string;
|
||||
// PSA VIN decode session parameters (set by decodeVinPsa)
|
||||
psaFamilyId?: string;
|
||||
psaSalesTypeId?: string;
|
||||
psaMode?: string;
|
||||
psaUpds?: string;
|
||||
}
|
||||
|
||||
export interface PL24MainGroup {
|
||||
@@ -518,6 +627,8 @@ export interface PL24PartsResponse {
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
schemaImageUrl?: string;
|
||||
schemaImageBuffer?: Buffer; // pre-downloaded buffer (PSA: ticket URLs expire quickly)
|
||||
schemaImageContentType?: string;
|
||||
schemaWidth?: number;
|
||||
schemaHeight?: number;
|
||||
parts: PL24Part[];
|
||||
@@ -589,37 +700,96 @@ export interface PL24DecodedPart {
|
||||
// ==================== BRAND MAP ====================
|
||||
|
||||
export const SERVICE_TO_BRAND: Record<string, string> = {
|
||||
// Volkswagen Group
|
||||
vw_parts: "Volkswagen",
|
||||
vwclassic_parts: "Volkswagen",
|
||||
vn_parts: "Volkswagen",
|
||||
audi_parts: "Audi",
|
||||
seat_parts: "SEAT",
|
||||
seat_parts: "Seat",
|
||||
cupra_parts: "Cupra",
|
||||
skoda_parts: "Skoda",
|
||||
bentley_parts: "Bentley",
|
||||
// BMW Group
|
||||
bmw_parts: "BMW",
|
||||
bmwclassic_parts: "BMW",
|
||||
bmwmotorrad_parts: "BMW",
|
||||
bmwmotorradclassic_parts: "BMW",
|
||||
mini_parts: "MINI",
|
||||
miniclassic_parts: "MINI",
|
||||
mini_parts: "Mini",
|
||||
miniclassic_parts: "Mini",
|
||||
// Mercedes-Benz Group
|
||||
mercedes_parts: "Mercedes-Benz",
|
||||
mercedesclassic_parts: "Mercedes-Benz",
|
||||
mercedesvans_parts: "Mercedes-Benz",
|
||||
mercedestrucks_parts: "Mercedes-Benz",
|
||||
mercedesunimog_parts: "Mercedes-Benz",
|
||||
smart_parts: "smart",
|
||||
smart_parts: "Smart",
|
||||
// Porsche (VAG backend)
|
||||
porsche_parts: "Porsche",
|
||||
porscheclassic_parts: "Porsche",
|
||||
// Toyota Group
|
||||
toyota_parts: "Toyota",
|
||||
lexus_parts: "Lexus",
|
||||
// Renault Group
|
||||
renault_parts: "Renault",
|
||||
dacia_parts: "Dacia",
|
||||
alpine_parts: "Alpine",
|
||||
// Jaguar Land Rover
|
||||
jaguar_parts: "Jaguar",
|
||||
landrover_parts: "Land Rover",
|
||||
// Other P5
|
||||
man_parts: "MAN",
|
||||
mmc_parts: "Mitsubishi",
|
||||
suzuki_parts: "Suzuki",
|
||||
// PSA Group
|
||||
citroen_parts: "Citroen",
|
||||
citroenDs_parts: "Citroen",
|
||||
peugeot_parts: "Peugeot",
|
||||
// Ford Group
|
||||
fordp_parts: "Ford",
|
||||
fordt_parts: "Ford",
|
||||
// Hyundai-Kia Group
|
||||
hyundai_parts: "Hyundai",
|
||||
kia_parts: "Kia",
|
||||
// Nissan/Infiniti
|
||||
nissan_parts: "Nissan",
|
||||
infiniti_parts: "Infiniti",
|
||||
// GM / Stellantis
|
||||
opel_parts: "Opel",
|
||||
vauxhall_parts: "Opel",
|
||||
// Volvo/Polestar
|
||||
volvo_parts: "Volvo",
|
||||
polestar_parts: "Polestar",
|
||||
};
|
||||
|
||||
// Display names for services that share a brand (multi-catalog brands)
|
||||
export const SERVICE_DISPLAY_NAMES: Record<string, string> = {
|
||||
// BMW Group
|
||||
bmw_parts: "BMW",
|
||||
bmwclassic_parts: "BMW Classic",
|
||||
bmwmotorrad_parts: "BMW Motorrad",
|
||||
bmwmotorradclassic_parts: "BMW Motorrad Classic",
|
||||
mini_parts: "Mini",
|
||||
miniclassic_parts: "Mini Classic",
|
||||
// Mercedes-Benz Group
|
||||
mercedes_parts: "Mercedes-Benz",
|
||||
mercedesclassic_parts: "Mercedes-Benz Classic",
|
||||
mercedesvans_parts: "Mercedes-Benz Vans",
|
||||
mercedestrucks_parts: "Mercedes-Benz Trucks",
|
||||
mercedesunimog_parts: "Mercedes-Benz Unimog",
|
||||
// Volkswagen Group
|
||||
vw_parts: "Volkswagen",
|
||||
vwclassic_parts: "Volkswagen Classic",
|
||||
vn_parts: "Volkswagen Nfz",
|
||||
porsche_parts: "Porsche",
|
||||
porscheclassic_parts: "Porsche Classic",
|
||||
// Renault Group
|
||||
renault_parts: "Renault",
|
||||
dacia_parts: "Dacia",
|
||||
alpine_parts: "Alpine",
|
||||
// Ford
|
||||
fordp_parts: "Ford",
|
||||
fordt_parts: "Ford Ticari",
|
||||
// Citroen
|
||||
citroen_parts: "Citroen",
|
||||
citroenDs_parts: "Citroen DS",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user