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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user