feat: initial commit
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
"""KPI / analytics query functions.
|
||||
|
||||
All functions return plain dicts or lists of dicts.
|
||||
Uses text() for raw SQL to avoid ORM lazy-loading surprises.
|
||||
Every function accepts date_from / date_to ISO-date strings to scope metrics.
|
||||
"""
|
||||
from datetime import date as _date
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
def _parse_date(s: str) -> _date:
|
||||
"""Convert ISO date string to datetime.date for asyncpg compatibility."""
|
||||
return _date.fromisoformat(s)
|
||||
|
||||
|
||||
async def order_throughput_by_week(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> list[dict]:
|
||||
"""Weekly order creation + completion counts within the date range."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT
|
||||
TO_CHAR(DATE_TRUNC('week', created_at), 'IYYY-"W"IW') AS week,
|
||||
COUNT(*) AS count,
|
||||
COUNT(*) FILTER (WHERE status = 'completed') AS completed
|
||||
FROM orders
|
||||
WHERE created_at >= CAST(:date_from AS date)
|
||||
AND created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY DATE_TRUNC('week', created_at)
|
||||
ORDER BY DATE_TRUNC('week', created_at)
|
||||
"""
|
||||
)
|
||||
result = await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})
|
||||
rows = result.fetchall()
|
||||
return [{"week": r[0], "count": r[1], "completed": r[2]} for r in rows]
|
||||
|
||||
|
||||
async def processing_time_stats(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> dict:
|
||||
"""Average and percentile processing times for completed orders in range."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT
|
||||
EXTRACT(EPOCH FROM AVG(completed_at - submitted_at))::FLOAT
|
||||
AS avg_submit_to_complete_s,
|
||||
EXTRACT(EPOCH FROM AVG(processing_started_at - submitted_at))::FLOAT
|
||||
AS avg_submit_to_processing_s,
|
||||
EXTRACT(EPOCH FROM PERCENTILE_CONT(0.5) WITHIN GROUP (
|
||||
ORDER BY completed_at - submitted_at
|
||||
))::FLOAT AS p50_s,
|
||||
EXTRACT(EPOCH FROM PERCENTILE_CONT(0.95) WITHIN GROUP (
|
||||
ORDER BY completed_at - submitted_at
|
||||
))::FLOAT AS p95_s
|
||||
FROM orders
|
||||
WHERE status = 'completed'
|
||||
AND submitted_at IS NOT NULL
|
||||
AND completed_at IS NOT NULL
|
||||
AND submitted_at >= CAST(:date_from AS date)
|
||||
AND submitted_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
"""
|
||||
)
|
||||
result = await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})
|
||||
row = result.fetchone()
|
||||
if row is None:
|
||||
return {
|
||||
"avg_submit_to_complete_s": None,
|
||||
"avg_submit_to_processing_s": None,
|
||||
"p50_s": None,
|
||||
"p95_s": None,
|
||||
}
|
||||
return {
|
||||
"avg_submit_to_complete_s": row[0],
|
||||
"avg_submit_to_processing_s": row[1],
|
||||
"p50_s": row[2],
|
||||
"p95_s": row[3],
|
||||
}
|
||||
|
||||
|
||||
async def revenue_overview(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> list[dict]:
|
||||
"""Monthly revenue (sum of estimated_price for completed orders) in range."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT
|
||||
TO_CHAR(DATE_TRUNC('month', completed_at), 'YYYY-MM') AS month,
|
||||
COALESCE(SUM(estimated_price), 0)::FLOAT AS revenue,
|
||||
COUNT(*) AS order_count
|
||||
FROM orders
|
||||
WHERE status = 'completed'
|
||||
AND completed_at >= CAST(:date_from AS date)
|
||||
AND completed_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY DATE_TRUNC('month', completed_at)
|
||||
ORDER BY DATE_TRUNC('month', completed_at)
|
||||
"""
|
||||
)
|
||||
result = await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})
|
||||
rows = result.fetchall()
|
||||
return [{"month": r[0], "revenue": r[1], "order_count": r[2]} for r in rows]
|
||||
|
||||
|
||||
async def item_status_breakdown(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> dict:
|
||||
"""Count of order lines grouped by item_status, scoped to orders in range."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT ol.item_status, COUNT(*) AS cnt
|
||||
FROM order_lines ol
|
||||
JOIN orders o ON o.id = ol.order_id
|
||||
WHERE o.created_at >= CAST(:date_from AS date)
|
||||
AND o.created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY ol.item_status
|
||||
"""
|
||||
)
|
||||
result = await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})
|
||||
rows = result.fetchall()
|
||||
out: dict = {"pending": 0, "approved": 0, "rejected": 0}
|
||||
for row in rows:
|
||||
key = str(row[0])
|
||||
out[key] = int(row[1])
|
||||
return out
|
||||
|
||||
|
||||
async def render_time_breakdown(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> dict:
|
||||
"""Average render duration from completed order lines, scoped to date range.
|
||||
|
||||
Uses render_started_at / render_completed_at on order_lines (added in migration 015).
|
||||
avg_stl_s is not tracked at order-line level, so only avg_render_s and sample_count
|
||||
are meaningful here; avg_stl_s is left None for UI compatibility.
|
||||
"""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT
|
||||
AVG(EXTRACT(EPOCH FROM (render_completed_at - render_started_at))) AS avg_render_s,
|
||||
COUNT(*) AS sample_count
|
||||
FROM order_lines
|
||||
WHERE render_status = 'completed'
|
||||
AND render_started_at IS NOT NULL
|
||||
AND render_completed_at IS NOT NULL
|
||||
AND render_completed_at >= CAST(:date_from AS date)
|
||||
AND render_completed_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
"""
|
||||
)
|
||||
result = await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})
|
||||
row = result.fetchone()
|
||||
if row is None or row[1] == 0:
|
||||
return {"avg_stl_s": None, "avg_render_s": None, "avg_total_s": None, "sample_count": 0}
|
||||
return {
|
||||
"avg_stl_s": None,
|
||||
"avg_render_s": float(row[0]) if row[0] is not None else None,
|
||||
"avg_total_s": float(row[0]) if row[0] is not None else None,
|
||||
"sample_count": int(row[1]),
|
||||
}
|
||||
|
||||
|
||||
async def render_time_by_output_type(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> list[dict]:
|
||||
"""Render time statistics per output type for completed order lines."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT
|
||||
COALESCE(ot.name, 'Unknown') AS output_type,
|
||||
COUNT(*) AS job_count,
|
||||
AVG(EXTRACT(EPOCH FROM (ol.render_completed_at - ol.render_started_at))) AS avg_render_s,
|
||||
MIN(EXTRACT(EPOCH FROM (ol.render_completed_at - ol.render_started_at))) AS min_render_s,
|
||||
MAX(EXTRACT(EPOCH FROM (ol.render_completed_at - ol.render_started_at))) AS max_render_s,
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP (
|
||||
ORDER BY EXTRACT(EPOCH FROM (ol.render_completed_at - ol.render_started_at))
|
||||
) AS p50_render_s
|
||||
FROM order_lines ol
|
||||
LEFT JOIN output_types ot ON ot.id = ol.output_type_id
|
||||
WHERE ol.render_status = 'completed'
|
||||
AND ol.render_started_at IS NOT NULL
|
||||
AND ol.render_completed_at IS NOT NULL
|
||||
AND ol.render_completed_at >= CAST(:date_from AS date)
|
||||
AND ol.render_completed_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY ot.id, ot.name
|
||||
ORDER BY avg_render_s DESC NULLS LAST
|
||||
"""
|
||||
)
|
||||
result = await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})
|
||||
return [
|
||||
{
|
||||
"output_type": r[0],
|
||||
"job_count": int(r[1]),
|
||||
"avg_render_s": float(r[2]) if r[2] is not None else None,
|
||||
"min_render_s": float(r[3]) if r[3] is not None else None,
|
||||
"max_render_s": float(r[4]) if r[4] is not None else None,
|
||||
"p50_render_s": float(r[5]) if r[5] is not None else None,
|
||||
}
|
||||
for r in result.fetchall()
|
||||
]
|
||||
|
||||
|
||||
async def top_level_summary(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> dict:
|
||||
"""High-level summary counts and totals within the date range."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT
|
||||
COUNT(*) AS total_orders,
|
||||
COUNT(*) FILTER (WHERE status = 'completed') AS completed_orders,
|
||||
COALESCE(SUM(estimated_price) FILTER (WHERE status = 'completed'), 0)::FLOAT
|
||||
AS total_revenue
|
||||
FROM orders
|
||||
WHERE created_at >= CAST(:date_from AS date)
|
||||
AND created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
"""
|
||||
)
|
||||
result = await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})
|
||||
row = result.fetchone()
|
||||
|
||||
items_sql = text(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM order_lines ol
|
||||
JOIN orders o ON o.id = ol.order_id
|
||||
WHERE ol.output_type_id IS NOT NULL
|
||||
AND o.created_at >= CAST(:date_from AS date)
|
||||
AND o.created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
"""
|
||||
)
|
||||
items_result = await db.execute(items_sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})
|
||||
items_count = items_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"total_orders": int(row[0]) if row else 0,
|
||||
"completed_orders": int(row[1]) if row else 0,
|
||||
"total_revenue": float(row[2]) if row else 0.0,
|
||||
"total_rendering_items": int(items_count),
|
||||
}
|
||||
|
||||
|
||||
async def product_and_category_stats(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> dict:
|
||||
"""Product-level stats: unique rendered, total products, CAD coverage, by category."""
|
||||
params = {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)}
|
||||
|
||||
rendered_sql = text(
|
||||
"""
|
||||
SELECT COUNT(DISTINCT ol.product_id)
|
||||
FROM order_lines ol
|
||||
JOIN orders o ON o.id = ol.order_id
|
||||
WHERE ol.render_status = 'completed'
|
||||
AND o.created_at >= CAST(:date_from AS date)
|
||||
AND o.created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
"""
|
||||
)
|
||||
rendered = (await db.execute(rendered_sql, params)).scalar() or 0
|
||||
|
||||
totals_sql = text(
|
||||
"""
|
||||
SELECT COUNT(*) AS total, COUNT(cad_file_id) AS with_cad
|
||||
FROM products
|
||||
"""
|
||||
)
|
||||
totals_row = (await db.execute(totals_sql)).fetchone()
|
||||
total_products = int(totals_row[0]) if totals_row else 0
|
||||
products_with_cad = int(totals_row[1]) if totals_row else 0
|
||||
|
||||
cat_sql = text(
|
||||
"""
|
||||
SELECT COALESCE(category_key, 'unknown') AS category, COUNT(*) AS cnt
|
||||
FROM products
|
||||
GROUP BY category_key
|
||||
ORDER BY cnt DESC
|
||||
"""
|
||||
)
|
||||
cat_rows = (await db.execute(cat_sql)).fetchall()
|
||||
|
||||
return {
|
||||
"unique_products_rendered": int(rendered),
|
||||
"total_products": total_products,
|
||||
"products_with_cad": products_with_cad,
|
||||
"products_by_category": [
|
||||
{"category": r[0], "count": int(r[1])} for r in cat_rows
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def output_type_usage(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> list[dict]:
|
||||
"""Order lines grouped by output type name."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT ot.name AS output_type, COUNT(*) AS cnt
|
||||
FROM order_lines ol
|
||||
JOIN output_types ot ON ot.id = ol.output_type_id
|
||||
JOIN orders o ON o.id = ol.order_id
|
||||
WHERE o.created_at >= CAST(:date_from AS date)
|
||||
AND o.created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY ot.name
|
||||
ORDER BY cnt DESC
|
||||
"""
|
||||
)
|
||||
rows = (await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})).fetchall()
|
||||
return [{"output_type": r[0], "count": int(r[1])} for r in rows]
|
||||
|
||||
|
||||
async def render_status_distribution(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> tuple[dict, list[dict]]:
|
||||
"""(a) order_lines by render_status, (b) cad_files by renderer from render_log."""
|
||||
params = {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)}
|
||||
|
||||
status_sql = text(
|
||||
"""
|
||||
SELECT ol.render_status, COUNT(*) AS cnt
|
||||
FROM order_lines ol
|
||||
JOIN orders o ON o.id = ol.order_id
|
||||
WHERE o.created_at >= CAST(:date_from AS date)
|
||||
AND o.created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY ol.render_status
|
||||
"""
|
||||
)
|
||||
status_rows = (await db.execute(status_sql, params)).fetchall()
|
||||
status_map: dict = {"pending": 0, "processing": 0, "completed": 0, "failed": 0}
|
||||
for row in status_rows:
|
||||
key = str(row[0])
|
||||
status_map[key] = int(row[1])
|
||||
|
||||
renderer_sql = text(
|
||||
"""
|
||||
SELECT render_log->>'renderer' AS renderer, COUNT(*) AS cnt
|
||||
FROM cad_files
|
||||
WHERE render_log IS NOT NULL
|
||||
AND render_log->>'renderer' IS NOT NULL
|
||||
AND created_at >= CAST(:date_from AS date)
|
||||
AND created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY render_log->>'renderer'
|
||||
ORDER BY cnt DESC
|
||||
"""
|
||||
)
|
||||
renderer_rows = (await db.execute(renderer_sql, params)).fetchall()
|
||||
renderer_usage = [{"renderer": r[0], "count": int(r[1])} for r in renderer_rows]
|
||||
|
||||
return status_map, renderer_usage
|
||||
|
||||
|
||||
async def top_products(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
limit: int = 10,
|
||||
) -> list[dict]:
|
||||
"""Top N most-ordered products by order line count."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT p.pim_id, p.name AS product_name,
|
||||
COALESCE(p.category_key, 'unknown') AS category,
|
||||
COUNT(*) AS order_count
|
||||
FROM order_lines ol
|
||||
JOIN products p ON p.id = ol.product_id
|
||||
JOIN orders o ON o.id = ol.order_id
|
||||
WHERE o.created_at >= CAST(:date_from AS date)
|
||||
AND o.created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY p.id, p.pim_id, p.name, p.category_key
|
||||
ORDER BY order_count DESC
|
||||
LIMIT :lim
|
||||
"""
|
||||
)
|
||||
rows = (await db.execute(sql, {
|
||||
"date_from": _parse_date(date_from),
|
||||
"date_to": _parse_date(date_to),
|
||||
"lim": limit,
|
||||
})).fetchall()
|
||||
return [
|
||||
{"pim_id": r[0], "product_name": r[1], "category": r[2], "order_count": int(r[3])}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
async def category_revenue(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> list[dict]:
|
||||
"""Proportional revenue by category: order price / line count, summed per category."""
|
||||
sql = text(
|
||||
"""
|
||||
WITH order_share AS (
|
||||
SELECT o.id AS order_id,
|
||||
COALESCE(o.estimated_price, 0) / GREATEST(COUNT(ol.id), 1) AS per_line_price,
|
||||
COALESCE(p.category_key, 'unknown') AS category
|
||||
FROM orders o
|
||||
JOIN order_lines ol ON ol.order_id = o.id
|
||||
JOIN products p ON p.id = ol.product_id
|
||||
WHERE o.status = 'completed'
|
||||
AND o.completed_at >= CAST(:date_from AS date)
|
||||
AND o.completed_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY o.id, o.estimated_price, p.category_key
|
||||
)
|
||||
SELECT category,
|
||||
COUNT(DISTINCT order_id) AS order_count,
|
||||
COALESCE(SUM(per_line_price), 0)::FLOAT AS revenue
|
||||
FROM order_share
|
||||
GROUP BY category
|
||||
ORDER BY revenue DESC
|
||||
"""
|
||||
)
|
||||
rows = (await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})).fetchall()
|
||||
return [{"category": r[0], "order_count": int(r[1]), "revenue": r[2]} for r in rows]
|
||||
|
||||
|
||||
async def render_backend_stats(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> list[dict]:
|
||||
"""Render time + count by backend (celery vs flamenco)."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT
|
||||
COALESCE(render_backend_used, 'unknown') AS backend,
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE render_status = 'completed') AS completed,
|
||||
COUNT(*) FILTER (WHERE render_status = 'failed') AS failed,
|
||||
EXTRACT(EPOCH FROM AVG(
|
||||
render_completed_at - render_started_at
|
||||
) FILTER (WHERE render_status = 'completed'))::FLOAT AS avg_render_s,
|
||||
EXTRACT(EPOCH FROM PERCENTILE_CONT(0.5) WITHIN GROUP (
|
||||
ORDER BY render_completed_at - render_started_at
|
||||
) FILTER (WHERE render_status = 'completed'))::FLOAT AS p50_render_s
|
||||
FROM order_lines ol
|
||||
JOIN orders o ON o.id = ol.order_id
|
||||
WHERE render_backend_used IS NOT NULL
|
||||
AND o.created_at >= CAST(:date_from AS date)
|
||||
AND o.created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY render_backend_used
|
||||
ORDER BY total DESC
|
||||
"""
|
||||
)
|
||||
rows = (await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})).fetchall()
|
||||
return [
|
||||
{
|
||||
"backend": r[0],
|
||||
"total": int(r[1]),
|
||||
"completed": int(r[2]),
|
||||
"failed": int(r[3]),
|
||||
"avg_render_s": r[4],
|
||||
"p50_render_s": r[5],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
async def orders_by_user(
|
||||
db: AsyncSession, date_from: str, date_to: str,
|
||||
) -> list[dict]:
|
||||
"""Orders grouped by user with counts and revenue."""
|
||||
sql = text(
|
||||
"""
|
||||
SELECT u.full_name, u.email, u.role,
|
||||
COUNT(*) AS order_count,
|
||||
COALESCE(SUM(o.estimated_price) FILTER (WHERE o.status = 'completed'), 0)::FLOAT AS revenue
|
||||
FROM orders o
|
||||
JOIN users u ON u.id = o.created_by
|
||||
WHERE o.created_at >= CAST(:date_from AS date)
|
||||
AND o.created_at < CAST(:date_to AS date) + INTERVAL '1 day'
|
||||
GROUP BY u.id, u.full_name, u.email, u.role
|
||||
ORDER BY order_count DESC
|
||||
"""
|
||||
)
|
||||
rows = (await db.execute(sql, {"date_from": _parse_date(date_from), "date_to": _parse_date(date_to)})).fetchall()
|
||||
return [
|
||||
{"full_name": r[0], "email": r[1], "role": r[2], "order_count": int(r[3]), "revenue": r[4]}
|
||||
for r in rows
|
||||
]
|
||||
Reference in New Issue
Block a user