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
JavaScript
;
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(/ /gi, ' ')
.replace(/&/gi, '&')
.replace(/</gi, '<')
.replace(/>/gi, '>')
.replace(/"/gi, '"')
.replace(/'/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