fix(pl24): P4 decode returns null on model-selection page (Hyundai/Kia/Nissan)
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
When PL24 can't identify a VIN, vin-group.action redirects to vehicle.action (a brand-wide model picker). The generic P4 parser collapsed that page's <title> to the brand name with a bogus VIN-char year, and doResolveVin's fastest-source-wins treated that truthy result as a definitive decode — masking the pcat/emex candidate fallback. parseP4VehicleResponse now returns null when the extracted model is just the brand (or contains the VIN), so the pipeline falls back to pcat/emex instead of storing a fake vehicle. Live discovery: 7/11 Hyundai/Kia/Nissan VINs hit the picker (newer 2024+ / Nissan EU model-pick catalog); the 4 PL24-indexed VINs (GETZ 02, RIO 15/17/20) still decode. Adds pl24-ford-legacy.service.spec.ts; biome-formats two pre-existing lines in the touched file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user