Backend scores updates on products

This commit is contained in:
sriram
2026-09-05 11:53:13 +05:30
parent 0c36628d4b
commit 7ac3571b59
10 changed files with 984 additions and 2 deletions

View File

@@ -86,6 +86,18 @@ def _row_to_product_out(row: dict, fallback_brand: str) -> ProductOut:
if fssai is not None:
fssai = str(fssai).strip() or None
# NUMERIC arrives as decimal.Decimal, which Pydantic would coerce but JSON
# would not. Converted here so 0.0 survives - `or None` would turn a real
# score of zero into "not scored", and zero is a valid health score.
def _score(key: str) -> Optional[float]:
raw = row.get(key)
if raw is None:
return None
try:
return float(raw)
except (TypeError, ValueError):
return None
return ProductOut(
image_id=image_id,
image_url=primary_url,
@@ -108,6 +120,8 @@ def _row_to_product_out(row: dict, fallback_brand: str) -> ProductOut:
selling_price=sp,
barcode=bcd,
barcode_type=bcd_type,
nutrition_score=_score("nutrition_score"),
health_score=_score("health_score"),
)

View File

@@ -569,6 +569,8 @@ async def upload_nutrition_file(file: UploadFile = File(...)) -> Dict[str, Any]:
skipped = 0
skipped_non_consumable = 0
status_counts = {"verified": 0, "partial": 0, "unavailable": 0}
# Which brands to mirror onto the brand tables once the import commits.
touched_brands: set = set()
try:
with conn, conn.cursor() as cur:
@@ -752,6 +754,7 @@ async def upload_nutrition_file(file: UploadFile = File(...)) -> Dict[str, Any]:
)
imported_count += 1
touched_brands.add(brand)
except HTTPException:
raise
@@ -761,6 +764,14 @@ async def upload_nutrition_file(file: UploadFile = File(...)) -> Dict[str, Any]:
finally:
conn.close()
# Mirror onto the brand tables, so a consumer reading product rows directly
# sees an uploaded score too. After the commit and outside the try, because
# this must never turn a successful import into a 500 - sync_after_write
# swallows its own failures for the same reason.
if touched_brands:
from app.services.nutrition_score_sync import sync_after_write
sync_after_write(sorted(touched_brands))
return _finish(
file.filename, len(df), imported_count, errors, skipped,
"nutritional intelligence items",

View File

@@ -32,6 +32,10 @@ class ProductOut(BaseModel):
selling_price: Optional[float] = None
barcode: Optional[str] = None
barcode_type: Optional[str] = None
# Mirrored from nutrition_insights onto the brand table by
# nutrition_score_sync. None means "not scored yet", never "scored zero".
nutrition_score: Optional[float] = None
health_score: Optional[float] = None
class SourceProductOut(BaseModel):
@@ -56,6 +60,10 @@ class SourceProductOut(BaseModel):
selling_price: Optional[float] = None
barcode: Optional[str] = None
barcode_type: Optional[str] = None
# Mirrored from nutrition_insights onto the brand table by
# nutrition_score_sync. None means "not scored yet", never "scored zero".
nutrition_score: Optional[float] = None
health_score: Optional[float] = None
similarity: float