habulaj commited on
Commit
6bd9c66
·
verified ·
1 Parent(s): 998f9ff

Update routes/dashboard_home.py

Browse files
Files changed (1) hide show
  1. 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
- from fastapi import APIRouter, HTTPException, Header, Query
 
 
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
- def verify_admin_token(user_token: str) -> str:
33
- """Verifica se o token pertence a um administrador"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  headers = {
35
  "Authorization": f"Bearer {user_token}",
36
  "apikey": SUPABASE_KEY,
37
  "Content-Type": "application/json"
38
  }
39
 
40
- # Primeiro, verifica se o token é válido
41
- response = requests.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers)
42
- if response.status_code != 200:
43
- raise HTTPException(status_code=401, detail="Token inválido ou expirado")
44
-
45
- user_data = response.json()
46
- user_id = user_data.get("id")
47
- if not user_id:
48
- raise HTTPException(status_code=400, detail="ID do usuário não encontrado")
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
- user_info = response.json()[0]
58
- is_admin = user_info.get("is_admin", False)
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 get_total_platform_revenue(start_timestamp: int = None, end_timestamp: int = None) -> Dict[str, Any]:
66
- """Obtém o faturamento total da plataforma no Stripe"""
 
 
 
 
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
- # Paginar resultados para obter todos os pagamentos
83
  while has_more:
84
  if last_id:
85
  query_params["starting_after"] = last_id
86
-
87
- payment_list = stripe.Charge.list(**query_params)
 
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
- total_revenue = 0
98
- successful_revenue = 0
99
- failed_revenue = 0
100
 
101
  for payment in payments:
102
  amount = payment.amount
103
- total_revenue += amount
104
 
105
  if payment.status == 'succeeded':
106
- successful_revenue += amount
107
  elif payment.status == 'failed':
108
- failed_revenue += amount
109
 
110
  return {
111
- "total_revenue": total_revenue,
112
- "successful_revenue": successful_revenue,
113
- "failed_revenue": 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
- # Paginar resultados para obter todas as transferências
147
  while has_more:
148
  if last_id:
149
  query_params["starting_after"] = last_id
150
-
151
- transfer_list = stripe.Transfer.list(**query_params)
 
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
- # Calcular total transferido para estilistas
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 get_monthly_revenue(target_date) -> Dict[str, Any]:
198
- """Obtém o faturamento de um mês específico"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = get_total_platform_revenue(start_timestamp, end_timestamp)
216
- transfer_data = get_platform_transfers(start_timestamp, end_timestamp)
 
 
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
- # Obter dados gerais de faturamento
296
- revenue_data = get_total_platform_revenue(start_timestamp, end_timestamp)
297
- transfer_data = get_platform_transfers(start_timestamp, end_timestamp)
 
 
 
298
 
299
- # Calcular divisão de receita entre app e estilistas
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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