Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export async function register() {
const { MexicoScraper } = await import("./scrapers/mexico");
const { OCMScraper } = await import("./scrapers/ocm");
const { REVEScraper } = await import("./scrapers/reve");
const { resolveSpainEvSource } = await import("./scrapers/spain-ev-source");
const { StaticScraper } = await import("./scrapers/static");
const { STATIC_DATASETS } = await import("./scrapers/data");

Expand Down Expand Up @@ -229,19 +230,13 @@ export async function register() {
: Object.keys(scraperFactories).filter((c) => !c.startsWith("EV_"));
}

// Spain: the official Mapa REVE registry supersedes OpenChargeMap whenever a
// key is configured. They must never both run — 92% of REVE locations sit
// within 50m of an existing OpenChargeMap row, so the map would double-pin
// every Spanish charger. REVE also retires the rows it replaces once its
// backfill is complete (see scrapers/reve.ts).
if (process.env.PUMPERLY_REVE_API_KEY && countries.includes("EV_ES")) {
countries = countries.filter((c) => c !== "EV_ES");
if (!countries.includes("EV_ES_REVE")) countries.push("EV_ES_REVE");
// Spain has two possible EV sources and must never run both — see
// scrapers/spain-ev-source.ts for why, and scrapers/reve.ts for the handover.
countries = resolveSpainEvSource(countries);
if (countries.includes("EV_ES_REVE")) {
console.log(
"[scraper] Spain EV: using Mapa REVE (official registry) instead of OpenChargeMap",
);
} else {
countries = countries.filter((c) => c !== "EV_ES_REVE");
}

// Resolve per-country intervals
Expand Down
5 changes: 4 additions & 1 deletion src/scrapers/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { ArgentinaScraper } from "./argentina";
import { MexicoScraper } from "./mexico";
import { OCMScraper } from "./ocm";
import { REVEScraper } from "./reve";
import { resolveSpainEvSource } from "./spain-ev-source";

// ---------------------------------------------------------------------------
// Scraper CLI
Expand Down Expand Up @@ -153,9 +154,11 @@ function parseArgs(argv: string[]): { countries: string[] } {
usage();
}

// `all` must not run both Spanish EV sources. Naming EV_ES explicitly still
// works — the CLI is a manual override, and asking for it by name means it.
const countries =
countryArg === "ALL"
? Object.keys(SCRAPERS)
? resolveSpainEvSource(Object.keys(SCRAPERS))
: countryArg.split(",").map((c) => c.trim().toUpperCase());

// Validate
Expand Down
53 changes: 53 additions & 0 deletions src/scrapers/spain-ev-source.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { resolveSpainEvSource } from "./spain-ev-source";

// The scheduler and the manual CLI both route Spain's EV scraping through this,
// so this is the test that stops them drifting apart. Drift is silent: the map
// just quietly refills with duplicate Spanish chargers.

describe("resolveSpainEvSource", () => {
afterEach(() => {
vi.unstubAllEnvs();
});

// What the scheduler produces once EV_XX codes are derived.
const SCHEDULED = ["ES", "FR", "EV_ES", "EV_FR", "EV_US"];
// What `--country=all` produces: both Spanish sources are registered.
const ALL = ["ES", "FR", "EV_ES", "EV_FR", "EV_ES_REVE", "EV_US"];

it("uses REVE for Spain when a key is configured", () => {
vi.stubEnv("PUMPERLY_REVE_API_KEY", "a-key");
expect(resolveSpainEvSource(SCHEDULED)).toContain("EV_ES_REVE");
expect(resolveSpainEvSource(SCHEDULED)).not.toContain("EV_ES");
});

it("keeps OpenChargeMap for Spain when no key is configured", () => {
vi.stubEnv("PUMPERLY_REVE_API_KEY", "");
expect(resolveSpainEvSource(SCHEDULED)).toContain("EV_ES");
expect(resolveSpainEvSource(SCHEDULED)).not.toContain("EV_ES_REVE");
});

it("collapses --country=all down to one Spanish EV source", () => {
for (const key of ["a-key", ""]) {
vi.stubEnv("PUMPERLY_REVE_API_KEY", key);
const out = resolveSpainEvSource(ALL);
expect(out.filter((c) => c === "EV_ES" || c === "EV_ES_REVE")).toHaveLength(1);
}
});

it("leaves every other country untouched", () => {
vi.stubEnv("PUMPERLY_REVE_API_KEY", "a-key");
expect(resolveSpainEvSource(ALL).filter((c) => !c.startsWith("EV_ES"))).toEqual([
"ES",
"FR",
"EV_FR",
"EV_US",
]);
});

it("adds nothing when Spain has no EV scraper enabled", () => {
vi.stubEnv("PUMPERLY_REVE_API_KEY", "a-key");
const fuelOnly = ["ES", "FR", "IT"];
expect(resolveSpainEvSource(fuelOnly)).toEqual(fuelOnly);
});
});
31 changes: 31 additions & 0 deletions src/scrapers/spain-ev-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// ---------------------------------------------------------------------------
// Which source supplies Spain's EV chargers
// ---------------------------------------------------------------------------
// Spain has two: OpenChargeMap (`EV_ES`, crowdsourced, every country) and Mapa
// REVE (`EV_ES_REVE`, the official registry every Spanish CPO files into).
//
// They must never both run. 92% of REVE locations sit within 50m of an existing
// OpenChargeMap row, so running both double-pins ~13k Spanish chargers — and
// once REVE has retired those rows (see scrapers/reve.ts), a single stray run of
// the OpenChargeMap scraper would put all ~19k of them straight back.
//
// This rule is needed by both the scheduler (instrumentation.ts) and the manual
// CLI (scrapers/cli.ts), so it lives here rather than in either of them. Two
// copies would drift, and the way it drifts is silent: the map just quietly
// fills up with duplicates again.
// ---------------------------------------------------------------------------

/**
* Collapse Spain's two EV scrapers down to whichever one is configured.
*
* Returns `codes` unchanged when Spain has no EV scraper enabled at all (e.g.
* PUMPERLY_EV_ENABLED=0). Otherwise exactly one of `EV_ES` / `EV_ES_REVE`
* survives: REVE when an API key is set, OpenChargeMap when it is not.
*/
export function resolveSpainEvSource(codes: string[]): string[] {
const hasSpainEv = codes.includes("EV_ES") || codes.includes("EV_ES_REVE");
const rest = codes.filter((c) => c !== "EV_ES" && c !== "EV_ES_REVE");
if (!hasSpainEv) return rest;
rest.push(process.env.PUMPERLY_REVE_API_KEY ? "EV_ES_REVE" : "EV_ES");
return rest;
}
Loading