From 8734067dcd2f0877081c3dc0a8dd33438e2ec88e Mon Sep 17 00:00:00 2001 From: Dicken Date: Mon, 20 Jul 2026 13:15:56 +0200 Subject: [PATCH] fix: KoePi verwandte Ketten (Netto Marken-/Getraenke-Discount) werden jetzt gemeinsam durchsucht --- backend/src/tools/koepi/routes.js | 45 ++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/backend/src/tools/koepi/routes.js b/backend/src/tools/koepi/routes.js index 9a85387..0291933 100644 --- a/backend/src/tools/koepi/routes.js +++ b/backend/src/tools/koepi/routes.js @@ -62,10 +62,20 @@ function addressesMatch(a, b) { // aktuellen und kürzlich abgelaufenen Prospekte GENAU dieser Kette, ganz ohne // Umkreis-Raten. addressMatch bestimmt, welche der (ggf. mehreren // bundesweiten) Filialen dieser Kette tatsächlich eure ist. +// Manche Ketten teilen sich bei marktguru offenbar denselben Datentopf — ein +// Prospekt kann unter der "falschen" uniqueName auftauchen (z.B. das +// Getränke-Discount-Prospekt bei "netto-marken-discount" statt bei +// "netto-getraenke-discount"). Für solche Familien wird beim Auflösen die +// gesamte Gruppe gemeinsam durchsucht, nicht nur die eigene Liste. +const RETAILER_FAMILIES = { + 'netto-marken-discount': ['netto-marken-discount', 'netto-getraenke-discount'], + 'netto-getraenke-discount':['netto-marken-discount', 'netto-getraenke-discount'], +}; + const LOCAL_STORE_TARGETS = [ { retailer: 'Netto Marken-Discount', uniqueName: 'netto-marken-discount', addressMatch: 'Im Bonnefeld 19' }, { retailer: 'Netto Getränke-Discount', uniqueName: 'netto-getraenke-discount', addressMatch: 'Im Bonnefeld 21' }, - { retailer: 'trinkgut', uniqueName: 'trinkgut-discount', addressMatch: 'Keniastraße 39' }, + { retailer: 'trinkgut', uniqueName: 'trinkgut', addressMatch: 'Keniastraße 39' }, { retailer: 'REWE', uniqueName: 'rewe', addressMatch: 'Fischerstr. 110-112' }, { retailer: 'Kaufland', uniqueName: 'kaufland', addressMatch: 'Auf der Höhe 20' }, { retailer: 'Penny', uniqueName: 'penny', addressMatch: 'Sittardsberger Allee 10' }, @@ -249,9 +259,20 @@ async function resolveLocalStores() { const cache = new Map(); let resolved = 0; + // Alle benötigten uniqueNames einmal laden (Mehrfachabrufe vermeiden, falls + // mehrere Ziele/Familien sich eine uniqueName teilen) + const uniqueNames = [...new Set(LOCAL_STORE_TARGETS.map(t => t.uniqueName))]; + const leafletsByUniqueName = new Map(); + for (const uniqueName of uniqueNames) { + leafletsByUniqueName.set(uniqueName, await fetchLeafletsForRetailer(uniqueName)); + } + for (const target of LOCAL_STORE_TARGETS) { - const leaflets = await fetchLeafletsForRetailer(target.uniqueName); - console.log(`🍺 KöPi: "${target.retailer}" (${target.uniqueName}) — ${leaflets.length} Prospekt(e) gefunden`); + // Bei verwandten Ketten (siehe RETAILER_FAMILIES) alle Prospekte der + // ganzen Familie gemeinsam durchsuchen, sonst nur die eigene Liste + const familyNames = RETAILER_FAMILIES[target.uniqueName] || [target.uniqueName]; + const leaflets = familyNames.flatMap(name => leafletsByUniqueName.get(name) || []); + console.log(`🍺 KöPi: "${target.retailer}" (${target.uniqueName}) — ${leaflets.length} Prospekt(e) durchsucht${familyNames.length>1?` (Familie: ${familyNames.join(', ')})`:''}`); let found = null; for (const l of leaflets) { if (!l.mainLeafletId) continue; @@ -617,11 +638,21 @@ async function scrapeProspekte() { }).map(({ rawName, ...rest }) => rest); // rawName war nur intern nötig const targeted = withoutSondermagazin.filter(b => b.isTarget); - // Chronologisch in "aktuell laufend" und "kommend" trennen - const todayIso = new Date().toISOString().slice(0, 10); + // Chronologisch in "aktuell laufend" und "kommend" trennen — WICHTIG: in + // lokaler Zeit vergleichen, nicht per rohem UTC-Substring. "22:00 UTC" ist + // bei uns lokal schon der nächste Tag (z.B. "2026-07-19T22:00:00Z" = lokal + // 20.07., 00:00 Uhr) — ein reiner String-Schnitt hätte das um einen Tag + // verschoben. + const localDateStr = isoOrDate => { + const d = isoOrDate instanceof Date ? isoOrDate : new Date(isoOrDate); + if (Number.isNaN(d.getTime())) return ''; + const pad = n => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + }; + const todayIso = localDateStr(new Date()); const isCurrent = b => { - const from = (b.validFrom || '').slice(0, 10); - const to = (b.validTo || '').slice(0, 10); + const from = localDateStr(b.validFrom); + const to = localDateStr(b.validTo); if (!from) return true; // ohne Datum lieber anzeigen als verstecken if (from > todayIso) return false; // startet erst noch if (to && to < todayIso) return false; // schon abgelaufen