Files

133 lines
4.4 KiB
Python

"""Products router.
GET /api/products (filters: status, category, search, limit, offset)
GET /api/products/{id}
POST /api/products/{id}/publish
POST /api/products/{id}/pause
GET /api/products/{id}/price-history
POST /api/products/import (body: {supplier_id, feed_type, data_or_url})
NOTE: /import is declared before /{id} so it isn't captured by the path param.
"""
from decimal import Decimal
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import or_
from sqlalchemy.orm import Session
from app.database import get_db
from app.engines.importer import import_feed
from app.models import AuditLog, PriceHistory, Product, Supplier
from app.schemas import ImportRequest, PriceHistoryOut, ProductOut
router = APIRouter()
PUBLISHABLE_FROM = {"imported", "price_calculated", "content_generated", "quality_check", "paused"}
def _get_product_or_404(db, product_id):
product = db.query(Product).filter(Product.id == product_id).first()
if product is None:
raise HTTPException(status_code=404, detail="product not found")
return product
@router.get("", response_model=List[ProductOut])
def list_products(
status: Optional[str] = None,
category: Optional[str] = None,
search: Optional[str] = None,
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
):
q = db.query(Product)
if status:
q = q.filter(Product.status == status)
if category:
q = q.filter(Product.category == category)
if search:
like = f"%{search}%"
q = q.filter(or_(Product.title.ilike(like), Product.sku.ilike(like)))
return q.order_by(Product.created_at.desc()).offset(offset).limit(limit).all()
@router.post("/import")
def import_products(body: ImportRequest, db: Session = Depends(get_db)):
supplier = db.query(Supplier).filter(Supplier.id == body.supplier_id).first()
if supplier is None:
raise HTTPException(status_code=404, detail="supplier not found")
try:
summary = import_feed(db, supplier, feed_type=body.feed_type, data_or_url=body.data_or_url)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
db.add(
AuditLog(
actor="api",
action="import_requested",
entity="supplier",
entity_id=str(supplier.id),
detail={"feed_type": body.feed_type, "received": summary["received"]},
)
)
db.commit()
return summary
@router.get("/{product_id}", response_model=ProductOut)
def get_product(product_id, db: Session = Depends(get_db)):
return _get_product_or_404(db, product_id)
@router.post("/{product_id}/publish", response_model=ProductOut)
def publish_product(product_id, db: Session = Depends(get_db)):
product = _get_product_or_404(db, product_id)
if product.status not in PUBLISHABLE_FROM:
raise HTTPException(status_code=400, detail=f"cannot publish product in status '{product.status}'")
if product.retail_price is None or Decimal(str(product.retail_price)) <= 0:
raise HTTPException(status_code=400, detail="product has no calculated retail price")
product.status = "published"
db.add(
AuditLog(
actor="api",
action="product_published",
entity="product",
entity_id=str(product.id),
detail={"sku": product.sku, "retail_price": float(product.retail_price)},
)
)
db.commit()
db.refresh(product)
return product
@router.post("/{product_id}/pause", response_model=ProductOut)
def pause_product(product_id, db: Session = Depends(get_db)):
product = _get_product_or_404(db, product_id)
product.status = "paused"
db.add(
AuditLog(
actor="api",
action="product_paused",
entity="product",
entity_id=str(product.id),
detail={"sku": product.sku},
)
)
db.commit()
db.refresh(product)
return product
@router.get("/{product_id}/price-history", response_model=List[PriceHistoryOut])
def price_history(product_id, db: Session = Depends(get_db)):
_get_product_or_404(db, product_id)
rows = (
db.query(PriceHistory)
.filter(PriceHistory.product_id == product_id)
.order_by(PriceHistory.created_at.desc())
.all()
)
return rows