From d70d934ac83196ceff5158e1f7e7f82181b59a25 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:16:58 +0200 Subject: [PATCH] fix(ev): keep the BNetzA safety floor armed when it is set below 1 A fractional or zero PUMPERLY_BNETZA_MIN_STATIONS floored to 0, and `refreshed < 0` is never true, so the guard on the two destructive sweeps silently disappeared. Values below 1 now fall back to the 10,000 default like non-numeric ones. Also fixes the README line that still said Open Charge Map covers every country. --- README.md | 2 +- src/scrapers/bnetza.test.ts | 16 ++++++++++++++++ src/scrapers/bnetza.ts | 4 +++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 91bf3a1..fa9f4fb 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Pumperly combines route planning with real-time fuel prices and EV charging stat - **Route planning** — Geocoding via [Photon](https://github.com/komoot/photon), routing via [Valhalla](https://github.com/valhalla/valhalla), with alternative routes - **Real-time fuel prices** — From government open data APIs and community sources -- **EV charging stations** — Via [Open Charge Map](https://openchargemap.org) across all supported countries, plus the official [Mapa REVE](https://www.mapareve.es) registry in Spain and the [BNetzA Ladesäulenregister](https://www.bundesnetzagentur.de/DE/Fachthemen/ElektrizitaetundGas/E-Mobilitaet/Ladesaeulenkarte/start.html) in Germany +- **EV charging stations** — Official registries where they exist ([Mapa REVE](https://www.mapareve.es) in Spain, the [BNetzA Ladesäulenregister](https://www.bundesnetzagentur.de/DE/Fachthemen/ElektrizitaetundGas/E-Mobilitaet/Ladesaeulenkarte/start.html) in Germany), [Open Charge Map](https://openchargemap.org) everywhere else - **Detour calculation** — Each station shows estimated detour time from your route - **"Cheapest within N min"** — Slider filters stations by maximum detour, highlights the best deal - **Corridor station list** — Sorted by position along route, with price deltas vs average diff --git a/src/scrapers/bnetza.test.ts b/src/scrapers/bnetza.test.ts index aac7daf..f60e293 100644 --- a/src/scrapers/bnetza.test.ts +++ b/src/scrapers/bnetza.test.ts @@ -526,6 +526,22 @@ describe("BNetzAScraper cleanup", () => { expect(stub.queries).toHaveLength(1); // fell back to the 10,000 default }); + it("treats a floor below 1 as nonsensical too", async () => { + // 0.5 floors to 0, and `refreshed < 0` is never true: the guard would be + // gone without anyone noticing. Same fallback as a non-numeric value. + for (const raw of ["0.5", "0", "-3"]) { + vi.stubEnv("PUMPERLY_BNETZA_MIN_STATIONS", raw); + const stub = await setup({ results: [[{ count: BigInt(42) }]] }); + const { BNetzAScraper } = await import("./bnetza"); + + await new BNetzAScraper().run(); + + expect(stub.queries, `floor "${raw}"`).toHaveLength(1); + vi.resetModules(); + vi.restoreAllMocks(); + } + }); + it("does not clean up after a run that reported errors", async () => { const stub = await setup({ errors: ["Station batch 0-500: boom"] }); const { BNetzAScraper } = await import("./bnetza"); diff --git a/src/scrapers/bnetza.ts b/src/scrapers/bnetza.ts index e38bd00..6d37455 100644 --- a/src/scrapers/bnetza.ts +++ b/src/scrapers/bnetza.ts @@ -70,8 +70,10 @@ const COORD_PRECISION = 5; // clears the empty-fetch guard must not be able to retire OpenChargeMap or bulk // delete yesterday's stations. The register holds ~74k; 10k is a wide margin. const rawMinStations = Number(process.env.PUMPERLY_BNETZA_MIN_STATIONS ?? "10000"); +// Anything that floors below 1 (0, 0.5, negatives) would make `refreshed < MIN_STATIONS` +// impossible to satisfy and silently disable the guard, so it falls back too. const MIN_STATIONS = - Number.isFinite(rawMinStations) && rawMinStations > 0 ? Math.floor(rawMinStations) : 10_000; + Number.isFinite(rawMinStations) && rawMinStations >= 1 ? Math.floor(rawMinStations) : 10_000; // Slack added to the staleness cutoff so the sweep can never delete a row // written in the opening moments of the run it belongs to.