ipulse-shared-core-ftredge 20.0.1__tar.gz → 22.1.1__tar.gz
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-20.0.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-22.1.1}/PKG-INFO +3 -4
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/setup.py +3 -4
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/cache/shared_cache.py +1 -2
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +4 -4
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/exceptions/base_exceptions.py +23 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +3 -7
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +273 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/catalog/usertype.py +170 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/models/subscription.py → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/user_subscription.py +66 -20
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/models/user_auth.py → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/userauth.py +19 -10
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/models/user_profile.py → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/userprofile.py +53 -21
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/user/userstatus.py +430 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/monitoring/__init__.py +0 -2
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/monitoring/tracemon.py +6 -6
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/__init__.py +23 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py +73 -14
- {ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/base}/cache_aware_firestore_service.py +46 -32
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +273 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +307 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/__init__.py +17 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/firebase_auth_admin_helpers.py +160 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +559 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +726 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +484 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
- ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user/userstatus_operations.py +212 -0
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/charging_service.py → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/services/user_charging_service.py +9 -9
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +3 -4
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +21 -13
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +1 -2
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/models/user_status.py +0 -495
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -526
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/__init__.py +0 -25
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/__init__.py +0 -37
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +0 -651
- ipulse_shared_core_ftredge-20.0.1/src/ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
- ipulse_shared_core_ftredge-20.0.1/tests/test_cache_aware_service.py +0 -270
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/README.md +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/exceptions/__init__.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/models/base_api_response.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
- {ipulse_shared_core_ftredge-20.0.1 → ipulse_shared_core_ftredge-22.1.1}/tests/test_shared_cache.py +0 -0
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version:
|
|
3
|
+
Version: 22.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
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
11
|
License-File: LICENCE
|
|
12
12
|
Requires-Dist: pydantic[email]~=2.5
|
|
13
13
|
Requires-Dist: python-dateutil~=2.8
|
|
14
14
|
Requires-Dist: fastapi~=0.115.8
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
Requires-Dist: ipulse_shared_base_ftredge==7.2.0
|
|
15
|
+
Requires-Dist: ipulse_shared_base_ftredge==10.2.1
|
|
17
16
|
Dynamic: author
|
|
18
17
|
Dynamic: classifier
|
|
19
18
|
Dynamic: home-page
|
|
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
|
|
3
3
|
|
|
4
4
|
setup(
|
|
5
5
|
name='ipulse_shared_core_ftredge',
|
|
6
|
-
version='
|
|
6
|
+
version='22.1.1',
|
|
7
7
|
package_dir={'': 'src'}, # Specify the source directory
|
|
8
8
|
packages=find_packages(where='src'), # Look for packages in 'src'
|
|
9
9
|
install_requires=[
|
|
@@ -11,8 +11,7 @@ setup(
|
|
|
11
11
|
'pydantic[email]~=2.5',
|
|
12
12
|
'python-dateutil~=2.8',
|
|
13
13
|
'fastapi~=0.115.8',
|
|
14
|
-
'
|
|
15
|
-
'ipulse_shared_base_ftredge==7.2.0',
|
|
14
|
+
'ipulse_shared_base_ftredge==10.2.1',
|
|
16
15
|
],
|
|
17
16
|
author='Russlan Ramdowar',
|
|
18
17
|
description='Shared Core models and Logger util for the Pulse platform project. Using AI for financial advisory and investment management.',
|
|
@@ -23,5 +22,5 @@ setup(
|
|
|
23
22
|
'License :: OSI Approved :: MIT License',
|
|
24
23
|
'Operating System :: OS Independent',
|
|
25
24
|
],
|
|
26
|
-
python_requires='>=3.
|
|
25
|
+
python_requires='>=3.12',
|
|
27
26
|
)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"""Module for shared caching functionality that can be used across microservices."""
|
|
2
|
-
import os
|
|
3
2
|
import time
|
|
4
3
|
import logging
|
|
5
4
|
import traceback
|
|
6
5
|
import inspect
|
|
7
6
|
import asyncio
|
|
8
7
|
import threading
|
|
9
|
-
from typing import Dict, Any, Optional, TypeVar, Generic, Callable, Tuple,
|
|
8
|
+
from typing import Dict, Any, Optional, TypeVar, Generic, Callable, Tuple,Awaitable
|
|
10
9
|
|
|
11
10
|
T = TypeVar('T')
|
|
12
11
|
|
|
@@ -9,7 +9,7 @@ import json
|
|
|
9
9
|
import httpx
|
|
10
10
|
from fastapi import HTTPException, Request
|
|
11
11
|
from google.cloud import firestore
|
|
12
|
-
from ipulse_shared_core_ftredge.
|
|
12
|
+
from ipulse_shared_core_ftredge.exceptions import ServiceError, AuthorizationError, ResourceNotFoundError
|
|
13
13
|
from ipulse_shared_core_ftredge.models import UserStatus
|
|
14
14
|
from ipulse_shared_core_ftredge.utils.json_encoder import convert_to_json_serializable
|
|
15
15
|
|
|
@@ -278,8 +278,8 @@ async def authorizeAPIRequest(
|
|
|
278
278
|
primary_usertype = userstatus.get("primary_usertype")
|
|
279
279
|
secondary_usertypes = userstatus.get("secondary_usertypes", [])
|
|
280
280
|
|
|
281
|
-
# Extract IAM
|
|
282
|
-
|
|
281
|
+
# Extract IAM permissions
|
|
282
|
+
iam_permissions = userstatus.get("iam_permissions", {})
|
|
283
283
|
|
|
284
284
|
# Format the authz_input to match what the OPA policies expect
|
|
285
285
|
authz_input = {
|
|
@@ -290,7 +290,7 @@ async def authorizeAPIRequest(
|
|
|
290
290
|
"secondary_usertypes": secondary_usertypes,
|
|
291
291
|
"usertypes": [primary_usertype] + secondary_usertypes if primary_usertype else secondary_usertypes,
|
|
292
292
|
"email_verified": request.state.user.get("email_verified", False),
|
|
293
|
-
"
|
|
293
|
+
"iam_permissions": iam_permissions,
|
|
294
294
|
"sbscrptn_based_insight_credits": userstatus.get("sbscrptn_based_insight_credits", 0),
|
|
295
295
|
"extra_insight_credits": userstatus.get("extra_insight_credits", 0)
|
|
296
296
|
},
|
|
@@ -133,3 +133,26 @@ class ValidationError(BaseServiceException):
|
|
|
133
133
|
resource_id=resource_id,
|
|
134
134
|
additional_info=additional_info
|
|
135
135
|
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ConfigurationError(BaseServiceException):
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
detail: str,
|
|
142
|
+
resource_type: str = "configuration",
|
|
143
|
+
resource_id: Optional[str] = None,
|
|
144
|
+
operation: Optional[str] = None,
|
|
145
|
+
additional_info: Optional[Dict[str, Any]] = None,
|
|
146
|
+
original_error: Optional[Exception] = None
|
|
147
|
+
):
|
|
148
|
+
if operation:
|
|
149
|
+
detail = f"{detail} (Operation: {operation})"
|
|
150
|
+
|
|
151
|
+
super().__init__(
|
|
152
|
+
status_code=500,
|
|
153
|
+
detail=detail,
|
|
154
|
+
resource_type=resource_type,
|
|
155
|
+
resource_id=resource_id,
|
|
156
|
+
additional_info=additional_info,
|
|
157
|
+
original_error=original_error
|
|
158
|
+
)
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
from .user_profile import UserProfile
|
|
2
|
-
from .subscription import Subscription
|
|
3
|
-
from .user_status import UserStatus, IAMUnitRefAssignment
|
|
4
|
-
from .user_auth import UserAuth
|
|
5
|
-
from .base_api_response import BaseAPIResponse , CustomJSONResponse, CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
|
|
6
1
|
from .base_data_model import BaseDataModel
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
from .base_api_response import BaseAPIResponse , CustomJSONResponse, CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
|
|
3
|
+
from .user import UserProfile, UserSubscription, UserStatus, UserAuth, UserPermission
|
|
4
|
+
from .catalog import SubscriptionPlan, ProrationMethod, PlanUpgradePath, UserType
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
2
|
from typing import Any
|
|
3
3
|
from typing import ClassVar
|
|
4
|
-
from typing import Optional, Dict
|
|
5
4
|
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
|
6
5
|
import dateutil.parser
|
|
7
6
|
|
|
@@ -21,9 +20,9 @@ class BaseDataModel(BaseModel):
|
|
|
21
20
|
frozen=True # Keep schema version frozen for data integrity
|
|
22
21
|
)
|
|
23
22
|
|
|
24
|
-
# Audit fields -
|
|
25
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
26
|
-
created_by: str = Field(
|
|
23
|
+
# Audit fields - created fields are frozen after creation, updated fields are mutable
|
|
24
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), frozen=True)
|
|
25
|
+
created_by: str = Field(..., frozen=True)
|
|
27
26
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
28
27
|
updated_by: str = Field(...)
|
|
29
28
|
|
|
@@ -35,22 +34,21 @@ class BaseDataModel(BaseModel):
|
|
|
35
34
|
@field_validator('created_at', 'updated_at', mode='before')
|
|
36
35
|
@classmethod
|
|
37
36
|
def parse_datetime(cls, v: Any) -> datetime:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
"""
|
|
38
|
+
Ensures that datetime fields are properly parsed into datetime objects.
|
|
39
|
+
Handles both datetime objects (from Firestore) and ISO format strings (from APIs).
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(v, datetime):
|
|
42
|
+
# If it's already a datetime object (including Firestore's DatetimeWithNanoseconds),
|
|
43
|
+
# return it directly.
|
|
44
|
+
return v
|
|
45
|
+
|
|
46
|
+
if isinstance(v, str):
|
|
47
|
+
# If it's a string, parse it into a datetime object.
|
|
41
48
|
try:
|
|
42
49
|
return dateutil.parser.isoparse(v)
|
|
43
50
|
except (TypeError, ValueError) as e:
|
|
44
51
|
raise ValueError(f"Invalid datetime string format: {v} - {e}")
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
# but has isoformat(), perhaps try that, but it's unlikely with current Firestore client.
|
|
49
|
-
# For example, if v is some custom timestamp object from an older library:
|
|
50
|
-
if hasattr(v, 'isoformat'): # Fallback for unknown datetime-like objects
|
|
51
|
-
try:
|
|
52
|
-
return dateutil.parser.isoparse(v.isoformat())
|
|
53
|
-
except Exception as e:
|
|
54
|
-
raise ValueError(f"Could not parse datetime-like object: {v} - {e}")
|
|
55
|
-
|
|
56
|
-
raise ValueError(f"Unsupported type for datetime parsing: {type(v)} value: {v}")
|
|
52
|
+
|
|
53
|
+
# If the type is not a datetime or a string, it's an unsupported format.
|
|
54
|
+
raise ValueError(f"Unsupported type for datetime parsing: {type(v)}")
|
ipulse_shared_core_ftredge-22.1.1/src/ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subscription Plan Defaults Model
|
|
3
|
+
|
|
4
|
+
This module defines the configuration templates for subscription plans that are stored in Firestore.
|
|
5
|
+
These templates are used to create actual user subscriptions with consistent settings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Optional, ClassVar, List
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from pydantic import Field, ConfigDict, field_validator,model_validator, BaseModel
|
|
11
|
+
from ipulse_shared_base_ftredge import (Layer, Module, list_enums_as_lower_strings,
|
|
12
|
+
Subject, SubscriptionPlanName,ObjectOverallStatus,
|
|
13
|
+
SubscriptionStatus, TimeUnit)
|
|
14
|
+
from ..base_data_model import BaseDataModel
|
|
15
|
+
from ..user.user_permissions import UserPermission
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProrationMethod(StrEnum):
|
|
19
|
+
"""Methods for handling proration when upgrading plans."""
|
|
20
|
+
IMMEDIATE = "immediate"
|
|
21
|
+
PRORATED = "prorated"
|
|
22
|
+
END_OF_CYCLE = "end_of_cycle"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Proration(BaseModel):
|
|
26
|
+
"""Defines the proration behavior for subscription changes."""
|
|
27
|
+
pro_rata_billing: bool = Field(
|
|
28
|
+
default=True,
|
|
29
|
+
description="If true, charge a pro-rated amount for the remaining time in the current billing cycle."
|
|
30
|
+
)
|
|
31
|
+
proration_date: Optional[int] = Field(
|
|
32
|
+
default=None,
|
|
33
|
+
description="The specific date to use for proration calculations, if applicable."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PlanUpgradePath(BaseModel):
|
|
38
|
+
"""Represents an upgrade path from a source plan to the plan where this path is defined."""
|
|
39
|
+
|
|
40
|
+
price_usd: float = Field(
|
|
41
|
+
...,
|
|
42
|
+
ge=0,
|
|
43
|
+
description="Price for upgrading to this plan in USD"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
proration_method: ProrationMethod = Field(
|
|
47
|
+
...,
|
|
48
|
+
description="How to handle proration when upgrading"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION IF SCHEMA IS BEING MODIFIED !!! ############################################
|
|
53
|
+
class SubscriptionPlan(BaseDataModel):
|
|
54
|
+
"""
|
|
55
|
+
Configuration template for subscription plans stored in Firestore.
|
|
56
|
+
These templates define the default settings applied when creating user subscriptions.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(extra="forbid")
|
|
60
|
+
|
|
61
|
+
VERSION: ClassVar[float] = 1.0
|
|
62
|
+
DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE, Subject.CATALOG))
|
|
63
|
+
OBJ_REF: ClassVar[str] = "subscriptionplan"
|
|
64
|
+
|
|
65
|
+
# System-managed fields
|
|
66
|
+
schema_version: float = Field(
|
|
67
|
+
default=VERSION,
|
|
68
|
+
description="Version of this Class == version of DB Schema",
|
|
69
|
+
frozen=True
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
id: Optional[str] = Field(
|
|
73
|
+
default=None,
|
|
74
|
+
description="Unique identifier for this plan template (e.g., 'free_subscription_1'). Auto-generated if not provided.",
|
|
75
|
+
frozen=True
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
plan_name: SubscriptionPlanName = Field(
|
|
79
|
+
...,
|
|
80
|
+
description="Subscription plan type (FREE, BASE, PREMIUM)",
|
|
81
|
+
frozen=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
plan_version: int = Field(
|
|
85
|
+
...,
|
|
86
|
+
ge=1,
|
|
87
|
+
description="Version of this plan template",
|
|
88
|
+
frozen=True
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
pulse_status: ObjectOverallStatus = Field(
|
|
92
|
+
default=ObjectOverallStatus.ACTIVE,
|
|
93
|
+
description="Overall status of this subscription plan configuration"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Display information
|
|
97
|
+
display_name: str = Field(
|
|
98
|
+
...,
|
|
99
|
+
min_length=1,
|
|
100
|
+
description="Human-readable plan name",
|
|
101
|
+
frozen=True
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
description: str = Field(
|
|
105
|
+
...,
|
|
106
|
+
min_length=1,
|
|
107
|
+
description="Description of what this plan includes",
|
|
108
|
+
frozen=True
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
granted_iam_permissions: List[UserPermission] = Field(
|
|
112
|
+
default_factory=list,
|
|
113
|
+
description="List of all IAM permission granted by this plan",
|
|
114
|
+
frozen=True
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Credit configuration
|
|
118
|
+
subscription_based_insight_credits_per_update: int = Field(
|
|
119
|
+
...,
|
|
120
|
+
ge=0,
|
|
121
|
+
description="Number of insight credits added per update cycle",
|
|
122
|
+
frozen=True
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
subscription_based_insight_credits_update_freq_h: int = Field(
|
|
126
|
+
...,
|
|
127
|
+
gt=0,
|
|
128
|
+
description="How often insight credits are updated (in hours)",
|
|
129
|
+
frozen=True
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
extra_insight_credits_per_cycle: int = Field(
|
|
133
|
+
...,
|
|
134
|
+
ge=0,
|
|
135
|
+
description="Bonus insight credits granted per subscription cycle",
|
|
136
|
+
frozen=True
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
voting_credits_per_update: int = Field(
|
|
140
|
+
...,
|
|
141
|
+
ge=0,
|
|
142
|
+
description="Number of voting credits added per update cycle",
|
|
143
|
+
frozen=True
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
voting_credits_update_freq_h: int = Field(
|
|
147
|
+
...,
|
|
148
|
+
gt=0,
|
|
149
|
+
description="How often voting credits are updated (in hours)",
|
|
150
|
+
frozen=True
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Plan cycle configuration
|
|
154
|
+
plan_validity_cycle_length: int = Field(
|
|
155
|
+
...,
|
|
156
|
+
gt=0,
|
|
157
|
+
description="Length of each subscription cycle (e.g., 1, 3, 12)",
|
|
158
|
+
frozen=True
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
plan_validity_cycle_unit: TimeUnit = Field(
|
|
162
|
+
...,
|
|
163
|
+
description="Unit for the cycle length (month, year, etc.)",
|
|
164
|
+
frozen=True
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Pricing
|
|
168
|
+
plan_per_cycle_price_usd: float = Field(
|
|
169
|
+
...,
|
|
170
|
+
ge=0,
|
|
171
|
+
description="Price per subscription cycle in USD",
|
|
172
|
+
frozen=True
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Features and customization
|
|
176
|
+
plan_extra_features: Dict[str, Any] = Field(
|
|
177
|
+
default_factory=dict,
|
|
178
|
+
description="Additional features enabled by this plan",
|
|
179
|
+
frozen=True
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Upgrade paths
|
|
183
|
+
plan_upgrade_paths: Dict[str, PlanUpgradePath] = Field(
|
|
184
|
+
default_factory=dict,
|
|
185
|
+
description="Defines valid upgrade paths TO this plan FROM other plans (source_plan_id -> upgrade_details)",
|
|
186
|
+
frozen=True
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Default settings
|
|
190
|
+
plan_default_auto_renewal: bool = Field(
|
|
191
|
+
...,
|
|
192
|
+
description="Default auto-renewal setting for new subscriptions",
|
|
193
|
+
frozen=True
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
plan_default_status: SubscriptionStatus = Field(
|
|
197
|
+
...,
|
|
198
|
+
description="Default status for new subscriptions with this plan",
|
|
199
|
+
frozen=True
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Fallback configuration
|
|
203
|
+
fallback_plan_id_if_current_plan_expired: Optional[str] = Field(
|
|
204
|
+
...,
|
|
205
|
+
description="Plan to fall back to when this plan expires (None for no fallback)",
|
|
206
|
+
frozen=True
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
@model_validator(mode='before')
|
|
210
|
+
@classmethod
|
|
211
|
+
def set_id_if_not_provided(cls, data: Any) -> Any:
|
|
212
|
+
"""Generate an ID from plan_name and plan_version if not provided."""
|
|
213
|
+
if isinstance(data, dict):
|
|
214
|
+
plan_name = data.get('plan_name')
|
|
215
|
+
plan_version = data.get('plan_version')
|
|
216
|
+
provided_id = data.get('id')
|
|
217
|
+
|
|
218
|
+
if plan_name and plan_version is not None:
|
|
219
|
+
plan_name_str = str(plan_name)
|
|
220
|
+
expected_id = f"{plan_name_str}_{plan_version}"
|
|
221
|
+
|
|
222
|
+
if provided_id is None:
|
|
223
|
+
# Auto-generate ID
|
|
224
|
+
data['id'] = expected_id
|
|
225
|
+
else:
|
|
226
|
+
# Validate provided ID matches expected format
|
|
227
|
+
if provided_id != expected_id:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Invalid ID format. Expected '{expected_id}' based on "
|
|
230
|
+
f"plan_name='{plan_name_str}' and plan_version={plan_version}, "
|
|
231
|
+
f"but got '{provided_id}'. ID must follow format: {{plan_name}}_{{plan_version}}"
|
|
232
|
+
)
|
|
233
|
+
return data
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@field_validator('granted_iam_permissions')
|
|
237
|
+
@classmethod
|
|
238
|
+
def validate_iam_permissions(cls, v: List[UserPermission]) -> List[UserPermission]:
|
|
239
|
+
"""Validate IAM permissions structure."""
|
|
240
|
+
if not isinstance(v, list):
|
|
241
|
+
raise ValueError("granted_iam_permissions must be a list")
|
|
242
|
+
|
|
243
|
+
for i, permission in enumerate(v):
|
|
244
|
+
if not isinstance(permission, UserPermission):
|
|
245
|
+
raise ValueError(f"Permission at index {i} must be a UserPermission instance")
|
|
246
|
+
|
|
247
|
+
return v
|
|
248
|
+
|
|
249
|
+
@field_validator('plan_upgrade_paths')
|
|
250
|
+
@classmethod
|
|
251
|
+
def validate_upgrade_paths(cls, v: Dict[str, PlanUpgradePath]) -> Dict[str, PlanUpgradePath]:
|
|
252
|
+
"""Validate upgrade paths."""
|
|
253
|
+
for source_plan_id, upgrade_path in v.items():
|
|
254
|
+
if not isinstance(source_plan_id, str) or not source_plan_id.strip():
|
|
255
|
+
raise ValueError(f"Source plan ID must be a non-empty string, got: {source_plan_id}")
|
|
256
|
+
|
|
257
|
+
if not isinstance(upgrade_path, PlanUpgradePath):
|
|
258
|
+
raise ValueError(f"Upgrade path for '{source_plan_id}' must be a PlanUpgradePath instance")
|
|
259
|
+
|
|
260
|
+
return v
|
|
261
|
+
|
|
262
|
+
def get_cycle_duration_hours(self) -> int:
|
|
263
|
+
"""Calculate the total duration of one cycle in hours."""
|
|
264
|
+
unit_to_hours = {
|
|
265
|
+
TimeUnit.MINUTE: 1/60,
|
|
266
|
+
TimeUnit.HOUR: 1,
|
|
267
|
+
TimeUnit.DAY: 24,
|
|
268
|
+
TimeUnit.WEEK: 24 * 7,
|
|
269
|
+
TimeUnit.MONTH: 24 * 30, # Approximate
|
|
270
|
+
TimeUnit.YEAR: 24 * 365, # Approximate
|
|
271
|
+
}
|
|
272
|
+
multiplier = unit_to_hours.get(self.plan_validity_cycle_unit, 24 * 30)
|
|
273
|
+
return int(self.plan_validity_cycle_length * multiplier)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Defaults Model
|
|
3
|
+
|
|
4
|
+
This module defines the configuration templates for user type defaults that are stored in Firestore.
|
|
5
|
+
These templates are used to create user profiles and statuses with consistent default settings
|
|
6
|
+
based on their user type (superadmin, admin, internal, authenticated, anonymous).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Any, Optional, ClassVar, List
|
|
10
|
+
from pydantic import Field, ConfigDict, field_validator, model_validator
|
|
11
|
+
from ipulse_shared_base_ftredge import Layer, Module, list_enums_as_lower_strings, Subject, ObjectOverallStatus
|
|
12
|
+
from ipulse_shared_base_ftredge.enums.enums_iam import IAMUserType
|
|
13
|
+
from ipulse_shared_core_ftredge.models.base_data_model import BaseDataModel
|
|
14
|
+
from ipulse_shared_core_ftredge.models.user.user_permissions import UserPermission
|
|
15
|
+
|
|
16
|
+
# ORIGINAL AUTHOR ="russlan.ramdowar;russlan@ftredge.com"
|
|
17
|
+
# CLASS_ORIGIN_DATE="2025-06-27"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
############################################ !!!!! ALWAYS UPDATE SCHEMA VERSION IF SCHEMA IS BEING MODIFIED !!! ############################################
|
|
21
|
+
class UserType(BaseDataModel):
|
|
22
|
+
"""
|
|
23
|
+
Configuration template for user type defaults stored in Firestore.
|
|
24
|
+
These templates define the default settings applied when creating users of specific types.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(extra="forbid")
|
|
28
|
+
|
|
29
|
+
VERSION: ClassVar[float] = 1.0
|
|
30
|
+
DOMAIN: ClassVar[str] = "_".join(list_enums_as_lower_strings(Layer.PULSE_APP, Module.CORE.name, Subject.CATALOG.name))
|
|
31
|
+
OBJ_REF: ClassVar[str] = "usertype"
|
|
32
|
+
|
|
33
|
+
# System-managed fields
|
|
34
|
+
schema_version: float = Field(
|
|
35
|
+
default=VERSION,
|
|
36
|
+
description="Version of this Class == version of DB Schema",
|
|
37
|
+
frozen=True
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
id: Optional[str] = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
description="Unique identifier for this user type template (e.g., 'superadmin_1', 'authenticated_1'). Auto-generated if not provided.",
|
|
43
|
+
frozen=True
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
version: int = Field(
|
|
47
|
+
...,
|
|
48
|
+
ge=1,
|
|
49
|
+
description="Version of this user type template",
|
|
50
|
+
frozen=True
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
pulse_status: ObjectOverallStatus = Field(
|
|
54
|
+
default=ObjectOverallStatus.ACTIVE,
|
|
55
|
+
description="Overall status of this user type configuration"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# User type configuration
|
|
59
|
+
primary_usertype: IAMUserType = Field(
|
|
60
|
+
...,
|
|
61
|
+
description="Primary user type for this configuration template",
|
|
62
|
+
frozen=True
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
secondary_usertypes: List[IAMUserType] = Field(
|
|
66
|
+
default_factory=list,
|
|
67
|
+
description="Secondary user types automatically assigned to users of this primary type",
|
|
68
|
+
frozen=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Organization defaults
|
|
72
|
+
default_organizations: List[str] = Field(
|
|
73
|
+
default_factory=list,
|
|
74
|
+
description="Default organization UIDs for users of this type",
|
|
75
|
+
frozen=True
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# IAM permissions structure - simplified flattened list
|
|
79
|
+
granted_iam_permissions: List[UserPermission] = Field(
|
|
80
|
+
default_factory=list,
|
|
81
|
+
description="Default IAM permissions granted to users of this type.",
|
|
82
|
+
frozen=True
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
default_extra_insight_credits: int = Field(
|
|
86
|
+
default=0,
|
|
87
|
+
ge=0,
|
|
88
|
+
description="Default extra insight credits for users of this type",
|
|
89
|
+
frozen=True
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
default_voting_credits: int = Field(
|
|
93
|
+
default=0,
|
|
94
|
+
ge=0,
|
|
95
|
+
description="Default voting credits for users of this type",
|
|
96
|
+
frozen=True
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Subscription defaults
|
|
100
|
+
default_subscription_plan_if_unpaid: Optional[str] = Field(
|
|
101
|
+
default=None,
|
|
102
|
+
description="Default subscription plan ID to assign if user has no active subscription",
|
|
103
|
+
frozen=True
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Additional metadata
|
|
107
|
+
metadata: Dict[str, Any] = Field(
|
|
108
|
+
default_factory=dict,
|
|
109
|
+
description="Additional metadata for this user type configuration",
|
|
110
|
+
frozen=True
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@model_validator(mode='before')
|
|
114
|
+
@classmethod
|
|
115
|
+
def set_id_if_not_provided(cls, data: Any) -> Any:
|
|
116
|
+
"""Generate an ID from primary_usertype and version if not provided."""
|
|
117
|
+
if isinstance(data, dict):
|
|
118
|
+
primary_usertype = data.get('primary_usertype')
|
|
119
|
+
version = data.get('version')
|
|
120
|
+
provided_id = data.get('id')
|
|
121
|
+
|
|
122
|
+
if primary_usertype and version is not None:
|
|
123
|
+
primary_usertype_str = str(primary_usertype)
|
|
124
|
+
expected_id = f"{primary_usertype_str}_{version}"
|
|
125
|
+
|
|
126
|
+
if provided_id is None:
|
|
127
|
+
# Auto-generate ID
|
|
128
|
+
data['id'] = expected_id
|
|
129
|
+
else:
|
|
130
|
+
# Validate provided ID matches expected format
|
|
131
|
+
if provided_id != expected_id:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Invalid ID format. Expected '{expected_id}' based on "
|
|
134
|
+
f"primary_usertype='{primary_usertype_str}' and version={version}, "
|
|
135
|
+
f"but got '{provided_id}'. ID must follow format: {{primary_usertype}}_{{version}}"
|
|
136
|
+
)
|
|
137
|
+
return data
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def usertype_id(self) -> str:
|
|
141
|
+
"""Get the ID as a non-optional string. ID is always set after validation."""
|
|
142
|
+
if self.id is None:
|
|
143
|
+
raise ValueError("UserType ID is not set - this should not happen after model validation")
|
|
144
|
+
return self.id
|
|
145
|
+
|
|
146
|
+
@field_validator('granted_iam_permissions')
|
|
147
|
+
@classmethod
|
|
148
|
+
def validate_iam_permissions(cls, v: List[UserPermission]) -> List[UserPermission]:
|
|
149
|
+
"""Validate IAM permissions structure."""
|
|
150
|
+
if not isinstance(v, list):
|
|
151
|
+
raise ValueError("granted_iam_permissions must be a list")
|
|
152
|
+
|
|
153
|
+
for i, permission in enumerate(v):
|
|
154
|
+
if not isinstance(permission, UserPermission):
|
|
155
|
+
raise ValueError(f"Permission at index {i} must be a UserPermission instance")
|
|
156
|
+
|
|
157
|
+
return v
|
|
158
|
+
|
|
159
|
+
@field_validator('secondary_usertypes')
|
|
160
|
+
@classmethod
|
|
161
|
+
def validate_secondary_usertypes(cls, v: List[IAMUserType]) -> List[IAMUserType]:
|
|
162
|
+
"""Validate secondary user types list."""
|
|
163
|
+
# Remove duplicates while preserving order
|
|
164
|
+
seen = set()
|
|
165
|
+
unique_list = []
|
|
166
|
+
for user_type in v:
|
|
167
|
+
if user_type not in seen:
|
|
168
|
+
seen.add(user_type)
|
|
169
|
+
unique_list.append(user_type)
|
|
170
|
+
return unique_list
|