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>
BIN
apps/api/public/images/schemas/05-05211.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
apps/api/public/images/schemas/05-052A11.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
apps/api/public/images/schemas/09-09111.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
apps/api/public/images/schemas/20-20111.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/api/public/images/schemas/20-201A11.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/api/public/images/schemas/20-20211.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/api/public/images/schemas/20-202A11.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/api/public/images/schemas/20-20312.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
apps/api/public/images/schemas/20-203A11.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/api/public/images/schemas/20-21111.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
apps/api/public/images/schemas/20-211A11.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/api/public/images/schemas/20-21311.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/api/public/images/schemas/20-213A11.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/api/public/images/schemas/20-21512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
apps/api/public/images/schemas/20-215A11.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/api/public/images/schemas/20-21611.png
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
apps/api/public/images/schemas/20-216A11.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
apps/api/public/images/schemas/20-22111.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/api/public/images/schemas/20-221A11.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/api/public/images/schemas/20-22412.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/api/public/images/schemas/20-224A11.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/api/public/images/schemas/41-410A11.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/api/public/images/schemas/41-41611.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
apps/api/public/images/schemas/43-430A11.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/api/public/images/schemas/43-431A11.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/api/public/images/schemas/43-432A12.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
apps/api/public/images/schemas/43-436A11.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
apps/api/public/images/schemas/43-437A12.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
apps/api/public/images/schemas/43-43811.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/api/public/images/schemas/43-43911.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
apps/api/public/images/schemas/43-45011.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/api/public/images/schemas/50-51711.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/api/public/images/schemas/50-52711.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/api/public/images/schemas/50-52911.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/api/public/images/schemas/54-54511.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
apps/api/public/images/schemas/54-54611.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/api/public/images/schemas/54-54811.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
apps/api/public/images/schemas/54-55311.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/api/public/images/schemas/54-55511.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/api/public/images/schemas/56-56111.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/api/public/images/schemas/56-56311.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/api/public/images/schemas/56-56911.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/api/public/images/schemas/60-62411.png
Normal file
|
After Width: | Height: | Size: 317 KiB |
BIN
apps/api/public/images/schemas/60-62412.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
apps/api/public/images/schemas/60-64011.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/api/public/images/schemas/60-65111.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
apps/api/public/images/schemas/60-65112.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
apps/api/public/images/schemas/60-66011.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/api/public/images/schemas/60-66711.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/api/public/images/schemas/60-67111.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/api/public/images/schemas/60-69011.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/api/public/images/schemas/60-71011.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/api/public/images/schemas/60-71012.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
apps/api/public/images/schemas/60-76011.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/api/public/images/schemas/81-81012.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
apps/api/public/images/schemas/81-81111.png
Normal file
|
After Width: | Height: | Size: 318 KiB |
BIN
apps/api/public/images/schemas/81-81311.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/api/public/images/schemas/81-81411.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/api/public/images/schemas/81-81512.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/api/public/images/schemas/81-81612.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
apps/api/public/images/schemas/81-81912.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
apps/api/public/images/schemas/81-82111.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/api/public/images/schemas/81-82312.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
apps/api/public/images/schemas/81-82411.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
apps/api/public/images/schemas/91-91111.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/api/public/images/schemas/91-91212.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/api/public/images/schemas/91-91311.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/api/public/images/schemas/91-91411.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/api/public/images/schemas/91-91511.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/api/public/images/schemas/91-91611.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/api/public/images/schemas/91-91811.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/api/public/images/schemas/91-91911.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
apps/api/public/images/schemas/91-92111.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/api/public/images/schemas/91-92311.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/api/public/images/schemas/91-92411.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
apps/api/public/images/schemas/91-92511.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
apps/api/public/images/schemas/C16HDEZ3030105A.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
apps/api/public/images/schemas/CAK1000105.png
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
apps/api/public/images/schemas/CAK2040105.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
apps/api/public/images/schemas/CAK4120105.png
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
apps/api/public/images/schemas/CAK5010110.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
apps/api/public/images/schemas/CIB53070305A.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
apps/api/public/images/schemas/TKE1000105A.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
apps/api/public/images/schemas/TKE2040105.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
apps/api/public/images/schemas/TKE4120105B.png
Normal file
|
After Width: | Height: | Size: 493 KiB |
BIN
apps/api/public/images/schemas/TKE5010110A.png
Normal file
|
After Width: | Height: | Size: 499 KiB |
BIN
apps/api/public/images/schemas/TUNIV3070705A.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
apps/api/public/images/schemas/ZEA02030.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
apps/api/public/images/schemas/ZEB02005.png
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
apps/api/public/images/schemas/ZED03010.png
Normal file
|
After Width: | Height: | Size: 950 KiB |
BIN
apps/api/public/images/schemas/ZEE01505.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
apps/api/public/images/schemas/ZEF01001.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
apps/api/public/images/schemas/ZEG01015.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
BIN
apps/api/public/images/schemas/ZEH05040.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
apps/api/public/images/schemas/ZFA01020.png
Normal file
|
After Width: | Height: | Size: 818 KiB |
492
apps/api/scripts/fetch-nhtsa-wmi.ts
Normal 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);
|
||||
17
apps/api/src/integrations/corgi/corgi.module.ts
Normal 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 {}
|
||||
273
apps/api/src/integrations/corgi/corgi.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
64
apps/api/src/integrations/corgi/corgi.types.ts
Normal 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;
|
||||
}
|
||||
7
apps/api/src/integrations/corgi/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Corgi VIN Decoder Integration
|
||||
*/
|
||||
|
||||
export * from './corgi.types';
|
||||
export * from './corgi.service';
|
||||
export * from './corgi.module';
|
||||