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
@@ -1,384 +0,0 @@
1
- """
2
- Subscription Management Operations - Handle user subscriptions and related operations
3
- """
4
- import logging
5
- from typing import Dict, Any, Optional, List
6
- from datetime import datetime, timezone
7
- from google.cloud import firestore
8
- from pydantic import BaseModel
9
-
10
- from ...models.subscription import Subscription
11
- from ...exceptions import ResourceNotFoundError
12
- from ..base import BaseFirestoreService
13
- from ipulse_shared_base_ftredge.enums import SubscriptionPlan, SubscriptionStatus
14
- from ...exceptions import SubscriptionError, UserStatusError
15
-
16
-
17
- # Model for subscription plan data from Firestore
18
- class SubscriptionPlanDocument(BaseModel):
19
- """Model for subscription plan documents stored in Firestore"""
20
- id: str
21
- plan_name: str
22
- plan_version: int
23
- display_name: str
24
- description: str
25
- default_iam_domain_permissions: Optional[Dict[str, Dict[str, List[str]]]] = None
26
- subscription_based_insight_credits_per_update: Optional[int] = None
27
- subscription_based_insight_credits_update_freq_h: Optional[int] = None
28
- extra_insight_credits_per_cycle: Optional[int] = None
29
- voting_credits_per_update: Optional[int] = None
30
- voting_credits_update_freq_h: Optional[int] = None
31
- plan_validity_cycle_length: Optional[int] = None
32
- plan_validity_cycle_unit: Optional[str] = None
33
- plan_per_cycle_price_usd: Optional[float] = None
34
- plan_auto_renewal: Optional[bool] = None
35
- fallback_plan_id_if_current_plan_expired: Optional[str] = None
36
-
37
-
38
- class SubscriptionManagementOperations:
39
- """
40
- Handles subscription-related operations for users
41
- """
42
-
43
- def __init__(
44
- self,
45
- firestore_client: firestore.Client,
46
- user_account_ops, # UserManagementOperations instance
47
- logger: Optional[logging.Logger] = None,
48
- timeout: float = 10.0,
49
- subscription_plans_collection: str = "papp_core_configs_subscriptionplans_defaults"
50
- ):
51
- self.db = firestore_client
52
- self.user_account_ops = user_account_ops
53
- self.logger = logger or logging.getLogger(__name__)
54
- self.timeout = timeout
55
-
56
- # Initialize subscription plans DB service
57
- self._subscription_plans_db_service = BaseFirestoreService[SubscriptionPlanDocument](
58
- db=self.db,
59
- collection_name=subscription_plans_collection,
60
- resource_type="SubscriptionPlan",
61
- logger=self.logger,
62
- timeout=self.timeout
63
- )
64
-
65
- async def fetch_subscription_plan_details(self, plan_id: str) -> Optional[SubscriptionPlanDocument]:
66
- """Fetch subscription plan details from Firestore"""
67
- try:
68
- plan_data_dict = await self._subscription_plans_db_service.get_document(plan_id)
69
- if not plan_data_dict:
70
- self.logger.warning(f"Subscription plan with ID '{plan_id}' not found")
71
- return None
72
-
73
- # Add the plan_id to the dict if it's not already there
74
- plan_data_dict.setdefault('id', plan_id)
75
- plan_doc = SubscriptionPlanDocument(**plan_data_dict)
76
- self.logger.info(f"Successfully fetched subscription plan details for plan_id: {plan_id}")
77
- return plan_doc
78
-
79
- except ResourceNotFoundError:
80
- self.logger.warning(f"Subscription plan '{plan_id}' not found")
81
- return None
82
- except Exception as e:
83
- self.logger.error(f"Error fetching subscription plan details for {plan_id}: {e}", exc_info=True)
84
- raise SubscriptionError(
85
- detail=f"Failed to fetch subscription plan: {str(e)}",
86
- plan_id=plan_id,
87
- operation="fetch_subscription_plan_details",
88
- original_error=e
89
- )
90
-
91
- async def apply_subscription_plan(
92
- self,
93
- user_uid: str,
94
- plan_id: str,
95
- source: str = "system_default_config"
96
- ) -> Subscription:
97
- """Apply a subscription plan to a user"""
98
- user_status = await self.user_account_ops.get_userstatus(user_uid)
99
- if not user_status:
100
- raise UserStatusError(
101
- detail=f"UserStatus not found for user_uid {user_uid}",
102
- user_uid=user_uid,
103
- operation="apply_subscription_plan"
104
- )
105
-
106
- plan_doc = await self.fetch_subscription_plan_details(plan_id)
107
- if not plan_doc:
108
- raise SubscriptionError(
109
- detail=f"Subscription plan {plan_id} not found",
110
- user_uid=user_uid,
111
- plan_id=plan_id,
112
- operation="apply_subscription_plan"
113
- )
114
-
115
- # Validate plan data
116
- plan_name_str = plan_doc.plan_name
117
- if not plan_name_str:
118
- raise SubscriptionError(
119
- detail="Plan name missing in plan details",
120
- user_uid=user_uid,
121
- plan_id=plan_id,
122
- operation="apply_subscription_plan"
123
- )
124
-
125
- try:
126
- plan_name_enum = SubscriptionPlan(plan_name_str)
127
- except ValueError as e:
128
- raise SubscriptionError(
129
- detail=f"Invalid plan name '{plan_name_str}': {str(e)}",
130
- user_uid=user_uid,
131
- plan_id=plan_id,
132
- operation="apply_subscription_plan",
133
- original_error=e
134
- )
135
-
136
- # Validate required fields
137
- plan_version = plan_doc.plan_version
138
- validity_length = plan_doc.plan_validity_cycle_length
139
- validity_unit = plan_doc.plan_validity_cycle_unit
140
-
141
- if not all([
142
- plan_version is not None and isinstance(plan_version, int),
143
- validity_length is not None and isinstance(validity_length, int),
144
- validity_unit is not None and isinstance(validity_unit, str)
145
- ]):
146
- raise SubscriptionError(
147
- detail="Missing or invalid subscription duration fields",
148
- user_uid=user_uid,
149
- plan_id=plan_id,
150
- operation="apply_subscription_plan"
151
- )
152
-
153
- # Create subscription
154
- start_date = datetime.now(timezone.utc)
155
- # At this point, we know validity_length and validity_unit are not None
156
- # Type assertions to satisfy type checker
157
- assert validity_length is not None and validity_unit is not None
158
- end_date = Subscription.calculate_cycle_end_date(start_date, validity_length, validity_unit)
159
-
160
- try:
161
- new_subscription = Subscription(
162
- plan_name=plan_name_enum,
163
- plan_version=int(plan_version),
164
- plan_id=plan_id,
165
- cycle_start_date=start_date,
166
- cycle_end_date=end_date,
167
- validity_time_length=validity_length, # We already validated it's not None
168
- validity_time_unit=validity_unit, # We already validated it's not None
169
- auto_renew=plan_doc.plan_auto_renewal if plan_doc.plan_auto_renewal is not None else False,
170
- status=SubscriptionStatus.ACTIVE,
171
- default_iam_domain_permissions=plan_doc.default_iam_domain_permissions or {},
172
- fallback_plan_id=plan_doc.fallback_plan_id_if_current_plan_expired,
173
- price_paid_usd=float(plan_doc.plan_per_cycle_price_usd if plan_doc.plan_per_cycle_price_usd is not None else 0.0),
174
- created_by=source,
175
- updated_by=source,
176
- subscription_based_insight_credits_per_update=int(plan_doc.subscription_based_insight_credits_per_update if plan_doc.subscription_based_insight_credits_per_update is not None else 0),
177
- subscription_based_insight_credits_update_freq_h=int(plan_doc.subscription_based_insight_credits_update_freq_h if plan_doc.subscription_based_insight_credits_update_freq_h is not None else 24),
178
- extra_insight_credits_per_cycle=int(plan_doc.extra_insight_credits_per_cycle if plan_doc.extra_insight_credits_per_cycle is not None else 0),
179
- voting_credits_per_update=int(plan_doc.voting_credits_per_update if plan_doc.voting_credits_per_update is not None else 0),
180
- voting_credits_update_freq_h=int(plan_doc.voting_credits_update_freq_h if plan_doc.voting_credits_update_freq_h is not None else 744),
181
- )
182
- except Exception as e:
183
- raise SubscriptionError(
184
- detail=f"Failed to create subscription object: {str(e)}",
185
- user_uid=user_uid,
186
- plan_id=plan_id,
187
- operation="apply_subscription_plan",
188
- original_error=e
189
- )
190
-
191
- # Apply subscription to user status
192
- try:
193
- user_status.apply_subscription(new_subscription)
194
- user_status.updated_at = datetime.now(timezone.utc)
195
- user_status.updated_by = f"SubscriptionManagement.apply_plan:{source}"
196
-
197
- await self.user_account_ops.update_userstatus(
198
- user_uid=user_uid,
199
- status_data=user_status.model_dump(exclude_none=True),
200
- updater_uid=f"SubscriptionManagement:{source}"
201
- )
202
-
203
- self.logger.info(f"Successfully applied subscription plan {plan_id} for user {user_uid}")
204
- return new_subscription
205
-
206
- except Exception as e:
207
- self.logger.error(f"Failed to apply subscription to user status: {e}", exc_info=True)
208
- raise SubscriptionError(
209
- detail=f"Failed to apply subscription to user: {str(e)}",
210
- user_uid=user_uid,
211
- plan_id=plan_id,
212
- operation="apply_subscription_plan",
213
- original_error=e
214
- )
215
-
216
- async def get_user_active_subscription(self, user_uid: str) -> Optional[Subscription]:
217
- """Get the user's currently active subscription"""
218
- user_status = await self.user_account_ops.get_userstatus(user_uid)
219
- if user_status and user_status.active_subscription and user_status.active_subscription.is_active():
220
- self.logger.info(f"Active subscription found for user {user_uid}: {user_status.active_subscription.plan_id}")
221
- return user_status.active_subscription
222
-
223
- self.logger.info(f"No active subscription found for user {user_uid}")
224
- return None
225
-
226
- async def change_user_subscription(
227
- self,
228
- user_uid: str,
229
- new_plan_id: str,
230
- source: Optional[str] = None
231
- ) -> Optional[Subscription]:
232
- """Change a user's subscription to a new plan"""
233
- self.logger.info(f"Attempting to change subscription for user {user_uid} to plan {new_plan_id}")
234
-
235
- user_status = await self.user_account_ops.get_userstatus(user_uid)
236
- if not user_status:
237
- raise UserStatusError(
238
- detail=f"UserStatus not found for user_uid {user_uid}",
239
- user_uid=user_uid,
240
- operation="change_user_subscription"
241
- )
242
-
243
- effective_source = source or f"user_initiated_change_uid_{user_uid}"
244
-
245
- # Archive current subscription if exists
246
- if user_status.active_subscription:
247
- self.logger.info(f"Archiving current active subscription {user_status.active_subscription.plan_id} for user {user_uid}")
248
- old_subscription = user_status.active_subscription
249
-
250
- try:
251
- old_subscription_dict = old_subscription.model_dump()
252
- old_subscription_dict['status'] = SubscriptionStatus.INACTIVE
253
- old_subscription_dict['updated_at'] = datetime.now(timezone.utc)
254
- old_subscription_dict['updated_by'] = f"superseded_by_{new_plan_id}_via_{effective_source}"
255
-
256
- modified_old_subscription = Subscription(**old_subscription_dict)
257
-
258
- if user_status.subscriptions_history is None:
259
- user_status.subscriptions_history = {}
260
- user_status.subscriptions_history[modified_old_subscription.uuid] = modified_old_subscription
261
-
262
- except Exception as e:
263
- self.logger.error(f"Failed to archive old subscription: {e}", exc_info=True)
264
- raise SubscriptionError(
265
- detail=f"Failed to archive old subscription: {str(e)}",
266
- user_uid=user_uid,
267
- plan_id=new_plan_id,
268
- operation="change_user_subscription",
269
- original_error=e
270
- )
271
-
272
- # Apply new subscription
273
- try:
274
- new_subscription_obj = await self.apply_subscription_plan(user_uid, new_plan_id, source=effective_source)
275
- self.logger.info(f"Successfully changed subscription for user {user_uid} to {new_plan_id}")
276
- return new_subscription_obj
277
- except Exception as e:
278
- self.logger.error(f"Error changing subscription for user {user_uid} to {new_plan_id}: {e}", exc_info=True)
279
- raise
280
-
281
- async def cancel_user_subscription(
282
- self,
283
- user_uid: str,
284
- reason: Optional[str] = None,
285
- cancelled_by: Optional[str] = None
286
- ) -> bool:
287
- """Cancel a user's active subscription"""
288
- self.logger.info(f"Attempting to cancel subscription for user {user_uid}. Reason: {reason}")
289
-
290
- user_status = await self.user_account_ops.get_userstatus(user_uid)
291
- if not user_status:
292
- raise UserStatusError(
293
- detail=f"UserStatus not found for user_uid {user_uid}",
294
- user_uid=user_uid,
295
- operation="cancel_user_subscription"
296
- )
297
-
298
- effective_canceller = cancelled_by or f"SubscriptionManagement.cancel:{reason or 'not_specified'}"
299
-
300
- if user_status.active_subscription and user_status.active_subscription.status == SubscriptionStatus.ACTIVE:
301
- try:
302
- active_sub_dict = user_status.active_subscription.model_dump()
303
-
304
- self.logger.info(f"Cancelling active subscription {active_sub_dict['plan_id']} for user {user_uid}")
305
-
306
- active_sub_dict['status'] = SubscriptionStatus.CANCELLED
307
- active_sub_dict['auto_renew'] = False
308
- active_sub_dict['updated_at'] = datetime.now(timezone.utc)
309
- active_sub_dict['updated_by'] = effective_canceller
310
-
311
- cancelled_subscription = Subscription(**active_sub_dict)
312
-
313
- if user_status.subscriptions_history is None:
314
- user_status.subscriptions_history = {}
315
- user_status.subscriptions_history[cancelled_subscription.uuid] = cancelled_subscription
316
-
317
- user_status.revoke_subscription()
318
- user_status.updated_at = datetime.now(timezone.utc)
319
- user_status.updated_by = effective_canceller
320
-
321
- await self.user_account_ops.update_userstatus(
322
- user_uid=user_uid,
323
- status_data=user_status.model_dump(exclude_none=True),
324
- updater_uid=effective_canceller
325
- )
326
-
327
- self.logger.info(f"Successfully cancelled subscription for user {user_uid}")
328
- return True
329
-
330
- except Exception as e:
331
- self.logger.error(f"Failed to cancel subscription: {e}", exc_info=True)
332
- raise SubscriptionError(
333
- detail=f"Failed to cancel subscription: {str(e)}",
334
- user_uid=user_uid,
335
- operation="cancel_user_subscription",
336
- original_error=e
337
- )
338
- else:
339
- self.logger.info(f"No active subscription to cancel for user {user_uid}")
340
- return False
341
-
342
- async def get_subscription_history(self, user_uid: str) -> Dict[str, Subscription]:
343
- """Get the user's subscription history"""
344
- user_status = await self.user_account_ops.get_userstatus(user_uid)
345
- if user_status and user_status.subscriptions_history:
346
- return user_status.subscriptions_history
347
- return {}
348
-
349
- async def get_all_subscription_plans(self) -> List[SubscriptionPlanDocument]:
350
- """Get all available subscription plans"""
351
- try:
352
- # This would require implementing a list_documents method in BaseFirestoreService
353
- # For now, we'll implement a basic version
354
- collection_ref = self.db.collection(self._subscription_plans_db_service.collection_name)
355
- docs = collection_ref.stream()
356
-
357
- plans = []
358
- for doc in docs:
359
- plan_data = doc.to_dict()
360
- plan_data['id'] = doc.id
361
- try:
362
- plan = SubscriptionPlanDocument(**plan_data)
363
- plans.append(plan)
364
- except Exception as e:
365
- self.logger.warning(f"Failed to parse subscription plan {doc.id}: {e}")
366
- continue
367
-
368
- return plans
369
-
370
- except Exception as e:
371
- self.logger.error(f"Error fetching subscription plans: {e}", exc_info=True)
372
- raise SubscriptionError(
373
- detail=f"Failed to fetch subscription plans: {str(e)}",
374
- operation="get_all_subscription_plans",
375
- original_error=e
376
- )
377
-
378
- async def validate_subscription_plan(self, plan_id: str) -> bool:
379
- """Validate if a subscription plan exists and is valid"""
380
- try:
381
- plan = await self.fetch_subscription_plan_details(plan_id)
382
- return plan is not None
383
- except Exception:
384
- return False