# app/modules/billing/routes/api/store_checkout.py """ Store 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. Resolves store_id to (merchant_id, platform_id) for all billing service calls. """ import logging from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from app.api.deps import get_current_store_api, require_module_access from app.core.config import settings from app.core.database import get_db from app.modules.billing.schemas.billing import ( CancelRequest, CancelResponse, ChangeTierRequest, ChangeTierResponse, CheckoutRequest, CheckoutResponse, PortalResponse, UpcomingInvoiceResponse, ) from app.modules.billing.services import billing_service from app.modules.billing.services.subscription_service import subscription_service from app.modules.enums import FrontendType from app.modules.tenancy.schemas.auth import UserContext store_checkout_router = APIRouter( dependencies=[Depends(require_module_access("billing", FrontendType.STORE))], ) logger = logging.getLogger(__name__) # ============================================================================ # Endpoints # ============================================================================ @store_checkout_router.post("/checkout", response_model=CheckoutResponse) def create_checkout_session( request: CheckoutRequest, current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """Create a Stripe checkout session for subscription.""" store_id = current_user.token_store_id merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id, current_user.token_platform_id) store_code = subscription_service.get_store_code(db, store_id) base_url = settings.app_base_url.rstrip("/") success_url = f"{base_url}/store/{store_code}/billing?success=true" cancel_url = f"{base_url}/store/{store_code}/billing?cancelled=true" result = billing_service.create_checkout_session( db=db, merchant_id=merchant_id, platform_id=platform_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"]) @store_checkout_router.post("/portal", response_model=PortalResponse) def create_portal_session( current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """Create a Stripe customer portal session.""" store_id = current_user.token_store_id merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id, current_user.token_platform_id) store_code = subscription_service.get_store_code(db, store_id) return_url = f"{settings.app_base_url.rstrip('/')}/store/{store_code}/billing" result = billing_service.create_portal_session(db, merchant_id, platform_id, return_url) return PortalResponse(portal_url=result["portal_url"]) @store_checkout_router.post("/cancel", response_model=CancelResponse) def cancel_subscription( request: CancelRequest, current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """Cancel subscription.""" store_id = current_user.token_store_id merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id, current_user.token_platform_id) result = billing_service.cancel_subscription( db=db, merchant_id=merchant_id, platform_id=platform_id, reason=request.reason, immediately=request.immediately, ) db.commit() return CancelResponse( message=result["message"], effective_date=result["effective_date"], ) @store_checkout_router.post("/reactivate") def reactivate_subscription( current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """Reactivate a cancelled subscription.""" store_id = current_user.token_store_id merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id, current_user.token_platform_id) result = billing_service.reactivate_subscription(db, merchant_id, platform_id) db.commit() return result @store_checkout_router.get("/upcoming-invoice", response_model=UpcomingInvoiceResponse) def get_upcoming_invoice( current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """Preview the upcoming invoice.""" store_id = current_user.token_store_id merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id, current_user.token_platform_id) result = billing_service.get_upcoming_invoice(db, merchant_id, platform_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", []), ) @store_checkout_router.post("/change-tier", response_model=ChangeTierResponse) def change_tier( request: ChangeTierRequest, current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """Change subscription tier (upgrade/downgrade).""" store_id = current_user.token_store_id merchant_id, platform_id = subscription_service.resolve_store_to_merchant(db, store_id, current_user.token_platform_id) result = billing_service.change_tier( db=db, merchant_id=merchant_id, platform_id=platform_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"], )