fix(pl24): correct PSA VIN decode via FI/VIN-indexed flow + cycle-correct model year
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
PSA (Peugeot/Citroën/DS) VIN decode was systemically broken: the catalog
vin-group page returns all families unfiltered, so decode fell back to the first
family/salesType (a manual base variant) — yielding "{Brand} {VIN}" model names,
empty transmission, wrong model year, and manual-only parts trees (automatic
gearbox parts missing). Reported for a 1999 Peugeot 106 automatic shown as a 2029
manual with no automatic parts.
- New self-contained PL24PsaService: consumes PL24's FI flow (vin.action →
hintstoken → FI page → json-vin-main-groups → json-vin-illustrations →
vin-image-board). Reads model/year/transmission from the FI identification
table; builds the VIN-indexed parts tree (correct per actual VIN). Does not
touch Ford/Volvo/Nissan/Opel/Hyundai-Kia/Fiat.
- Orchestrator + categories.service route PSA VIN decode/drill to the new service.
- Cycle-correct extractModelYear in @sase/shared (X→1999, not 2029): resolve the
30-yr VIN year code to the most-recent plausible year (≤ now+1); dedupe 6 copies.
Validated live against 13 already-decoded PSA VINs: 12/13 full trees with real
model/year/transmission; automatics correctly detected (106 BVA, 206 AL4, 3008 BVA8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,11 +14,13 @@ import {
|
||||
ServiceUnavailableException,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { extractModelYear } from "@sase/shared";
|
||||
import { isBackfillContext } from "../../jobs/prefetch-context";
|
||||
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 { PL24PsaService } from "./pl24-psa.service";
|
||||
import { PL24_DEFAULTS } from "./pl24.constants";
|
||||
import {
|
||||
type PL24DecodedCategory,
|
||||
@@ -45,6 +47,7 @@ export class PL24Service {
|
||||
constructor(
|
||||
private readonly authService: PL24AuthService,
|
||||
private readonly fordLegacyService: PL24FordLegacyService,
|
||||
private readonly psaService: PL24PsaService,
|
||||
private configService: ConfigService,
|
||||
private redis: RedisService,
|
||||
private storage: StorageService,
|
||||
@@ -77,6 +80,10 @@ export class PL24Service {
|
||||
|
||||
// Dispatch P4 legacy architectures to the generic legacy service
|
||||
if (!isP5Modern(serviceName)) {
|
||||
// PSA (Peugeot/Citroën/DS) uses a dedicated FI/VIN-indexed decode service.
|
||||
if (getServiceConfig(serviceName)?.architecture === "LEGACY_PSA") {
|
||||
return this.psaService.decodeVinForService(cleanVin, serviceName, userId);
|
||||
}
|
||||
if (isLegacyArchitecture(serviceName)) {
|
||||
return this.fordLegacyService.decodeVinForService(cleanVin, serviceName, userId);
|
||||
}
|
||||
@@ -245,7 +252,12 @@ export class PL24Service {
|
||||
if (this.isP4LegacyPath(linkPath)) {
|
||||
return this.fordLegacyService.fetchPartsByPath(linkPath, serviceName, userId);
|
||||
}
|
||||
// PSA image-board dispatch
|
||||
// PSA VIN-indexed parts (vin-image-board.action) → dedicated FI service.
|
||||
// Must come BEFORE isPsaBoardPath (vin-image-board.action also contains image-board.action).
|
||||
if (this.isPsaVinBoardPath(linkPath)) {
|
||||
return this.psaService.fetchVinParts(linkPath, serviceName);
|
||||
}
|
||||
// PSA catalog-browse image-board dispatch
|
||||
if (this.isPsaBoardPath(linkPath)) {
|
||||
return this.fordLegacyService.fetchPsaParts(linkPath, serviceName, body, engine, gearbox);
|
||||
}
|
||||
@@ -389,6 +401,15 @@ export class PL24Service {
|
||||
if (this.isP4LegacyPath(linkPath)) {
|
||||
return this.fordLegacyService.fetchSubGroupsByPath(linkPath, serviceName, userId);
|
||||
}
|
||||
// PSA VIN-indexed drill → dedicated FI service.
|
||||
// scope → main groups (json-vin-main-groups.action)
|
||||
if (this.isPsaVinMainGroupsPath(linkPath)) {
|
||||
return this.psaService.fetchVinMainGroups(linkPath);
|
||||
}
|
||||
// main group → illustrations (json-vin-illustrations.action)
|
||||
if (this.isPsaVinIllusPath(linkPath)) {
|
||||
return this.psaService.fetchVinIllustrations(linkPath);
|
||||
}
|
||||
// PSA scope dispatch ("psa::{svc}::scope=..." → main groups)
|
||||
if (this.isPsaPath(linkPath)) {
|
||||
return this.fordLegacyService.fetchPsaSubGroups(linkPath, body, engine, gearbox);
|
||||
@@ -941,44 +962,6 @@ export class PL24Service {
|
||||
return PL24_WMI_SERVICE_MAP[wmi] || null;
|
||||
}
|
||||
|
||||
private getYearFromVin(vin: string): number | null {
|
||||
if (!vin || vin.length < 10) return null;
|
||||
const yearChar = vin.charAt(9).toUpperCase();
|
||||
const yearMap: Record<string, number> = {
|
||||
"1": 2001,
|
||||
"2": 2002,
|
||||
"3": 2003,
|
||||
"4": 2004,
|
||||
"5": 2005,
|
||||
"6": 2006,
|
||||
"7": 2007,
|
||||
"8": 2008,
|
||||
"9": 2009,
|
||||
A: 2010,
|
||||
B: 2011,
|
||||
C: 2012,
|
||||
D: 2013,
|
||||
E: 2014,
|
||||
F: 2015,
|
||||
G: 2016,
|
||||
H: 2017,
|
||||
J: 2018,
|
||||
K: 2019,
|
||||
L: 2020,
|
||||
M: 2021,
|
||||
N: 2022,
|
||||
P: 2023,
|
||||
R: 2024,
|
||||
S: 2025,
|
||||
T: 2026,
|
||||
V: 2027,
|
||||
W: 2028,
|
||||
X: 2029,
|
||||
Y: 2030,
|
||||
};
|
||||
return yearMap[yearChar] || null;
|
||||
}
|
||||
|
||||
// ==================== PRIVATE: Response parsers ====================
|
||||
|
||||
/**
|
||||
@@ -1057,8 +1040,7 @@ export class PL24Service {
|
||||
return {
|
||||
brand: SERVICE_TO_BRAND[serviceName] || serviceName.replace("_parts", ""),
|
||||
model: lookup("model")?.trim() || (data.description as string)?.split(" - ")[0]?.trim() || "",
|
||||
year:
|
||||
Number.parseInt(lookup("model_yili", "year") || "", 10) || this.getYearFromVin(vin) || 0,
|
||||
year: Number.parseInt(lookup("model_yili", "year") || "", 10) || extractModelYear(vin) || 0,
|
||||
series: lookup("satis_tipi", "sales_type"),
|
||||
bodyType,
|
||||
engineCode: engineCode || (engineDesc ? engineDesc.split("/")[0]?.trim() : null),
|
||||
@@ -1531,6 +1513,19 @@ export class PL24Service {
|
||||
return linkPath.includes("/psa/") && linkPath.includes("image-board.action");
|
||||
}
|
||||
|
||||
// PSA VIN-indexed (FI) drill paths — handled by the dedicated PL24PsaService.
|
||||
private isPsaVinMainGroupsPath(linkPath: string): boolean {
|
||||
return linkPath.includes("/psa/") && linkPath.includes("json-vin-main-groups.action");
|
||||
}
|
||||
|
||||
private isPsaVinIllusPath(linkPath: string): boolean {
|
||||
return linkPath.includes("/psa/") && linkPath.includes("json-vin-illustrations.action");
|
||||
}
|
||||
|
||||
private isPsaVinBoardPath(linkPath: string): boolean {
|
||||
return linkPath.includes("/psa/") && linkPath.includes("vin-image-board.action");
|
||||
}
|
||||
|
||||
private isDaimlerService(serviceName: string): boolean {
|
||||
return serviceName.startsWith("mercedes") || serviceName === "smart_parts";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user