Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 48 additions & 12 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,17 @@
import io
import uuid
import random
import json
from pathlib import Path
from datetime import datetime, timezone
from contextlib import asynccontextmanager
from typing import Optional
from auth import get_current_user, get_google_oauth_url, exchange_code_for_session
from turnstile import TURNSTILE_SECRET_KEY, verify_turnstile_token
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from rate_limiter import limiter
from fastapi_cache import FastAPICache
from fastapi_cache.backends.inmemory import InMemoryBackend
from fastapi_cache.decorator import cache
from chat_router import router as chat_router



# Load .env file if present (python-dotenv)
try:
Expand All @@ -26,6 +22,16 @@
except ImportError:
pass

from auth import get_current_user, get_google_oauth_url, exchange_code_for_session
from turnstile import TURNSTILE_SECRET_KEY, verify_turnstile_token
from chat_router import router as chat_router
from vendors import (
get_vendor_achievements,
recalculate_vendor_metrics_and_achievements,
register_routes,
router as vendors_router,
)

from fastapi import Body, FastAPI, File, UploadFile, Form, HTTPException, Depends, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse, JSONResponse
Expand Down Expand Up @@ -287,6 +293,16 @@ def _row_to_payload(row: dict) -> dict:
alerts = row.get("alert_flags") or []
photos = row.get("photo_urls") or []

if isinstance(bm, str):
try:
bm = json.loads(bm)
except json.JSONDecodeError:
bm = {}
if isinstance(alerts, str):
alerts = [alerts]
if isinstance(photos, str):
photos = [photos]

# Use _build_biomarkers as a fallback when biomarker_json was not stored
if not bm:
bm = _build_biomarkers(freshness, freshness, freshness)
Expand Down Expand Up @@ -320,6 +336,16 @@ def _row_to_payload(row: dict) -> dict:
}


async def _refresh_vendor_after_scan(vendor_id: Optional[str]) -> None:
if not vendor_id:
return
try:
recalculate_vendor_metrics_and_achievements(_db(), vendor_id)
await FastAPICache.clear(namespace="markets")
except Exception as exc:
print(f"Vendor achievement refresh skipped for {vendor_id}: {exc}")


async def _upload_image(image_bytes: bytes, user_id: str, scan_id: str) -> Optional[str]:
try:
client = supabase_service or supabase
Expand Down Expand Up @@ -481,6 +507,7 @@ async def process_scan(
"is_target_domain": is_target_domain,
}
).execute()
await _refresh_vendor_after_scan(vendor_id)
except Exception as exc:
print(f"DB write failed (demo): {exc}")

Expand Down Expand Up @@ -517,6 +544,7 @@ async def process_scan(
"is_target_domain": is_target_domain,
}
).execute()
await _refresh_vendor_after_scan(vendor_id)
except Exception as exc:
print(f"DB write failed: {exc}")

Expand Down Expand Up @@ -751,7 +779,11 @@ async def get_vendors():
"trust_score, total_scans, avg_freshness_score, vendor_count"
)
resp = _db().table("vendors").select(fields).execute()
return {"success": True, "vendors": resp.data}
rows = resp.data or []
achievements = get_vendor_achievements(_db(), [str(v["id"]) for v in rows])
for vendor in rows:
vendor["achievements"] = achievements.get(str(vendor["id"]), [])
return {"success": True, "vendors": rows}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))

Expand All @@ -762,12 +794,16 @@ async def get_vendor_leaderboard():
resp = (
_db()
.table("vendors")
.select("id, name, trust_score, total_scans, avg_freshness_score, lat, lng")
.select("id, name, address, trust_score, total_scans, avg_freshness_score, trust_badge, trend, lat, lng")
.order("trust_score", desc=True)
.limit(10)
.execute()
)
return {"success": True, "leaderboard": resp.data}
rows = resp.data or []
achievements = get_vendor_achievements(_db(), [str(v["id"]) for v in rows])
for vendor in rows:
vendor["achievements"] = achievements.get(str(vendor["id"]), [])
return {"success": True, "leaderboard": rows}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))

Expand All @@ -783,6 +819,8 @@ async def _get_markets_cached() -> dict:
.select("id, name, avg_freshness_score, trust_score, lat, lng, vendor_count")
.execute()
)
rows = resp.data or []
achievements = get_vendor_achievements(_db(), [str(v["id"]) for v in rows])
markets = [
{
"id": i + 1,
Expand All @@ -791,8 +829,9 @@ async def _get_markets_cached() -> dict:
"lat": float(v.get("lat") or 0),
"lng": float(v.get("lng") or 0),
"vendors": int(v.get("vendor_count") or 1),
"achievements": achievements.get(str(v["id"]), []),
}
for i, v in enumerate(resp.data or [])
for i, v in enumerate(rows)
if v.get("lat") and v.get("lng")
]
return {"success": True, "markets": markets}
Expand Down Expand Up @@ -996,9 +1035,6 @@ def _bwd_hook(_module, _grad_in, grad_out):
}


# -- VENDOR TRUST SCORE (Issue #45) -----------------------------------------
from vendors import router as vendors_router, register_routes

register_routes(vendors_router, _db)
from markets import router as markets_router
app.include_router(markets_router)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- Vendor achievements awarded from scan history.

CREATE TABLE IF NOT EXISTS public.vendor_achievements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
vendor_id UUID NOT NULL REFERENCES public.vendors(id) ON DELETE CASCADE,
code TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
icon TEXT NOT NULL,
tier TEXT NOT NULL DEFAULT 'neon',
awarded_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc', now()) NOT NULL,
metadata JSONB DEFAULT '{}',
UNIQUE (vendor_id, code)
);

ALTER TABLE public.vendor_achievements ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS "Anyone can view vendor achievements" ON public.vendor_achievements;
CREATE POLICY "Anyone can view vendor achievements"
ON public.vendor_achievements FOR SELECT USING (true);

CREATE INDEX IF NOT EXISTS vendor_achievements_vendor_id_idx
ON public.vendor_achievements (vendor_id);

CREATE INDEX IF NOT EXISTS vendor_achievements_code_idx
ON public.vendor_achievements (code);
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Grant table privileges required by local Supabase API roles.
-- RLS policies still enforce row-level access; these grants only allow the
-- roles to access the tables through PostgREST/Supabase clients.

GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;

GRANT SELECT ON TABLE public.vendors TO anon, authenticated, service_role;
GRANT UPDATE ON TABLE public.vendors TO service_role;

GRANT SELECT, INSERT ON TABLE public.scans TO authenticated, service_role;

GRANT SELECT ON TABLE public.vendor_achievements TO anon, authenticated, service_role;
GRANT INSERT, UPDATE ON TABLE public.vendor_achievements TO service_role;
45 changes: 45 additions & 0 deletions backend/supabase/snippets/Untitled query 230.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
insert into public.vendor_achievements
(vendor_id, code, title, description, icon, tier)
select
Comment on lines +1 to +3
id,
badge.code,
badge.title,
badge.description,
badge.icon,
badge.tier
from public.vendors
cross join (
values
(
'fresh_streak_50',
'50x Grade A Streak',
'Awarded for 50 consecutive Grade A scans.',
'bolt',
'legendary'
),
(
'consistent_30d',
'30 Days Consistently Fresh',
'Awarded after Grade A scans across 30 clean scan days.',
'calendar',
'neon'
),
(
'trusted_volume_100',
'100 Verified Scans',
'Awarded after 100 recorded freshness scans.',
'shield',
'trusted'
),
(
'top_1_percent',
'Top 1% Freshness',
'Awarded to the highest freshness performers on the vendor leaderboard.',
'crown',
'elite'
)
) as badge(code, title, description, icon, tier)
where public.vendors.id = (
select id from public.vendors order by avg_freshness_score desc limit 1
)
on conflict (vendor_id, code) do nothing;
Comment on lines +1 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Seed script awards achievements without validating criteria.

This unconditionally inserts all four achievement badges (fresh_streak_50, consistent_30d, trusted_volume_100, top_1_percent) for whichever vendor currently has the highest avg_freshness_score, with no check against the actual thresholds (50-scan streak, 30 clean days, 100 scans, top-1% ranking) enforced by recalculate_vendor_metrics_and_achievements in backend/vendors.py. If this is run against production, it will fabricate achievement data inconsistent with the computed logic elsewhere in the PR.

If this is intended purely as local/demo seed data, consider a comment header clarifying that, and/or gating it so it can't accidentally be applied to a real environment.

🧰 Tools
🪛 SQLFluff (4.2.2)

[error] 10-10: Duplicate table alias 'vendors'. Table aliases should be unique.

(AL04)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/supabase/snippets/Untitled` query 230.sql around lines 1 - 45, The
seed SQL in the vendor achievements insert is creating all badge rows for the
top-ranked vendor without checking the real achievement conditions. Update the
script so it either derives eligibility from the same metric rules used by
recalculate_vendor_metrics_and_achievements in backend/vendors.py, or clearly
marks the data as non-production demo seed and gates it from real environments.
Keep the existing insert pattern with vendor_achievements, vendors, and the
badge values, but ensure only genuinely qualified achievements are inserted.

Loading
Loading