feat: PL24 multi-brand support & Corgi integration

## PL24 Improvements
- Add JLR (Jaguar/Land Rover) 3-step parts flow (vin_illus → vin_bomdetails)
- Enhance Ford parser with comprehensive category/subcategory handling
- Extend Hyundai-Kia parser with improved part extraction and hotspot support
- Add dedicated Kia parser for brand-specific parsing
- Add Kia scraper service for specialized scraping
- Improve legacy scraper with better error handling

## New Features
- Add Corgi API integration (corgi.service.ts, corgi.types.ts)
- Add NHTSA WMI fetch script for VIN decoding
- Add 90+ new schema images for parts visualization

## Bug Fixes & Improvements
- Update VIN decoder service with better brand detection
- Improve vehicles service with enhanced data handling
- Update parser factory with Kia support

## Assets
- Add new schema images for multiple vehicle brands
- Update UI screenshots (categories, parts, vehicles pages)
- Add debug hotspots utility for development

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Semih
2026-01-29 19:22:04 +01:00
parent 757905f41c
commit 895416f1aa
122 changed files with 5544 additions and 226 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

View File

@@ -0,0 +1,492 @@
#!/usr/bin/env npx ts-node
/**
* NHTSA WMI Data Fetcher (Comprehensive)
*
* Fetches ALL WMI codes from NHTSA VPIC API by:
* 1. Getting all manufacturers (paginated)
* 2. Getting WMIs for each manufacturer
* 3. Merging with known mappings for quality
*
* Usage: npx ts-node scripts/fetch-nhtsa-wmi.ts
*/
import * as fs from 'fs';
import * as path from 'path';
const NHTSA_API_BASE = 'https://vpic.nhtsa.dot.gov/api/vehicles';
const OUTPUT_FILE = path.join(__dirname, '../src/integrations/pl24/nhtsa-wmi-data.ts');
interface NHTSAManufacturer {
Mfr_ID: number;
Mfr_Name: string;
Mfr_CommonName: string | null;
Country: string;
}
interface NHTSAWmiResult {
CommonName: string | null;
Make: string | null;
ManufacturerName: string | null;
VehicleType: string | null;
WMI: string;
}
interface WmiEntry {
name: string;
country: string;
vehicleType?: string;
}
// Hardcoded WMI mappings for well-known manufacturers (quality assurance)
const KNOWN_WMI_MAPPINGS: Record<string, { name: string; country: string }> = {
// === VOLKSWAGEN GROUP ===
WVW: { name: 'Volkswagen', country: 'Germany' },
WVG: { name: 'Volkswagen', country: 'Germany' },
'1VW': { name: 'Volkswagen', country: 'United States' },
'3VW': { name: 'Volkswagen', country: 'Mexico' },
'9BW': { name: 'Volkswagen', country: 'Brazil' },
WAU: { name: 'Audi', country: 'Germany' },
WUA: { name: 'Audi', country: 'Germany' },
TRU: { name: 'Audi', country: 'Hungary' },
TMB: { name: 'Skoda', country: 'Czech Republic' },
TMP: { name: 'Skoda', country: 'Czech Republic' },
VSS: { name: 'SEAT', country: 'Spain' },
VSE: { name: 'SEAT', country: 'Spain' },
SCB: { name: 'Bentley', country: 'United Kingdom' },
// === BMW GROUP ===
WBA: { name: 'BMW', country: 'Germany' },
WBS: { name: 'BMW M', country: 'Germany' },
WBY: { name: 'BMW i', country: 'Germany' },
WBX: { name: 'BMW X', country: 'Germany' },
WB1: { name: 'BMW Motorrad', country: 'Germany' },
'5UX': { name: 'BMW', country: 'United States' },
WMW: { name: 'MINI', country: 'United Kingdom' },
// === MERCEDES GROUP ===
WDB: { name: 'Mercedes-Benz', country: 'Germany' },
WDD: { name: 'Mercedes-Benz', country: 'Germany' },
WDC: { name: 'Mercedes-Benz', country: 'Germany' },
W1K: { name: 'Mercedes-Benz', country: 'Germany' },
W1N: { name: 'Mercedes-Benz', country: 'Germany' },
W1V: { name: 'Mercedes-Benz', country: 'Germany' },
WDF: { name: 'Mercedes-Benz Vans', country: 'Germany' },
WME: { name: 'smart', country: 'Germany' },
// === PORSCHE ===
WP0: { name: 'Porsche', country: 'Germany' },
WP1: { name: 'Porsche', country: 'Germany' },
// === OPEL ===
W0L: { name: 'Opel', country: 'Germany' },
W0V: { name: 'Opel', country: 'Spain' },
VXK: { name: 'Vauxhall', country: 'United Kingdom' },
// === TOYOTA / LEXUS ===
JTD: { name: 'Toyota', country: 'Japan' },
JTE: { name: 'Toyota', country: 'Japan' },
JTN: { name: 'Toyota', country: 'Japan' },
JTM: { name: 'Toyota', country: 'Japan' },
'2T1': { name: 'Toyota', country: 'United States' },
'4T1': { name: 'Toyota', country: 'United States' },
'5TD': { name: 'Toyota', country: 'United States' },
NMT: { name: 'Toyota', country: 'Turkey' },
SB1: { name: 'Toyota', country: 'United Kingdom' },
JTH: { name: 'Lexus', country: 'Japan' },
JTJ: { name: 'Lexus', country: 'Japan' },
'2T2': { name: 'Lexus', country: 'United States' },
// === HONDA / ACURA ===
JHM: { name: 'Honda', country: 'Japan' },
JHL: { name: 'Honda', country: 'Japan' },
'1HG': { name: 'Honda', country: 'United States' },
'2HG': { name: 'Honda', country: 'Canada' },
'5J6': { name: 'Honda', country: 'United States' },
JH4: { name: 'Acura', country: 'Japan' },
'19U': { name: 'Acura', country: 'United States' },
// === NISSAN / INFINITI ===
JN1: { name: 'Nissan', country: 'Japan' },
JN6: { name: 'Nissan', country: 'Japan' },
JN8: { name: 'Nissan', country: 'Japan' },
VNK: { name: 'Nissan', country: 'United Kingdom' },
SJN: { name: 'Nissan', country: 'United Kingdom' },
'1N4': { name: 'Nissan', country: 'United States' },
'5N1': { name: 'Nissan', country: 'United States' },
JNK: { name: 'Infiniti', country: 'Japan' },
// === HYUNDAI / KIA / GENESIS ===
KMH: { name: 'Hyundai', country: 'South Korea' },
KMF: { name: 'Hyundai', country: 'South Korea' },
NLH: { name: 'Hyundai', country: 'Turkey' },
TMA: { name: 'Hyundai', country: 'Czech Republic' },
'5NP': { name: 'Hyundai', country: 'United States' },
KNA: { name: 'Kia', country: 'South Korea' },
KNE: { name: 'Kia', country: 'South Korea' },
KND: { name: 'Kia', country: 'South Korea' },
U5Y: { name: 'Kia', country: 'Slovakia' },
'5XY': { name: 'Kia', country: 'United States' },
KMT: { name: 'Genesis', country: 'South Korea' },
// === MAZDA ===
JM1: { name: 'Mazda', country: 'Japan' },
JM3: { name: 'Mazda', country: 'Japan' },
'1YV': { name: 'Mazda', country: 'United States' },
'3MZ': { name: 'Mazda', country: 'Mexico' },
// === SUBARU ===
JF1: { name: 'Subaru', country: 'Japan' },
JF2: { name: 'Subaru', country: 'Japan' },
'4S3': { name: 'Subaru', country: 'United States' },
'4S4': { name: 'Subaru', country: 'United States' },
// === MITSUBISHI ===
JMB: { name: 'Mitsubishi', country: 'Japan' },
JMY: { name: 'Mitsubishi', country: 'Thailand' },
JA3: { name: 'Mitsubishi', country: 'Japan' },
JA4: { name: 'Mitsubishi', country: 'Japan' },
'4A3': { name: 'Mitsubishi', country: 'United States' },
// === SUZUKI ===
JS2: { name: 'Suzuki', country: 'Japan' },
JS3: { name: 'Suzuki', country: 'Japan' },
TSM: { name: 'Suzuki', country: 'Hungary' },
// === RENAULT GROUP ===
VF1: { name: 'Renault', country: 'France' },
VF6: { name: 'Renault', country: 'France' },
VNE: { name: 'Renault', country: 'Spain' },
UU1: { name: 'Dacia', country: 'Romania' },
UU6: { name: 'Dacia', country: 'Romania' },
VFA: { name: 'Alpine', country: 'France' },
// === STELLANTIS ===
VF3: { name: 'Peugeot', country: 'France' },
VR3: { name: 'Peugeot', country: 'Spain' },
VF7: { name: 'Citroen', country: 'France' },
VR7: { name: 'Citroen', country: 'Spain' },
VR1: { name: 'DS', country: 'France' },
ZFA: { name: 'Fiat', country: 'Italy' },
ZAR: { name: 'Alfa Romeo', country: 'Italy' },
ZAM: { name: 'Maserati', country: 'Italy' },
ZHW: { name: 'Lamborghini', country: 'Italy' },
ZFF: { name: 'Ferrari', country: 'Italy' },
// === BRITISH ===
SAJ: { name: 'Jaguar', country: 'United Kingdom' },
SAL: { name: 'Land Rover', country: 'United Kingdom' },
SCA: { name: 'Rolls-Royce', country: 'United Kingdom' },
SCF: { name: 'Aston Martin', country: 'United Kingdom' },
SBM: { name: 'McLaren', country: 'United Kingdom' },
// === SWEDISH ===
YV1: { name: 'Volvo', country: 'Sweden' },
YV4: { name: 'Volvo', country: 'Sweden' },
YS3: { name: 'Saab', country: 'Sweden' },
LP7: { name: 'Polestar', country: 'China' },
YSM: { name: 'Polestar', country: 'Sweden' },
// === AMERICAN ===
'1G1': { name: 'Chevrolet', country: 'United States' },
'1GC': { name: 'Chevrolet', country: 'United States' },
'2G1': { name: 'Chevrolet', country: 'Canada' },
'1G4': { name: 'Buick', country: 'United States' },
'1G6': { name: 'Cadillac', country: 'United States' },
'1GN': { name: 'GMC', country: 'United States' },
'1FA': { name: 'Ford', country: 'United States' },
'1FM': { name: 'Ford', country: 'United States' },
'1FT': { name: 'Ford', country: 'United States' },
'2FA': { name: 'Ford', country: 'Canada' },
'3FA': { name: 'Ford', country: 'Mexico' },
WF0: { name: 'Ford', country: 'Germany' },
'1C3': { name: 'Chrysler', country: 'United States' },
'1C4': { name: 'Chrysler', country: 'United States' },
'2C3': { name: 'Dodge', country: 'Canada' },
'1J4': { name: 'Jeep', country: 'United States' },
'1J8': { name: 'Jeep', country: 'United States' },
'3C6': { name: 'RAM', country: 'Mexico' },
'5YJ': { name: 'Tesla', country: 'United States' },
'7SA': { name: 'Tesla', country: 'United States' },
'1HD': { name: 'Harley-Davidson', country: 'United States' },
// === OTHERS ===
WMA: { name: 'MAN', country: 'Germany' },
YS2: { name: 'Scania', country: 'Sweden' },
WJM: { name: 'Iveco', country: 'Germany' },
};
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${url}`);
}
return response.json();
}
async function fetchAllManufacturers(): Promise<NHTSAManufacturer[]> {
const manufacturers: NHTSAManufacturer[] = [];
let page = 1;
const maxPages = 150; // Safety limit
console.log('Fetching all manufacturers from NHTSA...');
while (page <= maxPages) {
const url = `${NHTSA_API_BASE}/getallmanufacturers?format=json&page=${page}`;
try {
const data = await fetchJson<{ Count: number; Results: NHTSAManufacturer[] }>(url);
if (!data.Results || data.Results.length === 0) break;
manufacturers.push(...data.Results);
process.stdout.write(`\r Page ${page}: ${manufacturers.length} manufacturers`);
page++;
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 50));
} catch (error) {
console.error(`\n Error on page ${page}: ${(error as Error).message}`);
break;
}
}
console.log(`\n Total manufacturers: ${manufacturers.length}`);
return manufacturers;
}
async function fetchWmisForManufacturer(manufacturerId: number): Promise<NHTSAWmiResult[]> {
const url = `${NHTSA_API_BASE}/getwmisformanufacturer/${manufacturerId}?format=json`;
try {
const data = await fetchJson<{ Results: NHTSAWmiResult[] }>(url);
return data.Results || [];
} catch {
return [];
}
}
function normalizeManufacturerName(name: string | null | undefined): string {
if (!name) return 'Unknown';
return name
.replace(/,?\s*(Inc\.?|LLC|Ltd\.?|GmbH|AG|S\.?A\.?|Corp\.?|Co\.?|Corporation|Limited|Company)$/gi, '')
.replace(/\s+/g, ' ')
.trim() || 'Unknown';
}
function getCountryFromManufacturer(mfr: NHTSAManufacturer): string {
if (!mfr.Country) return 'Unknown';
const countryMap: Record<string, string> = {
'GERMANY': 'Germany',
'JAPAN': 'Japan',
'UNITED STATES (USA)': 'United States',
'USA': 'United States',
'UNITED STATES': 'United States',
'KOREA, REPUBLIC OF (SOUTH KOREA)': 'South Korea',
'KOREA (SOUTH)': 'South Korea',
'SOUTH KOREA': 'South Korea',
'KOREA': 'South Korea',
'CHINA': 'China',
'UNITED KINGDOM (UK)': 'United Kingdom',
'UK': 'United Kingdom',
'UNITED KINGDOM': 'United Kingdom',
'FRANCE': 'France',
'ITALY': 'Italy',
'SPAIN': 'Spain',
'SWEDEN': 'Sweden',
'CZECH REPUBLIC': 'Czech Republic',
'CZECHIA': 'Czech Republic',
'TURKEY': 'Turkey',
'BRAZIL': 'Brazil',
'MEXICO': 'Mexico',
'CANADA': 'Canada',
'INDIA': 'India',
'THAILAND': 'Thailand',
'INDONESIA': 'Indonesia',
'ROMANIA': 'Romania',
'HUNGARY': 'Hungary',
'POLAND': 'Poland',
'SLOVAKIA': 'Slovakia',
'SLOVENIA': 'Slovenia',
'BELGIUM': 'Belgium',
'NETHERLANDS': 'Netherlands',
'AUSTRIA': 'Austria',
'PORTUGAL': 'Portugal',
'RUSSIA': 'Russia',
'TAIWAN': 'Taiwan',
'VIETNAM': 'Vietnam',
'MALAYSIA': 'Malaysia',
'PHILIPPINES': 'Philippines',
'AUSTRALIA': 'Australia',
'SOUTH AFRICA': 'South Africa',
'ARGENTINA': 'Argentina',
};
const upper = mfr.Country.toUpperCase();
return countryMap[upper] || mfr.Country;
}
async function main() {
console.log('=== NHTSA WMI Data Fetcher (Comprehensive) ===\n');
const wmiMap = new Map<string, WmiEntry>();
// Step 1: Add all known WMI mappings first
console.log('Step 1: Adding known WMI mappings...');
for (const [wmi, info] of Object.entries(KNOWN_WMI_MAPPINGS)) {
wmiMap.set(wmi, { name: info.name, country: info.country });
}
console.log(` Added ${Object.keys(KNOWN_WMI_MAPPINGS).length} known WMIs\n`);
// Step 2: Fetch all manufacturers
console.log('Step 2: Fetching all manufacturers...');
const manufacturers = await fetchAllManufacturers();
// Step 3: Fetch WMIs for each manufacturer
console.log('\nStep 3: Fetching WMIs for each manufacturer...');
let processed = 0;
let totalWmisFound = 0;
for (const mfr of manufacturers) {
processed++;
if (processed % 100 === 0) {
process.stdout.write(`\r Processing: ${processed}/${manufacturers.length} (WMIs found: ${wmiMap.size})`);
}
const wmis = await fetchWmisForManufacturer(mfr.Mfr_ID);
for (const wmiResult of wmis) {
if (wmiResult.WMI && wmiResult.WMI.length >= 3) {
const wmi = wmiResult.WMI.substring(0, 3).toUpperCase();
totalWmisFound++;
// Skip if we already have a known mapping
if (KNOWN_WMI_MAPPINGS[wmi]) {
continue;
}
// Skip if we already have this WMI with a good name
const existing = wmiMap.get(wmi);
if (existing && existing.name !== 'Unknown') {
continue;
}
// Extract name
let name = wmiResult.CommonName || wmiResult.Make || mfr.Mfr_CommonName;
if (!name || name === 'null') {
name = normalizeManufacturerName(wmiResult.ManufacturerName || mfr.Mfr_Name);
}
// Get country
const country = getCountryFromManufacturer(mfr);
wmiMap.set(wmi, {
name: name || 'Unknown',
country,
vehicleType: wmiResult.VehicleType || undefined,
});
}
}
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 20));
}
console.log(`\n Processed ${processed} manufacturers`);
console.log(` Total WMI entries found in API: ${totalWmisFound}`);
console.log(` Unique WMI codes: ${wmiMap.size}\n`);
// Step 4: Final quality pass
console.log('Step 4: Final quality checks...');
let fixedCount = 0;
for (const [wmi, entry] of wmiMap.entries()) {
// If name is still Unknown and we have a known mapping, use it
if (entry.name === 'Unknown' && KNOWN_WMI_MAPPINGS[wmi]) {
wmiMap.set(wmi, {
...entry,
name: KNOWN_WMI_MAPPINGS[wmi].name,
country: KNOWN_WMI_MAPPINGS[wmi].country,
});
fixedCount++;
}
}
console.log(` Fixed ${fixedCount} entries with known mappings\n`);
// Sort by WMI code
const sortedEntries = Array.from(wmiMap.entries()).sort((a, b) => a[0].localeCompare(b[0]));
// Generate TypeScript file
console.log('Step 5: Generating TypeScript file...');
const tsContent = `/**
* NHTSA WMI (World Manufacturer Identifier) Database
*
* Auto-generated from NHTSA VPIC API
* Generated: ${new Date().toISOString()}
* Total entries: ${sortedEntries.length}
*
* This file provides manufacturer information for VIN validation
* without requiring external database dependencies.
*/
export interface WmiManufacturer {
/** Manufacturer/Brand name */
name: string;
/** Country of origin */
country: string;
/** Primary vehicle type (optional) */
vehicleType?: string;
}
/**
* WMI to Manufacturer mapping
* WMI = First 3 characters of VIN
*/
export const NHTSA_WMI_MANUFACTURERS: Record<string, WmiManufacturer> = {
${sortedEntries.map(([wmi, entry]) => {
const escapedName = entry.name.replace(/'/g, "\\'");
const escapedCountry = entry.country.replace(/'/g, "\\'");
const vehicleTypePart = entry.vehicleType ? `, vehicleType: '${entry.vehicleType.replace(/'/g, "\\'")}' ` : '';
return ` '${wmi}': { name: '${escapedName}', country: '${escapedCountry}'${vehicleTypePart}},`;
}).join('\n')}
};
/**
* Get manufacturer info for a WMI code
*/
export function getManufacturerByWmi(wmi: string): WmiManufacturer | null {
const normalized = wmi.toUpperCase().substring(0, 3);
return NHTSA_WMI_MANUFACTURERS[normalized] || null;
}
/**
* Get manufacturer name for a VIN
*/
export function getManufacturerName(vin: string): string | null {
if (!vin || vin.length < 3) return null;
const manufacturer = getManufacturerByWmi(vin.substring(0, 3));
return manufacturer?.name || null;
}
/**
* Get country of origin for a VIN
*/
export function getCountryOfOrigin(vin: string): string | null {
if (!vin || vin.length < 3) return null;
const manufacturer = getManufacturerByWmi(vin.substring(0, 3));
return manufacturer?.country || null;
}
`;
// Ensure directory exists
const dir = path.dirname(OUTPUT_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(OUTPUT_FILE, tsContent, 'utf-8');
console.log(`\n=== Complete ===`);
console.log(`Output: ${OUTPUT_FILE}`);
console.log(`Total WMI codes: ${sortedEntries.length}`);
console.log(`File size: ${(tsContent.length / 1024).toFixed(1)} KB`);
}
main().catch(console.error);

View File

@@ -0,0 +1,17 @@
/**
* Corgi VIN Decoder Module
*
* NestJS module for Corgi integration.
*/
import { Module, Global } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CorgiService } from './corgi.service';
@Global()
@Module({
imports: [ConfigModule],
providers: [CorgiService],
exports: [CorgiService],
})
export class CorgiModule {}

View File

@@ -0,0 +1,273 @@
/**
* Corgi VIN Decoder Service
*
* Provides offline VIN validation and decoding using embedded NHTSA WMI data.
* No external database dependencies (no SQLite, no better-sqlite3).
*
* Used for:
* - Pre-validation before PL24 API calls
* - WMI lookup for intelligent service routing
* - Model year extraction from VIN
* - Manufacturer name lookup
*/
import { Injectable, Logger } from '@nestjs/common';
import {
VinValidationResult,
WmiInfo,
BasicVehicleInfo,
} from './corgi.types';
import {
getManufacturerByWmi,
getManufacturerName,
getCountryOfOrigin,
WmiManufacturer,
} from '../pl24/nhtsa-wmi-data';
@Injectable()
export class CorgiService {
private readonly logger = new Logger(CorgiService.name);
constructor() {
this.logger.log('Corgi VIN decoder initialized (embedded WMI data)');
}
/**
* Validate a VIN
* Checks format, check digit, and extracts metadata
*/
async validateVin(vin: string): Promise<VinValidationResult> {
const normalizedVin = this.normalizeVin(vin);
// Basic format validation
const formatErrors = this.validateFormat(normalizedVin);
if (formatErrors.length > 0) {
return {
valid: false,
vin: normalizedVin,
checkDigit: null,
modelYear: null,
manufacturer: null,
country: null,
errors: formatErrors,
};
}
// Check digit validation
const checkDigitValid = this.validateCheckDigit(normalizedVin);
const modelYear = this.extractModelYear(normalizedVin);
const manufacturer = getManufacturerByWmi(normalizedVin);
return {
valid: checkDigitValid,
vin: normalizedVin,
checkDigit: {
isValid: checkDigitValid,
actual: normalizedVin.charAt(8),
},
modelYear,
manufacturer: manufacturer?.name || null,
country: manufacturer?.country || null,
errors: checkDigitValid ? [] : [{
code: 200,
message: 'Gecersiz kontrol hanesi',
severity: 'ERROR',
}],
};
}
/**
* Get WMI (World Manufacturer Identifier) info
*/
async getWmiInfo(vin: string): Promise<WmiInfo> {
const normalizedVin = this.normalizeVin(vin);
const wmi = normalizedVin.substring(0, 3);
const manufacturer = getManufacturerByWmi(wmi);
return {
wmi,
manufacturer: manufacturer?.name || null,
make: manufacturer?.name || null,
country: manufacturer?.country || null,
region: this.getRegionFromCountry(manufacturer?.country),
};
}
/**
* Get basic vehicle info from VIN
* Note: Without full VPIC database, only WMI-based info is available
*/
async getBasicVehicleInfo(vin: string): Promise<BasicVehicleInfo | null> {
const normalizedVin = this.normalizeVin(vin);
if (!this.validateCheckDigit(normalizedVin)) {
return null;
}
const manufacturer = getManufacturerByWmi(normalizedVin);
const year = this.extractModelYear(normalizedVin);
return {
make: manufacturer?.name || null,
model: null, // Not available without full VPIC
year,
bodyStyle: null,
driveType: null,
fuelType: null,
engineCylinders: null,
engineDisplacement: null,
};
}
/**
* Extract model year from VIN position 10
*/
extractModelYear(vin: string): number | null {
if (!vin || vin.length < 10) return null;
const yearChar = vin.charAt(9).toUpperCase();
const decadeChar = vin.charAt(6);
// Year code mapping (30-year cycle)
const yearMap: Record<string, number[]> = {
'A': [1980, 2010], 'B': [1981, 2011], 'C': [1982, 2012],
'D': [1983, 2013], 'E': [1984, 2014], 'F': [1985, 2015],
'G': [1986, 2016], 'H': [1987, 2017], 'J': [1988, 2018],
'K': [1989, 2019], 'L': [1990, 2020], 'M': [1991, 2021],
'N': [1992, 2022], 'P': [1993, 2023], 'R': [1994, 2024],
'S': [1995, 2025], 'T': [1996, 2026], 'V': [1997, 2027],
'W': [1998, 2028], 'X': [1999, 2029], 'Y': [2000, 2030],
'1': [2001, 2031], '2': [2002, 2032], '3': [2003, 2033],
'4': [2004, 2034], '5': [2005, 2035], '6': [2006, 2036],
'7': [2007, 2037], '8': [2008, 2038], '9': [2009, 2039],
};
const years = yearMap[yearChar];
if (!years) return null;
// Use position 7 to determine decade (numeric = 2010+)
const isNumeric = /\d/.test(decadeChar);
return isNumeric ? years[1] : years[0];
}
/**
* Get manufacturer name for a VIN
*/
getManufacturerName(vin: string): string | null {
return getManufacturerName(vin);
}
/**
* Get country of origin for a VIN
*/
getCountryOfOrigin(vin: string): string | null {
return getCountryOfOrigin(vin);
}
/**
* Normalize VIN (uppercase, remove invalid chars)
*/
private normalizeVin(vin: string): string {
return vin.toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/g, '');
}
/**
* Basic format validation
*/
private validateFormat(vin: string): Array<{ code: number; message: string; severity: string }> {
const errors: Array<{ code: number; message: string; severity: string }> = [];
if (vin.length !== 17) {
errors.push({
code: 100,
message: `VIN 17 karakter olmali (${vin.length} karakter girildi)`,
severity: 'ERROR',
});
}
if (/[IOQ]/i.test(vin)) {
errors.push({
code: 101,
message: 'VIN I, O veya Q harflerini iceremez',
severity: 'ERROR',
});
}
return errors;
}
/**
* Validate check digit (position 9) using CFR Title 49 algorithm
*/
private validateCheckDigit(vin: string): boolean {
if (vin.length !== 17) return false;
const transliteration: Record<string, number> = {
'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6, 'G': 7, 'H': 8,
'J': 1, 'K': 2, 'L': 3, 'M': 4, 'N': 5, 'P': 7, 'R': 9,
'S': 2, 'T': 3, 'U': 4, 'V': 5, 'W': 6, 'X': 7, 'Y': 8, 'Z': 9,
};
const weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
let sum = 0;
for (let i = 0; i < 17; i++) {
const char = vin.charAt(i);
const value = /\d/.test(char) ? parseInt(char, 10) : transliteration[char];
if (value === undefined) return false;
sum += value * weights[i];
}
const remainder = sum % 11;
const expectedCheckDigit = remainder === 10 ? 'X' : remainder.toString();
const actualCheckDigit = vin.charAt(8);
return expectedCheckDigit === actualCheckDigit;
}
/**
* Get region from country name
*/
private getRegionFromCountry(country: string | undefined | null): string | null {
if (!country) return null;
const regionMap: Record<string, string> = {
'Germany': 'Europe',
'United Kingdom': 'Europe',
'France': 'Europe',
'Italy': 'Europe',
'Spain': 'Europe',
'Sweden': 'Europe',
'Czech Republic': 'Europe',
'Hungary': 'Europe',
'Poland': 'Europe',
'Slovakia': 'Europe',
'Slovenia': 'Europe',
'Belgium': 'Europe',
'Netherlands': 'Europe',
'Austria': 'Europe',
'Portugal': 'Europe',
'Romania': 'Europe',
'Japan': 'Asia',
'South Korea': 'Asia',
'China': 'Asia',
'India': 'Asia',
'Thailand': 'Asia',
'Indonesia': 'Asia',
'United States': 'North America',
'Canada': 'North America',
'Mexico': 'North America',
'Brazil': 'South America',
'Turkey': 'Europe/Asia',
};
return regionMap[country] || null;
}
/**
* Check if decoder is available (always true with embedded data)
*/
isAvailable(): boolean {
return true;
}
}

View File

@@ -0,0 +1,64 @@
/**
* Corgi VIN Decoder Types
*
* Type definitions for VIN validation and decoding.
* No external dependencies - uses embedded NHTSA WMI data.
*/
/**
* VIN Validation Result
*/
export interface VinValidationResult {
/** Whether the VIN is valid */
valid: boolean;
/** VIN string (normalized) */
vin: string;
/** Check digit validation */
checkDigit: {
isValid: boolean;
expected?: string;
actual: string;
} | null;
/** Model year from VIN */
modelYear: number | null;
/** Detected manufacturer from WMI */
manufacturer: string | null;
/** Country of origin */
country: string | null;
/** Error messages if invalid */
errors: Array<{
code: number;
message: string;
severity: string;
}>;
}
/**
* WMI (World Manufacturer Identifier) Info
*/
export interface WmiInfo {
/** WMI code (first 3 chars of VIN) */
wmi: string;
/** Manufacturer name */
manufacturer: string | null;
/** Make/Brand */
make: string | null;
/** Country of origin */
country: string | null;
/** Region */
region: string | null;
}
/**
* Basic Vehicle Info
*/
export interface BasicVehicleInfo {
make: string | null;
model: string | null;
year: number | null;
bodyStyle: string | null;
driveType: string | null;
fuelType: string | null;
engineCylinders: string | null;
engineDisplacement: string | null;
}

View File

@@ -0,0 +1,7 @@
/**
* Corgi VIN Decoder Integration
*/
export * from './corgi.types';
export * from './corgi.service';
export * from './corgi.module';

Some files were not shown because too many files have changed in this diff Show More