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:
2026-06-04 13:38:22 +03:00
parent 50fd0b0e26
commit 836b7c43a6
4 changed files with 138 additions and 8 deletions

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

@@ -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 {}

View File

@@ -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);
}