Some checks failed
- 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>
918 lines
63 KiB
Markdown
918 lines
63 KiB
Markdown
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 │
|
|
└──────┴─────────────────────────────────────┴───────────────────────────────────────────────────────────────────────┘
|
|
---
|
|
2. 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 │
|
|
└──────┴───────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
|
|
---
|
|
3. 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
|
|
2. Add metrics_provider: Callable | None field to ModuleDefinition base class
|
|
3. 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
|
|
2. 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
|
|
2. customers → customer_metrics.py
|
|
- Customer count, new customers, active customers
|
|
3. 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
|
|
2. Analytics module can use stats_aggregator for base data
|
|
3. 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)
|