File size: 28,182 Bytes
912124c 0a2b45f 01ccbf3 5e6bfbb 01ccbf3 c919ade 370df27 5252942 01ccbf3 912124c 0a2b45f fb71f9d 9686665 912124c 9686665 703a507 9686665 01ccbf3 912124c 8c90efe dbbfb58 8c90efe 01ccbf3 3ac3303 5d7e429 732810d e9983ad 50a3d09 b598016 50a3d09 732810d 940799f 28c1611 940799f 28c1611 940799f 03be51c 940799f 03be51c 940799f da5b64a 59aeb79 da5b64a deff01c bbd5a8a fd41c6e 68dc6ca bbd5a8a 68dc6ca bbd5a8a 68dc6ca a0c68f3 68dc6ca bbd5a8a deff01c bbd5a8a 8bd2328 bbd5a8a c38a888 f3c072b 8825b34 8bd2328 a0c68f3 8825b34 bbd5a8a deff01c bbd5a8a da5b64a 8bd2328 2375d08 da5b64a 427eb72 680dbcb ad67519 680dbcb 427eb72 680dbcb b598016 50a3d09 6e544eb 90f8b9c 680dbcb e9983ad 8093d40 427eb72 8093d40 94ce89a 50a3d09 6e544eb 2ccdcce 6e544eb 2ccdcce 6e544eb 2ccdcce 5644e04 b014290 94ce89a b014290 94ce89a 13b2342 94ce89a b014290 13b2342 261640c 50a3d09 13b2342 5644e04 261640c ad67519 13b2342 e9983ad ceb5d72 d97c844 ceb5d72 6e544eb 5aab8a4 ad67519 5aab8a4 e9983ad 427eb72 261640c 6e544eb 7c86d6c ed0d9dd 7d06ed4 912124c 7d06ed4 ebb4d38 50a3d09 90f8b9c 36ac915 ebb4d38 ef840ba 01ccbf3 f70e965 ebb4d38 01ccbf3 427eb72 36ac915 59615d0 08cdb44 01ccbf3 36ac915 c9000d6 7d06ed4 d80190a c9000d6 d80190a ebb4d38 d80190a c9000d6 36ac915 a6bb840 36ac915 ebb4d38 968f9d5 9aebe5f b3116aa 912124c 24c2864 912124c 7ef17d7 9aebe5f fd41c6e b3116aa fd41c6e b3116aa 1735268 36ac915 a0c68f3 1735268 b3116aa a0c68f3 ab80e0b 36ac915 b3116aa 968f9d5 b3116aa 36ac915 01ccbf3 912124c b598016 d80190a 0ea8eb8 a664e20 912124c ed0d9dd 912124c 9b46162 c919ade 6873666 3cbd47c 6873666 9b46162 6873666 3cbd47c 6873666 f2aed3f c919ade f2aed3f c919ade f2aed3f 602fe8a c919ade 3cbd47c 9b46162 d8e0746 c919ade fb19d38 c919ade fb19d38 c919ade fb19d38 c919ade fb19d38 48e31fe 39e10b6 9bfe44f 39e10b6 3cbd47c 6873666 3cbd47c 6873666 602fe8a 6873666 3cbd47c 6873666 c919ade 3cbd47c c919ade 6873666 602fe8a 9b46162 3cbd47c 9b46162 fb19d38 39e10b6 fb19d38 c919ade fb19d38 c919ade 39e10b6 c919ade 9b46162 9bfe44f 9b46162 9bfe44f c919ade |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 |
import stripe
import logging
import json
from datetime import datetime
import pytz
import os
import requests
import asyncio
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.get("/subscription/status/{subscription_id}")
async def check_subscription_status(subscription_id: str):
try:
# 🔹 Consulta a Stripe diretamente
stripe_url = f"https://api.stripe.com/v1/subscriptions/{subscription_id}"
headers = {
"Authorization": f"Bearer {stripe.api_key}" # ✅ Usa a chave definida no ambiente
}
response = requests.get(stripe_url, headers=headers)
if response.status_code != 200:
raise HTTPException(status_code=404, detail="Subscription not found in Stripe")
subscription = response.json()
# 🔹 Pega os dados necessários
status = subscription.get("status")
cancel_at_period_end = subscription.get("cancel_at_period_end", False)
canceled_at = subscription.get("canceled_at")
current_period_end = subscription.get("current_period_end")
# 🔹 Converte timestamps para data legível
ny_tz = pytz.timezone("America/New_York")
canceled_at_date = (
datetime.utcfromtimestamp(canceled_at).replace(tzinfo=pytz.utc).astimezone(ny_tz).isoformat()
if canceled_at else None
)
expiration_date = (
datetime.utcfromtimestamp(current_period_end).replace(tzinfo=pytz.utc).astimezone(ny_tz).isoformat()
if current_period_end else None
)
return {
"subscription_id": subscription_id,
"status": status,
"cancel_at_period_end": cancel_at_period_end,
"canceled_at": canceled_at_date,
"expiration_date": expiration_date
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@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")
user_id = metadata.get("user_id") or subscription_metadata.get("user_id") or line_item_metadata.get("user_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}")
logger.info(f"👥 User ID: {user_id}")
logger.info(f"💰 Estilista recebe: R$ {stylist_amount / 100:.2f}")
logger.info(f"🏛️ Plataforma fica com: R$ {platform_amount / 100:.2f}")
# 🔹 Inserir dados na tabela Subscriptions no Supabase
subscription_data = {
"stylist_id": stylist_id,
"customer_id": user_id,
"active": True, # Assinatura ativa após pagamento
"sub_id": subscription_id, # ID da assinatura
"price_id": price_id # ID do preço
}
supabase_headers = {
"Authorization": f"Bearer {SUPABASE_KEY}",
"apikey": SUPABASE_KEY,
"Content-Type": "application/json"
}
subscription_url = f"{SUPABASE_URL}/rest/v1/Subscriptions"
response_subscription = requests.post(
subscription_url,
headers=supabase_headers,
json=subscription_data
)
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,
"user_id": user_id,
"stylist_amount": stylist_amount / 100,
"platform_amount": platform_amount / 100
}
elif event_type == "customer.subscription.deleted":
subscription = payload.get("data", {}).get("object", {})
subscription_id = subscription.get("id")
if not subscription_id:
logger.error("❌ Subscription ID not found in event payload.")
return {"status": "error", "message": "Subscription ID missing."}
logger.info(f"🔹 Subscription {subscription_id} canceled. Updating database...")
# 🔹 Atualizar active = False no Supabase
update_url = f"{SUPABASE_URL}/rest/v1/Subscriptions?sub_id=eq.{subscription_id}"
update_data = {"active": False}
supabase_headers = {
"Authorization": f"Bearer {SUPABASE_KEY}",
"apikey": SUPABASE_KEY,
"Content-Type": "application/json"
}
response_update = requests.patch(update_url, headers=supabase_headers, json=update_data)
if response_update.status_code in [200, 204]:
logger.info(f"✅ Subscription {subscription_id} marked as inactive in Supabase.")
return {"status": "success", "message": "Subscription canceled and updated."}
else:
logger.error(f"❌ Failed to update subscription: {response_update.status_code} - {response_update.text}")
return {"status": "error", "message": "Failed to update subscription."}
if event_type == "customer.subscription.updated":
subscription = payload.get("data", {}).get("object", {})
subscription_id = subscription.get("id")
canceled_at = subscription.get("canceled_at") # Timestamp do cancelamento
cancel_status = subscription.get("cancel_at_period_end", False)
if canceled_at:
# Convertendo timestamp para data e horário de Nova York
ny_tz = pytz.timezone("America/New_York")
canceled_date = datetime.fromtimestamp(canceled_at, ny_tz).isoformat()
logger.info(f"🔹 Subscription {subscription_id} canceled at {canceled_date} (New York Time).")
# 🔹 Atualizando Supabase
update_url = f"{SUPABASE_URL}/rest/v1/Subscriptions?sub_id=eq.{subscription_id}"
update_data = {
"canceled": True,
"canceled_date": canceled_date
}
supabase_headers = {
"Authorization": f"Bearer {SUPABASE_KEY}",
"apikey": SUPABASE_KEY,
"Content-Type": "application/json"
}
response_update = requests.patch(update_url, headers=supabase_headers, json=update_data)
if response_update.status_code in [200, 204]:
logger.info(f"✅ Subscription {subscription_id} updated with canceled date.")
return {"status": "success", "message": "Subscription updated with cancellation date."}
else:
logger.error(f"❌ Failed to update subscription: {response_update.status_code} - {response_update.text}")
return {"status": "error", "message": "Failed to update subscription."}
else:
logger.info(f"🔹 Subscription {subscription_id} updated, but not canceled.")
return {"status": "success", "message": "Subscription updated but not canceled."}
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, name, avatar e bio 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")
user_name = user.get("name", "Unknown User") # 🔹 Recuperar o nome do usuário
user_avatar = user.get("avatar", None) # 🔹 Recuperar a URL do avatar do usuário, se existir
user_bio = user.get("bio", "No description provided.") # 🔹 Recuperar a bio, com padrão se estiver vazia
logger.info(f"🔹 Existing price_id: {existing_price_id}, user_name: {user_name}, user_avatar: {user_avatar}, user_bio: {user_bio}")
# 🔹 5. Criar produto no Stripe com nome, imagem e descrição baseados no usuário
product_data = {
"name": f"{user_name} - {amount} BRL", # Nome do produto
"description": user_bio, # Descrição do produto, com fallback se vazia
}
if user_avatar: # Verificar se o avatar existe e adicionar a imagem
product_data["images"] = [user_avatar]
product = stripe.Product.create(**product_data)
product_id = product.id
logger.info(f"✅ New product created: {product_id}")
# 🔹 6. Criar novo preço no Stripe associado ao produto
price = stripe.Price.create(
unit_amount=amount,
currency="brl",
recurring={"interval": "month"},
product=product_id # Associando o preço ao produto
)
new_price_id = price.id
logger.info(f"✅ New price created: {new_price_id}")
# 🔹 7. 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.")
# 🔹 8. 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))
@router.post("/check_subscription")
async def check_subscription(
data: CheckSubscriptionRequest,
user_token: str = Header(None, alias="User-key")
):
try:
if not user_token:
raise HTTPException(status_code=401, detail="Missing User-key header")
# 🔹 Validar o token e obter user_id do cliente
user_id = verify_token(user_token)
# 🔹 Buscar o stripe_id do usuário no Supabase
response_user = await async_request(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}",
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 do cliente (ativas, canceladas, expiradas, etc.)
subscriptions = await async_stripe_request(
stripe.Subscription.list,
customer=user_stripe_id,
expand=["data.items"]
)
logger.info(f"Stripe response data: {subscriptions}")
if not subscriptions or not subscriptions["data"]:
# Se não houver assinaturas para o cliente, desativa as assinaturas associadas ao estilista no banco de dados
logger.info(f"No active subscription found for user {user_id}. Deactivating all subscriptions for stylist {data.stylist_id}.")
# 🔹 Buscar todas as assinaturas do estilista no banco de dados para este cliente
response_subscriptions = await async_request(
f"{SUPABASE_URL}/rest/v1/Subscriptions?customer_id=eq.{user_id}&stylist_id=eq.{data.stylist_id}",
SUPABASE_HEADERS
)
if response_subscriptions.status_code == 200:
subscriptions_data = response_subscriptions.json()
await asyncio.gather(
*(update_subscription_status(sub['id'], False) for sub in subscriptions_data)
)
else:
logger.error(f"❌ Failed to fetch subscriptions from Supabase for stylist {data.stylist_id} and user {user_id}.")
return {"status": "inactive", "message": "No active subscription found for stylist."}
# 🔹 Verifica se existe uma assinatura ativa para o estilista
stylist_found = False
for subscription in subscriptions["data"]:
if subscription["metadata"].get("stylist_id") == data.stylist_id:
stylist_found = True
subscription_id = subscription["id"]
price_id = subscription["items"]["data"][0]["price"]["id"]
status = subscription.get("status")
cancel_at_period_end = subscription.get("cancel_at_period_end", False)
canceled_at = subscription.get("canceled_at")
current_period_end = subscription.get("current_period_end")
# 🔹 Converte timestamps para data legível
nyc_tz = pytz.timezone('America/New_York')
canceled_at_date = (
datetime.utcfromtimestamp(canceled_at).replace(tzinfo=pytz.utc).astimezone(nyc_tz).isoformat()
if canceled_at else None
)
expiration_date = (
datetime.utcfromtimestamp(current_period_end).replace(tzinfo=pytz.utc).astimezone(nyc_tz).isoformat()
if current_period_end else None
)
# 🔹 Atualiza o banco de dados Supabase com os dados da assinatura
subscription_data = {
"stylist_id": data.stylist_id,
"customer_id": user_id,
"active": status == "active",
"sub_id": subscription_id,
"price_id": price_id,
"canceled": cancel_at_period_end or (status == "canceled"),
"canceled_date": canceled_at_date
}
await async_request(
f"{SUPABASE_URL}/rest/v1/Subscriptions",
SUPABASE_HEADERS,
json=subscription_data,
method='POST'
)
# Retorna informações sobre a assinatura sincronizada
return {
"status": status,
"subscription_id": subscription_id,
"consultations_per_month": subscription["metadata"].get("consultations_per_month", None),
"cancel_at_period_end": cancel_at_period_end,
"canceled_at": canceled_at_date,
"expiration_date": expiration_date
}
# 🔹 Caso não tenha encontrado assinatura ativa para o estilista
if not stylist_found:
logger.info(f"No active subscription found for stylist {data.stylist_id}. Deactivating all subscriptions for this stylist.")
# 🔹 Buscar todas as assinaturas do estilista no banco de dados
response_subscriptions = await async_request(
f"{SUPABASE_URL}/rest/v1/Subscriptions?stylist_id=eq.{data.stylist_id}",
SUPABASE_HEADERS
)
if response_subscriptions.status_code == 200:
subscriptions_data = response_subscriptions.json()
await asyncio.gather(
*(update_subscription_status(sub['id'], False) for sub in subscriptions_data)
)
else:
logger.error(f"❌ Failed to fetch subscriptions from Supabase for stylist {data.stylist_id}.")
return {"status": "inactive", "message": "No active subscription found for stylist."}
except stripe.error.StripeError as e:
logger.error(f"Stripe error: {str(e)}")
raise HTTPException(status_code=500, detail=f"Stripe error: {str(e)}")
except Exception as e:
logger.error(f"Error checking subscription: {str(e)}")
raise HTTPException(status_code=500, detail="Error checking subscription.")
# Função assíncrona para fazer requisições de rede
async def async_request(url, headers, json=None, method='GET'):
method_func = requests.post if method == 'POST' else requests.get
response = method_func(url, headers=headers, json=json)
return response
# Função assíncrona para chamar a Stripe API
async def async_stripe_request(func, **kwargs):
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, lambda: func(**kwargs))
return response
# Função assíncrona para atualizar o status de uma assinatura
async def update_subscription_status(subscription_id, status):
update_data = {
"active": status
}
subscription_url = f"{SUPABASE_URL}/rest/v1/Subscriptions?id=eq.{subscription_id}"
update_response = await async_request(
subscription_url,
SUPABASE_HEADERS,
json=update_data,
method='PATCH'
)
if update_response.status_code == 200:
logger.info(f"✅ Subscription {subscription_id} deactivated successfully.")
else:
logger.error(f"❌ Failed to deactivate subscription {subscription_id}.") |