diff --git a/app/modules/billing/services/billing_metrics.py b/app/modules/billing/services/billing_metrics.py index 7fbc4e8c..4d5eb803 100644 --- a/app/modules/billing/services/billing_metrics.py +++ b/app/modules/billing/services/billing_metrics.py @@ -108,6 +108,45 @@ class BillingMetricsProvider: logger.warning(f"Failed to get billing platform metrics: {e}") return [] + def get_merchant_metrics( + self, + db: Session, + merchant_id: int, + context: MetricsContext | None = None, + ) -> list[MetricValue]: + """ + Get subscription metrics for a specific merchant. + + Provides: + - Active subscriptions (active + trial) + """ + from app.modules.billing.models import MerchantSubscription + + try: + active_subs = ( + db.query(func.count(MerchantSubscription.id)) + .filter( + MerchantSubscription.merchant_id == merchant_id, + MerchantSubscription.status.in_(["active", "trial"]), + ) + .scalar() + or 0 + ) + + return [ + MetricValue( + key="billing.active_subscriptions", + value=active_subs, + label="Active Subscriptions", + category="billing", + icon="clipboard-list", + description="Active or trial subscriptions for this merchant", + ), + ] + except Exception as e: + logger.warning(f"Failed to get billing merchant metrics: {e}") + return [] + # Singleton instance billing_metrics_provider = BillingMetricsProvider() diff --git a/app/modules/contracts/metrics.py b/app/modules/contracts/metrics.py index c4c949c1..c7a67e94 100644 --- a/app/modules/contracts/metrics.py +++ b/app/modules/contracts/metrics.py @@ -207,6 +207,28 @@ class MetricsProviderProtocol(Protocol): """ ... + def get_merchant_metrics( + self, + db: "Session", + merchant_id: int, + context: MetricsContext | None = None, + ) -> list[MetricValue]: + """ + Get metrics scoped to a specific merchant. + + Called by the merchant dashboard to display merchant-scoped statistics. + Should only include data belonging to the specified merchant. + + Args: + db: Database session for queries + merchant_id: ID of the merchant to get metrics for + context: Optional filtering/scoping context + + Returns: + List of MetricValue objects for this merchant + """ + ... + __all__ = [ "MetricValue", diff --git a/app/modules/core/routes/api/merchant.py b/app/modules/core/routes/api/merchant.py new file mode 100644 index 00000000..88f95b1e --- /dev/null +++ b/app/modules/core/routes/api/merchant.py @@ -0,0 +1,22 @@ +# app/modules/core/routes/api/merchant.py +""" +Core module merchant API routes. + +Auto-discovered by the route system (merchant.py in routes/api/ triggers +registration under /api/v1/merchants/core/*). + +Aggregates: +- /dashboard/* - Merchant dashboard statistics +""" + +from fastapi import APIRouter + +from .merchant_dashboard import merchant_dashboard_router + +ROUTE_CONFIG = { + "prefix": "/core", +} + +router = APIRouter() + +router.include_router(merchant_dashboard_router, tags=["merchant-dashboard"]) diff --git a/app/modules/core/routes/api/merchant_dashboard.py b/app/modules/core/routes/api/merchant_dashboard.py new file mode 100644 index 00000000..11fa010f --- /dev/null +++ b/app/modules/core/routes/api/merchant_dashboard.py @@ -0,0 +1,58 @@ +# app/modules/core/routes/api/merchant_dashboard.py +""" +Merchant dashboard statistics endpoint. + +Merchant Context: Uses get_merchant_for_current_user to resolve the merchant +from the JWT token. The dependency guarantees an active merchant exists. + +This module uses the StatsAggregator service from core to collect metrics from all +enabled modules via the MetricsProvider protocol. +""" + +import logging + +from fastapi import APIRouter, Depends, Request +from sqlalchemy.orm import Session + +from app.api.deps import get_merchant_for_current_user +from app.core.database import get_db +from app.modules.core.schemas.dashboard import MerchantDashboardStatsResponse +from app.modules.core.services.stats_aggregator import stats_aggregator + +merchant_dashboard_router = APIRouter(prefix="/dashboard") +logger = logging.getLogger(__name__) + + +@merchant_dashboard_router.get("/stats", response_model=MerchantDashboardStatsResponse) +def get_merchant_dashboard_stats( + request: Request, + merchant=Depends(get_merchant_for_current_user), + db: Session = Depends(get_db), +): + """ + Get merchant-scoped dashboard statistics. + + Returns aggregated statistics for the current merchant: + - Active subscriptions + - Total stores + - Total customers across all stores + - Team members across all stores + + Merchant is resolved from the JWT token. + Requires Authorization header (API endpoint). + """ + platform = getattr(request.state, "platform", None) + platform_id = platform.id if platform else 1 + + flat = stats_aggregator.get_merchant_stats_flat( + db=db, + merchant_id=merchant.id, + platform_id=platform_id, + ) + + return MerchantDashboardStatsResponse( + active_subscriptions=int(flat.get("billing.active_subscriptions", 0)), + total_stores=int(flat.get("tenancy.total_stores", 0)), + total_customers=int(flat.get("customers.total", 0)), + team_members=int(flat.get("tenancy.team_members", 0)), + ) diff --git a/app/modules/core/schemas/dashboard.py b/app/modules/core/schemas/dashboard.py index be5ed065..f1601e5f 100644 --- a/app/modules/core/schemas/dashboard.py +++ b/app/modules/core/schemas/dashboard.py @@ -221,6 +221,23 @@ class StoreDashboardStatsResponse(BaseModel): revenue: StoreRevenueStats +# ============================================================================ +# Merchant Dashboard Statistics +# ============================================================================ + + +class MerchantDashboardStatsResponse(BaseModel): + """Merchant dashboard statistics response schema. + + Used by: GET /api/v1/merchants/core/dashboard/stats + """ + + active_subscriptions: int = Field(0, description="Active or trial subscriptions") + total_stores: int = Field(0, description="Total stores owned by this merchant") + total_customers: int = Field(0, description="Total customers across all stores") + team_members: int = Field(0, description="Distinct active team members across stores") + + __all__ = [ # Stats responses "StatsResponse", @@ -240,4 +257,6 @@ __all__ = [ "StoreRevenueStats", "StoreInfo", "StoreDashboardStatsResponse", + # Merchant dashboard + "MerchantDashboardStatsResponse", ] diff --git a/app/modules/core/services/stats_aggregator.py b/app/modules/core/services/stats_aggregator.py index 90f6aff1..f8f636d2 100644 --- a/app/modules/core/services/stats_aggregator.py +++ b/app/modules/core/services/stats_aggregator.py @@ -196,6 +196,68 @@ class StatsAggregatorService: categorized = self.get_store_dashboard_stats(db, store_id, platform_id, context) return self._flatten_metrics(categorized) + def get_merchant_dashboard_stats( + self, + db: Session, + merchant_id: int, + platform_id: int, + context: MetricsContext | None = None, + ) -> dict[str, list[MetricValue]]: + """ + Get all metrics for a merchant, grouped by category. + + Called by the merchant dashboard to display merchant-scoped statistics. + + Args: + db: Database session + merchant_id: ID of the merchant to get metrics for + platform_id: Platform ID (for module enablement check) + context: Optional filtering/scoping context + + Returns: + Dict mapping category name to list of MetricValue objects + """ + providers = self._get_enabled_providers(db, platform_id) + result: dict[str, list[MetricValue]] = {} + + for module, provider in providers: + if not hasattr(provider, "get_merchant_metrics"): + continue + try: + metrics = provider.get_merchant_metrics(db, merchant_id, context) + if metrics: + result[provider.metrics_category] = metrics + except Exception as e: + logger.warning( + f"Failed to get merchant metrics from module {module.code}: {e}" + ) + + return result + + def get_merchant_stats_flat( + self, + db: Session, + merchant_id: int, + platform_id: int, + context: MetricsContext | None = None, + ) -> dict[str, Any]: + """ + Get merchant metrics as a flat dictionary. + + Args: + db: Database session + merchant_id: ID of the merchant to get metrics for + platform_id: Platform ID (for module enablement check) + context: Optional filtering/scoping context + + Returns: + Flat dict mapping metric keys to values + """ + categorized = self.get_merchant_dashboard_stats( + db, merchant_id, platform_id, context + ) + return self._flatten_metrics(categorized) + def get_admin_stats_flat( self, db: Session, diff --git a/app/modules/core/templates/core/merchant/dashboard.html b/app/modules/core/templates/core/merchant/dashboard.html index 2a388f98..a0d9d0e1 100644 --- a/app/modules/core/templates/core/merchant/dashboard.html +++ b/app/modules/core/templates/core/merchant/dashboard.html @@ -13,7 +13,7 @@ -
Current Plan
---
+Customers
+--
+Team Members
+--