feat: admin user creation, Vite migration, dialog fix, pl24 integration

- Add POST /admin/users endpoint with password hashing and role support
- Add user creation dialog to admin users page
- Migrate web from Next.js to Vite + TanStack Router
- Fix Dialog component positioning for Tailwind CSS v4
- Add @source directive for @sase/ui package scanning
- Add pl24 integration parsers and vehicle decode flow
- Backup old Next.js app to apps/web-nj

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-02-12 14:24:58 +00:00
parent bf7f876abd
commit 996d614d50
211 changed files with 23805 additions and 549 deletions

View File

@@ -1,11 +1,19 @@
import { ParsedVehicle, ParsedCategory, PL24PartResponse } from "../pl24.types";
/**
* Base PL24 Parser
*
* Note: With the real PL24 API integration, parsing is done directly in
* PL24Service. These parsers are kept for potential future brand-specific
* response normalization.
*/
import type { PL24DecodedVehicle, PL24DecodedCategory, PL24Part } from "../pl24.types";
export abstract class BasePL24Parser {
abstract readonly brandName: string;
abstract parseVehicle(raw: Record<string, unknown>): ParsedVehicle;
abstract parseCategories(raw: unknown[]): ParsedCategory[];
abstract parseParts(raw: unknown[]): PL24PartResponse[];
abstract parseVehicle(raw: Record<string, unknown>): Partial<PL24DecodedVehicle>;
abstract parseCategories(raw: unknown[]): PL24DecodedCategory[];
abstract parseParts(raw: unknown[]): PL24Part[];
protected safeString(value: unknown): string {
if (typeof value === "string") return value;

View File

@@ -1,17 +1,7 @@
import { GenericPL24Parser } from "./generic-parser";
import { ParsedVehicle } from "../pl24.types";
export class BmwPL24Parser extends GenericPL24Parser {
constructor() {
super("BMW");
}
parseVehicle(raw: Record<string, unknown>): ParsedVehicle {
const base = super.parseVehicle(raw);
// BMW-specific: extract series from model code (E90, F30, G20, etc.)
if (base.modelCode && typeof raw.series === "string") {
base.name = `${raw.series} ${base.modelCode}`;
}
return base;
}
}

View File

@@ -1,5 +1,12 @@
/**
* Generic PL24 Parser
*
* Kept for backwards compatibility. With the real PL24 API integration,
* parsing is done directly in PL24Service using the actual API response format.
*/
import { BasePL24Parser } from "./base-parser";
import { ParsedVehicle, ParsedCategory, PL24PartResponse } from "../pl24.types";
import type { PL24DecodedVehicle, PL24DecodedCategory, PL24Part } from "../pl24.types";
export class GenericPL24Parser extends BasePL24Parser {
readonly brandName: string;
@@ -9,48 +16,43 @@ export class GenericPL24Parser extends BasePL24Parser {
this.brandName = brandName;
}
parseVehicle(raw: Record<string, unknown>): ParsedVehicle {
parseVehicle(raw: Record<string, unknown>): Partial<PL24DecodedVehicle> {
return {
vehicleId: this.safeString(raw.vehicleId || raw.id),
catalogId: this.safeString(raw.catalogId || raw.catalog_id),
name: this.safeString(raw.name || raw.description),
modelCode: this.safeString(raw.modelCode || raw.model_code || raw.model),
engine: this.safeString(raw.engine || raw.engineCode),
transmission: this.safeString(raw.transmission || raw.gearbox),
bodyType: this.safeString(raw.bodyType || raw.body_type || raw.body),
market: this.safeString(raw.market || raw.region),
yearFrom: this.safeNumber(raw.yearFrom || raw.year_from || raw.prodFrom),
yearTo: this.safeNumber(raw.yearTo || raw.year_to || raw.prodTo),
brand: this.brandName,
model: this.safeString(raw.model || raw.description),
year: this.safeNumber(raw.modelYear || raw.year),
series: this.safeString(raw.series) || null,
bodyType: this.safeString(raw.bodyType) || null,
engineCode: this.safeString(raw.engineCode) || null,
engineType: null,
engineVolume: null,
transmission: this.safeString(raw.transmission) || null,
driveType: this.safeString(raw.driveType) || null,
colorCode: this.safeString(raw.colorCode) || null,
raw,
catalogInfo: null,
categories: [],
};
}
parseCategories(raw: unknown[]): ParsedCategory[] {
return raw.map((item: any, index: number) => ({
groupId: this.safeString(item.groupId || item.id || item.group_id),
name: this.safeString(item.name || item.description),
parentGroupId: item.parentGroupId || item.parent_group_id || null,
sortOrder: this.safeNumber(item.sortOrder || item.sort_order || index),
hasSchemaPic: !!item.hasSchemaPic || !!item.has_schema || !!item.imageUrl,
}));
}
parseParts(raw: unknown[]): PL24PartResponse[] {
parseCategories(raw: unknown[]): PL24DecodedCategory[] {
return raw.map((item: any) => ({
partId: this.safeString(item.partId || item.id || item.part_id),
name: this.safeString(item.name || item.description),
description: this.safeString(item.description || item.additionalInfo || ""),
quantity: this.safeNumber(item.quantity || item.qty || 1),
position: this.safeString(item.position || item.pos || ""),
hotspotIndex: item.hotspotIndex ?? item.hotspot_index ?? item.callout ?? null,
oemCodes: this.extractOemCodes(item),
code: this.safeString(item.code || item.id),
nameEn: this.safeString(item.name || item.description),
description: this.safeString(item.description) || null,
iconUrl: null,
subGroups: [],
}));
}
private extractOemCodes(item: any): string[] {
if (Array.isArray(item.oemCodes)) return item.oemCodes;
if (Array.isArray(item.partNumbers)) return item.partNumbers.map((p: any) => p.code || p);
if (item.oemCode) return [item.oemCode];
if (item.partNumber) return [item.partNumber];
return [];
parseParts(raw: unknown[]): PL24Part[] {
return raw.map((item: any) => ({
id: this.safeString(item.id || item.partId),
oemCode: this.safeString(item.oemCode || item.partNumber),
name: this.safeString(item.name || item.description),
description: this.safeString(item.description) || undefined,
quantity: this.safeNumber(item.quantity || 1),
positionCode: this.safeString(item.position) || undefined,
}));
}
}

View File

@@ -1,17 +1,7 @@
import { GenericPL24Parser } from "./generic-parser";
import { ParsedVehicle } from "../pl24.types";
export class MercedesPL24Parser extends GenericPL24Parser {
constructor() {
super("Mercedes-Benz");
}
parseVehicle(raw: Record<string, unknown>): ParsedVehicle {
const base = super.parseVehicle(raw);
// Mercedes-specific: extract class (W205, W213, etc.)
if (typeof raw.baumuster === "string") {
base.modelCode = raw.baumuster;
}
return base;
}
}

View File

@@ -2,9 +2,10 @@ import { BasePL24Parser } from "./base-parser";
import { BmwPL24Parser } from "./bmw-parser";
import { MercedesPL24Parser } from "./mercedes-parser";
import { GenericPL24Parser } from "./generic-parser";
import { PL24_WMI_SERVICE_MAP, isP5Modern } from "../pl24.types";
const PARSER_MAP: Record<string, () => BasePL24Parser> = {
"BMW": () => new BmwPL24Parser(),
BMW: () => new BmwPL24Parser(),
"Mercedes-Benz": () => new MercedesPL24Parser(),
};
@@ -13,3 +14,15 @@ export function createParser(brandName: string): BasePL24Parser {
if (factory) return factory();
return new GenericPL24Parser(brandName);
}
/**
* Get service name from VIN's WMI (first 3 chars).
* Only returns P5 Modern services.
*/
export function getServiceForVin(vin: string): string | null {
if (!vin || vin.length < 3) return null;
const wmi = vin.substring(0, 3).toUpperCase();
const service = PL24_WMI_SERVICE_MAP[wmi];
if (service && isP5Modern(service)) return service;
return null;
}

View File

@@ -1,61 +1,308 @@
import { Injectable, Logger } from "@nestjs/common";
/**
* PartsLink24 Authentication Service
*
* Handles JWT authentication, token refresh, and session management
* for the partslink24.com API. Tokens cached in-memory (short-lived).
*/
import { Injectable, Logger, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { RedisService } from "../../redis/redis.service";
import { PL24_DEFAULTS } from "./pl24.constants";
import { PL24_ENDPOINTS } from "./pl24.constants";
import type {
PL24LoginRequest,
PL24LoginResponse,
PL24TokenData,
PL24JWTPayload,
PL24AuthorizeRequest,
PL24AuthorizeResponse,
} from "./pl24.types";
@Injectable()
export class PL24AuthService {
private readonly logger = new Logger(PL24AuthService.name);
private readonly cacheKey = `${PL24_DEFAULTS.CACHE_PREFIX}auth_token`;
private tokenData: PL24TokenData | null = null;
private serviceTokens = new Map<
string,
{ token: string; expiresAt: Date }
>();
constructor(
private configService: ConfigService,
private redis: RedisService,
) {}
private readonly baseUrl: string;
private readonly companyCode: string;
private readonly username: string;
private readonly password: string;
private readonly timeout: number;
async getToken(): Promise<string> {
// Check Redis cache
const cached = await this.redis.get(this.cacheKey);
if (cached) return cached;
constructor(private configService: ConfigService) {
this.baseUrl = this.configService.get<string>(
"pl24.baseUrl",
"https://www.partslink24.com",
);
this.companyCode = this.configService.get<string>("pl24.companyCode", "");
this.username = this.configService.get<string>("pl24.username", "");
this.password = this.configService.get<string>("pl24.password", "");
this.timeout = 30000;
// Authenticate with PL24
const token = await this.authenticate();
await this.redis.set(this.cacheKey, token, PL24_DEFAULTS.AUTH_TOKEN_TTL);
return token;
if (!this.companyCode || !this.username || !this.password) {
this.logger.warn(
"PL24 credentials not configured. Set PL24_BASE_URL, PL24_COMPANY_CODE, PL24_USERNAME, PL24_PASSWORD",
);
}
}
private async authenticate(): Promise<string> {
const apiUrl = this.configService.get<string>("pl24.apiUrl");
const username = this.configService.get<string>("pl24.username");
const password = this.configService.get<string>("pl24.password");
if (!apiUrl || !username || !password) {
this.logger.warn("PL24 credentials not configured");
throw new Error("PL24 credentials not configured");
/**
* Login to PL24 and get access token.
* Uses squeezeOut=true to force logout other sessions.
*/
async login(forceNew = false): Promise<PL24TokenData> {
if (!forceNew && this.tokenData && this.isTokenValid(this.tokenData)) {
return this.tokenData;
}
this.logger.log("Logging in to PL24...");
const loginRequest: PL24LoginRequest = {
authentication: {
account: this.companyCode,
user: this.username,
pwd: this.password,
},
device: {
id: "0",
os: "Windows 10",
offset: "0",
lang: "en-US",
"os-version": "0",
},
"app-version": "",
squeezeOut: true,
};
try {
const response = await fetch(`${apiUrl}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
signal: AbortSignal.timeout(PL24_DEFAULTS.REQUEST_TIMEOUT),
});
const response = await fetch(
`${this.baseUrl}${PL24_ENDPOINTS.LOGIN}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
body: JSON.stringify(loginRequest),
signal: AbortSignal.timeout(this.timeout),
},
);
if (!response.ok) {
throw new Error(`PL24 auth failed: ${response.status}`);
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = (await response.json()) as { token: string };
this.logger.log("PL24 authenticated successfully");
return data.token;
const data = (await response.json()) as PL24LoginResponse;
if (data.status === "USER_ALREADY_LOGGED_IN") {
this.logger.warn("User already logged in, session squeezed out");
}
if (!data.token?.access_token) {
this.logger.error(
`PL24 login failed: ${data.status} - ${data.message || "No token returned"}`,
);
throw new UnauthorizedException(
`PL24 giris basarisiz: ${data.message || data.status || "Token alinamadi"}`,
);
}
// Extract session cookie
const setCookie = response.headers.get("set-cookie");
const sessionCookie = this.extractSessionCookie(setCookie);
// Decode JWT for expiration + services
const payload = this.decodeJWT(data.token.access_token);
this.tokenData = {
accessToken: data.token.access_token,
refreshToken: data.refreshToken || "",
sessionCookie,
expiresAt: new Date(payload.exp * 1000),
services: payload.services || [],
};
this.logger.log(
`PL24 login successful. Token expires at ${this.tokenData.expiresAt.toISOString()}`,
);
this.logger.log(
`Available services: ${this.tokenData.services.length}`,
);
return this.tokenData;
} catch (error) {
this.logger.error("PL24 authentication failed", error);
throw error;
const err = error as Error;
if (err.name === "TimeoutError") {
throw new UnauthorizedException("PL24 giris zaman asimina ugradi");
}
this.logger.error(`PL24 login error: ${err.message}`);
throw new UnauthorizedException(`PL24 giris hatasi: ${err.message}`);
}
}
async invalidateToken(): Promise<void> {
await this.redis.del(this.cacheKey);
/**
* Get service-specific authorization token.
* Required for accessing specific catalogs.
*/
async authorizeService(serviceName: string): Promise<string> {
const cached = this.serviceTokens.get(serviceName);
if (cached && cached.expiresAt > new Date()) {
return cached.token;
}
const mainToken = await this.getAccessToken();
this.logger.log(`Authorizing service: ${serviceName}`);
const authorizeRequest: PL24AuthorizeRequest = {
serviceNames: [
"cart",
"pl24-full-vin-data",
"pl24-orderbridge",
"pl24-orderbridge-cart",
"pl24-sendbtmail",
"pl24-qparts",
"orderBook",
"pl24-usage",
"pl24-tls-pilot",
serviceName,
],
serviceCategoryNames: ["pl24-shop-universal", "pl24-shop-tools"],
withLogin: true,
};
try {
const response = await fetch(
`${this.baseUrl}${PL24_ENDPOINTS.AUTHORIZE}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${mainToken}`,
Cookie: this.tokenData?.sessionCookie || "",
},
body: JSON.stringify(authorizeRequest),
signal: AbortSignal.timeout(this.timeout),
},
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = (await response.json()) as PL24AuthorizeResponse;
const accessToken = data.access_token || data.token?.access_token;
if (!accessToken) {
throw new Error("No service token in response");
}
const payload = this.decodeJWT(accessToken);
this.serviceTokens.set(serviceName, {
token: accessToken,
expiresAt: new Date(payload.exp * 1000),
});
this.logger.log(`Service ${serviceName} authorized successfully`);
return accessToken;
} catch (error) {
const err = error as Error;
this.logger.error(`Service authorization error: ${err.message}`);
throw new UnauthorizedException(
`Servis yetkilendirme hatasi: ${err.message}`,
);
}
}
async getAccessToken(): Promise<string> {
if (!this.tokenData || !this.isTokenValid(this.tokenData)) {
await this.login();
}
return this.tokenData!.accessToken;
}
async getSessionCookie(): Promise<string> {
if (!this.tokenData || !this.isTokenValid(this.tokenData)) {
await this.login();
}
return this.tokenData!.sessionCookie;
}
getAvailableServices(): string[] {
return this.tokenData?.services || [];
}
hasService(serviceName: string): boolean {
return this.tokenData?.services.includes(serviceName) || false;
}
clearTokens(): void {
this.tokenData = null;
this.serviceTokens.clear();
this.logger.log("All PL24 tokens cleared");
}
/**
* Build authorization headers for API requests.
*/
async buildAuthHeaders(
serviceName?: string,
includeContentType = false,
): Promise<Record<string, string>> {
const token = serviceName
? await this.authorizeService(serviceName)
: await this.getAccessToken();
const sessionCookie = await this.getSessionCookie();
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
Cookie: sessionCookie,
Accept: "application/json",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
};
if (includeContentType) {
headers["Content-Type"] = "application/json";
}
return headers;
}
private isTokenValid(token: PL24TokenData): boolean {
const bufferMs = 60 * 1000;
return token.expiresAt.getTime() - bufferMs > Date.now();
}
private decodeJWT(token: string): PL24JWTPayload {
try {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format");
}
const payload = Buffer.from(parts[1], "base64").toString("utf-8");
return JSON.parse(payload);
} catch {
this.logger.error("Failed to decode JWT");
throw new Error("Invalid JWT token");
}
}
private extractSessionCookie(setCookie: string | null): string {
if (!setCookie) return "";
const match = setCookie.match(/PL24TOKEN=([^;]+)/);
if (match) {
return `PL24TOKEN=${match[1]}`;
}
return setCookie.split(";")[0];
}
}

View File

@@ -4,3 +4,15 @@ export const PL24_DEFAULTS = {
REQUEST_TIMEOUT: 30000,
MAX_RETRIES: 3,
} as const;
export const PL24_ENDPOINTS = {
// Auth
LOGIN: "/pl24-appgtw/ext/api/1.0/login",
AUTHORIZE: "/auth/ext/api/1.1/authorize",
// Catalog
MANUFACTURERS: "/pl24-manufacturer/ext/api/1.0/manufacturers/",
// Image server
IMAGESERVER: "/imageserver/ext/api/images",
} as const;

View File

@@ -4,6 +4,6 @@ import { PL24AuthService } from "./pl24-auth.service";
@Module({
providers: [PL24Service, PL24AuthService],
exports: [PL24Service],
exports: [PL24Service, PL24AuthService],
})
export class PL24Module {}

File diff suppressed because it is too large Load Diff

View File

@@ -1,74 +1,602 @@
export interface PL24AuthResponse {
token: string;
expiresIn: number;
/**
* PartsLink24 (PL24) API Types
*
* Type definitions for partslink24.com VIN integration.
* PL24 uses JWT authentication and provides P5 architecture for modern catalogs.
*/
// ==================== AUTH TYPES ====================
export interface PL24LoginRequest {
authentication: {
account: string;
user: string;
pwd: string;
};
device: {
id: string;
os: string;
offset: string;
lang: string;
"os-version": string;
};
"app-version": string;
squeezeOut: boolean;
}
export interface PL24VehicleResponse {
export interface PL24LoginResponse {
status:
| "OK"
| "USER_ALREADY_LOGGED_IN"
| "INVALID_CREDENTIALS"
| "ERROR"
| null;
message?: string;
token?: {
access_token: string;
token_type: string;
expires_in: number;
scope: string;
};
refreshToken?: string;
securables?: unknown;
title?: string | null;
}
export interface PL24JWTPayload {
iat: number;
exp: number;
sid: string;
aid: number;
uid: number;
services: string[];
licid: number;
app: string;
type: string;
country: string;
ulo: string;
alo: string;
}
export interface PL24TokenData {
accessToken: string;
refreshToken: string;
sessionCookie: string;
expiresAt: Date;
services: string[];
}
export interface PL24AuthorizeRequest {
serviceNames: string[];
serviceCategoryNames: string[];
withLogin: boolean;
}
export interface PL24AuthorizeResponse {
access_token?: string;
token_type?: string;
expires_in?: number;
scope?: string;
session_status?: string;
lcSessionId?: string | null;
token?: {
access_token: string;
token_type: string;
expires_in: number;
};
}
// ==================== CATALOG TYPES ====================
export type PL24ApiArchitecture =
| "P5_MODERN"
| "LEGACY_PSA"
| "LEGACY_HYUNDAI_KIA"
| "LEGACY_KIA"
| "LEGACY_FORD"
| "LEGACY_NISSAN"
| "LEGACY_OPEL"
| "LEGACY_VOLVO";
export interface PL24CatalogConfig {
basePath: string;
apiPath: string;
architecture: PL24ApiArchitecture;
}
export const PL24_SERVICE_CATALOGS: Record<string, PL24CatalogConfig> = {
// ==================== P5 MODERN ARCHITECTURE ====================
// Volkswagen Group
vw_parts: {
basePath: "/pl24-app/vw_parts",
apiPath: "/p5vwag",
architecture: "P5_MODERN",
},
vwclassic_parts: {
basePath: "/pl24-app/vwclassic_parts",
apiPath: "/p5vwag",
architecture: "P5_MODERN",
},
vn_parts: {
basePath: "/pl24-app/vn_parts",
apiPath: "/p5vwag",
architecture: "P5_MODERN",
},
audi_parts: {
basePath: "/pl24-app/audi_parts",
apiPath: "/p5vwag",
architecture: "P5_MODERN",
},
skoda_parts: {
basePath: "/pl24-app/skoda_parts",
apiPath: "/p5vwag",
architecture: "P5_MODERN",
},
seat_parts: {
basePath: "/pl24-app/seat_parts",
apiPath: "/p5vwag",
architecture: "P5_MODERN",
},
cupra_parts: {
basePath: "/pl24-app/cupra_parts",
apiPath: "/p5vwag",
architecture: "P5_MODERN",
},
bentley_parts: {
basePath: "/pl24-app/bentley_parts",
apiPath: "/p5vwag",
architecture: "P5_MODERN",
},
// BMW Group
bmw_parts: {
basePath: "/pl24-app/bmw_parts",
apiPath: "/p5bmw",
architecture: "P5_MODERN",
},
bmwclassic_parts: {
basePath: "/pl24-app/bmwclassic_parts",
apiPath: "/p5bmw",
architecture: "P5_MODERN",
},
bmwmotorrad_parts: {
basePath: "/pl24-app/bmwmotorrad_parts",
apiPath: "/p5bmw",
architecture: "P5_MODERN",
},
bmwmotorradclassic_parts: {
basePath: "/pl24-app/bmwmotorradclassic_parts",
apiPath: "/p5bmw",
architecture: "P5_MODERN",
},
mini_parts: {
basePath: "/pl24-app/mini_parts",
apiPath: "/p5bmw",
architecture: "P5_MODERN",
},
miniclassic_parts: {
basePath: "/pl24-app/miniclassic_parts",
apiPath: "/p5bmw",
architecture: "P5_MODERN",
},
// Mercedes Group
mercedes_parts: {
basePath: "/pl24-app/mercedes_parts",
apiPath: "/p5daimler",
architecture: "P5_MODERN",
},
mercedesclassic_parts: {
basePath: "/p5/latest",
apiPath: "/p5daimler",
architecture: "P5_MODERN",
},
mercedesvans_parts: {
basePath: "/pl24-app/mercedesvans_parts",
apiPath: "/p5daimler",
architecture: "P5_MODERN",
},
mercedestrucks_parts: {
basePath: "/pl24-app/mercedestrucks_parts",
apiPath: "/p5daimler",
architecture: "P5_MODERN",
},
mercedesunimog_parts: {
basePath: "/pl24-app/mercedesunimog_parts",
apiPath: "/p5daimler",
architecture: "P5_MODERN",
},
smart_parts: {
basePath: "/pl24-app/smart_parts",
apiPath: "/p5daimler",
architecture: "P5_MODERN",
},
// Porsche
porsche_parts: {
basePath: "/pl24-app/porsche_parts",
apiPath: "/p5porsche",
architecture: "P5_MODERN",
},
porscheclassic_parts: {
basePath: "/pl24-app/porscheclassic_parts",
apiPath: "/p5porsche",
architecture: "P5_MODERN",
},
// Toyota/Lexus
toyota_parts: {
basePath: "/pl24-app/toyota_parts",
apiPath: "/p5toyota",
architecture: "P5_MODERN",
},
lexus_parts: {
basePath: "/pl24-app/lexus_parts",
apiPath: "/p5toyota",
architecture: "P5_MODERN",
},
// Renault Group
renault_parts: {
basePath: "/pl24-app/renault_parts",
apiPath: "/p5renault",
architecture: "P5_MODERN",
},
dacia_parts: {
basePath: "/pl24-app/dacia_parts",
apiPath: "/p5renault",
architecture: "P5_MODERN",
},
alpine_parts: {
basePath: "/pl24-app/alpine_parts",
apiPath: "/p5renault",
architecture: "P5_MODERN",
},
// Jaguar Land Rover
jaguar_parts: {
basePath: "/pl24-app/jaguar_parts",
apiPath: "/p5jlr",
architecture: "P5_MODERN",
},
landrover_parts: {
basePath: "/pl24-app/landrover_parts",
apiPath: "/p5jlr",
architecture: "P5_MODERN",
},
// MAN
man_parts: {
basePath: "/pl24-app/man_parts",
apiPath: "/p5man",
architecture: "P5_MODERN",
},
// Mitsubishi
mmc_parts: {
basePath: "/pl24-app/mmc_parts",
apiPath: "/p5mmc",
architecture: "P5_MODERN",
},
// Suzuki
suzuki_parts: {
basePath: "/pl24-app/suzuki_parts",
apiPath: "/p5suzuki",
architecture: "P5_MODERN",
},
};
// ==================== HELPER FUNCTIONS ====================
export function getServiceApiPath(serviceName: string): string {
const config = PL24_SERVICE_CATALOGS[serviceName];
return config?.apiPath || "/p5vwag";
}
export function getServiceConfig(
serviceName: string,
): PL24CatalogConfig | null {
return PL24_SERVICE_CATALOGS[serviceName] || null;
}
export function isP5Modern(serviceName: string): boolean {
const config = PL24_SERVICE_CATALOGS[serviceName];
return config?.architecture === "P5_MODERN";
}
export function isLegacyArchitecture(serviceName: string): boolean {
const config = PL24_SERVICE_CATALOGS[serviceName];
if (!config) return false;
return config.architecture !== "P5_MODERN";
}
// ==================== WMI MAP ====================
export const PL24_WMI_SERVICE_MAP: Record<string, string> = {
// Volkswagen
WVW: "vw_parts",
WVG: "vw_parts",
"1VW": "vw_parts",
"3VW": "vw_parts",
"9BW": "vw_parts",
// Audi
WAU: "audi_parts",
TRU: "audi_parts",
"93U": "audi_parts",
// Skoda
TMB: "skoda_parts",
TMP: "skoda_parts",
// SEAT / Cupra
VSS: "seat_parts",
VSE: "seat_parts",
// Bentley
SCB: "bentley_parts",
// BMW
WBA: "bmw_parts",
WBS: "bmw_parts",
WBY: "bmw_parts",
WBX: "bmw_parts",
// BMW Motorrad
WB1: "bmwmotorrad_parts",
// MINI
WMW: "mini_parts",
// Mercedes-Benz
WDB: "mercedes_parts",
WDD: "mercedes_parts",
WDC: "mercedes_parts",
W1K: "mercedes_parts",
W1N: "mercedes_parts",
// Mercedes Vans
WDF: "mercedesvans_parts",
WD3: "mercedesvans_parts",
WD4: "mercedesvans_parts",
// smart
WME: "smart_parts",
TRD: "smart_parts",
// Porsche
WP0: "porsche_parts",
WP1: "porsche_parts",
// Toyota
JTD: "toyota_parts",
JTE: "toyota_parts",
JTN: "toyota_parts",
JTM: "toyota_parts",
SB1: "toyota_parts",
"1NX": "toyota_parts",
"2T1": "toyota_parts",
"4T1": "toyota_parts",
"5TD": "toyota_parts",
"5TF": "toyota_parts",
NMT: "toyota_parts",
MR0: "toyota_parts",
// Lexus
JTH: "lexus_parts",
JTJ: "lexus_parts",
"2T2": "lexus_parts",
// Renault
VF1: "renault_parts",
VF6: "renault_parts",
VNE: "renault_parts",
// Dacia
UU1: "dacia_parts",
UU6: "dacia_parts",
// Alpine
VFA: "alpine_parts",
// Jaguar
SAJ: "jaguar_parts",
// Land Rover
SAL: "landrover_parts",
// MAN
WMA: "man_parts",
WMH: "man_parts",
// Mitsubishi
JMB: "mmc_parts",
JMY: "mmc_parts",
MMB: "mmc_parts",
ML3: "mmc_parts",
// Suzuki
JS2: "suzuki_parts",
JS3: "suzuki_parts",
TSM: "suzuki_parts",
MA3: "suzuki_parts",
MBH: "suzuki_parts",
};
// ==================== VEHICLE TYPES ====================
export interface PL24CatalogInfo {
serviceName: string;
vehicleId: string;
catalogId: string;
name: string;
modelCode: string;
engine: string;
transmission: string;
bodyType: string;
market: string;
yearFrom: number;
yearTo: number;
raw: Record<string, unknown>;
catalogPath: string;
baseUrl: string;
mainGroupsPath?: string;
}
export interface PL24CategoryResponse {
groupId: string;
export interface PL24MainGroup {
id: string;
code: string;
name: string;
parentGroupId: string | null;
sortOrder: number;
hasSchemaPic: boolean;
description?: string;
iconUrl?: string;
subGroups?: PL24SubGroup[];
linkPath?: string;
linkWid?: string;
}
export interface PL24PartResponse {
partId: string;
export interface PL24SubGroup {
id: string;
code: string;
name: string;
description: string;
quantity: number;
position: string;
hotspotIndex: number | null;
oemCodes: string[];
description?: string;
imageUrl?: string;
partCount?: number;
}
export interface PL24SchemaPicResponse {
imageUrl: string;
hotspots: PL24Hotspot[];
export interface PL24Part {
id: string;
oemCode: string;
formattedPartNo?: string;
name: string;
description?: string;
remark?: string;
quantity?: number;
positionCode?: string;
modelCodes?: string;
notes?: string;
superseded?: {
oldCode: string;
newCode: string;
};
restrictions?: string[];
additionalInfo?: Record<string, string>;
hotspotId?: string;
linkPath?: string;
}
export interface PL24HotspotArea {
left: number;
top: number;
width: number;
height: number;
descr?: string | null;
}
export interface PL24Hotspot {
index: number;
x: number;
y: number;
width: number;
height: number;
shape: "rect" | "circle" | "polygon";
points?: { x: number; y: number }[];
key: string;
areas: PL24HotspotArea[];
hotspotKeyLinks?: unknown[];
masks?: unknown[];
}
export interface ParsedVehicle {
vehicleId: string;
catalogId: string;
name: string;
modelCode: string;
engine: string;
transmission: string;
bodyType: string;
market: string;
yearFrom: number;
yearTo: number;
}
export interface ParsedCategory {
export interface PL24PartsResponse {
success: boolean;
groupId: string;
name: string;
parentGroupId: string | null;
sortOrder: number;
hasSchemaPic: boolean;
groupName: string;
schemaImageUrl?: string;
schemaWidth?: number;
schemaHeight?: number;
parts: PL24Part[];
hotspots?: PL24Hotspot[];
}
export interface PL24ImageResponse {
originalHeight: number;
originalWidth: number;
scaledHeight: number;
scaledWidth: number;
image: string;
hotspots: PL24Hotspot[];
}
// ==================== STANDARDIZED OUTPUT ====================
export interface PL24DecodedVehicle {
brand: string;
model: string;
year: number;
series: string | null;
bodyType: string | null;
engineCode: string | null;
engineType: string | null;
engineVolume: string | null;
transmission: string | null;
driveType: string | null;
colorCode: string | null;
productionDate?: string | null;
raw: Record<string, unknown>;
catalogInfo: PL24CatalogInfo | null;
categories: PL24DecodedCategory[];
}
export interface PL24DecodedCategory {
code: string;
nameEn: string;
nameTr?: string;
description: string | null;
iconUrl: string | null;
subGroups: PL24DecodedSubGroup[];
linkPath?: string;
linkWid?: string;
}
export interface PL24DecodedSubGroup {
code: string;
nameEn: string;
nameTr?: string;
description: string | null;
schemaImageUrl: string | null;
partCount: number;
}
export interface PL24DecodedPart {
oemCode: string;
alternativeOems?: string[];
nameEn: string;
nameTr?: string;
description: string | null;
positionCode?: string;
positionX?: number;
positionY?: number;
quantity?: number;
notes?: string;
}
// ==================== BRAND MAP ====================
export const SERVICE_TO_BRAND: Record<string, string> = {
vw_parts: "Volkswagen",
vwclassic_parts: "Volkswagen",
vn_parts: "Volkswagen",
audi_parts: "Audi",
seat_parts: "SEAT",
cupra_parts: "Cupra",
skoda_parts: "Skoda",
bentley_parts: "Bentley",
bmw_parts: "BMW",
bmwclassic_parts: "BMW",
bmwmotorrad_parts: "BMW",
bmwmotorradclassic_parts: "BMW",
mini_parts: "MINI",
miniclassic_parts: "MINI",
mercedes_parts: "Mercedes-Benz",
mercedesclassic_parts: "Mercedes-Benz",
mercedesvans_parts: "Mercedes-Benz",
mercedestrucks_parts: "Mercedes-Benz",
mercedesunimog_parts: "Mercedes-Benz",
smart_parts: "smart",
porsche_parts: "Porsche",
porscheclassic_parts: "Porsche",
toyota_parts: "Toyota",
lexus_parts: "Lexus",
renault_parts: "Renault",
dacia_parts: "Dacia",
alpine_parts: "Alpine",
jaguar_parts: "Jaguar",
landrover_parts: "Land Rover",
man_parts: "MAN",
mmc_parts: "Mitsubishi",
suzuki_parts: "Suzuki",
};