Update routes/dashboard_home.py
Browse files- routes/dashboard_home.py +134 -95
routes/dashboard_home.py
CHANGED
@@ -3,10 +3,14 @@ import stripe
|
|
3 |
import requests
|
4 |
import logging
|
5 |
import pytz
|
6 |
-
|
|
|
|
|
7 |
from datetime import datetime, timedelta
|
8 |
from dateutil.relativedelta import relativedelta
|
9 |
-
from typing import Dict, Any
|
|
|
|
|
10 |
|
11 |
router = APIRouter()
|
12 |
|
@@ -29,41 +33,55 @@ SUPABASE_HEADERS = {
|
|
29 |
logging.basicConfig(level=logging.INFO)
|
30 |
logger = logging.getLogger(__name__)
|
31 |
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
headers = {
|
35 |
"Authorization": f"Bearer {user_token}",
|
36 |
"apikey": SUPABASE_KEY,
|
37 |
"Content-Type": "application/json"
|
38 |
}
|
39 |
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
# Verifica se o usuário é um administrador
|
51 |
-
user_data_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}"
|
52 |
-
response = requests.get(user_data_url, headers=SUPABASE_HEADERS)
|
53 |
-
|
54 |
-
if response.status_code != 200 or not response.json():
|
55 |
-
raise HTTPException(status_code=404, detail="Usuário não encontrado")
|
56 |
|
57 |
-
|
58 |
-
is_admin =
|
59 |
|
60 |
if not is_admin:
|
61 |
raise HTTPException(status_code=403, detail="Acesso negado: privilégios de administrador necessários")
|
62 |
|
63 |
return user_id
|
64 |
|
65 |
-
def
|
66 |
-
"""
|
|
|
|
|
|
|
|
|
67 |
try:
|
68 |
query_params = {"limit": 100}
|
69 |
|
@@ -79,12 +97,13 @@ def get_total_platform_revenue(start_timestamp: int = None, end_timestamp: int =
|
|
79 |
has_more = True
|
80 |
last_id = None
|
81 |
|
82 |
-
#
|
83 |
while has_more:
|
84 |
if last_id:
|
85 |
query_params["starting_after"] = last_id
|
86 |
-
|
87 |
-
|
|
|
88 |
payments.extend(payment_list.data)
|
89 |
|
90 |
has_more = payment_list.has_more
|
@@ -93,24 +112,22 @@ def get_total_platform_revenue(start_timestamp: int = None, end_timestamp: int =
|
|
93 |
else:
|
94 |
has_more = False
|
95 |
|
96 |
-
# Calcular totais
|
97 |
-
|
98 |
-
successful_revenue = 0
|
99 |
-
failed_revenue = 0
|
100 |
|
101 |
for payment in payments:
|
102 |
amount = payment.amount
|
103 |
-
|
104 |
|
105 |
if payment.status == 'succeeded':
|
106 |
-
|
107 |
elif payment.status == 'failed':
|
108 |
-
|
109 |
|
110 |
return {
|
111 |
-
"total_revenue":
|
112 |
-
"successful_revenue":
|
113 |
-
"failed_revenue":
|
114 |
"currency": "BRL",
|
115 |
"payment_count": len(payments)
|
116 |
}
|
@@ -126,8 +143,8 @@ def get_total_platform_revenue(start_timestamp: int = None, end_timestamp: int =
|
|
126 |
"error": str(e)
|
127 |
}
|
128 |
|
129 |
-
def get_platform_transfers(start_timestamp: int = None, end_timestamp: int = None) -> Dict[str, Any]:
|
130 |
-
"""Obtém o total transferido para estilistas"""
|
131 |
try:
|
132 |
query_params = {"limit": 100}
|
133 |
|
@@ -143,12 +160,13 @@ def get_platform_transfers(start_timestamp: int = None, end_timestamp: int = Non
|
|
143 |
has_more = True
|
144 |
last_id = None
|
145 |
|
146 |
-
#
|
147 |
while has_more:
|
148 |
if last_id:
|
149 |
query_params["starting_after"] = last_id
|
150 |
-
|
151 |
-
|
|
|
152 |
transfers.extend(transfer_list.data)
|
153 |
|
154 |
has_more = transfer_list.has_more
|
@@ -157,7 +175,7 @@ def get_platform_transfers(start_timestamp: int = None, end_timestamp: int = Non
|
|
157 |
else:
|
158 |
has_more = False
|
159 |
|
160 |
-
#
|
161 |
total_transferred = sum(transfer.amount for transfer in transfers)
|
162 |
|
163 |
return {
|
@@ -194,8 +212,46 @@ def get_app_revenue_share(total_revenue: int, total_transferred: int) -> Dict[st
|
|
194 |
"currency": "BRL"
|
195 |
}
|
196 |
|
197 |
-
def
|
198 |
-
"""Obtém
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
199 |
ny_timezone = pytz.timezone('America/New_York')
|
200 |
if not isinstance(target_date, datetime):
|
201 |
target_date = datetime.now(ny_timezone)
|
@@ -211,9 +267,11 @@ def get_monthly_revenue(target_date) -> Dict[str, Any]:
|
|
211 |
start_timestamp = int(month_start.timestamp())
|
212 |
end_timestamp = int(month_end.timestamp())
|
213 |
|
214 |
-
# Obter dados do mês
|
215 |
-
revenue_data =
|
216 |
-
|
|
|
|
|
217 |
|
218 |
# Calcular divisão app/estilistas
|
219 |
total_revenue = revenue_data["successful_revenue"]
|
@@ -231,52 +289,18 @@ def get_monthly_revenue(target_date) -> Dict[str, Any]:
|
|
231 |
"transfer_count": transfer_data["transfer_count"]
|
232 |
}
|
233 |
|
234 |
-
def get_platform_users() -> Dict[str, Any]:
|
235 |
-
"""Obtém informações sobre usuários e estilistas da plataforma"""
|
236 |
-
try:
|
237 |
-
# Obter número total de usuários
|
238 |
-
users_url = f"{SUPABASE_URL}/rest/v1/User?select=id"
|
239 |
-
users_response = requests.get(users_url, headers=SUPABASE_HEADERS)
|
240 |
-
|
241 |
-
total_users = 0
|
242 |
-
if users_response.status_code == 200:
|
243 |
-
total_users = len(users_response.json())
|
244 |
-
|
245 |
-
# Obter número total de estilistas
|
246 |
-
stylists_url = f"{SUPABASE_URL}/rest/v1/User?role=eq.stylist&select=id"
|
247 |
-
stylists_response = requests.get(stylists_url, headers=SUPABASE_HEADERS)
|
248 |
-
|
249 |
-
total_stylists = 0
|
250 |
-
if stylists_response.status_code == 200:
|
251 |
-
total_stylists = len(stylists_response.json())
|
252 |
-
|
253 |
-
logger.info(f"Total de usuários: {total_users}, Total de estilistas: {total_stylists}")
|
254 |
-
|
255 |
-
return {
|
256 |
-
"total_users": total_users,
|
257 |
-
"total_stylists": total_stylists
|
258 |
-
}
|
259 |
-
|
260 |
-
except Exception as e:
|
261 |
-
logger.error(f"❌ Erro ao obter informações de usuários: {str(e)}")
|
262 |
-
return {
|
263 |
-
"total_users": 0,
|
264 |
-
"total_stylists": 0,
|
265 |
-
"error": str(e)
|
266 |
-
}
|
267 |
-
|
268 |
@router.get("/admin/dashboard")
|
269 |
-
def get_admin_dashboard(
|
270 |
user_token: str = Header(None, alias="User-key"),
|
271 |
period: str = Query("all_time", description="Período: all_time, last_month, last_year")
|
272 |
):
|
273 |
"""
|
274 |
Endpoint para dashboard administrativo com métricas de faturamento
|
275 |
-
e divisão entre app e estilistas
|
276 |
"""
|
277 |
try:
|
278 |
# Verificar se é um administrador
|
279 |
-
user_id = verify_admin_token(user_token)
|
280 |
|
281 |
# Definir intervalo de datas baseado no período solicitado
|
282 |
ny_timezone = pytz.timezone('America/New_York')
|
@@ -292,19 +316,36 @@ def get_admin_dashboard(
|
|
292 |
start_date = now_ny - relativedelta(years=1)
|
293 |
start_timestamp = int(start_date.timestamp())
|
294 |
|
295 |
-
#
|
296 |
-
|
297 |
-
|
|
|
|
|
|
|
298 |
|
299 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
300 |
revenue_share = get_app_revenue_share(
|
301 |
revenue_data["successful_revenue"],
|
302 |
transfer_data["total_transferred_to_stylists"]
|
303 |
)
|
304 |
|
305 |
-
# Obter dados de usuários
|
306 |
-
user_data = get_platform_users()
|
307 |
-
|
308 |
# Preparar resposta
|
309 |
response = {
|
310 |
"total_revenue": revenue_data["successful_revenue"],
|
@@ -321,9 +362,7 @@ def get_admin_dashboard(
|
|
321 |
}
|
322 |
|
323 |
# Adicionar dados específicos de mês apenas para período last_month
|
324 |
-
if period == "last_month":
|
325 |
-
target_date = now_ny - relativedelta(months=1)
|
326 |
-
monthly_data = get_monthly_revenue(target_date)
|
327 |
response["monthly_data"] = monthly_data
|
328 |
|
329 |
return response
|
|
|
3 |
import requests
|
4 |
import logging
|
5 |
import pytz
|
6 |
+
import asyncio
|
7 |
+
import aiohttp
|
8 |
+
from fastapi import APIRouter, HTTPException, Header, Query, Depends
|
9 |
from datetime import datetime, timedelta
|
10 |
from dateutil.relativedelta import relativedelta
|
11 |
+
from typing import Dict, Any, Optional, List, Tuple
|
12 |
+
from functools import lru_cache
|
13 |
+
from concurrent.futures import ThreadPoolExecutor
|
14 |
|
15 |
router = APIRouter()
|
16 |
|
|
|
33 |
logging.basicConfig(level=logging.INFO)
|
34 |
logger = logging.getLogger(__name__)
|
35 |
|
36 |
+
# Pool de threads para chamadas paralelas
|
37 |
+
thread_pool = ThreadPoolExecutor(max_workers=10)
|
38 |
+
|
39 |
+
# Cache para reduzir chamadas repetidas
|
40 |
+
@lru_cache(maxsize=128)
|
41 |
+
def get_cached_admin_status(user_id: str) -> bool:
|
42 |
+
"""Obtém e armazena em cache se um usuário é admin"""
|
43 |
+
user_data_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}"
|
44 |
+
response = requests.get(user_data_url, headers=SUPABASE_HEADERS)
|
45 |
+
|
46 |
+
if response.status_code != 200 or not response.json():
|
47 |
+
return False
|
48 |
+
|
49 |
+
user_info = response.json()[0]
|
50 |
+
return user_info.get("is_admin", False)
|
51 |
+
|
52 |
+
async def verify_admin_token(user_token: str) -> str:
|
53 |
+
"""Verifica se o token pertence a um administrador de forma assíncrona"""
|
54 |
headers = {
|
55 |
"Authorization": f"Bearer {user_token}",
|
56 |
"apikey": SUPABASE_KEY,
|
57 |
"Content-Type": "application/json"
|
58 |
}
|
59 |
|
60 |
+
async with aiohttp.ClientSession() as session:
|
61 |
+
# Verificar se o token é válido
|
62 |
+
async with session.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers) as response:
|
63 |
+
if response.status != 200:
|
64 |
+
raise HTTPException(status_code=401, detail="Token inválido ou expirado")
|
65 |
+
|
66 |
+
user_data = await response.json()
|
67 |
+
user_id = user_data.get("id")
|
68 |
+
if not user_id:
|
69 |
+
raise HTTPException(status_code=400, detail="ID do usuário não encontrado")
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
|
71 |
+
# Usar cache para verificar se é admin
|
72 |
+
is_admin = await asyncio.to_thread(get_cached_admin_status, user_id)
|
73 |
|
74 |
if not is_admin:
|
75 |
raise HTTPException(status_code=403, detail="Acesso negado: privilégios de administrador necessários")
|
76 |
|
77 |
return user_id
|
78 |
|
79 |
+
async def fetch_stripe_data(fetch_func, **params):
|
80 |
+
"""Executa chamadas ao Stripe em thread separada para não bloquear"""
|
81 |
+
return await asyncio.to_thread(fetch_func, **params)
|
82 |
+
|
83 |
+
async def get_total_platform_revenue(start_timestamp: Optional[int] = None, end_timestamp: Optional[int] = None) -> Dict[str, Any]:
|
84 |
+
"""Obtém o faturamento total da plataforma no Stripe de forma otimizada"""
|
85 |
try:
|
86 |
query_params = {"limit": 100}
|
87 |
|
|
|
97 |
has_more = True
|
98 |
last_id = None
|
99 |
|
100 |
+
# Usar batch para reduzir chamadas
|
101 |
while has_more:
|
102 |
if last_id:
|
103 |
query_params["starting_after"] = last_id
|
104 |
+
|
105 |
+
# Usar thread para não bloquear o evento loop
|
106 |
+
payment_list = await fetch_stripe_data(stripe.Charge.list, **query_params)
|
107 |
payments.extend(payment_list.data)
|
108 |
|
109 |
has_more = payment_list.has_more
|
|
|
112 |
else:
|
113 |
has_more = False
|
114 |
|
115 |
+
# Calcular totais em uma única passagem
|
116 |
+
totals = {"total": 0, "succeeded": 0, "failed": 0}
|
|
|
|
|
117 |
|
118 |
for payment in payments:
|
119 |
amount = payment.amount
|
120 |
+
totals["total"] += amount
|
121 |
|
122 |
if payment.status == 'succeeded':
|
123 |
+
totals["succeeded"] += amount
|
124 |
elif payment.status == 'failed':
|
125 |
+
totals["failed"] += amount
|
126 |
|
127 |
return {
|
128 |
+
"total_revenue": totals["total"],
|
129 |
+
"successful_revenue": totals["succeeded"],
|
130 |
+
"failed_revenue": totals["failed"],
|
131 |
"currency": "BRL",
|
132 |
"payment_count": len(payments)
|
133 |
}
|
|
|
143 |
"error": str(e)
|
144 |
}
|
145 |
|
146 |
+
async def get_platform_transfers(start_timestamp: Optional[int] = None, end_timestamp: Optional[int] = None) -> Dict[str, Any]:
|
147 |
+
"""Obtém o total transferido para estilistas de forma otimizada"""
|
148 |
try:
|
149 |
query_params = {"limit": 100}
|
150 |
|
|
|
160 |
has_more = True
|
161 |
last_id = None
|
162 |
|
163 |
+
# Usar batch para reduzir chamadas
|
164 |
while has_more:
|
165 |
if last_id:
|
166 |
query_params["starting_after"] = last_id
|
167 |
+
|
168 |
+
# Usar thread para não bloquear o evento loop
|
169 |
+
transfer_list = await fetch_stripe_data(stripe.Transfer.list, **query_params)
|
170 |
transfers.extend(transfer_list.data)
|
171 |
|
172 |
has_more = transfer_list.has_more
|
|
|
175 |
else:
|
176 |
has_more = False
|
177 |
|
178 |
+
# Cálculo otimizado
|
179 |
total_transferred = sum(transfer.amount for transfer in transfers)
|
180 |
|
181 |
return {
|
|
|
212 |
"currency": "BRL"
|
213 |
}
|
214 |
|
215 |
+
async def get_platform_users() -> Dict[str, Any]:
|
216 |
+
"""Obtém informações sobre usuários e estilistas da plataforma de forma assíncrona"""
|
217 |
+
try:
|
218 |
+
async with aiohttp.ClientSession() as session:
|
219 |
+
# Executar ambas as chamadas em paralelo
|
220 |
+
tasks = [
|
221 |
+
session.get(f"{SUPABASE_URL}/rest/v1/User?select=id", headers=SUPABASE_HEADERS),
|
222 |
+
session.get(f"{SUPABASE_URL}/rest/v1/User?role=eq.stylist&select=id", headers=SUPABASE_HEADERS)
|
223 |
+
]
|
224 |
+
|
225 |
+
responses = await asyncio.gather(*tasks)
|
226 |
+
users_response, stylists_response = responses
|
227 |
+
|
228 |
+
total_users = 0
|
229 |
+
if users_response.status == 200:
|
230 |
+
users_data = await users_response.json()
|
231 |
+
total_users = len(users_data)
|
232 |
+
|
233 |
+
total_stylists = 0
|
234 |
+
if stylists_response.status == 200:
|
235 |
+
stylists_data = await stylists_response.json()
|
236 |
+
total_stylists = len(stylists_data)
|
237 |
+
|
238 |
+
logger.info(f"Total de usuários: {total_users}, Total de estilistas: {total_stylists}")
|
239 |
+
|
240 |
+
return {
|
241 |
+
"total_users": total_users,
|
242 |
+
"total_stylists": total_stylists
|
243 |
+
}
|
244 |
+
|
245 |
+
except Exception as e:
|
246 |
+
logger.error(f"❌ Erro ao obter informações de usuários: {str(e)}")
|
247 |
+
return {
|
248 |
+
"total_users": 0,
|
249 |
+
"total_stylists": 0,
|
250 |
+
"error": str(e)
|
251 |
+
}
|
252 |
+
|
253 |
+
async def get_monthly_revenue_data(target_date) -> Dict[str, Any]:
|
254 |
+
"""Obtém o faturamento de um mês específico de forma otimizada"""
|
255 |
ny_timezone = pytz.timezone('America/New_York')
|
256 |
if not isinstance(target_date, datetime):
|
257 |
target_date = datetime.now(ny_timezone)
|
|
|
267 |
start_timestamp = int(month_start.timestamp())
|
268 |
end_timestamp = int(month_end.timestamp())
|
269 |
|
270 |
+
# Obter dados do mês em paralelo
|
271 |
+
revenue_data, transfer_data = await asyncio.gather(
|
272 |
+
get_total_platform_revenue(start_timestamp, end_timestamp),
|
273 |
+
get_platform_transfers(start_timestamp, end_timestamp)
|
274 |
+
)
|
275 |
|
276 |
# Calcular divisão app/estilistas
|
277 |
total_revenue = revenue_data["successful_revenue"]
|
|
|
289 |
"transfer_count": transfer_data["transfer_count"]
|
290 |
}
|
291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
292 |
@router.get("/admin/dashboard")
|
293 |
+
async def get_admin_dashboard(
|
294 |
user_token: str = Header(None, alias="User-key"),
|
295 |
period: str = Query("all_time", description="Período: all_time, last_month, last_year")
|
296 |
):
|
297 |
"""
|
298 |
Endpoint para dashboard administrativo com métricas de faturamento
|
299 |
+
e divisão entre app e estilistas - versão otimizada
|
300 |
"""
|
301 |
try:
|
302 |
# Verificar se é um administrador
|
303 |
+
user_id = await verify_admin_token(user_token)
|
304 |
|
305 |
# Definir intervalo de datas baseado no período solicitado
|
306 |
ny_timezone = pytz.timezone('America/New_York')
|
|
|
316 |
start_date = now_ny - relativedelta(years=1)
|
317 |
start_timestamp = int(start_date.timestamp())
|
318 |
|
319 |
+
# Executar todas as chamadas em paralelo
|
320 |
+
tasks = [
|
321 |
+
get_total_platform_revenue(start_timestamp, end_timestamp),
|
322 |
+
get_platform_transfers(start_timestamp, end_timestamp),
|
323 |
+
get_platform_users()
|
324 |
+
]
|
325 |
|
326 |
+
# Adicionar task para dados mensais se necessário
|
327 |
+
monthly_task = None
|
328 |
+
if period == "last_month":
|
329 |
+
target_date = now_ny - relativedelta(months=1)
|
330 |
+
monthly_task = get_monthly_revenue_data(target_date)
|
331 |
+
tasks.append(monthly_task)
|
332 |
+
|
333 |
+
# Esperar por todas as chamadas completarem
|
334 |
+
results = await asyncio.gather(*tasks)
|
335 |
+
|
336 |
+
# Extrair resultados
|
337 |
+
if period == "last_month":
|
338 |
+
revenue_data, transfer_data, user_data, monthly_data = results
|
339 |
+
else:
|
340 |
+
revenue_data, transfer_data, user_data = results
|
341 |
+
monthly_data = None
|
342 |
+
|
343 |
+
# Calcular divisão de receita
|
344 |
revenue_share = get_app_revenue_share(
|
345 |
revenue_data["successful_revenue"],
|
346 |
transfer_data["total_transferred_to_stylists"]
|
347 |
)
|
348 |
|
|
|
|
|
|
|
349 |
# Preparar resposta
|
350 |
response = {
|
351 |
"total_revenue": revenue_data["successful_revenue"],
|
|
|
362 |
}
|
363 |
|
364 |
# Adicionar dados específicos de mês apenas para período last_month
|
365 |
+
if period == "last_month" and monthly_data:
|
|
|
|
|
366 |
response["monthly_data"] = monthly_data
|
367 |
|
368 |
return response
|