Skip to content

achievement badges added#147

Open
ValayaDase wants to merge 1 commit into
jpdevhub:mainfrom
ValayaDase:achievements
Open

achievement badges added#147
ValayaDase wants to merge 1 commit into
jpdevhub:mainfrom
ValayaDase:achievements

Conversation

@ValayaDase

@ValayaDase ValayaDase commented Jul 2, 2026

Copy link
Copy Markdown

Description

image

Adds vendor achievement badges to incentivize vendors based on scan-history milestones.

This PR introduces a new vendor_achievements table, backend award logic, and UI support for displaying achievement badges in the Vendor Leaderboard and Market Map surfaces.

Achievements currently supported:

  • 50x Grade A Streak
  • 30 Days Consistently Fresh
  • 100 Verified Scans
  • Top 1% Freshness

Backend changes include:

  • New vendor achievement persistence table.
  • Recalculation logic for vendor trust metrics and achievements.
  • Achievement data included in leaderboard and market map API responses.
  • Automatic achievement refresh after vendor scans.
  • More defensive scan payload conversion for older/malformed scan rows.

UI changes include:

  • Small cyberpunk-styled achievement icons on /leaderboard.

Manual badge test SQL used:
insert into public.vendor_achievements
(vendor_id, code, title, description, icon, tier)
select
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;

Checklist

  • npm run lint passes with no errors
  • [ *] npm run build compiles without TypeScript errors
  • python -m pytest passes (including new tests I added)
  • [* ] No .env files, API keys, secrets, model weights, or __pycache__ in this diff
  • Branch is rebased on main, not merged

Summary by CodeRabbit

  • New Features

    • Added vendor achievement badges across leaderboard, vendor details, and map views.
    • Vendor and market responses now include achievement data for display in the app.
  • Bug Fixes

    • Improved scan result handling so stored fields are parsed more reliably, even when values arrive in different formats.
    • Vendor metrics and achievements now refresh automatically after new scans are recorded.
  • Database

    • Added support for storing and serving vendor achievement records.

Copilot AI review requested due to automatic review settings July 2, 2026 10:28
@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

@ValayaDase is attempting to deploy a commit to the karan3431's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🎉 Thank you for your Pull Request! We're thrilled to have your contribution to FreshScan AI.

Before we review, please make sure you have:

  • Followed the CONTRIBUTING.md guidelines.
  • Ensured all automated CI checks (linting, tests) are passing.
  • Checked that your commit messages follow the Conventional Commits format.

A maintainer will review your code as soon as possible!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds vendor achievement badges across the backend and frontend so vendor milestone achievements can be persisted, recalculated, and displayed in the Leaderboard and Market Map UI surfaces.

Changes:

  • Introduces public.vendor_achievements table + RLS/grants for persisting awarded achievements.
  • Adds backend award/recalculation + API enrichment to return vendor achievement data alongside vendor/market responses.
  • Adds UI rendering for achievement badge icons (Leaderboard and Market Map) and updates shared TS types.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
supabase/.temp/cli-latest Adds a Supabase CLI artifact file (should likely be ignored).
supabase/.branches/_current_branch Adds a Supabase branch-tracking artifact file (should likely be ignored).
src/pages/MarketMapPage.tsx Renders up to 4 achievement icons in map popups and the selected market panel.
src/pages/Leaderboard.tsx Renders achievement icons under vendor entries on the leaderboard.
src/lib/types.ts Adds VendorAchievement type and attaches achievements to Market.
backend/vendors.py Implements achievement definitions, awarding logic, lookup helper, and enriches vendor API responses with achievements.
backend/supabase/snippets/Untitled query 230.sql Adds a manual SQL snippet for inserting achievements (likely dev-only).
backend/supabase/migrations/20240702001000_grant_app_table_permissions.sql Grants required table privileges for Supabase API roles.
backend/supabase/migrations/20240702000000_add_vendor_achievements.sql Creates vendor_achievements table with indexes + RLS policy.
backend/main.py Adds scan-payload hardening, triggers achievement refresh after scans, and enriches vendor/market endpoints with achievements.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/vendors.py
Comment on lines +104 to +105
ts = row.get("timestamp") or ""
day = ts[:10]
Comment thread backend/vendors.py
Comment on lines +172 to +173
recent_streak = _longest_recent_grade_a_streak(rows)
clean_days = _clean_grade_a_days(rows)
Comment thread backend/vendors.py
Comment on lines +112 to +118
def _top_percent_vendor_ids(db) -> set[str]:
vendors = (
db.table("vendors")
.select("id, avg_freshness_score, total_scans")
.order("avg_freshness_score", desc=True)
.execute()
)
Comment thread backend/vendors.py
Comment on lines 223 to +240
@@ -70,7 +233,11 @@ async def get_leaderboard(limit: int = Query(default=20, ge=1, le=100)):
.limit(limit)
.execute()
)
return {"success": True, "leaderboard": resp.data or []}
rows = resp.data or []
achievements = get_vendor_achievements(db_getter(), [str(v["id"]) for v in rows])
for vendor in rows:
vendor["achievements"] = achievements.get(str(vendor["id"]), [])
return {"success": True, "leaderboard": rows}
Comment thread backend/main.py
Comment on lines 790 to +806
@app.get("/api/v1/vendors/leaderboard")
async def get_vendor_leaderboard():
"""Get vendor leaderboard sorted by trust score"""
try:
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}
Comment on lines +1 to +3
insert into public.vendor_achievements
(vendor_id, code, title, description, icon, tier)
select
Comment thread src/pages/Leaderboard.tsx
Comment on lines +47 to +75
const ACHIEVEMENT_ICON = {
bolt: Zap,
calendar: CalendarCheck,
crown: Crown,
shield: ShieldCheck,
};

function AchievementBadges({ achievements = [] }: { achievements?: VendorAchievement[] }) {
if (!achievements.length) return null;

return (
<div className="mt-2 flex flex-wrap gap-1.5">
{achievements.slice(0, 4).map((achievement) => {
const Icon =
ACHIEVEMENT_ICON[achievement.icon as keyof typeof ACHIEVEMENT_ICON] ?? ShieldCheck;
return (
<span
key={achievement.code}
title={`${achievement.title}: ${achievement.description}`}
className="inline-flex h-6 w-6 items-center justify-center border border-neon/50 bg-surface-lowest text-neon shadow-[0_0_14px_rgba(195,244,0,0.22)]"
aria-label={achievement.title}
>
<Icon size={13} strokeWidth={2.4} />
</span>
);
})}
</div>
);
}
Comment on lines +37 to +65
const ACHIEVEMENT_ICON = {
bolt: Zap,
calendar: CalendarCheck,
crown: Crown,
shield: ShieldCheck,
};

function AchievementBadges({ achievements = [] }: { achievements?: VendorAchievement[] }) {
if (!achievements.length) return null;

return (
<div className="mt-2 flex flex-wrap gap-1.5">
{achievements.slice(0, 4).map((achievement) => {
const Icon =
ACHIEVEMENT_ICON[achievement.icon as keyof typeof ACHIEVEMENT_ICON] ?? ShieldCheck;
return (
<span
key={achievement.code}
title={`${achievement.title}: ${achievement.description}`}
className="inline-flex h-6 w-6 items-center justify-center border border-[#c3f400]/60 bg-[#071014] text-[#c3f400] shadow-[0_0_14px_rgba(195,244,0,0.24)]"
aria-label={achievement.title}
>
<Icon size={13} strokeWidth={2.4} />
</span>
);
})}
</div>
);
}
Comment thread src/pages/Leaderboard.tsx
Comment on lines +63 to +68
<span
key={achievement.code}
title={`${achievement.title}: ${achievement.description}`}
className="inline-flex h-6 w-6 items-center justify-center border border-neon/50 bg-surface-lowest text-neon shadow-[0_0_14px_rgba(195,244,0,0.22)]"
aria-label={achievement.title}
>
Comment on lines +53 to +58
<span
key={achievement.code}
title={`${achievement.title}: ${achievement.description}`}
className="inline-flex h-6 w-6 items-center justify-center border border-[#c3f400]/60 bg-[#071014] text-[#c3f400] shadow-[0_0_14px_rgba(195,244,0,0.24)]"
aria-label={achievement.title}
>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid input: expected object, received boolean at "reviews.auto_review"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

This PR adds a vendor achievements system: a new database table with RLS/permissions, backend logic to compute streaks, clean-day counts, and top-percent rankings and award achievements, integration into scan/vendor/leaderboard/market endpoints, and frontend badge rendering on Leaderboard and Market Map pages.

Changes

Vendor Achievements

Layer / File(s) Summary
Database schema, permissions, and seed data
backend/supabase/migrations/20240702000000_add_vendor_achievements.sql, backend/supabase/migrations/20240702001000_grant_app_table_permissions.sql, backend/supabase/snippets/Untitled query 230.sql
Creates vendor_achievements table with RLS/policy/indexes, grants role permissions on vendors/scans/vendor_achievements, and adds a seed insert for the top vendor's badges.
Achievement computation and vendor metrics recalculation
backend/vendors.py
Adds ACHIEVEMENTS mapping, streak/clean-day/top-percent helpers, recalculate_vendor_metrics_and_achievements, and get_vendor_achievements; wires these into leaderboard, trust-score, and recalculate endpoints.
Scan and market API integration
backend/main.py
Reorganizes imports, normalizes scan payload fields, refreshes vendor metrics/achievements after scan creation, and attaches achievements to vendors, leaderboard, and market responses.
Frontend achievement badges
src/lib/types.ts, src/pages/Leaderboard.tsx, src/pages/MarketMapPage.tsx
Adds VendorAchievement type and extends Market/Vendor types; renders achievement badge icons on the Leaderboard vendor rows and Market Map popups/panels.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ScanEndpoint
  participant recalculate_vendor_metrics_and_achievements
  participant Database
  participant Cache

  Client->>ScanEndpoint: POST /api/v1/scan
  ScanEndpoint->>Database: insert scan record
  ScanEndpoint->>recalculate_vendor_metrics_and_achievements: _refresh_vendor_after_scan(vendor_id)
  recalculate_vendor_metrics_and_achievements->>Database: load scan history, compute metrics
  recalculate_vendor_metrics_and_achievements->>Database: update vendor trust_score/badge/trend
  recalculate_vendor_metrics_and_achievements->>Database: upsert vendor_achievements
  recalculate_vendor_metrics_and_achievements->>Cache: clear markets namespace
  ScanEndpoint-->>Client: scan payload
Loading

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding vendor achievement badges.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/main.py (1)

774-786: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Remove the shadowed vendor leaderboard route and extract the achievement-attachment helper
backend/main.py already defines /api/v1/vendors/leaderboard before vendors_router is included, so the /leaderboard handler in backend/vendors.py is shadowed. The repeated get_vendor_achievements → attach loop across these vendor/map responses should be shared to avoid drift.

🤖 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/main.py` around lines 774 - 786, The `/api/v1/vendors/leaderboard`
handler in `vendors.py` is being shadowed by the earlier route defined in
`main.py`, so remove or relocate the duplicate route registration and keep a
single authoritative leaderboard endpoint. Also extract the repeated
`get_vendor_achievements` plus per-vendor `achievements` attachment logic used
by `get_vendors` and the other vendor/map responses into a shared helper so the
behavior stays consistent. Use the existing `get_vendors`,
`get_vendor_achievements`, and `vendors_router` symbols to locate and
consolidate the affected code.
🧹 Nitpick comments (3)
src/pages/Leaderboard.tsx (1)

47-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract AchievementBadges/ACHIEVEMENT_ICON into a shared component.

This component and icon map are duplicated verbatim in src/pages/MarketMapPage.tsx (only styling classes differ). Extracting to a shared src/components/AchievementBadges.tsx avoids divergence (e.g., icon mapping updates only applied in one place) and centralizes styling.

🤖 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 `@src/pages/Leaderboard.tsx` around lines 47 - 75, `AchievementBadges` and
`ACHIEVEMENT_ICON` are duplicated in `Leaderboard.tsx` and `MarketMapPage.tsx`;
extract them into a shared `AchievementBadges` component so icon mapping and
rendering stay in sync. Move the shared logic (including the icon lookup and
slice/map behavior) into a reusable component under `src/components`, then
update both pages to import it and pass any page-specific styling or class
overrides through props instead of keeping separate copies.
backend/supabase/migrations/20240702000000_add_vendor_achievements.sql (1)

13-13: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Redundant index on vendor_id.

The UNIQUE (vendor_id, code) constraint at Line 13 already creates a composite index with vendor_id as the leading column, which Postgres can use for vendor_id-only lookups. The separate vendor_achievements_vendor_id_idx (Line 22-23) duplicates that coverage and adds unnecessary write/storage overhead.

♻️ Proposed simplification
-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);

Also applies to: 22-23

🤖 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/migrations/20240702000000_add_vendor_achievements.sql` at
line 13, The migration adds a redundant standalone index on vendor_id because
the UNIQUE (vendor_id, code) constraint in the vendor_achievements table already
provides usable coverage for vendor_id lookups. Remove the extra
vendor_achievements_vendor_id_idx creation from the migration and keep the
unique composite constraint as the only index-related definition for this access
pattern.
backend/supabase/snippets/Untitled query 230.sql (1)

10-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Static analysis: duplicate table alias.

SQLFluff flags public.vendors as reused without an alias between the outer query and the correlated subquery. This is likely a false positive (separate scopes), but adding explicit aliases would remove the ambiguity and satisfy the linter.

♻️ Proposed clarification
-from public.vendors
+from public.vendors v
 cross join (
   values
     ...
 ) as badge(code, title, description, icon, tier)
-where public.vendors.id = (
+where v.id = (
   select id from public.vendors order by avg_freshness_score desc limit 1
 )

Also applies to: 42-44

🤖 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 at line 10, The query is
using public.vendors in both the outer query and a correlated subquery without
explicit aliases, which triggers the duplicate table alias lint warning. Update
the SQL in the affected snippet to give each vendors reference a distinct alias
in the outer query and subquery, and then adjust all column references to use
those aliases consistently. This should be applied wherever the same pattern
appears in the snippet, including the referenced repeated section.

Source: Linters/SAST tools

🤖 Prompt for all review comments with 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.

Inline comments:
In `@backend/supabase/snippets/Untitled` query 230.sql:
- Around line 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.

In `@backend/vendors.py`:
- Around line 112-198: The vendor metrics refresh is doing unbounded
full-history and full-table reads on every scan, which makes the request path
grow linearly with data size. Update recalculate_vendor_metrics_and_achievements
and _top_percent_vendor_ids so they avoid fetching all scans/vendors each time,
using incremental counters and/or LIMIT/aggregate queries for the top-percent
lookup. Also move the _refresh_vendor_after_scan call off the synchronous
scan-processing path in backend/main.py into a background task or queue so scan
submission is not blocked.

---

Outside diff comments:
In `@backend/main.py`:
- Around line 774-786: The `/api/v1/vendors/leaderboard` handler in `vendors.py`
is being shadowed by the earlier route defined in `main.py`, so remove or
relocate the duplicate route registration and keep a single authoritative
leaderboard endpoint. Also extract the repeated `get_vendor_achievements` plus
per-vendor `achievements` attachment logic used by `get_vendors` and the other
vendor/map responses into a shared helper so the behavior stays consistent. Use
the existing `get_vendors`, `get_vendor_achievements`, and `vendors_router`
symbols to locate and consolidate the affected code.

---

Nitpick comments:
In `@backend/supabase/migrations/20240702000000_add_vendor_achievements.sql`:
- Line 13: The migration adds a redundant standalone index on vendor_id because
the UNIQUE (vendor_id, code) constraint in the vendor_achievements table already
provides usable coverage for vendor_id lookups. Remove the extra
vendor_achievements_vendor_id_idx creation from the migration and keep the
unique composite constraint as the only index-related definition for this access
pattern.

In `@backend/supabase/snippets/Untitled` query 230.sql:
- Line 10: The query is using public.vendors in both the outer query and a
correlated subquery without explicit aliases, which triggers the duplicate table
alias lint warning. Update the SQL in the affected snippet to give each vendors
reference a distinct alias in the outer query and subquery, and then adjust all
column references to use those aliases consistently. This should be applied
wherever the same pattern appears in the snippet, including the referenced
repeated section.

In `@src/pages/Leaderboard.tsx`:
- Around line 47-75: `AchievementBadges` and `ACHIEVEMENT_ICON` are duplicated
in `Leaderboard.tsx` and `MarketMapPage.tsx`; extract them into a shared
`AchievementBadges` component so icon mapping and rendering stay in sync. Move
the shared logic (including the icon lookup and slice/map behavior) into a
reusable component under `src/components`, then update both pages to import it
and pass any page-specific styling or class overrides through props instead of
keeping separate copies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7aa97097-0723-488d-b34f-aabb6431cff7

📥 Commits

Reviewing files that changed from the base of the PR and between aabf0bb and d296cb0.

⛔ Files ignored due to path filters (1)
  • supabase/.temp/cli-latest is excluded by !**/.temp/**
📒 Files selected for processing (9)
  • backend/main.py
  • backend/supabase/migrations/20240702000000_add_vendor_achievements.sql
  • backend/supabase/migrations/20240702001000_grant_app_table_permissions.sql
  • backend/supabase/snippets/Untitled query 230.sql
  • backend/vendors.py
  • src/lib/types.ts
  • src/pages/Leaderboard.tsx
  • src/pages/MarketMapPage.tsx
  • supabase/.branches/_current_branch

Comment on lines +1 to +45
insert into public.vendor_achievements
(vendor_id, code, title, description, icon, tier)
select
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; No newline at end of file

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.

Comment thread backend/vendors.py
Comment on lines +112 to +198
def _top_percent_vendor_ids(db) -> set[str]:
vendors = (
db.table("vendors")
.select("id, avg_freshness_score, total_scans")
.order("avg_freshness_score", desc=True)
.execute()
)
rows = [v for v in (vendors.data or []) if (v.get("total_scans") or 0) >= 5]
if not rows:
return set()
cutoff = max(1, int(len(rows) * 0.01))
return {str(v["id"]) for v in rows[:cutoff]}


def _upsert_vendor_achievement(db, vendor_id: str, code: str, metadata: dict | None = None):
achievement = ACHIEVEMENTS[code]
db.table("vendor_achievements").upsert(
{
"vendor_id": vendor_id,
"code": code,
"title": achievement["title"],
"description": achievement["description"],
"icon": achievement["icon"],
"tier": achievement["tier"],
"metadata": metadata or {},
},
on_conflict="vendor_id,code",
).execute()


def recalculate_vendor_metrics_and_achievements(db, vendor_id: str) -> dict:
"""Update vendor trust metrics and award achievements from scan history."""
scans = (
db.table("scans")
.select("freshness_index, final_grade, timestamp")
.eq("vendor_id", vendor_id)
.order("timestamp", desc=True)
.execute()
)
rows = [r for r in (scans.data or []) if r.get("freshness_index") is not None]
if not rows:
return {"total_scans": 0, "achievements": []}

scores = [r["freshness_index"] for r in rows]
total = len(scores)
avg = round(sum(scores) / total, 2)
badge = _compute_badge(avg, total)
trend = _compute_trend(db, vendor_id)

db.table("vendors").update(
{
"avg_freshness_score": avg,
"trust_score": avg,
"total_scans": total,
"trust_badge": badge,
"trend": trend,
}
).eq("id", vendor_id).execute()

awarded = []
recent_streak = _longest_recent_grade_a_streak(rows)
clean_days = _clean_grade_a_days(rows)

candidates = []
if recent_streak >= 50:
candidates.append(("fresh_streak_50", {"streak": recent_streak}))
if clean_days >= 30:
candidates.append(("consistent_30d", {"clean_days": clean_days}))
if total >= 100:
candidates.append(("trusted_volume_100", {"total_scans": total}))
if vendor_id in _top_percent_vendor_ids(db):
candidates.append(("top_1_percent", {"avg_freshness_score": avg}))

for code, metadata in candidates:
try:
_upsert_vendor_achievement(db, vendor_id, code, metadata)
awarded.append(code)
except Exception as exc:
print(f"Achievement award failed for {vendor_id}/{code}: {exc}")

return {
"avg_score": avg,
"total_scans": total,
"trust_badge": badge,
"trend": trend,
"achievements": awarded,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Unbounded full-history recompute on every scan write.

recalculate_vendor_metrics_and_achievements fetches the vendor's entire scan history with no limit/pagination (Line 144-150), and _top_percent_vendor_ids (Line 112-123) fetches every vendor row on each call. Both run synchronously inside _refresh_vendor_after_scan, which is awaited directly in the scan-processing request path in backend/main.py (Lines 510, 547). As scan volume and vendor count grow, this adds unbounded, linearly-increasing latency to every single scan submission.

Consider either:

  • Moving this recalculation to a background task (e.g., BackgroundTasks/task queue) so it doesn't block the scan response, and/or
  • Maintaining incremental counters (e.g., running streak, clean-day count) instead of recomputing from full history each time, and querying only the top-percent cutoff via LIMIT/aggregate instead of pulling all vendor rows.
🤖 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/vendors.py` around lines 112 - 198, The vendor metrics refresh is
doing unbounded full-history and full-table reads on every scan, which makes the
request path grow linearly with data size. Update
recalculate_vendor_metrics_and_achievements and _top_percent_vendor_ids so they
avoid fetching all scans/vendors each time, using incremental counters and/or
LIMIT/aggregate queries for the top-percent lookup. Also move the
_refresh_vendor_after_scan call off the synchronous scan-processing path in
backend/main.py into a background task or queue so scan submission is not
blocked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants