ipulse-shared-core-ftredge 20.0.1__py3-none-any.whl → 23.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ipulse-shared-core-ftredge might be problematic. Click here for more details.

Files changed (48) hide show
  1. ipulse_shared_core_ftredge/cache/shared_cache.py +1 -2
  2. ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +60 -23
  3. ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +128 -157
  4. ipulse_shared_core_ftredge/exceptions/base_exceptions.py +35 -4
  5. ipulse_shared_core_ftredge/models/__init__.py +3 -7
  6. ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
  7. ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
  8. ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +274 -0
  9. ipulse_shared_core_ftredge/models/catalog/usertype.py +177 -0
  10. ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
  11. ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
  12. ipulse_shared_core_ftredge/models/user/user_subscription.py +348 -0
  13. ipulse_shared_core_ftredge/models/{user_auth.py → user/userauth.py} +19 -10
  14. ipulse_shared_core_ftredge/models/{user_profile.py → user/userprofile.py} +53 -21
  15. ipulse_shared_core_ftredge/models/user/userstatus.py +479 -0
  16. ipulse_shared_core_ftredge/monitoring/__init__.py +0 -2
  17. ipulse_shared_core_ftredge/monitoring/tracemon.py +6 -6
  18. ipulse_shared_core_ftredge/services/__init__.py +11 -13
  19. ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
  20. ipulse_shared_core_ftredge/services/base/base_firestore_service.py +77 -16
  21. ipulse_shared_core_ftredge/services/{cache_aware_firestore_service.py → base/cache_aware_firestore_service.py} +46 -32
  22. ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
  23. ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +277 -0
  24. ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +376 -0
  25. ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
  26. ipulse_shared_core_ftredge/services/user/__init__.py +5 -25
  27. ipulse_shared_core_ftredge/services/user/user_core_service.py +536 -510
  28. ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +796 -0
  29. ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
  30. ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +488 -0
  31. ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
  32. ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
  33. ipulse_shared_core_ftredge/services/user/userstatus_operations.py +476 -0
  34. ipulse_shared_core_ftredge/services/{charging_service.py → user_charging_service.py} +9 -9
  35. {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/METADATA +3 -4
  36. ipulse_shared_core_ftredge-23.1.1.dist-info/RECORD +50 -0
  37. ipulse_shared_core_ftredge/models/subscription.py +0 -190
  38. ipulse_shared_core_ftredge/models/user_status.py +0 -495
  39. ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -526
  40. ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
  41. ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
  42. ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
  43. ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
  44. ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
  45. ipulse_shared_core_ftredge-20.0.1.dist-info/RECORD +0 -42
  46. {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/WHEEL +0 -0
  47. {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/licenses/LICENCE +0 -0
  48. {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,796 @@
1
+ """
2
+ User Multistep Operations - Complete user lifecycle operations
3
+
4
+ Handles complete user creation and deletion operations that span across
5
+ Firebase Auth, UserProfile, and UserStatus in coordinated transactions.
6
+ """
7
+ import asyncio
8
+ import logging
9
+ from typing import Dict, Any, Optional, List, Tuple, cast
10
+ from ipulse_shared_base_ftredge.enums import IAMUserType
11
+ from ...models import UserProfile, UserStatus, UserAuth, UserType
12
+ from .userauth_operations import UserauthOperations
13
+ from .userprofile_operations import UserprofileOperations
14
+ from .userstatus_operations import UserstatusOperations
15
+ from .user_subscription_operations import UsersubscriptionOperations
16
+ from .user_permissions_operations import UserpermissionsOperations
17
+ from ..catalog.catalog_usertype_service import CatalogUserTypeService
18
+ from ..catalog.catalog_subscriptionplan_service import CatalogSubscriptionPlanService
19
+
20
+ from ...exceptions import (
21
+ UserCreationError
22
+ )
23
+
24
+
25
+ class UsermultistepOperations:
26
+ """
27
+ Handles complete user lifecycle operations including coordinated creation and deletion
28
+ of Firebase Auth users, UserProfile, and UserStatus documents.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ userprofile_ops: UserprofileOperations,
34
+ userstatus_ops: UserstatusOperations,
35
+ userauth_ops: UserauthOperations,
36
+ usersubscription_ops: UsersubscriptionOperations,
37
+ useriam_ops: UserpermissionsOperations,
38
+ catalog_usertype_service: CatalogUserTypeService,
39
+ catalog_subscriptionplan_service: CatalogSubscriptionPlanService,
40
+ logger: Optional[logging.Logger] = None
41
+ ):
42
+ self.userprofile_ops = userprofile_ops
43
+ self.userstatus_ops = userstatus_ops
44
+ self.userauth_ops = userauth_ops
45
+ self.usersubscription_ops = usersubscription_ops
46
+ self.useriam_ops = useriam_ops
47
+ self.catalog_usertype_service = catalog_usertype_service
48
+ self.catalog_subscriptionplan_service = catalog_subscriptionplan_service
49
+ self.logger = logger or logging.getLogger(__name__)
50
+
51
+
52
+
53
+ async def _rollback_user_creation(
54
+ self,
55
+ user_uid: Optional[str],
56
+ profile_created: bool,
57
+ status_created: bool,
58
+ error_context: str
59
+ ) -> None:
60
+ """Rollback user creation on failure."""
61
+ if not user_uid:
62
+ self.logger.error("Rollback cannot proceed: user_uid is None. Context: %s", error_context)
63
+ return
64
+
65
+ self.logger.warning("Rolling back user creation for UID: %s. Context: %s", user_uid, error_context)
66
+
67
+ # Attempt to clean up Firestore documents if they were created
68
+ if profile_created:
69
+ try:
70
+ await self.userprofile_ops.delete_userprofile(user_uid, "rollback", archive=False)
71
+ self.logger.info("Successfully deleted orphaned UserProfile for: %s", user_uid)
72
+ except Exception as del_prof_e:
73
+ self.logger.error("Failed to delete orphaned UserProfile for %s: %s", user_uid, del_prof_e)
74
+ if status_created:
75
+ try:
76
+ await self.userstatus_ops.delete_userstatus(user_uid, "rollback", archive=False)
77
+ self.logger.info("Successfully deleted orphaned UserStatus for: %s", user_uid)
78
+ except Exception as del_stat_e:
79
+ self.logger.error("Failed to delete orphaned UserStatus for %s: %s", user_uid, del_stat_e)
80
+
81
+ # Attempt to delete the orphaned Firebase Auth user
82
+ try:
83
+ await self.userauth_ops.delete_userauth(user_uid)
84
+ self.logger.info("Successfully deleted orphaned Firebase Auth user: %s", user_uid)
85
+ except Exception as delete_e:
86
+ self.logger.error("Failed to delete orphaned Firebase Auth user %s: %s", user_uid, delete_e, exc_info=True)
87
+
88
+ def _validate_usertype_consistency(
89
+ self,
90
+ userprofile: UserProfile,
91
+ custom_claims: Optional[Dict[str, Any]] = None
92
+ ) -> None:
93
+ """
94
+ Validate usertype consistency between UserProfile and custom claims.
95
+
96
+ Args:
97
+ userprofile: UserProfile model to validate
98
+ custom_claims: Custom claims to validate against
99
+
100
+ Raises:
101
+ UserCreationError: If usertypes are inconsistent
102
+ """
103
+ if not custom_claims:
104
+ return # No claims to validate against
105
+
106
+ userauth_primary_usertype = custom_claims.get("primary_usertype")
107
+ userauth_secondary_usertypes = custom_claims.get("secondary_usertypes", [])
108
+
109
+ # Convert to strings for comparison
110
+ userprofile_primary_str = str(userprofile.primary_usertype)
111
+ userprofile_secondary_strs = [str(ut) for ut in userprofile.secondary_usertypes]
112
+
113
+ # Validate primary usertype consistency
114
+ if userauth_primary_usertype and userauth_primary_usertype != userprofile_primary_str:
115
+ raise UserCreationError(
116
+ f"Primary usertype mismatch between UserProfile ({userprofile_primary_str}) "
117
+ f"and custom claims ({userauth_primary_usertype})"
118
+ )
119
+
120
+ # Validate secondary usertypes consistency
121
+ if userauth_secondary_usertypes and set(userauth_secondary_usertypes) != set(userprofile_secondary_strs):
122
+ raise UserCreationError(
123
+ f"Secondary usertypes mismatch between UserProfile ({userprofile_secondary_strs}) "
124
+ f"and custom claims ({userauth_secondary_usertypes})"
125
+ )
126
+
127
+ # Complete User Creation Methods - New Strategic API
128
+
129
+ async def create_user_from_models(
130
+ self,
131
+ userprofile: UserProfile,
132
+ userstatus: UserStatus,
133
+ userauth: Optional[UserAuth] = None,
134
+ validate_userauth_consistency: bool = False,
135
+ validate_userauth_exists: bool = False
136
+ ) -> Tuple[str, UserProfile, UserStatus]:
137
+ """
138
+ Create a complete user from ready UserAuth, UserProfile, and UserStatus models.
139
+
140
+ This method efficiently commits pre-configured models to database.
141
+
142
+ For new user creation (when userauth is provided):
143
+ - Creates Firebase Auth user first to get the actual UID
144
+ - Creates new UserProfile and UserStatus models with the Firebase UID
145
+ - Original models serve as templates
146
+
147
+ For existing user (when userauth is None):
148
+ - Models should already have all subscription and permission configuration applied
149
+ - Uses the user_uid from the models to work with existing Firebase Auth user
150
+
151
+ Args:
152
+ userprofile: Complete UserProfile model (template for new user, or ready for existing user)
153
+ userstatus: Complete UserStatus model (template for new user, or ready for existing user)
154
+ userauth: Optional UserAuth model. If provided, creates new Firebase Auth user
155
+ validate_userauth_consistency: If True, validates userauth is consistent with userprofile
156
+ validate_userauth_exists: If True, validates userauth exists in Firebase Auth
157
+
158
+ Returns:
159
+ Tuple of (user_uid, userprofile, userstatus)
160
+ """
161
+ profile_created = False
162
+ status_created = False
163
+ firebase_user_uid = None
164
+
165
+ # Validate that UserProfile and UserStatus have matching user_uid
166
+ if userprofile.user_uid != userstatus.user_uid:
167
+ raise UserCreationError(f"UserProfile and UserStatus user_uid mismatch: {userprofile.user_uid} != {userstatus.user_uid}")
168
+
169
+ try:
170
+ # Step 1: Handle Firebase Auth user creation or validation
171
+ if userauth:
172
+ # Creating new user - Firebase will generate UID
173
+
174
+ # Validate usertype consistency if requested
175
+ if validate_userauth_consistency:
176
+ self._validate_usertype_consistency(userprofile, userauth.custom_claims)
177
+
178
+ # Create Firebase Auth user with all configuration
179
+ self.logger.info("Creating Firebase Auth user with custom claims for email: %s", userauth.email)
180
+ firebase_user_uid = await self.userauth_ops.create_userauth(userauth)
181
+
182
+ # Create new models with the Firebase UID, using original models as templates
183
+ userprofile_data = userprofile.model_dump()
184
+ userprofile_data['user_uid'] = firebase_user_uid
185
+ # Remove id so it gets auto-generated from user_uid
186
+ userprofile_data.pop('id', None)
187
+ final_userprofile = UserProfile(**userprofile_data)
188
+
189
+ userstatus_data = userstatus.model_dump()
190
+ userstatus_data['user_uid'] = firebase_user_uid
191
+ # Remove id so it gets auto-generated from user_uid
192
+ userstatus_data.pop('id', None)
193
+ final_userstatus = UserStatus(**userstatus_data)
194
+
195
+ user_uid = firebase_user_uid
196
+
197
+ else:
198
+ # Working with existing user - use models as-is
199
+ user_uid = userprofile.user_uid
200
+ final_userprofile = userprofile
201
+ final_userstatus = userstatus
202
+
203
+ # Validate userauth exists if requested (only if validate_userauth_exists is True)
204
+ if validate_userauth_exists:
205
+ if not await self.userauth_ops.userauth_exists(user_uid):
206
+ raise UserCreationError(f"Firebase Auth user {user_uid} does not exist")
207
+
208
+ # Validate userauth consistency if requested
209
+ if validate_userauth_consistency:
210
+ existing_userauth = await self.userauth_ops.get_userauth(user_uid, get_model=True)
211
+ if existing_userauth and existing_userauth.custom_claims:
212
+ self._validate_usertype_consistency(userprofile, existing_userauth.custom_claims)
213
+
214
+ # Step 2: Create UserProfile and UserStatus in database (2 operations only)
215
+ self.logger.info("Creating UserProfile for user: %s", user_uid)
216
+ await self.userprofile_ops.create_userprofile(final_userprofile)
217
+ profile_created = True
218
+
219
+ self.logger.info("Creating UserStatus for user: %s (with %d IAM permissions)",
220
+ user_uid, len(final_userstatus.iam_permissions))
221
+ await self.userstatus_ops.create_userstatus(final_userstatus)
222
+ status_created = True
223
+
224
+ # Step 3: Fetch final state to return
225
+ final_profile = await self.userprofile_ops.get_userprofile(user_uid)
226
+ final_status = await self.userstatus_ops.get_userstatus(user_uid)
227
+
228
+ if not final_profile or not final_status:
229
+ raise UserCreationError("Failed to retrieve user documents after creation.")
230
+
231
+ self.logger.info("Successfully created user from ready models: %s", user_uid)
232
+ return user_uid, final_profile, final_status
233
+
234
+ except Exception as e:
235
+ error_context = f"User creation from models failed: {e}"
236
+ # Use firebase_user_uid if available, otherwise fall back to the original user_uid
237
+ cleanup_uid = firebase_user_uid or userprofile.user_uid
238
+ await self._rollback_user_creation(cleanup_uid, profile_created, status_created, error_context)
239
+ raise UserCreationError(f"Failed to create user from models: {str(e)}") from e
240
+
241
+ async def create_user_from_manual_usertype(
242
+ self,
243
+ userprofile: UserProfile,
244
+ usertype: UserType,
245
+ userauth: Optional[UserAuth] = None,
246
+ extra_insight_credits_override: Optional[int] = None,
247
+ voting_credits_override: Optional[int] = None,
248
+ subscriptionplan_id_override: Optional[str] = None,
249
+ creator_uid: Optional[str] = None,
250
+ apply_usertype_associated_subscriptionplan: bool = True,
251
+ validate_userauth_consistency: bool = False,
252
+ validate_userauth_exists: bool = False
253
+ ) -> Tuple[str, UserProfile, UserStatus]:
254
+ """
255
+ Create a complete user with manual UserType configuration.
256
+
257
+ This method builds UserStatus from usertype defaults and applies subscription/permissions
258
+ in memory before committing to database. Organizations are always taken from usertype.
259
+
260
+ Args:
261
+ userprofile: Complete UserProfile model (mandatory)
262
+ usertype: Manual UserType configuration (mandatory)
263
+ userauth: Optional UserAuth model. If not provided, assumes user exists
264
+ extra_insight_credits_override: Override extra credits from usertype
265
+ voting_credits_override: Override voting credits from usertype
266
+ subscriptionplan_id_override: Override subscription plan from usertype default
267
+ creator_uid: Who is creating this user
268
+ apply_usertype_associated_subscriptionplan: Whether to apply the usertype's default subscription plan
269
+ validate_userauth_consistency: If True, validates userauth is consistent with userprofile
270
+ validate_userauth_exists: If True, validates userauth exists in Firebase Auth
271
+
272
+ Returns:
273
+ Tuple of (user_uid, userprofile, userstatus)
274
+ """
275
+ try:
276
+ # Always use organizations from usertype
277
+ final_organizations = set(usertype.default_organizations)
278
+ final_extra_credits = extra_insight_credits_override if extra_insight_credits_override is not None else usertype.default_extra_insight_credits
279
+ final_voting_credits = voting_credits_override if voting_credits_override is not None else usertype.default_voting_credits
280
+
281
+ # Build initial UserStatus from usertype defaults
282
+ userstatus = UserStatus(
283
+ user_uid=userprofile.user_uid,
284
+ organizations_uids=final_organizations,
285
+ iam_permissions=usertype.granted_iam_permissions or [],
286
+ extra_insight_credits=final_extra_credits,
287
+ voting_credits=final_voting_credits,
288
+ metadata={},
289
+ created_by=creator_uid or f"system_manual_usertype_{userprofile.user_uid}",
290
+ updated_by=creator_uid or f"system_manual_usertype_{userprofile.user_uid}"
291
+ )
292
+
293
+ # Apply subscription to UserStatus in memory if plan specified
294
+ plan_to_apply = subscriptionplan_id_override or usertype.default_subscriptionplan_if_unpaid
295
+ if plan_to_apply and apply_usertype_associated_subscriptionplan:
296
+ try:
297
+ self.logger.info("Applying subscription plan %s to UserStatus", plan_to_apply)
298
+
299
+ # Fetch subscription plan from catalog
300
+ subscription_plan = await self.catalog_subscriptionplan_service.get_subscriptionplan(plan_to_apply)
301
+ if not subscription_plan:
302
+ self.logger.warning("Subscription plan %s not found in catalog, skipping application", plan_to_apply)
303
+ else:
304
+ # Create UserSubscription using the helper method from subscription operations
305
+ # Pass usertype's default auto-renewal end if specified, otherwise use plan default
306
+ usertype_auto_renewal_end = getattr(usertype, 'default_subscriptionplan_auto_renewal_end', None)
307
+ user_subscription = self.usersubscription_ops.create_subscription_from_subscriptionplan(
308
+ plan=subscription_plan,
309
+ source=f"usertype_default_{creator_uid or 'system'}",
310
+ granted_at=None, # Will use current time
311
+ auto_renewal_end=usertype_auto_renewal_end # Usertype override or None for plan default
312
+ )
313
+
314
+ # Apply subscription to UserStatus (this updates credits and permissions)
315
+ userstatus.apply_subscription(
316
+ subscription=user_subscription,
317
+ add_associated_permissions=True,
318
+ remove_previous_subscription_permissions=False, # First subscription, no existing ones
319
+ granted_by=creator_uid or f"system_manual_usertype_{userprofile.user_uid}"
320
+ )
321
+
322
+ self.logger.info("Successfully applied subscription plan %s to UserStatus", plan_to_apply)
323
+
324
+ except Exception as e:
325
+ self.logger.error("Failed to apply subscription plan %s to UserStatus: %s", plan_to_apply, e)
326
+ # Don't fail user creation if subscription application fails
327
+ elif plan_to_apply:
328
+ self.logger.info("Subscription plan %s will be applied after user creation (apply_usertype_associated_subscriptionplan=False)", plan_to_apply)
329
+
330
+ # Create user from ready models
331
+ return await self.create_user_from_models(
332
+ userprofile=userprofile,
333
+ userstatus=userstatus,
334
+ userauth=userauth,
335
+ validate_userauth_consistency=validate_userauth_consistency,
336
+ validate_userauth_exists=validate_userauth_exists
337
+ )
338
+
339
+ except Exception as e:
340
+ self.logger.error("Failed to create user from manual usertype: %s", e)
341
+ raise UserCreationError(f"Failed to create user from manual usertype: {str(e)}") from e
342
+
343
+ async def create_user_from_catalog_usertype(
344
+ self,
345
+ usertype_id: str,
346
+ userprofile: UserProfile,
347
+ userauth: Optional[UserAuth] = None,
348
+ extra_insight_credits_override: Optional[int] = None,
349
+ voting_credits_override: Optional[int] = None,
350
+ subscriptionplan_id_override: Optional[str] = None,
351
+ creator_uid: Optional[str] = None,
352
+ apply_usertype_associated_subscriptionplan: bool = True,
353
+ validate_userauth_consistency: bool = False,
354
+ validate_userauth_exists: bool = False
355
+ ) -> Tuple[str, UserProfile, UserStatus]:
356
+ """
357
+ Create a complete user based on UserType catalog configuration.
358
+
359
+ This method fetches UserType from catalog and creates a user with
360
+ appropriate defaults, allowing selective overrides. Organizations are always taken from usertype.
361
+
362
+ Args:
363
+ usertype_id: ID of the UserType configuration to fetch from catalog (mandatory)
364
+ userprofile: Complete UserProfile model (mandatory)
365
+ userauth: Optional UserAuth model. If not provided, assumes user exists
366
+ extra_insight_credits_override: Override extra credits from usertype
367
+ voting_credits_override: Override voting credits from usertype
368
+ subscriptionplan_id_override: Override subscription plan from usertype default
369
+ creator_uid: Who is creating this user
370
+ apply_usertype_associated_subscriptionplan: Whether to apply the usertype's default subscription plan
371
+ validate_userauth_consistency: If True, validates userauth is consistent with userprofile
372
+ validate_userauth_exists: If True, validates userauth exists in Firebase Auth
373
+
374
+ Returns:
375
+ Tuple of (user_uid, userprofile, userstatus)
376
+ """
377
+ try:
378
+ # Step 1: Fetch UserType configuration from catalog
379
+ self.logger.info("Fetching usertype configuration for: %s", usertype_id)
380
+ usertype_config = await self.catalog_usertype_service.get_usertype(usertype_id)
381
+ if not usertype_config:
382
+ raise UserCreationError(f"UserType {usertype_id} not found in catalog")
383
+
384
+ # Step 2: Create user using manual usertype method
385
+ return await self.create_user_from_manual_usertype(
386
+ userprofile=userprofile,
387
+ usertype=usertype_config,
388
+ userauth=userauth,
389
+ extra_insight_credits_override=extra_insight_credits_override,
390
+ voting_credits_override=voting_credits_override,
391
+ subscriptionplan_id_override=subscriptionplan_id_override,
392
+ creator_uid=creator_uid,
393
+ apply_usertype_associated_subscriptionplan=apply_usertype_associated_subscriptionplan,
394
+ validate_userauth_consistency=validate_userauth_consistency,
395
+ validate_userauth_exists=validate_userauth_exists
396
+ )
397
+
398
+ except Exception as e:
399
+ self.logger.error("Failed to create user from catalog usertype %s: %s", usertype_id, e)
400
+ raise UserCreationError(f"Failed to create user from catalog usertype {usertype_id}: {str(e)}") from e
401
+
402
+ # Complete User Deletion
403
+
404
+ async def delete_user(
405
+ self,
406
+ user_uid: str,
407
+ delete_auth_user: bool = True,
408
+ delete_profile: bool = True,
409
+ delete_status: bool = True,
410
+ updater_uid: str = "system_deletion",
411
+ archive: bool = True
412
+ ) -> Dict[str, Any]:
413
+ """
414
+ Delete a user holistically, including their auth, profile, and status.
415
+
416
+ Args:
417
+ user_uid: The UID of the user to delete.
418
+ delete_auth_user: Whether to delete the Firebase Auth user.
419
+ delete_profile: Whether to delete the UserProfile document.
420
+ delete_status: Whether to delete the UserStatus document.
421
+ updater_uid: The identifier of the entity performing the deletion.
422
+ archive: Whether to archive documents before deletion. Defaults to True.
423
+
424
+ Returns:
425
+ A dictionary with the results of the deletion operations.
426
+ """
427
+ results = {
428
+ "auth_deleted_successfully": not delete_auth_user,
429
+ "profile_deleted_successfully": not delete_profile,
430
+ "status_deleted_successfully": not delete_status,
431
+ "errors": []
432
+ }
433
+
434
+ # Delete UserProfile
435
+ if delete_profile:
436
+ try:
437
+ results["profile_deleted_successfully"] = await self.userprofile_ops.delete_userprofile(
438
+ user_uid, updater_uid, archive=archive
439
+ )
440
+ except Exception as e:
441
+ error_msg = f"Failed to delete user profile for {user_uid}: {e}"
442
+ self.logger.error(error_msg, exc_info=True)
443
+ results["errors"].append(error_msg)
444
+
445
+ # Delete UserStatus
446
+ if delete_status:
447
+ try:
448
+ results["status_deleted_successfully"] = await self.userstatus_ops.delete_userstatus(
449
+ user_uid, updater_uid, archive=archive
450
+ )
451
+ except Exception as e:
452
+ error_msg = f"Failed to delete user status for {user_uid}: {e}"
453
+ self.logger.error(error_msg, exc_info=True)
454
+ results["errors"].append(error_msg)
455
+
456
+ # Delete Firebase Auth user
457
+ if delete_auth_user:
458
+ try:
459
+ # Assuming delete_userauth also accepts an archive flag for consistency
460
+ results["auth_deleted_successfully"] = await self.userauth_ops.delete_userauth(user_uid, archive=archive)
461
+ except Exception as e:
462
+ error_msg = f"Failed to delete Firebase Auth user {user_uid}: {e}"
463
+ self.logger.error(error_msg, exc_info=True)
464
+ results["errors"].append(error_msg)
465
+
466
+ return results
467
+
468
+ async def batch_delete_users(
469
+ self,
470
+ user_uids: List[str],
471
+ delete_auth_user: bool,
472
+ delete_profile: bool = True,
473
+ delete_status: bool = True,
474
+ updater_uid: str = "system_batch_deletion",
475
+ archive: bool = True
476
+ ) -> Dict[str, Dict[str, Any]]:
477
+ """
478
+ Batch delete multiple users holistically.
479
+
480
+ Args:
481
+ user_uids: A list of user UIDs to delete.
482
+ delete_auth_user: Whether to delete the Firebase Auth users.
483
+ delete_profile: Whether to delete the UserProfile documents.
484
+ delete_status: Whether to delete the UserStatus documents.
485
+ updater_uid: The identifier of the entity performing the deletion.
486
+ archive: Overrides the default archival behavior for all users in the batch.
487
+
488
+ Returns:
489
+ A dictionary where keys are user UIDs and values are deletion result dictionaries.
490
+ """
491
+ batch_results = {}
492
+ for user_uid in user_uids:
493
+ batch_results[user_uid] = await self.delete_user(
494
+ user_uid=user_uid,
495
+ delete_auth_user=delete_auth_user,
496
+ delete_profile=delete_profile,
497
+ delete_status=delete_status,
498
+ updater_uid=updater_uid,
499
+ archive=archive
500
+ )
501
+ return batch_results
502
+
503
+ # Document-level batch operations
504
+
505
+ async def batch_delete_user_core_docs(
506
+ self,
507
+ user_uids: List[str],
508
+ updater_uid: str = "system_batch_deletion"
509
+ ) -> Dict[str, Tuple[bool, bool, Optional[str]]]:
510
+ """Batch delete multiple users' documents (profile and status only)"""
511
+ batch_results: Dict[str, Tuple[bool, bool, Optional[str]]] = {}
512
+
513
+ # Process sequentially to avoid overwhelming the database
514
+ for user_uid in user_uids:
515
+ self.logger.info("Batch deletion: Processing user_uid: %s", user_uid)
516
+ item_deleted_by = f"{updater_uid}_batch_item_{user_uid}"
517
+
518
+ try:
519
+ # Use delete_user but only for documents, not auth
520
+ result = await self.delete_user(
521
+ user_uid=user_uid,
522
+ delete_auth_user=False, # Only delete documents
523
+ delete_profile=True,
524
+ delete_status=True,
525
+ updater_uid=item_deleted_by
526
+ )
527
+
528
+ batch_results[user_uid] = (
529
+ result["profile_deleted_successfully"],
530
+ result["status_deleted_successfully"],
531
+ result["errors"][0] if result["errors"] else None
532
+ )
533
+ except Exception as e:
534
+ self.logger.error(f"Batch deletion failed for user {user_uid}: {e}", exc_info=True)
535
+ batch_results[user_uid] = (False, False, str(e))
536
+
537
+ return batch_results
538
+
539
+ # Utility Methods
540
+
541
+ async def user_exists_fully(self, user_uid: str) -> Dict[str, bool]:
542
+ """Check if complete user exists (Auth, Profile, Status)"""
543
+ return {
544
+ "auth_exists": await self.userauth_ops.userauth_exists(user_uid),
545
+ "profile_exists": (await self.userprofile_ops.get_userprofile(user_uid)) is not None,
546
+ "status_exists": (await self.userstatus_ops.get_userstatus(user_uid)) is not None
547
+ }
548
+
549
+ async def validate_user_fully_enabled(
550
+ self,
551
+ user_uid: str,
552
+ email_verified_must: bool = True,
553
+ approved_must: bool = True,
554
+ active_subscription_must: bool = True,
555
+ valid_permissions_must: bool = True
556
+ ) -> Dict[str, Any]:
557
+ """
558
+ Validate complete user integrity and operational readiness
559
+
560
+ This method performs comprehensive validation to ensure a user is:
561
+ - Complete (auth, profile, status exist)
562
+ - Consistent (matching UIDs and usertypes across components)
563
+ - Enabled (auth enabled, approved status)
564
+ - Operational (active subscription, valid permissions)
565
+
566
+ Args:
567
+ user_uid: The UID of the user to validate
568
+ email_verified_must: If True, email must be verified for full enablement (default: True)
569
+ approved_must: If True, approval status must be APPROVED for full enablement (default: True)
570
+ active_subscription_must: If True, active subscription required for full enablement (default: True)
571
+ valid_permissions_must: If True, valid permissions required for full enablement (default: True)
572
+
573
+ Returns:
574
+ Dict with validation results including status, errors, and detailed checks
575
+ """
576
+ validation_results = {
577
+ "user_uid": user_uid,
578
+ "exists": {"auth_exists": False, "profile_exists": False, "status_exists": False},
579
+ "is_complete": False,
580
+ "missing_components": [],
581
+ "validation_errors": [],
582
+ "is_fully_enabled": False,
583
+ "detailed_checks": {
584
+ "auth_enabled": False,
585
+ "email_verified": False,
586
+ "approval_status_approved": False,
587
+ "uid_consistency": False,
588
+ "usertype_consistency": False,
589
+ "has_active_subscription": False,
590
+ "has_valid_permissions": False
591
+ }
592
+ }
593
+
594
+ try:
595
+ # Get all user components in parallel for efficiency
596
+ userauth_result, userprofile_result, userstatus_result = await asyncio.gather(
597
+ self.userauth_ops.get_userauth(user_uid, get_model=True),
598
+ self.userprofile_ops.get_userprofile(user_uid),
599
+ self.userstatus_ops.get_userstatus(user_uid),
600
+ return_exceptions=True
601
+ )
602
+
603
+ # Handle exceptions and determine existence
604
+ validation_results["exists"]["auth_exists"] = not isinstance(userauth_result, Exception) and userauth_result is not None
605
+ validation_results["exists"]["profile_exists"] = not isinstance(userprofile_result, Exception) and userprofile_result is not None
606
+ validation_results["exists"]["status_exists"] = not isinstance(userstatus_result, Exception) and userstatus_result is not None
607
+
608
+ validation_results["is_complete"] = all(validation_results["exists"].values())
609
+ validation_results["missing_components"] = [k for k, v in validation_results["exists"].items() if not v]
610
+
611
+ # If user is not complete, skip detailed validations
612
+ if not validation_results["is_complete"]:
613
+ validation_results["validation_errors"].append("User is incomplete - missing components")
614
+ return validation_results
615
+
616
+ # If we have exceptions instead of models, handle them
617
+ if isinstance(userauth_result, Exception):
618
+ validation_results["validation_errors"].append(f"Auth retrieval error: {str(userauth_result)}")
619
+ return validation_results
620
+ if isinstance(userprofile_result, Exception):
621
+ validation_results["validation_errors"].append(f"Profile retrieval error: {str(userprofile_result)}")
622
+ return validation_results
623
+ if isinstance(userstatus_result, Exception):
624
+ validation_results["validation_errors"].append(f"Status retrieval error: {str(userstatus_result)}")
625
+ return validation_results
626
+
627
+ # Additional null checks - should not happen if exists checks passed, but for safety
628
+ if not userauth_result or not userprofile_result or not userstatus_result:
629
+ validation_results["validation_errors"].append("Retrieved user components are null despite existence checks passing")
630
+ return validation_results
631
+
632
+ # Type narrow the results to the actual model types after validation
633
+ userauth_record = cast(UserAuth, userauth_result) # Now known to be UserAuth
634
+ userprofile = cast(UserProfile, userprofile_result) # Now known to be UserProfile
635
+ userstatus = cast(UserStatus, userstatus_result) # Now known to be UserStatus
636
+
637
+ # Now perform detailed validations with valid models
638
+
639
+ # 1. Auth enabled validation (uses the UserAuth model disabled field)
640
+ validation_results["detailed_checks"]["auth_enabled"] = not userauth_record.disabled
641
+ if userauth_record.disabled:
642
+ validation_results["validation_errors"].append("Firebase Auth user is disabled")
643
+
644
+ # 2. Email verification validation
645
+ validation_results["detailed_checks"]["email_verified"] = userauth_record.email_verified
646
+ if email_verified_must and not userauth_record.email_verified:
647
+ validation_results["validation_errors"].append("User email is not verified")
648
+
649
+ # 3. UID consistency validation
650
+ auth_uid = getattr(userauth_record, 'uid', None) or getattr(userauth_record, 'firebase_uid', None)
651
+ uids_consistent = (
652
+ auth_uid == user_uid and
653
+ userprofile.user_uid == user_uid and
654
+ userstatus.user_uid == user_uid
655
+ )
656
+ validation_results["detailed_checks"]["uid_consistency"] = uids_consistent
657
+ if not uids_consistent:
658
+ validation_results["validation_errors"].append(
659
+ f"UID inconsistency detected - Auth: {auth_uid}, "
660
+ f"Profile: {userprofile.user_uid}, Status: {userstatus.user_uid}"
661
+ )
662
+
663
+ # 4. Usertype consistency validation
664
+ userauth_claims = userauth_record.custom_claims or {}
665
+ userauth_primary = userauth_claims.get("primary_usertype")
666
+ userauth_secondary = userauth_claims.get("secondary_usertypes", [])
667
+
668
+ userprofile_primary_str = str(userprofile.primary_usertype)
669
+ userprofile_secondary_strs = [str(ut) for ut in userprofile.secondary_usertypes]
670
+
671
+ usertypes_consistent = (
672
+ userauth_primary == userprofile_primary_str and
673
+ set(userauth_secondary) == set(userprofile_secondary_strs)
674
+ )
675
+ validation_results["detailed_checks"]["usertype_consistency"] = usertypes_consistent
676
+ if not usertypes_consistent:
677
+ validation_results["validation_errors"].append(
678
+ f"Usertype inconsistency - Auth primary: {userauth_primary}, "
679
+ f"Profile primary: {userprofile_primary_str}, "
680
+ f"Auth secondary: {userauth_secondary}, "
681
+ f"Profile secondary: {userprofile_secondary_strs}"
682
+ )
683
+
684
+ # 5. Approval status validation
685
+ user_approval_status = userauth_claims.get("user_approval_status")
686
+ approval_approved = user_approval_status == "APPROVED"
687
+ validation_results["detailed_checks"]["approval_status_approved"] = approval_approved
688
+ if approved_must and not approval_approved:
689
+ validation_results["validation_errors"].append(
690
+ f"User approval status is not APPROVED (current: {user_approval_status})"
691
+ )
692
+
693
+ # 6. Active subscription validation - use UserStatus methods
694
+ has_active_subscription = userstatus.is_subscription_active()
695
+ validation_results["detailed_checks"]["has_active_subscription"] = has_active_subscription
696
+ if active_subscription_must and not has_active_subscription:
697
+ validation_results["validation_errors"].append("User has no active subscription")
698
+
699
+ # 7. Valid permissions validation - use UserStatus get_valid_permissions method
700
+ valid_permissions = userstatus.get_valid_permissions()
701
+ has_valid_permissions = len(valid_permissions) > 0
702
+
703
+ validation_results["detailed_checks"]["has_valid_permissions"] = has_valid_permissions
704
+ if valid_permissions_must and not has_valid_permissions:
705
+ validation_results["validation_errors"].append("User has no valid (non-expired) IAM permissions")
706
+
707
+ # Overall validation result - only consider checks that are required based on flags
708
+ required_checks = []
709
+ required_checks.append(validation_results["detailed_checks"]["auth_enabled"]) # Always required
710
+ required_checks.append(validation_results["detailed_checks"]["uid_consistency"]) # Always required
711
+ required_checks.append(validation_results["detailed_checks"]["usertype_consistency"]) # Always required
712
+
713
+ if email_verified_must:
714
+ required_checks.append(validation_results["detailed_checks"]["email_verified"])
715
+ if approved_must:
716
+ required_checks.append(validation_results["detailed_checks"]["approval_status_approved"])
717
+ if active_subscription_must:
718
+ required_checks.append(validation_results["detailed_checks"]["has_active_subscription"])
719
+ if valid_permissions_must:
720
+ required_checks.append(validation_results["detailed_checks"]["has_valid_permissions"])
721
+
722
+ validation_results["is_fully_enabled"] = all(required_checks)
723
+
724
+ except Exception as e:
725
+ validation_results["validation_errors"].append(f"Validation process error: {str(e)}")
726
+
727
+ return validation_results
728
+
729
+ async def update_user_usertype(
730
+ self,
731
+ user_uid: str,
732
+ primary_usertype: Optional['IAMUserType'] = None,
733
+ secondary_usertypes: Optional[List['IAMUserType']] = None,
734
+ updater_uid: str = "system_usertype_update"
735
+ ) -> Tuple[UserProfile, Dict[str, Any]]:
736
+ """
737
+ Update user's primary and/or secondary usertypes efficiently across UserProfile and Firebase Auth.
738
+
739
+ This method leverages existing operations to update usertypes efficiently without
740
+ unnecessary fetching and model conversions.
741
+
742
+ Args:
743
+ user_uid: The UID of the user to update
744
+ primary_usertype: New primary usertype (optional, keeps existing if None)
745
+ secondary_usertypes: New secondary usertypes list (optional, keeps existing if None)
746
+ updater_uid: Who is performing this update
747
+
748
+ Returns:
749
+ Tuple of (updated_userprofile, updated_custom_claims)
750
+ """
751
+ try:
752
+ self.logger.info("Updating usertypes for user: %s", user_uid)
753
+
754
+ # Build update payloads
755
+ profile_update_data = {}
756
+ claims_update_data = {}
757
+
758
+ if primary_usertype is not None:
759
+ profile_update_data['primary_usertype'] = primary_usertype
760
+ claims_update_data['primary_usertype'] = str(primary_usertype)
761
+
762
+ if secondary_usertypes is not None:
763
+ profile_update_data['secondary_usertypes'] = secondary_usertypes
764
+ claims_update_data['secondary_usertypes'] = [str(ut) for ut in secondary_usertypes]
765
+
766
+ # Nothing to update
767
+ if not profile_update_data:
768
+ current_profile = await self.userprofile_ops.get_userprofile(user_uid)
769
+ if not current_profile:
770
+ raise UserCreationError(f"User profile not found: {user_uid}")
771
+ user_record = await self.userauth_ops.get_userauth(user_uid)
772
+ return current_profile, user_record.custom_claims if user_record else {}
773
+
774
+ # Update both in parallel for efficiency
775
+ await asyncio.gather(
776
+ self.userprofile_ops.update_userprofile(user_uid, profile_update_data, updater_uid),
777
+ self.userauth_ops.set_userauth_custom_claims(user_uid, claims_update_data)
778
+ )
779
+
780
+ # Get updated data for return
781
+ updated_profile, user_record = await asyncio.gather(
782
+ self.userprofile_ops.get_userprofile(user_uid),
783
+ self.userauth_ops.get_userauth(user_uid)
784
+ )
785
+
786
+ if not updated_profile:
787
+ raise UserCreationError(f"Failed to retrieve updated user profile: {user_uid}")
788
+
789
+ updated_claims = user_record.custom_claims if user_record else {}
790
+
791
+ self.logger.info("Successfully updated usertypes for user: %s", user_uid)
792
+ return updated_profile, updated_claims
793
+
794
+ except Exception as e:
795
+ self.logger.error("Failed to update usertypes for user %s: %s", user_uid, e)
796
+ raise UserCreationError(f"Failed to update usertypes for user {user_uid}: {str(e)}") from e