ipulse-shared-core-ftredge 24.2.1__tar.gz → 26.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.
- {ipulse_shared_core_ftredge-24.2.1/src/ipulse_shared_core_ftredge.egg-info → ipulse_shared_core_ftredge-26.1.1}/PKG-INFO +1 -1
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/setup.py +1 -1
- ipulse_shared_core_ftredge-26.1.1/src/ipulse_shared_core_ftredge/dependencies/__init__.py +3 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +0 -2
- ipulse_shared_core_ftredge-26.1.1/src/ipulse_shared_core_ftredge/dependencies/authz_credit_extraction.py +67 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +6 -6
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/__init__.py +3 -1
- ipulse_shared_core_ftredge-26.1.1/src/ipulse_shared_core_ftredge/models/base_api_response.py +29 -0
- ipulse_shared_core_ftredge-26.1.1/src/ipulse_shared_core_ftredge/models/credit_api_response.py +26 -0
- ipulse_shared_core_ftredge-24.2.1/src/ipulse_shared_core_ftredge/models/base_api_response.py → ipulse_shared_core_ftredge-26.1.1/src/ipulse_shared_core_ftredge/models/custom_json_response.py +4 -38
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/user/user_subscription.py +7 -7
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/user/userstatus.py +1 -1
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/charging_processors.py +15 -15
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/user/__init__.py +1 -0
- ipulse_shared_core_ftredge-26.1.1/src/ipulse_shared_core_ftredge/services/user/user_charging_operations.py +721 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/user/user_core_service.py +123 -20
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +42 -52
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +1 -1
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/user/userstatus_operations.py +1 -1
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/user_charging_service.py +19 -19
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1/src/ipulse_shared_core_ftredge.egg-info}/PKG-INFO +1 -1
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge.egg-info/SOURCES.txt +4 -0
- ipulse_shared_core_ftredge-24.2.1/src/ipulse_shared_core_ftredge/dependencies/__init__.py +0 -1
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/LICENCE +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/README.md +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/pyproject.toml +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/setup.cfg +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/cache/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/cache/shared_cache.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/dependencies/auth_protected_router.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/dependencies/firestore_client.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/exceptions/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/exceptions/base_exceptions.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/exceptions/user_exceptions.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/base_data_model.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/catalog/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/catalog/usertype.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/user/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/user/user_permissions.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/user/userauth.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/models/user/userprofile.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/monitoring/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/monitoring/tracemon.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/base/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/base/base_firestore_service.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/base/cache_aware_firestore_service.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/catalog/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/user/userauth_operations.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/services/user/userprofile_operations.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/utils/__init__.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/utils/custom_json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge/utils/json_encoder.py +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge.egg-info/dependency_links.txt +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge.egg-info/requires.txt +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/src/ipulse_shared_core_ftredge.egg-info/top_level.txt +0 -0
- {ipulse_shared_core_ftredge-24.2.1 → ipulse_shared_core_ftredge-26.1.1}/tests/test_shared_cache.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipulse_shared_core_ftredge
|
|
3
|
-
Version:
|
|
3
|
+
Version: 26.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
|
|
@@ -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='26.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=[
|
|
@@ -67,7 +67,6 @@ async def verify_firebase_token(
|
|
|
67
67
|
email = decoded_token.get('email')
|
|
68
68
|
if not email:
|
|
69
69
|
raise ValueError("Token must contain user email")
|
|
70
|
-
|
|
71
70
|
user_auth = UserAuth(
|
|
72
71
|
email=email,
|
|
73
72
|
password=None, # No password for token-based auth
|
|
@@ -93,4 +92,3 @@ async def verify_firebase_token(
|
|
|
93
92
|
|
|
94
93
|
# Type alias for dependency injection
|
|
95
94
|
AuthUser = Annotated[UserAuth, Depends(verify_firebase_token)]
|
|
96
|
-
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for extracting credit information from authorization responses.
|
|
3
|
+
|
|
4
|
+
This module provides helper functions to extract credit information from OPA
|
|
5
|
+
authorization decisions, avoiding the need for complex dependency injection.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_credits_from_authz_response(authz_info: Any) -> Optional[Dict[str, float]]:
|
|
14
|
+
"""
|
|
15
|
+
Extract credit information from OPA authorization decision.
|
|
16
|
+
|
|
17
|
+
This function replaces the complex PreFetchedCredits dependency by directly
|
|
18
|
+
extracting credit information from the authorization response.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
authz_info: Authorization information from OPA decision (typically AuthorizedRequest)
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Dictionary with credit information or None if not available:
|
|
25
|
+
{
|
|
26
|
+
"sbscrptn_based_insight_credits": float,
|
|
27
|
+
"extra_insight_credits": float
|
|
28
|
+
}
|
|
29
|
+
"""
|
|
30
|
+
if not authz_info:
|
|
31
|
+
logger.debug("No authorization info provided for credit extraction")
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# Handle dict-like authorization info (AuthorizedRequest typically converts to dict)
|
|
36
|
+
if isinstance(authz_info, dict) and 'opa_decision' in authz_info:
|
|
37
|
+
requestor_info = authz_info['opa_decision'].get("requestor_post_authz", {})
|
|
38
|
+
if requestor_info:
|
|
39
|
+
extracted_credits = {
|
|
40
|
+
"sbscrptn_based_insight_credits": float(requestor_info.get("sbscrptn_based_insight_credits", 0.0)),
|
|
41
|
+
"extra_insight_credits": float(requestor_info.get("extra_insight_credits", 0.0))
|
|
42
|
+
}
|
|
43
|
+
logger.debug("Successfully extracted credits from authz response: %s", extracted_credits)
|
|
44
|
+
return extracted_credits
|
|
45
|
+
else:
|
|
46
|
+
logger.debug("No requestor_post_authz found in OPA decision")
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
# Handle object-like authorization info with attributes
|
|
50
|
+
elif hasattr(authz_info, 'opa_decision'):
|
|
51
|
+
opa_decision = getattr(authz_info, 'opa_decision', {})
|
|
52
|
+
if isinstance(opa_decision, dict):
|
|
53
|
+
requestor_info = opa_decision.get("requestor_post_authz", {})
|
|
54
|
+
if requestor_info:
|
|
55
|
+
extracted_credits = {
|
|
56
|
+
"sbscrptn_based_insight_credits": float(requestor_info.get("sbscrptn_based_insight_credits", 0.0)),
|
|
57
|
+
"extra_insight_credits": float(requestor_info.get("extra_insight_credits", 0.0))
|
|
58
|
+
}
|
|
59
|
+
logger.debug("Successfully extracted credits from authz response (object): %s", extracted_credits)
|
|
60
|
+
return extracted_credits
|
|
61
|
+
|
|
62
|
+
logger.debug("No valid OPA decision structure found in authz_info for credit extraction")
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
except (AttributeError, KeyError, TypeError, ValueError) as e:
|
|
66
|
+
logger.warning("Failed to extract credits from authz response: %s", e)
|
|
67
|
+
return None
|
|
@@ -194,6 +194,7 @@ async def authorizeAPIRequest(
|
|
|
194
194
|
|
|
195
195
|
# Extract request context and Firebase user claims
|
|
196
196
|
firebase_user = request.state.user
|
|
197
|
+
logger.debug(f"Firebase user: {firebase_user}")
|
|
197
198
|
user_uid = firebase_user.get('uid')
|
|
198
199
|
if not user_uid:
|
|
199
200
|
logger.debug(f"Authorization denied for {request.method} {request.url.path}: No user UID found")
|
|
@@ -203,10 +204,9 @@ async def authorizeAPIRequest(
|
|
|
203
204
|
)
|
|
204
205
|
|
|
205
206
|
# Get usertype information from Firebase custom claims (primary source)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
user_approval_status = custom_claims.get('user_approval_status', str(ApprovalStatus.UNKNOWN))
|
|
207
|
+
primary_usertype = firebase_user.get('primary_usertype')
|
|
208
|
+
secondary_usertypes = firebase_user.get('secondary_usertypes', [])
|
|
209
|
+
user_approval_status = firebase_user.get('user_approval_status', str(ApprovalStatus.UNKNOWN))
|
|
210
210
|
|
|
211
211
|
# Determine if we need fresh status for permissions and credits
|
|
212
212
|
force_fresh = _should_force_fresh_status(request)
|
|
@@ -217,6 +217,7 @@ async def authorizeAPIRequest(
|
|
|
217
217
|
)
|
|
218
218
|
|
|
219
219
|
# Perform comprehensive review and cleanup synchronously to ensure accurate auth data
|
|
220
|
+
logger.debug(f"Comprehensive review for userstatus : {user_status_obj} during authz")
|
|
220
221
|
if user_core_service:
|
|
221
222
|
try:
|
|
222
223
|
review_result = await user_core_service.review_and_clean_active_subscription_credits_and_permissions(
|
|
@@ -227,7 +228,7 @@ async def authorizeAPIRequest(
|
|
|
227
228
|
clean_expired_permissions=True,
|
|
228
229
|
review_credits=True
|
|
229
230
|
)
|
|
230
|
-
|
|
231
|
+
logger.debug(f"Review result for userstatus : {review_result} during authz")
|
|
231
232
|
# Refresh user status after comprehensive review if any actions were taken
|
|
232
233
|
if review_result.get('actions_taken'):
|
|
233
234
|
logger.info(f"Auth middleware performed comprehensive review for user {user_uid}: {review_result['actions_taken']}")
|
|
@@ -268,7 +269,6 @@ async def authorizeAPIRequest(
|
|
|
268
269
|
"uid": user_uid,
|
|
269
270
|
"primary_usertype": primary_usertype,
|
|
270
271
|
"secondary_usertypes": secondary_usertypes,
|
|
271
|
-
"usertypes": [primary_usertype] + secondary_usertypes if primary_usertype else secondary_usertypes,
|
|
272
272
|
"email_verified": firebase_user.get("email_verified", False),
|
|
273
273
|
"user_approval_status": user_approval_status,
|
|
274
274
|
"iam_permissions": valid_permissions,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from .base_data_model import BaseDataModel
|
|
2
|
-
from .base_api_response import BaseAPIResponse
|
|
2
|
+
from .base_api_response import BaseAPIResponse, PaginatedAPIResponse
|
|
3
|
+
from .credit_api_response import CreditChargeableAPIResponse, UserCreditBalance, UpdatedUserCreditInfo
|
|
4
|
+
from .custom_json_response import CustomJSONResponse
|
|
3
5
|
from .user import UserProfile, UserSubscription, UserStatus, UserAuth, UserAuthCreateNew, UserPermission
|
|
4
6
|
from .catalog import SubscriptionPlan, ProrationMethod, PlanUpgradePath, UserType
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Core API response models."""
|
|
2
|
+
from typing import Generic, TypeVar, Optional, Any, Dict, List
|
|
3
|
+
import datetime as dt
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
T = TypeVar('T')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseAPIResponse(BaseModel, Generic[T]):
|
|
11
|
+
"""Base API response model for all endpoints."""
|
|
12
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
13
|
+
success: bool
|
|
14
|
+
chargeable: bool = False # Added chargeable attribute
|
|
15
|
+
data: Optional[T] = None
|
|
16
|
+
message: Optional[str] = None
|
|
17
|
+
error: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
metadata: Dict[str, Any] = {
|
|
20
|
+
"timestamp": dt.datetime.now(dt.timezone.utc).isoformat()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
|
|
25
|
+
"""API response for paginated data."""
|
|
26
|
+
total_count: int
|
|
27
|
+
page: int
|
|
28
|
+
page_size: int
|
|
29
|
+
items: List[T]
|
ipulse_shared_core_ftredge-26.1.1/src/ipulse_shared_core_ftredge/models/credit_api_response.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Credit-related API response models."""
|
|
2
|
+
from typing import Generic, TypeVar, Optional
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from .base_api_response import BaseAPIResponse
|
|
5
|
+
|
|
6
|
+
T = TypeVar('T')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserCreditBalance(BaseModel):
|
|
10
|
+
"""User's current credit balance."""
|
|
11
|
+
sbscrptn_based_insight_credits: float
|
|
12
|
+
extra_insight_credits: float
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UpdatedUserCreditInfo(BaseModel):
|
|
16
|
+
"""Information about credit charging attempt and results."""
|
|
17
|
+
charge_attempted: bool
|
|
18
|
+
charge_successful: bool
|
|
19
|
+
cost_incurred: float
|
|
20
|
+
items_processed_for_charge: int
|
|
21
|
+
user_balance: UserCreditBalance
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CreditChargeableAPIResponse(BaseAPIResponse[T], Generic[T]):
|
|
25
|
+
"""API response for endpoints that may charge credits."""
|
|
26
|
+
updated_user_credit_info: Optional[UpdatedUserCreditInfo] = None
|
|
@@ -1,46 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import datetime as dt
|
|
1
|
+
"""Custom JSON response handlers for FastAPI."""
|
|
3
2
|
import json
|
|
4
|
-
from pydantic import BaseModel, ConfigDict
|
|
5
3
|
from fastapi.responses import JSONResponse
|
|
6
4
|
from ipulse_shared_core_ftredge.utils.json_encoder import EnsureJSONEncoderCompatibility, convert_to_json_serializable
|
|
7
5
|
|
|
8
6
|
|
|
9
|
-
T = TypeVar('T')
|
|
10
|
-
|
|
11
|
-
class BaseAPIResponse(BaseModel, Generic[T]):
|
|
12
|
-
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
13
|
-
success: bool
|
|
14
|
-
chargeable: bool = False # Added chargeable attribute
|
|
15
|
-
data: Optional[T] = None
|
|
16
|
-
message: Optional[str] = None
|
|
17
|
-
error: Optional[str] = None
|
|
18
|
-
|
|
19
|
-
metadata: Dict[str, Any] = {
|
|
20
|
-
"timestamp": dt.datetime.now(dt.timezone.utc).isoformat()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
class UserCreditBalance(BaseModel):
|
|
24
|
-
sbscrptn_based_insight_credits: float
|
|
25
|
-
extra_insight_credits: float
|
|
26
|
-
|
|
27
|
-
class UpdatedUserCreditInfo(BaseModel):
|
|
28
|
-
charge_attempted: bool
|
|
29
|
-
charge_successful: bool
|
|
30
|
-
cost_incurred: float
|
|
31
|
-
items_processed_for_charge: int
|
|
32
|
-
user_balance: UserCreditBalance
|
|
33
|
-
|
|
34
|
-
class CreditChargeableAPIResponse(BaseAPIResponse[T], Generic[T]):
|
|
35
|
-
updated_user_credit_info: Optional[UpdatedUserCreditInfo] = None
|
|
36
|
-
|
|
37
|
-
class PaginatedAPIResponse(BaseAPIResponse, Generic[T]):
|
|
38
|
-
total_count: int
|
|
39
|
-
page: int
|
|
40
|
-
page_size: int
|
|
41
|
-
items: List[T]
|
|
42
|
-
|
|
43
7
|
class CustomJSONResponse(JSONResponse):
|
|
8
|
+
"""Custom JSON response with enhanced serialization support."""
|
|
9
|
+
|
|
44
10
|
def render(self, content) -> bytes:
|
|
45
11
|
# First preprocess content with our utility function
|
|
46
12
|
if isinstance(content, dict) and "data" in content and hasattr(content["data"], "model_dump"):
|
|
@@ -63,4 +29,4 @@ class CustomJSONResponse(JSONResponse):
|
|
|
63
29
|
indent=None,
|
|
64
30
|
separators=(",", ":"),
|
|
65
31
|
cls=EnsureJSONEncoderCompatibility
|
|
66
|
-
).encode("utf-8")
|
|
32
|
+
).encode("utf-8")
|
|
@@ -56,7 +56,7 @@ class UserSubscription(BaseDataModel):
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
# Cycle duration fields
|
|
59
|
-
|
|
59
|
+
cycle_start_datetime: datetime = Field(
|
|
60
60
|
..., # Required field, no default
|
|
61
61
|
description="Subscription Cycle Start Date"
|
|
62
62
|
)
|
|
@@ -163,7 +163,7 @@ class UserSubscription(BaseDataModel):
|
|
|
163
163
|
@classmethod
|
|
164
164
|
def auto_calculate_cycle_end_date(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
165
165
|
"""
|
|
166
|
-
Auto-calculate cycle_end_datetime if not provided, based on
|
|
166
|
+
Auto-calculate cycle_end_datetime if not provided, based on cycle_start_datetime,
|
|
167
167
|
validity_time_length, and validity_time_unit.
|
|
168
168
|
"""
|
|
169
169
|
if not isinstance(data, dict):
|
|
@@ -176,19 +176,19 @@ class UserSubscription(BaseDataModel):
|
|
|
176
176
|
(isinstance(data.get('cycle_end_datetime'), datetime) and
|
|
177
177
|
abs((data['cycle_end_datetime'] - datetime.now(timezone.utc)).total_seconds()) < 5)):
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
cycle_start_datetime = data.get('cycle_start_datetime')
|
|
180
180
|
validity_time_length = data.get('validity_time_length')
|
|
181
181
|
validity_time_unit = data.get('validity_time_unit')
|
|
182
182
|
|
|
183
|
-
if
|
|
183
|
+
if cycle_start_datetime and validity_time_length and validity_time_unit:
|
|
184
184
|
data['cycle_end_datetime'] = cls.calculate_cycle_end_date(
|
|
185
|
-
|
|
185
|
+
cycle_start_datetime, validity_time_length, validity_time_unit
|
|
186
186
|
)
|
|
187
187
|
else:
|
|
188
188
|
raise ValueError(
|
|
189
189
|
"Cannot create subscription without cycle_end_datetime. "
|
|
190
190
|
"Either provide cycle_end_datetime directly or provide "
|
|
191
|
-
"
|
|
191
|
+
"cycle_start_datetime, validity_time_length, and validity_time_unit for auto-calculation."
|
|
192
192
|
)
|
|
193
193
|
|
|
194
194
|
return data
|
|
@@ -242,7 +242,7 @@ class UserSubscription(BaseDataModel):
|
|
|
242
242
|
now = datetime.now(timezone.utc)
|
|
243
243
|
return (
|
|
244
244
|
self.status == SubscriptionStatus.ACTIVE and
|
|
245
|
-
self.
|
|
245
|
+
self.cycle_start_datetime <= now <= self.cycle_end_datetime_safe
|
|
246
246
|
)
|
|
247
247
|
|
|
248
248
|
def is_expired(self) -> bool:
|
|
@@ -442,7 +442,7 @@ class UserStatus(BaseDataModel):
|
|
|
442
442
|
return False
|
|
443
443
|
|
|
444
444
|
now = datetime.now(timezone.utc)
|
|
445
|
-
cycle_start = self.active_subscription.
|
|
445
|
+
cycle_start = self.active_subscription.cycle_start_datetime
|
|
446
446
|
update_frequency_hours = self.active_subscription.subscription_based_insight_credits_update_freq_h
|
|
447
447
|
|
|
448
448
|
# Calculate when the next credit update should happen based on cycle start
|
|
@@ -56,7 +56,7 @@ class ChargingProcessor:
|
|
|
56
56
|
updated_user_credits = pre_fetched_credits
|
|
57
57
|
elif self.user_charging_service: # Attempt to get current credits if not pre-fetched
|
|
58
58
|
try:
|
|
59
|
-
_, current_user_credits_from_verify = await self.user_charging_service.
|
|
59
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
|
|
60
60
|
updated_user_credits = current_user_credits_from_verify
|
|
61
61
|
except Exception: # pylint: disable=broad-except
|
|
62
62
|
self.logger.warning(f"Could not fetch current credits for user {user_uid} for free item.")
|
|
@@ -77,7 +77,7 @@ class ChargingProcessor:
|
|
|
77
77
|
updated_user_credits = pre_fetched_credits
|
|
78
78
|
elif self.user_charging_service:
|
|
79
79
|
try:
|
|
80
|
-
_, current_user_credits_from_verify = await self.user_charging_service.
|
|
80
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
|
|
81
81
|
updated_user_credits = current_user_credits_from_verify
|
|
82
82
|
except Exception: # pylint: disable=broad-except
|
|
83
83
|
self.logger.warning(f"Could not fetch current credits for user {user_uid} during debug bypass.")
|
|
@@ -91,7 +91,7 @@ class ChargingProcessor:
|
|
|
91
91
|
|
|
92
92
|
# Verify credit service is available
|
|
93
93
|
if not self.user_charging_service:
|
|
94
|
-
self.logger.error("
|
|
94
|
+
self.logger.error("UserChargingService not initialized.")
|
|
95
95
|
return {
|
|
96
96
|
'access_granted': False,
|
|
97
97
|
'charge_successful': False,
|
|
@@ -101,7 +101,7 @@ class ChargingProcessor:
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
# Verify user has enough credits
|
|
104
|
-
has_credits, current_user_credits_from_verify = await self.user_charging_service.
|
|
104
|
+
has_credits, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(
|
|
105
105
|
user_uid,
|
|
106
106
|
credit_cost,
|
|
107
107
|
pre_fetched_user_credits=pre_fetched_credits
|
|
@@ -121,7 +121,7 @@ class ChargingProcessor:
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
# Charge the user - this now returns (bool, Optional[Dict])
|
|
124
|
-
charged, calculated_updated_credits = await self.user_charging_service.
|
|
124
|
+
charged, calculated_updated_credits = await self.user_charging_service.debit_credits_transaction(
|
|
125
125
|
user_uid,
|
|
126
126
|
credit_cost,
|
|
127
127
|
operation_description
|
|
@@ -133,7 +133,7 @@ class ChargingProcessor:
|
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
return {
|
|
136
|
-
'access_granted': True, # Access granted because
|
|
136
|
+
'access_granted': True, # Access granted because verify_enough_credits passed
|
|
137
137
|
'charge_successful': charged,
|
|
138
138
|
'cost': credit_cost,
|
|
139
139
|
'reason': 'charged' if charged else 'charge_failed',
|
|
@@ -145,7 +145,7 @@ class ChargingProcessor:
|
|
|
145
145
|
# Try to get current credits to return
|
|
146
146
|
if self.user_charging_service:
|
|
147
147
|
try:
|
|
148
|
-
_, updated_user_credits = await self.user_charging_service.
|
|
148
|
+
_, updated_user_credits = await self.user_charging_service.verify_enough_credits(user_uid, 0, pre_fetched_credits)
|
|
149
149
|
except Exception: # pylint: disable=broad-except
|
|
150
150
|
pass # Keep updated_user_credits as None
|
|
151
151
|
ve.additional_info = ve.additional_info or {}
|
|
@@ -157,7 +157,7 @@ class ChargingProcessor:
|
|
|
157
157
|
current_user_credits_on_error = None
|
|
158
158
|
if self.user_charging_service:
|
|
159
159
|
try:
|
|
160
|
-
_, current_user_credits_on_error = await self.user_charging_service.
|
|
160
|
+
_, current_user_credits_on_error = await self.user_charging_service.verify_enough_credits(user_uid, 0, pre_fetched_credits)
|
|
161
161
|
except Exception: # pylint: disable=broad-except
|
|
162
162
|
pass
|
|
163
163
|
return {
|
|
@@ -230,7 +230,7 @@ class ChargingProcessor:
|
|
|
230
230
|
updated_user_credits = pre_fetched_credits
|
|
231
231
|
elif self.user_charging_service:
|
|
232
232
|
try:
|
|
233
|
-
_, current_user_credits_from_verify = await self.user_charging_service.
|
|
233
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
|
|
234
234
|
updated_user_credits = current_user_credits_from_verify
|
|
235
235
|
|
|
236
236
|
except Exception: # pylint: disable=broad-except
|
|
@@ -252,7 +252,7 @@ class ChargingProcessor:
|
|
|
252
252
|
updated_user_credits = pre_fetched_credits
|
|
253
253
|
elif self.user_charging_service:
|
|
254
254
|
try:
|
|
255
|
-
_, current_user_credits_from_verify = await self.user_charging_service.
|
|
255
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, None)
|
|
256
256
|
updated_user_credits = current_user_credits_from_verify
|
|
257
257
|
except Exception: # pylint: disable=broad-except
|
|
258
258
|
self.logger.warning(f"Could not fetch current credits for user {user_uid} during debug bypass for batch.")
|
|
@@ -268,7 +268,7 @@ class ChargingProcessor:
|
|
|
268
268
|
|
|
269
269
|
# Verify credit service is available
|
|
270
270
|
if not self.user_charging_service:
|
|
271
|
-
self.logger.error("
|
|
271
|
+
self.logger.error("UserChargingService not initialized for batch processing.")
|
|
272
272
|
return {
|
|
273
273
|
'accessible_items': free_items,
|
|
274
274
|
'charge_successful': False,
|
|
@@ -280,7 +280,7 @@ class ChargingProcessor:
|
|
|
280
280
|
|
|
281
281
|
try:
|
|
282
282
|
# Verify user has enough credits for total cost
|
|
283
|
-
has_credits, current_user_credits_from_verify = await self.user_charging_service.
|
|
283
|
+
has_credits, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(
|
|
284
284
|
user_uid,
|
|
285
285
|
total_cost,
|
|
286
286
|
pre_fetched_user_credits=pre_fetched_credits
|
|
@@ -300,7 +300,7 @@ class ChargingProcessor:
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
# Charge the user for all paid items
|
|
303
|
-
charged, calculated_updated_credits = await self.user_charging_service.
|
|
303
|
+
charged, calculated_updated_credits = await self.user_charging_service.debit_credits_transaction(
|
|
304
304
|
user_uid,
|
|
305
305
|
total_cost,
|
|
306
306
|
f"{operation_description} ({len(paid_items)} items, total cost: {total_cost})"
|
|
@@ -324,7 +324,7 @@ class ChargingProcessor:
|
|
|
324
324
|
self.logger.error(f"Validation error during batch credit check for user {user_uid}: {str(ve)}")
|
|
325
325
|
if self.user_charging_service:
|
|
326
326
|
try:
|
|
327
|
-
_, current_user_credits_from_verify = await self.user_charging_service.
|
|
327
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_enough_credits(user_uid, 0, pre_fetched_credits)
|
|
328
328
|
updated_user_credits = current_user_credits_from_verify
|
|
329
329
|
except Exception: # pylint: disable=broad-except
|
|
330
330
|
pass
|
|
@@ -336,7 +336,7 @@ class ChargingProcessor:
|
|
|
336
336
|
current_credits_on_error = None
|
|
337
337
|
if self.user_charging_service:
|
|
338
338
|
try:
|
|
339
|
-
_, current_credits_on_error = await self.user_charging_service.
|
|
339
|
+
_, current_credits_on_error = await self.user_charging_service.verify_enough_credits(user_uid, 0, pre_fetched_credits)
|
|
340
340
|
updated_user_credits = current_credits_on_error
|
|
341
341
|
except Exception: # pylint: disable=broad-except
|
|
342
342
|
pass
|
|
@@ -14,4 +14,5 @@ from .user_subscription_operations import UsersubscriptionOperations
|
|
|
14
14
|
from .user_permissions_operations import UserpermissionsOperations
|
|
15
15
|
from .userauth_operations import UserauthOperations
|
|
16
16
|
from .user_multistep_operations import UsermultistepOperations
|
|
17
|
+
from .user_charging_operations import UserChargingOperations
|
|
17
18
|
from .user_core_service import UserCoreService
|