Files
orion/app/modules/billing/definition.py
Samir Boulahtit c3d26e9aa4 refactor(migrations): squash 75 migrations into 12 per-module initial migrations
The old migration chain was broken (downgrade path through vendor->merchant
rename made rollbacks impossible). This squashes everything into fresh
per-module migrations with zero schema drift, verified by autogenerate.

Changes:
- Replace 75 accumulated migrations with 12 per-module initial migrations
  (core, billing, catalog, marketplace, cms, customers, orders, inventory,
  cart, messaging, loyalty, dev_tools) in a linear chain
- Fix make db-reset to use SQL DROP SCHEMA instead of alembic downgrade base
- Enable migration autodiscovery for all modules (migrations_path in definitions)
- Rewrite alembic/env.py to import all 75 model tables across 13 modules
- Fix AdminNotification import (was incorrectly from tenancy, now from messaging)
- Update squash_migrations.py to handle all module migration directories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 11:51:37 +01:00

284 lines
10 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 SubscriptionTier, TierCode
tiers_db = (
db.query(SubscriptionTier)
.filter(
SubscriptionTier.is_active == True, # noqa: E712
SubscriptionTier.is_public == True, # noqa: E712
)
.order_by(SubscriptionTier.display_order)
.all()
)
tiers = []
for tier in tiers_db:
feature_codes = sorted(tier.get_feature_codes())
tiers.append({
"code": tier.code,
"name": tier.name,
"price_monthly": tier.price_monthly_cents / 100,
"price_annual": (tier.price_annual_cents / 100)
if tier.price_annual_cents
else None,
"feature_codes": feature_codes,
"products_limit": tier.get_limit_for_feature("products_limit"),
"orders_per_month": tier.get_limit_for_feature("orders_per_month"),
"team_members": tier.get_limit_for_feature("team_members"),
"is_popular": tier.code == TierCode.PROFESSIONAL.value,
"is_enterprise": tier.code == TierCode.ENTERPRISE.value,
})
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_store_router():
"""Lazy import of store router to avoid circular imports."""
from app.modules.billing.routes.api.store import store_router
return store_router
def _get_feature_provider():
"""Lazy import of feature provider to avoid circular imports."""
from app.modules.billing.services.billing_features import billing_feature_provider
return billing_feature_provider
# Billing module definition
billing_module = ModuleDefinition(
code="billing",
name="Billing & Subscriptions",
description=(
"Core subscription management, tier limits, store billing, and invoice history. "
"Provides tier-based feature gating used throughout the platform. "
"Uses the payments module for actual payment processing."
),
version="1.0.0",
requires=["payments"], # Depends on payments module (also core) 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 store trial periods
"limit_overrides", # Override tier limits per store
],
# 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 store subscriptions
"billing-history", # View all invoices
],
FrontendType.STORE: [
"billing", # Store billing dashboard
"invoices", # Store 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.store_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.STORE: [
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="/store/{store_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="/store/{store_code}/billing",
order=30,
),
],
),
],
},
is_core=True, # Core module - tier limits and subscription management are fundamental
# 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",
migrations_path="migrations",
# =========================================================================
# 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"},
),
],
# Feature provider for feature flags
feature_provider=_get_feature_provider,
)
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.store_router = _get_store_router()
return billing_module
__all__ = ["billing_module", "get_billing_module_with_routers"]