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:
Sase Dev
2026-03-02 00:31:15 +00:00
parent f2189a014b
commit 72c0de65de
90 changed files with 12043 additions and 419 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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 (C450C454).
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 }> {

View File

@@ -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",
};