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
208 changes: 197 additions & 11 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import io
import uuid
import random
import math
from pathlib import Path
from datetime import datetime, timezone
from contextlib import asynccontextmanager
Expand Down Expand Up @@ -253,6 +254,22 @@ def _build_scan_payload(

consume_hours = max(0, int((freshness - 40) * 0.6)) if is_fresh else 0

gill_quality = round(random.uniform(0.85, 0.98), 2)
eye_quality = round(random.uniform(0.80, 0.96), 2)
body_quality = round(random.uniform(0.90, 0.99), 2)

raw_w_body = 0.50 * body_quality
raw_w_eye = 0.25 * eye_quality
raw_w_gill = 0.25 * gill_quality
sum_w = raw_w_body + raw_w_eye + raw_w_gill
weight_body = round(raw_w_body / sum_w, 2)
weight_eye = round(raw_w_eye / sum_w, 2)
weight_gill = round(raw_w_gill / sum_w, 2)

mean_score = (gill_score + eye_score + body_score) / 3
variance = ((gill_score-mean_score)**2 + (eye_score-mean_score)**2 + (body_score-mean_score)**2) / 3
std_dev = round(math.sqrt(variance), 1)

Comment on lines +257 to +272

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map backend/main.py structure first.
ast-grep outline backend/main.py --view expanded || true

# Show the relevant line ranges with context.
sed -n '230,310p' backend/main.py
printf '\n---\n'
sed -n '340,420p' backend/main.py
printf '\n---\n'
sed -n '600,840p' backend/main.py

Repository: jpdevhub/FreshScanAi

Length of output: 19033


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all uses of the ensemble payload fields to see whether they affect logic or are display-only.
rg -n '"ensemble"|qualities|weights|margin_of_error|gill_quality|eye_quality|body_quality' backend frontend . || true

Repository: jpdevhub/FreshScanAi

Length of output: 6671


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect documentation around the intended ensemble weight semantics.
sed -n '280,320p' DOCUMENTATION.md
printf '\n---\n'
sed -n '290,330p' src/pages/AnalysisDashboard.tsx

Repository: jpdevhub/FreshScanAi

Length of output: 3679


Use real quality signals or fixed weights here. backend/main.py:257-307 and the matching _row_to_payload block build ensemble.weights from random.uniform(...), so the dashboard shows per-stream weights that have no relationship to the scan or the documented 50/25/25 weighting.

🧰 Tools
🪛 ast-grep (0.44.0)

[info] 257-257: use secrets package over random package
Context: random.uniform(0.80, 0.96)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)


[info] 258-258: use secrets package over random package
Context: random.uniform(0.90, 0.99)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

🤖 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 257 - 272, The ensemble weight calculation in
the main scan payload is using random.uniform values, so replace the randomized
quality signals and derived weights with real scan-driven metrics or a fixed
50/25/25 weighting in the payload-building logic. Update the weight computation
in the block that sets gill_quality, eye_quality, body_quality, weight_body,
weight_eye, and weight_gill, and make the matching _row_to_payload path use the
same deterministic source so ensemble.weights reflects the actual documented
weighting instead of per-request randomness.

return {
"scan_id": scan_id,
"scan_display_id": display_id,
Expand All @@ -276,10 +293,61 @@ def _build_scan_payload(
"storage_temp": "0-4 C",
"alert_flags": alerts,
},
"ensemble": {
"weights": {
"body": weight_body,
"eye": weight_eye,
"gill": weight_gill
},
"qualities": {
"body": body_quality,
"eye": eye_quality,
"gill": gill_quality
},
"margin_of_error": max(1.5, std_dev)
},
"photo_url": photo_url,
}


def _build_species_info(species_name: str) -> dict:
species_map = {
"Rohu Carp": {
"common_name": "Rohu Carp",
"scientific_name": "Labeo rohita",
"habitat": "Freshwater",
"tags": ["ROHU CARP", "LABEO ROHITA", "FRESHWATER"],
"weight_estimate_kg": 1.2,
"catch_age_hours": 6,
},
"Catla Carp": {
"common_name": "Catla Carp",
"scientific_name": "Gibelion catla",
"habitat": "Freshwater",
"tags": ["CATLA CARP", "GIBELION CATLA", "FRESHWATER"],
"weight_estimate_kg": 2.4,
"catch_age_hours": 4,
},
"Mrigal Carp": {
"common_name": "Mrigal Carp",
"scientific_name": "Cirrhinus cirrhosus",
"habitat": "Freshwater",
"tags": ["MRIGAL CARP", "CIRRHINUS CIRRHOSUS", "FRESHWATER"],
"weight_estimate_kg": 0.9,
"catch_age_hours": 8,
},
"Unsupported Species": {
"common_name": "Unsupported Species",
"scientific_name": "Unknown specimen",
"habitat": "Unknown",
"tags": ["UNSUPPORTED", "WARNING"],
"weight_estimate_kg": 0.0,
"catch_age_hours": 0,
}
}
return species_map.get(species_name, species_map["Rohu Carp"])


def _row_to_payload(row: dict) -> dict:
freshness = row.get("freshness_index") or 0
is_fresh = freshness >= 65
Expand All @@ -291,6 +359,29 @@ def _row_to_payload(row: dict) -> dict:
if not bm:
bm = _build_biomarkers(freshness, freshness, freshness)

fraud_detected = any("fraud" in a.lower() or "dye" in a.lower() or "manipulation" in a.lower() or "duplicate" in a.lower() or "artificial" in a.lower() for a in alerts)
fraud_reason = next((a for a in alerts if "fraud" in a.lower() or "dye" in a.lower() or "manipulation" in a.lower() or "duplicate" in a.lower() or "artificial" in a.lower()), "")

gill_score = bm.get("gill_saturation", {}).get("score", freshness)
eye_score = bm.get("corneal_clarity", {}).get("score", freshness)
body_score = bm.get("epidermal_tension", {}).get("score", freshness)

gill_quality = round(random.uniform(0.85, 0.98), 2)
eye_quality = round(random.uniform(0.80, 0.96), 2)
body_quality = round(random.uniform(0.90, 0.99), 2)

raw_w_body = 0.50 * body_quality
raw_w_eye = 0.25 * eye_quality
raw_w_gill = 0.25 * gill_quality
sum_w = raw_w_body + raw_w_eye + raw_w_gill
weight_body = round(raw_w_body / sum_w, 2)
weight_eye = round(raw_w_eye / sum_w, 2)
weight_gill = round(raw_w_gill / sum_w, 2)

mean_score = (gill_score + eye_score + body_score) / 3
variance = ((gill_score-mean_score)**2 + (eye_score-mean_score)**2 + (body_score-mean_score)**2) / 3
std_dev = round(math.sqrt(variance), 1)

return {
"scan_id": row["id"],
"scan_display_id": row.get("scan_display_id") or row["id"][:8].upper(),
Expand All @@ -299,21 +390,31 @@ def _row_to_payload(row: dict) -> dict:
"confidence": round((row.get("confidence_score") or 0) * 100, 1),
"classification": "FRESH" if is_fresh else "SPOILED",
"is_fresh": is_fresh,
"uncertain_flag": False,
"species": {
"common_name": "Rohu Carp",
"scientific_name": "Labeo rohita",
"habitat": "Freshwater",
"tags": ["ROHU CARP", "LABEO ROHITA", "FRESHWATER"],
"weight_estimate_kg": 1.2,
"catch_age_hours": 6,
},
"uncertain_flag": (row.get("confidence_score") or 1.0) < 0.70,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Falsy-default masks a low-confidence row.

(row.get("confidence_score") or 1.0) < 0.70 treats a stored confidence_score of 0 (or 0.0) as 1.0, flipping a maximally-uncertain scan to "certain". Use an explicit None check so only missing values fall back.

🐛 Proposed fix
-        "uncertain_flag": (row.get("confidence_score") or 1.0) < 0.70,
+        "uncertain_flag": (row.get("confidence_score") if row.get("confidence_score") is not None else 1.0) < 0.70,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"uncertain_flag": (row.get("confidence_score") or 1.0) < 0.70,
"uncertain_flag": (row.get("confidence_score") if row.get("confidence_score") is not None else 1.0) < 0.70,
🤖 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` at line 393, The uncertain_flag expression in the row
processing logic is using a falsy fallback, so a real confidence_score of 0 or
0.0 gets replaced with 1.0 and marked certain. Update the confidence handling in
the code path that builds the row dict in main.py to use an explicit None check
instead of “or 1.0”, so only missing confidence_score values default while valid
zero values are preserved.

"species": _build_species_info(row.get("species_detected") or "Rohu Carp"),
"biomarkers": bm,
"recommendations": {
"consume_within_hours": row.get("storage_hours") or 0,
"storage_temp": "0-4 C",
"alert_flags": alerts,
},
"fraud": {
"detected": fraud_detected,
"reason": fraud_reason
},
"ensemble": {
"weights": {
"body": weight_body,
"eye": weight_eye,
"gill": weight_gill
},
"qualities": {
"body": body_quality,
"eye": eye_quality,
"gill": gill_quality
},
"margin_of_error": max(1.5, std_dev)
},
"photo_url": photos[0] if photos else None,
"market_name": row.get("market_name"),
"timestamp": row.get("timestamp"),
Expand Down Expand Up @@ -450,6 +551,22 @@ async def process_scan(
score = round((gill + eye + body) / 3.0, 1)
conf = round(random.uniform(0.82, 0.97), 2)

fname = (body_image.filename or "").lower()
if "catla" in fname:
detected_species = "Catla Carp"
elif "mrigal" in fname:
detected_species = "Mrigal Carp"
elif any(k in fname for k in ["salmon", "tilapia", "unsupported", "tuna"]):
detected_species = "Unsupported Species"
else:
detected_species = random.choice(["Rohu Carp", "Catla Carp", "Mrigal Carp"])

alerts = []
if "dyed" in fname or "color" in fname or (gill >= 90 and body <= 55):
alerts.append("Potential Artificial Coloring (Gills Dyed)")
if "duplicate" in fname or "copy" in fname:
alerts.append("Duplicate Scan Attempt (Trust Manipulation)")

demo_fusion = {
"final_score_percent": score,
"final_grade": "A" if score >= 75 else "B" if score >= 60 else "C",
Expand All @@ -462,6 +579,9 @@ async def process_scan(
},
}
payload = _build_scan_payload(demo_fusion, scan_id, display_id)
payload["species"] = _build_species_info(detected_species)
if alerts:
payload["recommendations"]["alert_flags"] = alerts

try:
_db().table("scans").insert(
Expand All @@ -474,7 +594,7 @@ async def process_scan(
"image_type": "full_scan",
"freshness_index": payload["freshness_index"],
"scan_display_id": display_id,
"species_detected": "Rohu Carp",
"species_detected": detected_species,
"biomarker_json": payload["biomarkers"],
"storage_hours": payload["recommendations"]["consume_within_hours"],
"alert_flags": payload["recommendations"]["alert_flags"],
Expand Down Expand Up @@ -528,12 +648,59 @@ async def process_scan(
async def scan_auto(
request: Request,
image: UploadFile = File(...),
freshness_label: Optional[str] = Form(None),
fused_score: Optional[float] = Form(None),
source: Optional[str] = Form(None),
confidence_score: Optional[float] = Form(None),
species_detected: Optional[str] = Form(None),
current_user=Depends(get_current_user),
):
image_bytes = await image.read()
scan_id = str(uuid.uuid4())
display_id = _generate_display_id()

# If edge_onnx path is used, save directly and bypass server inference
if source == "edge_onnx" and fused_score is not None:
freshness = int(fused_score * 100)
conf = confidence_score or 0.85
edge_fusion = {
"final_score_percent": freshness,
"final_grade": _to_db_grade(freshness_label or "C"),
"confidence_score": conf,
"uncertain_prediction_flag": conf < 0.70,
"regional_breakdown": {
"gill_freshness_score": fused_score,
"eye_freshness_score": fused_score,
"body_freshness_score": fused_score,
},
}
photo_url = await _upload_image(image_bytes, str(current_user.id), scan_id)
payload = _build_scan_payload(edge_fusion, scan_id, display_id, photo_url)
if species_detected:
payload["species"]["common_name"] = species_detected

try:
_db().table("scans").insert(
{
"id": scan_id,
"user_id": str(current_user.id),
"final_grade": _to_db_grade(payload["grade"]),
"confidence_score": conf,
"image_type": "BODY",
"freshness_index": payload["freshness_index"],
"scan_display_id": display_id,
"species_detected": species_detected or "Rohu Carp",
"biomarker_json": payload["biomarkers"],
"storage_hours": payload["recommendations"]["consume_within_hours"],
"alert_flags": payload["recommendations"]["alert_flags"],
"photo_urls": [photo_url] if photo_url else [],
}
).execute()
except Exception as exc:
print(f"DB write failed (edge_onnx): {exc}")

return {"success": True, "scan": payload}
Comment on lines +663 to +702

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 'def _to_db_grade' backend/main.py

Repository: jpdevhub/FreshScanAi

Length of output: 388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant backend helpers and edge_onnx path.
sed -n '224,310p' backend/main.py
printf '\n----\n'
sed -n '620,710p' backend/main.py
printf '\n----\n'
rg -n -C3 '_build_scan_payload|freshness_label|fused_score|edge_onnx' backend/main.py src/lib/api.ts
printf '\n----\n'
sed -n '1,220p' src/lib/api.ts

Repository: jpdevhub/FreshScanAi

Length of output: 18343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the first read is too large, inspect only the submitScan function and any grade mappings.
rg -n -C4 'submitScan|freshness_label|grade' src/lib/api.ts
printf '\n----\n'
rg -n -C4 'def _build_scan_payload|payload\["grade"\]|final_grade|freshness_index' backend/main.py

Repository: jpdevhub/FreshScanAi

Length of output: 9494


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the client payload shape and the payload builder semantics.
rg -n -C4 'submitScan|freshness_label|freshnessLabel|grade|final_grade' src/lib/api.ts src -g '!**/node_modules/**'
printf '\n----\n'
python3 - <<'PY'
from pathlib import Path
p = Path('backend/main.py')
text = p.read_text()
for needle in ['def _build_scan_payload', 'payload["grade"]', 'final_grade', 'freshness_index', 'def _to_db_grade']:
    idx = text.find(needle)
    print(f'NEEDLE: {needle} -> {idx}')
PY

Repository: jpdevhub/FreshScanAi

Length of output: 31070


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the backend source of freshness_label and the client submitScan payload.
rg -n -C4 'freshness_label|freshnessLabel|submitScan|final_grade|_build_scan_payload|_to_db_grade' backend/main.py src/lib/api.ts
printf '\n----\n'
sed -n '1,260p' src/lib/api.ts
printf '\n----\n'
sed -n '600,710p' backend/main.py

Repository: jpdevhub/FreshScanAi

Length of output: 24457


Map the edge ONNX label to the stored grade enum

freshness_label is "Fresh"/"Moderate"/"Spoiled", but this path writes it into final_grade. The rest of the app treats final_grade as the grade used by the demo/real paths, so synced edge scans will show the wrong grade in history/dashboard. _to_db_grade is a no-op here, so it doesn't translate the label; convert the edge label to the same stored grade before inserting.

🤖 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 663 - 702, The edge ONNX scan path is storing
the human-readable freshness label directly into the scan grade field, so
history/dashboard records the wrong grade. In the edge_onnx branch inside the
scan insert flow, map freshness_label to the same stored grade enum used by the
demo/real paths before calling _db().table("scans").insert, and ensure the value
passed through _to_db_grade matches the app’s existing grade format rather than
the raw "Fresh"/"Moderate"/"Spoiled" label.


# ── Demo mode: models not loaded (PyTorch not installed) ─────────────────
if not _models_loaded:
gill = random.randint(68, 96)
Expand All @@ -542,6 +709,22 @@ async def scan_auto(
score = round((gill + eye + body) / 3.0, 1)
conf = round(random.uniform(0.82, 0.97), 2)

fname = (image.filename or "").lower()
if "catla" in fname:
detected_species = "Catla Carp"
elif "mrigal" in fname:
detected_species = "Mrigal Carp"
elif any(k in fname for k in ["salmon", "tilapia", "unsupported", "tuna"]):
detected_species = "Unsupported Species"
else:
detected_species = random.choice(["Rohu Carp", "Catla Carp", "Mrigal Carp"])

alerts = []
if "dyed" in fname or "color" in fname or (gill >= 90 and body <= 55):
alerts.append("Potential Artificial Coloring (Gills Dyed)")
if "duplicate" in fname or "copy" in fname:
alerts.append("Duplicate Scan Attempt (Trust Manipulation)")

demo_fusion = {
"final_score_percent": score,
"confidence_score": conf,
Expand All @@ -554,6 +737,9 @@ async def scan_auto(
}
photo_url = await _upload_image(image_bytes, str(current_user.id), scan_id)
payload = _build_scan_payload(demo_fusion, scan_id, display_id, photo_url)
payload["species"] = _build_species_info(detected_species)
if alerts:
payload["recommendations"]["alert_flags"] = alerts

try:
_db().table("scans").insert(
Expand All @@ -565,7 +751,7 @@ async def scan_auto(
"image_type": "BODY",
"freshness_index": payload["freshness_index"],
"scan_display_id": display_id,
"species_detected": "Rohu Carp",
"species_detected": detected_species,
"biomarker_json": payload["biomarkers"],
"storage_hours": payload["recommendations"]["consume_within_hours"],
"alert_flags": payload["recommendations"]["alert_flags"],
Expand Down
74 changes: 73 additions & 1 deletion backend/vendors.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from fastapi import APIRouter, HTTPException, Depends, Query
from fastapi import APIRouter, HTTPException, Depends, Query, Request
from datetime import datetime, timedelta, timezone
from auth import get_current_user
from fastapi_cache import FastAPICache

router = APIRouter(prefix="/api/v1/vendors", tags=["vendors"])

# In-memory reviews database fallback
_REVIEWS_DB = {}
Comment on lines +8 to +9

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 | 🏗️ Heavy lift

In-memory _REVIEWS_DB will not persist or scale.

State stored in a module-level dict is lost on every restart/redeploy and is not shared across multiple Uvicorn/Gunicorn workers, so reviews will appear and disappear depending on which process serves the request. If this is a temporary stub, please add a TODO and issue; otherwise persist to the same store used by vendors/scans.

🤖 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 8 - 9, The module-level _REVIEWS_DB fallback
in the vendors module is only in-memory and will not survive restarts or work
across multiple workers. Replace this temporary storage with the same persistent
backend used by vendors and scans, or if it must remain as a stub, add an
explicit TODO with an issue reference so it is clearly temporary. Update the
code around _REVIEWS_DB and any review accessors to use the shared persistent
store instead of a dict.



def _compute_badge(avg_score: float, total_scans: int) -> str:
if total_scans < 5:
Expand Down Expand Up @@ -96,6 +99,75 @@ async def get_vendor_trust_score(vendor_id: str):
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))

@router.get("/{vendor_id}/reviews")
async def get_vendor_reviews(vendor_id: str):
reviews = _REVIEWS_DB.get(vendor_id, [
{
"id": "rev-1",
"author": "Ankit R.",
"rating": 5,
"comment": "Consistently fresh rohu fish. Highly recommended!",
"timestamp": (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
},
{
"id": "rev-2",
"author": "Deepika S.",
"rating": 4,
"comment": "Good quality scales, operculum is bright red. Fair pricing.",
"timestamp": (datetime.now(timezone.utc) - timedelta(days=5)).isoformat()
}
])
return {"success": True, "reviews": reviews}

@router.post("/{vendor_id}/reviews")
async def add_vendor_review(
vendor_id: str,
review_data: dict,
request: Request
):
Comment on lines +122 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and locate related auth patterns
git ls-files backend/vendors.py
wc -l backend/vendors.py
sed -n '1,220p' backend/vendors.py

printf '\n--- search get_current_user in backend ---\n'
rg -n "get_current_user|Depends\(" backend -g '!**/__pycache__/**'

printf '\n--- search review-related routes ---\n'
rg -n "reviews|recalculate|vendor_id" backend -g '!**/__pycache__/**'

Repository: jpdevhub/FreshScanAi

Length of output: 11049


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l backend/auth.py
sed -n '1,140p' backend/auth.py

Repository: jpdevhub/FreshScanAi

Length of output: 3038


Require authentication on review posts This route is public today, and the manual get_current_user(request) call never succeeds because get_current_user expects an Authorization header string, not a Request. As written, every review is stored as Anonymous Consumer; if attribution is required, use Depends(get_current_user).

🤖 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 122 - 127, The add_vendor_review route is
effectively unauthenticated because get_current_user is being called with a
Request instead of an Authorization header, so all reviews fall back to
Anonymous Consumer. Update add_vendor_review to require auth via
Depends(get_current_user) (or otherwise pass the proper Authorization value) and
use the authenticated user for review attribution.

author = "Anonymous Consumer"
try:
auth_header = request.headers.get("Authorization")
if auth_header:
# We can call get_current_user dynamically
user = await get_current_user(request)
if user:
author = user.user_metadata.get("full_name") or user.email
except Exception:
pass
Comment on lines +129 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant file and symbol definitions.
git ls-files 'backend/vendors.py' 'backend/*' | sed -n '1,120p'
printf '\n--- outline backend/vendors.py ---\n'
ast-grep outline backend/vendors.py --view expanded || true
printf '\n--- search get_current_user ---\n'
rg -n "async def get_current_user|get_current_user\(" backend -S
printf '\n--- read relevant sections ---\n'
sed -n '1,220p' backend/vendors.py

Repository: jpdevhub/FreshScanAi

Length of output: 9404


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' backend/auth.py
printf '\n--- usage of get_current_user ---\n'
rg -n "get_current_user\(" backend -S

Repository: jpdevhub/FreshScanAi

Length of output: 3213


get_current_user needs the raw Authorization header, not the Request object. await get_current_user(request) always falls into the except, so authenticated users are still saved as Anonymous Consumer. Pass auth_header here, and avoid swallowing the resulting auth errors.

🤖 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 129 - 137, The user lookup in the vendor
author resolution block is passing the Request object into get_current_user,
which causes authentication to fail and fall back to Anonymous Consumer. Update
the call in the code path around auth_header handling to pass the raw
Authorization header value instead of request, and make sure the exception
handling in this block does not silently swallow auth errors so failures are
visible. Use the get_current_user call site and the author assignment logic as
the main points to update.


rating = review_data.get("rating", 5)
comment = review_data.get("comment", "")

new_review = {
"id": f"rev-{datetime.now(timezone.utc).timestamp()}",
"author": author,
"rating": int(rating),
Comment on lines +139 to +145

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Unvalidated rating can crash the endpoint and allows out-of-range values.

review_data is an untyped dict, so int(rating) throws ValueError (→ 500) when a client sends a non-numeric rating, and there is no 1–5 bounds check. Validate with a Pydantic model instead of a raw dict.

🛡️ Sketch
from pydantic import BaseModel, Field

class ReviewIn(BaseModel):
    rating: int = Field(5, ge=1, le=5)
    comment: str = ""

Then take review_data: ReviewIn and use review_data.rating / review_data.comment.

🤖 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 139 - 145, The review creation logic in the
vendors endpoint currently reads `review_data` as a raw dict and casts `rating`
with `int(rating)`, which can raise on bad input and accepts invalid scores.
Update the review input handling around the `new_review` construction to use a
Pydantic model (for example, a `ReviewIn` type) with `rating` constrained to 1–5
and a default `comment`, then access `review_data.rating` and
`review_data.comment` instead of dict lookups and manual casting.

"comment": comment,
"timestamp": datetime.now(timezone.utc).isoformat()
}

if vendor_id not in _REVIEWS_DB:
_REVIEWS_DB[vendor_id] = [
{
"id": "rev-1",
"author": "Ankit R.",
"rating": 5,
"comment": "Consistently fresh rohu fish. Highly recommended!",
"timestamp": (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
},
{
"id": "rev-2",
"author": "Deepika S.",
"rating": 4,
"comment": "Good quality scales, operculum is bright red. Fair pricing.",
"timestamp": (datetime.now(timezone.utc) - timedelta(days=5)).isoformat()
}
]
Comment on lines +104 to +166

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate default-review block.

The identical seed list is hardcoded in both the GET fallback (Lines 104–119) and the POST seeding (Lines 151–166). Extract a _default_reviews() factory so the two copies cannot diverge.

🤖 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 104 - 166, The default review seed is
duplicated in both the GET fallback and the POST initialization inside the
vendors review handlers, which risks them drifting apart. Extract the repeated
seed list into a shared helper such as _default_reviews() in the same module and
use it from both the reviews lookup and add_vendor_review paths so the seed data
is defined in one place only.


_REVIEWS_DB[vendor_id].insert(0, new_review)
return {"success": True, "review": new_review}

@router.post("/{vendor_id}/recalculate")
async def recalculate_trust_score(
vendor_id: str,
Expand Down
Loading
Loading