ipulse-shared-core-ftredge 19.0.1__py3-none-any.whl → 22.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 (47) hide show
  1. ipulse_shared_core_ftredge/cache/shared_cache.py +1 -2
  2. ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +4 -4
  3. ipulse_shared_core_ftredge/exceptions/base_exceptions.py +23 -0
  4. ipulse_shared_core_ftredge/models/__init__.py +3 -7
  5. ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
  6. ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
  7. ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +273 -0
  8. ipulse_shared_core_ftredge/models/catalog/usertype.py +170 -0
  9. ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
  10. ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
  11. ipulse_shared_core_ftredge/models/{subscription.py → user/user_subscription.py} +66 -20
  12. ipulse_shared_core_ftredge/models/{user_auth.py → user/userauth.py} +19 -10
  13. ipulse_shared_core_ftredge/models/{user_profile.py → user/userprofile.py} +53 -21
  14. ipulse_shared_core_ftredge/models/user/userstatus.py +430 -0
  15. ipulse_shared_core_ftredge/monitoring/__init__.py +2 -2
  16. ipulse_shared_core_ftredge/monitoring/tracemon.py +320 -0
  17. ipulse_shared_core_ftredge/services/__init__.py +11 -13
  18. ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
  19. ipulse_shared_core_ftredge/services/base/base_firestore_service.py +73 -14
  20. ipulse_shared_core_ftredge/services/{cache_aware_firestore_service.py → base/cache_aware_firestore_service.py} +46 -32
  21. ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
  22. ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +273 -0
  23. ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +307 -0
  24. ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
  25. ipulse_shared_core_ftredge/services/user/__init__.py +5 -25
  26. ipulse_shared_core_ftredge/services/user/firebase_auth_admin_helpers.py +160 -0
  27. ipulse_shared_core_ftredge/services/user/user_core_service.py +423 -515
  28. ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +726 -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 +484 -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 +212 -0
  34. ipulse_shared_core_ftredge/services/{charging_service.py → user_charging_service.py} +9 -9
  35. {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/METADATA +3 -4
  36. ipulse_shared_core_ftredge-22.1.1.dist-info/RECORD +51 -0
  37. ipulse_shared_core_ftredge/models/user_status.py +0 -495
  38. ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -483
  39. ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
  40. ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
  41. ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
  42. ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
  43. ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
  44. ipulse_shared_core_ftredge-19.0.1.dist-info/RECORD +0 -41
  45. {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/WHEEL +0 -0
  46. {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/licenses/LICENCE +0 -0
  47. {ipulse_shared_core_ftredge-19.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/top_level.txt +0 -0
@@ -1,495 +0,0 @@
1
- """ User Status model for tracking user subscription and access rights. """
2
- from datetime import datetime, timezone
3
- from dateutil.relativedelta import relativedelta # Add missing import
4
- from typing import Set, Optional, Dict, List, ClassVar, Any
5
- from pydantic import Field, ConfigDict, field_validator, computed_field, BaseModel, model_validator
6
- from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject,SubscriptionPlan, SubscriptionStatus
7
- from ipulse_shared_base_ftredge.enums.enums_iam import IAMUnitType
8
- from .subscription import Subscription
9
- from .base_data_model import BaseDataModel
10
-
11
- # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
12
- # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
13
-
14
- class IAMUnitRefAssignment(BaseModel):
15
- """
16
- Represents an IAM assignment (for groups, roles, or permissions) with expiration tracking.
17
- """
18
- # Identity of the IAM unit reference
19
- iam_unit_ref: str = Field(
20
- ...,
21
- description="Reference name of the IAM unit (e.g., 'base_subscription_group')"
22
- )
23
-
24
- # Expiration tracking
25
- expires_at: Optional[datetime] = Field(
26
- default=None,
27
- description="When this assignment expires (null for permanent)"
28
- )
29
-
30
- source: str = Field(
31
- ...,
32
- description="Source of this assignment (subscription plan ID, 'system_default', etc.)"
33
- )
34
-
35
- def is_valid(self) -> bool:
36
- """Check if the assignment is currently valid (not expired)."""
37
- if self.expires_at is None:
38
- return True
39
- return datetime.now(timezone.utc) <= self.expires_at
40
-
41
- ############################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! #################################
42
- class UserStatus(BaseDataModel):
43
- """
44
- User Status model for tracking user subscription and access rights.
45
- """
46
- # Set frozen=False to allow modification of attributes
47
- model_config = ConfigDict(frozen=False, extra="forbid")
48
-
49
- # Class constants
50
- VERSION: ClassVar[float] = 5.1 # Incremented version for removing computed fields
51
- DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
52
- OBJ_REF: ClassVar[str] = "userstatus"
53
-
54
- # Centralized collection name and document ID prefix
55
- COLLECTION_NAME: ClassVar[str] = "papp_core_user_userstatuss"
56
-
57
-
58
- # System-managed fields
59
- schema_version: float = Field(
60
- default=VERSION,
61
- frozen=True,
62
- description="Version of this Class == version of DB Schema"
63
- )
64
-
65
- id: str = Field(
66
- ..., # Still required, but will be auto-generated by model_validator if not provided
67
- description=f"User ID, format: {OBJ_REF}_user_uid"
68
- )
69
-
70
- user_uid: str = Field(
71
- ...,
72
- description="User UID from Firebase Auth"
73
- )
74
-
75
- # Added organizations field for consistency with UserProfile
76
- organizations_uids: Set[str] = Field(
77
- default_factory=set,
78
- description="Organization UIDs the user belongs to"
79
- )
80
-
81
- # Enhanced IAM permissions structure with expiration - update to use string for enum keys
82
- iam_domain_permissions: Dict[str, Dict[str, Dict[str, IAMUnitRefAssignment]]] = Field(
83
- ...,
84
- description="Domain -> IAM unit type (groups/roles/permissions) -> unit reference name -> assignment details (with expiration)"
85
- )
86
-
87
- # Subscription Management - Single active subscription instead of dictionary
88
- subscriptions_history: Dict[str, Subscription] = Field(
89
- default_factory=dict, # Initialize with empty dict
90
- description="Dictionary of user\'s past recent subscriptions, keyed by subscription ID"
91
- )
92
-
93
- # Changed from dictionary to single Optional subscription
94
- active_subscription: Optional[Subscription] = Field(
95
- default=None,
96
- description="The user's currently active subscription, if any"
97
- )
98
-
99
- # Credit management fields
100
- sbscrptn_based_insight_credits: int = Field(
101
- ...,
102
- description="Subscription-based insight credits (expire with subscription)"
103
- )
104
-
105
- sbscrptn_based_insight_credits_updtd_on: datetime = Field(
106
- default_factory=lambda: datetime.now(timezone.utc),
107
- description="Last update timestamp for subscription credits"
108
- )
109
-
110
- extra_insight_credits: int = Field(
111
- ...,
112
- description="Additional purchased insight credits (non-expiring)"
113
- )
114
-
115
- extra_insight_credits_updtd_on: datetime = Field(
116
- default_factory=lambda: datetime.now(timezone.utc),
117
- description="Last update timestamp for extra credits"
118
- )
119
-
120
- voting_credits: int = Field(
121
- ...,
122
- description="Voting credits for user"
123
- )
124
-
125
- voting_credits_updtd_on: datetime = Field(
126
- default_factory=lambda: datetime.now(timezone.utc),
127
- description="Last update timestamp for voting credits"
128
- )
129
-
130
- metadata: Dict[str, Any] = Field(
131
- default_factory=dict,
132
- description="Additional metadata for the user status"
133
- )
134
-
135
- @model_validator(mode='before')
136
- @classmethod
137
- def ensure_id_exists(cls, data: Dict[str, Any]) -> Dict[str, Any]:
138
- """
139
- Ensures the id field exists by generating it from user_uid if needed.
140
- This runs BEFORE validation, guaranteeing id will be present for validators.
141
- """
142
- if not isinstance(data, dict):
143
- return data
144
-
145
- # If id is already in the data, leave it alone
146
- if 'id' in data and data['id']:
147
- return data
148
-
149
- # If user_uid exists but id doesn't, generate id from user_uid
150
- if 'user_uid' in data and data['user_uid']:
151
- data['id'] = f"{cls.OBJ_REF}_{data['user_uid']}"
152
-
153
- return data
154
-
155
- # Utility methods for subscription management - updated for single subscription
156
-
157
- def get_active_subscription(self) -> Optional[Subscription]:
158
- """Get the currently active subscription if it exists."""
159
- return self.active_subscription
160
-
161
- def get_subscription_plan_name(self) -> Optional[SubscriptionPlan]:
162
- """Get the current subscription plan name."""
163
- if self.active_subscription:
164
- return self.active_subscription.plan_name
165
- return None
166
-
167
- def has_valid_permission_type_for_domain(self, domain: str, iam_unit_type: IAMUnitType = IAMUnitType.GROUPS) -> bool:
168
- """Check if the user has any valid IAM permissions of specified type for the domain."""
169
- if domain not in self.iam_domain_permissions:
170
- return False
171
-
172
- # Update to use string value from enum
173
- domain_permissions = self.iam_domain_permissions[domain].get(iam_unit_type.value, {})
174
- return any(assignment.is_valid() for assignment in domain_permissions.values())
175
-
176
- def has_valid_groups_for_domain(self, domain: str) -> bool:
177
- """Check if the user has any valid IAM groups for the specified domain (legacy method)."""
178
- return self.has_valid_permission_type_for_domain(domain, IAMUnitType.GROUPS)
179
-
180
- def get_valid_permissions_for_domain(self, domain: str, iam_unit_type: IAMUnitType = IAMUnitType.GROUPS) -> List[str]:
181
- """Get a list of valid (non-expired) permission names of specified type for the domain."""
182
- if domain not in self.iam_domain_permissions:
183
- return []
184
-
185
- # Update to use string value from enum
186
- domain_permissions = self.iam_domain_permissions[domain].get(iam_unit_type.value, {})
187
- return [
188
- iam_unit_ref
189
- for iam_unit_ref, assignment in domain_permissions.items()
190
- if assignment.is_valid()
191
- ]
192
-
193
- def get_valid_groups_for_domain(self, domain: str) -> List[str]:
194
- """Get a list of valid (non-expired) group names for the domain (legacy method)."""
195
- return self.get_valid_permissions_for_domain(domain, IAMUnitType.GROUPS)
196
-
197
- def add_iam_unit_ref_assignment(
198
- self,
199
- domain: str,
200
- iam_unit_ref: str,
201
- iam_unit_type: IAMUnitType,
202
- source: str,
203
- expires_at: Optional[datetime] = None
204
- ) -> None:
205
- """
206
- Add a permission assignment to the user's IAM domain permissions.
207
-
208
- Args:
209
- domain: The domain for the permission (e.g., 'papp')
210
- iam_unit_ref: The name/identifier of the permission to add
211
- iam_unit_type: Type of IAM assignment (GROUP, ROLE, PERMISSION)
212
- source: Source identifier for this assignment (e.g., subscription ID)
213
- expires_at: Optional expiration date
214
- """
215
- # Ensure domain exists
216
- if domain not in self.iam_domain_permissions:
217
- self.iam_domain_permissions[domain] = {}
218
-
219
- # Ensure permission type section exists - use string value from enum
220
- if iam_unit_type.value not in self.iam_domain_permissions[domain]:
221
- self.iam_domain_permissions[domain][iam_unit_type.value] = {}
222
-
223
- # Create new assignment
224
- assignment = IAMUnitRefAssignment(
225
- iam_unit_ref=iam_unit_ref,
226
- source=source,
227
- expires_at=expires_at
228
- )
229
-
230
- # Add the permission - use string value from enum
231
- self.iam_domain_permissions[domain][iam_unit_type.value][iam_unit_ref] = assignment
232
-
233
- def add_group_assignment(
234
- self,
235
- domain: str,
236
- group_name: str,
237
- source: str,
238
- expires_at: Optional[datetime] = None
239
- ) -> None:
240
- """
241
- Add a group assignment to the user's IAM groups (legacy method).
242
-
243
- Args:
244
- domain: The domain for the group (e.g., 'papp')
245
- group_name: The name of the group to add
246
- source: Source identifier for this assignment (e.g., subscription ID)
247
- expires_at: Optional expiration date
248
- """
249
- self.add_iam_unit_ref_assignment(domain, group_name, IAMUnitType.GROUPS, source, expires_at)
250
-
251
- def remove_expired_iam_unit_refs(self, iam_unit_type: Optional[IAMUnitType] = None) -> int:
252
- """
253
- Remove all expired permission assignments of a specific type or all types.
254
-
255
- Args:
256
- iam_unit_type: If provided, only remove this type of permissions
257
-
258
- Returns:
259
- Number of removed permission assignments
260
- """
261
- now = datetime.now(timezone.utc)
262
- removed_count = 0
263
-
264
- # Create a deep copy of domains to avoid modification during iteration
265
- domains = list(self.iam_domain_permissions.keys())
266
-
267
- for domain in domains:
268
- # If iam_unit_type is specified, only check that type
269
- if iam_unit_type:
270
- unit_type_value = iam_unit_type.value
271
- if unit_type_value not in self.iam_domain_permissions[domain]:
272
- continue
273
-
274
- iam_unit_types_to_check = [unit_type_value]
275
- else:
276
- # Check all permission types
277
- iam_unit_types_to_check = list(self.iam_domain_permissions[domain].keys())
278
-
279
- # Process each permission type
280
- for perm_type in iam_unit_types_to_check:
281
- # Create a list of permissions to remove
282
- permissions_to_remove = [
283
- iam_unit_ref
284
- for iam_unit_ref, assignment in self.iam_domain_permissions[domain][perm_type].items()
285
- if assignment.expires_at and assignment.expires_at < now
286
- ]
287
-
288
- # Remove expired permissions
289
- for iam_unit_ref in permissions_to_remove:
290
- del self.iam_domain_permissions[domain][perm_type][iam_unit_ref]
291
- removed_count += 1
292
-
293
- return removed_count
294
-
295
- def remove_expired_groups(self) -> int:
296
- """
297
- Remove all expired group assignments (legacy method).
298
-
299
- Returns:
300
- Number of removed group assignments
301
- """
302
- return self.remove_expired_iam_unit_refs(IAMUnitType.GROUPS)
303
-
304
- def update_iam_unit_refs_from_subscription(self, subscription: Subscription) -> int:
305
- """
306
- Update IAM permissions based on a subscription.
307
-
308
- Args:
309
- subscription: Subscription to apply
310
-
311
- Returns:
312
- Number of permission assignments added
313
- """
314
- added_count = 0
315
-
316
- for domain, permissions_by_type in subscription.default_iam_domain_permissions.items():
317
- for iam_unit_type_str, iam_unit_refs in permissions_by_type.items():
318
- # Convert string to enum if needed for internal processing
319
- try:
320
- iam_unit_type = IAMUnitType(iam_unit_type_str)
321
- for iam_unit_ref in iam_unit_refs:
322
- self.add_iam_unit_ref_assignment(
323
- domain=domain,
324
- iam_unit_ref=iam_unit_ref,
325
- iam_unit_type=iam_unit_type,
326
- source=f"{subscription.plan_name.value}_v{subscription.plan_version}",
327
- expires_at=subscription.cycle_end_date
328
- )
329
- added_count += 1
330
- except ValueError:
331
- # Skip invalid unit types
332
- continue
333
-
334
- return added_count
335
-
336
- def update_groups_from_subscription(self, subscription: Subscription) -> int:
337
- """
338
- Update IAM groups based on a subscription (legacy method).
339
-
340
- Args:
341
- subscription: Subscription to apply
342
-
343
- Returns:
344
- Number of group assignments added
345
- """
346
- return self.update_iam_unit_refs_from_subscription(subscription)
347
-
348
- # Method instead of computed field
349
- def is_subscription_active(self) -> bool:
350
- """Check if the user has an active subscription."""
351
- if self.active_subscription:
352
- return self.active_subscription.is_active()
353
- return False
354
-
355
- # Method instead of computed field
356
- def subscription_expires_in_days(self) -> Optional[int]:
357
- """Get days until subscription expiration."""
358
- if self.active_subscription and self.active_subscription.is_active():
359
- return self.active_subscription.days_remaining()
360
- return None
361
-
362
- def apply_subscription(self, subscription: Subscription) -> None:
363
- """
364
- Apply a subscription's benefits to the user status.
365
- This updates credits, permissions, and sets the active subscription.
366
-
367
- Args:
368
- subscription: The subscription to apply
369
- """
370
- if not subscription:
371
- return
372
-
373
- # Add IAM permissions from subscription
374
- self.update_iam_unit_refs_from_subscription(subscription)
375
-
376
- # Update subscription-based credits
377
- credits_per_update = subscription.subscription_based_insight_credits_per_update
378
- if credits_per_update > 0:
379
- self.sbscrptn_based_insight_credits = credits_per_update
380
- self.sbscrptn_based_insight_credits_updtd_on = datetime.now(timezone.utc)
381
-
382
- # Update voting credits directly from subscription attributes
383
- voting_credits = subscription.voting_credits_per_update
384
- if voting_credits > 0:
385
- self.voting_credits = voting_credits
386
- self.voting_credits_updtd_on = datetime.now(timezone.utc)
387
-
388
- # Store subscription details
389
- self.active_subscription = subscription
390
-
391
- def revoke_subscription(self) -> None:
392
- """
393
- Revoke the current subscription benefits.
394
- This clears subscription-based credits and removes the active subscription.
395
- """
396
- if not self.active_subscription:
397
- return
398
-
399
- # Clear subscription-based credits and active subscription
400
- self.sbscrptn_based_insight_credits = 0
401
- self.sbscrptn_based_insight_credits_updtd_on = datetime.now(timezone.utc)
402
- self.active_subscription = None
403
-
404
- def apply_subscription_plan(self,
405
- plan_data: Dict[str, Any],
406
- source: str = "default_configuration",
407
- expires_at: Optional[datetime] = None) -> None:
408
- """
409
- Apply a subscription plan\'s benefits from plan data dictionary.
410
-
411
- Args:
412
- plan_data: Dictionary containing subscription plan details
413
- source: Source identifier for this application
414
- expires_at: Optional expiration date for the subscription
415
- """
416
- # Default expiration date (1 month from now) if not provided
417
- if not expires_at:
418
- expires_at = datetime.now(timezone.utc) + relativedelta(months=1)
419
-
420
- # Extract IAM permissions
421
- iam_domain_permissions = plan_data.get("default_iam_domain_permissions", {})
422
-
423
- # Extract plan name - no default fallbacks
424
- plan_name_str = plan_data.get("plan_name")
425
- if not plan_name_str:
426
- # Consider logging a warning or raising an error if plan_name is critical
427
- return # Cannot create subscription without plan name
428
-
429
- try:
430
- plan_name = SubscriptionPlan(plan_name_str)
431
- except ValueError:
432
- # Consider logging a warning or raising an error for invalid plan_name
433
- return # Invalid plan name
434
-
435
- # Extract required fields - no default fallbacks
436
- plan_version = plan_data.get("plan_version")
437
- validity_time_length = plan_data.get("plan_validity_cycle_length")
438
- validity_time_unit = plan_data.get("plan_validity_cycle_unit")
439
-
440
- # If any required field is missing, return without creating subscription
441
- if plan_version is None or validity_time_length is None or validity_time_unit is None:
442
- # Consider logging a warning or raising an error if these fields are critical
443
- return
444
-
445
- # Calculate directly assigned fields
446
- plan_id = f"{plan_name_str}_{plan_version}" # Use the string version for ID
447
- cycle_start_date = datetime.now(timezone.utc)
448
-
449
- # Ensure cycle_end_date is calculated correctly using the provided expires_at or calculated from plan details
450
- # If expires_at is provided, it should ideally align with plan's cycle, but we prioritize it if present.
451
- # However, the current Subscription.calculate_cycle_end_date might be more robust if plan details are the source of truth.
452
- # For now, let's assume expires_at is a specific override if provided, otherwise calculate from plan.
453
- # This logic might need refinement based on how expires_at is intended to be used.
454
- # If expires_at is meant to be the definitive end date, use it. Otherwise, calculate.
455
- # The original code used expires_at for IAM assignments but calculated cycle_end_date for the subscription object itself.
456
- # Let's stick to calculating cycle_end_date from plan details for the Subscription object for consistency.
457
-
458
- cycle_end_date = Subscription.calculate_cycle_end_date(
459
- cycle_start_date,
460
- validity_time_length,
461
- validity_time_unit
462
- )
463
-
464
- # If an explicit expires_at is given and differs, it might indicate a special case (e.g. trial ending sooner)
465
- # For IAM assignments, the original code used the `expires_at` passed to `apply_subscription_plan`
466
- # For the subscription object itself, it calculated `cycle_end_date`.
467
- # We will use the calculated `cycle_end_date` for the subscription object.
468
- # The `expires_at` for IAM assignments will be handled by `update_iam_unit_refs_from_subscription`
469
- # which uses `subscription.cycle_end_date`.
470
-
471
- # Create subscription object with direct fields instead of computed
472
- subscription = Subscription(
473
- plan_name=plan_name, # Enum version
474
- plan_version=plan_version,
475
- plan_id=plan_id, # String version
476
- cycle_start_date=cycle_start_date,
477
- cycle_end_date=cycle_end_date, # Calculated cycle_end_date
478
- validity_time_length=validity_time_length,
479
- validity_time_unit=validity_time_unit,
480
- auto_renew=plan_data.get("plan_auto_renewal", False),
481
- status=SubscriptionStatus.ACTIVE, # Default to ACTIVE when applying
482
- default_iam_domain_permissions=iam_domain_permissions,
483
- fallback_plan_id=plan_data.get("fallback_plan_id_if_current_plan_expired"),
484
- price_paid_usd=plan_data.get("plan_per_cycle_price_usd") or 0.0,
485
- created_by=source,
486
- updated_by=source,
487
- subscription_based_insight_credits_per_update=plan_data.get("subscription_based_insight_credits_per_update") or 0,
488
- subscription_based_insight_credits_update_freq_h=plan_data.get("subscription_based_insight_credits_update_freq_h") or 24,
489
- extra_insight_credits_per_cycle=plan_data.get("extra_insight_credits_per_cycle") or 0,
490
- voting_credits_per_update=plan_data.get("voting_credits_per_update") or 0,
491
- voting_credits_update_freq_h=plan_data.get("voting_credits_update_freq_h") or 62 # Corrected typo from 62 to, e.g., 720 for monthly if intended
492
- )
493
-
494
- # Apply this new subscription
495
- self.apply_subscription(subscription)