Spaces:
Sleeping
Sleeping
| 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" | |
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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"} | |