Some checks failed
Resolves the billing (core) → monitoring (optional) architecture violation by moving CapacityForecastService to the monitoring module where it belongs. - Create BillingMetricsProvider to expose subscription counts via stats_aggregator - Move CapacitySnapshot model from billing to monitoring - Replace direct MerchantSubscription queries with stats_aggregator calls - Fix middleware test mocks to cover StoreDomain/MerchantDomain fallback chains Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
299 lines
10 KiB
Python
299 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_metrics_provider():
|
|
"""Lazy import of metrics provider to avoid circular imports."""
|
|
from app.modules.billing.services.billing_metrics import billing_metrics_provider
|
|
|
|
return billing_metrics_provider
|
|
|
|
|
|
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.merchant_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,
|
|
# Metrics provider for subscription metrics
|
|
metrics_provider=_get_metrics_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"]
|