import type { Method } from "@prisma/client"; // Pure rate resolution — no DB calls (CLAUDE.md: inject data). The caller // (apps.scheduling.availability.tsx) fetches the shop's Rate rows for the // method and passes them in here. export interface RateLike { id: string; method: Method; zoneId: string | null; name: string; priceCents: number; keyedBy: string; // "zone" | "distance" minDistanceKm: number | null; maxDistanceKm: number | null; } export interface RateContext { method: Method; zoneId?: string; distanceKm?: number; } /** * Picks the rate that applies to a given method/zone/distance. Zone-keyed * rates are matched by exact zoneId; distance-keyed rates by falling within * [minDistanceKm, maxDistanceKm) (open-ended bounds are unbounded on that * side). When more than one rate matches, the cheapest wins — a merchant * with overlapping bands almost certainly wants the shopper quoted the best * available price, not an arbitrary one. */ export function resolveRate(rates: RateLike[], context: RateContext): RateLike | null { const candidates = rates.filter((rate) => { if (rate.method !== context.method) return false; if (rate.keyedBy === "zone") { return context.zoneId !== undefined && rate.zoneId === context.zoneId; } if (rate.keyedBy === "distance") { if (context.distanceKm === undefined) return false; const min = rate.minDistanceKm ?? 0; const max = rate.maxDistanceKm ?? Infinity; return context.distanceKm >= min && context.distanceKm < max; } return false; }); if (candidates.length === 0) return null; return candidates.reduce((cheapest, candidate) => (candidate.priceCents < cheapest.priceCents ? candidate : cheapest)); } // formatPriceLabel moved to app/lib/currency.ts (2026-08-24): it's pure/ // no-I/O, but a route *component* needs it (app.rates._index.tsx), and // Remix refuses to bundle anything imported from a .server.ts path for the // client — import it from lib/currency directly, not re-exported here.