diff --git a/backend/src/tools/koepi/routes.js b/backend/src/tools/koepi/routes.js index cc99a7d..22e8b24 100644 --- a/backend/src/tools/koepi/routes.js +++ b/backend/src/tools/koepi/routes.js @@ -41,6 +41,29 @@ function getAddress(retailer) { return ''; } +// Straßennamen robust vergleichbar machen (Groß-/Kleinschreibung, "straße"/"str.", +// Leerzeichen/Punkte egal — Hausnummer bleibt Teil des Vergleichs) +function normalizeAddress(s) { + return (s || '') + .toLowerCase() + .replace(/straße|strasse/g, 'str') + .replace(/[^a-zäöüß0-9]/g, ''); +} + +// Prüft, ob die tatsächlich gescrapte Adresse zu einer der bekannten LOKALEN +// Filialen für diesen Händler passt. Ohne erkannten Händler oder ohne gescrapte +// Adresse wird NICHT gefiltert (lieber ein Angebot zuviel zeigen als eins zu +// Unrecht verstecken, falls marktguru das Layout mal ändert). +function isLocalBranch(retailer, realAddress) { + const n = (retailer || '').toLowerCase(); + const key = Object.keys(RETAILER_ADDRESSES).find(k => n.includes(k)); + if (!key) return true; + if (!realAddress) return true; + const expectedParts = RETAILER_ADDRESSES[key].split('/').map(p => normalizeAddress(p.trim())); + const real = normalizeAddress(realAddress); + return expectedParts.some(p => real.includes(p) || (p && p.includes(real))); +} + // ── Angebote via Puppeteer von marktguru ───────────────────────────────────── async function scrapeMarktguru(forceRefresh = false) { const cacheKey = 'koepi:mg'; @@ -104,7 +127,10 @@ async function scrapeMarktguru(forceRefresh = false) { const img = card.querySelector('img.offer-list-item-img'); const badge = card.querySelector('.badge, .discount-badge')?.textContent?.trim() || ''; const imgSrc = img?.src?.replace('/medium.webp', '/large.webp') || img?.src || null; - return { name, brand, price, oldPrice, retailer, dateRange, validity, description, badge, address: '', image: imgSrc }; + const retailerDd = card.querySelector('dd.retailer-name'); + const addressDd = retailerDd?.nextElementSibling?.tagName === 'DD' ? retailerDd.nextElementSibling : null; + const realAddress = addressDd?.textContent?.trim() || ''; + return { name, brand, price, oldPrice, retailer, dateRange, validity, description, badge, address: '', realAddress, image: imgSrc }; }); }); @@ -138,8 +164,14 @@ async function scrapeMarktguru(forceRefresh = false) { return hasKP && isTargetRetailer(o.retailer); }); + // Nur Angebote der tatsächlich bekannten, lokalen Filialen behalten (marktguru + // zeigt teils auch weiter entfernte Filialen derselben Kette, z.B. "REWE Dortmund" + // oder einen anderen Netto/Kaufland-Standort — anhand der echten gescrapten + // Adresse rausfiltern statt sie fälschlich mit der lokalen Adresse zu überschreiben) + const localOnly = filtered.filter(o => isLocalBranch(o.retailer, o.realAddress)); + // Händler normalisieren: E center = EDEKA - for (const o of filtered) { + for (const o of localOnly) { if (o.retailer?.toLowerCase().includes('e center')) o.retailerDisplay = o.retailer; else o.retailerDisplay = o.retailer; } @@ -147,7 +179,7 @@ async function scrapeMarktguru(forceRefresh = false) { // Deduplizieren: gleicher normalisierter Händler + gleicher Preis → ein Eintrag // Kürzeste Beschreibung nehmen (sauberste) const merged = new Map(); - for (const o of filtered) { + for (const o of localOnly) { const normalizedRetailer = o.retailer?.toLowerCase().replace('e center','edeka') || ''; const key = `${normalizedRetailer}|${o.price}`; if (merged.has(key)) {