connect / routes /subscription.py
habulaj's picture
Update routes/subscription.py
48e31fe verified
raw
history blame
20.3 kB
import stripe
import logging
import json
import datetime
import pytz
import os
import requests
import jwt
from fastapi import APIRouter, HTTPException, Request, Header
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):
stylist_id: str
user_token: str = Header(None, alias="User-key")
class SubscriptionRequest(BaseModel):
id: str # ID do estilista
class CreatePriceRequest(BaseModel):
amount: int # Valor em centavos (ex: 2500 para R$25,00)
emergency_price: int # Valor de emergência (ex: 500 para R$5,00)
consultations: int # Número de consultas (ex: 3)
def verify_token(user_token: str) -> str:
"""
Valida o token JWT no Supabase e retorna o user_id se for válido.
"""
headers = {
"Authorization": f"Bearer {user_token}",
"apikey": SUPABASE_KEY,
"Content-Type": "application/json"
}
response = requests.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers)
if response.status_code == 200:
user_data = response.json()
user_id = user_data.get("id")
if not user_id:
raise HTTPException(status_code=400, detail="Invalid token: User ID not found")
return user_id
else:
raise HTTPException(status_code=401, detail="Invalid or expired token")
@router.post("/webhook")
async def stripe_webhook(request: Request):
try:
payload = await request.json()
event_type = payload.get("type")
if event_type == "invoice.payment_succeeded":
invoice = payload.get("data", {}).get("object", {})
# 🔹 Capturar valores da fatura
amount_paid = invoice.get("amount_paid", 0) # Valor total pago (em centavos)
currency = invoice.get("currency", "usd") # Moeda do pagamento
# 🔹 Buscando metadados
metadata = invoice.get("metadata", {})
subscription_metadata = invoice.get("subscription_details", {}).get("metadata", {})
line_items = invoice.get("lines", {}).get("data", [])
line_item_metadata = line_items[0].get("metadata", {}) if line_items else {}
# 🔹 Pegando os valores corretos
stylist_id = metadata.get("stylist_id") or subscription_metadata.get("stylist_id") or line_item_metadata.get("stylist_id")
stylist_stripe_id = metadata.get("stylist_stripe_id") or subscription_metadata.get("stylist_stripe_id") or line_item_metadata.get("stylist_stripe_id")
user_id = metadata.get("user_id") or subscription_metadata.get("user_id") or line_item_metadata.get("user_id")
user_stripe_id = metadata.get("user_stripe_id") or subscription_metadata.get("user_stripe_id") or line_item_metadata.get("user_stripe_id")
price_id = metadata.get("price_id") or subscription_metadata.get("price_id") or line_item_metadata.get("price_id")
subscription_id = invoice.get("subscription") # Capturando o ID da assinatura
# 🔹 Calculando a divisão do pagamento
stylist_amount = int(amount_paid * 0.8) # 80% para o estilista
platform_amount = int(amount_paid * 0.2) # 20% para a plataforma
# 🔹 Logando as informações detalhadas
logger.info(f"✅ Pagamento bem-sucedido! Valor total: R$ {amount_paid / 100:.2f}")
logger.info(f"👤 Stylist ID: {stylist_id} | Stripe ID: {stylist_stripe_id}")
logger.info(f"👥 User ID: {user_id} | Stripe ID: {user_stripe_id}")
logger.info(f"💰 Estilista recebe: R$ {stylist_amount / 100:.2f}")
logger.info(f"🏛️ Plataforma fica com: R$ {platform_amount / 100:.2f}")
# 🔹 Definir o fuso horário de Nova York usando pytz
nyc_tz = pytz.timezone('America/New_York')
created_at = datetime.datetime.now(nyc_tz) # Hora atual de Nova York
# 🔹 Inserir dados na tabela Subscriptions no Supabase (não incluindo o id)
subscription_data = {
"stylist_id": stylist_id,
"customer_id": user_id,
"active": True, # Como o pagamento foi bem-sucedido, a assinatura está ativa
"created_at": created_at.isoformat(), # Hora atual em formato ISO 8601
"sub_id": subscription_id, # ID da assinatura
"price_id": price_id # ID do preço
}
# Configuração do cabeçalho de autenticação para o Supabase
supabase_headers = {
"Authorization": f"Bearer {SUPABASE_KEY}",
"apikey": SUPABASE_KEY,
"Content-Type": "application/json"
}
# Inserir nova linha na tabela Subscriptions (não precisamos definir 'id' explicitamente)
subscription_url = f"{SUPABASE_URL}/rest/v1/Subscriptions"
response_subscription = requests.post(
subscription_url,
headers=supabase_headers,
json=subscription_data
)
# Verificando a resposta da inserção
if response_subscription.status_code == 201:
logger.info(f"✅ Subscription added successfully for user {user_id}")
else:
logger.error(f"❌ Failed to add subscription: {response_subscription.status_code} - {response_subscription.text}")
return {
"status": "success",
"total_paid": amount_paid / 100,
"stylist_id": stylist_id,
"stylist_stripe_id": stylist_stripe_id,
"user_id": user_id,
"user_stripe_id": user_stripe_id,
"stylist_amount": stylist_amount / 100,
"platform_amount": platform_amount / 100
}
except Exception as e:
logger.error(f"❌ Erro no webhook: {str(e)}")
return {"status": "error", "message": str(e)}
@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")
# 🔹 1. Validar o token e obter user_id
user_id = verify_token(user_token)
logger.info(f"🔹 User verified. user_id: {user_id}")
amount = data.amount
emergency_price = data.emergency_price
consultations = data.consultations
# 🔹 2. Verificar se os valores estão dentro dos limites permitidos
if not (500 <= amount <= 99900):
raise HTTPException(status_code=400, detail="Amount must be between $5 and $999")
if not (500 <= emergency_price <= 99900):
raise HTTPException(status_code=400, detail="Emergency price must be between $5 and $999")
# 🔹 3. Consultations precisa ser 0 obrigatoriamente se não for definido
if consultations is None:
consultations = 0
elif consultations < 0:
raise HTTPException(status_code=400, detail="Consultations must be greater than or equal to 0")
logger.info(f"🔹 Validated amounts: amount = {amount}, emergency_price = {emergency_price}, consultations = {consultations}")
# 🔹 4. Buscar price_id do usuário no Supabase
supabase_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}"
headers = {
"Authorization": f"Bearer {user_token}", # 🔹 Incluindo token correto no header
**SUPABASE_HEADERS
}
response = requests.get(supabase_url, headers=headers)
logger.info(f"🔹 Supabase GET response: {response.status_code} - {response.text}")
if response.status_code != 200:
raise HTTPException(status_code=500, detail=f"Failed to fetch user from Supabase: {response.text}")
user_data = response.json()
if not user_data:
raise HTTPException(status_code=404, detail="User not found in Supabase")
user = user_data[0]
existing_price_id = user.get("price_id")
logger.info(f"🔹 Existing price_id: {existing_price_id}")
# 🔹 5. Criar novo preço no Stripe
price = stripe.Price.create(
unit_amount=amount,
currency="brl",
recurring={"interval": "month"},
product_data={"name": "Custom Subscription Price"}
)
new_price_id = price.id
logger.info(f"✅ New price created: {new_price_id}")
# 🔹 6. Se já houver um price_id, cancelar assinaturas ativas ao final do período
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, cancel_at_period_end=True)
logger.info(f"🔹 Subscription {sub.id} set to cancel at period end.")
# 🔹 7. Atualizar Supabase com o novo price_id e valores adicionais
update_data = {
"price_id": new_price_id, # Novo price_id
"price": amount, # Atualizando o valor de 'price'
"emergency_price": emergency_price, # Atualizando o valor de 'emergency_price'
"consultations": consultations # Atualizando o valor de 'consultations'
}
# Aqui estamos incluindo o cabeçalho de autenticação com o token JWT correto
update_headers = {
"Authorization": f"Bearer {user_token}", # Incluindo token correto no header
"apikey": SUPABASE_KEY,
"Content-Type": "application/json"
}
update_response = requests.patch(supabase_url, headers=update_headers, json=update_data)
# Log detalhado para verificar a resposta
logger.info(f"🔹 Supabase PATCH response: {update_response.status_code} - {update_response.text}")
if update_response.status_code not in [200, 204]:
raise HTTPException(status_code=500, detail=f"Failed to update Supabase: {update_response.text}")
logger.info(f"✅ Successfully updated user {user_id} with new price_id, price, emergency_price, and consultations")
return {"message": "Price created and user updated successfully!", "price_id": new_price_id}
except Exception as e:
logger.error(f"❌ Error creating price: {e}")
raise HTTPException(status_code=500, detail=f"Error creating price: {str(e)}")
@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. Validar o token do cliente e obter user_id (cliente)
user_id = verify_token(user_token)
# 🔹 2. Buscar dados do estilista no Supabase
response_stylist = requests.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{data.id}",
headers=SUPABASE_HEADERS
)
stylist_data = response_stylist.json()
if not stylist_data:
raise HTTPException(status_code=404, detail="Stylist not found")
stylist = stylist_data[0]
stylist_id = stylist.get("id") # ID do estilista no Supabase
stylist_stripe_id = stylist.get("stripe_id") # ID do estilista no Stripe
price_id = stylist.get("price_id") # ID do preço no Stripe
if not stylist_stripe_id or not price_id:
raise HTTPException(status_code=400, detail="Stylist profile is incomplete")
# 🔹 3. Buscar dados do cliente no Supabase
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="Client not found")
user = user_data[0]
user_stripe_id = user.get("stripe_id") # ID do cliente no Stripe
if not user_stripe_id:
raise HTTPException(status_code=400, detail="Client does not have a Stripe Customer ID")
# 🔹 Buscar detalhes do preço no Stripe
price_details = stripe.Price.retrieve(price_id)
stylist_price = price_details.unit_amount # Obtém o valor do preço em centavos
# 🔹 Criar Checkout Session no Stripe (Sem `payment_intent_data`)
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
}
],
subscription_data={ # 🔹 Aplicando divisão de receita via `application_fee_percent`
"application_fee_percent": 20, # 20% para a plataforma
"transfer_data": {
"destination": stylist_stripe_id # Conta do estilista no Stripe
},
"metadata": {
"stylist_id": stylist_id,
"stylist_stripe_id": stylist_stripe_id,
"user_id": user_id,
"user_stripe_id": user_stripe_id,
"price_id": price_id
}
},
metadata={ # 🔹 Adicionando metadados no checkout
"stylist_id": stylist_id,
"stylist_stripe_id": stylist_stripe_id,
"user_id": user_id,
"user_stripe_id": user_stripe_id,
"price_id": price_id
}
)
logger.info(f"📌 Checkout session created successfully: {session.url}")
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.")
### **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,
user_token: str = Header(None, alias="User-key") # Recebendo o token do cabeçalho
):
try:
if not user_token:
raise HTTPException(status_code=401, detail="Missing User-key header")
# 🔹 Validar o token do cliente e obter user_id (cliente)
user_id = verify_token(user_token)
# 🔹 Buscar o stripe_id do usuário no Supabase
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_stripe_id = user_data[0].get("stripe_id")
if not user_stripe_id:
raise HTTPException(status_code=404, detail="Stripe customer not found for user")
# 🔹 Buscar todas as assinaturas ativas para o usuário (cliente) com stripe_id
subscriptions = stripe.Subscription.list(
customer=user_stripe_id, # Usando stripe_id do cliente para buscar assinaturas
status="active", # Assinaturas ativas
expand=["data.items"] # Expande os dados para pegar os itens e o metadata
)
if "data" not in subscriptions:
raise HTTPException(status_code=500, detail="No subscriptions data found in response")
# 🔹 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:
# Se encontrou a assinatura ativa, vamos registrar a assinatura no Supabase
subscription_id = subscription.id # ID da assinatura
price_id = subscription.items.data[0].price.id # ID do preço da assinatura
# 🔹 Definir o fuso horário de Nova York usando pytz
nyc_tz = pytz.timezone('America/New_York')
created_at = datetime.now(nyc_tz) # Hora atual de Nova York
# 🔹 Inserir dados na tabela Subscriptions no Supabase
subscription_data = {
"stylist_id": data.stylist_id, # ID do estilista
"customer_id": user_id, # ID do cliente
"active": True, # Assinatura ativa
"created_at": created_at.isoformat(), # Hora atual em formato ISO 8601
"sub_id": subscription_id, # ID da assinatura
"price_id": price_id # ID do preço
}
# Configuração do cabeçalho de autenticação para o Supabase
supabase_headers = {
"Authorization": f"Bearer {SUPABASE_KEY}",
"apikey": SUPABASE_KEY,
"Content-Type": "application/json"
}
# Inserir nova linha na tabela Subscriptions (não precisamos definir 'id' explicitamente)
subscription_url = f"{SUPABASE_URL}/rest/v1/Subscriptions"
response_subscription = requests.post(
subscription_url,
headers=supabase_headers,
json=subscription_data
)
# Verificando a resposta da inserção
if response_subscription.status_code == 201:
logger.info(f"✅ Subscription added successfully for user {user_id}")
else:
logger.error(f"❌ Failed to add subscription: {response_subscription.status_code} - {response_subscription.text}")
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.")