omgy commited on
Commit
d9f0b6f
·
verified ·
1 Parent(s): c65d4c7

Update app/services/firebase_service.py

Browse files
Files changed (1) hide show
  1. app/services/firebase_service.py +491 -491
app/services/firebase_service.py CHANGED
@@ -1,491 +1,491 @@
1
- """
2
- Firebase service for Firestore database and Authentication.
3
- Handles all Firebase operations including user profiles and loan applications.
4
- """
5
-
6
- import json
7
- import os
8
- from datetime import datetime
9
- from typing import Any, Dict, List, Optional
10
-
11
- import firebase_admin
12
- from app.config import settings
13
- from app.utils.logger import default_logger as logger
14
- from firebase_admin import auth, credentials, firestore
15
- from google.cloud.firestore_v1 import FieldFilter
16
-
17
-
18
- class FirebaseService:
19
- """Service for Firebase Firestore and Authentication operations."""
20
-
21
- def __init__(self):
22
- """Initialize Firebase Admin SDK and Firestore client."""
23
- self.db: Optional[firestore.Client] = None
24
- self.initialized = False
25
- self._initialize_firebase()
26
-
27
- def _initialize_firebase(self) -> None:
28
- """Initialize Firebase Admin SDK with credentials."""
29
- try:
30
- # Check if Firebase app is already initialized
31
- if not firebase_admin._apps:
32
- # Try to load credentials from environment or use default
33
- if settings.FIREBASE_CREDENTIALS:
34
- # If FIREBASE_CREDENTIALS is a JSON string
35
- if settings.FIREBASE_CREDENTIALS.startswith("{"):
36
- cred_dict = json.loads(settings.FIREBASE_CREDENTIALS)
37
- cred = credentials.Certificate(cred_dict)
38
- else:
39
- # If it's a file path
40
- cred = credentials.Certificate(settings.FIREBASE_CREDENTIALS)
41
-
42
- firebase_admin.initialize_app(
43
- cred, {"projectId": settings.FIREBASE_PROJECT_ID}
44
- )
45
- else:
46
- # Use Application Default Credentials (for local dev with gcloud)
47
- firebase_admin.initialize_app(
48
- options={"projectId": settings.FIREBASE_PROJECT_ID}
49
- )
50
-
51
- self.db = firestore.client()
52
- self.initialized = True
53
- logger.info("Firebase initialized successfully")
54
-
55
- except Exception as e:
56
- logger.error(f"Failed to initialize Firebase: {str(e)}")
57
- # For development, we can continue without Firebase
58
- logger.warning("Running without Firebase connection (dev mode)")
59
- self.initialized = False
60
-
61
- # ========================================================================
62
- # User Profile Operations
63
- # ========================================================================
64
-
65
- def get_user_profile(self, user_id: str) -> Optional[Dict[str, Any]]:
66
- """
67
- Retrieve user profile from Firestore.
68
-
69
- Args:
70
- user_id: User ID
71
-
72
- Returns:
73
- User profile dict or None if not found
74
- """
75
- if not self.initialized:
76
- logger.warning("Firebase not initialized, returning mock data")
77
- return self._get_mock_user_profile(user_id)
78
-
79
- try:
80
- doc_ref = self.db.collection("users").document(user_id)
81
- doc = doc_ref.get()
82
-
83
- if doc.exists:
84
- profile = doc.to_dict()
85
- profile["user_id"] = user_id
86
- logger.info(f"Retrieved profile for user {user_id}")
87
- return profile
88
- else:
89
- logger.warning(f"User profile not found: {user_id}")
90
- return None
91
-
92
- except Exception as e:
93
- logger.error(f"Error fetching user profile: {str(e)}")
94
- return None
95
-
96
- def create_user_profile(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
97
- """
98
- Create a new user profile in Firestore.
99
-
100
- Args:
101
- user_data: User profile data
102
-
103
- Returns:
104
- Created user profile with user_id
105
- """
106
- if not self.initialized:
107
- logger.warning("Firebase not initialized, returning mock data")
108
- return {**user_data, "user_id": "mock_user_123"}
109
-
110
- try:
111
- user_id = user_data.get("user_id")
112
- if not user_id:
113
- # Generate user_id from Firebase Auth UID or create new
114
- user_id = self.db.collection("users").document().id
115
-
116
- user_data["user_id"] = user_id
117
- user_data["created_at"] = datetime.utcnow()
118
- user_data["updated_at"] = datetime.utcnow()
119
-
120
- # Set default values
121
- user_data.setdefault("existing_emi", 0.0)
122
- user_data.setdefault("mock_credit_score", 650)
123
- user_data.setdefault("segment", "New to Credit")
124
-
125
- doc_ref = self.db.collection("users").document(user_id)
126
- doc_ref.set(user_data)
127
-
128
- logger.info(f"Created user profile: {user_id}")
129
- return user_data
130
-
131
- except Exception as e:
132
- logger.error(f"Error creating user profile: {str(e)}")
133
- raise
134
-
135
- def update_user_profile(
136
- self, user_id: str, update_data: Dict[str, Any]
137
- ) -> Dict[str, Any]:
138
- """
139
- Update an existing user profile.
140
-
141
- Args:
142
- user_id: User ID
143
- update_data: Fields to update
144
-
145
- Returns:
146
- Updated user profile
147
- """
148
- if not self.initialized:
149
- logger.warning("Firebase not initialized")
150
- return {"user_id": user_id, **update_data}
151
-
152
- try:
153
- update_data["updated_at"] = datetime.utcnow()
154
- doc_ref = self.db.collection("users").document(user_id)
155
- doc_ref.update(update_data)
156
-
157
- logger.info(f"Updated user profile: {user_id}")
158
- return self.get_user_profile(user_id)
159
-
160
- except Exception as e:
161
- logger.error(f"Error updating user profile: {str(e)}")
162
- raise
163
-
164
- # ========================================================================
165
- # Loan Application Operations
166
- # ========================================================================
167
-
168
- def create_loan_application(self, loan_data: Dict[str, Any]) -> Dict[str, Any]:
169
- """
170
- Create a new loan application in Firestore.
171
-
172
- Args:
173
- loan_data: Loan application data
174
-
175
- Returns:
176
- Created loan application with loan_id
177
- """
178
- if not self.initialized:
179
- logger.warning("Firebase not initialized, returning mock data")
180
- return {**loan_data, "loan_id": "mock_loan_123"}
181
-
182
- try:
183
- # Generate loan ID
184
- loan_ref = self.db.collection("loan_applications").document()
185
- loan_id = loan_ref.id
186
-
187
- loan_data["loan_id"] = loan_id
188
- loan_data["created_at"] = datetime.utcnow()
189
- loan_data["updated_at"] = datetime.utcnow()
190
-
191
- loan_ref.set(loan_data)
192
-
193
- logger.info(f"Created loan application: {loan_id}")
194
- return loan_data
195
-
196
- except Exception as e:
197
- logger.error(f"Error creating loan application: {str(e)}")
198
- raise
199
-
200
- def update_loan_application(
201
- self, loan_id: str, update_data: Dict[str, Any]
202
- ) -> Dict[str, Any]:
203
- """
204
- Update an existing loan application.
205
-
206
- Args:
207
- loan_id: Loan application ID
208
- update_data: Fields to update
209
-
210
- Returns:
211
- Updated loan application
212
- """
213
- if not self.initialized:
214
- logger.warning("Firebase not initialized")
215
- return {"loan_id": loan_id, **update_data}
216
-
217
- try:
218
- update_data["updated_at"] = datetime.utcnow()
219
- doc_ref = self.db.collection("loan_applications").document(loan_id)
220
- doc_ref.update(update_data)
221
-
222
- logger.info(f"Updated loan application: {loan_id}")
223
- return self.get_loan_application(loan_id)
224
-
225
- except Exception as e:
226
- logger.error(f"Error updating loan application: {str(e)}")
227
- raise
228
-
229
- def get_loan_application(self, loan_id: str) -> Optional[Dict[str, Any]]:
230
- """
231
- Retrieve a loan application by ID.
232
-
233
- Args:
234
- loan_id: Loan application ID
235
-
236
- Returns:
237
- Loan application dict or None if not found
238
- """
239
- if not self.initialized:
240
- logger.warning("Firebase not initialized, returning mock data")
241
- return self._get_mock_loan_application(loan_id)
242
-
243
- try:
244
- doc_ref = self.db.collection("loan_applications").document(loan_id)
245
- doc = doc_ref.get()
246
-
247
- if doc.exists:
248
- loan = doc.to_dict()
249
- loan["loan_id"] = loan_id
250
- logger.info(f"Retrieved loan application: {loan_id}")
251
- return loan
252
- else:
253
- logger.warning(f"Loan application not found: {loan_id}")
254
- return None
255
-
256
- except Exception as e:
257
- logger.error(f"Error fetching loan application: {str(e)}")
258
- return None
259
-
260
- def get_user_loans(self, user_id: str) -> List[Dict[str, Any]]:
261
- """
262
- Get all loan applications for a user.
263
-
264
- Args:
265
- user_id: User ID
266
-
267
- Returns:
268
- List of loan applications
269
- """
270
- if not self.initialized:
271
- logger.warning("Firebase not initialized, returning empty list")
272
- return []
273
-
274
- try:
275
- loans_ref = self.db.collection("loan_applications")
276
- query = loans_ref.where(
277
- filter=FieldFilter("user_id", "==", user_id)
278
- ).order_by("created_at", direction=firestore.Query.DESCENDING)
279
-
280
- loans = []
281
- for doc in query.stream():
282
- loan = doc.to_dict()
283
- loan["loan_id"] = doc.id
284
- loans.append(loan)
285
-
286
- logger.info(f"Retrieved {len(loans)} loans for user {user_id}")
287
- return loans
288
-
289
- except Exception as e:
290
- logger.error(f"Error fetching user loans: {str(e)}")
291
- return []
292
-
293
- def get_all_loans(self, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
294
- """
295
- Get all loan applications with pagination.
296
-
297
- Args:
298
- limit: Number of loans to retrieve
299
- offset: Number of loans to skip
300
-
301
- Returns:
302
- List of loan applications
303
- """
304
- if not self.initialized:
305
- logger.warning("Firebase not initialized, returning empty list")
306
- return []
307
-
308
- try:
309
- loans_ref = self.db.collection("loan_applications")
310
- query = loans_ref.order_by(
311
- "created_at", direction=firestore.Query.DESCENDING
312
- ).limit(limit)
313
-
314
- if offset > 0:
315
- # Get the document to start after
316
- skip_query = loans_ref.order_by(
317
- "created_at", direction=firestore.Query.DESCENDING
318
- ).limit(offset)
319
- skip_docs = list(skip_query.stream())
320
- if skip_docs:
321
- query = query.start_after(skip_docs[-1])
322
-
323
- loans = []
324
- for doc in query.stream():
325
- loan = doc.to_dict()
326
- loan["loan_id"] = doc.id
327
- loans.append(loan)
328
-
329
- logger.info(
330
- f"Retrieved {len(loans)} loans (limit={limit}, offset={offset})"
331
- )
332
- return loans
333
-
334
- except Exception as e:
335
- logger.error(f"Error fetching all loans: {str(e)}")
336
- return []
337
-
338
- # ========================================================================
339
- # Admin Operations
340
- # ========================================================================
341
-
342
- def get_admin_summary(self) -> Dict[str, Any]:
343
- """
344
- Get aggregated metrics for admin dashboard.
345
-
346
- Returns:
347
- Dictionary with admin metrics
348
- """
349
- if not self.initialized:
350
- logger.warning("Firebase not initialized, returning mock data")
351
- return self._get_mock_admin_summary()
352
-
353
- try:
354
- loans_ref = self.db.collection("loan_applications")
355
- loans = list(loans_ref.stream())
356
-
357
- total = len(loans)
358
- approved = 0
359
- rejected = 0
360
- adjust = 0
361
- total_amount = 0
362
- total_emi = 0
363
- total_credit = 0
364
- risk_dist = {"A": 0, "B": 0, "C": 0}
365
-
366
- today = datetime.utcnow().date()
367
- today_count = 0
368
-
369
- for doc in loans:
370
- loan = doc.to_dict()
371
-
372
- decision = loan.get("decision", "")
373
- if decision == "APPROVED":
374
- approved += 1
375
- elif decision == "REJECTED":
376
- rejected += 1
377
- elif decision == "ADJUST":
378
- adjust += 1
379
-
380
- total_amount += loan.get("approved_amount", 0)
381
- total_emi += loan.get("emi", 0)
382
- total_credit += loan.get("credit_score", 0)
383
-
384
- risk_band = loan.get("risk_band", "C")
385
- if risk_band in risk_dist:
386
- risk_dist[risk_band] += 1
387
-
388
- created_at = loan.get("created_at")
389
- if created_at and created_at.date() == today:
390
- today_count += 1
391
-
392
- summary = {
393
- "total_applications": total,
394
- "approved_count": approved,
395
- "rejected_count": rejected,
396
- "adjust_count": adjust,
397
- "avg_loan_amount": total_amount / total if total > 0 else 0,
398
- "avg_emi": total_emi / total if total > 0 else 0,
399
- "avg_credit_score": total_credit / total if total > 0 else 0,
400
- "today_applications": today_count,
401
- "risk_distribution": risk_dist,
402
- }
403
-
404
- logger.info("Generated admin summary")
405
- return summary
406
-
407
- except Exception as e:
408
- logger.error(f"Error generating admin summary: {str(e)}")
409
- return self._get_mock_admin_summary()
410
-
411
- # ========================================================================
412
- # Authentication Operations
413
- # ========================================================================
414
-
415
- def verify_token(self, id_token: str) -> Optional[Dict[str, Any]]:
416
- """
417
- Verify Firebase ID token.
418
-
419
- Args:
420
- id_token: Firebase ID token from client
421
-
422
- Returns:
423
- Decoded token with user info or None if invalid
424
- """
425
- if not self.initialized:
426
- logger.warning("Firebase not initialized, skipping token verification")
427
- return {"uid": "mock_user_123", "email": "test@example.com"}
428
-
429
- try:
430
- decoded_token = auth.verify_id_token(id_token)
431
- logger.info(f"Token verified for user: {decoded_token.get('uid')}")
432
- return decoded_token
433
-
434
- except Exception as e:
435
- logger.error(f"Token verification failed: {str(e)}")
436
- return None
437
-
438
- # ========================================================================
439
- # Mock Data Methods (for development)
440
- # ========================================================================
441
-
442
- def _get_mock_user_profile(self, user_id: str) -> Dict[str, Any]:
443
- """Return mock user profile for development."""
444
- return {
445
- "user_id": user_id,
446
- "full_name": "John Doe",
447
- "email": "john.doe@example.com",
448
- "monthly_income": 75000.0,
449
- "existing_emi": 5000.0,
450
- "mock_credit_score": 720,
451
- "segment": "Existing Customer",
452
- "created_at": datetime.utcnow(),
453
- }
454
-
455
- def _get_mock_loan_application(self, loan_id: str) -> Dict[str, Any]:
456
- """Return mock loan application for development."""
457
- return {
458
- "loan_id": loan_id,
459
- "user_id": "mock_user_123",
460
- "requested_amount": 500000.0,
461
- "requested_tenure_months": 36,
462
- "approved_amount": 500000.0,
463
- "tenure_months": 36,
464
- "emi": 16620.0,
465
- "interest_rate": 12.0,
466
- "credit_score": 720,
467
- "foir": 0.29,
468
- "decision": "APPROVED",
469
- "risk_band": "A",
470
- "explanation": "Approved based on excellent credit score and low FOIR",
471
- "created_at": datetime.utcnow(),
472
- "updated_at": datetime.utcnow(),
473
- }
474
-
475
- def _get_mock_admin_summary(self) -> Dict[str, Any]:
476
- """Return mock admin summary for development."""
477
- return {
478
- "total_applications": 25,
479
- "approved_count": 18,
480
- "rejected_count": 5,
481
- "adjust_count": 2,
482
- "avg_loan_amount": 425000.0,
483
- "avg_emi": 14250.0,
484
- "avg_credit_score": 695,
485
- "today_applications": 3,
486
- "risk_distribution": {"A": 12, "B": 10, "C": 3},
487
- }
488
-
489
-
490
- # Singleton instance
491
- firebase_service = FirebaseService()
 
1
+ """
2
+ Firebase service for Firestore database and Authentication.
3
+ Handles all Firebase operations including user profiles and loan applications.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ from datetime import datetime
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ import firebase_admin
12
+ from app.config import settings
13
+ from app.utils.logger import default_logger as logger
14
+ from firebase_admin import auth, credentials, firestore
15
+ from google.cloud.firestore_v1 import FieldFilter
16
+
17
+
18
+ class FirebaseService:
19
+ """Service for Firebase Firestore and Authentication operations."""
20
+
21
+ def __init__(self):
22
+ """Initialize Firebase Admin SDK and Firestore client."""
23
+ self.db: Optional[firestore.Client] = None
24
+ self.initialized = False
25
+ self._initialize_firebase()
26
+
27
+ def _initialize_firebase(self) -> None:
28
+ """Initialize Firebase Admin SDK with credentials."""
29
+ try:
30
+ # Check if Firebase app is already initialized
31
+ if not firebase_admin._apps:
32
+ # Try to load credentials from environment or use default
33
+ if settings.FIREBASE_CREDENTIALS:
34
+ # If FIREBASE_CREDENTIALS is a JSON string
35
+ if settings.FIREBASE_CREDENTIALS.startswith("{"):
36
+ cred_dict = json.loads(settings.FIREBASE_CREDENTIALS)
37
+ cred = credentials.Certificate(cred_dict)
38
+ else:
39
+ # If it's a file path
40
+ cred = credentials.Certificate(settings.FIREBASE_CREDENTIALS)
41
+
42
+ firebase_admin.initialize_app(
43
+ cred, {"projectId": settings.FIREBASE_PROJECT_ID}
44
+ )
45
+ else:
46
+ # Use Application Default Credentials (for local dev with gcloud)
47
+ firebase_admin.initialize_app(
48
+ options={"projectId": settings.FIREBASE_PROJECT_ID}
49
+ )
50
+
51
+ self.db = firestore.client()
52
+ self.initialized = True
53
+ logger.info("Firebase initialized successfully")
54
+
55
+ except Exception as e:
56
+ logger.error(f"Failed to initialize Firebase: {str(e)}")
57
+ # For development, we can continue without Firebase
58
+ logger.warning("Running without Firebase connection (dev mode)")
59
+ self.initialized = False
60
+
61
+ # ========================================================================
62
+ # User Profile Operations
63
+ # ========================================================================
64
+
65
+ def get_user_profile(self, user_id: str) -> Optional[Dict[str, Any]]:
66
+ """
67
+ Retrieve user profile from Firestore.
68
+
69
+ Args:
70
+ user_id: User ID
71
+
72
+ Returns:
73
+ User profile dict or None if not found
74
+ """
75
+ if not self.initialized:
76
+ logger.warning("Firebase not initialized, returning mock data")
77
+ return self._get_mock_user_profile(user_id)
78
+
79
+ try:
80
+ doc_ref = self.db.collection("users").document(user_id)
81
+ doc = doc_ref.get()
82
+
83
+ if doc.exists:
84
+ profile = doc.to_dict()
85
+ profile["user_id"] = user_id
86
+ logger.info(f"Retrieved profile for user {user_id}")
87
+ return profile
88
+ else:
89
+ logger.warning(f"User profile not found: {user_id}")
90
+ return None
91
+
92
+ except Exception as e:
93
+ logger.error(f"Error fetching user profile: {str(e)}")
94
+ return None
95
+
96
+ def create_user_profile(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
97
+ """
98
+ Create a new user profile in Firestore.
99
+
100
+ Args:
101
+ user_data: User profile data
102
+
103
+ Returns:
104
+ Created user profile with user_id
105
+ """
106
+ if not self.initialized:
107
+ logger.warning("Firebase not initialized, returning mock data")
108
+ return {**user_data, "user_id": "mock_user_123"}
109
+
110
+ try:
111
+ user_id = user_data.get("user_id")
112
+ if not user_id:
113
+ # Generate user_id from Firebase Auth UID or create new
114
+ user_id = self.db.collection("users").document().id
115
+
116
+ user_data["user_id"] = user_id
117
+ user_data["created_at"] = datetime.utcnow()
118
+ user_data["updated_at"] = datetime.utcnow()
119
+
120
+ # Set default values
121
+ user_data.setdefault("existing_emi", 0.0)
122
+ user_data.setdefault("mock_credit_score", 650)
123
+ user_data.setdefault("segment", "New to Credit")
124
+
125
+ doc_ref = self.db.collection("users").document(user_id)
126
+ doc_ref.set(user_data)
127
+
128
+ logger.info(f"Created user profile: {user_id}")
129
+ return user_data
130
+
131
+ except Exception as e:
132
+ logger.error(f"Error creating user profile: {str(e)}")
133
+ raise
134
+
135
+ def update_user_profile(
136
+ self, user_id: str, update_data: Dict[str, Any]
137
+ ) -> Dict[str, Any]:
138
+ """
139
+ Update an existing user profile.
140
+
141
+ Args:
142
+ user_id: User ID
143
+ update_data: Fields to update
144
+
145
+ Returns:
146
+ Updated user profile
147
+ """
148
+ if not self.initialized:
149
+ logger.warning("Firebase not initialized")
150
+ return {"user_id": user_id, **update_data}
151
+
152
+ try:
153
+ update_data["updated_at"] = datetime.utcnow()
154
+ doc_ref = self.db.collection("users").document(user_id)
155
+ doc_ref.update(update_data)
156
+
157
+ logger.info(f"Updated user profile: {user_id}")
158
+ return self.get_user_profile(user_id)
159
+
160
+ except Exception as e:
161
+ logger.error(f"Error updating user profile: {str(e)}")
162
+ raise
163
+
164
+ # ========================================================================
165
+ # Loan Application Operations
166
+ # ========================================================================
167
+
168
+ def create_loan_application(self, loan_data: Dict[str, Any]) -> str:
169
+ """
170
+ Create a new loan application in Firestore.
171
+
172
+ Args:
173
+ loan_data: Loan application data
174
+
175
+ Returns:
176
+ loan_id: The document ID string of the created loan application
177
+ """
178
+ if not self.initialized:
179
+ logger.warning("Firebase not initialized, returning mock loan ID")
180
+ return "mock_loan_123"
181
+
182
+ try:
183
+ # Generate loan ID
184
+ loan_ref = self.db.collection("loan_applications").document()
185
+ loan_id = loan_ref.id
186
+
187
+ loan_data["loan_id"] = loan_id
188
+ loan_data["created_at"] = datetime.utcnow()
189
+ loan_data["updated_at"] = datetime.utcnow()
190
+
191
+ loan_ref.set(loan_data)
192
+
193
+ logger.info(f"Created loan application: {loan_id}")
194
+ return loan_id # Return only the loan_id string
195
+
196
+ except Exception as e:
197
+ logger.error(f"Error creating loan application: {str(e)}")
198
+ raise
199
+
200
+ def update_loan_application(
201
+ self, loan_id: str, update_data: Dict[str, Any]
202
+ ) -> Dict[str, Any]:
203
+ """
204
+ Update an existing loan application.
205
+
206
+ Args:
207
+ loan_id: Loan application ID
208
+ update_data: Fields to update
209
+
210
+ Returns:
211
+ Updated loan application
212
+ """
213
+ if not self.initialized:
214
+ logger.warning("Firebase not initialized")
215
+ return {"loan_id": loan_id, **update_data}
216
+
217
+ try:
218
+ update_data["updated_at"] = datetime.utcnow()
219
+ doc_ref = self.db.collection("loan_applications").document(loan_id)
220
+ doc_ref.update(update_data)
221
+
222
+ logger.info(f"Updated loan application: {loan_id}")
223
+ return self.get_loan_application(loan_id)
224
+
225
+ except Exception as e:
226
+ logger.error(f"Error updating loan application: {str(e)}")
227
+ raise
228
+
229
+ def get_loan_application(self, loan_id: str) -> Optional[Dict[str, Any]]:
230
+ """
231
+ Retrieve a loan application by ID.
232
+
233
+ Args:
234
+ loan_id: Loan application ID
235
+
236
+ Returns:
237
+ Loan application dict or None if not found
238
+ """
239
+ if not self.initialized:
240
+ logger.warning("Firebase not initialized, returning mock data")
241
+ return self._get_mock_loan_application(loan_id)
242
+
243
+ try:
244
+ doc_ref = self.db.collection("loan_applications").document(loan_id)
245
+ doc = doc_ref.get()
246
+
247
+ if doc.exists:
248
+ loan = doc.to_dict()
249
+ loan["loan_id"] = loan_id
250
+ logger.info(f"Retrieved loan application: {loan_id}")
251
+ return loan
252
+ else:
253
+ logger.warning(f"Loan application not found: {loan_id}")
254
+ return None
255
+
256
+ except Exception as e:
257
+ logger.error(f"Error fetching loan application: {str(e)}")
258
+ return None
259
+
260
+ def get_user_loans(self, user_id: str) -> List[Dict[str, Any]]:
261
+ """
262
+ Get all loan applications for a user.
263
+
264
+ Args:
265
+ user_id: User ID
266
+
267
+ Returns:
268
+ List of loan applications
269
+ """
270
+ if not self.initialized:
271
+ logger.warning("Firebase not initialized, returning empty list")
272
+ return []
273
+
274
+ try:
275
+ loans_ref = self.db.collection("loan_applications")
276
+ query = loans_ref.where(
277
+ filter=FieldFilter("user_id", "==", user_id)
278
+ ).order_by("created_at", direction=firestore.Query.DESCENDING)
279
+
280
+ loans = []
281
+ for doc in query.stream():
282
+ loan = doc.to_dict()
283
+ loan["loan_id"] = doc.id
284
+ loans.append(loan)
285
+
286
+ logger.info(f"Retrieved {len(loans)} loans for user {user_id}")
287
+ return loans
288
+
289
+ except Exception as e:
290
+ logger.error(f"Error fetching user loans: {str(e)}")
291
+ return []
292
+
293
+ def get_all_loans(self, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
294
+ """
295
+ Get all loan applications with pagination.
296
+
297
+ Args:
298
+ limit: Number of loans to retrieve
299
+ offset: Number of loans to skip
300
+
301
+ Returns:
302
+ List of loan applications
303
+ """
304
+ if not self.initialized:
305
+ logger.warning("Firebase not initialized, returning empty list")
306
+ return []
307
+
308
+ try:
309
+ loans_ref = self.db.collection("loan_applications")
310
+ query = loans_ref.order_by(
311
+ "created_at", direction=firestore.Query.DESCENDING
312
+ ).limit(limit)
313
+
314
+ if offset > 0:
315
+ # Get the document to start after
316
+ skip_query = loans_ref.order_by(
317
+ "created_at", direction=firestore.Query.DESCENDING
318
+ ).limit(offset)
319
+ skip_docs = list(skip_query.stream())
320
+ if skip_docs:
321
+ query = query.start_after(skip_docs[-1])
322
+
323
+ loans = []
324
+ for doc in query.stream():
325
+ loan = doc.to_dict()
326
+ loan["loan_id"] = doc.id
327
+ loans.append(loan)
328
+
329
+ logger.info(
330
+ f"Retrieved {len(loans)} loans (limit={limit}, offset={offset})"
331
+ )
332
+ return loans
333
+
334
+ except Exception as e:
335
+ logger.error(f"Error fetching all loans: {str(e)}")
336
+ return []
337
+
338
+ # ========================================================================
339
+ # Admin Operations
340
+ # ========================================================================
341
+
342
+ def get_admin_summary(self) -> Dict[str, Any]:
343
+ """
344
+ Get aggregated metrics for admin dashboard.
345
+
346
+ Returns:
347
+ Dictionary with admin metrics
348
+ """
349
+ if not self.initialized:
350
+ logger.warning("Firebase not initialized, returning mock data")
351
+ return self._get_mock_admin_summary()
352
+
353
+ try:
354
+ loans_ref = self.db.collection("loan_applications")
355
+ loans = list(loans_ref.stream())
356
+
357
+ total = len(loans)
358
+ approved = 0
359
+ rejected = 0
360
+ adjust = 0
361
+ total_amount = 0
362
+ total_emi = 0
363
+ total_credit = 0
364
+ risk_dist = {"A": 0, "B": 0, "C": 0}
365
+
366
+ today = datetime.utcnow().date()
367
+ today_count = 0
368
+
369
+ for doc in loans:
370
+ loan = doc.to_dict()
371
+
372
+ decision = loan.get("decision", "")
373
+ if decision == "APPROVED":
374
+ approved += 1
375
+ elif decision == "REJECTED":
376
+ rejected += 1
377
+ elif decision == "ADJUST":
378
+ adjust += 1
379
+
380
+ total_amount += loan.get("approved_amount", 0)
381
+ total_emi += loan.get("emi", 0)
382
+ total_credit += loan.get("credit_score", 0)
383
+
384
+ risk_band = loan.get("risk_band", "C")
385
+ if risk_band in risk_dist:
386
+ risk_dist[risk_band] += 1
387
+
388
+ created_at = loan.get("created_at")
389
+ if created_at and created_at.date() == today:
390
+ today_count += 1
391
+
392
+ summary = {
393
+ "total_applications": total,
394
+ "approved_count": approved,
395
+ "rejected_count": rejected,
396
+ "adjust_count": adjust,
397
+ "avg_loan_amount": total_amount / total if total > 0 else 0,
398
+ "avg_emi": total_emi / total if total > 0 else 0,
399
+ "avg_credit_score": total_credit / total if total > 0 else 0,
400
+ "today_applications": today_count,
401
+ "risk_distribution": risk_dist,
402
+ }
403
+
404
+ logger.info("Generated admin summary")
405
+ return summary
406
+
407
+ except Exception as e:
408
+ logger.error(f"Error generating admin summary: {str(e)}")
409
+ return self._get_mock_admin_summary()
410
+
411
+ # ========================================================================
412
+ # Authentication Operations
413
+ # ========================================================================
414
+
415
+ def verify_token(self, id_token: str) -> Optional[Dict[str, Any]]:
416
+ """
417
+ Verify Firebase ID token.
418
+
419
+ Args:
420
+ id_token: Firebase ID token from client
421
+
422
+ Returns:
423
+ Decoded token with user info or None if invalid
424
+ """
425
+ if not self.initialized:
426
+ logger.warning("Firebase not initialized, skipping token verification")
427
+ return {"uid": "mock_user_123", "email": "test@example.com"}
428
+
429
+ try:
430
+ decoded_token = auth.verify_id_token(id_token)
431
+ logger.info(f"Token verified for user: {decoded_token.get('uid')}")
432
+ return decoded_token
433
+
434
+ except Exception as e:
435
+ logger.error(f"Token verification failed: {str(e)}")
436
+ return None
437
+
438
+ # ========================================================================
439
+ # Mock Data Methods (for development)
440
+ # ========================================================================
441
+
442
+ def _get_mock_user_profile(self, user_id: str) -> Dict[str, Any]:
443
+ """Return mock user profile for development."""
444
+ return {
445
+ "user_id": user_id,
446
+ "full_name": "John Doe",
447
+ "email": "john.doe@example.com",
448
+ "monthly_income": 75000.0,
449
+ "existing_emi": 5000.0,
450
+ "mock_credit_score": 720,
451
+ "segment": "Existing Customer",
452
+ "created_at": datetime.utcnow(),
453
+ }
454
+
455
+ def _get_mock_loan_application(self, loan_id: str) -> Dict[str, Any]:
456
+ """Return mock loan application for development."""
457
+ return {
458
+ "loan_id": loan_id,
459
+ "user_id": "mock_user_123",
460
+ "requested_amount": 500000.0,
461
+ "requested_tenure_months": 36,
462
+ "approved_amount": 500000.0,
463
+ "tenure_months": 36,
464
+ "emi": 16620.0,
465
+ "interest_rate": 12.0,
466
+ "credit_score": 720,
467
+ "foir": 0.29,
468
+ "decision": "APPROVED",
469
+ "risk_band": "A",
470
+ "explanation": "Approved based on excellent credit score and low FOIR",
471
+ "created_at": datetime.utcnow(),
472
+ "updated_at": datetime.utcnow(),
473
+ }
474
+
475
+ def _get_mock_admin_summary(self) -> Dict[str, Any]:
476
+ """Return mock admin summary for development."""
477
+ return {
478
+ "total_applications": 25,
479
+ "approved_count": 18,
480
+ "rejected_count": 5,
481
+ "adjust_count": 2,
482
+ "avg_loan_amount": 425000.0,
483
+ "avg_emi": 14250.0,
484
+ "avg_credit_score": 695,
485
+ "today_applications": 3,
486
+ "risk_distribution": {"A": 12, "B": 10, "C": 3},
487
+ }
488
+
489
+
490
+ # Singleton instance
491
+ firebase_service = FirebaseService()