""" RewardPilot - City & platform availability =========================================== Answers two questions the channel comparison needs: 1. Which city is the user in? (explicit city > city named in the query > nearest metro centroid from GPS coordinates) 2. Is a booking platform - and, when we know the chain, this specific merchant - actually available in that city? Curated seed, same philosophy as offers.py: structured and dated in spirit, refreshed by the ingestion pipeline (connectors/) later. Coverage values are illustrative until a live feed is wired. Availability levels: "available" - platform (and merchant, when known) confirmed in this city "unknown" - no data to confirm; channel is kept but flagged unverified "unavailable" - confirmed absent (platform not in city, or chain not listed on this platform); channel is dropped from the comparison Mirrored on-device in app/src/data/availability.ts. """ import math from typing import Dict, Optional, Set, Union # --------------------------------------------------------------------------- # Cities: canonical name -> (lat, lng) centroid. Used for GPS fallback. # --------------------------------------------------------------------------- CITIES: Dict[str, tuple] = { "mumbai": (19.0760, 72.8777), "delhi": (28.6139, 77.2090), "bengaluru": (12.9716, 77.5946), "hyderabad": (17.3850, 78.4867), "chennai": (13.0827, 80.2707), "kolkata": (22.5726, 88.3639), "pune": (18.5204, 73.8567), "ahmedabad": (23.0225, 72.5714), "jaipur": (26.9124, 75.7873), "surat": (21.1702, 72.8311), "lucknow": (26.8467, 80.9462), "chandigarh": (30.7333, 76.7794), "indore": (22.7196, 75.8577), "kochi": (9.9312, 76.2673), "goa": (15.4909, 73.8278), } # Common alternates / NCR satellites -> canonical city key. CITY_SYNONYMS: Dict[str, str] = { "bombay": "mumbai", "navi mumbai": "mumbai", "thane": "mumbai", "new delhi": "delhi", "gurgaon": "delhi", "gurugram": "delhi", "noida": "delhi", "ghaziabad": "delhi", "faridabad": "delhi", "bangalore": "bengaluru", "madras": "chennai", "calcutta": "kolkata", "cochin": "kochi", "secunderabad": "hyderabad", "panaji": "goa", "panjim": "goa", } def normalize_city(name: Optional[str]) -> Optional[str]: """Free-text city name -> canonical key, or None if we don't know it.""" t = (name or "").strip().lower() if not t: return None if t in CITIES: return t return CITY_SYNONYMS.get(t) def extract_city(text: str) -> Optional[str]: """Find a city mentioned inside a free-text query ("dinner ... in Mumbai").""" t = (text or "").lower() hits = [k for k in list(CITIES) + list(CITY_SYNONYMS) if k in t] if not hits: return None pick = sorted(hits, key=len, reverse=True)[0] return pick if pick in CITIES else CITY_SYNONYMS[pick] def city_from_latlng(lat: float, lng: float, max_km: float = 60.0) -> Optional[str]: """Nearest metro centroid within max_km, else None.""" best, best_d = None, max_km for city, (clat, clng) in CITIES.items(): p1, p2 = math.radians(lat), math.radians(clat) dp, dl = math.radians(clat - lat), math.radians(clng - lng) h = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 d = 2 * 6371.0 * math.asin(min(1.0, math.sqrt(h))) if d < best_d: best, best_d = city, d return best # --------------------------------------------------------------------------- # Platform coverage: (channel_key, kind) -> "*" (pan-India) or a city set. # Direct/walk-in channels and travel OTAs are always available. # --------------------------------------------------------------------------- _ALL = "*" PLATFORM_CITIES: Dict[tuple, Union[str, Set[str]]] = { ("dineout", "dining"): { "mumbai", "delhi", "bengaluru", "hyderabad", "chennai", "kolkata", "pune", "ahmedabad", "jaipur", "lucknow", "chandigarh", "indore", }, ("district", "dining"): { "mumbai", "delhi", "bengaluru", "hyderabad", "chennai", "kolkata", "pune", "ahmedabad", }, ("district", "movies"): { "mumbai", "delhi", "bengaluru", "hyderabad", "chennai", "pune", }, ("bookmyshow", "movies"): _ALL, } # --------------------------------------------------------------------------- # Merchant-level listings (dining chains we track): brand_key -> platform -> # "*" or city set. A chain present in this dict with a platform MISSING means # we know it is NOT listed there (e.g. QSR counter chains aren't on the # dine-in bill-payment platforms). # --------------------------------------------------------------------------- MERCHANT_PLATFORMS: Dict[str, Dict[str, Union[str, Set[str]]]] = { "copper_chimney": { "dineout": {"mumbai", "delhi", "pune", "bengaluru"}, "district": {"mumbai", "pune", "bengaluru"}, }, "barbeque_nation": {"dineout": _ALL, "district": _ALL}, # counter/QSR chains: you pay at the till, not through a dine-in bill app "starbucks": {}, "haldiram": {}, "dominos": {}, "pizza_hut": {}, "mcdonalds": {}, "kfc": {}, } def channel_availability( channel_key: str, kind: str, city: Optional[str], brand_key: Optional[str] = None, ) -> str: """'available' | 'unknown' | 'unavailable' for one channel in one city.""" if channel_key == "direct": return "available" if kind in ("flights", "hotels"): return "available" # travel OTAs are pan-India # Merchant-level check first (dining chains we track). if kind == "dining" and brand_key in MERCHANT_PLATFORMS: listed = MERCHANT_PLATFORMS[brand_key] cities = listed.get(channel_key) if cities is None: return "unavailable" # chain known, not listed on this platform if cities != _ALL: if city is None: return "unknown" # listed somewhere, can't confirm this city return "available" if city in cities else "unavailable" # cities == "*": fall through to the platform's own city coverage cov = PLATFORM_CITIES.get((channel_key, kind)) if cov is None: return "unknown" if cov == _ALL: return "available" if city is None: return "unknown" return "available" if city in cov else "unavailable"