/** * EMEX Response Mapper * * Transforms raw EmexVinScraper responses into the standardized DecodedVehicle * format. Vehicle attribute translations (body type, engine type, transmission, * drive type) live here. Category and part name translations now live in * TranslationsService (apps/api/src/translations/translations.service.ts) — * mapCategories returns nameTr=null and the insertion path * (categories.service.ts) calls translationsService.translateMany() before * persisting. */ import { extractModelYear } from "@sase/shared"; import { CATALOG_MAP, type DecodedCategory, type DecodedVehicle, type EmexCategory, type EmexScraperResponse, } from "./emex.types"; // ==================== VEHICLE ATTRIBUTE TRANSLATIONS ==================== const TR_TRANSLATIONS = { // Body types bodyTypes: { sedan: "Sedan", coupe: "Coupe", hatchback: "Hatchback", wagon: "Station Wagon", "station wagon": "Station Wagon", estate: "Station Wagon", convertible: "Ustu Acik", cabriolet: "Kabriyole", suv: "SUV", crossover: "Crossover", pickup: "Pikap", van: "Minivan", minivan: "Minivan", mpv: "Cok Amacli Arac", roadster: "Roadster", } as Record, engineTypes: { gasoline: "Benzin", petrol: "Benzin", benzin: "Benzin", diesel: "Dizel", electric: "Elektrik", hybrid: "Hibrit", "plug-in hybrid": "Sarjli Hibrit", phev: "Sarjli Hibrit", lpg: "LPG", cng: "CNG", hydrogen: "Hidrojen", } as Record, transmissions: { automatic: "Otomatik", manual: "Manuel", "semi-automatic": "Yari Otomatik", dct: "Cift Kavramali", cvt: "CVT", "dual clutch": "Cift Kavramali", dsg: "DSG", tiptronic: "Tiptronic", steptronic: "Steptronic", at: "Otomatik", mt: "Manuel", } as Record, driveTypes: { fwd: "Ondan Cekis", rwd: "Arkadan Itis", awd: "Dort Ceker", "4wd": "Dort Ceker", "4x4": "Dort Ceker", "front-wheel drive": "Ondan Cekis", "rear-wheel drive": "Arkadan Itis", "all-wheel drive": "Dort Ceker", quattro: "Quattro (Dort Ceker)", xdrive: "xDrive (Dort Ceker)", "4matic": "4MATIC (Dort Ceker)", } as Record, }; // ==================== TRANSLATION HELPERS ==================== function translateToTurkish( term: string | null | undefined, dictionary: Record, ): string | null { if (!term) return null; const normalized = term.toLowerCase().trim(); // Dictionary miss → return original term unchanged. Upstream values are // often heterogeneous (engine codes, multi-word descriptors) that the // single-word dictionary can't cover; losing them would be worse than // leaving them untranslated. return dictionary[normalized] ?? term; } export function translateBodyType(bodyType: string | null): string | null { return translateToTurkish(bodyType, TR_TRANSLATIONS.bodyTypes); } export function translateEngineType(engineType: string | null): string | null { return translateToTurkish(engineType, TR_TRANSLATIONS.engineTypes); } export function translateTransmission(transmission: string | null): string | null { return translateToTurkish(transmission, TR_TRANSLATIONS.transmissions); } export function translateDriveType(driveType: string | null): string | null { return translateToTurkish(driveType, TR_TRANSLATIONS.driveTypes); } // ==================== MAPPER FUNCTIONS ==================== /** * Maps raw EMEX scraper response to standardized DecodedVehicle */ export function mapEmexResponse(response: EmexScraperResponse): DecodedVehicle { const vehicle = response.vehicle; // Get brand from catalog map or use the one from response const wmi = response.vin.substring(0, 3); const catalogEntry = CATALOG_MAP[wmi]; const brand = catalogEntry?.brand || vehicle.brand || "Unknown"; return { brand: brand.toUpperCase(), model: vehicle.model || "Unknown", year: vehicle.year || extractYearFromVin(response.vin), series: vehicle.series || null, bodyType: vehicle.bodyType || null, engineCode: vehicle.engineCode || null, engineType: vehicle.engineType || null, engineVolume: vehicle.engineVolume || null, transmission: vehicle.transmission || null, driveType: vehicle.driveType || null, colorCode: null, // EMEX doesn't provide color info raw: buildRawResponse(response), categories: mapCategories(response.categories), }; } /** * Extracts year from VIN (10th character) */ function extractYearFromVin(vin: string): number { // Servis yıl vermediğinde son çare; bilinmeyen kod için içinde bulunulan yıl. return extractModelYear(vin) ?? new Date().getFullYear(); } /** * Builds the raw response object for storage * Includes category URLs for on-demand parts fetching */ function buildRawResponse(response: EmexScraperResponse): Record { return { source: "emex", // Explicit source identifier for on-demand loading method: response.method, vin: response.vin, catalogCode: response.catalogCode, ssd: response.ssd, quickGroupsUrl: response.quickGroupsUrl, timestamp: response.timestamp, success: response.success, message: response.message, parsedOptions: response.parsedOptions, rawResponse: response.rawResponse, // Store category tree for hierarchical insertion (QuickGroups.aspx) emexCategoryTree: response.categoryTree || [], // Store flat category URLs for on-demand parts fetching (fallback) emexCategories: response.categories?.map((cat) => ({ gid: cat.gid, name: cat.name, url: cat.url, })) || [], }; } /** * Maps EMEX categories to standardized DecodedCategory format. * nameTr is left null on purpose — the consumer (categories.service.ts) * runs nameEn through TranslationsService.translateMany() before insert. * Parts are NOT included here; they are fetched on-demand when the user * clicks a category. */ function mapCategories(categories?: EmexCategory[]): DecodedCategory[] { if (!categories || categories.length === 0) { return []; } return categories.map((cat, index) => { return { code: cat.gid || `CAT_${index}`, nameEn: cat.name, nameTr: undefined, description: null, iconName: deriveIconName(cat.name), schemaImageUrl: null, parts: [], // Parts will be fetched on-demand }; }); } /** * Derives icon name from category name */ function deriveIconName(categoryName: string): string | null { const normalized = categoryName.toLowerCase(); const iconMap: Record = { engine: "engine", motor: "engine", brake: "brake", brakes: "brake", suspension: "suspension", steering: "steering", transmission: "transmission", gearbox: "transmission", exhaust: "exhaust", cooling: "cooling", electrical: "electrical", interior: "interior", exterior: "exterior", body: "body", lighting: "lighting", lights: "lighting", wheels: "wheels", fuel: "fuel", air: "air", climate: "climate", filters: "filters", }; for (const [key, icon] of Object.entries(iconMap)) { if (normalized.includes(key)) { return icon; } } return null; } /** * Creates an empty/default DecodedVehicle for error cases */ export function createEmptyDecodedVehicle(vin: string, errorMessage?: string): DecodedVehicle { const wmi = vin.substring(0, 3); const catalogEntry = CATALOG_MAP[wmi]; return { brand: catalogEntry?.brand?.toUpperCase() || "UNKNOWN", model: "Unknown", year: extractYearFromVin(vin), series: null, bodyType: null, engineCode: null, engineType: null, engineVolume: null, transmission: null, driveType: null, colorCode: null, raw: { vin, error: errorMessage || "Vehicle data not found", source: "emexdwc.ae", }, categories: [], }; }