fix(pl24): P4 decode returns null on model-selection page (Hyundai/Kia/Nissan) #109

Merged
root merged 1 commits from dev into main 2026-06-05 02:37:44 +03:00
2 changed files with 92 additions and 6 deletions

View File

@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import { PL24FordLegacyService } from "./pl24-ford-legacy.service";
// configService.get(key, default) → default; the parser does not touch auth/redis/storage.
const configStub = { get: (_k: string, d?: unknown) => d } as never;
const svc = new PL24FordLegacyService(
{} as never, // authService
configStub, // configService
{} as never, // redis
{} as never, // storage
);
const p = svc as unknown as {
parseP4VehicleResponse(
html: string,
vin: string,
serviceName?: string,
brandHooks?: { parseVehicleInfo?: (html: string, vin: string) => unknown },
): { brand: string; model: string; year: number } | null;
};
// PL24 hands back a brand-wide model-selection catalog (vehicle.action) when it cannot
// identify the VIN. The generic extraction collapses such a page's <title> to the brand
// name ("Hyundai - partslink24" → "Hyundai"); that must NOT be treated as a decode.
const pickerHtml = (brand: string) =>
`<html><head><title>${brand} - partslink24</title></head><body><h2>Model seçimi</h2>${Array.from(
{ length: 12 },
(_, i) => `<a href="vehicle.action?ident=M${i}">MODEL ${i}</a>`,
).join("")}</body></html>`;
describe("PL24FordLegacyService.parseP4VehicleResponse — model-selection page guard", () => {
it("returns null for a Hyundai model-picker page (no fabricated brand-as-model)", () => {
const r = p.parseP4VehicleResponse(pickerHtml("Hyundai"), "KMHCF31FPRU061281", "hyundai_parts");
expect(r).toBeNull();
});
it("returns null for a Nissan model-picker page (generic path, no hook)", () => {
const r = p.parseP4VehicleResponse(pickerHtml("Nissan"), "SJNTAAJ12U1252571", "nissan_parts");
expect(r).toBeNull();
});
it("keeps a real decode when the brand hook resolves a model", () => {
const r = p.parseP4VehicleResponse(
"<html><head><title>Hyundai KMHBU51HP5U295422: GETZ 02: -OCT.2006 - partslink24</title></head><body></body></html>",
"KMHBU51HP5U295422",
"hyundai_parts",
{ parseVehicleInfo: () => ({ model: "GETZ 02", year: 2004, transmission: "AUTO" }) },
);
expect(r).not.toBeNull();
expect(r?.model).toBe("GETZ 02");
expect(r?.year).toBe(2004);
});
it("returns null when the model is just 'Brand {VIN}' (defensive — old Volvo-style fabrication)", () => {
const r = p.parseP4VehicleResponse(
"<html><head><title>Volvo - partslink24</title></head><body></body></html>",
"YV1RS494952480502",
"volvo_parts",
{ parseVehicleInfo: () => ({ model: "Volvo YV1RS494952480502" }) },
);
expect(r).toBeNull();
});
});

View File

@@ -2522,7 +2522,10 @@ export class PL24FordLegacyService {
? d.qty
: Number.parseFloat(String(d.qty ?? "1").replace(",", ".")) || 1;
const captionRaw = d.caption || row.caption;
const name = captionRaw.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
const name = captionRaw
.replace(/<[^>]+>/g, "")
.replace(/\s+/g, " ")
.trim();
parts.push({
id: `${row.pncHierCode}:${d.partno}`,
oemCode: d.partno,
@@ -2535,9 +2538,7 @@ export class PL24FordLegacyService {
});
}
} catch (e) {
this.logger.warn(
`FordVinBomParts pncHierCode=${row.pncHierCode}: ${(e as Error).message}`,
);
this.logger.warn(`FordVinBomParts pncHierCode=${row.pncHierCode}: ${(e as Error).message}`);
}
}
return parts;
@@ -4082,9 +4083,28 @@ export class PL24FordLegacyService {
model = "Ford";
}
// PL24 redirects vin-group.action → vehicle.action (a brand-wide model picker) when the
// VIN is not in its identification index. The generic extraction above then collapses the
// "model" to the brand name ("Hyundai"/"Kia"/"Nissan") with a bogus VIN-char year — which
// is NOT a decoded vehicle. Return null so doResolveVin falls back to pcat/emex candidates
// instead of masking them with a fake vehicle (fastest-source-wins treats any truthy PL24
// result as a definitive win). Confirmed via live discovery: 7/11 Hyundai/Kia/Nissan VINs
// hit this picker (newer 2024+ models / Nissan EU model-pick catalog).
const brand = SERVICE_TO_BRAND[serviceName] || serviceName.replace(/_parts$/, "");
const modelIsReal =
!!model &&
model.trim().toLowerCase() !== brand.toLowerCase() &&
!model.toUpperCase().includes(vin.toUpperCase());
if (!modelIsReal) {
this.logger.warn(
`P4 ${serviceName}: VIN ${vin} not identified by PL24 (model-selection page) → null (fallback to pcat/emex)`,
);
return null;
}
const config = getServiceConfig(serviceName);
return {
brand: SERVICE_TO_BRAND[serviceName] || serviceName.replace(/_parts$/, ""),
brand,
model,
year,
series: brandInfo?.series ?? null,
@@ -4096,7 +4116,11 @@ export class PL24FordLegacyService {
driveType: null,
colorCode: brandInfo?.colorCode ?? null,
productionDate: brandInfo?.productionDate ?? null,
raw: { html_length: html.length, has_vehicles_var: !!vehicleData, fiResolved: !!brandInfo?.model },
raw: {
html_length: html.length,
has_vehicles_var: !!vehicleData,
fiResolved: !!brandInfo?.model,
},
catalogInfo: {
serviceName,
vehicleId: vin,