feat: Ford legacy support, EMEX browser pooling, collapsible sidebar

- Add PL24 Ford legacy service for fordt_parts architecture
- Refactor EMEX to use persistent browser pool instead of per-call instances
- Make vehicle decode resilient: fallback to PL24 when Corgi doesn't recognize VIN
- Add collapsible sidebar with persistent user preference
- Improve brand access guard and categories service
- Add debug/test scripts for VIN e2e testing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-02-14 22:25:06 +00:00
parent 7b024df4d5
commit 4a06ba5fdb
53 changed files with 17053 additions and 682 deletions

View File

@@ -8,7 +8,7 @@ import {
} from "@nestjs/common";
import { eq, and, desc } from "drizzle-orm";
import { DATABASE, Database } from "../database/database.provider";
import { vehicles, queryLogs, brands, userBrands, userSubscriptions } from "../database/schema/core";
import { vehicles, queryLogs, brands, userBrands, userSubscriptions, plans } from "../database/schema/core";
import { CorgiService } from "../integrations/corgi/corgi.service";
import { PL24Service } from "../integrations/pl24/pl24.service";
import { VinApiService } from "../integrations/vin-api/vin-api.service";
@@ -53,32 +53,50 @@ export class VehiclesService {
// 2. Corgi decode (offline)
const corgiResult = this.corgiService.decodeVin(vin);
if (!corgiResult || !corgiResult.isKnown) {
await this.logQuery(userId, vin, null, "corgi", false, Date.now() - startTime, "Unknown VIN/brand");
throw new BadRequestException("VIN not recognized. Brand not supported.");
const corgiKnown = corgiResult && corgiResult.isKnown;
// 3. Brand access check (only if Corgi recognized the brand)
let brandId: string | null = null;
let brandName: string | null = null;
if (corgiKnown) {
const brand = await this.db
.select()
.from(brands)
.where(eq(brands.name, corgiResult.brandName))
.limit(1);
if (brand.length > 0) {
brandId = brand[0].id;
brandName = corgiResult.brandName;
await this.checkBrandAccess(userId, brandId);
}
}
// 3. Brand access check
const brand = await this.db
.select()
.from(brands)
.where(eq(brands.name, corgiResult.brandName))
.limit(1);
if (brand.length === 0) {
throw new BadRequestException(`Brand not supported: ${corgiResult.brandName}`);
}
const brandId = brand[0].id;
await this.checkBrandAccess(userId, brandId);
// 4. PL24 decode (real API)
// 4. PL24 decode (real API) — always attempt, PL24 has its own WMI map
let source = "corgi";
let pl24Vehicle = null;
try {
pl24Vehicle = await this.pl24Service.decodeVin(vin);
} catch (err) {
this.logger.warn(`PL24 decode failed for ${vin}: ${(err as Error).message}`);
if (this.pl24Service.isSupported(vin)) {
try {
pl24Vehicle = await this.pl24Service.decodeVin(vin);
// If Corgi didn't know the brand, resolve it from PL24's WMI map
if (!brandId && pl24Vehicle) {
const pl24Brand = this.pl24Service.getBrandName(vin);
if (pl24Brand) {
const brand = await this.db
.select()
.from(brands)
.where(eq(brands.name, pl24Brand))
.limit(1);
if (brand.length > 0) {
brandId = brand[0].id;
brandName = pl24Brand;
await this.checkBrandAccess(userId, brandId);
}
}
}
} catch (err) {
this.logger.warn(`PL24 decode failed for ${vin}: ${(err as Error).message}`);
}
}
// 5. Fallback to EMEX if PL24 not available
@@ -86,10 +104,19 @@ export class VehiclesService {
if (!pl24Vehicle) {
this.logger.log(`PL24 returned no data for ${vin}, trying EMEX fallback`);
try {
if (this.emexService.isSupported(vin)) {
const emexResult = await this.emexService.decodeVin(vin);
if (emexResult && emexResult.brand !== 'UNKNOWN') {
emexVehicle = emexResult;
const emexResult = await this.emexService.decodeVin(vin);
if (emexResult && emexResult.brand !== 'UNKNOWN') {
emexVehicle = emexResult;
if (!brandId && emexResult.brand) {
const emexBrand = await this.db
.select()
.from(brands)
.where(eq(brands.name, emexResult.brand))
.limit(1);
if (emexBrand.length > 0) {
brandId = emexBrand[0].id;
brandName = emexResult.brand;
}
}
}
} catch (emexError) {
@@ -97,6 +124,12 @@ export class VehiclesService {
}
}
// If nothing recognized this VIN at all, give up
if (!pl24Vehicle && !emexVehicle && !corgiKnown) {
await this.logQuery(userId, vin, null, "corgi", false, Date.now() - startTime, "Unknown VIN/brand");
throw new BadRequestException("VIN not recognized. Brand not supported.");
}
// 6. Fallback to VIN API if PL24 and EMEX not available
let vinApiData: any = null;
if (!pl24Vehicle && !emexVehicle) {
@@ -113,9 +146,9 @@ export class VehiclesService {
userId,
vin,
brandId,
brandName: corgiResult.brandName,
brandName: brandName || corgiResult?.brandName || emexVehicle?.brand || null,
model: pl24Vehicle?.model || emexVehicle?.model || vinApiData?.model || null,
year: pl24Vehicle?.year || emexVehicle?.year || corgiResult.modelYear || (vinApiData ? parseInt(vinApiData.modelYear) : null),
year: pl24Vehicle?.year || emexVehicle?.year || corgiResult?.modelYear || (vinApiData ? parseInt(vinApiData.modelYear) : null),
engine: pl24Vehicle?.engineType || pl24Vehicle?.engineCode || emexVehicle?.engineCode || emexVehicle?.engineType || vinApiData?.engineModel || null,
transmission: pl24Vehicle?.transmission || emexVehicle?.transmission || vinApiData?.transmissionStyle || null,
bodyType: pl24Vehicle?.bodyType || emexVehicle?.bodyType || vinApiData?.bodyClass || null,
@@ -177,8 +210,12 @@ export class VehiclesService {
private async checkBrandAccess(userId: string, brandId: string) {
const [sub] = await this.db
.select()
.select({
id: userSubscriptions.id,
brandCount: plans.brandCount,
})
.from(userSubscriptions)
.innerJoin(plans, eq(userSubscriptions.planId, plans.id))
.where(and(eq(userSubscriptions.userId, userId), eq(userSubscriptions.status, "active")))
.limit(1);
@@ -186,6 +223,9 @@ export class VehiclesService {
throw new ForbiddenException("No active subscription. Please subscribe to access vehicle data.");
}
// brandCount === 0 means unlimited (Full Paket) — skip per-brand check
if (sub.brandCount === 0) return;
const [access] = await this.db
.select()
.from(userBrands)