Files
orion/app/modules/billing/routes/api/admin.py
Samir Boulahtit b9f08b853f refactor: clean up legacy models and migrate remaining schemas
Delete empty stub files from models/database/:
- audit.py, backup.py, configuration.py, monitoring.py
- notification.py, payment.py, search.py, task.py

Delete re-export files:
- models/database/subscription.py → app.modules.billing.models
- models/database/architecture_scan.py → app.modules.dev_tools.models
- models/database/test_run.py → app.modules.dev_tools.models
- models/schema/subscription.py → app.modules.billing.schemas
- models/schema/marketplace.py (empty)
- models/schema/monitoring.py (empty)

Migrate schemas to canonical module locations:
- billing.py → app/modules/billing/schemas/
- vendor_product.py → app/modules/catalog/schemas/
- homepage_sections.py → app/modules/cms/schemas/

Keep as CORE (framework-level, used everywhere):
- models/schema/: admin, auth, base, company, email, image, media, team, vendor*
- models/database/: admin*, base, company, email, feature, media, platform*, user, vendor*

Update 30+ files to use canonical import locations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 18:45:46 +01:00

337 lines
11 KiB
Python

# 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.modules.billing.services import admin_subscription_service, subscription_service
from models.database.user import User
from app.modules.billing.schemas 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"],
)