ipulse-shared-core-ftredge 7.1.1__py3-none-any.whl → 8.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.

@@ -1,35 +1,56 @@
1
1
  """ User Status model for tracking user subscription and access rights. """
2
- from datetime import datetime
3
- from typing import Set, Optional, Dict, List, ClassVar
4
- from pydantic import Field, ConfigDict, field_validator
5
- from ipulse_shared_base_ftredge import Layer, Module, list_as_lower_strings, Subject
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, Union
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, ObjectOverallStatus, SubscriptionPlan, SubscriptionStatus
7
+ from ipulse_shared_base_ftredge.enums.enums_iam import IAMUnitType, IAMUserType
6
8
  from .subscription import Subscription
7
9
  from .base_data_model import BaseDataModel
8
10
 
9
11
  # ORIGINAL AUTHOR ="Russlan Ramdowar;russlan@ftredge.com"
10
12
  # CLASS_ORGIN_DATE=datetime(2024, 2, 12, 20, 5)
11
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
+
12
41
  ############################ !!!!! ALWAYS UPDATE SCHEMA VERSION , IF SCHEMA IS BEING MODIFIED !!! #################################
13
42
  class UserStatus(BaseDataModel):
14
43
  """
15
44
  User Status model for tracking user subscription and access rights.
16
45
  """
17
- model_config = ConfigDict(frozen=True, extra="forbid")
46
+ # Set frozen=False to allow modification of attributes
47
+ model_config = ConfigDict(frozen=False, extra="forbid")
18
48
 
19
49
  # Class constants
20
- VERSION: ClassVar[float] = 4.1
50
+ VERSION: ClassVar[float] = 5.0 # Incremented version for primary_user_type addition
21
51
  DOMAIN: ClassVar[str] = "_".join(list_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.USER.name))
22
52
  OBJ_REF: ClassVar[str] = "userstatus"
23
53
 
24
- # Default values as class variables
25
- DEFAULT_IAM_GROUPS: ClassVar[Dict[str, List[str]]] = {"pulseroot": ["full_open_read"]}
26
- DEFAULT_SUBSCRIPTION_PLAN: ClassVar[str] = "subscription_free"
27
- DEFAULT_SUBSCRIPTION_STATUS: ClassVar[str] = "active"
28
- DEFAULT_SUBSCRIPTION_INSIGHT_CREDITS: ClassVar[int] = 10
29
- DEFAULT_VOTING_CREDITS: ClassVar[int] = 0
30
- DEFAULT_EXTRA_INSIGHT_CREDITS: ClassVar[int] = 0
31
-
32
-
33
54
  # System-managed fields
34
55
  schema_version: float = Field(
35
56
  default=VERSION,
@@ -37,9 +58,9 @@ class UserStatus(BaseDataModel):
37
58
  description="Version of this Class == version of DB Schema"
38
59
  )
39
60
 
40
- id : str = Field(
41
- ...,
42
- description="User ID, format: {OBJ_REF}_{user_uid}"
61
+ id: str = Field(
62
+ ..., # Still required, but will be auto-generated by model_validator if not provided
63
+ description=f"User ID, format: {OBJ_REF}_user_uid"
43
64
  )
44
65
 
45
66
  user_uid: str = Field(
@@ -47,60 +68,508 @@ class UserStatus(BaseDataModel):
47
68
  description="User UID from Firebase Auth"
48
69
  )
49
70
 
50
- # IAM and subscription fields
51
- iam_groups: Dict[str, List[str]] = Field(
52
- default_factory=lambda: UserStatus.DEFAULT_IAM_GROUPS,
53
- description="User's Groups, with a default one for all authenticated Pulse users"
71
+ # Added primary_user_type field for main role categorization
72
+ primary_user_type: str = Field(
73
+ ...,
74
+ description="Primary user type (e.g., customer, internal, admin, superadmin)"
54
75
  )
55
76
 
56
- # Subscription Management
57
- subscriptions: Dict[str, Subscription] = Field(
58
- default_factory=dict,
59
- description="Dictionary of user's active and past subscriptions, keyed by plan name"
77
+ # Renamed user_types to secondary_user_types
78
+ secondary_user_types: List[str] = Field(
79
+ default_factory=list,
80
+ description="List of secondary user types/roles"
60
81
  )
61
82
 
62
- # Credits management
83
+ # Added organizations field for consistency with UserProfile
84
+ organizations_uids: Set[str] = Field(
85
+ default_factory=set,
86
+ description="Organization UIDs the user belongs to"
87
+ )
88
+
89
+ # Enhanced IAM permissions structure with expiration - update to use string for enum keys
90
+ iam_domain_permissions: Dict[str, Dict[str, Dict[str, IAMUnitRefAssignment]]] = Field(
91
+ ...,
92
+ description="Domain -> IAM unit type (groups/roles/permissions) -> unit reference name -> assignment details (with expiration)"
93
+ )
94
+
95
+ # Subscription Management - Single active subscription instead of dictionary
96
+ subscriptions_history: Dict[str, Subscription] = Field(
97
+ ...,
98
+ description="Dictionary of user's past recent subscriptions, keyed by subscription ID"
99
+ )
100
+
101
+ # Changed from dictionary to single Optional subscription
102
+ active_subscription: Optional[Subscription] = Field(
103
+ default=None,
104
+ description="The user's currently active subscription, if any"
105
+ )
106
+
107
+ # Credit management fields
63
108
  sbscrptn_based_insight_credits: int = Field(
64
- default_factory=lambda: UserStatus.DEFAULT_SUBSCRIPTION_INSIGHT_CREDITS,
65
- description="Subscription-based insight credits"
109
+ ...,
110
+ description="Subscription-based insight credits (expire with subscription)"
66
111
  )
112
+
67
113
  sbscrptn_based_insight_credits_updtd_on: datetime = Field(
68
- default_factory=datetime.now,
114
+ default_factory=lambda: datetime.now(timezone.utc),
69
115
  description="Last update timestamp for subscription credits"
70
116
  )
117
+
71
118
  extra_insight_credits: int = Field(
72
- default_factory=lambda: UserStatus.DEFAULT_EXTRA_INSIGHT_CREDITS,
119
+ ...,
73
120
  description="Additional purchased insight credits (non-expiring)"
74
121
  )
75
122
 
76
123
  extra_insight_credits_updtd_on: datetime = Field(
77
- default_factory=datetime.now,
124
+ default_factory=lambda: datetime.now(timezone.utc),
78
125
  description="Last update timestamp for extra credits"
79
126
  )
80
127
 
81
128
  voting_credits: int = Field(
82
- default_factory=lambda: UserStatus.DEFAULT_VOTING_CREDITS, # Changed default to default_factory
129
+ ...,
83
130
  description="Voting credits for user"
84
131
  )
85
132
 
86
- # Optional fields
87
- payment_refs_uids: Optional[Set[str]] = None
133
+ voting_credits_updtd_on: datetime = Field(
134
+ default_factory=lambda: datetime.now(timezone.utc),
135
+ description="Last update timestamp for voting credits"
136
+ )
88
137
 
89
- # Remove audit fields as they're inherited from BaseDataModel
138
+ metadata: Dict[str, Any] = Field(
139
+ default_factory=dict,
140
+ description="Additional metadata for the user status"
141
+ )
90
142
 
91
- @field_validator('id', mode='before')
143
+ @model_validator(mode='before')
92
144
  @classmethod
93
- def validate_or_generate_id(cls, v: Optional[str], info) -> str:
145
+ def ensure_id_exists(cls, data: Dict[str, Any]) -> Dict[str, Any]:
146
+ """
147
+ Ensures the id field exists by generating it from user_uid if needed.
148
+ This runs BEFORE validation, guaranteeing id will be present for validators.
149
+ """
150
+ if not isinstance(data, dict):
151
+ return data
152
+
153
+ # If id is already in the data, leave it alone
154
+ if 'id' in data and data['id']:
155
+ return data
156
+
157
+ # If user_uid exists but id doesn't, generate id from user_uid
158
+ if 'user_uid' in data and data['user_uid']:
159
+ data['id'] = f"{cls.OBJ_REF}_{data['user_uid']}"
160
+
161
+ return data
162
+
163
+ # Utility methods for subscription management - updated for single subscription
164
+
165
+ def get_active_subscription(self) -> Optional[Subscription]:
166
+ """Get the currently active subscription if it exists."""
167
+ return self.active_subscription
168
+
169
+ def get_subscription_plan_name(self) -> Optional[SubscriptionPlan]:
170
+ """Get the current subscription plan name."""
171
+ if self.active_subscription:
172
+ return self.active_subscription.plan_name
173
+ return None
174
+
175
+ def has_valid_permission_type_for_domain(self, domain: str, iam_unit_type: IAMUnitType = IAMUnitType.GROUPS) -> bool:
176
+ """Check if the user has any valid IAM permissions of specified type for the domain."""
177
+ if domain not in self.iam_domain_permissions:
178
+ return False
179
+
180
+ # Update to use string value from enum
181
+ domain_permissions = self.iam_domain_permissions[domain].get(iam_unit_type.value, {})
182
+ return any(assignment.is_valid() for assignment in domain_permissions.values())
183
+
184
+ def has_valid_groups_for_domain(self, domain: str) -> bool:
185
+ """Check if the user has any valid IAM groups for the specified domain (legacy method)."""
186
+ return self.has_valid_permission_type_for_domain(domain, IAMUnitType.GROUPS)
187
+
188
+ def get_valid_permissions_for_domain(self, domain: str, iam_unit_type: IAMUnitType = IAMUnitType.GROUPS) -> List[str]:
189
+ """Get a list of valid (non-expired) permission names of specified type for the domain."""
190
+ if domain not in self.iam_domain_permissions:
191
+ return []
192
+
193
+ # Update to use string value from enum
194
+ domain_permissions = self.iam_domain_permissions[domain].get(iam_unit_type.value, {})
195
+ return [
196
+ iam_unit_ref
197
+ for iam_unit_ref, assignment in domain_permissions.items()
198
+ if assignment.is_valid()
199
+ ]
200
+
201
+ def get_valid_groups_for_domain(self, domain: str) -> List[str]:
202
+ """Get a list of valid (non-expired) group names for the domain (legacy method)."""
203
+ return self.get_valid_permissions_for_domain(domain, IAMUnitType.GROUPS)
204
+
205
+ def add_iam_unit_ref_assignment(
206
+ self,
207
+ domain: str,
208
+ iam_unit_ref: str,
209
+ iam_unit_type: IAMUnitType,
210
+ source: str,
211
+ expires_at: Optional[datetime] = None
212
+ ) -> None:
213
+ """
214
+ Add a permission assignment to the user's IAM domain permissions.
215
+
216
+ Args:
217
+ domain: The domain for the permission (e.g., 'papp')
218
+ iam_unit_ref: The name/identifier of the permission to add
219
+ iam_unit_type: Type of IAM assignment (GROUP, ROLE, PERMISSION)
220
+ source: Source identifier for this assignment (e.g., subscription ID)
221
+ expires_at: Optional expiration date
222
+ """
223
+ # Ensure domain exists
224
+ if domain not in self.iam_domain_permissions:
225
+ self.iam_domain_permissions[domain] = {}
226
+
227
+ # Ensure permission type section exists - use string value from enum
228
+ if iam_unit_type.value not in self.iam_domain_permissions[domain]:
229
+ self.iam_domain_permissions[domain][iam_unit_type.value] = {}
230
+
231
+ # Create new assignment
232
+ assignment = IAMUnitRefAssignment(
233
+ iam_unit_ref=iam_unit_ref,
234
+ source=source,
235
+ expires_at=expires_at
236
+ )
237
+
238
+ # Add the permission - use string value from enum
239
+ self.iam_domain_permissions[domain][iam_unit_type.value][iam_unit_ref] = assignment
240
+
241
+ def add_group_assignment(
242
+ self,
243
+ domain: str,
244
+ group_name: str,
245
+ source: str,
246
+ expires_at: Optional[datetime] = None
247
+ ) -> None:
248
+ """
249
+ Add a group assignment to the user's IAM groups (legacy method).
250
+
251
+ Args:
252
+ domain: The domain for the group (e.g., 'papp')
253
+ group_name: The name of the group to add
254
+ source: Source identifier for this assignment (e.g., subscription ID)
255
+ expires_at: Optional expiration date
256
+ """
257
+ self.add_iam_unit_ref_assignment(domain, group_name, IAMUnitType.GROUPS, source, expires_at)
258
+
259
+ def remove_expired_iam_unit_refs(self, iam_unit_type: Optional[IAMUnitType] = None) -> int:
260
+ """
261
+ Remove all expired permission assignments of a specific type or all types.
262
+
263
+ Args:
264
+ iam_unit_type: If provided, only remove this type of permissions
265
+
266
+ Returns:
267
+ Number of removed permission assignments
268
+ """
269
+ now = datetime.now(timezone.utc)
270
+ removed_count = 0
271
+
272
+ # Create a deep copy of domains to avoid modification during iteration
273
+ domains = list(self.iam_domain_permissions.keys())
274
+
275
+ for domain in domains:
276
+ # If iam_unit_type is specified, only check that type
277
+ if iam_unit_type:
278
+ unit_type_value = iam_unit_type.value
279
+ if unit_type_value not in self.iam_domain_permissions[domain]:
280
+ continue
281
+
282
+ iam_unit_types_to_check = [unit_type_value]
283
+ else:
284
+ # Check all permission types
285
+ iam_unit_types_to_check = list(self.iam_domain_permissions[domain].keys())
286
+
287
+ # Process each permission type
288
+ for perm_type in iam_unit_types_to_check:
289
+ # Create a list of permissions to remove
290
+ permissions_to_remove = [
291
+ iam_unit_ref
292
+ for iam_unit_ref, assignment in self.iam_domain_permissions[domain][perm_type].items()
293
+ if assignment.expires_at and assignment.expires_at < now
294
+ ]
295
+
296
+ # Remove expired permissions
297
+ for iam_unit_ref in permissions_to_remove:
298
+ del self.iam_domain_permissions[domain][perm_type][iam_unit_ref]
299
+ removed_count += 1
300
+
301
+ return removed_count
302
+
303
+ def remove_expired_groups(self) -> int:
304
+ """
305
+ Remove all expired group assignments (legacy method).
306
+
307
+ Returns:
308
+ Number of removed group assignments
309
+ """
310
+ return self.remove_expired_iam_unit_refs(IAMUnitType.GROUPS)
311
+
312
+ def update_iam_unit_refs_from_subscription(self, subscription: Subscription) -> int:
313
+ """
314
+ Update IAM permissions based on a subscription.
315
+
316
+ Args:
317
+ subscription: Subscription to apply
318
+
319
+ Returns:
320
+ Number of permission assignments added
321
+ """
322
+ added_count = 0
323
+
324
+ for domain, permissions_by_type in subscription.iam_domain_permissions.items():
325
+ for iam_unit_type_str, iam_unit_refs in permissions_by_type.items():
326
+ # Convert string to enum if needed for internal processing
327
+ try:
328
+ iam_unit_type = IAMUnitType(iam_unit_type_str)
329
+ for iam_unit_ref in iam_unit_refs:
330
+ self.add_iam_unit_ref_assignment(
331
+ domain=domain,
332
+ iam_unit_ref=iam_unit_ref,
333
+ iam_unit_type=iam_unit_type,
334
+ source=f"{subscription.plan_name.value}_v{subscription.plan_version}",
335
+ expires_at=subscription.cycle_end_date
336
+ )
337
+ added_count += 1
338
+ except ValueError:
339
+ # Skip invalid unit types
340
+ continue
341
+
342
+ return added_count
343
+
344
+ def update_groups_from_subscription(self, subscription: Subscription) -> int:
345
+ """
346
+ Update IAM groups based on a subscription (legacy method).
347
+
348
+ Args:
349
+ subscription: Subscription to apply
350
+
351
+ Returns:
352
+ Number of group assignments added
353
+ """
354
+ return self.update_iam_unit_refs_from_subscription(subscription)
355
+
356
+ @computed_field
357
+ def is_subscription_active(self) -> bool:
358
+ """Check if the user has an active subscription."""
359
+ if self.active_subscription:
360
+ return self.active_subscription.is_active()
361
+ return False
362
+
363
+ @computed_field
364
+ def subscription_expires_in_days(self) -> Optional[int]:
365
+ """Get days until subscription expiration."""
366
+ if self.active_subscription and self.active_subscription.is_active():
367
+ return self.active_subscription.days_remaining()
368
+ return None
369
+
370
+ def apply_subscription(self, subscription: Subscription) -> None:
371
+ """
372
+ Apply a subscription's benefits to the user status.
373
+ This updates credits, permissions, and sets the active subscription.
374
+
375
+ Args:
376
+ subscription: The subscription to apply
377
+ """
378
+ if not subscription:
379
+ return
380
+
381
+ # Add IAM permissions from subscription
382
+ self.update_iam_unit_refs_from_subscription(subscription)
383
+
384
+ # We need to handle model_config.frozen - use object.__setattr__ if model is frozen
385
+ credits_per_update = subscription.subscription_based_insight_credits_per_update
386
+ if credits_per_update > 0:
387
+ if getattr(self.model_config, "frozen", False):
388
+ object.__setattr__(self, "sbscrptn_based_insight_credits", credits_per_update)
389
+ object.__setattr__(self, "sbscrptn_based_insight_credits_updtd_on", datetime.now(timezone.utc))
390
+ else:
391
+ self.sbscrptn_based_insight_credits = credits_per_update
392
+ self.sbscrptn_based_insight_credits_updtd_on = datetime.now(timezone.utc)
393
+
394
+ # Update voting credits directly from subscription attributes
395
+ voting_credits = subscription.voting_credits_per_update
396
+ if voting_credits > 0:
397
+ if getattr(self.model_config, "frozen", False):
398
+ object.__setattr__(self, "voting_credits", voting_credits)
399
+ object.__setattr__(self, "voting_credits_updtd_on", datetime.now(timezone.utc))
400
+ else:
401
+ self.voting_credits = voting_credits
402
+ self.voting_credits_updtd_on = datetime.now(timezone.utc)
403
+
404
+ # Store subscription details
405
+ # Use object.__setattr__ if model is frozen
406
+ if getattr(self.model_config, "frozen", False):
407
+ object.__setattr__(self, "active_subscription", subscription)
408
+ else:
409
+ self.active_subscription = subscription
410
+
411
+ def revoke_subscription(self) -> None:
412
+ """
413
+ Revoke the current subscription benefits.
414
+ This clears subscription-based credits and removes the active subscription.
415
+ """
416
+ if not self.active_subscription:
417
+ return
418
+
419
+ # Reset subscription-based credits - handle frozen model case
420
+ if getattr(self.model_config, "frozen", False):
421
+ object.__setattr__(self, "sbscrptn_based_insight_credits", 0)
422
+ object.__setattr__(self, "sbscrptn_based_insight_credits_updtd_on", datetime.now(timezone.utc))
423
+ object.__setattr__(self, "active_subscription", None)
424
+ else:
425
+ self.sbscrptn_based_insight_credits = 0
426
+ self.sbscrptn_based_insight_credits_updtd_on = datetime.now(timezone.utc)
427
+ self.active_subscription = None
428
+
429
+ def apply_subscription_plan(self,
430
+ plan_data: Dict[str, Any],
431
+ source: str = "default_configuration",
432
+ expires_at: Optional[datetime] = None) -> None:
433
+ """
434
+ Apply a subscription plan's benefits from plan data dictionary.
435
+
436
+ Args:
437
+ plan_data: Dictionary containing subscription plan details
438
+ source: Source identifier for this application
439
+ expires_at: Optional expiration date for the subscription
440
+ """
441
+ # Default expiration date (1 month from now) if not provided
442
+ if not expires_at:
443
+ expires_at = datetime.now(timezone.utc) + relativedelta(months=1)
444
+
445
+ # Extract IAM permissions
446
+ iam_domain_permissions = plan_data.get("default_iam_domain_permissions", {})
447
+
448
+ # Extract plan name - no default fallbacks
449
+ plan_name_str = plan_data.get("plan_name")
450
+ if not plan_name_str:
451
+ return # Cannot create subscription without plan name
452
+
453
+ try:
454
+ plan_name = SubscriptionPlan(plan_name_str)
455
+ except ValueError:
456
+ return # Invalid plan name
457
+
458
+ # Extract required fields - no default fallbacks
459
+ plan_version = plan_data.get("plan_version")
460
+ validity_time_length = plan_data.get("plan_validity_cycle_length")
461
+ validity_time_unit = plan_data.get("plan_validity_cycle_unit")
462
+
463
+ # If any required field is missing, return without creating subscription
464
+ if plan_version is None or validity_time_length is None or validity_time_unit is None:
465
+ return
466
+
467
+ # Create temporary subscription object with direct attributes instead of metadata
468
+ subscription = Subscription(
469
+ plan_name=plan_name,
470
+ plan_version=plan_version,
471
+ cycle_start_date=datetime.now(timezone.utc),
472
+ validity_time_length=validity_time_length,
473
+ validity_time_unit=validity_time_unit,
474
+ auto_renew=plan_data.get("plan_auto_renewal", False), # Default only for boolean fields
475
+ status=SubscriptionStatus.ACTIVE,
476
+ iam_domain_permissions=iam_domain_permissions,
477
+ fallback_plan_id=plan_data.get("fallback_plan_id_if_current_plan_expired"),
478
+ price_paid_usd=plan_data.get("plan_per_cycle_price_usd") or 0.0,
479
+ created_by=source,
480
+ updated_by=source,
481
+ # Direct attributes for credit-related fields
482
+ subscription_based_insight_credits_per_update=plan_data.get("subscription_based_insight_credits_per_update") or 0,
483
+ subscription_based_insight_credits_update_freq_h=plan_data.get("subscription_based_insight_credits_update_freq_h") or 24,
484
+ extra_insight_credits_per_cycle=plan_data.get("extra_insight_credits_per_cycle") or 0,
485
+ voting_credits_per_update=plan_data.get("voting_credits_per_update") or 0,
486
+ voting_credits_update_freq_h=plan_data.get("voting_credits_update_freq_h") or 62
487
+ )
488
+
489
+ # Apply the subscription
490
+ self.apply_subscription(subscription)
491
+
492
+ @staticmethod
493
+ def fetch_user_status_defaults(firestore_client,
494
+ primary_user_type: str,
495
+ collection: str = "papp_core_configs_user") -> Dict[str, Any]:
94
496
  """
95
- Validate or generate the id field based on user_uid.
497
+ Fetch user status defaults from Firestore.
498
+
499
+ Args:
500
+ firestore_client: Initialized Firestore client
501
+ primary_user_type: Primary type of user (customer, internal, admin, etc)
502
+ collection: Collection name for user status defaults
503
+
504
+ Returns:
505
+ Dictionary containing user status defaults, or empty dict if not found
506
+ """
507
+ try:
508
+ # Get the consolidated document containing all defaults
509
+ doc_ref = firestore_client.collection(collection).document("all_users_defaults")
510
+ doc = doc_ref.get()
511
+
512
+ if not doc.exists:
513
+ return {}
514
+
515
+ # Get the data
516
+ data = doc.to_dict()
517
+
518
+ # Find the latest version of defaults for the specified user type
519
+ latest_key = None
520
+ latest_version = -1
521
+
522
+ # Look for defaults with format "{user_type}_defaults_{version}"
523
+ for key in data.keys():
524
+ if key.startswith(f"{primary_user_type}_defaults_"):
525
+ try:
526
+ version = int(key.split("_")[-1])
527
+ if version > latest_version:
528
+ latest_version = version
529
+ latest_key = key
530
+ except ValueError:
531
+ continue
532
+
533
+ # Return the defaults if found
534
+ if latest_key and latest_key in data:
535
+ return data[latest_key]
536
+
537
+ return {}
538
+ except Exception:
539
+ # Return empty dict on error
540
+ return {}
541
+
542
+ @staticmethod
543
+ def fetch_subscription_plan(firestore_client,
544
+ plan_id: str,
545
+ collection: str = "papp_core_configs_subscriptionplans") -> Dict[str, Any]:
546
+ """
547
+ Fetch subscription plan details from Firestore.
548
+
549
+ Args:
550
+ firestore_client: Initialized Firestore client
551
+ plan_id: ID of the plan to fetch
552
+ collection: Collection name for subscription plans
553
+
554
+ Returns:
555
+ Dictionary containing subscription plan details, or empty dict if not found
96
556
  """
97
- # If id is already provided (Firebase Auth case), return it
98
- if v:
99
- return v
557
+ try:
558
+ # Get the consolidated document containing all plans
559
+ doc_ref = firestore_client.collection(collection).document("all_subscriptionplans_defaults")
560
+ doc = doc_ref.get()
561
+
562
+ if not doc.exists:
563
+ return {}
564
+
565
+ # Get the data
566
+ data = doc.to_dict()
567
+
568
+ # Return the plan if found
569
+ if plan_id in data:
570
+ return data[plan_id]
100
571
 
101
- # Fallback: generate from user_uid if needed
102
- values = info.data
103
- user_uid = values.get('user_uid')
104
- if not user_uid:
105
- raise ValueError("Either id or user_uid must be provided")
106
- return f"{cls.OBJ_REF}_{user_uid}"
572
+ return {}
573
+ except Exception:
574
+ # Return empty dict on error
575
+ return {}
@@ -25,10 +25,10 @@ class BaseFirestoreService(Generic[T]):
25
25
 
26
26
  # Add audit fields
27
27
  doc_data.update({
28
- 'creat_date': current_time.isoformat(),
29
- 'creat_by_user': creator_uid,
30
- 'updt_date': current_time.isoformat(),
31
- 'updt_by_user': creator_uid
28
+ 'created_at': current_time.isoformat(),
29
+ 'created_by': creator_uid,
30
+ 'updated_at': current_time.isoformat(),
31
+ 'updated_by': creator_uid
32
32
  })
33
33
 
34
34
  doc_ref = self.db.collection(self.collection_name).document(doc_id)
@@ -56,10 +56,10 @@ class BaseFirestoreService(Generic[T]):
56
56
  for doc in documents:
57
57
  doc_data = doc.model_dump(mode='json')
58
58
  doc_data.update({
59
- 'creat_date': current_time.isoformat(),
60
- 'creat_by_user': creator_uid,
61
- 'updt_date': current_time.isoformat(),
62
- 'updt_by_user': creator_uid
59
+ 'created_at': current_time.isoformat(),
60
+ 'created_by': creator_uid,
61
+ 'updated_at': current_time.isoformat(),
62
+ 'updated_by': creator_uid
63
63
  })
64
64
 
65
65
  doc_ref = self.db.collection(self.collection_name).document(doc_data.get('id'))
@@ -109,8 +109,8 @@ class BaseFirestoreService(Generic[T]):
109
109
 
110
110
  # Add audit fields
111
111
  valid_fields.update({
112
- 'updt_date': datetime.now(timezone.utc).isoformat(),
113
- 'updt_by_user': updater_uid
112
+ 'updated_at': datetime.now(timezone.utc).isoformat(),
113
+ 'updated_by': updater_uid
114
114
  })
115
115
 
116
116
  doc_ref.update(valid_fields)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: ipulse_shared_core_ftredge
3
- Version: 7.1.1
3
+ Version: 8.1.1
4
4
  Summary: Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.
5
5
  Home-page: https://github.com/TheFutureEdge/ipulse_shared_core
6
6
  Author: Russlan Ramdowar
@@ -17,6 +17,7 @@ Requires-Dist: ipulse_shared_base_ftredge>=5.7.1
17
17
  Dynamic: author
18
18
  Dynamic: classifier
19
19
  Dynamic: home-page
20
+ Dynamic: license-file
20
21
  Dynamic: requires-dist
21
22
  Dynamic: requires-python
22
23
  Dynamic: summary