Update routes/stylist.py
Browse files- routes/stylist.py +29 -225
routes/stylist.py
CHANGED
@@ -29,17 +29,12 @@ logging.basicConfig(level=logging.INFO)
|
|
29 |
logger = logging.getLogger(__name__)
|
30 |
|
31 |
def verify_token(user_token: str) -> str:
|
32 |
-
"""
|
33 |
-
Valida o token JWT no Supabase e retorna o user_id se for válido.
|
34 |
-
"""
|
35 |
headers = {
|
36 |
"Authorization": f"Bearer {user_token}",
|
37 |
"apikey": SUPABASE_KEY,
|
38 |
"Content-Type": "application/json"
|
39 |
}
|
40 |
-
|
41 |
response = requests.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers)
|
42 |
-
|
43 |
if response.status_code == 200:
|
44 |
user_data = response.json()
|
45 |
user_id = user_data.get("id")
|
@@ -49,286 +44,95 @@ def verify_token(user_token: str) -> str:
|
|
49 |
else:
|
50 |
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
def get_monthly_revenue(account_id: str) -> List[Dict[str, Any]]:
|
53 |
-
"""
|
54 |
-
Busca os valores recebidos nos últimos 6 meses para o account_id.
|
55 |
-
Retorna uma lista com número do mês, nome do mês, indicador se é o mês atual, valor
|
56 |
-
e porcentagem de crescimento/queda em relação ao mês anterior.
|
57 |
-
"""
|
58 |
-
# Obter data atual no fuso horário de Nova York
|
59 |
ny_timezone = pytz.timezone('America/New_York')
|
60 |
now_ny = datetime.now(ny_timezone)
|
61 |
current_month = now_ny.month
|
62 |
current_year = now_ny.year
|
63 |
-
|
64 |
-
# Preparar dicionário com os últimos 6 meses (incluindo o atual)
|
65 |
monthly_data = {}
|
66 |
-
|
67 |
-
# Criar entradas para os últimos 6 meses, mesmo sem dados
|
68 |
for i in range(6):
|
69 |
-
# Calcular o mês e ano para i meses atrás
|
70 |
target_date = now_ny - relativedelta(months=i)
|
71 |
month_num = target_date.month
|
72 |
-
month_name = target_date.strftime('%b')
|
73 |
year = target_date.year
|
74 |
-
|
75 |
-
# Chave composta para garantir unicidade (ano-mês)
|
76 |
month_key = f"{year}-{month_num}"
|
77 |
-
|
78 |
-
# Verificar se é o mês atual
|
79 |
is_current = (month_num == current_month and year == current_year)
|
80 |
-
|
81 |
-
monthly_data[month_key] = {
|
82 |
-
"month": month_num,
|
83 |
-
"name": month_name,
|
84 |
-
"current": is_current,
|
85 |
-
"amount": 0,
|
86 |
-
"growth_percentage": None # Será calculado depois
|
87 |
-
}
|
88 |
-
|
89 |
-
# Calcular o timestamp para 6 meses atrás
|
90 |
start_date = now_ny - relativedelta(months=6)
|
91 |
start_timestamp = int(start_date.timestamp())
|
92 |
-
|
93 |
-
# Buscar transferências para a conta
|
94 |
try:
|
95 |
-
transfers = stripe.Transfer.list(
|
96 |
-
destination=account_id,
|
97 |
-
created={"gte": start_timestamp},
|
98 |
-
limit=100 # Limitando a 100 transferências mais recentes
|
99 |
-
)
|
100 |
-
|
101 |
-
# Processar as transferências
|
102 |
for transfer in transfers.data:
|
103 |
transfer_date = datetime.fromtimestamp(transfer.created, ny_timezone)
|
104 |
month_num = transfer_date.month
|
105 |
year = transfer_date.year
|
106 |
month_key = f"{year}-{month_num}"
|
107 |
-
|
108 |
-
# Verificar se o mês está dentro dos últimos 6 meses
|
109 |
if month_key in monthly_data:
|
110 |
-
# Adicionar o valor da transferência (em centavos)
|
111 |
monthly_data[month_key]["amount"] += transfer.amount
|
112 |
-
|
113 |
-
# Converter para lista e ordenar por data (mais antigo primeiro)
|
114 |
result = list(monthly_data.values())
|
115 |
result.sort(key=lambda x: (current_month - x["month"]) % 12)
|
116 |
-
|
117 |
-
# Calcular a porcentagem de crescimento/queda para cada mês
|
118 |
-
for i in range(1, len(result)):
|
119 |
-
current_amount = result[i]["amount"]
|
120 |
-
previous_amount = result[i-1]["amount"]
|
121 |
-
|
122 |
-
# Lógica corrigida para calcular o crescimento
|
123 |
-
if previous_amount == 0 and current_amount > 0:
|
124 |
-
# Se o mês anterior foi zero e o atual tem valor, é crescimento de 100%
|
125 |
-
result[i]["growth_percentage"] = {
|
126 |
-
"direction": "up",
|
127 |
-
"value": 100.0
|
128 |
-
}
|
129 |
-
elif previous_amount == 0 and current_amount == 0:
|
130 |
-
# Se ambos são zero, não há mudança
|
131 |
-
result[i]["growth_percentage"] = None
|
132 |
-
elif previous_amount > 0 and current_amount == 0:
|
133 |
-
# Se o anterior tinha valor e o atual é zero, é queda de 100%
|
134 |
-
result[i]["growth_percentage"] = {
|
135 |
-
"direction": "down",
|
136 |
-
"value": 100.0
|
137 |
-
}
|
138 |
-
elif previous_amount > 0:
|
139 |
-
# Cálculo normal quando ambos têm valores
|
140 |
-
growth = ((current_amount - previous_amount) / previous_amount) * 100
|
141 |
-
direction = "up" if growth >= 0 else "down"
|
142 |
-
result[i]["growth_percentage"] = {
|
143 |
-
"direction": direction,
|
144 |
-
"value": round(abs(growth), 1)
|
145 |
-
}
|
146 |
-
|
147 |
return result
|
148 |
-
|
149 |
except Exception as e:
|
150 |
logger.error(f"❌ Error getting monthly revenue: {str(e)}")
|
151 |
-
# Retornar os meses mesmo sem transferências
|
152 |
return list(monthly_data.values())
|
153 |
|
154 |
def get_account_balance(account_id: str) -> Dict[str, Any]:
|
155 |
-
"""
|
156 |
-
Busca o saldo disponível e pendente da conta Stripe.
|
157 |
-
"""
|
158 |
try:
|
159 |
balance = stripe.Balance.retrieve(stripe_account=account_id)
|
160 |
-
|
161 |
-
# Inicializar valores
|
162 |
available_balance = 0
|
163 |
pending_balance = 0
|
164 |
-
currency = "BRL"
|
165 |
-
|
166 |
-
# Calcular saldo disponível
|
167 |
for balance_item in balance.available:
|
168 |
if balance_item.currency.upper() == "BRL":
|
169 |
available_balance = balance_item.amount
|
170 |
-
currency = "BRL"
|
171 |
break
|
172 |
-
|
173 |
-
# Calcular saldo pendente
|
174 |
for balance_item in balance.pending:
|
175 |
if balance_item.currency.upper() == "BRL":
|
176 |
pending_balance = balance_item.amount
|
177 |
break
|
178 |
-
|
179 |
-
return {
|
180 |
-
"available_balance": available_balance,
|
181 |
-
"pending_balance": pending_balance,
|
182 |
-
"currency": currency
|
183 |
-
}
|
184 |
-
|
185 |
except Exception as e:
|
186 |
logger.error(f"❌ Error getting account balance: {str(e)}")
|
187 |
-
return {
|
188 |
-
"available_balance": 0,
|
189 |
-
"pending_balance": 0,
|
190 |
-
"currency": "BRL"
|
191 |
-
}
|
192 |
-
|
193 |
-
def get_total_followers(user_id: str, user_token: str) -> int:
|
194 |
-
"""
|
195 |
-
Busca o total de seguidores do usuário na tabela 'followers'.
|
196 |
-
"""
|
197 |
-
try:
|
198 |
-
# Consulta para contar os seguidores onde following_id é o user_id
|
199 |
-
followers_url = f"{SUPABASE_URL}/rest/v1/followers?select=count&following_id=eq.{user_id}"
|
200 |
-
|
201 |
-
response = requests.get(
|
202 |
-
followers_url,
|
203 |
-
headers={
|
204 |
-
"Authorization": f"Bearer {user_token}",
|
205 |
-
"apikey": SUPABASE_KEY,
|
206 |
-
"Content-Type": "application/json",
|
207 |
-
"Prefer": "count=exact"
|
208 |
-
}
|
209 |
-
)
|
210 |
-
|
211 |
-
if response.status_code != 200:
|
212 |
-
logger.error(f"❌ Error getting followers count: Status code {response.status_code}")
|
213 |
-
return 0
|
214 |
-
|
215 |
-
# Tentar obter o formato de resposta do corpo - formato diferente do esperado
|
216 |
-
try:
|
217 |
-
# Se a resposta for um array com um objeto que tem o campo count
|
218 |
-
response_data = response.json()
|
219 |
-
if isinstance(response_data, list) and len(response_data) > 0 and "count" in response_data[0]:
|
220 |
-
return int(response_data[0]["count"])
|
221 |
-
except Exception as e:
|
222 |
-
logger.error(f"❌ Error parsing followers count: {str(e)}")
|
223 |
-
|
224 |
-
# Tentar o método anterior como fallback
|
225 |
-
count = int(response.headers.get("Content-Range", "0-0/0").split("/")[-1])
|
226 |
-
return count
|
227 |
-
|
228 |
-
except Exception as e:
|
229 |
-
logger.error(f"❌ Error getting total followers: {str(e)}")
|
230 |
-
return 0
|
231 |
-
|
232 |
-
def get_total_subscribers(user_id: str, user_token: str) -> int:
|
233 |
-
"""
|
234 |
-
Busca o total de inscritos ativos do usuário na tabela 'Subscriptions'.
|
235 |
-
"""
|
236 |
-
try:
|
237 |
-
# Consulta para contar as inscrições ativas onde stylist_id é o user_id
|
238 |
-
subscribers_url = f"{SUPABASE_URL}/rest/v1/Subscriptions?select=count&stylist_id=eq.{user_id}&active=eq.true"
|
239 |
-
|
240 |
-
response = requests.get(
|
241 |
-
subscribers_url,
|
242 |
-
headers={
|
243 |
-
"Authorization": f"Bearer {user_token}",
|
244 |
-
"apikey": SUPABASE_KEY,
|
245 |
-
"Content-Type": "application/json",
|
246 |
-
"Prefer": "count=exact"
|
247 |
-
}
|
248 |
-
)
|
249 |
-
|
250 |
-
if response.status_code != 200:
|
251 |
-
logger.error(f"❌ Error getting subscribers count: Status code {response.status_code}")
|
252 |
-
return 0
|
253 |
-
|
254 |
-
# Tentar obter o formato de resposta do corpo - formato diferente do esperado
|
255 |
-
try:
|
256 |
-
# Se a resposta for um array com um objeto que tem o campo count
|
257 |
-
response_data = response.json()
|
258 |
-
if isinstance(response_data, list) and len(response_data) > 0 and "count" in response_data[0]:
|
259 |
-
return int(response_data[0]["count"])
|
260 |
-
except Exception as e:
|
261 |
-
logger.error(f"❌ Error parsing subscribers count: {str(e)}")
|
262 |
-
|
263 |
-
# Tentar o método anterior como fallback
|
264 |
-
count = int(response.headers.get("Content-Range", "0-0/0").split("/")[-1])
|
265 |
-
return count
|
266 |
-
|
267 |
-
except Exception as e:
|
268 |
-
logger.error(f"❌ Error getting total subscribers: {str(e)}")
|
269 |
-
return 0
|
270 |
|
271 |
@router.get("/dashboard")
|
272 |
def get_dashboard(user_token: str = Header(None, alias="User-key")):
|
273 |
try:
|
274 |
if not user_token:
|
275 |
raise HTTPException(status_code=401, detail="Missing User-key header")
|
276 |
-
|
277 |
-
# Validar o token e obter user_id
|
278 |
user_id = verify_token(user_token)
|
279 |
logger.info(f"🔹 User verified. user_id: {user_id}")
|
280 |
-
|
281 |
-
# Buscar stripe_id do usuário no Supabase
|
282 |
user_data_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}"
|
283 |
-
response = requests.get(
|
284 |
-
user_data_url,
|
285 |
-
headers={
|
286 |
-
"Authorization": f"Bearer {user_token}",
|
287 |
-
"apikey": SUPABASE_KEY,
|
288 |
-
"Content-Type": "application/json"
|
289 |
-
}
|
290 |
-
)
|
291 |
-
|
292 |
if response.status_code != 200 or not response.json():
|
293 |
raise HTTPException(status_code=404, detail="User not found")
|
294 |
-
|
295 |
user_data = response.json()[0]
|
296 |
stripe_id = user_data.get("stripe_id")
|
297 |
-
|
298 |
-
|
299 |
-
total_followers = get_total_followers(user_id, user_token)
|
300 |
-
|
301 |
-
# Buscar total de inscritos ativos
|
302 |
-
total_subscribers = get_total_subscribers(user_id, user_token)
|
303 |
-
|
304 |
-
# Resposta padrão se não houver stripe_id
|
305 |
if not stripe_id:
|
306 |
-
return {
|
307 |
-
"stripe_id": None,
|
308 |
-
"available_balance": 0,
|
309 |
-
"pending_balance": 0,
|
310 |
-
"currency": "BRL",
|
311 |
-
"monthly_revenue": [],
|
312 |
-
"total_followers": total_followers,
|
313 |
-
"total_subscribers": total_subscribers
|
314 |
-
}
|
315 |
-
|
316 |
-
# Buscar saldo da conta
|
317 |
balance_info = get_account_balance(stripe_id)
|
318 |
-
|
319 |
-
# Buscar valores recebidos nos últimos 6 meses
|
320 |
monthly_revenue = get_monthly_revenue(stripe_id)
|
321 |
-
|
322 |
-
return {
|
323 |
-
"stripe_id": stripe_id,
|
324 |
-
"available_balance": balance_info["available_balance"],
|
325 |
-
"pending_balance": balance_info["pending_balance"],
|
326 |
-
"currency": balance_info["currency"],
|
327 |
-
"monthly_revenue": monthly_revenue,
|
328 |
-
"total_followers": total_followers,
|
329 |
-
"total_subscribers": total_subscribers
|
330 |
-
}
|
331 |
-
|
332 |
except HTTPException as http_err:
|
333 |
raise http_err
|
334 |
except Exception as e:
|
|
|
29 |
logger = logging.getLogger(__name__)
|
30 |
|
31 |
def verify_token(user_token: str) -> str:
|
|
|
|
|
|
|
32 |
headers = {
|
33 |
"Authorization": f"Bearer {user_token}",
|
34 |
"apikey": SUPABASE_KEY,
|
35 |
"Content-Type": "application/json"
|
36 |
}
|
|
|
37 |
response = requests.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers)
|
|
|
38 |
if response.status_code == 200:
|
39 |
user_data = response.json()
|
40 |
user_id = user_data.get("id")
|
|
|
44 |
else:
|
45 |
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
46 |
|
47 |
+
def get_total_followers(user_id: str) -> int:
|
48 |
+
response = requests.get(
|
49 |
+
f"{SUPABASE_URL}/rest/v1/followers?following_id=eq.{user_id}&select=id",
|
50 |
+
headers=SUPABASE_HEADERS
|
51 |
+
)
|
52 |
+
if response.status_code == 200:
|
53 |
+
return len(response.json())
|
54 |
+
return 0
|
55 |
+
|
56 |
+
def get_total_subscribers(user_id: str) -> int:
|
57 |
+
response = requests.get(
|
58 |
+
f"{SUPABASE_URL}/rest/v1/Subscriptions?stylist_id=eq.{user_id}&active=eq.true&select=id",
|
59 |
+
headers=SUPABASE_HEADERS
|
60 |
+
)
|
61 |
+
if response.status_code == 200:
|
62 |
+
return len(response.json())
|
63 |
+
return 0
|
64 |
+
|
65 |
def get_monthly_revenue(account_id: str) -> List[Dict[str, Any]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
ny_timezone = pytz.timezone('America/New_York')
|
67 |
now_ny = datetime.now(ny_timezone)
|
68 |
current_month = now_ny.month
|
69 |
current_year = now_ny.year
|
|
|
|
|
70 |
monthly_data = {}
|
|
|
|
|
71 |
for i in range(6):
|
|
|
72 |
target_date = now_ny - relativedelta(months=i)
|
73 |
month_num = target_date.month
|
74 |
+
month_name = target_date.strftime('%b')
|
75 |
year = target_date.year
|
|
|
|
|
76 |
month_key = f"{year}-{month_num}"
|
|
|
|
|
77 |
is_current = (month_num == current_month and year == current_year)
|
78 |
+
monthly_data[month_key] = {"month": month_num, "name": month_name, "current": is_current, "amount": 0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
start_date = now_ny - relativedelta(months=6)
|
80 |
start_timestamp = int(start_date.timestamp())
|
|
|
|
|
81 |
try:
|
82 |
+
transfers = stripe.Transfer.list(destination=account_id, created={"gte": start_timestamp}, limit=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
for transfer in transfers.data:
|
84 |
transfer_date = datetime.fromtimestamp(transfer.created, ny_timezone)
|
85 |
month_num = transfer_date.month
|
86 |
year = transfer_date.year
|
87 |
month_key = f"{year}-{month_num}"
|
|
|
|
|
88 |
if month_key in monthly_data:
|
|
|
89 |
monthly_data[month_key]["amount"] += transfer.amount
|
|
|
|
|
90 |
result = list(monthly_data.values())
|
91 |
result.sort(key=lambda x: (current_month - x["month"]) % 12)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
return result
|
|
|
93 |
except Exception as e:
|
94 |
logger.error(f"❌ Error getting monthly revenue: {str(e)}")
|
|
|
95 |
return list(monthly_data.values())
|
96 |
|
97 |
def get_account_balance(account_id: str) -> Dict[str, Any]:
|
|
|
|
|
|
|
98 |
try:
|
99 |
balance = stripe.Balance.retrieve(stripe_account=account_id)
|
|
|
|
|
100 |
available_balance = 0
|
101 |
pending_balance = 0
|
102 |
+
currency = "BRL"
|
|
|
|
|
103 |
for balance_item in balance.available:
|
104 |
if balance_item.currency.upper() == "BRL":
|
105 |
available_balance = balance_item.amount
|
|
|
106 |
break
|
|
|
|
|
107 |
for balance_item in balance.pending:
|
108 |
if balance_item.currency.upper() == "BRL":
|
109 |
pending_balance = balance_item.amount
|
110 |
break
|
111 |
+
return {"available_balance": available_balance, "pending_balance": pending_balance, "currency": currency}
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
except Exception as e:
|
113 |
logger.error(f"❌ Error getting account balance: {str(e)}")
|
114 |
+
return {"available_balance": 0, "pending_balance": 0, "currency": "BRL"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
|
116 |
@router.get("/dashboard")
|
117 |
def get_dashboard(user_token: str = Header(None, alias="User-key")):
|
118 |
try:
|
119 |
if not user_token:
|
120 |
raise HTTPException(status_code=401, detail="Missing User-key header")
|
|
|
|
|
121 |
user_id = verify_token(user_token)
|
122 |
logger.info(f"🔹 User verified. user_id: {user_id}")
|
|
|
|
|
123 |
user_data_url = f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}"
|
124 |
+
response = requests.get(user_data_url, headers={"Authorization": f"Bearer {user_token}", "apikey": SUPABASE_KEY, "Content-Type": "application/json"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
if response.status_code != 200 or not response.json():
|
126 |
raise HTTPException(status_code=404, detail="User not found")
|
|
|
127 |
user_data = response.json()[0]
|
128 |
stripe_id = user_data.get("stripe_id")
|
129 |
+
total_followers = get_total_followers(user_id)
|
130 |
+
total_subscribers = get_total_subscribers(user_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
if not stripe_id:
|
132 |
+
return {"stripe_id": None, "available_balance": 0, "pending_balance": 0, "currency": "BRL", "monthly_revenue": [], "total_followers": total_followers, "total_subscribers": total_subscribers}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
balance_info = get_account_balance(stripe_id)
|
|
|
|
|
134 |
monthly_revenue = get_monthly_revenue(stripe_id)
|
135 |
+
return {"stripe_id": stripe_id, "available_balance": balance_info["available_balance"], "pending_balance": balance_info["pending_balance"], "currency": balance_info["currency"], "monthly_revenue": monthly_revenue, "total_followers": total_followers, "total_subscribers": total_subscribers}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
except HTTPException as http_err:
|
137 |
raise http_err
|
138 |
except Exception as e:
|