rewardpilot-api / app /availability.py
sammy786's picture
Channel comparison + city availability + issuer portals + booking/apply links
faf005c
Raw
History Blame Contribute Delete
6.44 kB
"""
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"