Store detail page now shows all platform subscriptions instead of always "No Subscription Found". Subscriptions listing page renamed from Store to Merchant throughout (template, JS, menu, i18n) with Platform column added. Tiers API supports platform_id filtering. Merchant detail page no longer hardcodes 'oms' platform — loads all platforms, shows subscription cards per platform with labels, and the Create Subscription modal includes a platform selector with platform-filtered tiers. Create button always accessible in Quick Actions. Edit modal on /admin/subscriptions loads tiers from API filtered by platform instead of hardcoded options, sends tier_code (not tier) to match PATCH schema, and shows platform context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
284 lines
10 KiB
Python
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.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,
|
|
)
|
|
|
|
|
|
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"]
|