fix(pl24): Volvo + Ford + Opel/Vauxhall + Hyundai/Kia VIN decode (per-brand services) #100

Merged
root merged 3 commits from dev into main 2026-06-04 14:03:07 +03:00
11 changed files with 553 additions and 21 deletions

View File

@@ -25,6 +25,27 @@ import {
getServiceConfig,
} from "./pl24.types";
/**
* Per-brand vehicle-info parser hook (shared-core + thin-brand-service pattern).
* A thin PL24<Brand>Service supplies its own vehicle-info parser (e.g. Volvo reads the
* `vinInfoTable` caption/value rows that the generic Ford-shaped parser misses); the
* shared P4 engine below uses it, and its fields win over the generic extraction.
*/
export interface P4VehicleInfo {
model?: string;
year?: number;
series?: string | null;
bodyType?: string | null;
engineCode?: string | null;
engineType?: string | null;
transmission?: string | null;
colorCode?: string | null;
productionDate?: string | null;
}
export interface P4BrandHooks {
parseVehicleInfo?(html: string, vin: string): P4VehicleInfo | null;
}
@Injectable()
export class PL24FordLegacyService {
private readonly logger = new Logger(PL24FordLegacyService.name);
@@ -58,6 +79,7 @@ export class PL24FordLegacyService {
vin: string,
serviceName: string,
userId?: string,
brandHooks?: P4BrandHooks,
): Promise<PL24DecodedVehicle | null> {
const cacheKey = `${PL24_DEFAULTS.CACHE_PREFIX}vehicle:${vin}`;
const cached = await this.redis.getJson<PL24DecodedVehicle>(cacheKey);
@@ -100,10 +122,10 @@ export class PL24FordLegacyService {
this.logger.warn(`P4 legacy: still demo after retry for ${serviceName}`);
return null;
}
return this.parseAndCacheVehicle(retryHtml, vin, serviceName, cacheKey);
return this.parseAndCacheVehicle(retryHtml, vin, serviceName, cacheKey, brandHooks);
}
return this.parseAndCacheVehicle(html, vin, serviceName, cacheKey);
return this.parseAndCacheVehicle(html, vin, serviceName, cacheKey, brandHooks);
} catch (error) {
const err = error as Error;
this.logger.error(`P4 legacy VIN decode error (${serviceName}): ${err.message}`, err.stack);
@@ -3862,8 +3884,9 @@ export class PL24FordLegacyService {
vin: string,
serviceName: string,
cacheKey: string,
brandHooks?: P4BrandHooks,
): Promise<PL24DecodedVehicle | null> {
const vehicle = this.parseP4VehicleResponse(html, vin, serviceName);
const vehicle = this.parseP4VehicleResponse(html, vin, serviceName, brandHooks);
if (!vehicle) return null;
// For P4 services that expose a VIN-based catalog (Ford P/T, Volvo legacy),
@@ -3972,7 +3995,12 @@ export class PL24FordLegacyService {
html: string,
vin: string,
serviceName = "fordt_parts",
brandHooks?: P4BrandHooks,
): Omit<PL24DecodedVehicle, "categories"> | null {
// Brand-specific vehicle-info parser wins (e.g. Volvo's vinInfoTable, which the
// generic Ford-shaped extraction below misses); generic only fills the gaps.
const brandInfo = brandHooks?.parseVehicleInfo?.(html, vin) ?? null;
// Try extracting vehicle data from embedded JS
const vehicles = this.extractScriptVariable<Array<Record<string, string>>>(html, "vehicles");
const vehicleData = vehicles?.[0] || null;
@@ -3980,19 +4008,19 @@ export class PL24FordLegacyService {
// Try extracting from vehicle info table
const tableRows = this.extractTableRows(html);
// Build vehicle info from whatever we found
let model = "";
let year = 0;
let bodyType: string | null = null;
let engineCode: string | null = null;
let engineType: string | null = null;
// Build vehicle info from whatever we found (brand values take precedence)
let model = brandInfo?.model || "";
let year = brandInfo?.year || 0;
let bodyType: string | null = brandInfo?.bodyType ?? null;
let engineCode: string | null = brandInfo?.engineCode ?? null;
let engineType: string | null = brandInfo?.engineType ?? null;
if (vehicleData) {
model = vehicleData.model || vehicleData.modelName || vehicleData.description || "";
year = Number.parseInt(vehicleData.year || vehicleData.modelYear || "", 10) || 0;
bodyType = vehicleData.bodyStyle || vehicleData.body || null;
engineCode = vehicleData.engineCode || vehicleData.engine || null;
engineType = vehicleData.engineDescription || vehicleData.engineType || null;
model = model || vehicleData.model || vehicleData.modelName || vehicleData.description || "";
year = year || Number.parseInt(vehicleData.year || vehicleData.modelYear || "", 10) || 0;
bodyType = bodyType || vehicleData.bodyStyle || vehicleData.body || null;
engineCode = engineCode || vehicleData.engineCode || vehicleData.engine || null;
engineType = engineType || vehicleData.engineDescription || vehicleData.engineType || null;
}
// Try to extract from page title or description
@@ -4059,16 +4087,16 @@ export class PL24FordLegacyService {
brand: SERVICE_TO_BRAND[serviceName] || serviceName.replace(/_parts$/, ""),
model,
year,
series: null,
series: brandInfo?.series ?? null,
bodyType,
engineCode,
engineType,
engineVolume: null,
transmission: null,
transmission: brandInfo?.transmission ?? null,
driveType: null,
colorCode: null,
productionDate: null,
raw: { html_length: html.length, has_vehicles_var: !!vehicleData },
colorCode: brandInfo?.colorCode ?? null,
productionDate: brandInfo?.productionDate ?? null,
raw: { html_length: html.length, has_vehicles_var: !!vehicleData, fiResolved: !!brandInfo?.model },
catalogInfo: {
serviceName,
vehicleId: vin,

View File

@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { PL24FordService } from "./pl24-ford.service";
const svc = new PL24FordService({} as never);
const p = svc as unknown as {
parseFordVinInfo(
html: string,
vin: string,
): {
model?: string;
year?: number;
transmission?: string | null;
engineType?: string | null;
bodyType?: string | null;
productionDate?: string | null;
} | null;
};
describe("PL24FordService.parseFordVinInfo", () => {
// Real Ford vin-group info grid (the clean data the generic title-parser polluted with
// "Ford Nutzfahrzeuge {VIN}: Transit Connect - TC7 …").
const html = `<table><tbody>
<tr><td class="caption">Sasi numarasi</td><td>NM0GXXTTPG6J66399</td></tr>
<tr><td class="caption">Üretim tarihi</td><td>05.05.2006</td></tr>
<tr><td class="caption">Araç Hattı</td><td>Transit Connect 2002-</td></tr>
<tr><td class="caption">Motor Tipi</td><td>1.8L Duratorq TC (75PS) - Lynx</td></tr>
<tr><td class="caption">Vites Kutusu</td><td>5 Vitesli Düz Vites Kutusu MTX75</td></tr>
<tr><td class="caption">Gövde Tarzı</td><td>4 Kapı Sedan</td></tr>
</tbody></table>`;
it("reads model/build-year/transmission/engine from the Ford info grid", () => {
const r = p.parseFordVinInfo(html, "NM0GXXTTPG6J66399");
expect(r?.model).toBe("Transit Connect 2002-");
expect(r?.year).toBe(2006); // build date, NOT the VIN position-10 char (which gave 2016)
expect(r?.transmission).toBe("5 Vitesli Düz Vites Kutusu MTX75");
expect(r?.engineType).toBe("1.8L Duratorq TC (75PS) - Lynx");
expect(r?.bodyType).toBe("4 Kapı Sedan");
expect(r?.productionDate).toBe("05.05.2006");
});
it("returns null when there is no Araç Hattı row (generic parser handles it)", () => {
expect(p.parseFordVinInfo("<table><tr><td>x</td></tr></table>", "x")).toBeNull();
});
});

View File

@@ -0,0 +1,76 @@
/**
* PL24 Ford (fordp/fordt — passenger + commercial) — thin brand service over the shared P4 engine.
*
* Ford's vin-group page carries the real vehicle data in a caption/value grid, but the generic
* parser grabbed the polluted page <title> ("Ford Nutzfahrzeuge {VIN}: Transit Connect - TC7…")
* → messy model + VIN-char year (wrong) + empty transmission. This service reads the grid:
* Araç Hattı → model ("Transit Connect 2002-"), Üretim tarihi → build year,
* Vites Kutusu → transmission, Motor Tipi → engine, Gövde Tarzı → body, Sürüm/Seriler → series.
* Categories/drill are unchanged (shared engine).
*/
import { Injectable } from "@nestjs/common";
import {
type P4BrandHooks,
type P4VehicleInfo,
PL24FordLegacyService,
} from "./pl24-ford-legacy.service";
import type { PL24DecodedVehicle } from "./pl24.types";
@Injectable()
export class PL24FordService {
constructor(private readonly core: PL24FordLegacyService) {}
private readonly hooks: P4BrandHooks = {
parseVehicleInfo: (html, vin) => this.parseFordVinInfo(html, vin),
};
decodeVinForService(
vin: string,
serviceName: string,
userId?: string,
): Promise<PL24DecodedVehicle | null> {
return this.core.decodeVinForService(vin, serviceName, userId, this.hooks);
}
private parseFordVinInfo(html: string, _vin: string): P4VehicleInfo | null {
const map: Record<string, string> = {};
for (const m of html.matchAll(
/<td[^>]*class="caption"[^>]*>([^<]*)<\/td>\s*<td[^>]*>([^<]*)<\/td>/g,
)) {
const k = m[1]
.replace(/&[^;]+;/g, " ")
.replace(/\s+/g, " ")
.trim()
.toLocaleLowerCase("tr");
const v = m[2]
.replace(/&[^;]+;/g, " ")
.replace(/\s+/g, " ")
.trim();
if (k && v && !(k in map)) map[k] = v;
}
const get = (...keys: string[]): string | null => {
for (const k of keys) {
const v = map[k.toLocaleLowerCase("tr")];
if (v) return v;
}
return null;
};
const model = get("araç hattı", "arac hatti", "vehicle line");
if (!model) return null; // no Ford info grid → let the generic parser try
const prod = get("üretim tarihi", "uretim tarihi", "build date");
const year = prod
? Number.parseInt(prod.match(/(19|20)\d{2}/)?.[0] || "", 10) || undefined
: undefined;
return {
model,
year,
engineType: get("motor tipi", "engine"),
transmission: get("vites kutusu", "transmission"),
bodyType: get("gövde tarzı", "govde tarzi", "body style"),
series: get("sürüm", "surum", "seriler") || null,
productionDate: prod,
};
}
}

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { PL24HyundaiKiaService } from "./pl24-hyundai-kia.service";
const svc = new PL24HyundaiKiaService({} as never);
const p = svc as unknown as {
parseHyundaiKiaVinInfo(
html: string,
vin: string,
): {
model?: string;
year?: number;
transmission?: string | null;
bodyType?: string | null;
} | null;
};
describe("PL24HyundaiKiaService.parseHyundaiKiaVinInfo", () => {
const html = `<title>Hyundai KMHBU51HP5U295422: GETZ 02: -OCT.2006 - partslink24</title>
<table><tbody>
<tr><td class="caption">Sasi numarasi</td><td>KMHBU51HP5U295422</td></tr>
<tr><td class="caption">Üretim tarihi</td><td>14.Eyl.2004</td></tr>
<tr><td class="caption">BODY TYPE</td><td>5 DR WAGON</td></tr>
<tr><td class="caption">TRANSMISSION</td><td>5 SPEED MT 2WD</td></tr>
<tr><td class="caption">ENGINE CAPACITY</td><td>1300 CC</td></tr>
<tr><td class="caption">FUEL TYPE</td><td>MPI-SOHC</td></tr>
</tbody></table>`;
it("reads model from title + build-year/transmission from the English grid", () => {
const r = p.parseHyundaiKiaVinInfo(html, "KMHBU51HP5U295422");
expect(r?.model).toBe("GETZ 02");
expect(r?.year).toBe(2004);
expect(r?.transmission).toBe("5 SPEED MT 2WD"); // would be missed by tr-locale lowercase
expect(r?.bodyType).toBe("5 DR WAGON");
});
it("strips ' - partslink24' when the title has no date-range colon (Kia)", () => {
const r = p.parseHyundaiKiaVinInfo(
"<title>Kia KNAD6814BL6353828: RIO / STONIC 17 - partslink24</title>",
"KNAD6814BL6353828",
);
expect(r?.model).toBe("RIO / STONIC 17");
});
});

View File

@@ -0,0 +1,82 @@
/**
* PL24 Hyundai / Kia — thin brand service over the shared P4 engine.
*
* Friendly model lives in the <title> ("Hyundai {VIN}: GETZ 02: -OCT.2006 - partslink24"); the
* generic parser kept the VIN-prefixed breadcrumb ("{VIN}: GET"). The info grid uses English
* labels: Üretim tarihi (build date) / TRANSMISSION / BODY TYPE / ENGINE CAPACITY / FUEL TYPE.
*/
import { Injectable } from "@nestjs/common";
import {
type P4BrandHooks,
type P4VehicleInfo,
PL24FordLegacyService,
} from "./pl24-ford-legacy.service";
import type { PL24DecodedVehicle } from "./pl24.types";
@Injectable()
export class PL24HyundaiKiaService {
constructor(private readonly core: PL24FordLegacyService) {}
private readonly hooks: P4BrandHooks = {
parseVehicleInfo: (html, vin) => this.parseHyundaiKiaVinInfo(html, vin),
};
decodeVinForService(
vin: string,
serviceName: string,
userId?: string,
): Promise<PL24DecodedVehicle | null> {
return this.core.decodeVinForService(vin, serviceName, userId, this.hooks);
}
private parseHyundaiKiaVinInfo(html: string, vin: string): P4VehicleInfo | null {
// title: "Hyundai {VIN}: GETZ 02: -OCT.2006 - partslink24" → model = segment between the
// VIN colon and the next colon ("GETZ 02").
const title = (html.match(/<title>([^<]*)<\/title>/i)?.[1] || "")
.replace(/&[^;]+;/g, " ")
.replace(/\s*-\s*partslink24\s*$/i, "");
let model: string | null = null;
const afterVin = title.split(new RegExp(`${vin}\\s*:\\s*`, "i"))[1];
if (afterVin) model = afterVin.split(":")[0].replace(/\s+/g, " ").trim() || null;
if (!model) return null; // no model in title → let generic try
// Hyundai/Kia grid uses ENGLISH labels (TRANSMISSION/BODY TYPE/…); plain lowercase only —
// Turkish-locale lowercase would map "I"→dotless "ı" and break the match.
const map: Record<string, string> = {};
for (const m of html.matchAll(
/<td[^>]*class="caption"[^>]*>([^<]*)<\/td>\s*<td[^>]*>([^<]*)<\/td>/g,
)) {
const k = m[1]
.replace(/&[^;]+;/g, " ")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
const v = m[2]
.replace(/&[^;]+;/g, " ")
.replace(/\s+/g, " ")
.trim();
if (k && v && !(k in map)) map[k] = v;
}
const get = (...keys: string[]): string | null => {
for (const k of keys) {
const v = map[k.toLowerCase()];
if (v) return v;
}
return null;
};
const prod = get("üretim tarihi", "uretim tarihi", "build date");
const year = prod
? Number.parseInt(prod.match(/(19|20)\d{2}/)?.[0] || "", 10) || undefined
: undefined;
const cap = get("engine capacity");
const fuel = get("fuel type");
return {
model,
year,
engineType: [cap, fuel].filter(Boolean).join(" ") || null,
transmission: get("transmission"),
bodyType: get("body type"),
productionDate: prod,
};
}
}

View File

@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { PL24OpelService } from "./pl24-opel.service";
const svc = new PL24OpelService({} as never);
const p = svc as unknown as {
parseOpelVinInfo(
html: string,
vin: string,
): {
model?: string;
year?: number;
transmission?: string | null;
engineType?: string | null;
} | null;
};
describe("PL24OpelService.parseOpelVinInfo", () => {
const html = `<title>Opel W0LPD5EA6EG003869: P10 - ASTRA-J [2010-2020] - partslink24</title>
<table><tbody>
<tr><td class="caption">Sasi numarasi</td><td>W0LPD5EA6EG003869</td></tr>
<tr><td class="caption">Model yili</td><td>2014 (A)</td></tr>
<tr><td class="caption">Üretim tarihi</td><td>28.Ağu.2013</td></tr>
<tr><td class="caption">Motor tipi</td><td>A13DTE</td></tr>
<tr><td class="caption">Şanzıman kodu</td><td>MEM</td></tr>
<tr><td class="caption">Karoseri</td><td>D69</td></tr>
</tbody></table>`;
it("takes the friendly model from the title and year/transmission from the grid", () => {
const r = p.parseOpelVinInfo(html, "W0LPD5EA6EG003869");
expect(r?.model).toBe("ASTRA-J [2010-2020]");
expect(r?.year).toBe(2014);
expect(r?.transmission).toBe("MEM");
expect(r?.engineType).toBe("A13DTE");
});
it("returns null when the title has only a platform code (no ' - model')", () => {
expect(p.parseOpelVinInfo("<title>Opel W0L: H00 - partslink24</title>", "x")).toBeNull();
});
});

View File

@@ -0,0 +1,77 @@
/**
* PL24 Opel / Vauxhall — thin brand service over the shared P4 engine.
*
* Opel's friendly model name lives in the <title> ("Opel {VIN}: {code} - ASTRA-J [2010-2020] -
* partslink24"); the generic parser split on bare "-" and kept the platform code ("…: P10"). The
* vin-group info grid carries Model yili / Üretim tarihi / Motor tipi / Şanzıman kodu / Karoseri.
*/
import { Injectable } from "@nestjs/common";
import {
type P4BrandHooks,
type P4VehicleInfo,
PL24FordLegacyService,
} from "./pl24-ford-legacy.service";
import type { PL24DecodedVehicle } from "./pl24.types";
@Injectable()
export class PL24OpelService {
constructor(private readonly core: PL24FordLegacyService) {}
private readonly hooks: P4BrandHooks = {
parseVehicleInfo: (html, vin) => this.parseOpelVinInfo(html, vin),
};
decodeVinForService(
vin: string,
serviceName: string,
userId?: string,
): Promise<PL24DecodedVehicle | null> {
return this.core.decodeVinForService(vin, serviceName, userId, this.hooks);
}
private parseOpelVinInfo(html: string, _vin: string): P4VehicleInfo | null {
// model = the title segment after the first " - " (the platform code precedes it)
const title = (html.match(/<title>([^<]*)<\/title>/i)?.[1] || "")
.replace(/&[^;]+;/g, " ")
.replace(/\s*-\s*partslink24\s*$/i, "")
.trim();
const segs = title.split(/\s+-\s+/);
const model = segs.length > 1 ? segs.slice(1).join(" - ").replace(/\s+/g, " ").trim() : null;
if (!model) return null; // only a platform code / no model in title → let generic try
const map: Record<string, string> = {};
for (const m of html.matchAll(
/<td[^>]*class="caption"[^>]*>([^<]*)<\/td>\s*<td[^>]*>([^<]*)<\/td>/g,
)) {
const k = m[1]
.replace(/&[^;]+;/g, " ")
.replace(/\s+/g, " ")
.trim()
.toLocaleLowerCase("tr");
const v = m[2]
.replace(/&[^;]+;/g, " ")
.replace(/\s+/g, " ")
.trim();
if (k && v && !(k in map)) map[k] = v;
}
const get = (...keys: string[]): string | null => {
for (const k of keys) {
const v = map[k.toLocaleLowerCase("tr")];
if (v) return v;
}
return null;
};
const yearRaw = get("model yılı", "model yili", "üretim tarihi", "uretim tarihi");
const year = yearRaw
? Number.parseInt(yearRaw.match(/(19|20)\d{2}/)?.[0] || "", 10) || undefined
: undefined;
return {
model,
year,
engineType: get("motor tipi", "motor kodu"),
transmission: get("şanzıman kodu", "sanzıman kodu", "şanzıman tipi"),
bodyType: get("karoseri"),
productionDate: get("üretim tarihi", "uretim tarihi"),
};
}
}

View File

@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { PL24VolvoService } from "./pl24-volvo.service";
// parseVolvoVinInfo is the brand-specific fix; the shared engine is tested elsewhere.
const svc = new PL24VolvoService({} as never);
const p = svc as unknown as {
parseVolvoVinInfo(
html: string,
vin: string,
): { model?: string; year?: number; series?: string | null } | null;
};
describe("PL24VolvoService.parseVolvoVinInfo", () => {
// Real Volvo vin-group `vinInfoTable` shape (the model the Ford-shaped parser missed).
const html = `<table id="vinInfoTable"><tbody>
<tr><td class="caption">Sasi numarasi</td><td>YV1ZZA8VCM1066977</td></tr>
<tr><td class="caption">Model yili</td><td>2021</td></tr>
<tr><td class="caption">Model</td><td>V60 Cross Country (19-)</td></tr>
<tr><td class="caption">Türü</td><td>V60 CC II</td></tr>
<tr><td class="caption">Dis rengi</td><td>72700</td></tr>
</tbody></table>`;
it("extracts model, year, and type from the vinInfoTable caption/value rows", () => {
const r = p.parseVolvoVinInfo(html, "YV1ZZA8VCM1066977");
expect(r?.model).toBe("V60 Cross Country (19-)");
expect(r?.year).toBe(2021);
expect(r?.series).toBe("V60 CC II");
});
it("returns null when there is no Model row (so the generic parser still runs)", () => {
expect(p.parseVolvoVinInfo("<table><tr><td>x</td></tr></table>", "x")).toBeNull();
});
});

View File

@@ -0,0 +1,71 @@
/**
* PL24 Volvo / Polestar — thin brand service over the shared P4 engine (PL24FordLegacyService).
*
* Volvo's vin-group page ships model/year/type in a `<table id="vinInfoTable">` caption/value
* grid (e.g. Model = "V60 Cross Country (19-)", Model yili = 2021, Türü = "V60 CC II"). The
* generic Ford-shaped parser only looks at window.vehicles / <title> / <h1> — none present for
* Volvo (title is just "Volvo {VIN}") — so model fell back to "Volvo {VIN}". This service supplies
* a brand vehicle-info parser; the shared engine handles fetch/session/categories/drill unchanged.
*/
import { Injectable } from "@nestjs/common";
import {
type P4BrandHooks,
type P4VehicleInfo,
PL24FordLegacyService,
} from "./pl24-ford-legacy.service";
import type { PL24DecodedVehicle } from "./pl24.types";
@Injectable()
export class PL24VolvoService {
constructor(private readonly core: PL24FordLegacyService) {}
private readonly hooks: P4BrandHooks = {
parseVehicleInfo: (html, vin) => this.parseVolvoVinInfo(html, vin),
};
decodeVinForService(
vin: string,
serviceName: string,
userId?: string,
): Promise<PL24DecodedVehicle | null> {
return this.core.decodeVinForService(vin, serviceName, userId, this.hooks);
}
/** Parse the `vinInfoTable` caption/value rows that carry the real Volvo model/year/type. */
private parseVolvoVinInfo(html: string, _vin: string): P4VehicleInfo | null {
const map: Record<string, string> = {};
for (const m of html.matchAll(
/<td[^>]*class="caption"[^>]*>([^<]*)<\/td>\s*<td[^>]*>([^<]*)<\/td>/g,
)) {
const k = m[1]
.replace(/&[^;]+;/g, " ")
.replace(/\s+/g, " ")
.trim()
.toLocaleLowerCase("tr");
const v = m[2]
.replace(/&[^;]+;/g, " ")
.replace(/\s+/g, " ")
.trim();
if (k && v && !(k in map)) map[k] = v;
}
const get = (...keys: string[]): string | null => {
for (const k of keys) {
const v = map[k.toLocaleLowerCase("tr")];
if (v) return v;
}
return null;
};
const model = get("model");
if (!model) return null; // not a Volvo identification page → let the generic parser try
const yearRaw = get("model yılı", "model yili");
const year = yearRaw ? Number.parseInt(yearRaw, 10) || undefined : undefined;
return {
model,
year,
series: get("türü", "turu"), // e.g. "V60 CC II"
colorCode: get("dış rengi", "dis rengi"),
};
}
}

View File

@@ -1,11 +1,26 @@
import { Module } from "@nestjs/common";
import { PL24AuthService } from "./pl24-auth.service";
import { PL24FordLegacyService } from "./pl24-ford-legacy.service";
import { PL24FordService } from "./pl24-ford.service";
import { PL24HyundaiKiaService } from "./pl24-hyundai-kia.service";
import { PL24OpelService } from "./pl24-opel.service";
import { PL24PsaService } from "./pl24-psa.service";
import { PL24VolvoService } from "./pl24-volvo.service";
import { PL24Service } from "./pl24.service";
const PL24_PROVIDERS = [
PL24Service,
PL24AuthService,
PL24FordLegacyService,
PL24PsaService,
PL24VolvoService,
PL24FordService,
PL24OpelService,
PL24HyundaiKiaService,
];
@Module({
providers: [PL24Service, PL24AuthService, PL24FordLegacyService, PL24PsaService],
exports: [PL24Service, PL24AuthService, PL24FordLegacyService, PL24PsaService],
providers: PL24_PROVIDERS,
exports: PL24_PROVIDERS,
})
export class PL24Module {}

View File

@@ -20,7 +20,11 @@ import { RedisService } from "../../redis/redis.service";
import { StorageService } from "../../storage/storage.service";
import { PL24AuthService } from "./pl24-auth.service";
import { PL24FordLegacyService } from "./pl24-ford-legacy.service";
import { PL24FordService } from "./pl24-ford.service";
import { PL24HyundaiKiaService } from "./pl24-hyundai-kia.service";
import { PL24OpelService } from "./pl24-opel.service";
import { PL24PsaService } from "./pl24-psa.service";
import { PL24VolvoService } from "./pl24-volvo.service";
import { PL24_DEFAULTS } from "./pl24.constants";
import {
type PL24DecodedCategory,
@@ -48,6 +52,10 @@ export class PL24Service {
private readonly authService: PL24AuthService,
private readonly fordLegacyService: PL24FordLegacyService,
private readonly psaService: PL24PsaService,
private readonly volvoService: PL24VolvoService,
private readonly fordService: PL24FordService,
private readonly opelService: PL24OpelService,
private readonly hyundaiKiaService: PL24HyundaiKiaService,
private configService: ConfigService,
private redis: RedisService,
private storage: StorageService,
@@ -84,6 +92,22 @@ export class PL24Service {
if (getServiceConfig(serviceName)?.architecture === "LEGACY_PSA") {
return this.psaService.decodeVinForService(cleanVin, serviceName, userId);
}
// Volvo/Polestar: thin brand service supplies the vinInfoTable parser over the shared engine.
if (getServiceConfig(serviceName)?.architecture === "LEGACY_VOLVO") {
return this.volvoService.decodeVinForService(cleanVin, serviceName, userId);
}
// Ford (fordp/fordt): thin brand service reads the Ford info grid (model/year/transmission).
if (getServiceConfig(serviceName)?.architecture === "LEGACY_FORD") {
return this.fordService.decodeVinForService(cleanVin, serviceName, userId);
}
// Opel/Vauxhall: model from <title>, year/transmission/engine from the info grid.
if (getServiceConfig(serviceName)?.architecture === "LEGACY_OPEL") {
return this.opelService.decodeVinForService(cleanVin, serviceName, userId);
}
// Hyundai/Kia: model from <title>, build-year/transmission from the English-labelled grid.
if (getServiceConfig(serviceName)?.architecture === "LEGACY_HYUNDAI_KIA") {
return this.hyundaiKiaService.decodeVinForService(cleanVin, serviceName, userId);
}
if (isLegacyArchitecture(serviceName)) {
return this.fordLegacyService.decodeVinForService(cleanVin, serviceName, userId);
}