Add Polaris FastAPI backend: models/schemas, pricing+scoring+order router, importer, Celery tasks, API routers, seed script
This commit is contained in:
123
backend/app/routers/analytics.py
Normal file
123
backend/app/routers/analytics.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Analytics router.
|
||||
|
||||
GET /api/analytics/summary -> today revenue/orders/profit/margin/aov/conversion
|
||||
GET /api/analytics/top-products
|
||||
GET /api/analytics/supplier-performance
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, time, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Order, Supplier, SupplierPerformance
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
EXCLUDED = ["cancelled", "refunded"]
|
||||
|
||||
|
||||
def _f(value) -> float:
|
||||
try:
|
||||
return float(value or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
def summary(db: Session = Depends(get_db)):
|
||||
today_start = datetime.combine(datetime.now(timezone.utc).date(), time.min)
|
||||
|
||||
today_q = db.query(Order).filter(
|
||||
Order.created_at >= today_start, ~Order.status.in_(EXCLUDED)
|
||||
)
|
||||
all_q = db.query(Order).filter(~Order.status.in_(EXCLUDED))
|
||||
|
||||
def _kpis(query):
|
||||
orders = query.all()
|
||||
revenue = sum(_f(o.retail_total) for o in orders)
|
||||
profit = sum(_f(o.profit) for o in orders)
|
||||
count = len(orders)
|
||||
return {
|
||||
"revenue": round(revenue, 2),
|
||||
"orders": count,
|
||||
"profit": round(profit, 2),
|
||||
"gross_profit": round(revenue - profit, 2),
|
||||
"margin": round(profit / revenue * 100, 2) if revenue else 0.0,
|
||||
"aov": round(revenue / count, 2) if count else 0.0,
|
||||
# no traffic/visitor tracking table yet — conversion is not measurable
|
||||
"conversion": 0.0,
|
||||
}
|
||||
|
||||
refunds_today = (
|
||||
db.query(func.count(Order.id))
|
||||
.filter(Order.created_at >= today_start, Order.status.in_(["cancelled", "refunded"]))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
return {
|
||||
"today": {**_kpis(today_q), "refunds": refunds_today},
|
||||
"all_time": _kpis(all_q),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/top-products")
|
||||
def top_products(limit: int = 10, db: Session = Depends(get_db)):
|
||||
orders = (
|
||||
db.query(Order)
|
||||
.filter(~Order.status.in_(EXCLUDED))
|
||||
.order_by(Order.created_at.desc())
|
||||
.limit(500)
|
||||
.all()
|
||||
)
|
||||
stats = defaultdict(lambda: {"sku": None, "units": 0, "revenue": 0.0})
|
||||
for order in orders:
|
||||
for item in order.items or []:
|
||||
pid = item.get("product_id")
|
||||
if not pid:
|
||||
continue
|
||||
entry = stats[pid]
|
||||
entry["sku"] = item.get("sku")
|
||||
entry["units"] += int(item.get("qty") or 0)
|
||||
entry["revenue"] += float(item.get("unit_price") or 0) * int(item.get("qty") or 0)
|
||||
ranked = sorted(stats.items(), key=lambda kv: (-kv[1]["revenue"], -kv[1]["units"]))
|
||||
return [
|
||||
{"product_id": pid, "sku": s["sku"], "units": s["units"], "revenue": round(s["revenue"], 2)}
|
||||
for pid, s in ranked[:limit]
|
||||
]
|
||||
|
||||
|
||||
@router.get("/supplier-performance")
|
||||
def supplier_performance(db: Session = Depends(get_db)):
|
||||
out = []
|
||||
for supplier in db.query(Supplier).all():
|
||||
perf = (
|
||||
db.query(SupplierPerformance)
|
||||
.filter(SupplierPerformance.supplier_id == supplier.id)
|
||||
.first()
|
||||
)
|
||||
orders = (
|
||||
db.query(Order)
|
||||
.filter(Order.supplier_id == supplier.id, ~Order.status.in_(EXCLUDED))
|
||||
.all()
|
||||
)
|
||||
out.append(
|
||||
{
|
||||
"supplier_id": str(supplier.id),
|
||||
"name": supplier.name,
|
||||
"adapter_type": supplier.adapter_type,
|
||||
"status": supplier.status,
|
||||
"reliability_score": _f(supplier.reliability_score),
|
||||
"fulfillment_rate": _f(perf.fulfillment_rate) if perf else 100.0,
|
||||
"avg_shipping_days": _f(perf.avg_shipping_days) if perf and perf.avg_shipping_days is not None else None,
|
||||
"cancellation_rate": _f(perf.cancellation_rate) if perf else 0.0,
|
||||
"stock_accuracy": _f(perf.stock_accuracy) if perf else 100.0,
|
||||
"return_rate": _f(perf.return_rate) if perf else 0.0,
|
||||
"orders": len(orders),
|
||||
"profit": round(sum(_f(o.profit) for o in orders), 2),
|
||||
}
|
||||
)
|
||||
return out
|
||||
Reference in New Issue
Block a user