133 lines
4.4 KiB
Python
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
|