habulaj commited on
Commit
5567da6
·
verified ·
1 Parent(s): 7434f1d

Update routes/subscription.py

Browse files
Files changed (1) hide show
  1. 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
- logger.info(f"✅ User updated and notifications sent")
 
 
 
 
 
 
 
 
 
984
 
985
- return {"message": "Price created and user updated successfully!", "price_id": new_price_id}
 
 
 
 
 
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,