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

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:
2026-06-04 12:27:04 +03:00
parent 5573c4a401
commit 223c4bc12f
12 changed files with 982 additions and 248 deletions

View File

@@ -24,6 +24,11 @@ function createService(db: any) {
};
const pl24FordLegacyService = {
fetchCategoriesForPsaVin: vi.fn().mockResolvedValue([]),
fetchPsaParts: vi.fn().mockResolvedValue({ success: true, parts: [] }),
};
const pl24PsaService = {
fetchCategoriesForPsaVin: vi.fn().mockResolvedValue([]),
fetchVinParts: vi.fn().mockResolvedValue({ success: true, parts: [] }),
};
const translationsService = {
translate: vi
@@ -47,6 +52,7 @@ function createService(db: any) {
partsCatalogsService as any,
storage as any,
pl24FordLegacyService as any,
pl24PsaService as any,
translationsService as any,
pcatSourceDb as any,
emexSourceDb as any,

View File

@@ -8,6 +8,7 @@ import { EmexService } from "../integrations/emex/emex.service";
import { PartsCatalogsService } from "../integrations/parts-catalogs/parts-catalogs.service";
import { PcatGroup } from "../integrations/parts-catalogs/parts-catalogs.types";
import { PL24FordLegacyService } from "../integrations/pl24/pl24-ford-legacy.service";
import { PL24PsaService } from "../integrations/pl24/pl24-psa.service";
import { PL24Service } from "../integrations/pl24/pl24.service";
import { RedisService } from "../redis/redis.service";
import { StorageService } from "../storage/storage.service";
@@ -25,6 +26,7 @@ export class CategoriesService {
private partsCatalogsService: PartsCatalogsService,
private storage: StorageService,
private pl24FordLegacyService: PL24FordLegacyService,
private pl24PsaService: PL24PsaService,
private translationsService: TranslationsService,
private pcatSourceDb: PcatSourceDbService,
private emexSourceDb: EmexSourceDbService,
@@ -103,10 +105,7 @@ export class CategoriesService {
const svcName: string = rawData?.catalogInfo?.serviceName ?? "";
if (catPath.startsWith("/psa/") && svcName && vehicle.vin) {
try {
const scopes = await this.pl24FordLegacyService.fetchCategoriesForPsaVin(
svcName,
vehicle.vin,
);
const scopes = await this.pl24PsaService.fetchCategoriesForPsaVin(svcName, vehicle.vin);
if (scopes.length > 0) {
const insertData = scopes.map((s) => ({
vehicleId,
@@ -1298,16 +1297,26 @@ export class CategoriesService {
const isPsaBoard =
category.linkPath.includes("/psa/") &&
category.linkPath.includes("image-board.action");
// VIN-indexed leaves (vin-image-board.action) → dedicated PSA service.
const isPsaVinBoard =
category.linkPath.includes("/psa/") &&
category.linkPath.includes("vin-image-board.action");
if (needImage && isPsaBoard && !pl24Result.schemaImageBuffer) {
try {
const freshResult = await this.pl24FordLegacyService.fetchPsaParts(
category.linkPath,
catalogInfo.serviceName,
"_all_",
"_all_",
"_all_",
true,
);
const freshResult = isPsaVinBoard
? await this.pl24PsaService.fetchVinParts(
category.linkPath,
catalogInfo.serviceName,
true,
)
: await this.pl24FordLegacyService.fetchPsaParts(
category.linkPath,
catalogInfo.serviceName,
"_all_",
"_all_",
"_all_",
true,
);
if (freshResult.schemaImageBuffer) {
(pl24Result as any).schemaImageBuffer = freshResult.schemaImageBuffer;
(pl24Result as any).schemaImageContentType = freshResult.schemaImageContentType;