rairo commited on
Commit
f96cf30
·
verified ·
1 Parent(s): 502af87

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +300 -137
main.py CHANGED
@@ -18,98 +18,67 @@ CORS(app, resources={r"/api/*": {"origins": "*"}})
18
 
19
  # --- Firebase Initialization (Firestore) ---
20
  try:
21
- # 1. Load credentials from environment variable
22
  credentials_json_string = os.environ.get("FIREBASE")
23
  if not credentials_json_string:
24
  raise ValueError("The FIREBASE environment variable is not set.")
25
 
26
- # 2. Parse the JSON string into a dictionary
27
  credentials_json = json.loads(credentials_json_string)
28
-
29
- # 3. Create a credential object
30
  cred = credentials.Certificate(credentials_json)
31
 
32
- # 4. Initialize the app if not already done
33
  if not firebase_admin._apps:
34
  firebase_admin.initialize_app(cred)
35
  logging.info("Firebase Admin SDK initialized successfully.")
36
 
37
- # 5. Get a Firestore client instance
38
  db = firestore.client()
39
 
40
  except Exception as e:
41
  logging.critical(f"FATAL: Error initializing Firebase: {e}")
42
- # In a real deployment, this would cause the container/process to exit
43
  exit(1)
44
 
45
-
46
-
47
-
48
-
49
-
50
  # -----------------------------------------------------------------------------
51
  # 2. AUTHORIZATION MIDDLEWARE (HELPER FUNCTIONS)
52
  # -----------------------------------------------------------------------------
53
 
54
  def verify_token(auth_header):
55
- """Verifies the Firebase ID token from the Authorization header."""
56
  if not auth_header or not auth_header.startswith('Bearer '):
57
  return None
58
  token = auth_header.split('Bearer ')[1]
59
  try:
60
- decoded_token = auth.verify_id_token(token)
61
- return decoded_token['uid']
62
  except Exception as e:
63
  logging.warning(f"Token verification failed: {e}")
64
  return None
65
 
66
  def verify_admin_and_get_uid(auth_header):
67
- """
68
- Verifies if the user is an admin based on our Firestore `users` collection.
69
- Raises PermissionError if not authorized, otherwise returns admin UID.
70
- """
71
  uid = verify_token(auth_header)
72
  if not uid:
73
  raise PermissionError('Invalid or missing user token')
74
-
75
- user_ref = db.collection('users').document(uid)
76
- user_doc = user_ref.get()
77
-
78
  if not user_doc.exists or not user_doc.to_dict().get('isAdmin', False):
79
  raise PermissionError('Admin access required')
80
  return uid
81
 
82
-
83
  # -----------------------------------------------------------------------------
84
  # 3. AUTHENTICATION & USER MANAGEMENT ENDPOINTS
85
  # -----------------------------------------------------------------------------
86
 
87
  @app.route('/api/auth/signup', methods=['POST'])
88
  def signup():
89
- """Handles new user sign-up with email/password and creates their Firestore profile."""
90
  try:
91
  data = request.get_json()
92
  email, password, display_name = data.get('email'), data.get('password'), data.get('displayName')
93
 
94
- if not email or not password or not display_name:
95
  return jsonify({'error': 'Email, password, and display name are required'}), 400
96
 
97
- # Step 1: Create the user in Firebase Authentication
98
- user = auth.create_user(
99
- email=email,
100
- password=password,
101
- display_name=display_name
102
- )
103
 
104
- # Step 2: Create the corresponding user profile in the Firestore 'users' collection
105
  user_data = {
106
- 'uid': user.uid,
107
- 'email': email,
108
- 'displayName': display_name,
109
- 'isAdmin': False,
110
- 'phone': None,
111
- 'phoneStatus': 'unsubmitted', # Initial status
112
- 'organizationId': None,
113
  'createdAt': firestore.SERVER_TIMESTAMP
114
  }
115
  db.collection('users').document(user.uid).set(user_data)
@@ -127,33 +96,23 @@ def signup():
127
  def social_signin():
128
  """Ensures a user record exists in Firestore after a social login."""
129
  uid = verify_token(request.headers.get('Authorization'))
130
- if not uid:
131
- return jsonify({'error': 'Invalid or expired token'}), 401
132
 
133
  user_ref = db.collection('users').document(uid)
134
  user_doc = user_ref.get()
135
 
136
  if user_doc.exists:
137
- # User already exists, return their profile
138
  return jsonify({'uid': uid, **user_doc.to_dict()}), 200
139
  else:
140
- # This is a new user (first social login), create their full profile in Firestore.
141
  logging.info(f"New social user detected: {uid}. Creating database profile.")
142
  try:
143
  firebase_user = auth.get_user(uid)
144
  new_user_data = {
145
- 'uid': uid,
146
- 'email': firebase_user.email,
147
- 'displayName': firebase_user.display_name,
148
- 'isAdmin': False,
149
- 'phone': None,
150
- 'phoneStatus': 'unsubmitted',
151
- 'organizationId': None,
152
  'createdAt': firestore.SERVER_TIMESTAMP
153
  }
154
  user_ref.set(new_user_data)
155
-
156
- logging.info(f"Successfully created profile for new social user: {uid}")
157
  return jsonify({'success': True, 'uid': uid, **new_user_data}), 201
158
  except Exception as e:
159
  logging.error(f"Error creating profile for new social user {uid}: {e}")
@@ -167,12 +126,10 @@ def social_signin():
167
  def get_user_profile():
168
  """Retrieves the logged-in user's profile from Firestore."""
169
  uid = verify_token(request.headers.get('Authorization'))
170
- if not uid:
171
- return jsonify({'error': 'Invalid or expired token'}), 401
172
 
173
  user_doc = db.collection('users').document(uid).get()
174
- if not user_doc.exists:
175
- return jsonify({'error': 'User profile not found in database'}), 404
176
 
177
  return jsonify({'uid': uid, **user_doc.to_dict()})
178
 
@@ -180,8 +137,7 @@ def get_user_profile():
180
  def update_user_phone():
181
  """Allows a user to submit their WhatsApp phone number for admin approval."""
182
  uid = verify_token(request.headers.get('Authorization'))
183
- if not uid:
184
- return jsonify({'error': 'Invalid or expired token'}), 401
185
 
186
  data = request.get_json()
187
  phone_number = data.get('phone')
@@ -190,68 +146,53 @@ def update_user_phone():
190
  return jsonify({'error': 'A valid phone number is required.'}), 400
191
 
192
  try:
193
- user_ref = db.collection('users').document(uid)
194
- user_ref.update({
195
- 'phone': phone_number,
196
- 'phoneStatus': 'pending'
197
- })
198
-
199
  logging.info(f"User {uid} submitted phone number {phone_number} for approval.")
200
  return jsonify({'success': True, 'message': 'Phone number submitted for approval.'}), 200
201
-
202
  except Exception as e:
203
  logging.error(f"Error updating phone for user {uid}: {e}")
204
  return jsonify({'error': 'Failed to update phone number'}), 500
205
 
206
  @app.route('/api/user/dashboard', methods=['GET'])
207
  def get_user_dashboard():
208
- """
209
- Retrieves and aggregates data for the logged-in user's dashboard.
210
- This endpoint reads from the bot's data collections.
211
- """
212
  uid = verify_token(request.headers.get('Authorization'))
213
- if not uid:
214
- return jsonify({'error': 'Invalid or expired token'}), 401
215
 
216
- # Get the user's phone number from their main profile
217
  user_doc = db.collection('users').document(uid).get()
218
- if not user_doc.exists:
219
- return jsonify({'error': 'User not found'}), 404
220
 
221
  user_data = user_doc.to_dict()
222
  if user_data.get('phoneStatus') != 'approved':
223
- return jsonify({'error': 'Your phone number is not yet approved. Bot data is unavailable.'}), 403
224
 
225
  phone_number = user_data.get('phone')
226
- if not phone_number:
227
- return jsonify({'error': 'No phone number is associated with your account.'}), 404
228
 
229
  try:
230
- # The phone number is the document ID in the bot's user collection
231
  bot_user_ref = db.collection('users').document(phone_number)
232
 
233
- # Fetch data from sub-collections
234
  sales_docs = bot_user_ref.collection('sales').stream()
235
- expenses_docs = bot_user_ref.collection('expenses').stream()
236
-
237
- # Aggregate data
238
- total_sales = 0
239
- total_expenses = 0
240
- sales_count = 0
241
  for doc in sales_docs:
242
  details = doc.to_dict().get('details', {})
243
- total_sales += float(details.get('price', 0))
 
244
  sales_count += 1
245
 
246
- for doc in expenses_docs:
247
- details = doc.to_dict().get('details', {})
248
- total_expenses += float(details.get('amount', 0))
 
 
249
 
250
  dashboard_data = {
251
- 'totalSalesAmount': total_sales,
252
- 'totalSalesCount': sales_count,
253
- 'totalExpensesAmount': total_expenses,
254
- 'net': total_sales - total_expenses
 
 
255
  }
256
  return jsonify(dashboard_data), 200
257
 
@@ -260,85 +201,307 @@ def get_user_dashboard():
260
  return jsonify({'error': 'An error occurred while fetching your dashboard data.'}), 500
261
 
262
  # -----------------------------------------------------------------------------
263
- # 5. ADMIN ENDPOINTS
264
  # -----------------------------------------------------------------------------
265
 
266
  @app.route('/api/admin/users', methods=['GET'])
267
  def get_all_users():
268
- """Admin endpoint to retrieve a list of all users."""
269
  try:
270
  verify_admin_and_get_uid(request.headers.get('Authorization'))
271
-
272
- users_ref = db.collection('users')
273
- all_users = [doc.to_dict() for doc in users_ref.stream()]
274
-
275
  return jsonify(all_users), 200
276
- except PermissionError as e:
277
- return jsonify({'error': str(e)}), 403
278
  except Exception as e:
279
  logging.error(f"Admin failed to fetch all users: {e}")
280
  return jsonify({'error': 'An internal error occurred'}), 500
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  @app.route('/api/admin/users/approve', methods=['POST'])
283
  def approve_user_phone():
284
- """Admin endpoint to approve a user's phone number."""
285
  try:
286
  verify_admin_and_get_uid(request.headers.get('Authorization'))
287
  data = request.get_json()
288
  target_uid = data.get('uid')
289
- if not target_uid:
290
- return jsonify({'error': 'User UID is required'}), 400
291
-
292
  user_ref = db.collection('users').document(target_uid)
293
  user_doc = user_ref.get()
294
- if not user_doc.exists:
295
- return jsonify({'error': 'User to approve not found'}), 404
 
 
 
 
 
 
 
 
 
 
296
 
297
- user_data = user_doc.to_dict()
298
- phone_number = user_data.get('phone')
299
- if not phone_number:
300
- return jsonify({'error': 'User has not submitted a phone number'}), 400
301
 
302
- # Create a batch for atomic writes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  batch = db.batch()
 
 
 
 
 
 
 
 
304
 
305
- # Write 1: Update the main user profile
306
- batch.update(user_ref, {'phoneStatus': 'approved'})
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
- # Write 2: Update the bot's user data document
309
- bot_user_ref = db.collection('users').document(phone_number)
310
- batch.set(bot_user_ref, {'status': 'approved'}, merge=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
- # Commit both writes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  batch.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
- logging.info(f"Admin approved phone {phone_number} for user {target_uid}")
316
- return jsonify({'success': True, 'message': f'User {target_uid} approved.'}), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  except PermissionError as e:
319
  return jsonify({'error': str(e)}), 403
320
  except Exception as e:
321
- logging.error(f"Admin approval failed for user {data.get('uid')}: {e}")
322
- return jsonify({'error': 'An internal error occurred during approval'}), 500
323
-
324
- # Additional admin endpoints like 'reject', 'create_user', 'stats' can be added here
325
- # following the same pattern.
326
 
327
  # -----------------------------------------------------------------------------
328
- # 6. SERVER EXECUTION
329
  # -----------------------------------------------------------------------------
330
 
331
  if __name__ == '__main__':
332
  port = int(os.environ.get("PORT", 7860))
333
  debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
334
- logging.info(f"Starting Flask app. Debug mode: {debug_mode}, Port: {port}")
335
- if not db:
336
- logging.critical("Firestore client failed to initialize. Application cannot run.")
 
 
337
  else:
338
- if not debug_mode:
339
- from waitress import serve
340
- logging.info("Running in production mode using Waitress.")
341
- serve(app, host="0.0.0.0", port=port)
342
- else:
343
- logging.info("Running in debug mode using Flask development server.")
344
- app.run(debug=True, host="0.0.0.0", port=port)
 
18
 
19
  # --- Firebase Initialization (Firestore) ---
20
  try:
 
21
  credentials_json_string = os.environ.get("FIREBASE")
22
  if not credentials_json_string:
23
  raise ValueError("The FIREBASE environment variable is not set.")
24
 
 
25
  credentials_json = json.loads(credentials_json_string)
 
 
26
  cred = credentials.Certificate(credentials_json)
27
 
 
28
  if not firebase_admin._apps:
29
  firebase_admin.initialize_app(cred)
30
  logging.info("Firebase Admin SDK initialized successfully.")
31
 
 
32
  db = firestore.client()
33
 
34
  except Exception as e:
35
  logging.critical(f"FATAL: Error initializing Firebase: {e}")
 
36
  exit(1)
37
 
 
 
 
 
 
38
  # -----------------------------------------------------------------------------
39
  # 2. AUTHORIZATION MIDDLEWARE (HELPER FUNCTIONS)
40
  # -----------------------------------------------------------------------------
41
 
42
  def verify_token(auth_header):
43
+ """Verifies the Firebase ID token and returns the user's UID."""
44
  if not auth_header or not auth_header.startswith('Bearer '):
45
  return None
46
  token = auth_header.split('Bearer ')[1]
47
  try:
48
+ return auth.verify_id_token(token)['uid']
 
49
  except Exception as e:
50
  logging.warning(f"Token verification failed: {e}")
51
  return None
52
 
53
  def verify_admin_and_get_uid(auth_header):
54
+ """Verifies if the user is an admin and returns their UID."""
 
 
 
55
  uid = verify_token(auth_header)
56
  if not uid:
57
  raise PermissionError('Invalid or missing user token')
58
+ user_doc = db.collection('users').document(uid).get()
 
 
 
59
  if not user_doc.exists or not user_doc.to_dict().get('isAdmin', False):
60
  raise PermissionError('Admin access required')
61
  return uid
62
 
 
63
  # -----------------------------------------------------------------------------
64
  # 3. AUTHENTICATION & USER MANAGEMENT ENDPOINTS
65
  # -----------------------------------------------------------------------------
66
 
67
  @app.route('/api/auth/signup', methods=['POST'])
68
  def signup():
69
+ """Handles new user sign-up."""
70
  try:
71
  data = request.get_json()
72
  email, password, display_name = data.get('email'), data.get('password'), data.get('displayName')
73
 
74
+ if not all([email, password, display_name]):
75
  return jsonify({'error': 'Email, password, and display name are required'}), 400
76
 
77
+ user = auth.create_user(email=email, password=password, display_name=display_name)
 
 
 
 
 
78
 
 
79
  user_data = {
80
+ 'uid': user.uid, 'email': email, 'displayName': display_name, 'isAdmin': False,
81
+ 'phone': None, 'phoneStatus': 'unsubmitted', 'organizationId': None,
 
 
 
 
 
82
  'createdAt': firestore.SERVER_TIMESTAMP
83
  }
84
  db.collection('users').document(user.uid).set(user_data)
 
96
  def social_signin():
97
  """Ensures a user record exists in Firestore after a social login."""
98
  uid = verify_token(request.headers.get('Authorization'))
99
+ if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
 
100
 
101
  user_ref = db.collection('users').document(uid)
102
  user_doc = user_ref.get()
103
 
104
  if user_doc.exists:
 
105
  return jsonify({'uid': uid, **user_doc.to_dict()}), 200
106
  else:
 
107
  logging.info(f"New social user detected: {uid}. Creating database profile.")
108
  try:
109
  firebase_user = auth.get_user(uid)
110
  new_user_data = {
111
+ 'uid': uid, 'email': firebase_user.email, 'displayName': firebase_user.display_name,
112
+ 'isAdmin': False, 'phone': None, 'phoneStatus': 'unsubmitted', 'organizationId': None,
 
 
 
 
 
113
  'createdAt': firestore.SERVER_TIMESTAMP
114
  }
115
  user_ref.set(new_user_data)
 
 
116
  return jsonify({'success': True, 'uid': uid, **new_user_data}), 201
117
  except Exception as e:
118
  logging.error(f"Error creating profile for new social user {uid}: {e}")
 
126
  def get_user_profile():
127
  """Retrieves the logged-in user's profile from Firestore."""
128
  uid = verify_token(request.headers.get('Authorization'))
129
+ if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
 
130
 
131
  user_doc = db.collection('users').document(uid).get()
132
+ if not user_doc.exists: return jsonify({'error': 'User profile not found in database'}), 404
 
133
 
134
  return jsonify({'uid': uid, **user_doc.to_dict()})
135
 
 
137
  def update_user_phone():
138
  """Allows a user to submit their WhatsApp phone number for admin approval."""
139
  uid = verify_token(request.headers.get('Authorization'))
140
+ if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
 
141
 
142
  data = request.get_json()
143
  phone_number = data.get('phone')
 
146
  return jsonify({'error': 'A valid phone number is required.'}), 400
147
 
148
  try:
149
+ db.collection('users').document(uid).update({'phone': phone_number, 'phoneStatus': 'pending'})
 
 
 
 
 
150
  logging.info(f"User {uid} submitted phone number {phone_number} for approval.")
151
  return jsonify({'success': True, 'message': 'Phone number submitted for approval.'}), 200
 
152
  except Exception as e:
153
  logging.error(f"Error updating phone for user {uid}: {e}")
154
  return jsonify({'error': 'Failed to update phone number'}), 500
155
 
156
  @app.route('/api/user/dashboard', methods=['GET'])
157
  def get_user_dashboard():
158
+ """Retrieves and aggregates data for the user's dashboard with correct profit calculation."""
 
 
 
159
  uid = verify_token(request.headers.get('Authorization'))
160
+ if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
 
161
 
 
162
  user_doc = db.collection('users').document(uid).get()
163
+ if not user_doc.exists: return jsonify({'error': 'User not found'}), 404
 
164
 
165
  user_data = user_doc.to_dict()
166
  if user_data.get('phoneStatus') != 'approved':
167
+ return jsonify({'error': 'Your phone number is not yet approved.'}), 403
168
 
169
  phone_number = user_data.get('phone')
170
+ if not phone_number: return jsonify({'error': 'No phone number is associated with your account.'}), 404
 
171
 
172
  try:
 
173
  bot_user_ref = db.collection('users').document(phone_number)
174
 
 
175
  sales_docs = bot_user_ref.collection('sales').stream()
176
+ total_sales_revenue, total_cogs, sales_count = 0, 0, 0
 
 
 
 
 
177
  for doc in sales_docs:
178
  details = doc.to_dict().get('details', {})
179
+ total_sales_revenue += float(details.get('price', 0))
180
+ total_cogs += float(details.get('cost', 0))
181
  sales_count += 1
182
 
183
+ expenses_docs = bot_user_ref.collection('expenses').stream()
184
+ total_expenses = sum(float(doc.to_dict().get('details', {}).get('amount', 0)) for doc in expenses_docs)
185
+
186
+ gross_profit = total_sales_revenue - total_cogs
187
+ net_profit = gross_profit - total_expenses
188
 
189
  dashboard_data = {
190
+ 'totalSalesRevenue': round(total_sales_revenue, 2),
191
+ 'totalCostOfGoodsSold': round(total_cogs, 2),
192
+ 'grossProfit': round(gross_profit, 2),
193
+ 'totalExpenses': round(total_expenses, 2),
194
+ 'netProfit': round(net_profit, 2),
195
+ 'salesCount': sales_count,
196
  }
197
  return jsonify(dashboard_data), 200
198
 
 
201
  return jsonify({'error': 'An error occurred while fetching your dashboard data.'}), 500
202
 
203
  # -----------------------------------------------------------------------------
204
+ # 5. ADMIN USER MANAGEMENT (FULL CRUD)
205
  # -----------------------------------------------------------------------------
206
 
207
  @app.route('/api/admin/users', methods=['GET'])
208
  def get_all_users():
209
+ """Admin: Retrieve a list of all users."""
210
  try:
211
  verify_admin_and_get_uid(request.headers.get('Authorization'))
212
+ all_users = [doc.to_dict() for doc in db.collection('users').stream()]
 
 
 
213
  return jsonify(all_users), 200
214
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
 
215
  except Exception as e:
216
  logging.error(f"Admin failed to fetch all users: {e}")
217
  return jsonify({'error': 'An internal error occurred'}), 500
218
 
219
+ @app.route('/api/admin/users/<string:target_uid>', methods=['GET'])
220
+ def get_single_user(target_uid):
221
+ """Admin: Get the detailed profile of a single user."""
222
+ try:
223
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
224
+ user_doc = db.collection('users').document(target_uid).get()
225
+ if not user_doc.exists: return jsonify({'error': 'User not found'}), 404
226
+ return jsonify(user_doc.to_dict()), 200
227
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
228
+ except Exception as e:
229
+ logging.error(f"Admin failed to fetch user {target_uid}: {e}")
230
+ return jsonify({'error': 'An internal error occurred'}), 500
231
+
232
+ @app.route('/api/admin/users/<string:target_uid>', methods=['PUT'])
233
+ def admin_update_user(target_uid):
234
+ """Admin: Update a user's profile information."""
235
+ try:
236
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
237
+ data = request.get_json()
238
+ update_data = {}
239
+ if 'displayName' in data: update_data['displayName'] = data['displayName']
240
+ if 'phone' in data: update_data['phone'] = data['phone']
241
+ if 'isAdmin' in data: update_data['isAdmin'] = data['isAdmin']
242
+ if not update_data: return jsonify({'error': 'No update data provided'}), 400
243
+ db.collection('users').document(target_uid).update(update_data)
244
+ logging.info(f"Admin updated profile for user {target_uid}")
245
+ return jsonify({'success': True, 'message': 'User profile updated'}), 200
246
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
247
+ except Exception as e:
248
+ logging.error(f"Admin failed to update user {target_uid}: {e}")
249
+ return jsonify({'error': 'An internal error occurred'}), 500
250
+
251
+ @app.route('/api/admin/users/<string:target_uid>', methods=['DELETE'])
252
+ def admin_delete_user(target_uid):
253
+ """Admin: Delete a user from Auth and Firestore."""
254
+ try:
255
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
256
+ auth.delete_user(target_uid)
257
+ db.collection('users').document(target_uid).delete()
258
+ logging.info(f"Admin deleted user {target_uid} from Auth and Firestore.")
259
+ return jsonify({'success': True, 'message': 'User deleted successfully'}), 200
260
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
261
+ except Exception as e:
262
+ logging.error(f"Admin failed to delete user {target_uid}: {e}")
263
+ return jsonify({'error': 'An internal error occurred during deletion'}), 500
264
+
265
  @app.route('/api/admin/users/approve', methods=['POST'])
266
  def approve_user_phone():
267
+ """Admin: Approve a user's phone number, enabling bot access."""
268
  try:
269
  verify_admin_and_get_uid(request.headers.get('Authorization'))
270
  data = request.get_json()
271
  target_uid = data.get('uid')
272
+ if not target_uid: return jsonify({'error': 'User UID is required'}), 400
 
 
273
  user_ref = db.collection('users').document(target_uid)
274
  user_doc = user_ref.get()
275
+ if not user_doc.exists: return jsonify({'error': 'User not found'}), 404
276
+ phone_number = user_doc.to_dict().get('phone')
277
+ if not phone_number: return jsonify({'error': 'User has no phone number submitted'}), 400
278
+ batch = db.batch()
279
+ batch.update(user_ref, {'phoneStatus': 'approved'})
280
+ batch.set(db.collection('users').document(phone_number), {'status': 'approved'}, merge=True)
281
+ batch.commit()
282
+ return jsonify({'success': True, 'message': f'User {target_uid} approved.'}), 200
283
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
284
+ except Exception as e:
285
+ logging.error(f"Admin approval failed for user {data.get('uid')}: {e}")
286
+ return jsonify({'error': 'An internal error occurred'}), 500
287
 
288
+ # -----------------------------------------------------------------------------
289
+ # 6. ORGANIZATION MANAGEMENT (FULL CRUD)
290
+ # -----------------------------------------------------------------------------
 
291
 
292
+ @app.route('/api/organizations', methods=['POST'])
293
+ def create_organization():
294
+ """A logged-in user creates a new organization."""
295
+ uid = verify_token(request.headers.get('Authorization'))
296
+ if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
297
+
298
+ data = request.get_json()
299
+ org_name = data.get('name')
300
+ if not org_name: return jsonify({'error': 'Organization name is required'}), 400
301
+
302
+ try:
303
+ org_ref = db.collection('organizations').document()
304
+ org_data = {
305
+ 'id': org_ref.id, 'name': org_name, 'ownerUid': uid,
306
+ 'members': [uid], 'createdAt': firestore.SERVER_TIMESTAMP
307
+ }
308
+
309
  batch = db.batch()
310
+ batch.set(org_ref, org_data)
311
+ batch.update(db.collection('users').document(uid), {'organizationId': org_ref.id})
312
+ batch.commit()
313
+
314
+ return jsonify(org_data), 201
315
+ except Exception as e:
316
+ logging.error(f"User {uid} failed to create organization: {e}")
317
+ return jsonify({'error': 'An internal error occurred'}), 500
318
 
319
+ @app.route('/api/my-organization', methods=['GET'])
320
+ def get_my_organization():
321
+ """A logged-in user retrieves details of their organization."""
322
+ uid = verify_token(request.headers.get('Authorization'))
323
+ if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
324
+
325
+ user_doc = db.collection('users').document(uid).get()
326
+ org_id = user_doc.to_dict().get('organizationId')
327
+ if not org_id: return jsonify({'error': 'User does not belong to an organization'}), 404
328
+
329
+ org_doc = db.collection('organizations').document(org_id).get()
330
+ if not org_doc.exists: return jsonify({'error': 'Organization not found'}), 404
331
+
332
+ return jsonify(org_doc.to_dict()), 200
333
 
334
+ # --- Admin Organization Endpoints ---
335
+
336
+ @app.route('/api/admin/organizations', methods=['GET'])
337
+ def get_all_organizations():
338
+ """Admin: Get a list of all organizations."""
339
+ try:
340
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
341
+ orgs = [doc.to_dict() for doc in db.collection('organizations').stream()]
342
+ return jsonify(orgs), 200
343
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
344
+ except Exception as e:
345
+ logging.error(f"Admin failed to fetch organizations: {e}")
346
+ return jsonify({'error': 'An internal error occurred'}), 500
347
+
348
+ @app.route('/api/admin/organizations/<string:org_id>', methods=['PUT'])
349
+ def admin_update_organization(org_id):
350
+ """Admin: Update an organization's name."""
351
+ try:
352
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
353
+ data = request.get_json()
354
+ new_name = data.get('name')
355
+ if not new_name: return jsonify({'error': 'New name is required'}), 400
356
+ db.collection('organizations').document(org_id).update({'name': new_name})
357
+ return jsonify({'success': True, 'message': 'Organization updated'}), 200
358
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
359
+ except Exception as e:
360
+ logging.error(f"Admin failed to update organization {org_id}: {e}")
361
+ return jsonify({'error': 'An internal error occurred'}), 500
362
 
363
+ @app.route('/api/admin/organizations/<string:org_id>', methods=['DELETE'])
364
+ def admin_delete_organization(org_id):
365
+ """Admin: Delete an organization and clean up member profiles."""
366
+ try:
367
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
368
+ org_ref = db.collection('organizations').document(org_id)
369
+ org_doc = org_ref.get()
370
+ if not org_doc.exists: return jsonify({'error': 'Organization not found'}), 404
371
+
372
+ members = org_doc.to_dict().get('members', [])
373
+ for member_uid in members:
374
+ db.collection('users').document(member_uid).update({'organizationId': None})
375
+
376
+ org_ref.delete()
377
+ return jsonify({'success': True, 'message': 'Organization deleted'}), 200
378
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
379
+ except Exception as e:
380
+ logging.error(f"Admin failed to delete organization {org_id}: {e}")
381
+ return jsonify({'error': 'An internal error occurred'}), 500
382
+
383
+ @app.route('/api/admin/organizations/<string:org_id>/members', methods=['POST'])
384
+ def admin_add_member_to_org(org_id):
385
+ """Admin: Add a user to an organization."""
386
+ try:
387
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
388
+ data = request.get_json()
389
+ member_uid = data.get('uid')
390
+ if not member_uid: return jsonify({'error': 'User UID is required'}), 400
391
+
392
+ batch = db.batch()
393
+ batch.update(db.collection('organizations').document(org_id), {'members': firestore.ArrayUnion([member_uid])})
394
+ batch.update(db.collection('users').document(member_uid), {'organizationId': org_id})
395
  batch.commit()
396
+ return jsonify({'success': True, 'message': 'Member added'}), 200
397
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
398
+ except Exception as e:
399
+ logging.error(f"Admin failed to add member to org {org_id}: {e}")
400
+ return jsonify({'error': 'An internal error occurred'}), 500
401
+
402
+ @app.route('/api/admin/organizations/<string:org_id>/members/<string:member_uid>', methods=['DELETE'])
403
+ def admin_remove_member_from_org(org_id, member_uid):
404
+ """Admin: Remove a user from an organization."""
405
+ try:
406
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
407
+ batch = db.batch()
408
+ batch.update(db.collection('organizations').document(org_id), {'members': firestore.ArrayRemove([member_uid])})
409
+ batch.update(db.collection('users').document(member_uid), {'organizationId': None})
410
+ batch.commit()
411
+ return jsonify({'success': True, 'message': 'Member removed'}), 200
412
+ except PermissionError as e: return jsonify({'error': str(e)}), 403
413
+ except Exception as e:
414
+ logging.error(f"Admin failed to remove member from org {org_id}: {e}")
415
+ return jsonify({'error': 'An internal error occurred'}), 500
416
+
417
+ # -----------------------------------------------------------------------------
418
+ # 7. ADMIN DASHBOARD ENDPOINT (INCLUDED)
419
+ # -----------------------------------------------------------------------------
420
+
421
+ @app.route('/api/admin/dashboard/stats', methods=['GET'])
422
+ def get_admin_dashboard_stats():
423
+ """Retrieves global statistics for the admin dashboard with correct profit calculation."""
424
+ try:
425
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
426
+
427
+ # --- User and Organization Statistics ---
428
+ all_users_docs = list(db.collection('users').stream())
429
+ all_orgs_docs = list(db.collection('organizations').stream())
430
 
431
+ pending_approvals, approved_users, admin_count = 0, 0, 0
432
+ approved_phone_numbers = []
433
+
434
+ for doc in all_users_docs:
435
+ user_data = doc.to_dict()
436
+ if user_data.get('phoneStatus') == 'pending':
437
+ pending_approvals += 1
438
+ elif user_data.get('phoneStatus') == 'approved':
439
+ approved_users += 1
440
+ if user_data.get('phone'):
441
+ approved_phone_numbers.append(user_data.get('phone'))
442
+ if user_data.get('isAdmin', False):
443
+ admin_count += 1
444
+
445
+ user_stats = {
446
+ 'total': len(all_users_docs),
447
+ 'admins': admin_count,
448
+ 'approvedForBot': approved_users,
449
+ 'pendingApproval': pending_approvals,
450
+ }
451
+
452
+ org_stats = {'total': len(all_orgs_docs)}
453
+
454
+ # --- Global Bot Data Financial Statistics (Corrected Logic) ---
455
+ # Note: This reads from many collections and can be slow at large scale.
456
+ # For production, consider using Firebase Functions to aggregate this data periodically.
457
+ total_sales_revenue, total_cogs, total_expenses, sales_count = 0, 0, 0, 0
458
+
459
+ for phone in approved_phone_numbers:
460
+ bot_user_ref = db.collection('users').document(phone)
461
+ sales_docs = bot_user_ref.collection('sales').stream()
462
+ for sale_doc in sales_docs:
463
+ details = sale_doc.to_dict().get('details', {})
464
+ total_sales_revenue += float(details.get('price', 0))
465
+ total_cogs += float(details.get('cost', 0))
466
+ sales_count += 1
467
+
468
+ expenses_docs = bot_user_ref.collection('expenses').stream()
469
+ total_expenses += sum(float(doc.to_dict().get('details', {}).get('amount', 0)) for doc in expenses_docs)
470
+
471
+ gross_profit = total_sales_revenue - total_cogs
472
+ net_profit = gross_profit - total_expenses
473
+
474
+ system_stats = {
475
+ 'totalSalesRevenue': round(total_sales_revenue, 2),
476
+ 'totalCostOfGoodsSold': round(total_cogs, 2),
477
+ 'totalExpenses': round(total_expenses, 2),
478
+ 'totalNetProfit': round(net_profit, 2),
479
+ 'totalSalesCount': sales_count,
480
+ }
481
+
482
+ return jsonify({
483
+ 'userStats': user_stats,
484
+ 'organizationStats': org_stats,
485
+ 'systemStats': system_stats
486
+ }), 200
487
 
488
  except PermissionError as e:
489
  return jsonify({'error': str(e)}), 403
490
  except Exception as e:
491
+ logging.error(f"Admin failed to fetch dashboard stats: {e}", exc_info=True)
492
+ return jsonify({'error': 'An internal error occurred while fetching stats'}), 500
 
 
 
493
 
494
  # -----------------------------------------------------------------------------
495
+ # 8. SERVER EXECUTION
496
  # -----------------------------------------------------------------------------
497
 
498
  if __name__ == '__main__':
499
  port = int(os.environ.get("PORT", 7860))
500
  debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
501
+
502
+ logging.info(f"Starting Dashboard Server. Debug mode: {debug_mode}, Port: {port}")
503
+ if not debug_mode:
504
+ from waitress import serve
505
+ serve(app, host="0.0.0.0", port=port)
506
  else:
507
+ app.run(debug=True, host="0.0.0.0", port=port)