UNPKG

n8n-nodes-immometrica-v2

Version:

n8n community node for ImmoMetrica real estate data extraction with CSV-based approach and agent tool compatibility

660 lines 37.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ErrorHandler = exports.DataValidator = exports.CSVProcessor = exports.filterOptionsResource = exports.enableDetailScrapingResource = exports.maxResultsResource = exports.searchResource = void 0; exports.searchResource = { displayName: 'Search', name: 'searchId', type: 'options', noDataExpression: true, typeOptions: { loadOptionsMethod: 'getSavedSearches', }, default: '', required: true, description: 'Select a saved search from ImmoMetrica', }; exports.maxResultsResource = { displayName: 'Max Results', name: 'maxResults', type: 'number', default: 50, description: 'Maximum number of properties to extract (up to 1000)', typeOptions: { minValue: 1, maxValue: 1000, }, }; exports.enableDetailScrapingResource = { displayName: 'Enable Detail Scraping', name: 'enableDetailScraping', type: 'boolean', default: false, description: 'Extract additional details from individual property pages (slower but more comprehensive)', }; exports.filterOptionsResource = { displayName: 'Filter Options', name: 'filterOptions', type: 'collection', placeholder: 'Add Filter', default: {}, options: [ { displayName: 'Min ROI (%)', name: 'minRoi', type: 'number', default: 0, description: 'Minimum expected yield percentage', typeOptions: { numberPrecision: 2, }, }, { displayName: 'Max ROI (%)', name: 'maxRoi', type: 'number', default: 100, description: 'Maximum expected yield percentage', typeOptions: { numberPrecision: 2, }, }, { displayName: 'Min Price (€)', name: 'minPrice', type: 'number', default: 0, description: 'Minimum purchase price in euros', }, { displayName: 'Max Price (€)', name: 'maxPrice', type: 'number', default: 10000000, description: 'Maximum purchase price in euros', }, { displayName: 'Location Filter', name: 'locationFilter', type: 'string', default: '', placeholder: 'Berlin, München, Hamburg', description: 'Comma-separated list of locations to include (partial matching)', }, { displayName: 'Positive Cashflow Only', name: 'positiveCashflowOnly', type: 'boolean', default: false, description: 'Only include properties with positive monthly cashflow', }, { displayName: 'Property Types', name: 'propertyTypes', type: 'multiOptions', default: [], options: [ { name: 'Mehrfamilienhaus', value: 'mehrfamilienhaus', }, { name: 'Einfamilienhaus', value: 'einfamilienhaus', }, { name: 'Eigentumswohnung', value: 'eigentumswohnung', }, { name: 'Gewerbeimmobilie', value: 'gewerbeimmobilie', }, { name: 'Grundstück', value: 'grundstueck', }, ], description: 'Filter by specific property types', }, ], }; class CSVProcessor { static parseCSV(csvContent, searchName = '', searchId = '') { const properties = []; const lines = csvContent.split('\n').filter(line => line.trim()); console.log(`[CSV DEBUG] Total lines: ${lines.length}`); if (lines.length > 0) { console.log(`[CSV DEBUG] First line (headers): ${lines[0].substring(0, 200)}...`); } if (lines.length < 2) { console.log('[CSV DEBUG] Not enough lines for parsing'); return properties; } let headerLine = lines[0]; if (headerLine.charCodeAt(0) === 0xFEFF) { headerLine = headerLine.slice(1); } let headers = headerLine.split(';').map(h => h.trim().replace(/"/g, '')); const midPoint = Math.floor(headers.length / 2); const firstHalf = headers.slice(0, midPoint); const secondHalf = headers.slice(midPoint); const isDuplicated = firstHalf.length === secondHalf.length && firstHalf.every((header, index) => header === secondHalf[index]); if (isDuplicated) { headers = firstHalf; console.log(`[CSV DEBUG] Detected duplicate headers, using first half: ${headers.length} headers`); } console.log(`[CSV DEBUG] Headers count: ${headers.length}`); console.log(`[CSV DEBUG] All headers: ${headers.join(', ')}`); console.log(`[CSV DEBUG] Key headers found: PLZ=${headers.includes('PLZ')}, Adresse=${headers.includes('Adresse')}, Preis/m²=${headers.includes('Preis/m²')}, Titel=${headers.includes('Titel')}`); for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const values = this.parseCSVLine(line, ';'); console.log(`[CSV DEBUG] Line ${i}: ${values.length} values, headers: ${headers.length}`); if (values.length < headers.length) { console.log(`[CSV DEBUG] Skipping line ${i}: not enough values (${values.length} < ${headers.length})`); continue; } const property = this.mapCSVRowToProperty(headers, values, searchName, searchId); const isValid = this.validatePropertyData(property); if (isValid) { properties.push(property); } } console.log(`[CSV DEBUG] Total properties extracted: ${properties.length}`); const cleanedProperties = this.cleanAndValidateCSVData(properties); return cleanedProperties; } static parseCSVLine(line, delimiter = ';') { const values = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { inQuotes = !inQuotes; } else if (char === delimiter && !inQuotes) { values.push(current.trim()); current = ''; } else { current += char; } } values.push(current.trim()); return values; } static mapCSVRowToProperty(headers, values, searchName, searchId) { console.log(`[CSV MAPPING] Starting property mapping with ${headers.length} headers and ${values.length} values`); const property = { address: this.getCSVValue(headers, values, ['Adresse']) || '', city: this.getCSVValue(headers, values, ['Ort']) || '', postalCode: this.getCSVValue(headers, values, ['PLZ']) || '', title: this.getCSVValue(headers, values, ['Titel', 'Title', 'Bezeichnung', 'Name']) || 'Immobilie', propertyType: this.derivePropertyType(this.getCSVValue(headers, values, ['Quelle']) || ''), buildingType: this.getCSVValue(headers, values, ['Haustyp']) || this.deriveBuildingType(this.getCSVValue(headers, values, ['Zi']) || '0'), purchasePrice: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Preis']) || '0'), pricePerSqm: this.parseGermanNumber(this.getCSVValue(headers, values, ['Preis/m²']) || '0'), livingAreaSqm: this.parseGermanArea(this.getCSVValue(headers, values, ['Wfl.']) || '0'), rooms: this.parseGermanNumber(this.getCSVValue(headers, values, ['Zi']) || '0'), yearBuilt: parseInt(this.getCSVValue(headers, values, ['Bj']) || '0', 10) || 0, condition: this.getCSVValue(headers, values, ['Zustand', 'Condition', 'Bauzustand']) || '', landAreaSqm: this.parseGermanArea(this.getCSVValue(headers, values, ['Grundstfl.']) || '0'), expectedRentPerMonth: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Miete', 'Erwartete Miete/Monat', 'Miete/Monat', 'Miete (soll)', 'Soll-Miete']) || '0'), expectedRentPerSqm: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Miete/m²', 'Erwartete Miete/m²', 'Miete/m² (soll)', 'Soll-Miete/m²']) || '0'), expectedYieldPercent: this.parseGermanNumber(this.getCSVValue(headers, values, ['ROI (i)', 'ROI (s)', 'Erwartete Rendite', 'Rendite', 'Rendite (soll)', 'Soll-Rendite']) || '0'), actualYieldPercent: this.parseGermanNumber(this.getCSVValue(headers, values, ['ROI (i)', 'Ist-Rendite', 'Aktuelle Rendite', 'Rendite (ist)']) || '0'), estimatedRentSoll: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Miete (soll)', 'Soll-Miete', 'Geschätzte Miete', 'Erwartete Miete']) || '0'), estimatedRentPerSqmSoll: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Miete/m² (soll)', 'Soll-Miete/m²', 'Geschätzte Miete/m²']) || '0'), marketValuationEstimatePerSqm: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Marktwert/m²', 'Geschätzter Wert/m²', 'Wertschätzung/m²']) || '0'), immometricaMarktumfeldRenditeSoll: this.parseGermanNumber(this.getCSVValue(headers, values, ['Marktumfeld Rendite (soll)', 'Durchschnittsrendite', 'Regionale Rendite (soll)']) || '0'), immometricaMarktumfeldRenditeIst: this.parseGermanNumber(this.getCSVValue(headers, values, ['Marktumfeld Rendite (ist)', 'Aktuelle Marktrendite', 'Regionale Rendite (ist)']) || '0'), immometricaMarktumfeldMieteSoll: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Marktumfeld Miete (soll)', 'Durchschnittsmiete', 'Regionale Miete (soll)']) || '0'), immometricaMarktumfeldMieteIst: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Marktumfeld Miete (ist)', 'Aktuelle Marktmiete', 'Regionale Miete (ist)']) || '0'), marketValuation: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Markt-wert', 'Marktwert', 'Geschätzter Wert', 'Immobilienwert']) || '0'), marketValuationPerSqm: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Markt-wert/m²', 'Marktwert/m²', 'Geschätzter Wert/m²', 'Wert/m²']) || '0'), marketDeviationPercent: this.parseGermanNumber(this.getCSVValue(headers, values, ['Markt-wert %', 'Marktwert %', 'Abweichung %', 'Marktabweichung']) || '0'), regionalAverageYield: this.parseGermanNumber(this.getCSVValue(headers, values, ['Regionale Durchschnittsrendite', 'Durchschnittsrendite', 'Regionale Rendite', 'Marktrendite']) || '0'), regionalPricePerSqm: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Regionaler Preis/m²', 'Durchschnittspreis/m²', 'Marktpreis/m²', 'Regionale Preise/m²']) || '0'), regionalMarketDeviationPercent: this.parseGermanNumber(this.getCSVValue(headers, values, ['Regionale Marktabweichung', 'Regionale Abweichung', 'Marktabweichung %', 'Preisabweichung']) || '0'), commissionPercent: this.parseGermanNumber(this.getCSVValue(headers, values, ['Provision']) || '0'), commissionText: this.getCSVValue(headers, values, ['Provision Text']) || '', availableFrom: this.getCSVValue(headers, values, ['Verfügbar ab']) || '', privateSale: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Privat'])), isRented: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Vermietet'])), isForeclosure: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Zwangsversteigerung'])), hasTerrace: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Terrasse'])), hasGarden: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Garten'])), hasGuestToilet: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Gäste-WC'])), hasBasement: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Keller'])), energyCertificateAvailable: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Energieausweis'])), energySource: this.getCSVValue(headers, values, ['Energiequelle']) || '', heatingType: this.getCSVValue(headers, values, ['Heizungsart']) || '', firstSeenDate: this.parseDate(this.getCSVValue(headers, values, ['Datum'])) || new Date().toISOString().split('T')[0], daysOnline: parseInt(this.getCSVValue(headers, values, ['Tage online']) || '0', 10) || 0, maintenanceCosts: this.parseGermanCurrency(this.getCSVValue(headers, values, ['Hausgeld']) || '0'), grossYieldPercent: this.parseGermanNumber(this.getCSVValue(headers, values, ['ROI (s)']) || '0'), trend: this.getCSVValue(headers, values, ['Trend']) || '', isNew: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Neu'])), isFavorite: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Favorit'])), isHidden: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Ausgeblendet'])), isSeen: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Gesehen'])), isActive: this.parseGermanBoolean(this.getCSVValue(headers, values, ['Aktiv'])), platformLinks: {}, sourcePlatform: this.getCSVValue(headers, values, ['Quelle']) || '', notes: this.getCSVValue(headers, values, ['Notizen', 'Notes', 'Notiz']) || 'Notiz hinzufügen', immometricaLink: '', sourceSearchName: searchName, sourceSearchId: searchId, scrapeTimestamp: new Date().toISOString(), }; property.isPrivateSale = this.parseGermanBoolean(this.getCSVValue(headers, values, ['Privat'])); property.currency = this.getCSVValue(headers, values, ['Währung']) || 'EUR'; property.cashFlow = this.parseGermanCurrency(this.getCSVValue(headers, values, ['Cash-flow']) || '0'); property.sourcePlatform = this.getCSVValue(headers, values, ['Quelle']) || ''; property.csvPlatformLinks = { immoscout: this.getCSVValue(headers, values, ['Link ImmoScout', 'ImmoScout24', 'ImmoScout', 'Scout24']) || '', kleinanzeigen: this.getCSVValue(headers, values, ['Link Kleinanzeigen', 'Kleinanzeigen', 'eBay Kleinanzeigen']) || '', immonet: this.getCSVValue(headers, values, ['Link immonet', 'Immonet', 'immonet.de']) || '', immowelt: this.getCSVValue(headers, values, ['Link immowelt', 'Immowelt', 'immowelt.de']) || '', ohneMakler: this.getCSVValue(headers, values, ['Link Ohne Makler', 'Ohne Makler', 'ohneMakler']) || '', wohnungJetzt: this.getCSVValue(headers, values, ['Link Wohnung Jetzt', 'Wohnung Jetzt']) || '', regionalimmobilien: this.getCSVValue(headers, values, ['Link Regionalimmobilien', 'Regionalimmobilien']) || '', zvg24: this.getCSVValue(headers, values, ['Link ZVG24', 'ZVG24', 'ZVG']) || '', homegate: this.getCSVValue(headers, values, ['Link Homegate', 'Homegate']) || '', newhome: this.getCSVValue(headers, values, ['Link newhome', 'newhome', 'NewHome']) || '', flatfox: this.getCSVValue(headers, values, ['Link Flatfox', 'Flatfox']) || '', willhaben: this.getCSVValue(headers, values, ['Link willhaben', 'willhaben']) || '', immoscoutat: this.getCSVValue(headers, values, ['Link immoscoutat', 'immoscout.at']) || '', derstandard: this.getCSVValue(headers, values, ['Link derstandard', 'derstandard.at']) || '', makler: this.getCSVValue(headers, values, ['Link Makler', 'Makler']) || '', zeitungen: this.getCSVValue(headers, values, ['Link Zeitungen und sonstige', 'Zeitungen', 'Sonstige']) || '' }; if (property.expectedRentPerMonth > 0 && property.livingAreaSqm > 0 && property.expectedRentPerSqm === 0) { property.expectedRentPerSqm = Math.round((property.expectedRentPerMonth / property.livingAreaSqm) * 100) / 100; } if (property.estimatedRentSoll > 0 && property.livingAreaSqm > 0 && property.estimatedRentPerSqmSoll === 0) { property.estimatedRentPerSqmSoll = Math.round((property.estimatedRentSoll / property.livingAreaSqm) * 100) / 100; } if (property.marketValuation > 0 && property.livingAreaSqm > 0 && property.marketValuationPerSqm === 0) { property.marketValuationPerSqm = Math.round((property.marketValuation / property.livingAreaSqm) * 100) / 100; } if (property.marketValuation > 0 && property.livingAreaSqm > 0 && property.marketValuationEstimatePerSqm === 0) { property.marketValuationEstimatePerSqm = Math.round((property.marketValuation / property.livingAreaSqm) * 100) / 100; } const detailsValue = this.getCSVValue(headers, values, ['Details']) || ''; console.log(`[CSV DEBUG] Details field content: "${detailsValue}"`); if (detailsValue && detailsValue.includes('http')) { const urls = detailsValue.match(/(https?:\/\/[^\s,;]+)/g) || []; console.log(`[CSV DEBUG] Extracted URLs: ${JSON.stringify(urls)}`); const immometricaUrl = urls.find(url => url.includes('immometrica.com')); if (immometricaUrl) { property.immometricaLink = immometricaUrl; } else if (urls.length > 0) { property.immometricaLink = urls[0]; } else { property.immometricaLink = `https://www.immometrica.com/de/search/${searchId}/`; } urls.forEach(url => { if (url.includes('immoscout') || url.includes('scout24')) { property.platformLinks.immoscout = url; } else if (url.includes('immowelt')) { property.platformLinks.immowelt = url; } else if (url.includes('immonet')) { property.platformLinks.immonet = url; } else if (url.includes('kleinanzeigen') || url.includes('ebay-kleinanzeigen')) { property.platformLinks.kleinanzeigen = url; } }); } else { console.log(`[CSV DEBUG] Attempting to construct URL from search context for ${property.city}`); property.immometricaLink = `https://www.immometrica.com/de/search/${searchId}/`; } property.platformLinks = { ...property.platformLinks, ...Object.fromEntries(Object.entries(property.csvPlatformLinks).filter(([key, value]) => value !== undefined && value !== '')) }; console.log(`[CSV MAPPING] Completed property mapping for: ${property.city || property.address}`); return property; } static getCSVValue(headers, values, possibleNames) { const decodeHTML = (str) => { return str .replace(/<br\s*\/?>/gi, '') .replace(/&nbsp;/gi, ' ') .replace(/&amp;/gi, '&') .replace(/&lt;/gi, '<') .replace(/&gt;/gi, '>') .replace(/&quot;/gi, '"') .replace(/&#39;/gi, "'") .trim(); }; for (let i = 0; i < headers.length; i++) { const header = decodeHTML(headers[i]).toLowerCase().trim(); for (const name of possibleNames) { const normalizedName = name.toLowerCase().trim(); if (header === normalizedName) { if (values[i] && values[i].trim() !== '') { console.log(`[CSV MAPPING] Found exact match: "${name}" -> "${headers[i]}" (index: ${i}) = "${values[i]}"`); return values[i].replace(/"/g, '').trim(); } } } } for (let i = 0; i < headers.length; i++) { const header = decodeHTML(headers[i]).toLowerCase().trim(); for (const name of possibleNames) { const normalizedName = name.toLowerCase().trim(); if (header !== normalizedName && header.includes(normalizedName) && normalizedName.length >= 4) { if (values[i] && values[i].trim() !== '') { console.log(`[CSV MAPPING] Found partial match: "${name}" -> "${headers[i]}" (index: ${i}) = "${values[i]}"`); return values[i].replace(/"/g, '').trim(); } } } } return undefined; } static parseDate(value) { if (!value) return undefined; const germanDateMatch = value.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/); if (germanDateMatch) { const [, day, month, year] = germanDateMatch; return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; } const isoDateMatch = value.match(/(\d{4})-(\d{1,2})-(\d{1,2})/); if (isoDateMatch) { return value; } return undefined; } static parseGermanNumber(value) { if (!value || typeof value !== 'string') return 0; const cleaned = value .replace(/[^\d,.-]/g, '') .replace(/\./g, '') .replace(',', '.'); const parsed = parseFloat(cleaned); return isNaN(parsed) ? 0 : parsed; } static parseGermanCurrency(value) { if (!value || typeof value !== 'string') return 0; const cleaned = value .replace(/€/g, '') .replace(/EUR/g, '') .replace(/\s/g, '') .replace(/\./g, '') .replace(',', '.'); const parsed = parseFloat(cleaned); return isNaN(parsed) ? 0 : parsed; } static parseGermanArea(value) { if (!value || typeof value !== 'string') return 0; const cleaned = value .replace(/m²/g, '') .replace(/qm/g, '') .replace(/\s/g, '') .replace(/\./g, '') .replace(',', '.'); const parsed = parseFloat(cleaned); return isNaN(parsed) ? 0 : parsed; } static parseGermanBoolean(value) { if (!value) return false; const lower = value.toLowerCase().trim(); return lower === 'ja' || lower === 'yes' || lower === 'true' || lower === '1' || lower === 'vorhanden'; } static normalizePropertyType(value) { if (!value) return ''; const normalized = value.toLowerCase().trim(); const mappings = { 'mfh': 'Mehrfamilienhaus', 'mehrfamilienhaus': 'Mehrfamilienhaus', 'efh': 'Einfamilienhaus', 'einfamilienhaus': 'Einfamilienhaus', 'etw': 'Eigentumswohnung', 'eigentumswohnung': 'Eigentumswohnung', 'wohnung': 'Eigentumswohnung', 'gewerbe': 'Gewerbeimmobilie', 'gewerbeimmobilie': 'Gewerbeimmobilie', 'grundstück': 'Grundstück', 'bauland': 'Grundstück', }; return mappings[normalized] || value; } static derivePropertyType(sourcePlatform) { if (!sourcePlatform) return 'Wohnimmobilie'; const platform = sourcePlatform.toLowerCase(); if (platform.includes('immoscout') || platform.includes('scout')) { return 'Wohnimmobilie'; } else if (platform.includes('immowelt')) { return 'Wohnimmobilie'; } else if (platform.includes('immonet')) { return 'Wohnimmobilie'; } return 'Wohnimmobilie'; } static deriveBuildingType(rooms) { const roomCount = this.parseGermanNumber(rooms); if (roomCount >= 6) { return 'Mehrfamilienhaus'; } else if (roomCount >= 4) { return 'Einfamilienhaus'; } else if (roomCount >= 1) { return 'Wohnung'; } return 'Unbekannt'; } static extractIdFromUrl(url) { if (!url) return ''; const matches = url.match(/\/(\d+)(?:\?|$)/); return matches ? matches[1] : url.split('/').pop() || ''; } static extractPropertyIdFromCSV(headers, values) { const idFields = ['ID', 'Property ID', 'Objekt ID', 'ImmoMetrica ID', 'Offer ID']; for (const field of idFields) { const value = this.getCSVValue(headers, values, [field]); if (value && /^\d+$/.test(value)) { return value; } } const urlFields = ['Link', 'URL', 'Details URL', 'ImmoMetrica Link', 'Details']; for (const field of urlFields) { const value = this.getCSVValue(headers, values, [field]); if (value) { const id = this.extractIdFromUrl(value); if (id && /^\d+$/.test(id)) return id; } } const platformFields = ['ImmoScout', 'Immowelt', 'Platform Links']; for (const field of platformFields) { const value = this.getCSVValue(headers, values, [field]); if (value && value.includes('expose/')) { const matches = value.match(/expose\/(\d+)/); if (matches) return matches[1]; } } return null; } static deriveConditionFromBuildingData(yearBuilt, buildingType) { const year = parseInt(yearBuilt, 10); const currentYear = new Date().getFullYear(); if (!year || year < 1800) return ''; const age = currentYear - year; if (age <= 5) return 'Neuwertig'; if (age <= 15) return 'Gepflegt'; if (age <= 30) return 'Modernisiert'; if (age <= 50) return 'Renovierungsbedürftig'; return 'Sanierungsbedürftig'; } static generateFallbackPropertyId(property, searchId) { const cityHash = property.city ? property.city.toLowerCase().replace(/[^a-z0-9]/g, '').substring(0, 6) : ''; const priceHash = property.purchasePrice ? String(property.purchasePrice).substring(0, 4) : ''; const areaHash = property.livingAreaSqm ? String(property.livingAreaSqm).substring(0, 3) : ''; if (cityHash && priceHash && areaHash) { const hash = `${cityHash}${priceHash}${areaHash}${searchId}`.replace(/[^a-z0-9]/g, ''); let numericId = ''; for (let i = 0; i < hash.length && numericId.length < 8; i++) { const charCode = hash.charCodeAt(i); numericId += String(charCode % 10); } return numericId.padStart(8, '1'); } return null; } static validatePropertyData(property) { if (!property) return false; const hasLocation = Boolean((property.address && property.address !== '') || (property.city && property.city !== '')); const hasPurchasePrice = Boolean(property.purchasePrice !== undefined && property.purchasePrice !== '' && property.purchasePrice !== 0); return hasLocation && hasPurchasePrice; } static cleanAndValidateCSVData(rawData) { return rawData .filter(CSVProcessor.validatePropertyData) .map(property => ({ ...property, purchasePrice: typeof property.purchasePrice === 'number' ? property.purchasePrice : this.parseGermanCurrency(property.purchasePrice?.toString() || '0'), pricePerSqm: typeof property.pricePerSqm === 'number' ? property.pricePerSqm : this.parseGermanNumber(property.pricePerSqm?.toString() || '0'), livingAreaSqm: typeof property.livingAreaSqm === 'number' ? property.livingAreaSqm : this.parseGermanArea(property.livingAreaSqm?.toString() || '0'), landAreaSqm: typeof property.landAreaSqm === 'number' ? property.landAreaSqm : this.parseGermanArea(property.landAreaSqm?.toString() || '0'), rooms: typeof property.rooms === 'number' ? property.rooms : this.parseGermanNumber(property.rooms?.toString() || '0'), yearBuilt: typeof property.yearBuilt === 'number' ? property.yearBuilt : parseInt(property.yearBuilt?.toString() || '0', 10) || 0, expectedRentPerMonth: typeof property.expectedRentPerMonth === 'number' ? property.expectedRentPerMonth : this.parseGermanCurrency(property.expectedRentPerMonth?.toString() || '0'), expectedRentPerSqm: typeof property.expectedRentPerSqm === 'number' ? property.expectedRentPerSqm : this.parseGermanCurrency(property.expectedRentPerSqm?.toString() || '0'), expectedYieldPercent: typeof property.expectedYieldPercent === 'number' ? property.expectedYieldPercent : this.parseGermanNumber(property.expectedYieldPercent?.toString() || '0'), marketValuation: typeof property.marketValuation === 'number' ? property.marketValuation : this.parseGermanCurrency(property.marketValuation?.toString() || '0'), marketValuationPerSqm: typeof property.marketValuationPerSqm === 'number' ? property.marketValuationPerSqm : this.parseGermanCurrency(property.marketValuationPerSqm?.toString() || '0'), marketDeviationPercent: typeof property.marketDeviationPercent === 'number' ? property.marketDeviationPercent : this.parseGermanNumber(property.marketDeviationPercent?.toString() || '0'), regionalAverageYield: typeof property.regionalAverageYield === 'number' ? property.regionalAverageYield : this.parseGermanNumber(property.regionalAverageYield?.toString() || '0'), regionalPricePerSqm: typeof property.regionalPricePerSqm === 'number' ? property.regionalPricePerSqm : this.parseGermanCurrency(property.regionalPricePerSqm?.toString() || '0'), regionalMarketDeviationPercent: typeof property.regionalMarketDeviationPercent === 'number' ? property.regionalMarketDeviationPercent : this.parseGermanNumber(property.regionalMarketDeviationPercent?.toString() || '0'), commissionPercent: typeof property.commissionPercent === 'number' ? property.commissionPercent : this.parseGermanNumber(property.commissionPercent?.toString() || '0'), daysOnline: typeof property.daysOnline === 'number' ? property.daysOnline : parseInt(property.daysOnline?.toString() || '0', 10) || 0, maintenanceCosts: typeof property.maintenanceCosts === 'number' ? property.maintenanceCosts : this.parseGermanCurrency(property.maintenanceCosts?.toString() || '0'), grossYieldPercent: typeof property.grossYieldPercent === 'number' ? property.grossYieldPercent : this.parseGermanNumber(property.grossYieldPercent?.toString() || '0'), propertyType: this.normalizePropertyType(property.propertyType?.toString() || ''), address: property.address?.toString() || '', city: property.city?.toString() || '', postalCode: property.postalCode?.toString() || '', title: property.title?.toString() || '', buildingType: property.buildingType?.toString() || '', condition: property.condition?.toString() || '', trend: property.trend?.toString() || '', availableFrom: property.availableFrom?.toString() || '', commissionText: property.commissionText?.toString() || '', energySource: property.energySource?.toString() || '', heatingType: property.heatingType?.toString() || '', sourcePlatform: property.sourcePlatform?.toString() || '', notes: property.notes?.toString() || '', privateSale: typeof property.privateSale === 'boolean' ? property.privateSale : this.parseGermanBoolean(property.privateSale?.toString()), isRented: typeof property.isRented === 'boolean' ? property.isRented : this.parseGermanBoolean(property.isRented?.toString()), isForeclosure: typeof property.isForeclosure === 'boolean' ? property.isForeclosure : this.parseGermanBoolean(property.isForeclosure?.toString()), hasTerrace: typeof property.hasTerrace === 'boolean' ? property.hasTerrace : this.parseGermanBoolean(property.hasTerrace?.toString()), hasGarden: typeof property.hasGarden === 'boolean' ? property.hasGarden : this.parseGermanBoolean(property.hasGarden?.toString()), hasGuestToilet: typeof property.hasGuestToilet === 'boolean' ? property.hasGuestToilet : this.parseGermanBoolean(property.hasGuestToilet?.toString()), hasBasement: typeof property.hasBasement === 'boolean' ? property.hasBasement : this.parseGermanBoolean(property.hasBasement?.toString()), energyCertificateAvailable: typeof property.energyCertificateAvailable === 'boolean' ? property.energyCertificateAvailable : this.parseGermanBoolean(property.energyCertificateAvailable?.toString()), isNew: typeof property.isNew === 'boolean' ? property.isNew : this.parseGermanBoolean(property.isNew?.toString()), isFavorite: typeof property.isFavorite === 'boolean' ? property.isFavorite : this.parseGermanBoolean(property.isFavorite?.toString()), isHidden: typeof property.isHidden === 'boolean' ? property.isHidden : this.parseGermanBoolean(property.isHidden?.toString()), isSeen: typeof property.isSeen === 'boolean' ? property.isSeen : this.parseGermanBoolean(property.isSeen?.toString()), isActive: typeof property.isActive === 'boolean' ? property.isActive : this.parseGermanBoolean(property.isActive?.toString()), platformLinks: { ...property.platformLinks, ...Object.fromEntries(Object.entries(property.csvPlatformLinks || {}).filter(([key, value]) => value !== undefined && value !== '')) }, immometricaLink: property.immometricaLink?.toString() || '', sourceSearchName: property.sourceSearchName?.toString() || '', sourceSearchId: property.sourceSearchId?.toString() || '', scrapeTimestamp: property.scrapeTimestamp || new Date().toISOString(), firstSeenDate: property.firstSeenDate || new Date().toISOString().split('T')[0], })); } } exports.CSVProcessor = CSVProcessor; class DataValidator { static validateSearchId(searchId) { return typeof searchId === 'string' && searchId.length > 0 && /^\d+$/.test(searchId); } static validateMaxResults(maxResults) { return typeof maxResults === 'number' && maxResults > 0 && maxResults <= 1000; } static validateFilterOptions(filterOptions) { if (!filterOptions || typeof filterOptions !== 'object') return true; if (filterOptions.minRoi !== undefined && (typeof filterOptions.minRoi !== 'number' || filterOptions.minRoi < 0)) { return false; } if (filterOptions.maxRoi !== undefined && (typeof filterOptions.maxRoi !== 'number' || filterOptions.maxRoi > 100)) { return false; } if (filterOptions.minPrice !== undefined && (typeof filterOptions.minPrice !== 'number' || filterOptions.minPrice < 0)) { return false; } if (filterOptions.maxPrice !== undefined && (typeof filterOptions.maxPrice !== 'number' || filterOptions.maxPrice < 0)) { return false; } return true; } } exports.DataValidator = DataValidator; class ErrorHandler { static handleNetworkError(error) { if (error.message?.includes('net::ERR_INTERNET_DISCONNECTED')) { return new Error('Internetverbindung unterbrochen. Bitte überprüfen Sie Ihre Netzwerkverbindung.'); } if (error.message?.includes('net::ERR_NAME_NOT_RESOLVED')) { return new Error('ImmoMetrica-Server nicht erreichbar. Bitte versuchen Sie es später erneut.'); } if (error.message?.includes('timeout')) { return new Error('Zeitüberschreitung beim Laden der Seite. Bitte versuchen Sie es erneut.'); } return new Error(`Netzwerkfehler: ${error.message}`); } static handleParsingError(error, context) { return new Error(`Fehler beim Parsen der Daten (${context}): ${error.message}`); } static handleAuthenticationError(error) { if (error.message?.includes('Login fehlgeschlagen')) { return new Error('Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldedaten in den Credentials.'); } return new Error(`Authentifizierungsfehler: ${error.message}`); } static handleWebDriverError(error) { if (error.message?.includes('chrome') || error.message?.includes('driver')) { return new Error(`Chrome WebDriver Fehler: ${error.message}. Stellen Sie sicher, dass Chrome in Ihrer n8n-Umgebung installiert ist.`); } return new Error(`WebDriver Fehler: ${error.message}`); } } exports.ErrorHandler = ErrorHandler; //# sourceMappingURL=ImmoMetricaSearch.resource.js.map