Files
orion/app/modules/billing/definition.py
Samir Boulahtit b03406db45 feat: implement module-driven context providers for dynamic page context
Introduces a module-driven context provider system that allows modules to
dynamically contribute template context variables without hardcoding imports.

Key changes:
- Add context_providers field to ModuleDefinition in app/modules/base.py
- Create unified get_context_for_frontend() that queries enabled modules only
- Add context providers to CMS module (PLATFORM, STOREFRONT)
- Add context providers to billing module (PLATFORM)
- Fix SQLAlchemy cross-module relationship resolution (Order, AdminMenuConfig,
  MarketplaceImportJob) by ensuring models are imported before referencing
- Document the entire system in docs/architecture/module-system.md

Benefits:
- Zero coupling: adding/removing modules requires no route handler changes
- Lazy loading: module code only imported when that module is enabled
- Per-platform customization: each platform loads only what it needs
- Graceful degradation: one failing module doesn't break entire page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 19:22:52 +01:00

262 lines
9.4 KiB
Python

# app/modules/billing/definition.py
"""
Billing module definition.
Defines the billing module including its features, menu items,
route configurations, and scheduled tasks.
"""
import logging
from typing import Any
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition, PermissionDefinition, ScheduledTask
from app.modules.enums import FrontendType
logger = logging.getLogger(__name__)
# =============================================================================
# Context Providers
# =============================================================================
def _get_platform_context(request: Any, db: Any, platform: Any) -> dict[str, Any]:
"""
Provide billing context for platform/marketing pages.
Returns pricing tier data for the marketing pricing page.
"""
from app.core.config import settings
from app.modules.billing.models import TIER_LIMITS, TierCode
tiers = []
for tier_code, limits in TIER_LIMITS.items():
tiers.append({
"code": tier_code.value,
"name": limits["name"],
"price_monthly": limits["price_monthly_cents"] / 100,
"price_annual": (limits["price_annual_cents"] / 100)
if limits.get("price_annual_cents")
else None,
"orders_per_month": limits.get("orders_per_month"),
"products_limit": limits.get("products_limit"),
"team_members": limits.get("team_members"),
"features": limits.get("features", []),
"is_popular": tier_code == TierCode.PROFESSIONAL,
"is_enterprise": tier_code == TierCode.ENTERPRISE,
})
return {
"tiers": tiers,
"trial_days": settings.stripe_trial_days,
"stripe_publishable_key": settings.stripe_publishable_key,
}
# =============================================================================
# Router Lazy Imports
# =============================================================================
def _get_admin_router():
"""Lazy import of admin router to avoid circular imports."""
from app.modules.billing.routes.api.admin import admin_router
return admin_router
def _get_vendor_router():
"""Lazy import of vendor router to avoid circular imports."""
from app.modules.billing.routes.api.vendor import vendor_router
return vendor_router
# Billing module definition
billing_module = ModuleDefinition(
code="billing",
name="Billing & Subscriptions",
description=(
"Platform subscription management, vendor billing, and invoice history. "
"Uses the payments module for actual payment processing."
),
version="1.0.0",
requires=["payments"], # Depends on payments module for payment processing
features=[
"subscription_management", # Manage subscription tiers
"billing_history", # View invoices and payment history
"invoice_generation", # Generate and download invoices
"subscription_analytics", # Subscription stats and metrics
"trial_management", # Manage vendor trial periods
"limit_overrides", # Override tier limits per vendor
],
# Module-driven permissions
permissions=[
PermissionDefinition(
id="billing.view_tiers",
label_key="billing.permissions.view_tiers",
description_key="billing.permissions.view_tiers_desc",
category="billing",
),
PermissionDefinition(
id="billing.manage_tiers",
label_key="billing.permissions.manage_tiers",
description_key="billing.permissions.manage_tiers_desc",
category="billing",
),
PermissionDefinition(
id="billing.view_subscriptions",
label_key="billing.permissions.view_subscriptions",
description_key="billing.permissions.view_subscriptions_desc",
category="billing",
),
PermissionDefinition(
id="billing.manage_subscriptions",
label_key="billing.permissions.manage_subscriptions",
description_key="billing.permissions.manage_subscriptions_desc",
category="billing",
),
PermissionDefinition(
id="billing.view_invoices",
label_key="billing.permissions.view_invoices",
description_key="billing.permissions.view_invoices_desc",
category="billing",
),
],
menu_items={
FrontendType.ADMIN: [
"subscription-tiers", # Manage tier definitions
"subscriptions", # View/manage vendor subscriptions
"billing-history", # View all invoices
],
FrontendType.VENDOR: [
"billing", # Vendor billing dashboard
"invoices", # Vendor invoice history
],
},
# New module-driven menu definitions
menus={
FrontendType.ADMIN: [
MenuSectionDefinition(
id="billing",
label_key="billing.menu.billing_subscriptions",
icon="credit-card",
order=50,
items=[
MenuItemDefinition(
id="subscription-tiers",
label_key="billing.menu.subscription_tiers",
icon="tag",
route="/admin/subscription-tiers",
order=10,
),
MenuItemDefinition(
id="subscriptions",
label_key="billing.menu.vendor_subscriptions",
icon="credit-card",
route="/admin/subscriptions",
order=20,
),
MenuItemDefinition(
id="billing-history",
label_key="billing.menu.billing_history",
icon="document-text",
route="/admin/billing-history",
order=30,
),
],
),
],
FrontendType.VENDOR: [
MenuSectionDefinition(
id="sales",
label_key="billing.menu.sales_orders",
icon="currency-euro",
order=20,
items=[
MenuItemDefinition(
id="invoices",
label_key="billing.menu.invoices",
icon="currency-euro",
route="/vendor/{vendor_code}/invoices",
order=30,
),
],
),
MenuSectionDefinition(
id="account",
label_key="billing.menu.account_settings",
icon="credit-card",
order=900,
items=[
MenuItemDefinition(
id="billing",
label_key="billing.menu.billing",
icon="credit-card",
route="/vendor/{vendor_code}/billing",
order=30,
),
],
),
],
},
is_core=False, # Billing can be disabled (e.g., internal platforms)
# Context providers for dynamic page context
context_providers={
FrontendType.PLATFORM: _get_platform_context,
},
# =========================================================================
# Self-Contained Module Configuration
# =========================================================================
is_self_contained=True,
services_path="app.modules.billing.services",
models_path="app.modules.billing.models",
schemas_path="app.modules.billing.schemas",
exceptions_path="app.modules.billing.exceptions",
tasks_path="app.modules.billing.tasks",
# =========================================================================
# Scheduled Tasks
# =========================================================================
scheduled_tasks=[
ScheduledTask(
name="billing.reset_period_counters",
task="app.modules.billing.tasks.subscription.reset_period_counters",
schedule="5 0 * * *", # Daily at 00:05
options={"queue": "scheduled"},
),
ScheduledTask(
name="billing.check_trial_expirations",
task="app.modules.billing.tasks.subscription.check_trial_expirations",
schedule="0 1 * * *", # Daily at 01:00
options={"queue": "scheduled"},
),
ScheduledTask(
name="billing.sync_stripe_status",
task="app.modules.billing.tasks.subscription.sync_stripe_status",
schedule="30 * * * *", # Hourly at :30
options={"queue": "scheduled"},
),
ScheduledTask(
name="billing.cleanup_stale_subscriptions",
task="app.modules.billing.tasks.subscription.cleanup_stale_subscriptions",
schedule="0 3 * * 0", # Weekly on Sunday at 03:00
options={"queue": "scheduled"},
),
],
)
def get_billing_module_with_routers() -> ModuleDefinition:
"""
Get billing module with routers attached.
This function attaches the routers lazily to avoid circular imports
during module initialization.
"""
billing_module.admin_router = _get_admin_router()
billing_module.vendor_router = _get_vendor_router()
return billing_module
__all__ = ["billing_module", "get_billing_module_with_routers"]