From 7d1a89475a4d460524339077bfe41ef377695447 Mon Sep 17 00:00:00 2001 From: Ryan Chen Date: Thu, 17 Sep 2026 20:23:52 -0400 Subject: [PATCH] Fix Beli API client against actual spec - Category codes: RES/DES/BAR/BAK (was full English words) - score field: use 'value' from Score model (was 'score', which doesn't exist) - rank_index: pass array position (no rank field in response) - fetch_bookmarks: no query params (user param caused 405) - notes: correctly None (stored separately, not in ranking results) Co-Authored-By: Claude Sonnet 4.6 --- blueprints/beli/logic.py | 6 +++--- utils/beli_service.py | 35 +++++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/blueprints/beli/logic.py b/blueprints/beli/logic.py index 734b9d2..bd301a8 100644 --- a/blueprints/beli/logic.py +++ b/blueprints/beli/logic.py @@ -59,8 +59,8 @@ async def refresh_profile(profile_id: str) -> dict: results = await fetch_user_rankings( access_token, profile.beli_user_id, category ) - for item in results: - biz = extract_business(item) + for idx, item in enumerate(results): + biz = extract_business(item, rank_index=idx + 1) if not biz: continue await BeliRestaurant.update_or_create( @@ -84,7 +84,7 @@ async def refresh_profile(profile_id: str) -> dict: # Fetch bookmarks (want-to-try list) — non-fatal if unavailable await asyncio.sleep(REQUEST_SPACING) try: - bookmarks = await fetch_bookmarks(access_token, profile.beli_user_id) + bookmarks = await fetch_bookmarks(access_token) except Exception as e: logger.warning( "Beli bookmarks fetch failed for profile %s: %s", profile_id, e diff --git a/utils/beli_service.py b/utils/beli_service.py index 4f29c40..d1b8929 100644 --- a/utils/beli_service.py +++ b/utils/beli_service.py @@ -5,7 +5,8 @@ Reverse-engineered from ProjectBarks/beli-api (mirrored at git.torrtle.co/ryan/b Hosts and auth flow discovered from observed traffic — may break without notice. Auth: POST /api/token/ → {access, refresh} (access: 20 min, refresh: 7 days) -The API rejects requests without a browser-looking User-Agent and Origin header. +The API rejects requests without an Origin header (app uses capacitor://localhost; +https://localhost also works per external clients). """ import base64 @@ -21,9 +22,10 @@ logger = logging.getLogger(__name__) HOST_ONBOARD = "https://backoffice-service-onboarding-t57o3dxfca-nn.a.run.app" HOST_API = "https://backoffice-service-t57o3dxfca-nn.a.run.app" +# 3-letter category codes used by the Beli API RANKING_CATEGORIES = ["RES", "DES", "BAR", "BAK"] -# 350 ms between requests — the API throttles bursts +# 350 ms between requests — community-polite interval per API reference REQUEST_SPACING = 0.35 _USER_AGENTS = [ @@ -102,7 +104,7 @@ async def refresh_access_token(refresh_token: str) -> str: async def fetch_user_rankings( access_token: str, user_id: str, category: str ) -> list[dict]: - """Fetch a user's ranked restaurants for one category.""" + """Fetch a user's ranked places for one category (RES/DES/BAR/BAK).""" async with httpx.AsyncClient() as client: resp = await client.get( f"{HOST_API}/api/get-ranking/", @@ -114,13 +116,15 @@ async def fetch_user_rankings( return resp.json().get("results") or [] -async def fetch_bookmarks(access_token: str, user_id: str | None = None) -> list[dict]: - """Fetch the authenticated user's bookmarked (want-to-try) restaurants.""" - params = {"user": user_id} if user_id else {} +async def fetch_bookmarks(access_token: str) -> list[dict]: + """Fetch the authenticated user's bookmarked (want-to-try) places. + + The endpoint takes no query params — it returns bookmarks for whoever + owns the bearer token. + """ async with httpx.AsyncClient() as client: resp = await client.get( f"{HOST_API}/api/get-bookmark/", - params=params, headers=_base_headers(access_token), timeout=30, ) @@ -128,11 +132,14 @@ async def fetch_bookmarks(access_token: str, user_id: str | None = None) -> list return resp.json().get("results") or [] -def extract_business(item: dict) -> dict | None: - """Pull restaurant fields from a ranking or bookmark result item. +def extract_business(item: dict, rank_index: int | None = None) -> dict | None: + """Pull place fields from a ranking or bookmark result item. - The API returns results with the business either nested under a 'business' - key or flattened at the top level — handle both. + Ranking items have a nested 'business' object (integer id) plus top-level + score fields. The score is stored as 'value' (sentiment seed sent by client; + server computes the displayed 0-10 score server-side). + Rank is the position in the results list, not a field in the response. + Notes are stored separately via data-user-business-text; we don't fetch them. """ biz = item.get("business") or item biz_id = biz.get("id") or item.get("business_id") @@ -152,8 +159,8 @@ def extract_business(item: dict) -> dict | None: "neighborhood": biz.get("neighborhood") or biz.get("borough"), "cuisine": biz.get("cuisines") or [], "price_range": price_key, - "rank_index": item.get("rank") or item.get("rank_index"), - "score": item.get("score"), - "notes": item.get("notes"), + "rank_index": rank_index, + "score": item.get("value"), + "notes": None, "tags": item.get("tags") or [], }