diff --git a/backend/app/api/shopping.py b/backend/app/api/shopping.py index fa6d948..8fc57fa 100644 --- a/backend/app/api/shopping.py +++ b/backend/app/api/shopping.py @@ -2,7 +2,7 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import Response -from sqlalchemy import select +from sqlalchemy import select, text from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -216,6 +216,94 @@ async def delete_item( return Response(status_code=204) +@router.post("/lists/generate", response_model=ShoppingListDetailResponse, status_code=201) +async def generate_magic_list(session: AsyncSession = Depends(get_session)): + """Génère une liste à partir du score de fréquence (retard / intervalle moyen).""" + + query = text(""" + WITH achats AS ( + SELECT + COALESCE(li.custom_name, p.name) AS nom, + li.product_id, + li.custom_name, + p.default_unit, + sl.created_at::date AS date_achat + FROM shopping.list_items li + JOIN shopping.lists sl ON sl.id = li.list_id + LEFT JOIN shopping.products p ON p.id = li.product_id + WHERE li.is_checked = true + ), + achats_with_lag AS ( + SELECT + nom, + product_id, + custom_name, + default_unit, + date_achat, + date_achat - LAG(date_achat) OVER (PARTITION BY nom ORDER BY date_achat) AS interval_days + FROM achats + ), + stats AS ( + SELECT + nom, + product_id, + custom_name, + default_unit, + MAX(date_achat) AS last_purchased, + COUNT(*) AS nb_achats, + AVG(interval_days) AS avg_interval_days + FROM achats_with_lag + GROUP BY nom, product_id, custom_name, default_unit + ) + SELECT + nom, + product_id, + custom_name, + default_unit, + last_purchased, + nb_achats, + COALESCE(avg_interval_days, 30) AS avg_interval_days, + (CURRENT_DATE - last_purchased)::float + / NULLIF(COALESCE(avg_interval_days, 30), 0) AS score + FROM stats + WHERE (CURRENT_DATE - last_purchased)::float + / NULLIF(COALESCE(avg_interval_days, 30), 0) >= 0.7 + ORDER BY score DESC + LIMIT 50 + """) + + result = await session.execute(query) + rows = result.mappings().all() + + new_list = ShoppingList(name="Liste magique", status="draft") + session.add(new_list) + await session.flush() + + for row in rows: + session.add(ListItem( + list_id=new_list.id, + product_id=row["product_id"], + custom_name=row["custom_name"], + unit=row["default_unit"], + )) + + await session.commit() + + stmt = ( + select(ShoppingList) + .where(ShoppingList.id == new_list.id) + .options(selectinload(ShoppingList.items).selectinload(ListItem.product)) + ) + result2 = await session.execute(stmt) + new_list = result2.scalar_one() + sorted_items = sorted(new_list.items, key=lambda i: (i.sort_order or 999, str(i.id))) + + return ShoppingListDetailResponse( + **_list_to_response(new_list).model_dump(), + items=[_item_to_response(i) for i in sorted_items], + ) + + @router.post("/lists/{list_id}/finish", response_model=ShoppingListDetailResponse) async def finish_shopping(list_id: uuid.UUID, session: AsyncSession = Depends(get_session)): stmt = (