ipulse-shared-core-ftredge 22.1.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 (20) hide show
  1. ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +60 -23
  2. ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +128 -157
  3. ipulse_shared_core_ftredge/exceptions/base_exceptions.py +12 -4
  4. ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +4 -3
  5. ipulse_shared_core_ftredge/models/catalog/usertype.py +8 -1
  6. ipulse_shared_core_ftredge/models/user/user_subscription.py +142 -30
  7. ipulse_shared_core_ftredge/models/user/userstatus.py +63 -14
  8. ipulse_shared_core_ftredge/services/base/base_firestore_service.py +5 -3
  9. ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +27 -23
  10. ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +94 -25
  11. ipulse_shared_core_ftredge/services/user/user_core_service.py +141 -23
  12. ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +144 -74
  13. ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +24 -20
  14. ipulse_shared_core_ftredge/services/user/userstatus_operations.py +268 -4
  15. {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/METADATA +1 -1
  16. {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/RECORD +19 -20
  17. ipulse_shared_core_ftredge/services/user/firebase_auth_admin_helpers.py +0 -160
  18. {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/WHEEL +0 -0
  19. {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/licenses/LICENCE +0 -0
  20. {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/top_level.txt +0 -0
@@ -17,7 +17,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
17
17
  from datetime import datetime
18
18
  from google.cloud import firestore
19
19
  from firebase_admin.auth import UserRecord
20
- from ipulse_shared_base_ftredge import ApprovalStatus, IAMUnit
20
+ from ipulse_shared_base_ftredge import ApprovalStatus, IAMUnit, IAMUserType
21
21
  from ...models import UserProfile, UserStatus, UserAuth, UserSubscription, UserType, SubscriptionPlan, UserPermission
22
22
 
23
23
 
@@ -133,7 +133,8 @@ class UserCoreService:
133
133
  userprofile: UserProfile,
134
134
  userstatus: UserStatus,
135
135
  userauth: Optional[UserAuth] = None,
136
- custom_claims: Optional[Dict[str, Any]] = None
136
+ validate_userauth_consistency: bool = False,
137
+ validate_userauth_exists: bool = False
137
138
  ) -> Tuple[str, UserProfile, UserStatus]:
138
139
  """
139
140
  Create a complete user from ready UserAuth, UserProfile, and UserStatus models.
@@ -144,7 +145,8 @@ class UserCoreService:
144
145
  userprofile=userprofile,
145
146
  userstatus=userstatus,
146
147
  userauth=userauth,
147
- custom_claims=custom_claims
148
+ validate_userauth_consistency=validate_userauth_consistency,
149
+ validate_userauth_exists=validate_userauth_exists
148
150
  )
149
151
 
150
152
  async def create_user_from_manual_usertype(
@@ -152,13 +154,13 @@ class UserCoreService:
152
154
  userprofile: UserProfile,
153
155
  usertype: UserType,
154
156
  userauth: Optional[UserAuth] = None,
155
- custom_claims: Optional[Dict[str, Any]] = None,
156
- user_approval_status: ApprovalStatus = ApprovalStatus.PENDING,
157
- user_notes: str = "Created with manual usertype configuration",
158
157
  extra_insight_credits_override: Optional[int] = None,
159
158
  voting_credits_override: Optional[int] = None,
160
159
  subscriptionplan_id_override: Optional[str] = None,
161
- created_by: Optional[str] = None
160
+ created_by: Optional[str] = None,
161
+ apply_usertype_associated_subscriptionplan: bool = True,
162
+ validate_userauth_consistency: bool = False,
163
+ validate_userauth_exists: bool = False
162
164
  ) -> Tuple[str, UserProfile, UserStatus]:
163
165
  """
164
166
  Create a complete user with manual UserType configuration.
@@ -169,13 +171,13 @@ class UserCoreService:
169
171
  userprofile=userprofile,
170
172
  usertype=usertype,
171
173
  userauth=userauth,
172
- custom_claims=custom_claims,
173
- user_approval_status=user_approval_status,
174
- user_notes=user_notes,
175
174
  extra_insight_credits_override=extra_insight_credits_override,
176
175
  voting_credits_override=voting_credits_override,
177
176
  subscriptionplan_id_override=subscriptionplan_id_override,
178
- creator_uid=created_by
177
+ creator_uid=created_by,
178
+ apply_usertype_associated_subscriptionplan=apply_usertype_associated_subscriptionplan,
179
+ validate_userauth_consistency=validate_userauth_consistency,
180
+ validate_userauth_exists=validate_userauth_exists
179
181
  )
180
182
 
181
183
  async def create_user_from_catalog_usertype(
@@ -183,13 +185,13 @@ class UserCoreService:
183
185
  usertype_id: str,
184
186
  userprofile: UserProfile,
185
187
  userauth: Optional[UserAuth] = None,
186
- custom_claims: Optional[Dict[str, Any]] = None,
187
- user_approval_status: ApprovalStatus = ApprovalStatus.PENDING,
188
- user_notes: str = "Created from catalog usertype configuration",
189
188
  extra_insight_credits_override: Optional[int] = None,
190
189
  voting_credits_override: Optional[int] = None,
191
190
  subscriptionplan_id_override: Optional[str] = None,
192
- created_by: Optional[str] = None
191
+ created_by: Optional[str] = None,
192
+ apply_usertype_associated_subscriptionplan: bool = True,
193
+ validate_userauth_consistency: bool = False,
194
+ validate_userauth_exists: bool = False
193
195
  ) -> Tuple[str, UserProfile, UserStatus]:
194
196
  """
195
197
  Create a complete user based on UserType catalog configuration.
@@ -200,13 +202,13 @@ class UserCoreService:
200
202
  usertype_id=usertype_id,
201
203
  userprofile=userprofile,
202
204
  userauth=userauth,
203
- custom_claims=custom_claims,
204
- user_approval_status=user_approval_status,
205
- user_notes=user_notes,
206
205
  extra_insight_credits_override=extra_insight_credits_override,
207
206
  voting_credits_override=voting_credits_override,
208
207
  subscriptionplan_id_override=subscriptionplan_id_override,
209
- creator_uid=created_by
208
+ creator_uid=created_by,
209
+ apply_usertype_associated_subscriptionplan=apply_usertype_associated_subscriptionplan,
210
+ validate_userauth_consistency=validate_userauth_consistency,
211
+ validate_userauth_exists=validate_userauth_exists
210
212
  )
211
213
 
212
214
  ######################################################################
@@ -351,6 +353,71 @@ class UserCoreService:
351
353
  """Set user approval status in custom claims"""
352
354
  return await self.userauth_ops.set_user_approval_status(user_uid=user_uid, approval_status=status, updated_by=notes)
353
355
 
356
+ # Token and Security Operations
357
+
358
+ async def create_custom_token(
359
+ self,
360
+ user_uid: str,
361
+ additional_claims: Optional[Dict[str, Any]] = None
362
+ ) -> str:
363
+ """Creates a custom token for a user with optional additional claims"""
364
+ return await self.userauth_ops.create_custom_token(
365
+ user_uid=user_uid,
366
+ additional_claims=additional_claims
367
+ )
368
+
369
+ async def verify_id_token(
370
+ self,
371
+ token: str,
372
+ check_revoked: bool = False
373
+ ) -> Dict[str, Any]:
374
+ """Verifies an ID token and returns the token claims"""
375
+ return await self.userauth_ops.verify_id_token(
376
+ token=token,
377
+ check_revoked=check_revoked
378
+ )
379
+
380
+ async def get_user_auth_token(
381
+ self,
382
+ email: str,
383
+ password: str,
384
+ api_key: str
385
+ ) -> Optional[str]:
386
+ """
387
+ Gets a user authentication token using the Firebase REST API.
388
+
389
+ Note: This method requires the Firebase Web API key and should be used
390
+ for testing or specific admin scenarios. For production authentication,
391
+ prefer using the Firebase client SDKs.
392
+ """
393
+ return await self.userauth_ops.get_user_auth_token(
394
+ email=email,
395
+ password=password,
396
+ api_key=api_key
397
+ )
398
+
399
+ async def generate_password_reset_link(
400
+ self,
401
+ email: str,
402
+ action_code_settings: Optional[Dict[str, Any]] = None
403
+ ) -> str:
404
+ """Generates a password reset link for a user"""
405
+ return await self.userauth_ops.generate_password_reset_link(
406
+ email=email,
407
+ action_code_settings=action_code_settings
408
+ )
409
+
410
+ async def generate_email_verification_link(
411
+ self,
412
+ email: str,
413
+ action_code_settings: Optional[Dict[str, Any]] = None
414
+ ) -> str:
415
+ """Generates an email verification link for a user"""
416
+ return await self.userauth_ops.generate_email_verification_link(
417
+ email=email,
418
+ action_code_settings=action_code_settings
419
+ )
420
+
354
421
  ######################################################################
355
422
  ######################### UserProfile Operations ####################
356
423
  ######################################################################
@@ -432,7 +499,7 @@ class UserCoreService:
432
499
  updater_uid: str,
433
500
  source: str = "user_core_service",
434
501
  granted_at: Optional[datetime] = None,
435
- auto_renewal: Optional[bool] = None
502
+ auto_renewal_end: Optional[datetime] = None
436
503
  ) -> UserSubscription:
437
504
  """Fetch a subscription plan from catalog and apply to user"""
438
505
  return await self.subscription_ops.fetch_subscriptionplan_and_apply_subscription_to_user(
@@ -441,7 +508,7 @@ class UserCoreService:
441
508
  updater_uid=updater_uid,
442
509
  source=source,
443
510
  granted_at=granted_at,
444
- auto_renewal=auto_renewal
511
+ auto_renewal_end=auto_renewal_end
445
512
  )
446
513
 
447
514
  async def apply_subscriptionplan_to_user(
@@ -451,7 +518,7 @@ class UserCoreService:
451
518
  updater_uid: str,
452
519
  source: str = "user_core_service",
453
520
  granted_at: Optional[datetime] = None,
454
- auto_renewal: Optional[bool] = None
521
+ auto_renewal_end: Optional[datetime] = None
455
522
  ) -> UserSubscription:
456
523
  """Apply a subscription plan directly to user (plan already fetched)"""
457
524
  return await self.subscription_ops.apply_subscriptionplan(
@@ -460,7 +527,7 @@ class UserCoreService:
460
527
  updater_uid=updater_uid,
461
528
  source=source,
462
529
  granted_at=granted_at,
463
- auto_renewal=auto_renewal
530
+ auto_renewal_end=auto_renewal_end
464
531
  )
465
532
 
466
533
  async def cancel_user_subscription(self, user_uid: str, updater_uid: str) -> bool:
@@ -556,4 +623,55 @@ class UserCoreService:
556
623
  valid_only=valid_only
557
624
  )
558
625
 
626
+ async def update_user_usertype(
627
+ self,
628
+ user_uid: str,
629
+ primary_usertype: Optional['IAMUserType'] = None,
630
+ secondary_usertypes: Optional[List['IAMUserType']] = None,
631
+ updater_uid: str = "system_usertype_update"
632
+ ) -> Tuple[UserProfile, Dict[str, Any]]:
633
+ """
634
+ Update user's primary and/or secondary usertypes efficiently across UserProfile and Firebase Auth.
635
+
636
+ Args:
637
+ user_uid: The UID of the user to update
638
+ primary_usertype: New primary usertype (optional, keeps existing if None)
639
+ secondary_usertypes: New secondary usertypes list (optional, keeps existing if None)
640
+ updater_uid: Who is performing this update
641
+
642
+ Returns:
643
+ Tuple of (updated_userprofile, updated_custom_claims)
644
+ """
645
+ return await self.usermultistep_ops.update_user_usertype(
646
+ user_uid=user_uid,
647
+ primary_usertype=primary_usertype,
648
+ secondary_usertypes=secondary_usertypes,
649
+ updater_uid=updater_uid
650
+ )
651
+
652
+ ######################################################################
653
+ ####################### User Subscription/Status Review #############
654
+ ######################################################################
655
+
656
+ async def review_and_clean_active_subscription_credits_and_permissions(
657
+ self,
658
+ user_uid: str,
659
+ updater_uid: str = "system_review",
660
+ review_auto_renewal: bool = True,
661
+ apply_fallback: bool = True,
662
+ clean_expired_permissions: bool = True,
663
+ review_credits: bool = True
664
+ ) -> Dict[str, Any]:
665
+ """
666
+ Orchestrate a comprehensive review and cleanup of a user's active subscription, credits, and permissions.
667
+ Delegates to UserstatusOperations for the actual logic.
668
+ """
669
+ return await self.userstatus_ops.review_and_clean_active_subscription_credits_and_permissions(
670
+ user_uid=user_uid,
671
+ updater_uid=updater_uid,
672
+ review_auto_renewal=review_auto_renewal,
673
+ apply_fallback=apply_fallback,
674
+ clean_expired_permissions=clean_expired_permissions,
675
+ review_credits=review_credits
676
+ )
559
677
 
@@ -7,7 +7,7 @@ Firebase Auth, UserProfile, and UserStatus in coordinated transactions.
7
7
  import asyncio
8
8
  import logging
9
9
  from typing import Dict, Any, Optional, List, Tuple, cast
10
- from ipulse_shared_base_ftredge.enums import ApprovalStatus
10
+ from ipulse_shared_base_ftredge.enums import IAMUserType
11
11
  from ...models import UserProfile, UserStatus, UserAuth, UserType
12
12
  from .userauth_operations import UserauthOperations
13
13
  from .userprofile_operations import UserprofileOperations
@@ -50,9 +50,6 @@ class UsermultistepOperations:
50
50
 
51
51
 
52
52
 
53
-
54
-
55
-
56
53
  async def _rollback_user_creation(
57
54
  self,
58
55
  user_uid: Optional[str],
@@ -134,7 +131,8 @@ class UsermultistepOperations:
134
131
  userprofile: UserProfile,
135
132
  userstatus: UserStatus,
136
133
  userauth: Optional[UserAuth] = None,
137
- custom_claims: Optional[Dict[str, Any]] = None
134
+ validate_userauth_consistency: bool = False,
135
+ validate_userauth_exists: bool = False
138
136
  ) -> Tuple[str, UserProfile, UserStatus]:
139
137
  """
140
138
  Create a complete user from ready UserAuth, UserProfile, and UserStatus models.
@@ -154,7 +152,8 @@ class UsermultistepOperations:
154
152
  userprofile: Complete UserProfile model (template for new user, or ready for existing user)
155
153
  userstatus: Complete UserStatus model (template for new user, or ready for existing user)
156
154
  userauth: Optional UserAuth model. If provided, creates new Firebase Auth user
157
- custom_claims: Optional custom claims to set (will be merged with userauth.custom_claims)
155
+ validate_userauth_consistency: If True, validates userauth is consistent with userprofile
156
+ validate_userauth_exists: If True, validates userauth exists in Firebase Auth
158
157
 
159
158
  Returns:
160
159
  Tuple of (user_uid, userprofile, userstatus)
@@ -167,17 +166,14 @@ class UsermultistepOperations:
167
166
  if userprofile.user_uid != userstatus.user_uid:
168
167
  raise UserCreationError(f"UserProfile and UserStatus user_uid mismatch: {userprofile.user_uid} != {userstatus.user_uid}")
169
168
 
170
- # Validate usertype consistency between UserProfile and UserAuth if userauth is provided
171
169
  try:
172
170
  # Step 1: Handle Firebase Auth user creation or validation
173
171
  if userauth:
174
172
  # Creating new user - Firebase will generate UID
175
- # Merge custom claims into UserAuth model
176
- if custom_claims:
177
- userauth.custom_claims.update(custom_claims)
178
173
 
179
- # Validate usertype consistency
180
- self._validate_usertype_consistency(userprofile, userauth.custom_claims)
174
+ # Validate usertype consistency if requested
175
+ if validate_userauth_consistency:
176
+ self._validate_usertype_consistency(userprofile, userauth.custom_claims)
181
177
 
182
178
  # Create Firebase Auth user with all configuration
183
179
  self.logger.info("Creating Firebase Auth user with custom claims for email: %s", userauth.email)
@@ -204,33 +200,17 @@ class UsermultistepOperations:
204
200
  final_userprofile = userprofile
205
201
  final_userstatus = userstatus
206
202
 
207
- # Validate existing user and apply custom claims if provided
208
- if not await self.userauth_ops.userauth_exists(user_uid):
209
- raise UserCreationError(f"Firebase Auth user {user_uid} does not exist")
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")
210
207
 
211
- if custom_claims:
212
- # Check if custom_claims contain usertype information
213
- claims_have_usertype_info = (
214
- "primary_usertype" in custom_claims or
215
- "secondary_usertypes" in custom_claims
216
- )
217
-
218
- if claims_have_usertype_info:
219
- # Validate usertype consistency with provided custom claims
220
- self._validate_usertype_consistency(userprofile, custom_claims)
221
- else:
222
- # Custom claims don't have usertype info, validate against existing auth claims
208
+ # Validate userauth consistency if requested
209
+ if validate_userauth_consistency:
223
210
  existing_userauth = await self.userauth_ops.get_userauth(user_uid, get_model=True)
224
211
  if existing_userauth and existing_userauth.custom_claims:
225
212
  self._validate_usertype_consistency(userprofile, existing_userauth.custom_claims)
226
213
 
227
- await self.userauth_ops.set_userauth_custom_claims(user_uid, custom_claims)
228
- else:
229
- # No custom claims provided - validate against existing userauth custom claims
230
- existing_userauth = await self.userauth_ops.get_userauth(user_uid, get_model=True)
231
- if existing_userauth and existing_userauth.custom_claims:
232
- self._validate_usertype_consistency(userprofile, existing_userauth.custom_claims)
233
-
234
214
  # Step 2: Create UserProfile and UserStatus in database (2 operations only)
235
215
  self.logger.info("Creating UserProfile for user: %s", user_uid)
236
216
  await self.userprofile_ops.create_userprofile(final_userprofile)
@@ -263,13 +243,13 @@ class UsermultistepOperations:
263
243
  userprofile: UserProfile,
264
244
  usertype: UserType,
265
245
  userauth: Optional[UserAuth] = None,
266
- custom_claims: Optional[Dict[str, Any]] = None,
267
- user_approval_status: ApprovalStatus = ApprovalStatus.PENDING,
268
- user_notes: str = "Created with manual usertype configuration",
269
246
  extra_insight_credits_override: Optional[int] = None,
270
247
  voting_credits_override: Optional[int] = None,
271
248
  subscriptionplan_id_override: Optional[str] = None,
272
- creator_uid: 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
273
253
  ) -> Tuple[str, UserProfile, UserStatus]:
274
254
  """
275
255
  Create a complete user with manual UserType configuration.
@@ -281,13 +261,13 @@ class UsermultistepOperations:
281
261
  userprofile: Complete UserProfile model (mandatory)
282
262
  usertype: Manual UserType configuration (mandatory)
283
263
  userauth: Optional UserAuth model. If not provided, assumes user exists
284
- custom_claims: Optional custom claims to set
285
- user_approval_status: User approval status (for custom claims)
286
- user_notes: User notes to set in custom claims
287
- extra_insight_credits: Override extra credits from usertype
288
- voting_credits: Override voting credits from usertype
289
- subscription_plan_id: Override subscription plan from usertype default
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
290
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
291
271
 
292
272
  Returns:
293
273
  Tuple of (user_uid, userprofile, userstatus)
@@ -311,28 +291,49 @@ class UsermultistepOperations:
311
291
  )
312
292
 
313
293
  # Apply subscription to UserStatus in memory if plan specified
314
- plan_to_apply = subscriptionplan_id_override or usertype.default_subscription_plan_if_unpaid
315
- if plan_to_apply:
316
- # For now, we'll apply subscription after UserStatus is created
317
- # Future enhancement: Apply subscription directly to UserStatus in memory
318
- self.logger.info("Subscription plan %s will be applied after user creation", plan_to_apply)
319
-
320
- # Generate custom claims if not provided
321
- if not custom_claims:
322
- custom_claims = {
323
- "primary_usertype": str(userprofile.primary_usertype),
324
- "secondary_usertypes": [str(ut) for ut in userprofile.secondary_usertypes],
325
- "organizations_uids": list(final_organizations),
326
- "user_approval_status": str(user_approval_status),
327
- "user_notes": user_notes
328
- }
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
329
 
330
330
  # Create user from ready models
331
331
  return await self.create_user_from_models(
332
332
  userprofile=userprofile,
333
333
  userstatus=userstatus,
334
334
  userauth=userauth,
335
- custom_claims=custom_claims
335
+ validate_userauth_consistency=validate_userauth_consistency,
336
+ validate_userauth_exists=validate_userauth_exists
336
337
  )
337
338
 
338
339
  except Exception as e:
@@ -344,13 +345,13 @@ class UsermultistepOperations:
344
345
  usertype_id: str,
345
346
  userprofile: UserProfile,
346
347
  userauth: Optional[UserAuth] = None,
347
- custom_claims: Optional[Dict[str, Any]] = None,
348
- user_approval_status: ApprovalStatus = ApprovalStatus.PENDING,
349
- user_notes: str = "Created from catalog usertype configuration",
350
348
  extra_insight_credits_override: Optional[int] = None,
351
349
  voting_credits_override: Optional[int] = None,
352
350
  subscriptionplan_id_override: Optional[str] = None,
353
- creator_uid: 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
354
355
  ) -> Tuple[str, UserProfile, UserStatus]:
355
356
  """
356
357
  Create a complete user based on UserType catalog configuration.
@@ -362,13 +363,13 @@ class UsermultistepOperations:
362
363
  usertype_id: ID of the UserType configuration to fetch from catalog (mandatory)
363
364
  userprofile: Complete UserProfile model (mandatory)
364
365
  userauth: Optional UserAuth model. If not provided, assumes user exists
365
- custom_claims: Optional custom claims to set
366
- user_approval_status: User approval status (for custom claims)
367
- user_notes: User notes to set in custom claims
368
- extra_insight_credits: Override extra credits from usertype
369
- voting_credits: Override voting credits from usertype
370
- subscriptionplan_id: Override subscription plan from usertype default
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
371
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
372
373
 
373
374
  Returns:
374
375
  Tuple of (user_uid, userprofile, userstatus)
@@ -385,13 +386,13 @@ class UsermultistepOperations:
385
386
  userprofile=userprofile,
386
387
  usertype=usertype_config,
387
388
  userauth=userauth,
388
- custom_claims=custom_claims,
389
- user_approval_status=user_approval_status,
390
- user_notes=user_notes,
391
389
  extra_insight_credits_override=extra_insight_credits_override,
392
390
  voting_credits_override=voting_credits_override,
393
391
  subscriptionplan_id_override=subscriptionplan_id_override,
394
- creator_uid=creator_uid
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
395
396
  )
396
397
 
397
398
  except Exception as e:
@@ -724,3 +725,72 @@ class UsermultistepOperations:
724
725
  validation_results["validation_errors"].append(f"Validation process error: {str(e)}")
725
726
 
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