Files
orion/docs/proposals/decouple-modules.md
Samir Boulahtit 35d1559162
Some checks failed
CI / ruff (push) Successful in 10s
CI / pytest (push) Failing after 47m30s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat(monitoring): add Redis exporter + Sentry docs to deployment guide
- Add redis-exporter container to docker-compose (oliver006/redis_exporter, 32MB)
- Add Redis scrape target to Prometheus config
- Add 4 Redis alert rules: RedisDown, HighMemory, HighConnections, RejectedConnections
- Document Step 19b (Sentry Error Tracking) in Hetzner deployment guide
- Document Step 19c (Redis Monitoring) in Hetzner deployment guide
- Update resource budget and port reference tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 23:30:18 +01:00

63 KiB

Analytics Dependency Status: NOT FIXED ─

The MetricsProvider pattern exists in contracts/metrics.py, but admin_stores.py still has hard imports:

File: app/modules/tenancy/routes/api/admin_stores.py ┌──────┬────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────┐ │ Line │ Import │ Used In │ ├──────┼────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────┤ │ 20 │ from app.modules.analytics.services.stats_service import stats_service │ get_store_statistics_endpoint() (line 110) │ ├──────┼────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────┤ │ 23 │ from app.modules.analytics.schemas import StoreStatsResponse │ Return type (line 104) │ └──────┴────────────────────────────────────────────────────────────────────────┴─────────────────────────────────────────────┘

All Core → Optional Dependencies

  1. tenancy → marketplace

File: app/modules/tenancy/models/init.py:22 from app.modules.marketplace.models.marketplace_import_job import MarketplaceImportJob

  • Purpose: SQLAlchemy needs this to resolve User.marketplace_import_jobs and Store.marketplace_import_jobs relationships

File: app/modules/tenancy/services/store_service.py ┌──────┬─────────────────────────────────────┬───────────────────────────────────────────────────────────────────────┐ │ Line │ Import │ Functions Using It │ ├──────┼─────────────────────────────────────┼───────────────────────────────────────────────────────────────────────┤ │ 19 │ MarketplaceProductNotFoundException │ add_product_to_catalog() :463, _get_product_by_id_or_raise() :571 │ ├──────┼─────────────────────────────────────┼───────────────────────────────────────────────────────────────────────┤ │ 26 │ MarketplaceProduct model │ _get_product_by_id_or_raise() :565-566, add_product_to_catalog() :466 │ └──────┴─────────────────────────────────────┴───────────────────────────────────────────────────────────────────────┘

  1. tenancy → catalog

File: app/modules/tenancy/services/store_service.py ┌──────┬───────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐ │ Line │ Import │ Functions Using It │ ├──────┼───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ 18 │ ProductAlreadyExistsException │ add_product_to_catalog() :472 │ ├──────┼───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ 27 │ Product model │ add_product_to_catalog() :477, get_products() :533, _product_in_catalog() :579 │ ├──────┼───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤ │ 30 │ ProductCreate schema │ add_product_to_catalog() parameter type :447 │ └──────┴───────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘

  1. billing → catalog

File: app/modules/billing/services/subscription_service.py:48 ┌───────────────────────┬─────────┬─────────────────────────────────────────────────────────────────────────┐ │ Function │ Lines │ What It Does │ ├───────────────────────┼─────────┼─────────────────────────────────────────────────────────────────────────┤ │ get_usage() │ 371-376 │ db.query(func.count(Product.id)).filter(Product.store_id == store_id) │ ├───────────────────────┼─────────┼─────────────────────────────────────────────────────────────────────────┤ │ get_usage_summary() │ 420-425 │ Same product count query │ ├───────────────────────┼─────────┼─────────────────────────────────────────────────────────────────────────┤ │ can_add_product() │ 514-519 │ Same product count query │ ├───────────────────────┼─────────┼─────────────────────────────────────────────────────────────────────────┤ │ check_product_limit() │ 532-537 │ Same product count query │ └───────────────────────┴─────────┴─────────────────────────────────────────────────────────────────────────┘ File: app/modules/billing/services/admin_subscription_service.py:30 ┌───────────────────────────┬─────────┬─────────────────────────────────────────────────────────────────────────┐ │ Function │ Lines │ What It Does │ ├───────────────────────────┼─────────┼─────────────────────────────────────────────────────────────────────────┤ │ get_store_usage_counts() │ 212-217 │ db.query(func.count(Product.id)).filter(Product.store_id == store_id) │ └───────────────────────────┴─────────┴─────────────────────────────────────────────────────────────────────────┘ File: app/modules/billing/services/capacity_forecast_service.py:19 ┌──────────────────────────┬──────┬───────────────────────────────────────────┐ │ Function │ Line │ What It Does │ ├──────────────────────────┼──────┼───────────────────────────────────────────┤ │ capture_daily_snapshot() │ 91 │ db.query(func.count(Product.id)).scalar() │ └──────────────────────────┴──────┴───────────────────────────────────────────┘

Summary: What to Move Where? ┌──────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Dependency │ Recommendation │ ├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤ │ admin_stores.py → analytics │ Use the existing MetricsProvider pattern via stats_aggregator │ ├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤ │ store_service.py → marketplace/catalog │ Move add_product_to_catalog() and get_products() to catalog module - these are product operations │ ├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤ │ tenancy/models/init.py → marketplace │ Remove MarketplaceImportJob import, move relationships to marketplace module │ ├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────┤ │ billing/* → catalog (Product count) │ Create ProductCountProvider protocol in contracts │ └──────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────┘ Would you like me to start working on any of these?


Modular Architecture Analysis

Executive Summary

This analysis answers two questions:

  1. Are all modules self-contained and respecting architecture rules?
  2. Would the app still run if non-core modules are removed?

Key Findings:

  • Route registration is fully auto-discovered (no hardcoded module routes)
  • Middleware has no module-specific dependencies
  • API routers use dynamic discovery pattern
  • CRITICAL: Core modules have hard dependencies on optional modules - the app would CRASH if optional modules are removed

Part 1: Architecture Rule Compliance

What's Working Well ┌─────────────────────────┬────────┬────────────────────────────────────────────────┐ │ Aspect │ Status │ Details │ ├─────────────────────────┼────────┼────────────────────────────────────────────────┤ │ Module auto-discovery │ │ All modules discovered via definition.py │ ├─────────────────────────┼────────┼────────────────────────────────────────────────┤ │ Route auto-registration │ │ Admin, store, storefront routes fully dynamic │ ├─────────────────────────┼────────┼────────────────────────────────────────────────┤ │ Middleware stack │ │ No hardcoded module dependencies │ ├─────────────────────────┼────────┼────────────────────────────────────────────────┤ │ Static file mounting │ │ Dynamic scanning of module directories │ ├─────────────────────────┼────────┼────────────────────────────────────────────────┤ │ Module enablement │ │ Controlled via PlatformModule.is_enabled │ └─────────────────────────┴────────┴────────────────────────────────────────────────┘ Previous Issues Fixed (This Session) ┌───────────────────────────────────┬──────────┬─────────────────────────────────────────────────────────────┐ │ Issue │ Status │ Fix Applied │ ├───────────────────────────────────┼──────────┼─────────────────────────────────────────────────────────────┤ │ Module definition variable names │ Fixed │ catalog, checkout, cart renamed to module │ ├───────────────────────────────────┼──────────┼─────────────────────────────────────────────────────────────┤ │ Router attachment functions │ Fixed │ Added get_module_with_routers() functions │ ├───────────────────────────────────┼──────────┼─────────────────────────────────────────────────────────────┤ │ Billing exceptions location │ Fixed │ Moved to exceptions.py with backwards-compat aliases │ ├───────────────────────────────────┼──────────┼─────────────────────────────────────────────────────────────┤ │ StoreEmailSettingsService DI │ Fixed │ Changed to db-as-parameter pattern │ ├───────────────────────────────────┼──────────┼─────────────────────────────────────────────────────────────┤ │ StoreDomainService exception bug │ Fixed │ Added proper re-raise for DomainVerificationFailedException │ ├───────────────────────────────────┼──────────┼─────────────────────────────────────────────────────────────┤ │ StoreTeamService exception bug │ Fixed │ Fixed CannotRemoveOwnerException arguments │ └───────────────────────────────────┴──────────┴─────────────────────────────────────────────────────────────┘

Part 2: Critical Architecture Violations

Core → Optional Module Dependencies

The app would NOT run if optional modules are removed. Core modules have hard imports from optional modules:

tenancy (core) → optional modules

tenancy/models/init.py:22 → from app.modules.marketplace.models import MarketplaceImportJob

tenancy/services/admin_service.py:36,40 → from app.modules.marketplace.models import MarketplaceImportJob → from app.modules.marketplace.schemas import MarketplaceImportJobResponse

tenancy/services/store_service.py:18,19,26,27,30 → from app.modules.catalog.exceptions import ProductAlreadyExistsException → from app.modules.marketplace.exceptions import MarketplaceProductNotFoundException → from app.modules.marketplace.models import MarketplaceProduct → from app.modules.catalog.models import Product → from app.modules.catalog.schemas import ProductCreate

tenancy/services/store_team_service.py:34 → from app.modules.billing.exceptions import TierLimitExceededException

tenancy/routes/api/admin_stores.py:20,23,348,399 → from app.modules.analytics.services.stats_service import stats_service → from app.modules.analytics.schemas import StoreStatsResponse → from app.modules.marketplace.services import letzshop_export_service

tenancy/routes/api/admin_platform_users.py:18 → from app.modules.analytics.services.stats_service import stats_service

core (core) → analytics (optional)

core/routes/api/admin_dashboard.py:14,16 → from app.modules.analytics.services.stats_service import stats_service → from app.modules.analytics.schemas import StoreStatsResponse, ...

core/routes/api/store_dashboard.py:17,20 → from app.modules.analytics.services.stats_service import stats_service → from app.modules.analytics.schemas import StoreStatsResponse, ...

cms (core) → optional modules

cms/models/init.py:14, cms/models/media.py:9, cms/services/media_service.py:31 → from app.modules.catalog.models import ProductMedia

cms/routes/pages/platform.py:17 → from app.modules.billing.models import TIER_LIMITS, TierCode

cms/services/store_email_settings_service.py:33 → from app.modules.billing.models import StoreSubscription, TierCode

customers (core) → orders (optional)

customers/services/customer_service.py:332,368 → from app.modules.orders.models import Order # (lazy import but still hard dep)


Part 3: Module Categorization (UPDATED 2026-02-03)

Core Modules (8) - Always enabled ┌───────────┬───────────────────────────────────────────────────┬──────────────────────────────┐ │ Module │ Purpose │ Depends on Optional? │ ├───────────┼───────────────────────────────────────────────────┼──────────────────────────────┤ │ contracts │ Cross-module Protocol interfaces │ No │ ├───────────┼───────────────────────────────────────────────────┼──────────────────────────────┤ │ core │ Dashboard, settings, profile │ YES (analytics) │ ├───────────┼───────────────────────────────────────────────────┼──────────────────────────────┤ │ cms │ Content pages, media, themes │ YES (catalog) │ ├───────────┼───────────────────────────────────────────────────┼──────────────────────────────┤ │ customers │ Customer database, profiles │ YES (orders) │ ├───────────┼───────────────────────────────────────────────────┼──────────────────────────────┤ │ tenancy │ Platforms, merchants, stores, users │ YES (marketplace,catalog, │ │ │ │ analytics) │ ├───────────┼───────────────────────────────────────────────────┼──────────────────────────────┤ │ billing │ Subscriptions, tier limits, invoices │ No (depends on payments, │ │ │ │ which is also core) │ ├───────────┼───────────────────────────────────────────────────┼──────────────────────────────┤ │ payments │ Payment gateways (Stripe, PayPal) │ No │ ├───────────┼───────────────────────────────────────────────────┼──────────────────────────────┤ │ messaging │ Email, notifications, templates │ No │ └───────────┴───────────────────────────────────────────────────┴──────────────────────────────┘

Why billing, payments, messaging are core (decided 2026-02-03):

  • billing: Tier limits affect team size, product limits, email providers - pervasive
  • payments: Required by billing for subscription payment processing
  • messaging: Email required for registration, password reset, team invitations

Optional Modules (8) - Can be disabled per platform ┌─────────────┬────────────────────────────┬───────────────────────────────────┐ │ Module │ Purpose │ Dependencies │ ├─────────────┼────────────────────────────┼───────────────────────────────────┤ │ analytics │ Reports, dashboards │ (standalone) │ ├─────────────┼────────────────────────────┼───────────────────────────────────┤ │ cart │ Shopping cart │ inventory │ ├─────────────┼────────────────────────────┼───────────────────────────────────┤ │ catalog │ Product catalog │ inventory │ ├─────────────┼────────────────────────────┼───────────────────────────────────┤ │ checkout │ Order placement │ cart, orders, customers │ ├─────────────┼────────────────────────────┼───────────────────────────────────┤ │ inventory │ Stock management │ (standalone) │ ├─────────────┼────────────────────────────┼───────────────────────────────────┤ │ loyalty │ Loyalty programs │ customers │ ├─────────────┼────────────────────────────┼───────────────────────────────────┤ │ marketplace │ Letzshop integration │ inventory │ ├─────────────┼────────────────────────────┼───────────────────────────────────┤ │ orders │ Order management │ (standalone) │ └─────────────┴────────────────────────────┴───────────────────────────────────┘

Internal Modules (2) - Admin-only ┌────────────┬─────────────────────────────────┐ │ Module │ Purpose │ ├────────────┼─────────────────────────────────┤ │ dev-tools │ Component library, icon browser │ ├────────────┼─────────────────────────────────┤ │ monitoring │ Logs, tasks, health checks │ └────────────┴─────────────────────────────────┘

Part 4: Answer to User Questions

Q1: Are all modules self-contained and respecting architecture rules?

NO. While the route registration and middleware are properly decoupled, the core modules have hard import dependencies on optional modules. This violates the documented rule that "core modules cannot depend on optional modules."

Specific violations:

  • tenancy depends on: marketplace, billing, catalog, analytics
  • core depends on: analytics
  • cms depends on: catalog, billing
  • customers depends on: orders

Q2: Would the app still run if non-core modules are removed?

NO. The app would crash immediately on import with ImportError or ModuleNotFoundError because:

  1. tenancy/models/init.py imports MarketplaceImportJob at module load time
  2. core/routes/api/admin_dashboard.py imports stats_service at module load time
  3. cms/models/init.py imports ProductMedia at module load time

Even if using lazy imports (like in customers/services/customer_service.py), the code paths that use these imports would still fail at runtime.


Part 5: Contracts Pattern Deep Dive (Option A)

How the Existing Contracts Module Works

The app/modules/contracts/ module already implements Protocol-based interfaces:

app/modules/contracts/ ├── init.py # Exports all protocols ├── base.py # ServiceProtocol, CRUDServiceProtocol ├── cms.py # ContentServiceProtocol, MediaServiceProtocol └── definition.py # Module definition (is_core=True)

Key Pattern:

1. Protocol definition in contracts (no implementation)

@runtime_checkable class ContentServiceProtocol(Protocol): def get_page_for_store(self, db: Session, ...) -> object | None: ...

2. Implementation in the module itself

class ContentPageService: # Implements the protocol implicitly (duck typing) def get_page_for_store(self, db: Session, ...) -> ContentPage | None: # actual implementation

3. Usage in other modules (depends on protocol, not implementation)

class OrderService: def init(self, content: ContentServiceProtocol | None = None): self._content = content

 @property
 def content(self) -> ContentServiceProtocol:
     if self._content is None:
         from app.modules.cms.services import content_page_service  # Lazy load
         self._content = content_page_service
     return self._content

Part 6: Metrics Provider Pattern (Proposed)

Design Goal

Each module provides its own metrics/stats. The analytics module aggregates them but doesn't implement them.

Step 1: Define MetricsProvider Protocol

File: app/modules/contracts/metrics.py from typing import Protocol, runtime_checkable, TYPE_CHECKING from dataclasses import dataclass

if TYPE_CHECKING: from sqlalchemy.orm import Session

@dataclass class MetricValue: """Standard metric value with metadata.""" key: str # e.g., "orders.total_count" value: int | float | str label: str # Human-readable label category: str # Grouping category icon: str | None = None # Optional UI icon

@runtime_checkable class MetricsProviderProtocol(Protocol): """ Protocol for modules that provide metrics/statistics.

 Each module implements this to expose its own metrics.
 The analytics module discovers and aggregates all providers.
 """

 @property
 def metrics_category(self) -> str:
     """Category name for this provider's metrics (e.g., 'orders', 'inventory')."""
     ...

 def get_store_metrics(
     self,
     db: "Session",
     store_id: int,
     date_from: "datetime | None" = None,
     date_to: "datetime | None" = None,
 ) -> list[MetricValue]:
     """Get metrics for a specific store."""
     ...

 def get_platform_metrics(
     self,
     db: "Session",
     platform_id: int,
     date_from: "datetime | None" = None,
     date_to: "datetime | None" = None,
 ) -> list[MetricValue]:
     """Get metrics aggregated for a platform."""
     ...

Step 2: Each Module Implements the Protocol

File: app/modules/orders/services/order_metrics.py from app.modules.contracts.metrics import MetricsProviderProtocol, MetricValue from sqlalchemy.orm import Session

class OrderMetricsProvider: """Metrics provider for orders module."""

 @property
 def metrics_category(self) -> str:
     return "orders"

 def get_store_metrics(
     self, db: Session, store_id: int, date_from=None, date_to=None
 ) -> list[MetricValue]:
     from app.modules.orders.models import Order

     query = db.query(Order).filter(Order.store_id == store_id)
     if date_from:
         query = query.filter(Order.created_at >= date_from)
     if date_to:
         query = query.filter(Order.created_at <= date_to)

     total_orders = query.count()
     total_revenue = query.with_entities(func.sum(Order.total)).scalar() or 0

     return [
         MetricValue(
             key="orders.total_count",
             value=total_orders,
             label="Total Orders",
             category="orders",
             icon="shopping-cart"
         ),
         MetricValue(
             key="orders.total_revenue",
             value=float(total_revenue),
             label="Total Revenue",
             category="orders",
             icon="currency-euro"
         ),
     ]

 def get_platform_metrics(self, db: Session, platform_id: int, **kwargs):
     # Aggregate across all stores in platform
     ...

Singleton instance

order_metrics_provider = OrderMetricsProvider()

Step 3: Register Provider in Module Definition

File: app/modules/orders/definition.py from app.modules.base import ModuleDefinition

def _get_metrics_provider(): """Lazy load metrics provider to avoid circular imports.""" from app.modules.orders.services.order_metrics import order_metrics_provider return order_metrics_provider

orders_module = ModuleDefinition( code="orders", name="Order Management", # ... existing config ... metrics_provider=_get_metrics_provider, # NEW: Optional callable )

Step 4: Analytics Module Discovers All Providers

File: app/modules/analytics/services/stats_aggregator.py from app.modules.contracts.metrics import MetricsProviderProtocol, MetricValue from app.modules.registry import MODULES from sqlalchemy.orm import Session

class StatsAggregatorService: """Aggregates metrics from all module providers."""

 def _get_enabled_providers(self, db: Session, platform_id: int) -> list[MetricsProviderProtocol]:
     """Get metrics providers from enabled modules."""
     from app.modules.service import module_service

     providers = []
     for module in MODULES.values():
         # Check if module is enabled for this platform
         if not module_service.is_module_enabled(db, platform_id, module.code):
             continue

         # Check if module has a metrics provider
         if hasattr(module, 'metrics_provider') and module.metrics_provider:
             provider = module.metrics_provider()  # Call the lazy getter
             if provider:
                 providers.append(provider)

     return providers

 def get_store_stats(
     self, db: Session, store_id: int, platform_id: int, **kwargs
 ) -> dict[str, list[MetricValue]]:
     """Get all metrics for a store, grouped by category."""
     providers = self._get_enabled_providers(db, platform_id)

     result = {}
     for provider in providers:
         metrics = provider.get_store_metrics(db, store_id, **kwargs)
         result[provider.metrics_category] = metrics

     return result

stats_aggregator = StatsAggregatorService()

Step 5: Dashboard Uses Protocol, Not Implementation

File: app/modules/core/routes/api/store_dashboard.py from fastapi import APIRouter, Depends from sqlalchemy.orm import Session

from app.core.database import get_db from app.api.deps import get_current_store_api from models.schema.auth import UserContext

router = APIRouter(prefix="/dashboard")

@router.get("/stats") def get_dashboard_stats( current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """Get aggregated stats for store dashboard.""" # Lazy import - only fails if analytics module removed AND this endpoint called from app.modules.analytics.services.stats_aggregator import stats_aggregator

 return stats_aggregator.get_store_stats(
     db=db,
     store_id=current_user.token_store_id,
     platform_id=current_user.platform_id,
 )

Part 7: Creating a New Module with Metrics

Checklist for New Module with Metrics Support

  1. Create module structure: app/modules/my_module/ ├── init.py ├── definition.py # Include metrics_provider ├── models/ ├── services/ │ ├── init.py │ └── my_metrics.py # Implement MetricsProviderProtocol ├── routes/ └── schemas/
  2. Implement metrics provider:

app/modules/my_module/services/my_metrics.py

from app.modules.contracts.metrics import MetricsProviderProtocol, MetricValue

class MyModuleMetricsProvider: @property def metrics_category(self) -> str: return "my_module"

 def get_store_metrics(self, db, store_id, **kwargs):
     return [
         MetricValue(key="my_module.count", value=42, label="My Count", category="my_module")
     ]

 def get_platform_metrics(self, db, platform_id, **kwargs):
     return []

my_module_metrics = MyModuleMetricsProvider() 3. Register in definition:

app/modules/my_module/definition.py

def _get_metrics_provider(): from app.modules.my_module.services.my_metrics import my_module_metrics return my_module_metrics

my_module = ModuleDefinition( code="my_module", metrics_provider=_get_metrics_provider, # ... ) 4. Metrics appear automatically in dashboards when module is enabled


Part 8: Benefits of This Architecture ┌───────────────────────────┬───────────────────────┬──────────────────────────────────┐ │ Aspect │ Before │ After │ ├───────────────────────────┼───────────────────────┼──────────────────────────────────┤ │ Core depends on analytics │ Hard import │ Optional lazy import │ ├───────────────────────────┼───────────────────────┼──────────────────────────────────┤ │ Adding new metrics │ Edit analytics module │ Just add provider to your module │ ├───────────────────────────┼───────────────────────┼──────────────────────────────────┤ │ Module isolation │ Coupled │ Truly independent │ ├───────────────────────────┼───────────────────────┼──────────────────────────────────┤ │ Testing │ Hard (need analytics) │ Easy (mock protocol) │ ├───────────────────────────┼───────────────────────┼──────────────────────────────────┤ │ Disable analytics module │ App crashes │ Dashboard shows "no stats" │ └───────────────────────────┴───────────────────────┴──────────────────────────────────┘

Part 9: Core Module Metrics & Dashboard Architecture

Key Questions Answered

Q1: How do core modules implement MetricsProviderProtocol?

Core modules implement MetricsProviderProtocol the same way as optional modules. The difference is they're always enabled.

tenancy module metrics:

app/modules/tenancy/services/tenancy_metrics.py

class TenancyMetricsProvider: @property def metrics_category(self) -> str: return "tenancy"

 def get_store_metrics(self, db, store_id, **kwargs):
     # Store-specific: team members count, domains count
     from app.modules.tenancy.models import StoreUser, StoreDomain

     team_count = db.query(StoreUser).filter(
         StoreUser.store_id == store_id, StoreUser.is_active == True
     ).count()

     domains_count = db.query(StoreDomain).filter(
         StoreDomain.store_id == store_id
     ).count()

     return [
         MetricValue(key="tenancy.team_members", value=team_count,
                    label="Team Members", category="tenancy", icon="users"),
         MetricValue(key="tenancy.domains", value=domains_count,
                    label="Custom Domains", category="tenancy", icon="globe"),
     ]

 def get_platform_metrics(self, db, platform_id, **kwargs):
     # Platform-wide: total stores, total users, active stores
     from app.modules.tenancy.models import Store, User

     total_stores = db.query(Store).filter(
         Store.platform_id == platform_id
     ).count()

     active_stores = db.query(Store).filter(
         Store.platform_id == platform_id, Store.is_active == True
     ).count()

     return [
         MetricValue(key="tenancy.total_stores", value=total_stores,
                    label="Total Stores", category="tenancy", icon="store"),
         MetricValue(key="tenancy.active_stores", value=active_stores,
                    label="Active Stores", category="tenancy", icon="check-circle"),
     ]

customers module metrics:

app/modules/customers/services/customer_metrics.py

class CustomerMetricsProvider: @property def metrics_category(self) -> str: return "customers"

 def get_store_metrics(self, db, store_id, date_from=None, date_to=None):
     from app.modules.customers.models import Customer

     query = db.query(Customer).filter(Customer.store_id == store_id)
     total = query.count()

     # New customers in period
     if date_from:
         new_query = query.filter(Customer.created_at >= date_from)
         if date_to:
             new_query = new_query.filter(Customer.created_at <= date_to)
         new_count = new_query.count()
     else:
         new_count = 0

     return [
         MetricValue(key="customers.total", value=total,
                    label="Total Customers", category="customers", icon="users"),
         MetricValue(key="customers.new", value=new_count,
                    label="New Customers", category="customers", icon="user-plus"),
     ]

 def get_platform_metrics(self, db, platform_id, **kwargs):
     # Platform admin sees aggregated customer stats
     ...

Q2: Should analytics be part of core?

Recommendation: Split analytics into two parts: ┌────────────────────────┬─────────────────────────────┬─────────────────────────────────────────────────────┐ │ Component │ Location │ Purpose │ ├────────────────────────┼─────────────────────────────┼─────────────────────────────────────────────────────┤ │ StatsAggregatorService │ core module │ Basic metric discovery & aggregation for dashboards │ ├────────────────────────┼─────────────────────────────┼─────────────────────────────────────────────────────┤ │ Advanced Analytics │ analytics module (optional) │ Reports, charts, trends, exports, historical data │ └────────────────────────┴─────────────────────────────┴─────────────────────────────────────────────────────┘ Why this split works:

  • Dashboards always work (aggregator in core)
  • Advanced analytics features can be disabled per platform
  • No circular dependencies
  • Clear separation of concerns

Implementation: app/modules/core/services/ ├── stats_aggregator.py # Discovers & aggregates MetricsProviders (CORE) └── ...

app/modules/analytics/services/ ├── reports_service.py # Advanced reports (OPTIONAL) ├── trends_service.py # Historical trends (OPTIONAL) └── export_service.py # Data exports (OPTIONAL)

StatsAggregatorService in core:

app/modules/core/services/stats_aggregator.py

from app.modules.contracts.metrics import MetricsProviderProtocol, MetricValue from app.modules.registry import MODULES

class StatsAggregatorService: """ Core service that discovers and aggregates metrics from all modules. Lives in core module so dashboards always work. """

 def _get_providers(self, db, platform_id) -> list[MetricsProviderProtocol]:
     """Get all metrics providers from enabled modules."""
     from app.modules.service import module_service

     providers = []
     for module in MODULES.values():
         # Always include core modules, check others
         if not module.is_core:
             if not module_service.is_module_enabled(db, platform_id, module.code):
                 continue

         if hasattr(module, 'metrics_provider') and module.metrics_provider:
             provider = module.metrics_provider()
             if provider:
                 providers.append(provider)

     return providers

 def get_store_dashboard_stats(self, db, store_id, platform_id, **kwargs):
     """For store dashboard - single store metrics."""
     providers = self._get_providers(db, platform_id)
     return {p.metrics_category: p.get_store_metrics(db, store_id, **kwargs)
             for p in providers}

 def get_admin_dashboard_stats(self, db, platform_id, **kwargs):
     """For admin dashboard - platform-wide metrics."""
     providers = self._get_providers(db, platform_id)
     return {p.metrics_category: p.get_platform_metrics(db, platform_id, **kwargs)
             for p in providers}

stats_aggregator = StatsAggregatorService()

Q3: Should this be used by both admin and store dashboards?

YES. The protocol has two methods for this exact purpose: ┌───────────────────────────────────┬──────────────────┬───────────────────────────────┐ │ Method │ Used By │ Data Scope │ ├───────────────────────────────────┼──────────────────┼───────────────────────────────┤ │ get_store_metrics(store_id) │ Store Dashboard │ Single store's data │ ├───────────────────────────────────┼──────────────────┼───────────────────────────────┤ │ get_platform_metrics(platform_id) │ Admin Dashboard │ Aggregated across all stores │ └───────────────────────────────────┴──────────────────┴───────────────────────────────┘ Store Dashboard:

app/modules/core/routes/api/store_dashboard.py

@router.get("/stats") def get_store_stats( current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): from app.modules.core.services.stats_aggregator import stats_aggregator

 return stats_aggregator.get_store_dashboard_stats(
     db=db,
     store_id=current_user.token_store_id,
     platform_id=current_user.platform_id,
 )

Admin Dashboard:

app/modules/core/routes/api/admin_dashboard.py

@router.get("/stats") def get_platform_stats( current_user: UserContext = Depends(get_current_platform_admin_api), db: Session = Depends(get_db), ): from app.modules.core.services.stats_aggregator import stats_aggregator

 return stats_aggregator.get_admin_dashboard_stats(
     db=db,
     platform_id=current_user.platform_id,
 )

Module Categorization Update (UPDATED 2026-02-03)

With this architecture, module categorization becomes: ┌───────────┬──────────┬─────────────────┬───────────────────────────────────────┐ │ Module │ Type │ Has Metrics? │ Metrics Examples │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ core │ Core │ + Aggregator │ System stats, aggregator service │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ tenancy │ Core │ │ Stores, users, team members, domains │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ customers │ Core │ │ Customer counts, new customers │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ cms │ Core │ │ Pages, media items │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ contracts │ Core │ │ Just protocols, no data │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ billing │ Core │ │ Subscription stats, tier usage │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ payments │ Core │ │ Transaction stats │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ messaging │ Core │ │ Email stats, notification counts │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ orders │ Optional │ │ Order counts, revenue │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ inventory │ Optional │ │ Stock levels, low stock │ ├───────────┼──────────┼─────────────────┼───────────────────────────────────────┤ │ analytics │ Optional │ │ Advanced reports (uses aggregator) │ └───────────┴──────────┴─────────────────┴───────────────────────────────────────┘

Part 10: Implementation Steps

Phase 1: Create Metrics Contract (contracts module)

  1. Create app/modules/contracts/metrics.py with:
  • MetricValue dataclass
  • MetricsProviderProtocol
  1. Add metrics_provider: Callable | None field to ModuleDefinition base class
  2. Export from contracts/init.py

Phase 2: Create Stats Aggregator (core module)

  1. Create app/modules/core/services/stats_aggregator.py
  • StatsAggregatorService that discovers all metrics providers
  • get_store_dashboard_stats() method
  • get_admin_dashboard_stats() method
  1. Register in core module exports

Phase 3: Add Metrics Providers to Core Modules

  1. tenancy → tenancy_metrics.py
  • Store count, user count, team members, domains
  1. customers → customer_metrics.py
  • Customer count, new customers, active customers
  1. cms → cms_metrics.py
  • Page count, media count

Phase 4: Add Metrics Providers to Optional Modules

Priority order:

  1. orders → order_metrics.py - order counts, revenue
  2. inventory → inventory_metrics.py - stock levels, low stock alerts
  3. billing → billing_metrics.py - subscription stats
  4. marketplace → marketplace_metrics.py - import stats

Phase 5: Update Dashboard Routes

  1. Update core/routes/api/store_dashboard.py to use aggregator
  2. Update core/routes/api/admin_dashboard.py to use aggregator
  3. Remove direct imports from analytics module
  4. Handle graceful degradation when no metrics available

Phase 6: Refactor Analytics Module (Optional Features)

  1. Keep advanced features in analytics module:
  • reports_service.py - detailed reports
  • trends_service.py - historical trends
  • export_service.py - data exports
  1. Analytics module can use stats_aggregator for base data
  2. Remove duplicated metric calculations from analytics

Phase 7: Update Tests

  1. Add unit tests for StatsAggregatorService
  2. Add unit tests for each metrics provider
  3. Update dashboard API tests
  4. Test with analytics module disabled

Verification Commands

Check module discovery

python -c "from app.modules.registry import MODULES; print(list(MODULES.keys()))"

Test metrics provider discovery

python -c " from app.modules.registry import MODULES for m in MODULES.values(): if hasattr(m, 'metrics_provider') and m.metrics_provider: print(f'{m.code}: has metrics provider') "

Test stats aggregator (after implementation)

python -c " from app.modules.core.services.stats_aggregator import stats_aggregator from app.core.database import SessionLocal db = SessionLocal() try: # Test with platform_id=1 stats = stats_aggregator.get_admin_dashboard_stats(db, platform_id=1) print('Categories:', list(stats.keys())) finally: db.close() "

Run metrics provider tests

pytest tests/unit/services/test_*_metrics.py -v

Verify no core→optional imports remain

grep -r "from app.modules.analytics" app/modules/core/ app/modules/tenancy/ || echo "Clean!"


Summary: What Changes

Before (Current State)

core/routes/admin_dashboard.py └── imports stats_service from analytics module └── analytics calculates ALL metrics internally

After (New Architecture)

core/routes/admin_dashboard.py └── imports stats_aggregator from core module └── discovers MetricsProviders from ALL modules ├── tenancy_metrics (core) ├── customer_metrics (core) ├── order_metrics (optional, if enabled) ├── inventory_metrics (optional, if enabled) └── ...

analytics module (optional) └── Advanced features: reports, trends, exports └── can use stats_aggregator for base data

Key Benefits

  1. Dashboards always work - aggregator is in core
  2. Each module owns its metrics - no cross-module coupling
  3. Optional modules truly optional - can be removed without breaking app
  4. Easy to add new metrics - just implement protocol in your module
  5. Both dashboards supported - store (per-store) and admin (platform-wide)