fix(pl24): correct Opel/Vauxhall + Hyundai/Kia VIN decode via thin brand services
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Same shared-core + thin-brand pattern (P4BrandHooks). Both decoded with polluted models (Opel kept the platform code "…: P10"; Hyundai kept the VIN breadcrumb), wrong VIN-char years, empty transmission. Fix reads the vin-group info grid + title: - Opel: model from <title> (segment after the platform code), year from Model yili, transmission from Şanzıman kodu, engine from Motor tipi. Verified 6/6 prod (ASTRA-J/2014, CORSA-D/2011, INSIGNIA-A, AGILA-A…). - Hyundai/Kia: model from <title>, build-year from Üretim tarihi, transmission/body from the ENGLISH-labelled grid (plain lowercase — tr-locale would map I→ı and miss "TRANSMISSION"). Verified (GETZ 02/2004/5 SPEED MT, RIO / STONIC 17/2019…). Nissan deferred: both prod Nissan VINs are unresolvable upstream (PL24 returns "Model seçimi", no model) — nothing to parse. Engine/categories/drill unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
82
apps/api/src/integrations/pl24/pl24-hyundai-kia.service.ts
Normal file
82
apps/api/src/integrations/pl24/pl24-hyundai-kia.service.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
39
apps/api/src/integrations/pl24/pl24-opel.service.spec.ts
Normal file
39
apps/api/src/integrations/pl24/pl24-opel.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
77
apps/api/src/integrations/pl24/pl24-opel.service.ts
Normal file
77
apps/api/src/integrations/pl24/pl24-opel.service.ts
Normal 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"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ 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";
|
||||
@@ -13,6 +15,8 @@ const PL24_PROVIDERS = [
|
||||
PL24PsaService,
|
||||
PL24VolvoService,
|
||||
PL24FordService,
|
||||
PL24OpelService,
|
||||
PL24HyundaiKiaService,
|
||||
];
|
||||
|
||||
@Module({
|
||||
|
||||
@@ -21,6 +21,8 @@ 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";
|
||||
@@ -52,6 +54,8 @@ export class PL24Service {
|
||||
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,
|
||||
@@ -96,6 +100,14 @@ export class PL24Service {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user