Files
orion/app/modules/orders/services/order_metrics.py
Samir Boulahtit 39dff4ab7d refactor: fix architecture violations with provider patterns and dependency inversion
Major changes:
- Add AuditProvider protocol for cross-module audit logging
- Move customer order operations to orders module (dependency inversion)
- Add customer order metrics via MetricsProvider pattern
- Fix missing db parameter in get_admin_context() calls
- Move ProductMedia relationship to catalog module (proper ownership)
- Add marketplace breakdown stats to marketplace_widgets

New files:
- contracts/audit.py - AuditProviderProtocol
- core/services/audit_aggregator.py - Aggregates audit providers
- monitoring/services/audit_provider.py - Monitoring audit implementation
- orders/services/customer_order_service.py - Customer order operations
- orders/routes/api/vendor_customer_orders.py - Customer order endpoints
- catalog/services/product_media_service.py - Product media service
- Architecture documentation for patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 21:32:32 +01:00

414 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 (
MetricValue,
MetricsContext,
MetricsProviderProtocol,
)
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
class OrderMetricsProvider:
"""
Metrics provider for orders module.
Provides order and revenue metrics for vendor and platform dashboards.
"""
@property
def metrics_category(self) -> str:
return "orders"
def get_vendor_metrics(
self,
db: Session,
vendor_id: int,
context: MetricsContext | None = None,
) -> list[MetricValue]:
"""
Get order metrics for a specific vendor.
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.vendor_id == vendor_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.vendor_id == vendor_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.vendor_id == vendor_id)
.count()
)
# Revenue (sum of order totals) - if total_amount field exists
try:
total_revenue = (
db.query(func.sum(Order.total_amount))
.filter(Order.vendor_id == vendor_id)
.scalar()
or 0
)
revenue_in_period = (
db.query(func.sum(Order.total_amount))
.filter(
Order.vendor_id == vendor_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 vendor 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 vendors.
"""
from app.modules.orders.models import Order
from app.modules.tenancy.models import VendorPlatform
try:
# Get all vendor IDs for this platform using VendorPlatform junction table
vendor_ids = (
db.query(VendorPlatform.vendor_id)
.filter(
VendorPlatform.platform_id == platform_id,
VendorPlatform.is_active == True,
)
.subquery()
)
# Total orders
total_orders = (
db.query(Order).filter(Order.vendor_id.in_(vendor_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.vendor_id.in_(vendor_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()
# Vendors with orders
vendors_with_orders = (
db.query(func.count(func.distinct(Order.vendor_id)))
.filter(Order.vendor_id.in_(vendor_ids))
.scalar()
or 0
)
# Revenue metrics
try:
total_revenue = (
db.query(func.sum(Order.total_amount))
.filter(Order.vendor_id.in_(vendor_ids))
.scalar()
or 0
)
revenue_in_period = (
db.query(func.sum(Order.total_amount))
.filter(
Order.vendor_id.in_(vendor_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 vendors",
),
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.vendors_with_orders",
value=vendors_with_orders,
label="Vendors with Orders",
category="orders",
icon="store",
description="Vendors 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,
vendor_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
vendor_id: Vendor 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.vendor_id == vendor_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.vendor_id == vendor_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"]