70 lines
2.9 KiB
TypeScript
70 lines
2.9 KiB
TypeScript
/**
|
|
* 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<string, { at: number; v: CustomerAddress }>();
|
|
const TTL = 24 * 3600_000;
|
|
|
|
async function getJson(url: string): Promise<unknown> {
|
|
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<CustomerAddress & { degraded?: boolean }> {
|
|
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 };
|
|
}
|
|
}
|