fix(pl24): correct Ford VIN decode (model/build-year/transmission from info grid) via thin brand service
Ford decoded with a polluted model ("Ford Nutzfahrzeuge {VIN}: Transit Connect - TC7…", from the
page title), a wrong VIN-char year, and empty transmission. The clean data is in the vin-group info
grid (Araç Hattı=model line, Üretim tarihi=build date, Vites Kutusu=transmission, Motor Tipi=engine,
Gövde Tarzı=body). New thin PL24FordService supplies parseFordVinInfo via the shared P4 brand hook;
orchestrator routes LEGACY_FORD to it. Engine/categories/drill unchanged.
Verified live vs 9 prod Ford VINs: 7 now decode clean model + correct build-year + transmission
(Transit Connect/2006, Mondeo/1997 CD4E Otomatik, Focus/2000, …) — VIN-char years were off by up to a
decade and transmission was empty for all. 2 VINs are upstream gaps (no Araç Hattı → unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
44
apps/api/src/integrations/pl24/pl24-ford.service.spec.ts
Normal file
44
apps/api/src/integrations/pl24/pl24-ford.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
76
apps/api/src/integrations/pl24/pl24-ford.service.ts
Normal file
76
apps/api/src/integrations/pl24/pl24-ford.service.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
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 { 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,
|
||||
];
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
PL24Service,
|
||||
PL24AuthService,
|
||||
PL24FordLegacyService,
|
||||
PL24PsaService,
|
||||
PL24VolvoService,
|
||||
],
|
||||
exports: [PL24Service, PL24AuthService, PL24FordLegacyService, PL24PsaService, PL24VolvoService],
|
||||
providers: PL24_PROVIDERS,
|
||||
exports: PL24_PROVIDERS,
|
||||
})
|
||||
export class PL24Module {}
|
||||
|
||||
@@ -20,6 +20,7 @@ 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 { PL24PsaService } from "./pl24-psa.service";
|
||||
import { PL24VolvoService } from "./pl24-volvo.service";
|
||||
import { PL24_DEFAULTS } from "./pl24.constants";
|
||||
@@ -50,6 +51,7 @@ export class PL24Service {
|
||||
private readonly fordLegacyService: PL24FordLegacyService,
|
||||
private readonly psaService: PL24PsaService,
|
||||
private readonly volvoService: PL24VolvoService,
|
||||
private readonly fordService: PL24FordService,
|
||||
private configService: ConfigService,
|
||||
private redis: RedisService,
|
||||
private storage: StorageService,
|
||||
@@ -90,6 +92,10 @@ export class PL24Service {
|
||||
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);
|
||||
}
|
||||
if (isLegacyArchitecture(serviceName)) {
|
||||
return this.fordLegacyService.decodeVinForService(cleanVin, serviceName, userId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user