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.
- ipulse_shared_core_ftredge/__init__.py +1 -1
- ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +10 -2
- ipulse_shared_core_ftredge/dependencies/firestore_client.py +1 -1
- ipulse_shared_core_ftredge/models/__init__.py +1 -1
- ipulse_shared_core_ftredge/models/base_data_model.py +6 -6
- ipulse_shared_core_ftredge/models/organization_profile.py +4 -4
- ipulse_shared_core_ftredge/models/subscription.py +140 -21
- ipulse_shared_core_ftredge/models/user_profile.py +75 -46
- ipulse_shared_core_ftredge/models/user_status.py +517 -48
- ipulse_shared_core_ftredge/services/base_firestore_service.py +10 -10
- {ipulse_shared_core_ftredge-7.1.1.dist-info → ipulse_shared_core_ftredge-8.1.1.dist-info}/METADATA +3 -2
- {ipulse_shared_core_ftredge-7.1.1.dist-info → ipulse_shared_core_ftredge-8.1.1.dist-info}/RECORD +15 -16
- {ipulse_shared_core_ftredge-7.1.1.dist-info → ipulse_shared_core_ftredge-8.1.1.dist-info}/WHEEL +1 -1
- ipulse_shared_core_ftredge/models/resource_catalog_item.py +0 -115
- {ipulse_shared_core_ftredge-7.1.1.dist-info → ipulse_shared_core_ftredge-8.1.1.dist-info/licenses}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-7.1.1.dist-info → ipulse_shared_core_ftredge-8.1.1.dist-info}/top_level.txt +0 -0
|
@@ -1,35 +1,56 @@
|
|
|
1
1
|
""" User Status model for tracking user subscription and access rights. """
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
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
|
-
|
|
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] =
|
|
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
|
|
41
|
-
...,
|
|
42
|
-
description="User ID, format: {OBJ_REF}
|
|
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
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
description="
|
|
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
|
-
#
|
|
57
|
-
|
|
58
|
-
default_factory=
|
|
59
|
-
description="
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
...,
|
|
83
130
|
description="Voting credits for user"
|
|
84
131
|
)
|
|
85
132
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
138
|
+
metadata: Dict[str, Any] = Field(
|
|
139
|
+
default_factory=dict,
|
|
140
|
+
description="Additional metadata for the user status"
|
|
141
|
+
)
|
|
90
142
|
|
|
91
|
-
@
|
|
143
|
+
@model_validator(mode='before')
|
|
92
144
|
@classmethod
|
|
93
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
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
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
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
|
-
'
|
|
113
|
-
'
|
|
112
|
+
'updated_at': datetime.now(timezone.utc).isoformat(),
|
|
113
|
+
'updated_by': updater_uid
|
|
114
114
|
})
|
|
115
115
|
|
|
116
116
|
doc_ref.update(valid_fields)
|
{ipulse_shared_core_ftredge-7.1.1.dist-info → ipulse_shared_core_ftredge-8.1.1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version:
|
|
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
|