rairo commited on
Commit
ec42555
·
verified ·
1 Parent(s): 8fab0f2

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +74 -79
main.py CHANGED
@@ -69,6 +69,29 @@ def copy_collection(source_ref, dest_ref):
69
  for sub_coll_ref in doc.reference.collections():
70
  copy_collection(sub_coll_ref, dest_ref.document(doc.id).collection(sub_coll_ref.id))
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  # -----------------------------------------------------------------------------
73
  # 3. AUTHENTICATION & USER MANAGEMENT ENDPOINTS
74
  # -----------------------------------------------------------------------------
@@ -177,14 +200,12 @@ def get_user_profile():
177
 
178
  return jsonify({'uid': uid, **user_doc.to_dict()})
179
 
180
-
181
-
182
  @app.route('/api/user/dashboard', methods=['GET'])
183
  def get_user_dashboard():
184
  """
185
- Retrieves and aggregates data for the user's dashboard with correct profit calculation.
186
- **FIXED**: Now segregates all financial totals by currency and intelligently infers
187
- the currency for transactions where it is missing.
188
  """
189
  uid = verify_token(request.headers.get('Authorization'))
190
  if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
@@ -203,60 +224,42 @@ def get_user_dashboard():
203
  bot_data_id = phone_number.lstrip('+')
204
  bot_user_ref = db.collection('users').document(bot_data_id)
205
 
206
- # --- REVISED LOGIC FOR MULTI-CURRENCY ---
207
- # Use dictionaries to store totals for each currency
208
- sales_revenue_by_currency = {}
209
- cogs_by_currency = {}
210
- expenses_by_currency = {}
211
  sales_count = 0
212
 
 
213
  # Establish the user's default currency as the initial fallback
214
- last_seen_currency = user_data.get('defaultCurrency', 'USD')
 
215
 
216
  # Process Sales
217
- sales_docs = bot_user_ref.collection('sales').stream()
218
- for doc in sales_docs:
219
  details = doc.to_dict().get('details', {})
220
 
221
- # Infer currency: use the transaction's currency, or fall back to the last seen one
222
- currency = details.get('currency')
223
- if currency:
224
- last_seen_currency = currency
225
- else:
226
- currency = last_seen_currency
227
-
228
- quantity = int(details.get('quantity', 1))
229
- price = float(details.get('price', 0))
230
- cost = float(details.get('cost', 0))
231
 
232
- sales_revenue_by_currency[currency] = sales_revenue_by_currency.get(currency, 0) + (price * quantity)
233
- cogs_by_currency[currency] = cogs_by_currency.get(currency, 0) + (cost * quantity)
 
234
  sales_count += 1
235
 
236
  # Process Expenses
237
- expenses_docs = bot_user_ref.collection('expenses').stream()
238
- for doc in expenses_docs:
239
  details = doc.to_dict().get('details', {})
240
-
241
- # Infer currency using the same logic
242
- currency = details.get('currency')
243
- if currency:
244
- last_seen_currency = currency
245
- else:
246
- currency = last_seen_currency
247
 
248
  amount = float(details.get('amount', 0))
249
- expenses_by_currency[currency] = expenses_by_currency.get(currency, 0) + amount
250
 
251
- # Calculate final totals by currency
252
  all_currencies = set(sales_revenue_by_currency.keys()) | set(cogs_by_currency.keys()) | set(expenses_by_currency.keys())
253
- gross_profit_by_currency = {}
254
- net_profit_by_currency = {}
255
 
256
  for curr in all_currencies:
257
- revenue = sales_revenue_by_currency.get(curr, 0)
258
- cogs = cogs_by_currency.get(curr, 0)
259
- expenses = expenses_by_currency.get(curr, 0)
260
  gross_profit_by_currency[curr] = round(revenue - cogs, 2)
261
  net_profit_by_currency[curr] = round(revenue - cogs - expenses, 2)
262
 
@@ -268,7 +271,6 @@ def get_user_dashboard():
268
  'netProfitByCurrency': net_profit_by_currency,
269
  'salesCount': sales_count,
270
  }
271
- # --- END OF REVISED LOGIC ---
272
  return jsonify(dashboard_data), 200
273
 
274
  except Exception as e:
@@ -760,8 +762,8 @@ def admin_remove_member_from_org(org_id, member_uid):
760
  @app.route('/api/admin/dashboard/stats', methods=['GET'])
761
  def get_admin_dashboard_stats():
762
  """
763
- Retrieves global statistics for the admin dashboard, including multi-currency totals
764
- and leaderboards.
765
  """
766
  try:
767
  verify_admin_and_get_uid(request.headers.get('Authorization'))
@@ -769,18 +771,9 @@ def get_admin_dashboard_stats():
769
  all_users_docs = list(db.collection('users').stream())
770
  all_orgs_docs = list(db.collection('organizations').stream())
771
 
772
- # --- Initialize data structures ---
773
- user_sales_data = {}
774
- global_item_revenue = {}
775
- global_expense_totals = {}
776
- phone_to_user_map = {}
777
-
778
- # Global financial accumulators are now dictionaries
779
- global_sales_rev_by_curr = {}
780
- global_cogs_by_curr = {}
781
- global_expenses_by_curr = {}
782
 
783
- # --- First Pass: Get user info and list of approved phones ---
784
  pending_approvals, approved_users, admin_count = 0, 0, 0
785
  approved_phone_numbers = []
786
 
@@ -793,9 +786,8 @@ def get_admin_dashboard_stats():
793
  approved_users += 1
794
  approved_phone_numbers.append(phone)
795
  phone_to_user_map[phone] = {
796
- 'displayName': user_data.get('displayName', 'N/A'),
797
- 'uid': user_data.get('uid'),
798
- 'defaultCurrency': user_data.get('defaultCurrency', 'USD') # Store default currency
799
  }
800
  if user_data.get('isAdmin', False):
801
  admin_count += 1
@@ -803,7 +795,6 @@ def get_admin_dashboard_stats():
803
  user_stats = {'total': len(all_users_docs), 'admins': admin_count, 'approvedForBot': approved_users, 'pendingApproval': pending_approvals}
804
  org_stats = {'total': len(all_orgs_docs)}
805
 
806
- # --- Second Pass: Aggregate financial data ---
807
  sales_count = 0
808
 
809
  for phone in approved_phone_numbers:
@@ -813,59 +804,58 @@ def get_admin_dashboard_stats():
813
 
814
  user_sales_data[phone] = {'total_revenue_by_currency': {}, 'item_sales': {}}
815
  user_info = phone_to_user_map.get(phone, {})
816
- last_seen_currency = user_info.get('defaultCurrency', 'USD')
 
817
 
818
  # Process Sales
819
  for sale_doc in bot_user_ref.collection('sales').stream():
820
  details = sale_doc.to_dict().get('details', {})
821
- currency = details.get('currency')
822
- if currency: last_seen_currency = currency
823
- else: currency = last_seen_currency
824
 
825
- quantity, price = int(details.get('quantity', 1)), float(details.get('price', 0))
826
  sale_revenue = price * quantity
827
 
828
- global_sales_rev_by_curr[currency] = global_sales_rev_by_curr.get(currency, 0) + sale_revenue
829
- global_cogs_by_curr[currency] = global_cogs_by_curr.get(currency, 0) + (float(details.get('cost', 0)) * quantity)
830
  sales_count += 1
831
 
832
  item_name = details.get('item', 'Unknown Item')
833
- user_sales_data[phone]['total_revenue_by_currency'][currency] = user_sales_data[phone]['total_revenue_by_currency'].get(currency, 0) + sale_revenue
834
  user_sales_data[phone]['item_sales'][item_name] = user_sales_data[phone]['item_sales'].get(item_name, 0) + sale_revenue
835
  global_item_revenue[item_name] = global_item_revenue.get(item_name, 0) + sale_revenue
836
 
837
  # Process Expenses
838
  for expense_doc in bot_user_ref.collection('expenses').stream():
839
  details = expense_doc.to_dict().get('details', {})
840
- currency = details.get('currency')
841
- if currency: last_seen_currency = currency
842
- else: currency = last_seen_currency
843
 
844
  amount = float(details.get('amount', 0))
845
  category = details.get('description', 'Uncategorized')
846
- global_expenses_by_curr[currency] = global_expenses_by_curr.get(currency, 0) + amount
847
  global_expense_totals[category] = global_expense_totals.get(category, 0) + amount
848
  except Exception as e:
849
  logging.error(f"Admin stats: Could not process data for phone {phone}. Error: {e}")
850
  continue
851
 
852
- # --- Post-Processing: Leaderboards & Final Totals ---
853
- # Note: Ranking users with multiple currencies is complex. This example ranks them by the revenue
854
- # in their default currency. A more advanced version might convert to a single currency.
855
  sorted_users = sorted(user_sales_data.items(), key=lambda item: item[1]['total_revenue_by_currency'].get(phone_to_user_map.get(item[0], {}).get('defaultCurrency', 'USD'), 0), reverse=True)
856
  top_users_by_revenue = []
857
  for phone, data in sorted_users[:5]:
858
  user_info = phone_to_user_map.get(phone, {})
859
  top_item = max(data['item_sales'], key=data['item_sales'].get) if data['item_sales'] else 'N/A'
860
  top_users_by_revenue.append({
861
- 'displayName': user_info.get('displayName'),
862
- 'uid': user_info.get('uid'),
863
  'revenueByCurrency': {k: round(v, 2) for k, v in data['total_revenue_by_currency'].items()},
864
  'topSellingItem': top_item
865
  })
866
 
 
867
  sorted_items = sorted(global_item_revenue.items(), key=lambda item: item[1], reverse=True)
868
  top_selling_items = [{'item': name, 'totalRevenue': round(revenue, 2)} for name, revenue in sorted_items[:5]]
 
869
  sorted_expenses = sorted(global_expense_totals.items(), key=lambda item: item[1], reverse=True)
870
  top_expenses = [{'category': name, 'totalAmount': round(amount, 2)} for name, amount in sorted_expenses[:5]]
871
 
@@ -881,14 +871,19 @@ def get_admin_dashboard_stats():
881
  'totalSalesRevenueByCurrency': {k: round(v, 2) for k, v in global_sales_rev_by_curr.items()},
882
  'totalCostOfGoodsSoldByCurrency': {k: round(v, 2) for k, v in global_cogs_by_curr.items()},
883
  'totalExpensesByCurrency': {k: round(v, 2) for k, v in global_expenses_by_curr.items()},
884
- 'totalNetProfitByCurrency': global_net_profit_by_curr,
885
  'totalSalesCount': sales_count,
886
  }
887
 
888
  return jsonify({
889
- 'userStats': user_stats, 'organizationStats': org_stats,
 
890
  'systemStats': system_stats,
891
- 'leaderboards': {'topUsersByRevenue': top_users_by_revenue, 'topSellingItems': top_selling_items, 'topExpenses': top_expenses}
 
 
 
 
892
  }), 200
893
 
894
  except PermissionError as e:
 
69
  for sub_coll_ref in doc.reference.collections():
70
  copy_collection(sub_coll_ref, dest_ref.document(doc.id).collection(sub_coll_ref.id))
71
 
72
+ # In dashboard_server.py
73
+
74
+ def normalize_currency_code(raw_code, default_code='USD'):
75
+ """
76
+ Takes a messy currency string (e.g., '$', 'rand', 'R') and returns a
77
+ standard 3-letter ISO code (e.g., 'USD', 'ZAR').
78
+ """
79
+ if not raw_code or not isinstance(raw_code, str):
80
+ return default_code
81
+
82
+ # Create a mapping of common variations to the standard code
83
+ # The keys should be lowercase for case-insensitive matching
84
+ currency_map = {
85
+ # US Dollar
86
+ '$': 'USD', 'dollar': 'USD', 'dollars': 'USD', 'usd': 'USD',
87
+ # South African Rand
88
+ 'r': 'ZAR', 'rand': 'ZAR', 'rands': 'ZAR', 'zar': 'ZAR',
89
+ }
90
+
91
+ # Clean the input and check against the map
92
+ clean_code = raw_code.lower().strip()
93
+ return currency_map.get(clean_code, default_code)
94
+
95
  # -----------------------------------------------------------------------------
96
  # 3. AUTHENTICATION & USER MANAGEMENT ENDPOINTS
97
  # -----------------------------------------------------------------------------
 
200
 
201
  return jsonify({'uid': uid, **user_doc.to_dict()})
202
 
 
 
203
  @app.route('/api/user/dashboard', methods=['GET'])
204
  def get_user_dashboard():
205
  """
206
+ Retrieves and aggregates data for the user's dashboard.
207
+ **FIXED**: Now normalizes currency codes to prevent fragmentation (e.g., '$' and 'USD'
208
+ are treated as the same currency).
209
  """
210
  uid = verify_token(request.headers.get('Authorization'))
211
  if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
 
224
  bot_data_id = phone_number.lstrip('+')
225
  bot_user_ref = db.collection('users').document(bot_data_id)
226
 
227
+ sales_revenue_by_currency, cogs_by_currency, expenses_by_currency = {}, {}, {}
 
 
 
 
228
  sales_count = 0
229
 
230
+ # --- THE FIX IS HERE ---
231
  # Establish the user's default currency as the initial fallback
232
+ default_currency_code = normalize_currency_code(user_data.get('defaultCurrency'), 'USD')
233
+ last_seen_currency_code = default_currency_code
234
 
235
  # Process Sales
236
+ for doc in bot_user_ref.collection('sales').stream():
 
237
  details = doc.to_dict().get('details', {})
238
 
239
+ # Normalize the currency code using the helper function
240
+ currency_code = normalize_currency_code(details.get('currency'), last_seen_currency_code)
241
+ last_seen_currency_code = currency_code # Update for the next transaction
 
 
 
 
 
 
 
242
 
243
+ quantity, price, cost = int(details.get('quantity', 1)), float(details.get('price', 0)), float(details.get('cost', 0))
244
+ sales_revenue_by_currency[currency_code] = sales_revenue_by_currency.get(currency_code, 0) + (price * quantity)
245
+ cogs_by_currency[currency_code] = cogs_by_currency.get(currency_code, 0) + (cost * quantity)
246
  sales_count += 1
247
 
248
  # Process Expenses
249
+ for doc in bot_user_ref.collection('expenses').stream():
 
250
  details = doc.to_dict().get('details', {})
251
+ currency_code = normalize_currency_code(details.get('currency'), last_seen_currency_code)
252
+ last_seen_currency_code = currency_code
 
 
 
 
 
253
 
254
  amount = float(details.get('amount', 0))
255
+ expenses_by_currency[currency_code] = expenses_by_currency.get(currency_code, 0) + amount
256
 
257
+ # Calculate final totals using the clean, normalized currency codes
258
  all_currencies = set(sales_revenue_by_currency.keys()) | set(cogs_by_currency.keys()) | set(expenses_by_currency.keys())
259
+ gross_profit_by_currency, net_profit_by_currency = {}, {}
 
260
 
261
  for curr in all_currencies:
262
+ revenue, cogs, expenses = sales_revenue_by_currency.get(curr, 0), cogs_by_currency.get(curr, 0), expenses_by_currency.get(curr, 0)
 
 
263
  gross_profit_by_currency[curr] = round(revenue - cogs, 2)
264
  net_profit_by_currency[curr] = round(revenue - cogs - expenses, 2)
265
 
 
271
  'netProfitByCurrency': net_profit_by_currency,
272
  'salesCount': sales_count,
273
  }
 
274
  return jsonify(dashboard_data), 200
275
 
276
  except Exception as e:
 
762
  @app.route('/api/admin/dashboard/stats', methods=['GET'])
763
  def get_admin_dashboard_stats():
764
  """
765
+ Retrieves complete global statistics, including multi-currency totals and
766
+ leaderboards based on agreed-upon ranking logic.
767
  """
768
  try:
769
  verify_admin_and_get_uid(request.headers.get('Authorization'))
 
771
  all_users_docs = list(db.collection('users').stream())
772
  all_orgs_docs = list(db.collection('organizations').stream())
773
 
774
+ user_sales_data, global_item_revenue, global_expense_totals, phone_to_user_map = {}, {}, {}, {}
775
+ global_sales_rev_by_curr, global_cogs_by_curr, global_expenses_by_curr = {}, {}, {}
 
 
 
 
 
 
 
 
776
 
 
777
  pending_approvals, approved_users, admin_count = 0, 0, 0
778
  approved_phone_numbers = []
779
 
 
786
  approved_users += 1
787
  approved_phone_numbers.append(phone)
788
  phone_to_user_map[phone] = {
789
+ 'displayName': user_data.get('displayName', 'N/A'), 'uid': user_data.get('uid'),
790
+ 'defaultCurrency': user_data.get('defaultCurrency', 'USD')
 
791
  }
792
  if user_data.get('isAdmin', False):
793
  admin_count += 1
 
795
  user_stats = {'total': len(all_users_docs), 'admins': admin_count, 'approvedForBot': approved_users, 'pendingApproval': pending_approvals}
796
  org_stats = {'total': len(all_orgs_docs)}
797
 
 
798
  sales_count = 0
799
 
800
  for phone in approved_phone_numbers:
 
804
 
805
  user_sales_data[phone] = {'total_revenue_by_currency': {}, 'item_sales': {}}
806
  user_info = phone_to_user_map.get(phone, {})
807
+ default_currency_code = normalize_currency_code(user_info.get('defaultCurrency'), 'USD')
808
+ last_seen_currency_code = default_currency_code
809
 
810
  # Process Sales
811
  for sale_doc in bot_user_ref.collection('sales').stream():
812
  details = sale_doc.to_dict().get('details', {})
813
+ currency_code = normalize_currency_code(details.get('currency'), last_seen_currency_code)
814
+ last_seen_currency_code = currency_code
 
815
 
816
+ quantity, price, cost = int(details.get('quantity', 1)), float(details.get('price', 0)), float(details.get('cost', 0))
817
  sale_revenue = price * quantity
818
 
819
+ global_sales_rev_by_curr[currency_code] = global_sales_rev_by_curr.get(currency_code, 0) + sale_revenue
820
+ global_cogs_by_curr[currency_code] = global_cogs_by_curr.get(currency_code, 0) + (cost * quantity)
821
  sales_count += 1
822
 
823
  item_name = details.get('item', 'Unknown Item')
824
+ user_sales_data[phone]['total_revenue_by_currency'][currency_code] = user_sales_data[phone]['total_revenue_by_currency'].get(currency_code, 0) + sale_revenue
825
  user_sales_data[phone]['item_sales'][item_name] = user_sales_data[phone]['item_sales'].get(item_name, 0) + sale_revenue
826
  global_item_revenue[item_name] = global_item_revenue.get(item_name, 0) + sale_revenue
827
 
828
  # Process Expenses
829
  for expense_doc in bot_user_ref.collection('expenses').stream():
830
  details = expense_doc.to_dict().get('details', {})
831
+ currency_code = normalize_currency_code(details.get('currency'), last_seen_currency_code)
832
+ last_seen_currency_code = currency_code
 
833
 
834
  amount = float(details.get('amount', 0))
835
  category = details.get('description', 'Uncategorized')
836
+ global_expenses_by_curr[currency_code] = global_expenses_by_curr.get(currency_code, 0) + amount
837
  global_expense_totals[category] = global_expense_totals.get(category, 0) + amount
838
  except Exception as e:
839
  logging.error(f"Admin stats: Could not process data for phone {phone}. Error: {e}")
840
  continue
841
 
842
+ # --- Post-Processing and Final Assembly ---
843
+ # Rank users based on revenue in their primary/default currency.
 
844
  sorted_users = sorted(user_sales_data.items(), key=lambda item: item[1]['total_revenue_by_currency'].get(phone_to_user_map.get(item[0], {}).get('defaultCurrency', 'USD'), 0), reverse=True)
845
  top_users_by_revenue = []
846
  for phone, data in sorted_users[:5]:
847
  user_info = phone_to_user_map.get(phone, {})
848
  top_item = max(data['item_sales'], key=data['item_sales'].get) if data['item_sales'] else 'N/A'
849
  top_users_by_revenue.append({
850
+ 'displayName': user_info.get('displayName'), 'uid': user_info.get('uid'),
 
851
  'revenueByCurrency': {k: round(v, 2) for k, v in data['total_revenue_by_currency'].items()},
852
  'topSellingItem': top_item
853
  })
854
 
855
+ # Rank items and expenses based on their total nominal value (summing across currencies for ranking only).
856
  sorted_items = sorted(global_item_revenue.items(), key=lambda item: item[1], reverse=True)
857
  top_selling_items = [{'item': name, 'totalRevenue': round(revenue, 2)} for name, revenue in sorted_items[:5]]
858
+
859
  sorted_expenses = sorted(global_expense_totals.items(), key=lambda item: item[1], reverse=True)
860
  top_expenses = [{'category': name, 'totalAmount': round(amount, 2)} for name, amount in sorted_expenses[:5]]
861
 
 
871
  'totalSalesRevenueByCurrency': {k: round(v, 2) for k, v in global_sales_rev_by_curr.items()},
872
  'totalCostOfGoodsSoldByCurrency': {k: round(v, 2) for k, v in global_cogs_by_curr.items()},
873
  'totalExpensesByCurrency': {k: round(v, 2) for k, v in global_expenses_by_curr.items()},
874
+ 'totalNetProfitByCurrency': global_net_profit_by_curr,
875
  'totalSalesCount': sales_count,
876
  }
877
 
878
  return jsonify({
879
+ 'userStats': user_stats,
880
+ 'organizationStats': org_stats,
881
  'systemStats': system_stats,
882
+ 'leaderboards': {
883
+ 'topUsersByRevenue': top_users_by_revenue,
884
+ 'topSellingItems': top_selling_items,
885
+ 'topExpenses': top_expenses
886
+ }
887
  }), 200
888
 
889
  except PermissionError as e: