Files
contexta_be/app/routers/billing.py
2026-05-15 23:49:35 +00:00

227 lines
8.7 KiB
Python

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))