|
|
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__) |
|
|
|
|
|
|
|
|
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.get("/subscription/status/{subscription_id}") |
|
|
async def check_subscription_status(subscription_id: str): |
|
|
try: |
|
|
|
|
|
stripe_url = f"https://api.stripe.com/v1/subscriptions/{subscription_id}" |
|
|
headers = { |
|
|
"Authorization": f"Bearer {stripe.api_key}" |
|
|
} |
|
|
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() |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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", {}) |
|
|
|
|
|
|
|
|
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") |
|
|
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") |
|
|
|
|
|
|
|
|
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}") |
|
|
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}") |
|
|
|
|
|
|
|
|
subscription_data = { |
|
|
"stylist_id": stylist_id, |
|
|
"customer_id": user_id, |
|
|
"active": True, |
|
|
"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, |
|
|
"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...") |
|
|
|
|
|
|
|
|
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") |
|
|
cancel_status = subscription.get("cancel_at_period_end", False) |
|
|
|
|
|
if canceled_at: |
|
|
|
|
|
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).") |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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") |
|
|
user_name = user.get("name", "Unknown User") |
|
|
user_avatar = user.get("avatar", None) |
|
|
user_bio = user.get("bio", "No description provided.") |
|
|
logger.info(f"🔹 Existing price_id: {existing_price_id}, user_name: {user_name}, user_avatar: {user_avatar}, user_bio: {user_bio}") |
|
|
|
|
|
|
|
|
product_data = { |
|
|
"name": f"{user_name} - {amount} BRL", |
|
|
"description": user_bio, |
|
|
} |
|
|
|
|
|
if user_avatar: |
|
|
product_data["images"] = [user_avatar] |
|
|
|
|
|
product = stripe.Product.create(**product_data) |
|
|
product_id = product.id |
|
|
logger.info(f"✅ New product created: {product_id}") |
|
|
|
|
|
|
|
|
price = stripe.Price.create( |
|
|
unit_amount=amount, |
|
|
currency="brl", |
|
|
recurring={"interval": "month"}, |
|
|
product=product_id |
|
|
) |
|
|
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") |
|
|
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") |
|
|
|
|
|
|
|
|
user_id = verify_token(user_token) |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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"]: |
|
|
|
|
|
logger.info(f"No active subscription found for user {user_id}. Deactivating all subscriptions for stylist {data.stylist_id}.") |
|
|
|
|
|
|
|
|
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."} |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
|
|
|
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' |
|
|
) |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
if not stylist_found: |
|
|
logger.info(f"No active subscription found for stylist {data.stylist_id}. Deactivating all subscriptions for this stylist.") |
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
async def async_stripe_request(func, **kwargs): |
|
|
loop = asyncio.get_event_loop() |
|
|
response = await loop.run_in_executor(None, lambda: func(**kwargs)) |
|
|
return response |
|
|
|
|
|
|
|
|
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}.") |