# app/modules/billing/routes/api/vendor_checkout.py """ Vendor checkout and subscription management endpoints. Provides: - Stripe checkout session creation - Stripe portal session creation - Subscription cancellation and reactivation - Upcoming invoice preview - Tier changes (upgrade/downgrade) All routes require module access control for the 'billing' module. """ import logging from fastapi import APIRouter, Depends 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.modules.billing.services import billing_service, subscription_service from models.schema.auth import UserContext vendor_checkout_router = APIRouter( dependencies=[Depends(require_module_access("billing"))], ) logger = logging.getLogger(__name__) # ============================================================================ # Schemas # ============================================================================ class CheckoutRequest(BaseModel): """Request to create a checkout session.""" tier_code: str is_annual: bool = False class CheckoutResponse(BaseModel): """Checkout session response.""" checkout_url: str session_id: str class PortalResponse(BaseModel): """Customer portal session response.""" portal_url: str class CancelRequest(BaseModel): """Request to cancel subscription.""" reason: str | None = None immediately: bool = False class CancelResponse(BaseModel): """Cancellation response.""" message: str effective_date: str class UpcomingInvoiceResponse(BaseModel): """Upcoming invoice preview.""" amount_due_cents: int currency: str next_payment_date: str | None = None line_items: list[dict] = [] class ChangeTierRequest(BaseModel): """Request to change subscription tier.""" tier_code: str is_annual: bool = False class ChangeTierResponse(BaseModel): """Response for tier change.""" message: str new_tier: str effective_immediately: bool # ============================================================================ # Endpoints # ============================================================================ @vendor_checkout_router.post("/checkout", response_model=CheckoutResponse) def create_checkout_session( request: CheckoutRequest, current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """Create a Stripe checkout session for subscription.""" vendor_id = current_user.token_vendor_id vendor = billing_service.get_vendor(db, vendor_id) # Build URLs base_url = f"https://{settings.platform_domain}" success_url = f"{base_url}/vendor/{vendor.vendor_code}/billing?success=true" cancel_url = f"{base_url}/vendor/{vendor.vendor_code}/billing?cancelled=true" result = billing_service.create_checkout_session( db=db, vendor_id=vendor_id, tier_code=request.tier_code, is_annual=request.is_annual, success_url=success_url, cancel_url=cancel_url, ) db.commit() return CheckoutResponse(checkout_url=result["checkout_url"], session_id=result["session_id"]) @vendor_checkout_router.post("/portal", response_model=PortalResponse) def create_portal_session( current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """Create a Stripe customer portal session.""" vendor_id = current_user.token_vendor_id vendor = billing_service.get_vendor(db, vendor_id) return_url = f"https://{settings.platform_domain}/vendor/{vendor.vendor_code}/billing" result = billing_service.create_portal_session(db, vendor_id, return_url) return PortalResponse(portal_url=result["portal_url"]) @vendor_checkout_router.post("/cancel", response_model=CancelResponse) def cancel_subscription( request: CancelRequest, current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """Cancel subscription.""" vendor_id = current_user.token_vendor_id result = billing_service.cancel_subscription( db=db, vendor_id=vendor_id, reason=request.reason, immediately=request.immediately, ) db.commit() return CancelResponse( message=result["message"], effective_date=result["effective_date"], ) @vendor_checkout_router.post("/reactivate") def reactivate_subscription( current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """Reactivate a cancelled subscription.""" vendor_id = current_user.token_vendor_id result = billing_service.reactivate_subscription(db, vendor_id) db.commit() return result @vendor_checkout_router.get("/upcoming-invoice", response_model=UpcomingInvoiceResponse) def get_upcoming_invoice( current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """Preview the upcoming invoice.""" vendor_id = current_user.token_vendor_id result = billing_service.get_upcoming_invoice(db, vendor_id) return UpcomingInvoiceResponse( amount_due_cents=result.get("amount_due_cents", 0), currency=result.get("currency", "EUR"), next_payment_date=result.get("next_payment_date"), line_items=result.get("line_items", []), ) @vendor_checkout_router.post("/change-tier", response_model=ChangeTierResponse) def change_tier( request: ChangeTierRequest, current_user: UserContext = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """Change subscription tier (upgrade/downgrade).""" vendor_id = current_user.token_vendor_id result = billing_service.change_tier( db=db, vendor_id=vendor_id, new_tier_code=request.tier_code, is_annual=request.is_annual, ) db.commit() return ChangeTierResponse( message=result["message"], new_tier=result["new_tier"], effective_immediately=result["effective_immediately"], )