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>
This commit is contained in:
2026-02-04 21:32:32 +01:00
parent bd43e21940
commit 39dff4ab7d
34 changed files with 2751 additions and 407 deletions

View File

@@ -302,6 +302,111 @@ class OrderMetricsProvider:
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()