feat: EMEX catalog integration — schema, data migration, backend & frontend
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
Redesign emex_* tables from vehicle-centric to catalog-centric structure with junction tables (emex_vehicle_group_links, emex_vehicle_part_links). Migrate 92M+ rows from emex DB via postgres_fdw. Add EmexCatalogService for browse and VIN pre-scraped matching, with Redis caching. Frontend catalog page now has PL24/Emex tabs with full drill-down routes (brand → vehicle → group → parts). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,10 @@ import { CategoriesService } from "./categories.service";
|
||||
import { PL24Module } from "../integrations/pl24/pl24.module";
|
||||
import { EmexModule } from "../integrations/emex/emex.module";
|
||||
import { PartsCatalogsModule } from "../integrations/parts-catalogs/parts-catalogs.module";
|
||||
import { CatalogModule } from "../catalog/catalog.module";
|
||||
|
||||
@Module({
|
||||
imports: [PL24Module, EmexModule, PartsCatalogsModule],
|
||||
imports: [PL24Module, EmexModule, PartsCatalogsModule, CatalogModule],
|
||||
controllers: [CategoriesController],
|
||||
providers: [CategoriesService],
|
||||
exports: [CategoriesService],
|
||||
|
||||
@@ -25,7 +25,11 @@ function createService(db: any) {
|
||||
const pl24FordLegacyService = {
|
||||
fetchCategoriesForPsaVin: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
const service = new CategoriesService(db as any, redis as any, pl24Service as any, emexService as any, partsCatalogsService as any, storage as any, pl24FordLegacyService as any);
|
||||
const emexCatalogService = {
|
||||
matchByName: vi.fn().mockResolvedValue(null),
|
||||
getVehicleGroups: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
const service = new CategoriesService(db as any, redis as any, pl24Service as any, emexService as any, emexCatalogService as any, partsCatalogsService as any, storage as any, pl24FordLegacyService as any);
|
||||
return { service, db, redis, pl24Service };
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { RedisService } from "../redis/redis.service";
|
||||
import { PL24Service } from "../integrations/pl24/pl24.service";
|
||||
import { PL24FordLegacyService } from "../integrations/pl24/pl24-ford-legacy.service";
|
||||
import { EmexService } from "../integrations/emex/emex.service";
|
||||
import { EmexCatalogService } from "../catalog/emex-catalog.service";
|
||||
import { PartsCatalogsService } from "../integrations/parts-catalogs/parts-catalogs.service";
|
||||
import type { PcatGroup } from "../integrations/parts-catalogs/parts-catalogs.types";
|
||||
import { StorageService } from "../storage/storage.service";
|
||||
@@ -19,6 +20,7 @@ export class CategoriesService {
|
||||
private redis: RedisService,
|
||||
private pl24Service: PL24Service,
|
||||
private emexService: EmexService,
|
||||
private emexCatalogService: EmexCatalogService,
|
||||
private partsCatalogsService: PartsCatalogsService,
|
||||
private storage: StorageService,
|
||||
private pl24FordLegacyService: PL24FordLegacyService,
|
||||
@@ -203,9 +205,46 @@ export class CategoriesService {
|
||||
}
|
||||
}
|
||||
|
||||
// If still no categories, try EMEX fallback
|
||||
// If still no categories, try EMEX pre-scraped match first, then on-demand fallback
|
||||
if (dbCategories.length === 0 && vehicle.vin) {
|
||||
this.logger.log(`No PL24 categories for ${vehicle.vin}, trying EMEX fallback`);
|
||||
this.logger.log(`No PL24 categories for ${vehicle.vin}, trying EMEX pre-scraped match`);
|
||||
|
||||
// Try pre-scraped emex catalog match
|
||||
const vRawData = vehicle.rawData as Record<string, unknown> | null;
|
||||
const catalogCode = (vRawData?.catalogCode as string) || this.emexService.getCatalogCode(vehicle.vin);
|
||||
const vehicleName = (vRawData?.emexVehicleName as string) || (vRawData?.model as string);
|
||||
|
||||
if (catalogCode && vehicleName) {
|
||||
try {
|
||||
const match = await this.emexCatalogService.matchByName(catalogCode, vehicleName);
|
||||
if (match) {
|
||||
this.logger.log(`EMEX pre-scraped match found for ${vehicle.vin}: ${match.vehicleId}`);
|
||||
const groups = await this.emexCatalogService.getVehicleGroups(match.vehicleId);
|
||||
if (groups.length > 0) {
|
||||
// Convert emex groups to categories format for tree building
|
||||
const insertData = groups.map((g) => ({
|
||||
vehicleId,
|
||||
catalogVehicleId: null as string | null,
|
||||
name: g.name,
|
||||
nameOriginal: g.nameOriginal || g.name,
|
||||
parentId: null as string | null,
|
||||
externalId: g.groupId,
|
||||
linkPath: `emex-catalog:${match.vehicleId}:${g.id}`,
|
||||
linkWid: null as string | null,
|
||||
source: "emex" as const,
|
||||
}));
|
||||
dbCategories = await this.db.insert(categories).values(insertData).onConflictDoNothing().returning();
|
||||
this.logger.log(`Stored ${dbCategories.length} EMEX pre-scraped categories for ${vehicle.vin}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`EMEX pre-scraped match failed for ${vehicle.vin}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If pre-scraped match didn't work, try on-demand EMEX scrape
|
||||
if (dbCategories.length === 0) {
|
||||
this.logger.log(`No pre-scraped match for ${vehicle.vin}, trying EMEX on-demand fallback`);
|
||||
try {
|
||||
const emexResult = await this.emexService.decodeVin(vehicle.vin);
|
||||
if (emexResult) {
|
||||
@@ -302,6 +341,7 @@ export class CategoriesService {
|
||||
} catch (emexErr) {
|
||||
this.logger.warn(`EMEX category fallback failed for ${vehicle.vin}: ${(emexErr as Error).message}`);
|
||||
}
|
||||
} // end: if (dbCategories.length === 0) — on-demand fallback
|
||||
}
|
||||
|
||||
// Build tree
|
||||
|
||||
Reference in New Issue
Block a user