Update routes/subscription.py
Browse files- routes/subscription.py +152 -4
routes/subscription.py
CHANGED
@@ -8,8 +8,11 @@ import os
|
|
8 |
import requests
|
9 |
import asyncio
|
10 |
import jwt
|
|
|
11 |
from fastapi import APIRouter, HTTPException, Request, Header
|
12 |
from pydantic import BaseModel
|
|
|
|
|
13 |
|
14 |
router = APIRouter()
|
15 |
|
@@ -25,6 +28,10 @@ SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co"
|
|
25 |
SUPABASE_KEY = os.getenv("SUPA_KEY") # Lendo do ambiente
|
26 |
SUPABASE_ROLE_KEY = os.getenv("SUPA_SERVICE_KEY")
|
27 |
|
|
|
|
|
|
|
|
|
28 |
if not stripe.api_key or not SUPABASE_KEY or not SUPABASE_ROLE_KEY:
|
29 |
raise ValueError("❌ STRIPE_KEY, SUPA_KEY ou SUPA_SERVICE_KEY não foram definidos no ambiente!")
|
30 |
|
@@ -61,6 +68,133 @@ class CreatePriceRequest(BaseModel):
|
|
61 |
emergency_price: int # Valor de emergência (ex: 500 para R$5,00)
|
62 |
consultations: int # Número de consultas (ex: 3)
|
63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
def get_active_subscribers_by_price_id(price_id: str) -> list:
|
65 |
"""
|
66 |
Retorna uma lista de customer_ids que têm uma assinatura ativa com o price_id fornecido.
|
@@ -977,17 +1111,31 @@ async def create_price(data: CreatePriceRequest, user_token: str = Header(None,
|
|
977 |
if update_response.status_code not in [200, 204]:
|
978 |
raise HTTPException(status_code=500, detail=f"Failed to update user: {update_response.text}")
|
979 |
|
980 |
-
# 🔥 Cria notificações para os afetados
|
981 |
create_notifications_for_price_change(affected_users, stylist_id=user_id)
|
982 |
|
983 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
984 |
|
985 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
986 |
|
987 |
except Exception as e:
|
988 |
logger.error(f"❌ Error creating price: {e}")
|
989 |
raise HTTPException(status_code=500, detail=f"Error creating price: {str(e)}")
|
990 |
-
|
991 |
@router.post("/emergency_checkout_session")
|
992 |
def emergency_checkout_session(
|
993 |
data: EmergencyPaymentRequest,
|
|
|
8 |
import requests
|
9 |
import asyncio
|
10 |
import jwt
|
11 |
+
import hashlib
|
12 |
from fastapi import APIRouter, HTTPException, Request, Header
|
13 |
from pydantic import BaseModel
|
14 |
+
from google.oauth2 import service_account
|
15 |
+
from google.auth.transport.requests import Request as GoogleRequest
|
16 |
|
17 |
router = APIRouter()
|
18 |
|
|
|
28 |
SUPABASE_KEY = os.getenv("SUPA_KEY") # Lendo do ambiente
|
29 |
SUPABASE_ROLE_KEY = os.getenv("SUPA_SERVICE_KEY")
|
30 |
|
31 |
+
# Firebase config para notificações push
|
32 |
+
SERVICE_ACCOUNT_FILE = './closetcoach-2d50b-firebase-adminsdk-fbsvc-7fcccbacb1.json'
|
33 |
+
FCM_PROJECT_ID = "closetcoach-2d50b"
|
34 |
+
|
35 |
if not stripe.api_key or not SUPABASE_KEY or not SUPABASE_ROLE_KEY:
|
36 |
raise ValueError("❌ STRIPE_KEY, SUPA_KEY ou SUPA_SERVICE_KEY não foram definidos no ambiente!")
|
37 |
|
|
|
68 |
emergency_price: int # Valor de emergência (ex: 500 para R$5,00)
|
69 |
consultations: int # Número de consultas (ex: 3)
|
70 |
|
71 |
+
# ==================== FUNÇÕES DE NOTIFICAÇÃO PUSH ====================
|
72 |
+
|
73 |
+
def short_collapse_key(keyword: str, sender_id: str, receiver_id: str) -> str:
|
74 |
+
"""Gera uma chave de colapso curta para notificações"""
|
75 |
+
raw = f"{keyword}:{sender_id}:{receiver_id}"
|
76 |
+
return hashlib.sha1(raw.encode()).hexdigest()[:20]
|
77 |
+
|
78 |
+
async def fetch_supabase_async(table: str, select: str, filters: dict, headers=SUPABASE_ROLE_HEADERS):
|
79 |
+
"""Função assíncrona para buscar dados do Supabase"""
|
80 |
+
filter_query = '&'.join([f'{k}=eq.{v}' for k, v in filters.items()])
|
81 |
+
url = f"{SUPABASE_URL}/rest/v1/{table}?select={select}&{filter_query}&order=created_at.desc"
|
82 |
+
|
83 |
+
async with aiohttp.ClientSession() as session:
|
84 |
+
async with session.get(url, headers=headers) as resp:
|
85 |
+
if resp.status != 200:
|
86 |
+
detail = await resp.text()
|
87 |
+
raise HTTPException(status_code=500, detail=f"Supabase error: {detail}")
|
88 |
+
return await resp.json()
|
89 |
+
|
90 |
+
def format_name(full_name: str) -> str:
|
91 |
+
"""Formata o nome para exibição (Nome + inicial do sobrenome)"""
|
92 |
+
parts = full_name.strip().split()
|
93 |
+
if len(parts) == 1:
|
94 |
+
return parts[0]
|
95 |
+
return f"{parts[0]} {parts[1][0].upper()}."
|
96 |
+
|
97 |
+
async def get_user_info_async(user_id: str):
|
98 |
+
"""Busca informações do usuário de forma assíncrona"""
|
99 |
+
users = await fetch_supabase_async("User", "name,token_fcm", {"id": user_id})
|
100 |
+
if not users:
|
101 |
+
return None
|
102 |
+
return users[0]
|
103 |
+
|
104 |
+
def get_access_token():
|
105 |
+
"""Obtém token de acesso para Firebase Cloud Messaging"""
|
106 |
+
credentials = service_account.Credentials.from_service_account_file(
|
107 |
+
SERVICE_ACCOUNT_FILE
|
108 |
+
)
|
109 |
+
scoped_credentials = credentials.with_scopes(
|
110 |
+
['https://www.googleapis.com/auth/firebase.messaging']
|
111 |
+
)
|
112 |
+
scoped_credentials.refresh(GoogleRequest())
|
113 |
+
return scoped_credentials.token
|
114 |
+
|
115 |
+
async def send_push_notification(sender_id: str, target_user_id: str, keyword: str = "changeprice"):
|
116 |
+
"""Envia notificação push para um usuário específico"""
|
117 |
+
try:
|
118 |
+
# Buscar informações do usuário alvo
|
119 |
+
target_user = await get_user_info_async(target_user_id)
|
120 |
+
if not target_user or not target_user.get("token_fcm"):
|
121 |
+
logger.warning(f"⚠️ FCM token not found for user {target_user_id}")
|
122 |
+
return False
|
123 |
+
|
124 |
+
# Buscar informações do remetente (estilista)
|
125 |
+
actor_info = await get_user_info_async(sender_id)
|
126 |
+
if not actor_info or not actor_info.get("name"):
|
127 |
+
logger.warning(f"⚠️ Actor info not found for user {sender_id}")
|
128 |
+
return False
|
129 |
+
|
130 |
+
actor_name = format_name(actor_info["name"])
|
131 |
+
collapse_id = short_collapse_key(keyword, sender_id, target_user_id)
|
132 |
+
|
133 |
+
# Configurar conteúdo da notificação
|
134 |
+
title = "⚠️ Subscription Price Changed"
|
135 |
+
body = f"{actor_name} changed your subscription price. Your subscription was automatically canceled. Please check the chat with {actor_name} for reactivation options and more info."
|
136 |
+
|
137 |
+
# Montar mensagem FCM
|
138 |
+
message = {
|
139 |
+
"notification": {
|
140 |
+
"title": title,
|
141 |
+
"body": body,
|
142 |
+
},
|
143 |
+
"token": target_user["token_fcm"],
|
144 |
+
"android": {
|
145 |
+
"collapse_key": collapse_id,
|
146 |
+
"notification": {
|
147 |
+
"tag": collapse_id
|
148 |
+
}
|
149 |
+
},
|
150 |
+
"apns": {
|
151 |
+
"headers": {
|
152 |
+
"apns-collapse-id": collapse_id
|
153 |
+
}
|
154 |
+
}
|
155 |
+
}
|
156 |
+
|
157 |
+
payload = {"message": message}
|
158 |
+
|
159 |
+
# Enviar notificação
|
160 |
+
access_token = get_access_token()
|
161 |
+
headers = {
|
162 |
+
"Authorization": f"Bearer {access_token}",
|
163 |
+
"Content-Type": "application/json"
|
164 |
+
}
|
165 |
+
url = f"https://fcm.googleapis.com/v1/projects/{FCM_PROJECT_ID}/messages:send"
|
166 |
+
|
167 |
+
async with aiohttp.ClientSession() as session:
|
168 |
+
async with session.post(url, headers=headers, json=payload) as resp:
|
169 |
+
if resp.status == 200:
|
170 |
+
logger.info(f"✅ Push notification sent to user {target_user_id}")
|
171 |
+
return True
|
172 |
+
else:
|
173 |
+
resp_text = await resp.text()
|
174 |
+
logger.error(f"❌ FCM error for user {target_user_id}: {resp_text}")
|
175 |
+
return False
|
176 |
+
|
177 |
+
except Exception as e:
|
178 |
+
logger.error(f"❌ Error sending push notification to {target_user_id}: {e}")
|
179 |
+
return False
|
180 |
+
|
181 |
+
async def send_bulk_push_notifications(sender_id: str, target_user_ids: list):
|
182 |
+
"""Envia notificações push para múltiplos usuários"""
|
183 |
+
if not target_user_ids:
|
184 |
+
logger.info("📭 No users to notify")
|
185 |
+
return
|
186 |
+
|
187 |
+
logger.info(f"📤 Sending push notifications to {len(target_user_ids)} users")
|
188 |
+
|
189 |
+
# Enviar notificações em paralelo
|
190 |
+
tasks = [send_push_notification(sender_id, user_id) for user_id in target_user_ids]
|
191 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
192 |
+
|
193 |
+
success_count = sum(1 for result in results if result is True)
|
194 |
+
logger.info(f"✅ Successfully sent {success_count}/{len(target_user_ids)} push notifications")
|
195 |
+
|
196 |
+
# ==================== FUNÇÕES ORIGINAIS ====================
|
197 |
+
|
198 |
def get_active_subscribers_by_price_id(price_id: str) -> list:
|
199 |
"""
|
200 |
Retorna uma lista de customer_ids que têm uma assinatura ativa com o price_id fornecido.
|
|
|
1111 |
if update_response.status_code not in [200, 204]:
|
1112 |
raise HTTPException(status_code=500, detail=f"Failed to update user: {update_response.text}")
|
1113 |
|
1114 |
+
# 🔥 Cria notificações na base de dados para os afetados
|
1115 |
create_notifications_for_price_change(affected_users, stylist_id=user_id)
|
1116 |
|
1117 |
+
# 🚀 NOVA FUNCIONALIDADE: Enviar notificações push para todos os afetados
|
1118 |
+
if affected_users:
|
1119 |
+
try:
|
1120 |
+
await send_bulk_push_notifications(sender_id=user_id, target_user_ids=affected_users)
|
1121 |
+
logger.info(f"🔔 Push notifications process completed for {len(affected_users)} users")
|
1122 |
+
except Exception as push_error:
|
1123 |
+
logger.error(f"⚠️ Error sending push notifications: {push_error}")
|
1124 |
+
# Não falha a operação principal se as notificações push falharem
|
1125 |
+
|
1126 |
+
logger.info(f"✅ User updated, notifications created and push notifications sent")
|
1127 |
|
1128 |
+
return {
|
1129 |
+
"message": "Price created and user updated successfully!",
|
1130 |
+
"price_id": new_price_id,
|
1131 |
+
"affected_users_count": len(affected_users),
|
1132 |
+
"notifications_sent": True
|
1133 |
+
}
|
1134 |
|
1135 |
except Exception as e:
|
1136 |
logger.error(f"❌ Error creating price: {e}")
|
1137 |
raise HTTPException(status_code=500, detail=f"Error creating price: {str(e)}")
|
1138 |
+
|
1139 |
@router.post("/emergency_checkout_session")
|
1140 |
def emergency_checkout_session(
|
1141 |
data: EmergencyPaymentRequest,
|