/** * Maps adapter (Google Maps Platform). Used only when a shop has radius or * driving-distance zones; postal-code zones never call out. * * Failure policy: on timeout/error we return the address WITHOUT coordinates/driving * distances. The engine then refuses distance-based zones ("couldn't verify your * address") rather than accepting an unverified address. */ import type { LocationConfig } from "../engine/types"; import type { CustomerAddress } from "../engine/zones"; const KEY = process.env.GOOGLE_MAPS_API_KEY; const TIMEOUT = 2500; const cache = new Map(); const TTL = 24 * 3600_000; async function getJson(url: string): Promise { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), TIMEOUT); try { const r = await fetch(url, { signal: ctrl.signal }); if (!r.ok) throw new Error(`maps ${r.status}`); return await r.json(); } finally { clearTimeout(t); } } export function needsMaps(locations: LocationConfig[]) { return locations.some((l) => l.zones.some((z) => z.radiusKm != null || z.drivingKm != null)); } export async function resolveAddress( input: { postal?: string; address?: string; country?: string }, locations: LocationConfig[], ): Promise { const base: CustomerAddress = { postal: input.postal }; if (!KEY || !needsMaps(locations)) return base; const query = [input.address, input.postal, input.country].filter(Boolean).join(", "); if (!query) return base; const ck = `${query}|${locations.map((l) => l.id).join(",")}`; const hit = cache.get(ck); if (hit && Date.now() - hit.at < TTL) return hit.v; try { const geo = (await getJson( `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(query)}&key=${KEY}`, )) as { status: string; results: { geometry: { location: { lat: number; lng: number } } }[] }; if (geo.status !== "OK" || !geo.results[0]) return base; const { lat, lng } = geo.results[0].geometry.location; const out: CustomerAddress = { ...base, lat, lng }; const driving = locations.filter((l) => l.lat != null && l.zones.some((z) => z.drivingKm != null)); if (driving.length) { const dm = (await getJson( `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${driving .map((l) => `${l.lat},${l.lng}`) .join("|")}&destinations=${lat},${lng}&mode=driving&key=${KEY}`, )) as { rows: { elements: { status: string; distance?: { value: number } }[] }[] }; out.drivingKm = {}; driving.forEach((l, i) => { const el = dm.rows[i]?.elements[0]; out.drivingKm![l.id] = el?.status === "OK" && el.distance ? el.distance.value / 1000 : null; }); } cache.set(ck, { at: Date.now(), v: out }); return out; } catch { return { ...base, degraded: true }; } }