|
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__) |
|
|
|
|
|
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): |
|
user_id: str |
|
stylist_id: str |
|
|
|
|
|
class SubscriptionRequest(BaseModel): |
|
id: str |
|
|
|
class CreatePriceRequest(BaseModel): |
|
amount: int |
|
user_id: str |
|
|
|
from fastapi import Header |
|
|
|
@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") |
|
|
|
|
|
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") |
|
|
|
|
|
if existing_price_id: |
|
try: |
|
stripe.Price.modify( |
|
existing_price_id, |
|
unit_amount=amount, |
|
metadata={"updated_at": "now"} |
|
) |
|
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 |
|
|
|
|
|
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}") |
|
|
|
|
|
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.") |
|
|
|
|
|
stripe.Price.modify(existing_price_id, active=False) |
|
logger.info(f"🚫 Price {existing_price_id} deactivated.") |
|
|
|
|
|
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") |
|
|
|
|
|
try: |
|
payload = jwt.decode(user_token, JWT_SECRET, algorithms=["HS256"]) |
|
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") |
|
|
|
|
|
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") |
|
|
|
|
|
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") |
|
|
|
|
|
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, |
|
"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.") |
|
|
|
|
|
@router.post("/webhook") |
|
async def stripe_webhook(request: Request): |
|
payload = await request.body() |
|
headers = dict(request.headers) |
|
event = None |
|
|
|
try: |
|
|
|
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']}") |
|
|
|
|
|
if event["type"] == "checkout.session.completed": |
|
session = event["data"]["object"] |
|
payment_intent_id = session.get("payment_intent") |
|
|
|
try: |
|
|
|
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") |
|
|
|
|
|
user_id = session.metadata.get("user_id") |
|
stylist_id = session.metadata.get("stylist_id") |
|
consultations_per_month = session.metadata.get("consultations_per_month") |
|
|
|
|
|
amount = payment_intent["amount_received"] |
|
|
|
|
|
stylist_share = int(amount * 0.8) |
|
platform_share = amount - stylist_share |
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
"status": "Payment processed successfully!", |
|
"user_id": user_id, |
|
"stylist_id": stylist_id, |
|
"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."} |
|
|
|
|
|
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): |
|
try: |
|
|
|
subscriptions = stripe.Subscription.list( |
|
customer=data.user_id, |
|
status="active", |
|
expand=["data.items"] |
|
) |
|
|
|
|
|
for subscription in subscriptions["data"]: |
|
|
|
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") |
|
} |
|
|
|
|
|
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.") |