|
|
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__) |
|
|
|
|
|
|
|
|
stripe.api_key = os.getenv("STRIPE_KEY") |
|
|
stripe.api_version = "2023-10-16" |
|
|
|
|
|
|
|
|
SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co" |
|
|
SUPABASE_KEY = os.getenv("SUPA_KEY") |
|
|
|
|
|
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 |
|
|
|
|
|
class CreatePriceRequest(BaseModel): |
|
|
amount: int |
|
|
emergency_price: int |
|
|
consultations: int |
|
|
|
|
|
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", {}) |
|
|
|
|
|
|
|
|
amount_paid = invoice.get("amount_paid", 0) |
|
|
currency = invoice.get("currency", "usd") |
|
|
|
|
|
|
|
|
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 {} |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
stylist_amount = int(amount_paid * 0.8) |
|
|
platform_amount = int(amount_paid * 0.2) |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
nyc_tz = pytz.timezone('America/New_York') |
|
|
created_at = datetime.datetime.now(nyc_tz) |
|
|
|
|
|
|
|
|
subscription_data = { |
|
|
"stylist_id": stylist_id, |
|
|
"customer_id": user_id, |
|
|
"active": True, |
|
|
"created_at": created_at.isoformat(), |
|
|
"sub_id": subscription_id, |
|
|
"price_id": price_id |
|
|
} |
|
|
|
|
|
|
|
|
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, |
|
|
"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") |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
supabase_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}" |
|
|
headers = { |
|
|
"Authorization": f"Bearer {user_token}", |
|
|
**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}") |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
update_data = { |
|
|
"price_id": new_price_id, |
|
|
"price": amount, |
|
|
"emergency_price": emergency_price, |
|
|
"consultations": consultations |
|
|
} |
|
|
|
|
|
|
|
|
update_headers = { |
|
|
"Authorization": f"Bearer {user_token}", |
|
|
"apikey": SUPABASE_KEY, |
|
|
"Content-Type": "application/json" |
|
|
} |
|
|
|
|
|
update_response = requests.patch(supabase_url, headers=update_headers, json=update_data) |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
user_id = verify_token(user_token) |
|
|
|
|
|
|
|
|
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") |
|
|
stylist_stripe_id = stylist.get("stripe_id") |
|
|
price_id = stylist.get("price_id") |
|
|
|
|
|
if not stylist_stripe_id or not price_id: |
|
|
raise HTTPException(status_code=400, detail="Stylist profile is incomplete") |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
if not user_stripe_id: |
|
|
raise HTTPException(status_code=400, detail="Client does not have a Stripe Customer ID") |
|
|
|
|
|
|
|
|
price_details = stripe.Price.retrieve(price_id) |
|
|
stylist_price = price_details.unit_amount |
|
|
|
|
|
|
|
|
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={ |
|
|
"application_fee_percent": 20, |
|
|
"transfer_data": { |
|
|
"destination": stylist_stripe_id |
|
|
}, |
|
|
"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={ |
|
|
"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.") |
|
|
|
|
|
|
|
|
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") |
|
|
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") |
|
|
|
|
|
|
|
|
user_id = verify_token(user_token) |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
subscriptions = stripe.Subscription.list( |
|
|
customer=user_stripe_id, |
|
|
status="active", |
|
|
expand=["data.items"] |
|
|
) |
|
|
|
|
|
if "data" not in subscriptions: |
|
|
raise HTTPException(status_code=500, detail="No subscriptions data found in response") |
|
|
|
|
|
|
|
|
for subscription in subscriptions["data"]: |
|
|
|
|
|
if subscription.metadata.get("stylist_id") == data.stylist_id: |
|
|
|
|
|
subscription_id = subscription.id |
|
|
price_id = subscription.items.data[0].price.id |
|
|
|
|
|
|
|
|
nyc_tz = pytz.timezone('America/New_York') |
|
|
created_at = datetime.now(nyc_tz) |
|
|
|
|
|
|
|
|
subscription_data = { |
|
|
"stylist_id": data.stylist_id, |
|
|
"customer_id": user_id, |
|
|
"active": True, |
|
|
"created_at": created_at.isoformat(), |
|
|
"sub_id": subscription_id, |
|
|
"price_id": price_id |
|
|
} |
|
|
|
|
|
|
|
|
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": "active", |
|
|
"subscription_id": subscription.id, |
|
|
"consultations_per_month": subscription.metadata.get("consultations_per_month") |
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
"status": "inactive", |
|
|
"message": "No active subscription found for this stylist." |
|
|
} |
|
|
|
|
|
except stripe.error.StripeError as e: |
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Stripe error: {str(e)}") |
|
|
except Exception as e: |
|
|
|
|
|
logger.error(f"Error checking subscription: {e}") |
|
|
raise HTTPException(status_code=500, detail="Error checking subscription.") |