connect / routes /subscription.py
habulaj's picture
Update routes/subscription.py
6223a1f verified
raw
history blame
13.2 kB
import stripe
import logging
import json
import os
import requests
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
router = APIRouter()
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
# 🔥 Pegando as chaves do ambiente
stripe.api_key = os.getenv("STRIPE_KEY") # Lendo do ambiente
stripe.api_version = "2023-10-16"
# 🔥 Supabase Configuração com Secrets
SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co"
SUPABASE_KEY = os.getenv("SUPA_KEY") # Lendo do ambiente
if not stripe.api_key or not SUPABASE_KEY:
raise ValueError("❌ STRIPE_KEY ou SUPA_KEY não foram definidos no ambiente!")
SUPABASE_HEADERS = {
"apikey": SUPABASE_KEY,
"Authorization": f"Bearer {SUPABASE_KEY}",
"Content-Type": "application/json"
}
class CheckSubscriptionRequest(BaseModel):
user_id: str
stylist_id: str
# 📌 Agora recebemos `user_id` (ID do cliente que está comprando)
class SubscriptionRequest(BaseModel):
id: str # ID do estilista
class CreatePriceRequest(BaseModel):
amount: int # Valor em centavos (ex: 2500 para R$25,00)
user_id: str # ID do usuário que está criando o preço
from fastapi import Header # Import para pegar headers
@router.post("/create_price")
async def create_price(
data: CreatePriceRequest,
user_token: str = Header(None, alias="User-key")
):
try:
if not user_token:
raise HTTPException(status_code=401, detail="Missing User-key header")
amount = data.amount
user_id = data.user_id
if not amount or not user_id:
raise HTTPException(status_code=400, detail="Amount and user_id are required")
# 🔹 Buscar `price_id` do usuário no Supabase
supabase_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}"
supabase_headers = {
"apikey": SUPABASE_KEY,
"Authorization": f"Bearer {user_token}",
"Content-Type": "application/json"
}
response = requests.get(supabase_url, headers=supabase_headers)
user_data = response.json()
if not user_data:
raise HTTPException(status_code=404, detail="User not found")
user = user_data[0]
existing_price_id = user.get("price_id")
# 🔹 Se já existir um price_id, tentar atualizar
if existing_price_id:
try:
stripe.Price.modify(
existing_price_id,
unit_amount=amount,
metadata={"updated_at": "now"} # Apenas para gerar um evento de atualização
)
logger.info(f"✅ Price {existing_price_id} updated successfully.")
updated_price_id = existing_price_id
except stripe.error.InvalidRequestError:
logger.warning(f"⚠️ Failed to update price {existing_price_id}, creating a new one instead.")
updated_price_id = None
else:
updated_price_id = None
# 🔹 Se não conseguiu atualizar, criar um novo preço
if not updated_price_id:
price = stripe.Price.create(
unit_amount=amount,
currency="brl",
recurring={"interval": "month"},
product_data={"name": "Custom Subscription Price"}
)
updated_price_id = price.id
logger.info(f"✅ New price created: {updated_price_id}")
# 🔹 Pausar todas as assinaturas associadas ao preço antigo
if existing_price_id:
subscriptions = stripe.Subscription.list(status="active")
for sub in subscriptions.auto_paging_iter():
if sub["items"]["data"][0]["price"]["id"] == existing_price_id:
stripe.Subscription.modify(sub.id, pause_collection={"behavior": "void"})
logger.info(f"🔹 Subscription {sub.id} paused.")
# 🔹 Desativar o preço antigo
stripe.Price.modify(existing_price_id, active=False)
logger.info(f"🚫 Price {existing_price_id} deactivated.")
# 🔹 Atualizar Supabase com o novo `price_id`
update_response = requests.patch(
supabase_url,
headers=supabase_headers,
json={"price_id": updated_price_id}
)
if update_response.status_code not in [200, 204]:
raise HTTPException(status_code=500, detail=f"Failed to update Supabase: {update_response.text}")
return {"message": "Price created or updated successfully!", "price_id": updated_price_id}
except Exception as e:
logger.error(f"❌ Error creating/updating price: {e}")
raise HTTPException(status_code=500, detail="Error creating/updating price.")
@router.post("/create_checkout_session")
def create_checkout_session(
data: SubscriptionRequest,
user_token: str = Header(None, alias="User-key")
):
try:
if not user_token:
raise HTTPException(status_code=401, detail="Missing User-key header")
# 🔹 1. Decodificar o token JWT para obter o ID do usuário
try:
payload = jwt.decode(user_token, JWT_SECRET, algorithms=["HS256"]) # Substitua JWT_SECRET pela sua chave real
user_id = payload.get("id")
if not user_id:
raise HTTPException(status_code=400, detail="Invalid token: User ID not found")
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
# 🔹 2. Buscar estilista no Supabase
response = requests.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{data.id}",
headers=SUPABASE_HEADERS
)
stylist_data = response.json()
if not stylist_data:
raise HTTPException(status_code=404, detail="Stylist not found")
stylist = stylist_data[0]
stylist_stripe_id = stylist.get("stripe_id")
consultations = stylist.get("consultations")
if not consultations or not stylist_stripe_id:
raise HTTPException(status_code=400, detail="Stylist profile is incomplete")
# 🔹 3. Buscar o stripe_id e price_id do usuário autenticado
response_user = requests.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}",
headers=SUPABASE_HEADERS
)
user_data = response_user.json()
if not user_data:
raise HTTPException(status_code=404, detail="User not found")
user = user_data[0]
user_stripe_id = user.get("stripe_id")
price_id = user.get("price_id")
if not user_stripe_id:
raise HTTPException(status_code=400, detail="User does not have a Stripe ID")
if not price_id:
raise HTTPException(status_code=400, detail="User does not have a valid price ID")
# 🔹 4. Criar Checkout Session no Stripe
session = stripe.checkout.Session.create(
success_url="https://yourdomain.com/success",
cancel_url="https://yourdomain.com/cancel",
payment_method_types=["card"],
mode="subscription",
customer=user_stripe_id,
line_items=[
{
"price": price_id,
"quantity": 1,
"description": "Assinatura personalizada para usuário"
}
],
metadata={
"stylist_id": stylist_stripe_id,
"user_id": user_id, # 🔹 Continua no metadata para rastreamento interno
"consultations_per_month": consultations
}
)
return {
"message": "Checkout session created successfully!",
"checkout_url": session.url
}
except Exception as e:
logger.error(f"Error creating checkout session: {e}")
raise HTTPException(status_code=500, detail="Error creating checkout session.")
### **WEBHOOK PARA PROCESSAR PAGAMENTOS**
@router.post("/webhook")
async def stripe_webhook(request: Request):
payload = await request.body()
headers = dict(request.headers)
event = None
try:
# Tenta construir o evento Stripe a partir do payload recebido
event = stripe.Event.construct_from(json.loads(payload), stripe.api_key)
except Exception as e:
logger.error(f"⚠️ Error processing webhook: {e}")
raise HTTPException(status_code=400, detail="Webhook error")
logger.info(f"🔹 Event received: {event['type']}")
# Verifica se o evento é de uma sessão de checkout completada
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
payment_intent_id = session.get("payment_intent")
try:
# Recupera o PaymentIntent associado à sessão
payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
except Exception as e:
logger.error(f"⚠️ Error retrieving PaymentIntent: {e}")
raise HTTPException(status_code=400, detail="Error retrieving PaymentIntent")
# Extrai os dados do metadata
user_id = session.metadata.get("user_id") # ID do cliente
stylist_id = session.metadata.get("stylist_id") # ID do estilista no Stripe
consultations_per_month = session.metadata.get("consultations_per_month") # Consultas por mês
# Verifica o valor pago
amount = payment_intent["amount_received"]
# Define a divisão do pagamento entre a plataforma e o estilista
stylist_share = int(amount * 0.8) # 80% para o estilista
platform_share = amount - stylist_share # 20% para a plataforma
# Aqui, o pagamento já foi transferido para o estilista, então não há mais necessidade de fazer uma transferência manual
logger.info(f"💸 Payment of R${amount / 100:.2f} BRL received for stylist {stylist_id}.")
logger.info(f"💸 Stylist share: R${stylist_share / 100:.2f} BRL, Platform share: R${platform_share / 100:.2f} BRL.")
# Aqui você pode atualizar seu banco de dados com as informações do pagamento
# Exemplo de atualização do banco com o valor e o status do pagamento
# update_payment_status(user_id, stylist_id, amount, stylist_share, platform_share)
return {
"status": "Payment processed successfully!",
"user_id": user_id, # Retorna o ID do cliente
"stylist_id": stylist_id, # Retorna o ID do estilista
"consultations_per_month": consultations_per_month,
"total_paid": amount / 100,
"stylist_share": stylist_share / 100,
"platform_share": platform_share / 100
}
return {"status": "Event received, no action needed."}
### **CANCELAMENTO DE ASSINATURA**
class CancelSubscriptionRequest(BaseModel):
subscription_id: str
@router.post("/cancel_subscription")
def cancel_subscription(data: CancelSubscriptionRequest):
try:
subscription = stripe.Subscription.modify(
data.subscription_id,
cancel_at_period_end=True,
)
return {"status": "Subscription will be canceled at period end"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# 🚀 Verificação de Assinatura
@router.post("/check_subscription")
def check_subscription(data: CheckSubscriptionRequest):
try:
# Buscar todas as assinaturas ativas para o usuário (cliente)
subscriptions = stripe.Subscription.list(
customer=data.user_id, # ID do cliente
status="active", # Assinaturas ativas
expand=["data.items"] # Expande os dados para pegar os itens e o metadata
)
# Verificar se existe uma assinatura ativa para o estilista
for subscription in subscriptions["data"]:
# Verifica se o stylist_id está presente no metadata da assinatura
if subscription.metadata.get("stylist_id") == data.stylist_id:
return {
"status": "active",
"subscription_id": subscription.id,
"consultations_per_month": subscription.metadata.get("consultations_per_month")
}
# Caso não tenha encontrado assinatura ativa para o estilista
return {
"status": "inactive",
"message": "No active subscription found for this stylist."
}
except stripe.error.StripeError as e:
# Captura erros específicos do Stripe
raise HTTPException(status_code=500, detail=f"Stripe error: {str(e)}")
except Exception as e:
# Captura outros erros genéricos
logger.error(f"Error checking subscription: {e}")
raise HTTPException(status_code=500, detail="Error checking subscription.")