# app/api/v1/admin/subscriptions.py """ Admin Subscription Management API. Provides endpoints for platform administrators to manage: - Subscription tiers (CRUD) - Vendor subscriptions (view, update, override limits) - Billing history across all vendors - Subscription analytics """ import logging from fastapi import APIRouter, Depends, Path, Query from sqlalchemy import func from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from app.services.admin_subscription_service import admin_subscription_service from models.database.product import Product from models.database.user import User from models.database.vendor import VendorUser from app.services.subscription_service import subscription_service from models.schema.billing import ( BillingHistoryListResponse, BillingHistoryWithVendor, SubscriptionStatsResponse, SubscriptionTierCreate, SubscriptionTierListResponse, SubscriptionTierResponse, SubscriptionTierUpdate, VendorSubscriptionCreate, VendorSubscriptionListResponse, VendorSubscriptionResponse, VendorSubscriptionUpdate, VendorSubscriptionWithVendor, ) router = APIRouter(prefix="/subscriptions") logger = logging.getLogger(__name__) # ============================================================================ # Subscription Tier Endpoints # ============================================================================ @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), ) @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) @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) @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) @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 # ============================================================================ @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 # ============================================================================ @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 # ============================================================================ @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 # ============================================================================ @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. """ from models.database.vendor import Vendor # Verify vendor exists vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() if not vendor: from app.exceptions import ResourceNotFoundException raise ResourceNotFoundException("Vendor", str(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 products_count = ( db.query(func.count(Product.id)) .filter(Product.vendor_id == vendor_id) .scalar() or 0 ) team_count = ( db.query(func.count(VendorUser.id)) .filter( VendorUser.vendor_id == vendor_id, VendorUser.is_active == True, # noqa: E712 ) .scalar() or 0 ) 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=products_count, team_count=team_count, ) @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 products_count = ( db.query(func.count(Product.id)) .filter(Product.vendor_id == vendor_id) .scalar() or 0 ) team_count = ( db.query(func.count(VendorUser.id)) .filter( VendorUser.vendor_id == vendor_id, VendorUser.is_active == True, # noqa: E712 ) .scalar() or 0 ) return VendorSubscriptionWithVendor( **VendorSubscriptionResponse.model_validate(sub).model_dump(), vendor_name=vendor.name, vendor_code=vendor.subdomain, products_count=products_count, team_count=team_count, ) @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 products_count = ( db.query(func.count(Product.id)) .filter(Product.vendor_id == vendor_id) .scalar() or 0 ) team_count = ( db.query(func.count(VendorUser.id)) .filter( VendorUser.vendor_id == vendor_id, VendorUser.is_active == True, # noqa: E712 ) .scalar() or 0 ) return VendorSubscriptionWithVendor( **VendorSubscriptionResponse.model_validate(sub).model_dump(), vendor_name=vendor.name, vendor_code=vendor.subdomain, products_count=products_count, team_count=team_count, )