ipulse-shared-core-ftredge 18.0.1__py3-none-any.whl → 20.0.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.
- ipulse_shared_core_ftredge/__init__.py +1 -12
- ipulse_shared_core_ftredge/exceptions/__init__.py +47 -0
- ipulse_shared_core_ftredge/exceptions/user_exceptions.py +219 -0
- ipulse_shared_core_ftredge/models/__init__.py +0 -2
- ipulse_shared_core_ftredge/models/base_data_model.py +6 -6
- ipulse_shared_core_ftredge/models/user_auth.py +59 -4
- ipulse_shared_core_ftredge/models/user_profile.py +41 -7
- ipulse_shared_core_ftredge/models/user_status.py +44 -138
- ipulse_shared_core_ftredge/monitoring/__init__.py +7 -0
- ipulse_shared_core_ftredge/monitoring/microservmon.py +526 -0
- ipulse_shared_core_ftredge/monitoring/tracemon.py +320 -0
- ipulse_shared_core_ftredge/services/__init__.py +21 -14
- ipulse_shared_core_ftredge/services/base/__init__.py +12 -0
- ipulse_shared_core_ftredge/services/base/base_firestore_service.py +520 -0
- ipulse_shared_core_ftredge/services/cache_aware_firestore_service.py +44 -8
- ipulse_shared_core_ftredge/services/charging_service.py +1 -1
- ipulse_shared_core_ftredge/services/user/__init__.py +37 -0
- ipulse_shared_core_ftredge/services/user/iam_management_operations.py +326 -0
- ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +384 -0
- ipulse_shared_core_ftredge/services/user/user_account_operations.py +479 -0
- ipulse_shared_core_ftredge/services/user/user_auth_operations.py +305 -0
- ipulse_shared_core_ftredge/services/user/user_core_service.py +651 -0
- ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +436 -0
- {ipulse_shared_core_ftredge-18.0.1.dist-info → ipulse_shared_core_ftredge-20.0.1.dist-info}/METADATA +1 -1
- ipulse_shared_core_ftredge-20.0.1.dist-info/RECORD +42 -0
- ipulse_shared_core_ftredge/models/organization_profile.py +0 -96
- ipulse_shared_core_ftredge/models/user_profile_update.py +0 -39
- ipulse_shared_core_ftredge/services/base_firestore_service.py +0 -249
- ipulse_shared_core_ftredge/services/fastapiservicemon.py +0 -140
- ipulse_shared_core_ftredge/services/servicemon.py +0 -240
- ipulse_shared_core_ftredge-18.0.1.dist-info/RECORD +0 -33
- ipulse_shared_core_ftredge/{services/base_service_exceptions.py → exceptions/base_exceptions.py} +1 -1
- {ipulse_shared_core_ftredge-18.0.1.dist-info → ipulse_shared_core_ftredge-20.0.1.dist-info}/WHEEL +0 -0
- {ipulse_shared_core_ftredge-18.0.1.dist-info → ipulse_shared_core_ftredge-20.0.1.dist-info}/licenses/LICENCE +0 -0
- {ipulse_shared_core_ftredge-18.0.1.dist-info → ipulse_shared_core_ftredge-20.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced UserCoreService - Comprehensive user management orchestration
|
|
3
|
+
|
|
4
|
+
This service orchestrates all user-related operations by composing specialized
|
|
5
|
+
operation classes for different concerns:
|
|
6
|
+
- Firebase Auth User Management
|
|
7
|
+
- User Account Management (UserProfile and UserStatus CRUD operations)
|
|
8
|
+
- User Deletion Operations
|
|
9
|
+
- Subscription Management
|
|
10
|
+
- IAM Management
|
|
11
|
+
- Default Values by UserType
|
|
12
|
+
|
|
13
|
+
Can be used by Firebase Cloud Functions, Core microservice APIs, admin tools, and tests.
|
|
14
|
+
"""
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
17
|
+
from google.cloud import firestore
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
from ...models.user_profile import UserProfile
|
|
21
|
+
from ...models.user_status import UserStatus, IAMUnitRefAssignment
|
|
22
|
+
from ...models.user_auth import UserAuth
|
|
23
|
+
from ...models.subscription import Subscription
|
|
24
|
+
from ...exceptions import ServiceError, ResourceNotFoundError, UserCreationError
|
|
25
|
+
from ..base import BaseFirestoreService
|
|
26
|
+
from ipulse_shared_base_ftredge.enums.enums_iam import IAMUnitType
|
|
27
|
+
|
|
28
|
+
# Import specialized operation classes
|
|
29
|
+
from .user_account_operations import UserAccountOperations
|
|
30
|
+
from .subscription_management_operations import SubscriptionManagementOperations, SubscriptionPlanDocument
|
|
31
|
+
from .iam_management_operations import IAMManagementOperations
|
|
32
|
+
from .user_auth_operations import UserAuthOperations
|
|
33
|
+
from .user_holistic_operations import UserHolisticOperations
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Model for user type defaults from Firestore
|
|
37
|
+
class UserTypeDefaultsDocument(BaseModel):
|
|
38
|
+
"""Model for user type defaults documents stored in Firestore"""
|
|
39
|
+
id: str
|
|
40
|
+
default_iam_domain_permissions: Optional[Dict[str, Dict[str, Dict[str, IAMUnitRefAssignment]]]] = None
|
|
41
|
+
default_subscription_based_insight_credits: Optional[int] = None
|
|
42
|
+
default_extra_insight_credits: Optional[int] = None
|
|
43
|
+
default_voting_credits: Optional[int] = None
|
|
44
|
+
default_subscription_plan_if_unpaid: Optional[str] = None
|
|
45
|
+
default_secondary_usertypes: Optional[List[str]] = None
|
|
46
|
+
default_organizations_uids: Optional[List[str]] = None
|
|
47
|
+
default_subscription_plan_id: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class UserCoreService:
|
|
51
|
+
"""
|
|
52
|
+
Enhanced UserCoreService - Orchestrates all user-related operations
|
|
53
|
+
|
|
54
|
+
This service provides a unified interface for all user management operations
|
|
55
|
+
by composing specialized operation classes. It maintains backward compatibility
|
|
56
|
+
while providing enhanced functionality and better organization.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
firestore_client: firestore.Client,
|
|
62
|
+
logger: Optional[logging.Logger] = None,
|
|
63
|
+
default_timeout: float = 10.0,
|
|
64
|
+
profile_collection: Optional[str] = None,
|
|
65
|
+
status_collection: Optional[str] = None,
|
|
66
|
+
subscriptionplans_defaults_collection: str = "papp_core_configs_subscriptionplans_defaults",
|
|
67
|
+
users_defaults_collection: str = "papp_core_configs_users_defaults"
|
|
68
|
+
):
|
|
69
|
+
"""
|
|
70
|
+
Initialize the Enhanced UserCoreService
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
firestore_client: Initialized Firestore client
|
|
74
|
+
logger: Optional logger instance
|
|
75
|
+
default_timeout: Default timeout for Firestore operations
|
|
76
|
+
profile_collection: Collection name for user profiles
|
|
77
|
+
status_collection: Collection name for user statuses
|
|
78
|
+
subscriptionplans_defaults_collection: Collection name for subscription plans
|
|
79
|
+
users_defaults_collection: Collection name for user defaults
|
|
80
|
+
"""
|
|
81
|
+
self.db = firestore_client
|
|
82
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
83
|
+
self.timeout = default_timeout
|
|
84
|
+
|
|
85
|
+
self.profile_collection_name = profile_collection or UserProfile.get_collection_name()
|
|
86
|
+
self.status_collection_name = status_collection or UserStatus.get_collection_name()
|
|
87
|
+
|
|
88
|
+
# Initialize specialized operation classes in dependency order
|
|
89
|
+
self.user_auth_ops = UserAuthOperations(
|
|
90
|
+
logger=self.logger
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.user_account_ops = UserAccountOperations(
|
|
94
|
+
firestore_client=self.db,
|
|
95
|
+
logger=self.logger,
|
|
96
|
+
timeout=self.timeout,
|
|
97
|
+
profile_collection=self.profile_collection_name,
|
|
98
|
+
status_collection=self.status_collection_name
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.subscription_ops = SubscriptionManagementOperations(
|
|
102
|
+
firestore_client=self.db,
|
|
103
|
+
user_account_ops=self.user_account_ops,
|
|
104
|
+
logger=self.logger,
|
|
105
|
+
timeout=self.timeout,
|
|
106
|
+
subscription_plans_collection=subscriptionplans_defaults_collection
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.iam_ops = IAMManagementOperations(
|
|
110
|
+
user_account_ops=self.user_account_ops,
|
|
111
|
+
logger=self.logger
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Initialize holistic operations last as it depends on other operations
|
|
115
|
+
self.user_holistic_ops = UserHolisticOperations(
|
|
116
|
+
user_account_ops=self.user_account_ops,
|
|
117
|
+
user_auth_ops=self.user_auth_ops,
|
|
118
|
+
subscription_ops=self.subscription_ops,
|
|
119
|
+
iam_ops=self.iam_ops,
|
|
120
|
+
logger=self.logger
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Initialize defaults values per usertype service
|
|
124
|
+
self.usertype_defaults_collection_name = users_defaults_collection
|
|
125
|
+
self._user_defaults_db_service = BaseFirestoreService[UserTypeDefaultsDocument](
|
|
126
|
+
db=self.db,
|
|
127
|
+
collection_name=self.usertype_defaults_collection_name,
|
|
128
|
+
resource_type="UserTypeDefault",
|
|
129
|
+
logger=self.logger,
|
|
130
|
+
timeout=self.timeout
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
######################################################################
|
|
135
|
+
######################### UserAuth Operation #########################
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def generate_firebase_custom_claims(
|
|
142
|
+
self,
|
|
143
|
+
primary_usertype: str,
|
|
144
|
+
secondary_usertypes: Optional[List[str]] = None,
|
|
145
|
+
organizations_uids: Optional[List[str]] = None,
|
|
146
|
+
user_approval_status: str = "pending"
|
|
147
|
+
) -> Dict[str, Any]:
|
|
148
|
+
"""Generate Firebase custom claims for a user"""
|
|
149
|
+
return self.user_auth_ops.generate_firebase_custom_claims(
|
|
150
|
+
primary_usertype=primary_usertype,
|
|
151
|
+
secondary_usertypes=secondary_usertypes,
|
|
152
|
+
organizations_uids=organizations_uids,
|
|
153
|
+
user_approval_status=user_approval_status
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
################################################################################################
|
|
159
|
+
######################### Fetching Default User/Subscription Values #########################
|
|
160
|
+
|
|
161
|
+
async def fetch_user_defaults(self, usertype_name: str) -> Optional[Dict[str, Any]]:
|
|
162
|
+
"""Fetch user type defaults from Firestore"""
|
|
163
|
+
try:
|
|
164
|
+
user_defaults_data = await self._user_defaults_db_service.get_document(usertype_name)
|
|
165
|
+
if user_defaults_data:
|
|
166
|
+
self.logger.info(f"User defaults found for usertype: {usertype_name}")
|
|
167
|
+
# Convert UserTypeDefaultsDocument to dict for backward compatibility
|
|
168
|
+
if isinstance(user_defaults_data, UserTypeDefaultsDocument):
|
|
169
|
+
return user_defaults_data.model_dump()
|
|
170
|
+
return user_defaults_data
|
|
171
|
+
else:
|
|
172
|
+
self.logger.warning(f"User defaults not found for usertype: {usertype_name}")
|
|
173
|
+
return None
|
|
174
|
+
except ResourceNotFoundError:
|
|
175
|
+
self.logger.warning(f"User defaults not found for usertype: {usertype_name}")
|
|
176
|
+
return None
|
|
177
|
+
except Exception as e:
|
|
178
|
+
self.logger.error(f"Error fetching user defaults for {usertype_name}: {e}", exc_info=True)
|
|
179
|
+
raise ServiceError(
|
|
180
|
+
operation="fetch_user_defaults",
|
|
181
|
+
resource_type="UserTypeDefault",
|
|
182
|
+
resource_id=usertype_name,
|
|
183
|
+
error=e
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def _fetch_subscription_plan_details(self, plan_id: str) -> Optional[SubscriptionPlanDocument]:
|
|
187
|
+
"""Fetch subscription plan details (backward compatibility)"""
|
|
188
|
+
return await self.subscription_ops.fetch_subscription_plan_details(plan_id)
|
|
189
|
+
|
|
190
|
+
###############################################################################
|
|
191
|
+
############################## User Account Operation #########################
|
|
192
|
+
|
|
193
|
+
async def get_userprofile(self, user_uid: str) -> Optional[UserProfile]:
|
|
194
|
+
"""Get a user profile by user UID"""
|
|
195
|
+
return await self.user_account_ops.get_userprofile(user_uid)
|
|
196
|
+
|
|
197
|
+
async def get_userstatus(self, user_uid: str) -> Optional[UserStatus]:
|
|
198
|
+
"""Get a user status by user UID"""
|
|
199
|
+
return await self.user_account_ops.get_userstatus(user_uid)
|
|
200
|
+
|
|
201
|
+
async def create_userprofile(self, user_profile: UserProfile) -> UserProfile:
|
|
202
|
+
"""Create a new user profile"""
|
|
203
|
+
return await self.user_account_ops.create_userprofile(user_profile)
|
|
204
|
+
|
|
205
|
+
async def create_userstatus(
|
|
206
|
+
self,
|
|
207
|
+
user_uid: str,
|
|
208
|
+
primary_usertype: str,
|
|
209
|
+
organizations_uids: Optional[Set[str]] = None,
|
|
210
|
+
secondary_usertypes: Optional[List[str]] = None,
|
|
211
|
+
initial_subscription_plan_id: Optional[str] = None,
|
|
212
|
+
iam_domain_permissions: Optional[Dict[str, Dict[str, Dict[str, IAMUnitRefAssignment]]]] = None,
|
|
213
|
+
sbscrptn_based_insight_credits: int = 0,
|
|
214
|
+
extra_insight_credits: int = 0,
|
|
215
|
+
voting_credits: int = 0,
|
|
216
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
217
|
+
created_by: Optional[str] = None
|
|
218
|
+
) -> UserStatus:
|
|
219
|
+
"""Create a new user status with optional initial subscription"""
|
|
220
|
+
user_status = await self.user_account_ops.create_userstatus(
|
|
221
|
+
user_uid=user_uid,
|
|
222
|
+
primary_usertype=primary_usertype,
|
|
223
|
+
organizations_uids=organizations_uids,
|
|
224
|
+
secondary_usertypes=secondary_usertypes,
|
|
225
|
+
iam_domain_permissions=iam_domain_permissions,
|
|
226
|
+
sbscrptn_based_insight_credits=sbscrptn_based_insight_credits,
|
|
227
|
+
extra_insight_credits=extra_insight_credits,
|
|
228
|
+
voting_credits=voting_credits,
|
|
229
|
+
metadata=metadata,
|
|
230
|
+
created_by=created_by
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Apply initial subscription if provided
|
|
234
|
+
if initial_subscription_plan_id:
|
|
235
|
+
try:
|
|
236
|
+
await self.subscription_ops.apply_subscription_plan(
|
|
237
|
+
user_uid=user_uid,
|
|
238
|
+
plan_id=initial_subscription_plan_id,
|
|
239
|
+
source=f"initial_setup_by_{created_by or 'system'}"
|
|
240
|
+
)
|
|
241
|
+
# Fetch updated status with subscription
|
|
242
|
+
updated_status = await self.get_userstatus(user_uid)
|
|
243
|
+
return updated_status or user_status
|
|
244
|
+
except Exception as e:
|
|
245
|
+
self.logger.error(f"Failed to apply initial subscription plan {initial_subscription_plan_id} for {user_uid}: {e}")
|
|
246
|
+
# Return the status without subscription rather than failing completely
|
|
247
|
+
|
|
248
|
+
return user_status
|
|
249
|
+
|
|
250
|
+
async def update_userprofile(self, user_uid: str, profile_data: Dict[str, Any], updater_uid: str) -> UserProfile:
|
|
251
|
+
"""Update a user profile"""
|
|
252
|
+
return await self.user_account_ops.update_userprofile(user_uid, profile_data, updater_uid)
|
|
253
|
+
|
|
254
|
+
async def update_userstatus(self, user_uid: str, status_data: Dict[str, Any], updater_uid: str) -> UserStatus:
|
|
255
|
+
"""Update a user status"""
|
|
256
|
+
return await self.user_account_ops.update_userstatus(user_uid, status_data, updater_uid)
|
|
257
|
+
|
|
258
|
+
##########################################################################################
|
|
259
|
+
################################ Hollistic User Creation #################################
|
|
260
|
+
|
|
261
|
+
async def create_user(
|
|
262
|
+
self,
|
|
263
|
+
email: str,
|
|
264
|
+
provider_id: str,
|
|
265
|
+
primary_usertype_name: str,
|
|
266
|
+
initial_subscription_plan_id: Optional[str] = None,
|
|
267
|
+
organizations_uids: Optional[Set[str]] = None,
|
|
268
|
+
secondary_usertype_names: Optional[List[str]] = None,
|
|
269
|
+
profile_custom_data: Optional[Dict[str, Any]] = None,
|
|
270
|
+
status_custom_data: Optional[Dict[str, Any]] = None,
|
|
271
|
+
created_by: Optional[str] = None,
|
|
272
|
+
use_firestore_defaults: bool = True,
|
|
273
|
+
password: Optional[str] = None,
|
|
274
|
+
email_verified: bool = False,
|
|
275
|
+
disabled: bool = False
|
|
276
|
+
) -> Tuple[Optional[UserProfile], Optional[UserStatus], Optional[str]]:
|
|
277
|
+
"""
|
|
278
|
+
Create a complete user with Firebase Auth, UserProfile, and UserStatus
|
|
279
|
+
|
|
280
|
+
This method creates the complete user including Firebase Auth user with custom claims.
|
|
281
|
+
It delegates to UserHolisticOperations for coordinated user creation.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Tuple of (UserProfile, UserStatus, error_message)
|
|
285
|
+
If successful, error_message is None
|
|
286
|
+
If failed, one or both models may be None with error_message describing the failure
|
|
287
|
+
"""
|
|
288
|
+
self.logger.info(f"Starting complete user creation for Email: {email}, Type: {primary_usertype_name}")
|
|
289
|
+
effective_creator = created_by or f"UserCoreService.create_user:email_{email}"
|
|
290
|
+
|
|
291
|
+
# Fetch user type defaults if enabled
|
|
292
|
+
usertype_defaults: Dict[str, Any] = {}
|
|
293
|
+
if use_firestore_defaults:
|
|
294
|
+
fetched_defaults = await self.fetch_user_defaults(primary_usertype_name)
|
|
295
|
+
if fetched_defaults:
|
|
296
|
+
usertype_defaults = fetched_defaults
|
|
297
|
+
else:
|
|
298
|
+
#### TODO : FIX HOW ERRORS ARE ACTUALLY REPORTED. SERVICEMON ?
|
|
299
|
+
self.logger.error(f"User defaults not found for '{primary_usertype_name}', proceeding with empty defaults")
|
|
300
|
+
raise UserCreationError(f"User defaults not found for '{primary_usertype_name}'")
|
|
301
|
+
|
|
302
|
+
# Prepare data with defaults
|
|
303
|
+
final_secondary_usertypes = secondary_usertype_names or usertype_defaults.get('default_secondary_usertypes', [])
|
|
304
|
+
final_organizations_uids = organizations_uids or set(usertype_defaults.get('default_organizations_uids', []))
|
|
305
|
+
|
|
306
|
+
# Extract custom data fields
|
|
307
|
+
iam_domain_permissions = (status_custom_data or {}).get('iam_domain_permissions')
|
|
308
|
+
if iam_domain_permissions is None and use_firestore_defaults:
|
|
309
|
+
iam_domain_permissions = usertype_defaults.get('default_iam_domain_permissions', {})
|
|
310
|
+
|
|
311
|
+
effective_initial_plan_id = initial_subscription_plan_id
|
|
312
|
+
if effective_initial_plan_id is None and use_firestore_defaults:
|
|
313
|
+
effective_initial_plan_id = usertype_defaults.get('default_subscription_plan_id')
|
|
314
|
+
|
|
315
|
+
# Credits from defaults or custom data
|
|
316
|
+
default_sbscrptn_credits = usertype_defaults.get('default_subscription_based_insight_credits', 0) if use_firestore_defaults else 0
|
|
317
|
+
default_extra_credits = usertype_defaults.get('default_extra_insight_credits', 0) if use_firestore_defaults else 0
|
|
318
|
+
default_voting_credits = usertype_defaults.get('default_voting_credits', 0) if use_firestore_defaults else 0
|
|
319
|
+
|
|
320
|
+
sbscrptn_based_insight_credits = (status_custom_data or {}).get('sbscrptn_based_insight_credits', default_sbscrptn_credits)
|
|
321
|
+
extra_insight_credits = (status_custom_data or {}).get('extra_insight_credits', default_extra_credits)
|
|
322
|
+
voting_credits = (status_custom_data or {}).get('voting_credits', default_voting_credits)
|
|
323
|
+
metadata = (status_custom_data or {}).get('metadata', {})
|
|
324
|
+
|
|
325
|
+
# Extract profile fields from profile_custom_data
|
|
326
|
+
first_name = (profile_custom_data or {}).get('first_name')
|
|
327
|
+
last_name = (profile_custom_data or {}).get('last_name')
|
|
328
|
+
username = (profile_custom_data or {}).get('username')
|
|
329
|
+
mobile = (profile_custom_data or {}).get('mobile')
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
# Delegate to UserHolisticOperations for complete user creation
|
|
333
|
+
created_user_uid, created_profile, created_status = await self.user_holistic_ops.create_user(
|
|
334
|
+
email=email,
|
|
335
|
+
primary_usertype=primary_usertype_name,
|
|
336
|
+
password=password,
|
|
337
|
+
organizations_uids=final_organizations_uids,
|
|
338
|
+
secondary_usertypes=final_secondary_usertypes,
|
|
339
|
+
first_name=first_name,
|
|
340
|
+
last_name=last_name,
|
|
341
|
+
username=username,
|
|
342
|
+
mobile=mobile,
|
|
343
|
+
provider_id=provider_id,
|
|
344
|
+
email_verified=email_verified,
|
|
345
|
+
disabled=disabled,
|
|
346
|
+
initial_subscription_plan_id=effective_initial_plan_id,
|
|
347
|
+
iam_domain_permissions=iam_domain_permissions,
|
|
348
|
+
sbscrptn_based_insight_credits=sbscrptn_based_insight_credits,
|
|
349
|
+
extra_insight_credits=extra_insight_credits,
|
|
350
|
+
voting_credits=voting_credits,
|
|
351
|
+
metadata=metadata,
|
|
352
|
+
created_by=effective_creator,
|
|
353
|
+
set_custom_claims=True # Always set custom claims for complete user creation
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
self.logger.info(f"Successfully created complete user {created_user_uid}")
|
|
357
|
+
return created_profile, created_status, None
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
error_msg = f"Failed to create complete user for email {email}: {str(e)}"
|
|
361
|
+
self.logger.error(error_msg, exc_info=True)
|
|
362
|
+
return None, None, error_msg
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
##########################################################################################
|
|
366
|
+
################################ Subscription Operations #################################
|
|
367
|
+
|
|
368
|
+
async def get_user_active_subscription(self, user_uid: str) -> Optional[Subscription]:
|
|
369
|
+
"""Get the user's currently active subscription"""
|
|
370
|
+
return await self.subscription_ops.get_user_active_subscription(user_uid)
|
|
371
|
+
|
|
372
|
+
async def change_user_subscription(self, user_uid: str, new_plan_id: str, source: Optional[str] = None) -> Optional[Subscription]:
|
|
373
|
+
"""Change a user's subscription to a new plan"""
|
|
374
|
+
return await self.subscription_ops.change_user_subscription(user_uid, new_plan_id, source)
|
|
375
|
+
|
|
376
|
+
async def cancel_user_subscription(self, user_uid: str, reason: Optional[str] = None, cancelled_by: Optional[str] = None) -> bool:
|
|
377
|
+
"""Cancel a user's active subscription"""
|
|
378
|
+
return await self.subscription_ops.cancel_user_subscription(user_uid, reason, cancelled_by)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
async def _apply_subscription_plan(self, user_uid: str, plan_id: str, source: str = "system_default_config") -> Subscription:
|
|
382
|
+
"""Apply subscription plan (backward compatibility)"""
|
|
383
|
+
return await self.subscription_ops.apply_subscription_plan(user_uid, plan_id, source)
|
|
384
|
+
|
|
385
|
+
##########################################################################################
|
|
386
|
+
################################ IAM Operations Operations #################################
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# Deletion Operations (delegated to UserHolisticOperations)
|
|
390
|
+
|
|
391
|
+
async def delete_user_account_docs(
|
|
392
|
+
self,
|
|
393
|
+
user_uid: str,
|
|
394
|
+
deleted_by: str = "system_deletion"
|
|
395
|
+
) -> Tuple[bool, bool, Optional[str]]:
|
|
396
|
+
"""Delete user documents (profile and status)"""
|
|
397
|
+
# Use the new user_holistic_ops for coordinated deletion
|
|
398
|
+
result = await self.user_holistic_ops.delete_user(
|
|
399
|
+
user_uid=user_uid,
|
|
400
|
+
delete_auth_user=False, # Only delete profile and status
|
|
401
|
+
delete_profile=True,
|
|
402
|
+
delete_status=True,
|
|
403
|
+
deleted_by=deleted_by
|
|
404
|
+
)
|
|
405
|
+
return (
|
|
406
|
+
result["profile_deleted_successfully"],
|
|
407
|
+
result["status_deleted_successfully"],
|
|
408
|
+
result["errors"][0] if result["errors"] else None
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
async def delete_user_holistically_with_auth(
|
|
412
|
+
self,
|
|
413
|
+
user_uid: str,
|
|
414
|
+
delete_auth_user: bool = True,
|
|
415
|
+
deleted_by: str = "system_full_deletion"
|
|
416
|
+
) -> Dict[str, Any]:
|
|
417
|
+
"""Complete user deletion including Firebase Auth deletion"""
|
|
418
|
+
return await self.user_holistic_ops.delete_user(user_uid, delete_auth_user, deleted_by=deleted_by)
|
|
419
|
+
|
|
420
|
+
async def batch_delete_user_account_docs(
|
|
421
|
+
self,
|
|
422
|
+
user_uids: List[str],
|
|
423
|
+
deleted_by: str = "system_batch_deletion"
|
|
424
|
+
) -> Dict[str, Tuple[bool, bool, Optional[str]]]:
|
|
425
|
+
"""Batch delete multiple users' documents"""
|
|
426
|
+
results_holistic = await self.user_holistic_ops.batch_delete_users(user_uids, delete_auth_user=False, deleted_by=deleted_by)
|
|
427
|
+
|
|
428
|
+
# Convert to expected format
|
|
429
|
+
converted_results = {}
|
|
430
|
+
for user_uid, result in results_holistic.items():
|
|
431
|
+
error_msg = result["errors"][0] if result["errors"] else None
|
|
432
|
+
converted_results[user_uid] = (
|
|
433
|
+
result["profile_deleted_successfully"],
|
|
434
|
+
result["status_deleted_successfully"],
|
|
435
|
+
error_msg
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
return converted_results
|
|
439
|
+
|
|
440
|
+
# Utility Methods
|
|
441
|
+
|
|
442
|
+
async def get_user_account_docs(self, user_uid: str) -> Tuple[Optional[UserProfile], Optional[UserStatus]]:
|
|
443
|
+
"""Get both user profile and status"""
|
|
444
|
+
return await self.user_account_ops.get_user_core_docs(user_uid)
|
|
445
|
+
|
|
446
|
+
async def user_core_docs_exist(self, user_uid: str) -> Tuple[bool, bool]:
|
|
447
|
+
"""Check if user profile and/or status exist"""
|
|
448
|
+
return await self.user_account_ops.user_core_docs_exist(user_uid)
|
|
449
|
+
|
|
450
|
+
async def validate_user_account_data(
|
|
451
|
+
self,
|
|
452
|
+
profile_data: Optional[Dict[str, Any]] = None,
|
|
453
|
+
status_data: Optional[Dict[str, Any]] = None
|
|
454
|
+
) -> Tuple[bool, List[str]]:
|
|
455
|
+
"""Validate user profile and status data without creating documents"""
|
|
456
|
+
return await self.user_account_ops.validate_user_core_data(profile_data, status_data)
|
|
457
|
+
|
|
458
|
+
# Enhanced Methods for Advanced Operations
|
|
459
|
+
|
|
460
|
+
async def get_users_by_usertype(
|
|
461
|
+
self,
|
|
462
|
+
primary_usertype: str,
|
|
463
|
+
limit: Optional[int] = None
|
|
464
|
+
) -> List[UserProfile]:
|
|
465
|
+
"""Get users by primary usertype (requires custom implementation)"""
|
|
466
|
+
# This would require a more advanced query system
|
|
467
|
+
# For now, we'll raise NotImplementedError to indicate this needs custom implementation
|
|
468
|
+
raise NotImplementedError("get_users_by_usertype requires custom query implementation")
|
|
469
|
+
|
|
470
|
+
async def get_users_by_organization(
|
|
471
|
+
self,
|
|
472
|
+
organization_uid: str,
|
|
473
|
+
limit: Optional[int] = None
|
|
474
|
+
) -> List[UserProfile]:
|
|
475
|
+
"""Get users by organization (requires custom implementation)"""
|
|
476
|
+
# This would require a more advanced query system
|
|
477
|
+
raise NotImplementedError("get_users_by_organization requires custom query implementation")
|
|
478
|
+
|
|
479
|
+
async def bulk_update_user_permissions(
|
|
480
|
+
self,
|
|
481
|
+
user_uids: List[str],
|
|
482
|
+
domain: str,
|
|
483
|
+
permissions_to_add: List[str],
|
|
484
|
+
permissions_to_remove: List[str],
|
|
485
|
+
updater_uid: str
|
|
486
|
+
) -> Dict[str, bool]:
|
|
487
|
+
"""Bulk update permissions for multiple users"""
|
|
488
|
+
results = {}
|
|
489
|
+
|
|
490
|
+
for user_uid in user_uids:
|
|
491
|
+
try:
|
|
492
|
+
# Add permissions
|
|
493
|
+
for permission in permissions_to_add:
|
|
494
|
+
await self.iam_ops.add_user_permission(
|
|
495
|
+
user_uid=user_uid,
|
|
496
|
+
domain=domain,
|
|
497
|
+
permission_name=permission,
|
|
498
|
+
iam_unit_type=IAMUnitType.GROUPS, # Default to groups
|
|
499
|
+
source=f"bulk_update_by_{updater_uid}",
|
|
500
|
+
updater_uid=updater_uid
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Remove permissions
|
|
504
|
+
for permission in permissions_to_remove:
|
|
505
|
+
await self.iam_ops.remove_user_permission(
|
|
506
|
+
user_uid=user_uid,
|
|
507
|
+
domain=domain,
|
|
508
|
+
permission_name=permission,
|
|
509
|
+
iam_unit_type=IAMUnitType.GROUPS, # Default to groups
|
|
510
|
+
updater_uid=updater_uid
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
results[user_uid] = True
|
|
514
|
+
|
|
515
|
+
except Exception as e:
|
|
516
|
+
self.logger.error(f"Failed to update permissions for user {user_uid}: {e}")
|
|
517
|
+
results[user_uid] = False
|
|
518
|
+
|
|
519
|
+
return results
|
|
520
|
+
|
|
521
|
+
# Statistics and Analytics Methods
|
|
522
|
+
|
|
523
|
+
async def get_user_statistics(self) -> Dict[str, int]:
|
|
524
|
+
"""Get basic user statistics (requires custom implementation)"""
|
|
525
|
+
# This would require aggregation queries
|
|
526
|
+
raise NotImplementedError("get_user_statistics requires custom aggregation implementation")
|
|
527
|
+
|
|
528
|
+
async def get_subscription_statistics(self) -> Dict[str, int]:
|
|
529
|
+
"""Get subscription statistics (requires custom implementation)"""
|
|
530
|
+
# This would require aggregation queries
|
|
531
|
+
raise NotImplementedError("get_subscription_statistics requires custom aggregation implementation")
|
|
532
|
+
|
|
533
|
+
# User 360 Operations (Complete User Lifecycle)
|
|
534
|
+
|
|
535
|
+
async def create_complete_user_with_auth(
|
|
536
|
+
self,
|
|
537
|
+
email: str,
|
|
538
|
+
primary_usertype: str,
|
|
539
|
+
password: Optional[str] = None,
|
|
540
|
+
organizations_uids: Optional[Set[str]] = None,
|
|
541
|
+
secondary_usertypes: Optional[List[str]] = None,
|
|
542
|
+
first_name: Optional[str] = None,
|
|
543
|
+
last_name: Optional[str] = None,
|
|
544
|
+
username: Optional[str] = None,
|
|
545
|
+
mobile: Optional[str] = None,
|
|
546
|
+
provider_id: str = "password",
|
|
547
|
+
email_verified: bool = False,
|
|
548
|
+
disabled: bool = False,
|
|
549
|
+
initial_subscription_plan_id: Optional[str] = None,
|
|
550
|
+
iam_domain_permissions: Optional[Dict[str, Dict[str, Dict[str, IAMUnitRefAssignment]]]] = None,
|
|
551
|
+
sbscrptn_based_insight_credits: int = 0,
|
|
552
|
+
extra_insight_credits: int = 0,
|
|
553
|
+
voting_credits: int = 0,
|
|
554
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
555
|
+
created_by: Optional[str] = None,
|
|
556
|
+
set_custom_claims: bool = True
|
|
557
|
+
) -> Tuple[str, UserProfile, UserStatus]:
|
|
558
|
+
"""
|
|
559
|
+
Create complete user with Firebase Auth, UserProfile, and UserStatus
|
|
560
|
+
|
|
561
|
+
This method directly delegates to UserHolisticOperations.create_user
|
|
562
|
+
"""
|
|
563
|
+
return await self.user_holistic_ops.create_user(
|
|
564
|
+
email=email,
|
|
565
|
+
primary_usertype=primary_usertype,
|
|
566
|
+
password=password,
|
|
567
|
+
organizations_uids=organizations_uids,
|
|
568
|
+
secondary_usertypes=secondary_usertypes,
|
|
569
|
+
first_name=first_name,
|
|
570
|
+
last_name=last_name,
|
|
571
|
+
username=username,
|
|
572
|
+
mobile=mobile,
|
|
573
|
+
provider_id=provider_id,
|
|
574
|
+
email_verified=email_verified,
|
|
575
|
+
disabled=disabled,
|
|
576
|
+
initial_subscription_plan_id=initial_subscription_plan_id,
|
|
577
|
+
iam_domain_permissions=iam_domain_permissions,
|
|
578
|
+
sbscrptn_based_insight_credits=sbscrptn_based_insight_credits,
|
|
579
|
+
extra_insight_credits=extra_insight_credits,
|
|
580
|
+
voting_credits=voting_credits,
|
|
581
|
+
metadata=metadata,
|
|
582
|
+
created_by=created_by,
|
|
583
|
+
set_custom_claims=set_custom_claims
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
async def delete_complete_user(
|
|
587
|
+
self,
|
|
588
|
+
user_uid: str,
|
|
589
|
+
delete_auth_user: bool = True,
|
|
590
|
+
delete_profile: bool = True,
|
|
591
|
+
delete_status: bool = True,
|
|
592
|
+
deleted_by: str = "system_complete_deletion"
|
|
593
|
+
) -> Dict[str, Any]:
|
|
594
|
+
"""Delete complete user including Firebase Auth, UserProfile, and UserStatus"""
|
|
595
|
+
return await self.user_holistic_ops.delete_user(
|
|
596
|
+
user_uid=user_uid,
|
|
597
|
+
delete_auth_user=delete_auth_user,
|
|
598
|
+
delete_profile=delete_profile,
|
|
599
|
+
delete_status=delete_status,
|
|
600
|
+
deleted_by=deleted_by
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
async def user_exists_complete(self, user_uid: str) -> Dict[str, bool]:
|
|
604
|
+
"""Check if complete user exists (Auth, Profile, Status)"""
|
|
605
|
+
return await self.user_holistic_ops.user_exists_fully(user_uid)
|
|
606
|
+
|
|
607
|
+
async def validate_complete_user(self, user_uid: str) -> Dict[str, Any]:
|
|
608
|
+
"""Validate complete user integrity"""
|
|
609
|
+
return await self.user_holistic_ops.validate_user_full_existance(user_uid)
|
|
610
|
+
|
|
611
|
+
# User Auth Operations
|
|
612
|
+
|
|
613
|
+
async def create_userauth(
|
|
614
|
+
self,
|
|
615
|
+
email: str,
|
|
616
|
+
password: Optional[str] = None,
|
|
617
|
+
email_verified: bool = False,
|
|
618
|
+
disabled: bool = False,
|
|
619
|
+
display_name: Optional[str] = None,
|
|
620
|
+
phone_number: Optional[str] = None,
|
|
621
|
+
custom_claims: Optional[Dict[str, Any]] = None
|
|
622
|
+
) -> str:
|
|
623
|
+
"""Create Firebase Auth user"""
|
|
624
|
+
user_auth = UserAuth(
|
|
625
|
+
email=email,
|
|
626
|
+
password=password,
|
|
627
|
+
email_verified=email_verified,
|
|
628
|
+
disabled=disabled,
|
|
629
|
+
phone_number=phone_number,
|
|
630
|
+
custom_claims=custom_claims
|
|
631
|
+
)
|
|
632
|
+
return await self.user_auth_ops.create_userauth(user_auth)
|
|
633
|
+
|
|
634
|
+
async def delete_userauth(self, user_uid: str) -> bool:
|
|
635
|
+
"""Delete Firebase Auth user"""
|
|
636
|
+
return await self.user_auth_ops.delete_userauth(user_uid)
|
|
637
|
+
|
|
638
|
+
async def userauth_exists(self, user_uid: str) -> bool:
|
|
639
|
+
"""Check if Firebase Auth user exists"""
|
|
640
|
+
return await self.user_auth_ops.userauth_exists(user_uid)
|
|
641
|
+
|
|
642
|
+
# Custom Claims Operations (delegated to user_auth_ops)
|
|
643
|
+
|
|
644
|
+
async def set_userauth_custom_claims(
|
|
645
|
+
self,
|
|
646
|
+
user_uid: str,
|
|
647
|
+
custom_claims: Dict[str, Any],
|
|
648
|
+
merge_with_existing: bool = False
|
|
649
|
+
) -> bool:
|
|
650
|
+
"""Set Firebase Auth custom claims for a user with optional merging"""
|
|
651
|
+
return await self.user_auth_ops.set_userauth_custom_claims(user_uid, custom_claims, merge_with_existing)
|