grantforge-api / backend /endpoints /stripe_webhooks.py
GrantForge Bot
Deploy to Hugging Face
afd56bc
import os
import stripe
from fastapi import APIRouter, Request, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional
from core.subscription.middleware import verify_token
from core.subscription.db import SessionLocal
from core.subscription.models import User
from clerk_backend_api import Clerk
stripe_router = APIRouter()
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
endpoint_secret = os.getenv("STRIPE_WEBHOOK_SECRET")
clerk_secret = os.getenv("CLERK_SECRET_KEY")
clerk = Clerk(bearer_auth=clerk_secret) if clerk_secret else None
FRONTEND_URL = os.getenv("FRONTEND_URL", "https://grantforge-frontend.onrender.com")
# ─── Checkout session ───────────────────────────────────────────────────────
class CheckoutRequest(BaseModel):
plan: Optional[str] = "pro" # "pro" | "business"
@stripe_router.post("/subscription/checkout")
async def create_checkout_session(
payload: CheckoutRequest,
token_data: dict = Depends(verify_token),
):
"""Tworzy sesjΔ™ Stripe Checkout i zwraca URL do pΕ‚atnoΕ›ci."""
user_id = token_data.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="Brak autoryzacji")
price_id = os.getenv("STRIPE_PRICE_ID_PRO")
if not price_id:
raise HTTPException(
status_code=503, detail="Stripe price ID nie skonfigurowany."
)
if not stripe.api_key:
raise HTTPException(
status_code=503,
detail="Stripe nie jest skonfigurowany. Skontaktuj siΔ™ z supportem.",
)
success_url = f"{FRONTEND_URL}/projects?upgraded=1&tier={payload.plan}&session_id={{CHECKOUT_SESSION_ID}}"
cancel_url = f"{FRONTEND_URL}/cennik?cancelled=1"
try:
session = stripe.checkout.Session.create(
mode="subscription",
payment_method_types=["card"],
line_items=[{"price": price_id, "quantity": 1}],
success_url=success_url,
cancel_url=cancel_url,
client_reference_id=user_id,
metadata={"tier": payload.plan or "pro"},
locale="pl",
billing_address_collection="auto",
tax_id_collection={"enabled": True},
invoice_creation={"enabled": True},
)
return {"checkout_url": session.url, "session_id": session.id}
except stripe.error.InvalidRequestError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"BΕ‚Δ…d Stripe: {str(e)}")
# ─── Stripe Webhook ───────────────────────────────────────────────────────
@stripe_router.post("/webhook/stripe")
async def stripe_webhook_endpoint(request: Request):
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
if not endpoint_secret:
return {"status": "ignored", "reason": "No webhook secret configured"}
try:
event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid payload")
except stripe.error.SignatureVerificationError:
raise HTTPException(status_code=400, detail="Invalid signature")
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
user_id = session.get("client_reference_id")
customer_id = session.get("customer")
subscription_id = session.get("subscription")
tier = session.get("metadata", {}).get("tier", "pro").lower()
if user_id:
# 1. Aktualizacja Clerk public_metadata
if clerk:
try:
clerk.users.update_user(
user_id=user_id, public_metadata={"stripe_subscription": tier}
)
except Exception as e:
print(f"[Webhook] BΕ‚Δ…d Clerk update {user_id}: {e}")
# 2. Aktualizacja user.tier w PostgreSQL
db = SessionLocal()
try:
user = db.query(User).filter(User.clerk_id == user_id).first()
if not user:
user = User(clerk_id=user_id)
db.add(user)
user.tier = tier
user.stripe_customer_id = customer_id
user.stripe_subscription_id = subscription_id
db.commit()
print(f"[Webhook] βœ… User {user_id} upgrade -> {tier}")
except Exception as e:
db.rollback()
print(f"[Webhook] ❌ DB error {user_id}: {e}")
finally:
db.close()
elif event["type"] == "customer.subscription.deleted":
subscription = event["data"]["object"]
sub_id = subscription.get("id")
db = SessionLocal()
try:
user = db.query(User).filter(User.stripe_subscription_id == sub_id).first()
if user:
user.tier = "free"
if clerk:
try:
clerk.users.update_user(
user_id=user.clerk_id,
public_metadata={"stripe_subscription": "free"},
)
except Exception:
pass
db.commit()
print(f"[Webhook] Downgrade user {user.clerk_id} -> free")
except Exception:
db.rollback()
finally:
db.close()
return {"status": "success"}