feat: extract billing module with routes (Phase 3)
Create app/modules/billing/ directory structure with:
- definition.py: Module definition with features and menu items
- routes/admin.py: Admin billing routes with module access control
- routes/vendor.py: Vendor billing routes with module access control
Key changes:
- Billing module uses require_module_access("billing") dependency
- Admin router now includes billing module router instead of legacy
- Module registry imports billing_module from extracted location
- Routes have identical functionality but are now module-gated
Module structure pattern for future extractions:
app/modules/{module}/
├── __init__.py
├── definition.py (ModuleDefinition + router getters)
└── routes/
├── __init__.py
├── admin.py (require_module_access dependency)
└── vendor.py (require_module_access dependency)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,20 @@ IMPORTANT:
|
||||
- This router is for JSON API endpoints only
|
||||
- HTML page routes are mounted separately in main.py at /vendor/*
|
||||
- Do NOT include pages.router here - it causes route conflicts
|
||||
|
||||
MODULE SYSTEM:
|
||||
Routes can be module-gated using require_module_access() dependency.
|
||||
For multi-tenant apps, module enablement is checked at request time
|
||||
based on platform context (not at route registration time).
|
||||
|
||||
Extracted modules (app/modules/{module}/routes/):
|
||||
- billing: Subscription tiers, vendor billing, invoices
|
||||
|
||||
Future extractions will follow the same pattern:
|
||||
1. Create app/modules/{module}/ directory
|
||||
2. Move routes to app/modules/{module}/routes/admin.py
|
||||
3. Add require_module_access("{module}") to router
|
||||
4. Update this file to import from module instead
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
@@ -42,6 +56,7 @@ from . import (
|
||||
logs,
|
||||
marketplace,
|
||||
media,
|
||||
menu_config,
|
||||
messages,
|
||||
monitoring,
|
||||
notifications,
|
||||
@@ -51,7 +66,7 @@ from . import (
|
||||
platforms,
|
||||
products,
|
||||
settings,
|
||||
subscriptions,
|
||||
subscriptions, # Legacy - will be replaced by billing module router
|
||||
tests,
|
||||
users,
|
||||
vendor_domains,
|
||||
@@ -60,6 +75,9 @@ from . import (
|
||||
vendors,
|
||||
)
|
||||
|
||||
# Import extracted module routers
|
||||
from app.modules.billing.routes import admin_router as billing_admin_router
|
||||
|
||||
# Create admin router
|
||||
router = APIRouter()
|
||||
|
||||
@@ -96,6 +114,9 @@ router.include_router(
|
||||
# Include platforms management endpoints (multi-platform CMS)
|
||||
router.include_router(platforms.router, tags=["admin-platforms"])
|
||||
|
||||
# Include menu configuration endpoints (super admin only)
|
||||
router.include_router(menu_config.router, tags=["admin-menu-config"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# User Management
|
||||
@@ -192,11 +213,15 @@ router.include_router(
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Billing & Subscriptions
|
||||
# Billing & Subscriptions (Module-gated)
|
||||
# ============================================================================
|
||||
|
||||
# Include subscription management endpoints
|
||||
router.include_router(subscriptions.router, tags=["admin-subscriptions"])
|
||||
# Include billing module router (with module access control)
|
||||
# This router checks if the 'billing' module is enabled for the platform
|
||||
router.include_router(billing_admin_router, tags=["admin-billing"])
|
||||
|
||||
# Legacy subscriptions router (to be removed once billing module is fully tested)
|
||||
# router.include_router(subscriptions.router, tags=["admin-subscriptions"])
|
||||
|
||||
# Include feature management endpoints
|
||||
router.include_router(features.router, tags=["admin-features"])
|
||||
|
||||
22
app/modules/billing/__init__.py
Normal file
22
app/modules/billing/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# app/modules/billing/__init__.py
|
||||
"""
|
||||
Billing Module - Subscription and payment management.
|
||||
|
||||
This module provides:
|
||||
- Subscription tier management
|
||||
- Vendor subscription CRUD
|
||||
- Billing history and invoices
|
||||
- Stripe integration
|
||||
|
||||
Routes:
|
||||
- Admin: /api/v1/admin/subscriptions/*
|
||||
- Vendor: /api/v1/vendor/billing/*
|
||||
|
||||
Menu Items:
|
||||
- Admin: subscription-tiers, subscriptions, billing-history
|
||||
- Vendor: billing, invoices
|
||||
"""
|
||||
|
||||
from app.modules.billing.definition import billing_module
|
||||
|
||||
__all__ = ["billing_module"]
|
||||
71
app/modules/billing/definition.py
Normal file
71
app/modules/billing/definition.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# app/modules/billing/definition.py
|
||||
"""
|
||||
Billing module definition.
|
||||
|
||||
Defines the billing module including its features, menu items,
|
||||
and route configurations.
|
||||
"""
|
||||
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
|
||||
def _get_admin_router():
|
||||
"""Lazy import of admin router to avoid circular imports."""
|
||||
from app.modules.billing.routes.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.vendor import vendor_router
|
||||
|
||||
return vendor_router
|
||||
|
||||
|
||||
# Billing module definition
|
||||
billing_module = ModuleDefinition(
|
||||
code="billing",
|
||||
name="Billing & Subscriptions",
|
||||
description=(
|
||||
"Subscription tier management, vendor billing, payment processing, "
|
||||
"and invoice history. Integrates with Stripe for payment collection."
|
||||
),
|
||||
features=[
|
||||
"subscription_management", # Manage subscription tiers
|
||||
"billing_history", # View invoices and payment history
|
||||
"stripe_integration", # Stripe payment processing
|
||||
"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
|
||||
],
|
||||
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
|
||||
],
|
||||
},
|
||||
is_core=False, # Billing can be disabled (e.g., internal platforms)
|
||||
)
|
||||
|
||||
|
||||
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"]
|
||||
12
app/modules/billing/routes/__init__.py
Normal file
12
app/modules/billing/routes/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# app/modules/billing/routes/__init__.py
|
||||
"""
|
||||
Billing module route registration.
|
||||
|
||||
This module provides functions to register billing routes
|
||||
with module-based access control.
|
||||
"""
|
||||
|
||||
from app.modules.billing.routes.admin import admin_router
|
||||
from app.modules.billing.routes.vendor import vendor_router
|
||||
|
||||
__all__ = ["admin_router", "vendor_router"]
|
||||
337
app/modules/billing/routes/admin.py
Normal file
337
app/modules/billing/routes/admin.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# app/modules/billing/routes/admin.py
|
||||
"""
|
||||
Billing module admin routes.
|
||||
|
||||
This module wraps the existing admin subscription routes and adds
|
||||
module-based access control. The actual route implementations remain
|
||||
in app/api/v1/admin/subscriptions.py for now, but are accessed through
|
||||
this module-aware router.
|
||||
|
||||
Future: Move all route implementations here for full module isolation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_subscription_service import admin_subscription_service
|
||||
from app.services.subscription_service import subscription_service
|
||||
from models.database.user import User
|
||||
from models.schema.billing import (
|
||||
BillingHistoryListResponse,
|
||||
BillingHistoryWithVendor,
|
||||
SubscriptionStatsResponse,
|
||||
SubscriptionTierCreate,
|
||||
SubscriptionTierListResponse,
|
||||
SubscriptionTierResponse,
|
||||
SubscriptionTierUpdate,
|
||||
VendorSubscriptionCreate,
|
||||
VendorSubscriptionListResponse,
|
||||
VendorSubscriptionResponse,
|
||||
VendorSubscriptionUpdate,
|
||||
VendorSubscriptionWithVendor,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Admin router with module access control
|
||||
admin_router = APIRouter(
|
||||
prefix="/subscriptions",
|
||||
dependencies=[Depends(require_module_access("billing"))],
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Subscription Tier Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.get("/tiers", response_model=SubscriptionTierListResponse)
|
||||
def list_subscription_tiers(
|
||||
include_inactive: bool = Query(False, description="Include inactive tiers"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all subscription tiers.
|
||||
|
||||
Returns all tiers with their limits, features, and Stripe configuration.
|
||||
"""
|
||||
tiers = admin_subscription_service.get_tiers(db, include_inactive=include_inactive)
|
||||
|
||||
return SubscriptionTierListResponse(
|
||||
tiers=[SubscriptionTierResponse.model_validate(t) for t in tiers],
|
||||
total=len(tiers),
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("/tiers/{tier_code}", response_model=SubscriptionTierResponse)
|
||||
def get_subscription_tier(
|
||||
tier_code: str = Path(..., description="Tier code"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get a specific subscription tier by code."""
|
||||
tier = admin_subscription_service.get_tier_by_code(db, tier_code)
|
||||
return SubscriptionTierResponse.model_validate(tier)
|
||||
|
||||
|
||||
@admin_router.post("/tiers", response_model=SubscriptionTierResponse, status_code=201)
|
||||
def create_subscription_tier(
|
||||
tier_data: SubscriptionTierCreate,
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Create a new subscription tier."""
|
||||
tier = admin_subscription_service.create_tier(db, tier_data.model_dump())
|
||||
db.commit()
|
||||
db.refresh(tier)
|
||||
return SubscriptionTierResponse.model_validate(tier)
|
||||
|
||||
|
||||
@admin_router.patch("/tiers/{tier_code}", response_model=SubscriptionTierResponse)
|
||||
def update_subscription_tier(
|
||||
tier_data: SubscriptionTierUpdate,
|
||||
tier_code: str = Path(..., description="Tier code"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update a subscription tier."""
|
||||
update_data = tier_data.model_dump(exclude_unset=True)
|
||||
tier = admin_subscription_service.update_tier(db, tier_code, update_data)
|
||||
db.commit()
|
||||
db.refresh(tier)
|
||||
return SubscriptionTierResponse.model_validate(tier)
|
||||
|
||||
|
||||
@admin_router.delete("/tiers/{tier_code}", status_code=204)
|
||||
def delete_subscription_tier(
|
||||
tier_code: str = Path(..., description="Tier code"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Soft-delete a subscription tier.
|
||||
|
||||
Sets is_active=False rather than deleting to preserve history.
|
||||
"""
|
||||
admin_subscription_service.deactivate_tier(db, tier_code)
|
||||
db.commit()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Vendor Subscription Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.get("", response_model=VendorSubscriptionListResponse)
|
||||
def list_vendor_subscriptions(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
status: str | None = Query(None, description="Filter by status"),
|
||||
tier: str | None = Query(None, description="Filter by tier"),
|
||||
search: str | None = Query(None, description="Search vendor name"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all vendor subscriptions with filtering.
|
||||
|
||||
Includes vendor information for each subscription.
|
||||
"""
|
||||
data = admin_subscription_service.list_subscriptions(
|
||||
db, page=page, per_page=per_page, status=status, tier=tier, search=search
|
||||
)
|
||||
|
||||
subscriptions = []
|
||||
for sub, vendor in data["results"]:
|
||||
sub_dict = {
|
||||
**VendorSubscriptionResponse.model_validate(sub).model_dump(),
|
||||
"vendor_name": vendor.name,
|
||||
"vendor_code": vendor.subdomain,
|
||||
}
|
||||
subscriptions.append(VendorSubscriptionWithVendor(**sub_dict))
|
||||
|
||||
return VendorSubscriptionListResponse(
|
||||
subscriptions=subscriptions,
|
||||
total=data["total"],
|
||||
page=data["page"],
|
||||
per_page=data["per_page"],
|
||||
pages=data["pages"],
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Statistics Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.get("/stats", response_model=SubscriptionStatsResponse)
|
||||
def get_subscription_stats(
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get subscription statistics for admin dashboard."""
|
||||
stats = admin_subscription_service.get_stats(db)
|
||||
return SubscriptionStatsResponse(**stats)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Billing History Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.get("/billing/history", response_model=BillingHistoryListResponse)
|
||||
def list_billing_history(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
vendor_id: int | None = Query(None, description="Filter by vendor"),
|
||||
status: str | None = Query(None, description="Filter by status"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List billing history (invoices) across all vendors."""
|
||||
data = admin_subscription_service.list_billing_history(
|
||||
db, page=page, per_page=per_page, vendor_id=vendor_id, status=status
|
||||
)
|
||||
|
||||
invoices = []
|
||||
for invoice, vendor in data["results"]:
|
||||
invoice_dict = {
|
||||
"id": invoice.id,
|
||||
"vendor_id": invoice.vendor_id,
|
||||
"stripe_invoice_id": invoice.stripe_invoice_id,
|
||||
"invoice_number": invoice.invoice_number,
|
||||
"invoice_date": invoice.invoice_date,
|
||||
"due_date": invoice.due_date,
|
||||
"subtotal_cents": invoice.subtotal_cents,
|
||||
"tax_cents": invoice.tax_cents,
|
||||
"total_cents": invoice.total_cents,
|
||||
"amount_paid_cents": invoice.amount_paid_cents,
|
||||
"currency": invoice.currency,
|
||||
"status": invoice.status,
|
||||
"invoice_pdf_url": invoice.invoice_pdf_url,
|
||||
"hosted_invoice_url": invoice.hosted_invoice_url,
|
||||
"description": invoice.description,
|
||||
"created_at": invoice.created_at,
|
||||
"vendor_name": vendor.name,
|
||||
"vendor_code": vendor.subdomain,
|
||||
}
|
||||
invoices.append(BillingHistoryWithVendor(**invoice_dict))
|
||||
|
||||
return BillingHistoryListResponse(
|
||||
invoices=invoices,
|
||||
total=data["total"],
|
||||
page=data["page"],
|
||||
per_page=data["per_page"],
|
||||
pages=data["pages"],
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Vendor Subscription Detail Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.post("/{vendor_id}", response_model=VendorSubscriptionWithVendor, status_code=201)
|
||||
def create_vendor_subscription(
|
||||
create_data: VendorSubscriptionCreate,
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Create a subscription for a vendor.
|
||||
|
||||
Creates a new subscription with the specified tier and status.
|
||||
Defaults to Essential tier with trial status.
|
||||
"""
|
||||
# Verify vendor exists
|
||||
vendor = admin_subscription_service.get_vendor(db, vendor_id)
|
||||
|
||||
# Create subscription using the subscription service
|
||||
sub = subscription_service.get_or_create_subscription(
|
||||
db,
|
||||
vendor_id=vendor_id,
|
||||
tier=create_data.tier,
|
||||
trial_days=create_data.trial_days,
|
||||
)
|
||||
|
||||
# Update status if not trial
|
||||
if create_data.status != "trial":
|
||||
sub.status = create_data.status
|
||||
|
||||
sub.is_annual = create_data.is_annual
|
||||
|
||||
db.commit()
|
||||
db.refresh(sub)
|
||||
|
||||
# Get usage counts
|
||||
usage = admin_subscription_service.get_vendor_usage_counts(db, vendor_id)
|
||||
|
||||
logger.info(f"Admin created subscription for vendor {vendor_id}: tier={create_data.tier}")
|
||||
|
||||
return VendorSubscriptionWithVendor(
|
||||
**VendorSubscriptionResponse.model_validate(sub).model_dump(),
|
||||
vendor_name=vendor.name,
|
||||
vendor_code=vendor.subdomain,
|
||||
products_count=usage["products_count"],
|
||||
team_count=usage["team_count"],
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("/{vendor_id}", response_model=VendorSubscriptionWithVendor)
|
||||
def get_vendor_subscription(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get subscription details for a specific vendor."""
|
||||
sub, vendor = admin_subscription_service.get_subscription(db, vendor_id)
|
||||
|
||||
# Get usage counts
|
||||
usage = admin_subscription_service.get_vendor_usage_counts(db, vendor_id)
|
||||
|
||||
return VendorSubscriptionWithVendor(
|
||||
**VendorSubscriptionResponse.model_validate(sub).model_dump(),
|
||||
vendor_name=vendor.name,
|
||||
vendor_code=vendor.subdomain,
|
||||
products_count=usage["products_count"],
|
||||
team_count=usage["team_count"],
|
||||
)
|
||||
|
||||
|
||||
@admin_router.patch("/{vendor_id}", response_model=VendorSubscriptionWithVendor)
|
||||
def update_vendor_subscription(
|
||||
update_data: VendorSubscriptionUpdate,
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update a vendor's subscription.
|
||||
|
||||
Allows admins to:
|
||||
- Change tier
|
||||
- Update status
|
||||
- Set custom limit overrides
|
||||
- Extend trial period
|
||||
"""
|
||||
data = update_data.model_dump(exclude_unset=True)
|
||||
sub, vendor = admin_subscription_service.update_subscription(db, vendor_id, data)
|
||||
db.commit()
|
||||
db.refresh(sub)
|
||||
|
||||
# Get usage counts
|
||||
usage = admin_subscription_service.get_vendor_usage_counts(db, vendor_id)
|
||||
|
||||
return VendorSubscriptionWithVendor(
|
||||
**VendorSubscriptionResponse.model_validate(sub).model_dump(),
|
||||
vendor_name=vendor.name,
|
||||
vendor_code=vendor.subdomain,
|
||||
products_count=usage["products_count"],
|
||||
team_count=usage["team_count"],
|
||||
)
|
||||
216
app/modules/billing/routes/vendor.py
Normal file
216
app/modules/billing/routes/vendor.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# app/modules/billing/routes/vendor.py
|
||||
"""
|
||||
Billing module vendor routes.
|
||||
|
||||
This module wraps the existing vendor billing routes and adds
|
||||
module-based access control. The actual route implementations remain
|
||||
in app/api/v1/vendor/billing.py for now, but are accessed through
|
||||
this module-aware router.
|
||||
|
||||
Future: Move all route implementations here for full module isolation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api, require_module_access
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.services.billing_service import billing_service
|
||||
from app.services.subscription_service import subscription_service
|
||||
from models.database.user import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Vendor router with module access control
|
||||
vendor_router = APIRouter(
|
||||
prefix="/billing",
|
||||
dependencies=[Depends(require_module_access("billing"))],
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Schemas (re-exported from original module)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class SubscriptionStatusResponse(BaseModel):
|
||||
"""Current subscription status and usage."""
|
||||
|
||||
tier_code: str
|
||||
tier_name: str
|
||||
status: str
|
||||
is_trial: bool
|
||||
trial_ends_at: str | None = None
|
||||
period_start: str | None = None
|
||||
period_end: str | None = None
|
||||
cancelled_at: str | None = None
|
||||
cancellation_reason: str | None = None
|
||||
|
||||
# Usage
|
||||
orders_this_period: int
|
||||
orders_limit: int | None
|
||||
orders_remaining: int | None
|
||||
products_count: int
|
||||
products_limit: int | None
|
||||
products_remaining: int | None
|
||||
team_count: int
|
||||
team_limit: int | None
|
||||
team_remaining: int | None
|
||||
|
||||
# Payment
|
||||
has_payment_method: bool
|
||||
last_payment_error: str | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TierResponse(BaseModel):
|
||||
"""Subscription tier information."""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
description: str | None = None
|
||||
price_monthly_cents: int
|
||||
price_annual_cents: int | None = None
|
||||
orders_per_month: int | None = None
|
||||
products_limit: int | None = None
|
||||
team_members: int | None = None
|
||||
features: list[str] = []
|
||||
is_current: bool = False
|
||||
can_upgrade: bool = False
|
||||
can_downgrade: bool = False
|
||||
|
||||
|
||||
class TierListResponse(BaseModel):
|
||||
"""List of available tiers."""
|
||||
|
||||
tiers: list[TierResponse]
|
||||
current_tier: str
|
||||
|
||||
|
||||
class InvoiceResponse(BaseModel):
|
||||
"""Invoice information."""
|
||||
|
||||
id: int
|
||||
invoice_number: str | None = None
|
||||
invoice_date: str
|
||||
due_date: str | None = None
|
||||
total_cents: int
|
||||
amount_paid_cents: int
|
||||
currency: str
|
||||
status: str
|
||||
pdf_url: str | None = None
|
||||
hosted_url: str | None = None
|
||||
|
||||
|
||||
class InvoiceListResponse(BaseModel):
|
||||
"""List of invoices."""
|
||||
|
||||
invoices: list[InvoiceResponse]
|
||||
total: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Core Billing Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@vendor_router.get("/subscription", response_model=SubscriptionStatusResponse)
|
||||
def get_subscription_status(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get current subscription status and usage metrics."""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
usage = subscription_service.get_usage_summary(db, vendor_id)
|
||||
subscription, tier = billing_service.get_subscription_with_tier(db, vendor_id)
|
||||
|
||||
return SubscriptionStatusResponse(
|
||||
tier_code=subscription.tier,
|
||||
tier_name=tier.name if tier else subscription.tier.title(),
|
||||
status=subscription.status.value,
|
||||
is_trial=subscription.is_in_trial(),
|
||||
trial_ends_at=subscription.trial_ends_at.isoformat()
|
||||
if subscription.trial_ends_at
|
||||
else None,
|
||||
period_start=subscription.period_start.isoformat()
|
||||
if subscription.period_start
|
||||
else None,
|
||||
period_end=subscription.period_end.isoformat()
|
||||
if subscription.period_end
|
||||
else None,
|
||||
cancelled_at=subscription.cancelled_at.isoformat()
|
||||
if subscription.cancelled_at
|
||||
else None,
|
||||
cancellation_reason=subscription.cancellation_reason,
|
||||
orders_this_period=usage.orders_this_period,
|
||||
orders_limit=usage.orders_limit,
|
||||
orders_remaining=usage.orders_remaining,
|
||||
products_count=usage.products_count,
|
||||
products_limit=usage.products_limit,
|
||||
products_remaining=usage.products_remaining,
|
||||
team_count=usage.team_count,
|
||||
team_limit=usage.team_limit,
|
||||
team_remaining=usage.team_remaining,
|
||||
has_payment_method=bool(subscription.stripe_payment_method_id),
|
||||
last_payment_error=subscription.last_payment_error,
|
||||
)
|
||||
|
||||
|
||||
@vendor_router.get("/tiers", response_model=TierListResponse)
|
||||
def get_available_tiers(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get available subscription tiers for upgrade/downgrade."""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
subscription = subscription_service.get_or_create_subscription(db, vendor_id)
|
||||
current_tier = subscription.tier
|
||||
|
||||
tier_list, _ = billing_service.get_available_tiers(db, current_tier)
|
||||
|
||||
tier_responses = [TierResponse(**tier_data) for tier_data in tier_list]
|
||||
|
||||
return TierListResponse(tiers=tier_responses, current_tier=current_tier)
|
||||
|
||||
|
||||
@vendor_router.get("/invoices", response_model=InvoiceListResponse)
|
||||
def get_invoices(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get invoice history."""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
invoices, total = billing_service.get_invoices(db, vendor_id, skip=skip, limit=limit)
|
||||
|
||||
invoice_responses = [
|
||||
InvoiceResponse(
|
||||
id=inv.id,
|
||||
invoice_number=inv.invoice_number,
|
||||
invoice_date=inv.invoice_date.isoformat(),
|
||||
due_date=inv.due_date.isoformat() if inv.due_date else None,
|
||||
total_cents=inv.total_cents,
|
||||
amount_paid_cents=inv.amount_paid_cents,
|
||||
currency=inv.currency,
|
||||
status=inv.status,
|
||||
pdf_url=inv.invoice_pdf_url,
|
||||
hosted_url=inv.hosted_invoice_url,
|
||||
)
|
||||
for inv in invoices
|
||||
]
|
||||
|
||||
return InvoiceListResponse(invoices=invoice_responses, total=total)
|
||||
|
||||
|
||||
# NOTE: Additional endpoints (checkout, portal, cancel, addons, etc.)
|
||||
# are still handled by app/api/v1/vendor/billing.py for now.
|
||||
# They can be migrated here as part of a larger refactoring effort.
|
||||
@@ -7,11 +7,21 @@ enabled/disabled per platform. Core modules cannot be disabled.
|
||||
|
||||
Module Granularity (Medium - ~12 modules):
|
||||
Matches menu sections for intuitive mapping between modules and UI.
|
||||
|
||||
Module Structure:
|
||||
- Inline modules: Defined directly in this file (core, platform-admin, etc.)
|
||||
- Extracted modules: Imported from app/modules/{module}/ (billing, etc.)
|
||||
|
||||
As modules are extracted to their own directories, they are imported
|
||||
here and their inline definitions are replaced.
|
||||
"""
|
||||
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
|
||||
# Import extracted modules
|
||||
from app.modules.billing.definition import billing_module
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Module Definitions
|
||||
@@ -72,28 +82,8 @@ MODULES: dict[str, ModuleDefinition] = {
|
||||
# =========================================================================
|
||||
# Optional Modules
|
||||
# =========================================================================
|
||||
"billing": ModuleDefinition(
|
||||
code="billing",
|
||||
name="Billing & Subscriptions",
|
||||
description="Subscription tiers, billing history, and payment processing.",
|
||||
features=[
|
||||
"subscription_management",
|
||||
"billing_history",
|
||||
"stripe_integration",
|
||||
"invoice_generation",
|
||||
],
|
||||
menu_items={
|
||||
FrontendType.ADMIN: [
|
||||
"subscription-tiers",
|
||||
"subscriptions",
|
||||
"billing-history",
|
||||
],
|
||||
FrontendType.VENDOR: [
|
||||
"billing",
|
||||
"invoices",
|
||||
],
|
||||
},
|
||||
),
|
||||
# Billing module - imported from app/modules/billing/
|
||||
"billing": billing_module,
|
||||
"inventory": ModuleDefinition(
|
||||
code="inventory",
|
||||
name="Inventory Management",
|
||||
|
||||
Reference in New Issue
Block a user