from fastapi import APIRouter, HTTPException, Depends, Request, Header from app.models import CheckoutSessionCreate, CheckoutSessionResponse, SubscriptionResponse from app.database import get_supabase from app.dependencies import get_current_user from app.config import settings from typing import Optional import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/billing", tags=["Billing"]) PLAN_PRICE_IDS = { "starter": settings.stripe_starter_price_id, "business": settings.stripe_business_price_id, "agency": settings.stripe_agency_price_id, } @router.post("/checkout", response_model=CheckoutSessionResponse) async def create_checkout_session(data: CheckoutSessionCreate, user=Depends(get_current_user)): try: import stripe stripe.api_key = settings.stripe_secret_key if data.plan not in PLAN_PRICE_IDS: raise HTTPException(status_code=400, detail=f"Invalid plan: {data.plan}") price_id = PLAN_PRICE_IDS[data.plan] if not price_id: raise HTTPException(status_code=400, detail="Plan price not configured") supabase = get_supabase() sub = supabase.table("subscriptions").select("stripe_customer_id").eq("user_id", user.id).execute() customer_id = None if sub.data and sub.data[0].get("stripe_customer_id"): customer_id = sub.data[0]["stripe_customer_id"] else: customer = stripe.Customer.create(email=user.email) customer_id = customer.id session = stripe.checkout.Session.create( customer=customer_id, payment_method_types=["card"], line_items=[{"price": price_id, "quantity": 1}], mode="subscription", success_url=data.success_url + "?session_id={CHECKOUT_SESSION_ID}", cancel_url=data.cancel_url, metadata={"user_id": user.id, "plan": data.plan}, ) return CheckoutSessionResponse(checkout_url=session.url, session_id=session.id) except ImportError: raise HTTPException(status_code=500, detail="Stripe not configured") except HTTPException: raise except Exception as e: logger.error(f"Checkout error: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/webhook") async def stripe_webhook( request: Request, stripe_signature: Optional[str] = Header(None), ): try: import stripe stripe.api_key = settings.stripe_secret_key payload = await request.body() if settings.stripe_webhook_secret: if not stripe_signature: raise HTTPException(status_code=400, detail="Missing Stripe signature") try: event = stripe.Webhook.construct_event( payload, stripe_signature, settings.stripe_webhook_secret ) except stripe.error.SignatureVerificationError: raise HTTPException(status_code=400, detail="Invalid signature") elif settings.app_env == "production": raise HTTPException(status_code=500, detail="Webhook secret not configured") else: import json logger.warning("Stripe webhook received without signature verification (dev mode only)") event = stripe.Event.construct_from(json.loads(payload), stripe.api_key) supabase = get_supabase() event_type = event.type event_id = event.id # Idempotency check: skip already-processed events if event_id: existing = supabase.table("stripe_webhook_events") \ .select("stripe_event_id") \ .eq("stripe_event_id", event_id) \ .execute() if existing.data: logger.info(f"Stripe event {event_id} already processed, skipping") return {"received": True} if event_type == "checkout.session.completed": session = event.data.object user_id = (session.metadata or {}).get("user_id") plan = (session.metadata or {}).get("plan", "starter") customer_id = session.customer subscription_id = session.subscription if user_id: supabase.table("subscriptions").upsert({ "user_id": user_id, "plan": plan, "status": "active", "stripe_customer_id": customer_id, "stripe_subscription_id": subscription_id, }, on_conflict="user_id").execute() elif event_type in ("customer.subscription.updated", "customer.subscription.deleted"): sub_obj = event.data.object customer_id = sub_obj.customer status = sub_obj.status or "canceled" existing = supabase.table("subscriptions").select("*").eq("stripe_customer_id", customer_id).execute() if existing.data: mapped_status = "active" if status in ("active", "trialing") else "canceled" supabase.table("subscriptions").update({ "status": mapped_status, }).eq("stripe_customer_id", customer_id).execute() # Unpublish chatbots if subscription canceled if mapped_status == "canceled": user_id = existing.data[0]["user_id"] company = supabase.table("companies").select("id").eq("owner_id", user_id).execute() if company.data: supabase.table("chatbots").update({ "is_published": False, "visibility": "preview", }).eq("company_id", company.data[0]["id"]).execute() # Send cancellation notification via n8n if settings.n8n_handoff_webhook_url: try: from app.services.n8n_service import send_notification await send_notification( event_type="subscription_canceled", data={"user_id": user_id}, webhook_url=settings.n8n_handoff_webhook_url, ) except Exception as e: logger.warning(f"Failed to send cancellation notification: {e}") # Record event as processed if event_id: try: supabase.table("stripe_webhook_events").insert({ "stripe_event_id": event_id, "event_type": event_type, }).execute() except Exception as e: logger.warning(f"Failed to record stripe event {event_id}: {e}") return {"received": True} except HTTPException: raise except Exception as e: logger.error(f"Webhook error: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/subscription", response_model=SubscriptionResponse) async def get_subscription(user=Depends(get_current_user)): supabase = get_supabase() result = supabase.table("subscriptions").select("*").eq("user_id", user.id).execute() if not result.data: return SubscriptionResponse( id="free", user_id=user.id, plan="free", status="active", ) sub = result.data[0] return SubscriptionResponse( id=sub["id"], user_id=sub["user_id"], plan=sub["plan"], status=sub["status"], stripe_customer_id=sub.get("stripe_customer_id"), current_period_start=sub.get("current_period_start"), current_period_end=sub.get("current_period_end"), chatbots_published=sub.get("chatbots_published", 0), conversations_used=sub.get("conversations_used", 0), created_at=sub.get("created_at"), ) @router.post("/portal") async def customer_portal(request: Request, user=Depends(get_current_user)): """Create Stripe customer portal session""" try: import stripe stripe.api_key = settings.stripe_secret_key supabase = get_supabase() sub = supabase.table("subscriptions").select("stripe_customer_id").eq("user_id", user.id).execute() if not sub.data or not sub.data[0].get("stripe_customer_id"): raise HTTPException(status_code=404, detail="No subscription found") body = await request.json() return_url = body.get("return_url", "http://localhost:5173/settings") session = stripe.billing_portal.Session.create( customer=sub.data[0]["stripe_customer_id"], return_url=return_url, ) return {"url": session.url} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e))