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.
- ipulse_shared_core_ftredge/cache/shared_cache.py +1 -2
- ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +60 -23
- ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +128 -157
- ipulse_shared_core_ftredge/exceptions/base_exceptions.py +35 -4
- ipulse_shared_core_ftredge/models/__init__.py +3 -7
- ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
- ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
- ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +274 -0
- ipulse_shared_core_ftredge/models/catalog/usertype.py +177 -0
- ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
- ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
- ipulse_shared_core_ftredge/models/user/user_subscription.py +348 -0
- ipulse_shared_core_ftredge/models/{user_auth.py → user/userauth.py} +19 -10
- ipulse_shared_core_ftredge/models/{user_profile.py → user/userprofile.py} +53 -21
- ipulse_shared_core_ftredge/models/user/userstatus.py +479 -0
- ipulse_shared_core_ftredge/monitoring/__init__.py +0 -2
- ipulse_shared_core_ftredge/monitoring/tracemon.py +6 -6
- ipulse_shared_core_ftredge/services/__init__.py +11 -13
- ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
- ipulse_shared_core_ftredge/services/base/base_firestore_service.py +77 -16
- ipulse_shared_core_ftredge/services/{cache_aware_firestore_service.py → base/cache_aware_firestore_service.py} +46 -32
- ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +277 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +376 -0
- ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
- ipulse_shared_core_ftredge/services/user/__init__.py +5 -25
- ipulse_shared_core_ftredge/services/user/user_core_service.py +536 -510
- ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +796 -0
- ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
- ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +488 -0
- ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
- ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
- ipulse_shared_core_ftredge/services/user/userstatus_operations.py +476 -0
- ipulse_shared_core_ftredge/services/{charging_service.py → user_charging_service.py} +9 -9
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/METADATA +3 -4
- ipulse_shared_core_ftredge-23.1.1.dist-info/RECORD +50 -0
- ipulse_shared_core_ftredge/models/subscription.py +0 -190
- ipulse_shared_core_ftredge/models/user_status.py +0 -495
- ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -526
- ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
- ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
- ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
- ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
- ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
- ipulse_shared_core_ftredge-20.0.1.dist-info/RECORD +0 -42
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/WHEEL +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/licenses/LICENCE +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.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)
|