diff --git a/apps/api/src/integrations/pl24/__fixtures__/p5_volvo_en.json b/apps/api/src/integrations/pl24/__fixtures__/p5_volvo_en.json new file mode 100644 index 0000000..08538d7 --- /dev/null +++ b/apps/api/src/integrations/pl24/__fixtures__/p5_volvo_en.json @@ -0,0 +1,198 @@ +{ + "link": { + "wid": "mainGroupTable", + "path": "/p5volvo/extern/groups/vin/mainGroup?lang=en&model=308&modelYear=1620&partnerGroup=46&serviceName=volvo_parts&upds=2026-08-24--11-02&vin=YV1AS84ABD1168166" + }, + "segments": { + "vinfoBasic": { + "records": [ + { + "values": { + "description": "Vehicle Identification No.", + "value": "YV1AS84ABD1168166" + } + }, + { + "values": { + "description": "Year", + "value": "2013" + } + }, + { + "values": { + "description": "Model", + "value": "S80 (07\\-)" + } + }, + { + "values": { + "description": "Km/h or mph", + "value": "K" + } + }, + { + "values": { + "description": "Factory code", + "value": "21" + } + }, + { + "values": { + "description": "Steering gear prod no", + "value": "31360538" + } + }, + { + "values": { + "description": "Structure week", + "value": "201236" + } + }, + { + "values": { + "description": "Type", + "value": "S80" + } + }, + { + "values": { + "description": "Chassis", + "value": "168166" + } + }, + { + "values": { + "description": "Partner group", + "value": "Europe" + } + }, + { + "values": { + "description": "Upholstery/interior code", + "value": "210100" + } + }, + { + "values": { + "description": "Upholstery/interior", + "value": "LEATHER/ANTHR/QRTZCEIL/NV" + } + }, + { + "values": { + "description": "Exterior color", + "value": "61400" + } + }, + { + "values": { + "description": "Exterior color", + "value": "WHITE SOLID ICE WHITE" + } + }, + { + "values": { + "description": "Body style code", + "value": "0" + } + }, + { + "values": { + "description": "Body style", + "value": "Sedan" + } + }, + { + "values": { + "description": "Special vehicle code", + "value": " " + } + }, + { + "values": { + "description": "Special vehicles", + "value": " " + } + }, + { + "values": { + "description": "Sales type", + "value": "42" + } + }, + { + "values": { + "description": "Sales type", + "value": "SALES VERSION 42" + } + }, + { + "values": { + "description": "Market code", + "value": "49" + } + }, + { + "values": { + "description": "Market", + "value": "TR" + } + }, + { + "values": { + "description": "Engine Code", + "value": "84" + } + }, + { + "values": { + "description": "Engine", + "value": "D4162T" + } + }, + { + "values": { + "description": "Engine part no", + "value": "6906309" + } + }, + { + "values": { + "description": "Engine serial no", + "value": "00000000000004138845 / 0ELD61 2208122233587" + } + }, + { + "values": { + "description": "Transmission Code", + "value": "B" + } + }, + { + "values": { + "description": "Transmission", + "value": "6\\-PSHIFT 2WD / MPS6" + } + }, + { + "values": { + "description": "Transmission part no", + "value": "1285041" + } + }, + { + "values": { + "description": "Transmission serial no", + "value": "00AWBB1 170812170654" + } + }, + { + "values": { + "description": "Chassis code", + "value": "35659B7A6276" + } + } + ] + } + } +} diff --git a/apps/api/src/integrations/pl24/__fixtures__/p5_volvo_tr.json b/apps/api/src/integrations/pl24/__fixtures__/p5_volvo_tr.json new file mode 100644 index 0000000..5221916 --- /dev/null +++ b/apps/api/src/integrations/pl24/__fixtures__/p5_volvo_tr.json @@ -0,0 +1,198 @@ +{ + "link": { + "wid": "mainGroupTable", + "path": "/p5volvo/extern/groups/vin/mainGroup?lang=tr&model=308&modelYear=1620&partnerGroup=46&serviceName=volvo_parts&upds=2026-08-24--11-02&vin=YV1AS84ABD1168166" + }, + "segments": { + "vinfoBasic": { + "records": [ + { + "values": { + "description": "Sasi numarasi", + "value": "YV1AS84ABD1168166" + } + }, + { + "values": { + "description": "Model yili", + "value": "2013" + } + }, + { + "values": { + "description": "Model", + "value": "S80 (07\\-)" + } + }, + { + "values": { + "description": "Hız Birimi", + "value": "K" + } + }, + { + "values": { + "description": "Fabrika Kodu", + "value": "21" + } + }, + { + "values": { + "description": "Direksiyon Kutusu Üretim No", + "value": "31360538" + } + }, + { + "values": { + "description": "Üretim Haftası", + "value": "201236" + } + }, + { + "values": { + "description": "Türü", + "value": "S80" + } + }, + { + "values": { + "description": "Şasi", + "value": "168166" + } + }, + { + "values": { + "description": "Ortak grubu", + "value": "Europe" + } + }, + { + "values": { + "description": "Döşeme/iç mekan kodu", + "value": "210100" + } + }, + { + "values": { + "description": "Döşeme", + "value": "LEATHER/ANTHR/QRTZCEIL/NV" + } + }, + { + "values": { + "description": "Dis rengi", + "value": "61400" + } + }, + { + "values": { + "description": "Dis rengi", + "value": "WHITE SOLID ICE WHITE" + } + }, + { + "values": { + "description": "Karoseri Tipi Kodu", + "value": "0" + } + }, + { + "values": { + "description": "Kaporta Stili", + "value": "Sedan" + } + }, + { + "values": { + "description": "Özel Araç Kodu", + "value": " " + } + }, + { + "values": { + "description": "Özel araçlar", + "value": " " + } + }, + { + "values": { + "description": "Satis tipi", + "value": "42" + } + }, + { + "values": { + "description": "Satis tipi", + "value": "SALES VERSION 42" + } + }, + { + "values": { + "description": "Piyasa Kodu", + "value": "49" + } + }, + { + "values": { + "description": "Market", + "value": "TR" + } + }, + { + "values": { + "description": "Motor kodu", + "value": "84" + } + }, + { + "values": { + "description": "Motor", + "value": "D4162T" + } + }, + { + "values": { + "description": "Motor Parça No", + "value": "6906309" + } + }, + { + "values": { + "description": "Motor Seri Numarası", + "value": "00000000000004138845 / 0ELD61 2208122233587" + } + }, + { + "values": { + "description": "Şanzıman kodu", + "value": "B" + } + }, + { + "values": { + "description": "Şanzıman", + "value": "6\\-PSHIFT 2WD / MPS6" + } + }, + { + "values": { + "description": "Şanzıman Parça No", + "value": "1285041" + } + }, + { + "values": { + "description": "Şanzıman Seri No", + "value": "00AWBB1 170812170654" + } + }, + { + "values": { + "description": "Şasi Kodu", + "value": "35659B7A6276" + } + } + ] + } + } +} diff --git a/apps/api/src/integrations/pl24/pl24-volvo-vinfo.spec.ts b/apps/api/src/integrations/pl24/pl24-volvo-vinfo.spec.ts new file mode 100644 index 0000000..9f577df --- /dev/null +++ b/apps/api/src/integrations/pl24/pl24-volvo-vinfo.spec.ts @@ -0,0 +1,68 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { PL24Service } from "./pl24.service"; + +/** + * Volvo P5 vinfoBasic regression lock (plv2.md, finding p5core-09). + * + * Volvo emits BOTH a bare internal code and a readable description for the same + * attribute — "Şanzıman kodu" = "B" next to "Şanzıman" = "6-PSHIFT 2WD / MPS6", + * and "Satis tipi" = "42" next to "Türü" = "S80". The key order the parser needs + * for VAG (whose "Şanzıman kodu" IS the useful value, e.g. "MQ200") therefore + * rendered a single letter as the gearbox and a bare number as the series. + * + * Fixtures are the real 2026-09-16 discovery captures for VIN + * YV1AS84ABD1168166 (S80), trimmed to the segments the parser reads, in both + * the Turkish and the English label locale. + */ + +const fixture = (name: string) => + JSON.parse(readFileSync(join(__dirname, "__fixtures__", `${name}.json`), "utf-8")); + +// parseVehicleResponse only touches the response and the service name. +const parse = (data: unknown, serviceName = "volvo_parts") => { + const svc = Object.create(PL24Service.prototype) as unknown as { + parseVehicleResponse: (v: string, d: unknown, s: string) => Record; + isDaimlerService: (s: string) => boolean; + }; + svc.isDaimlerService = () => false; + return svc.parseVehicleResponse("YV1AS84ABD1168166", data, serviceName); +}; + +describe("Volvo P5 vinfoBasic — kod yerine açıklama", () => { + it("Türkçe etiketlerde şanzımanı okunur değerinden alır", () => { + const out = parse(fixture("p5_volvo_tr")); + expect(out.transmission).toBe("6-PSHIFT 2WD / MPS6"); + // Tek harflik "Şanzıman kodu" artık kazanmıyor. + expect(out.transmission).not.toBe("B"); + }); + + it("İngilizce etiketlerde de aynı sonucu verir", () => { + const out = parse(fixture("p5_volvo_en")); + expect(out.transmission).toBe("6-PSHIFT 2WD / MPS6"); + }); + + it("seri, çıplak satış kodu yerine gerçek tipi döner", () => { + expect(parse(fixture("p5_volvo_tr")).series).toBe("S80"); + expect(parse(fixture("p5_volvo_en")).series).toBe("S80"); + expect(parse(fixture("p5_volvo_tr")).series).not.toBe("42"); + }); + + it("kaporta stili iki dilde de çözülür ve '0' kodu sızmaz", () => { + expect(parse(fixture("p5_volvo_tr")).bodyType).toBe("Sedan"); + expect(parse(fixture("p5_volvo_en")).bodyType).toBe("Sedan"); + }); + + it("motor alanları bozulmadan kalır", () => { + const out = parse(fixture("p5_volvo_tr")); + expect(out.engineCode).toBe("84"); + expect(out.engineType).toBe("D4162T"); + }); + + it("model ve yıl korunur", () => { + const out = parse(fixture("p5_volvo_tr")); + expect(out.model).toBe("S80 (07-)"); + expect(out.year).toBe(2013); + }); +}); diff --git a/apps/api/src/integrations/pl24/pl24.service.ts b/apps/api/src/integrations/pl24/pl24.service.ts index 24e4f06..3bf511d 100644 --- a/apps/api/src/integrations/pl24/pl24.service.ts +++ b/apps/api/src/integrations/pl24/pl24.service.ts @@ -1184,6 +1184,31 @@ export class PL24Service { return null; }; + /** + * Like `lookup`, but prefers a human-readable value over a bare internal + * code when the record carries both. + * + * Volvo's vinfoBasic has BOTH "Şanzıman kodu" ("B") and "Şanzıman" + * ("6-PSHIFT 2WD / MPS6"), and BOTH "Satis tipi" ("42") and a second + * "Satis tipi" ("SALES VERSION 42"). The code-first key order that VAG needs + * (its "Şanzıman kodu" IS the useful value, e.g. "MQ200") therefore rendered + * a single letter as the Volvo gearbox and a bare number as its series. + * + * Falls back to the first present value, so a backend that only ever emits + * codes behaves exactly as before. + */ + const lookupDescriptive = (...keys: string[]): string | null => { + let firstPresent: string | null = null; + for (const k of keys) { + const v = vehicleData[k]?.trim(); + if (!v) continue; + if (firstPresent === null) firstPresent = v; + // Two characters or fewer, or digits only → an index, not a description. + if (v.length > 2 && !/^\d+$/.test(v)) return v; + } + return firstPresent; + }; + // Extract prNr records for richer vehicle attributes const prNrRecords = segments.prNr?.records || []; const prNrByCode: Record = {}; @@ -1226,13 +1251,17 @@ export class PL24Service { // normalizeLabel folds ı→i and strips diacritics, so "Şanzıman kodu" and // "ŞANZIMAN KODU" both arrive as "sanziman_kodu". PSA uses "AKTARMA // SİSTEMLERİ" ("5 MEKANİK VİTES KUTUSU"), Subaru "Mission". - const transmissionCode = lookup( + const transmissionCode = lookupDescriptive( "sanziman_kodu", "transmission_code", "vites_kutusu", "atm,mtm", "aktarma_sistemleri", "sanziman", + // Volvo in the English locale: "Transmission code" ("B") plus the readable + // "Transmission" ("6-PSHIFT 2WD / MPS6"). Without the plain key the + // English response falls back to the single-letter code. + "transmission", "sanziman_numarasi", "mission", ); @@ -1242,7 +1271,10 @@ export class PL24Service { const bodyType = Object.entries(prNrByCode).find(([code]) => code.startsWith("K8"))?.[1] || // PSA "GÖVDE TİPİ" ("4 KAPILI SEDAN"), Volvo "Kaporta Stili" ("Sedan"). - lookup("karoseri", "body", "body_type", "govde_tipi", "kaporta_stili") || + // Volvo: TR "Kaporta Stili" / EN "Body style" ("Sedan"). Its + // "Karoseri Tipi Kodu" / "Body style code" is a bare "0" — never matched + // here because the lookup is exact-key, and that is deliberate. + lookup("karoseri", "body", "body_type", "govde_tipi", "kaporta_stili", "body_style") || null; // Engine description from prNr D3* (Motor nitelikleri) @@ -1301,7 +1333,10 @@ export class PL24Service { damToModelYear(lookup("dam")) || extractModelYear(vin) || 0, - series: lookup("seri", "satis_tipi", "sales_type", "turu"), + // Volvo's "Satis tipi" is the bare sales code ("42") with the readable + // form ("SALES VERSION 42") in a duplicate row, and its "Türü" is the real + // series ("S80") — so prefer whichever of these is actually descriptive. + series: lookupDescriptive("seri", "satis_tipi", "sales_type", "turu", "type"), bodyType, engineCode: engineCode ||