Files
orion/app/modules/orders/services/order_metrics.py
Samir Boulahtit f20266167d
Some checks failed
CI / ruff (push) Failing after 7s
CI / pytest (push) Failing after 1s
CI / architecture (push) Failing after 9s
CI / dependency-scanning (push) Successful in 27s
CI / audit (push) Successful in 8s
CI / docs (push) Has been skipped
fix(lint): auto-fix ruff violations and tune lint rules
- Auto-fixed 4,496 lint issues (import sorting, modern syntax, etc.)
- Added ignore rules for patterns intentional in this codebase:
  E402 (late imports), E712 (SQLAlchemy filters), B904 (raise from),
  SIM108/SIM105/SIM117 (readability preferences)
- Added per-file ignores for tests and scripts
- Excluded broken scripts/rename_terminology.py (has curly quotes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:10:42 +01:00

413 lines
14 KiB
Python

# app/modules/orders/services/order_metrics.py
"""
Metrics provider for the orders module.
Provides metrics for:
- Order counts and status
- Revenue metrics
- Invoice statistics
"""
import logging
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.modules.contracts.metrics import (
MetricsContext,
MetricValue,
)
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
class OrderMetricsProvider:
"""
Metrics provider for orders module.
Provides order and revenue metrics for store and platform dashboards.
"""
@property
def metrics_category(self) -> str:
return "orders"
def get_store_metrics(
self,
db: Session,
store_id: int,
context: MetricsContext | None = None,
) -> list[MetricValue]:
"""
Get order metrics for a specific store.
Provides:
- Total orders
- Orders by status
- Revenue metrics
"""
from app.modules.orders.models import Order, OrderItem
try:
# Total orders
total_orders = (
db.query(Order).filter(Order.store_id == store_id).count()
)
# Orders in period (default to last 30 days)
date_from = context.date_from if context else None
if date_from is None:
date_from = datetime.utcnow() - timedelta(days=30)
orders_in_period_query = db.query(Order).filter(
Order.store_id == store_id,
Order.created_at >= date_from,
)
if context and context.date_to:
orders_in_period_query = orders_in_period_query.filter(
Order.created_at <= context.date_to
)
orders_in_period = orders_in_period_query.count()
# Total order items
total_order_items = (
db.query(OrderItem)
.join(Order, Order.id == OrderItem.order_id)
.filter(Order.store_id == store_id)
.count()
)
# Revenue (sum of order totals) - if total_amount field exists
try:
total_revenue = (
db.query(func.sum(Order.total_amount))
.filter(Order.store_id == store_id)
.scalar()
or 0
)
revenue_in_period = (
db.query(func.sum(Order.total_amount))
.filter(
Order.store_id == store_id,
Order.created_at >= date_from,
)
.scalar()
or 0
)
except Exception:
# Field may not exist
total_revenue = 0
revenue_in_period = 0
# Average order value
avg_order_value = round(total_revenue / total_orders, 2) if total_orders > 0 else 0
return [
MetricValue(
key="orders.total",
value=total_orders,
label="Total Orders",
category="orders",
icon="shopping-cart",
description="Total orders received",
),
MetricValue(
key="orders.in_period",
value=orders_in_period,
label="Recent Orders",
category="orders",
icon="clock",
description="Orders in the selected period",
),
MetricValue(
key="orders.total_items",
value=total_order_items,
label="Total Items Sold",
category="orders",
icon="box",
description="Total order items",
),
MetricValue(
key="orders.total_revenue",
value=float(total_revenue),
label="Total Revenue",
category="orders",
icon="currency-euro",
unit="EUR",
description="Total revenue from orders",
),
MetricValue(
key="orders.revenue_period",
value=float(revenue_in_period),
label="Period Revenue",
category="orders",
icon="trending-up",
unit="EUR",
description="Revenue in the selected period",
),
MetricValue(
key="orders.avg_value",
value=avg_order_value,
label="Avg Order Value",
category="orders",
icon="calculator",
unit="EUR",
description="Average order value",
),
]
except Exception as e:
logger.warning(f"Failed to get order store metrics: {e}")
return []
def get_platform_metrics(
self,
db: Session,
platform_id: int,
context: MetricsContext | None = None,
) -> list[MetricValue]:
"""
Get order metrics aggregated for a platform.
Aggregates order data across all stores.
"""
from app.modules.orders.models import Order
from app.modules.tenancy.models import StorePlatform
try:
# Get all store IDs for this platform using StorePlatform junction table
store_ids = (
db.query(StorePlatform.store_id)
.filter(
StorePlatform.platform_id == platform_id,
StorePlatform.is_active == True,
)
.subquery()
)
# Total orders
total_orders = (
db.query(Order).filter(Order.store_id.in_(store_ids)).count()
)
# Orders in period (default to last 30 days)
date_from = context.date_from if context else None
if date_from is None:
date_from = datetime.utcnow() - timedelta(days=30)
orders_in_period_query = db.query(Order).filter(
Order.store_id.in_(store_ids),
Order.created_at >= date_from,
)
if context and context.date_to:
orders_in_period_query = orders_in_period_query.filter(
Order.created_at <= context.date_to
)
orders_in_period = orders_in_period_query.count()
# Stores with orders
stores_with_orders = (
db.query(func.count(func.distinct(Order.store_id)))
.filter(Order.store_id.in_(store_ids))
.scalar()
or 0
)
# Revenue metrics
try:
total_revenue = (
db.query(func.sum(Order.total_amount))
.filter(Order.store_id.in_(store_ids))
.scalar()
or 0
)
revenue_in_period = (
db.query(func.sum(Order.total_amount))
.filter(
Order.store_id.in_(store_ids),
Order.created_at >= date_from,
)
.scalar()
or 0
)
except Exception:
total_revenue = 0
revenue_in_period = 0
# Average order value
avg_order_value = round(total_revenue / total_orders, 2) if total_orders > 0 else 0
return [
MetricValue(
key="orders.total",
value=total_orders,
label="Total Orders",
category="orders",
icon="shopping-cart",
description="Total orders across all stores",
),
MetricValue(
key="orders.in_period",
value=orders_in_period,
label="Recent Orders",
category="orders",
icon="clock",
description="Orders in the selected period",
),
MetricValue(
key="orders.stores_with_orders",
value=stores_with_orders,
label="Stores with Orders",
category="orders",
icon="store",
description="Stores that have received orders",
),
MetricValue(
key="orders.total_revenue",
value=float(total_revenue),
label="Total Revenue",
category="orders",
icon="currency-euro",
unit="EUR",
description="Total revenue across platform",
),
MetricValue(
key="orders.revenue_period",
value=float(revenue_in_period),
label="Period Revenue",
category="orders",
icon="trending-up",
unit="EUR",
description="Revenue in the selected period",
),
MetricValue(
key="orders.avg_value",
value=avg_order_value,
label="Avg Order Value",
category="orders",
icon="calculator",
unit="EUR",
description="Average order value",
),
]
except Exception as e:
logger.warning(f"Failed to get order platform metrics: {e}")
return []
def get_customer_order_metrics(
self,
db: Session,
store_id: int,
customer_id: int,
context: MetricsContext | None = None,
) -> list[MetricValue]:
"""
Get order metrics for a specific customer.
This is an entity-level method (not dashboard-level) that provides
order statistics for a specific customer. Used by customer detail pages.
Args:
db: Database session
store_id: Store ID (for ownership verification)
customer_id: Customer ID
context: Optional filtering context
Returns:
List of MetricValue objects for this customer's order activity
"""
from app.modules.orders.models import Order
try:
# Base query for customer orders
base_query = db.query(Order).filter(
Order.customer_id == customer_id,
Order.store_id == store_id,
)
# Total orders
total_orders = base_query.count()
# Revenue stats
revenue_query = db.query(
func.sum(Order.total_amount_cents).label("total_spent_cents"),
func.avg(Order.total_amount_cents).label("avg_order_cents"),
func.max(Order.created_at).label("last_order_date"),
func.min(Order.created_at).label("first_order_date"),
).filter(
Order.customer_id == customer_id,
Order.store_id == store_id,
)
stats = revenue_query.first()
total_spent_cents = stats.total_spent_cents or 0
avg_order_cents = stats.avg_order_cents or 0
last_order_date = stats.last_order_date
first_order_date = stats.first_order_date
# Convert cents to currency
total_spent = total_spent_cents / 100
avg_order_value = avg_order_cents / 100 if avg_order_cents else 0.0
return [
MetricValue(
key="customer.total_orders",
value=total_orders,
label="Total Orders",
category="customer_orders",
icon="shopping-bag",
description="Total orders placed by this customer",
),
MetricValue(
key="customer.total_spent",
value=round(total_spent, 2),
label="Total Spent",
category="customer_orders",
icon="currency-euro",
unit="EUR",
description="Total amount spent by this customer",
),
MetricValue(
key="customer.avg_order_value",
value=round(avg_order_value, 2),
label="Avg Order Value",
category="customer_orders",
icon="calculator",
unit="EUR",
description="Average order value for this customer",
),
MetricValue(
key="customer.last_order_date",
value=last_order_date.isoformat() if last_order_date else "",
label="Last Order",
category="customer_orders",
icon="calendar",
description="Date of most recent order",
),
MetricValue(
key="customer.first_order_date",
value=first_order_date.isoformat() if first_order_date else "",
label="First Order",
category="customer_orders",
icon="calendar-plus",
description="Date of first order",
),
]
except Exception as e:
logger.warning(f"Failed to get customer order metrics: {e}")
return []
# Singleton instance
order_metrics_provider = OrderMetricsProvider()
__all__ = ["OrderMetricsProvider", "order_metrics_provider"]